二、黑箱:深层神经网络的不可解释性
首先从结构上来看,多层神经网络比单层神经网络多出了“中间层”。中间层常常被称为隐藏层(hidden layer),理论上来说可以有无限层,所以在图像表示中经常被省略。层数越多,神经网络的模型复杂度越高,一般也认为更深的神经网络可以解决更加复杂的问题。在学习中,通常我们最多只会设置3~5个隐藏层,但在实际工业场景中会更多。还记得这张图吗?当数据量够大时,现代神经网络层数越深,效果越好。
在一个神经网络中,更靠近输入层的层级相对于其他层级叫做"上层",更靠近输出层的则相对于其他层级叫做"下层"。若从输入层开始从左向右编号,则输入层为第0层,输出层为最后一层。除了输入层以外,每个神经元中都存在着对数据进行处理的数个函数。在我们的例子异或门(XOR)中,隐藏层中的函数是NAND函数和OR函数(也就是线性回归的加和函数+阶跃函数),输出层上的函数是AND函数。对于所有神经元和所有层而言,加和函数的部分都是一致的(都得到结果
z
z
z),因此我们需要关注的是加和之外的那部分函数。在隐藏层中这个函数被称为激活函数,符号为
h
(
z
)
h(z)
h(z),在输出层中这个函数只是普通的连接函数,我们定义为是
g
(
z
)
g(z)
g(z)。我们的数据被逐层传递,每个下层的神经元都必须处理上层的神经元中的
h
(
z
)
h(z)
h(z)处理完毕的数据 ,整个流程本质是一个嵌套计算结果的过程。
在神经网络中,任意层上都有至少一个神经元,最上面的是常量神经元,连接常量神经元的箭头上的参数是截距
b
b
b,剩余的是特征神经元,连接这些神经元的箭头上的参数都是权重
w
w
w。神经元是从上至下进行编号,需要注意的是,常量神经元与特征神经元是分别编号的。和从0开始编号的层数不同,神经元是从1开始编号的。在异或门的例子中,含有1的偏差神经元是1号偏差神经元,含有特征的神经元则是1号特征神经元。
除了神经元和网络层,权重、偏差、神经元上的取值也存在编号。这些编号规律分别如下:
这些编号实在很复杂,因此在本次课程中,我将编号改写为如下情况:
有了这些编号说明,我们就可以用数学公式来表示从输入层传入到第一层隐藏层的信号了。以上一节中说明的XOR异或门为例子,对于仅有两个特征的单一样本而言,在第一层的第一个特征神经元中获得加和结果的式子可以表示为:
z
1
1
=
b
→
1
→
1
+
x
1
w
1
→
1
0
→
1
+
x
2
w
2
→
1
0
→
1
z_{1}^{1}=b_{\rightarrow 1}^{\rightarrow 1}+x_{1} w_{1 \rightarrow 1}^{0 \rightarrow 1}+x_{2} w_{2 \rightarrow 1}^{0 \rightarrow 1}
z11=b→1→1+x1w1→10→1+x2w2→10→1
而隐藏层中被
h
(
z
)
h(z)
h(z)处理的公式可以写作:
σ
1
1
=
h
(
z
1
1
)
=
h
(
b
→
1
1
+
x
1
w
1
→
1
0
→
1
+
x
2
w
2
→
1
0
→
1
)
\begin{aligned} \sigma_{1}^{1} &=h\left(z_{1}^{1}\right) \\ &=h\left(b_{\rightarrow 1}^{1}+x_{1} w_{1 \rightarrow 1}^{0 \rightarrow 1}+x_{2} w_{2 \rightarrow 1}^{0 \rightarrow 1}\right) \end{aligned}
σ11=h(z11)=h(b→11+x1w1→10→1+x2w2→10→1)
根据我们之前写的NAND函数,这里的
h
(
z
)
h(z)
h(z)为阶跃函数。
现在,我们用矩阵来表示数据从输入层传入到第一层,并在第一层的神经元中被处理成
σ
\sigma
σ的情况:
Z
1
=
W
1
⋅
X
+
B
1
\mathbf{Z}^{1}=\mathbf{W}^{1} \cdot \mathbf{X}+\mathbf{B}^{1}
Z1=W1⋅X+B1
[
z
1
1
z
2
1
]
=
[
w
1
→
1
0
→
1
w
1
→
2
0
→
1
w
2
→
1
0
→
1
w
2
→
2
0
→
1
]
∗
[
x
1
x
2
]
+
[
b
→
1
→
1
b
→
2
0
]
\left[\begin{array}{l} z_{1}^{1} \\ z_{2}^{1} \end{array}\right]=\left[\begin{array}{ll} w_{1 \rightarrow 1}^{0 \rightarrow 1} & w_{1 \rightarrow 2}^{0 \rightarrow 1} \\ w_{2 \rightarrow 1}^{0 \rightarrow 1} & w_{2 \rightarrow 2}^{0 \rightarrow 1} \end{array}\right] *\left[\begin{array}{l} x_{1} \\ x_{2} \end{array}\right]+\left[\begin{array}{l} b_{\rightarrow 1}^{\rightarrow 1} \\ b_{\rightarrow 2}^{0} \end{array}\right]
[z11z21]=[w1→10→1w2→10→1w1→20→1w2→20→1]∗[x1x2]+[b→1→1b→20]
矩阵结构表示为:
(
2
,
1
)
=
(
2
,
2
)
∗
(
2
,
1
)
+
(
2
,
1
)
\text { 矩阵结构表示为: }(2,1)=(2,2) *(2,1)+(2,1)
矩阵结构表示为: (2,1)=(2,2)∗(2,1)+(2,1)
=
[
x
1
w
1
→
1
0
→
1
+
x
2
w
1
→
2
0
→
1
x
1
w
2
→
1
0
→
1
+
x
2
w
2
→
2
0
→
1
]
+
[
b
→
1
→
1
b
→
1
b
→
2
]
=
[
x
1
w
1
→
1
0
→
1
+
x
2
w
1
→
2
0
→
1
+
b
→
1
→
1
x
1
w
2
→
1
0
→
1
+
x
2
w
2
→
2
0
→
1
+
b
→
2
→
1
]
[
σ
1
1
σ
2
1
]
=
[
h
(
x
1
w
1
→
1
0
→
1
+
x
2
w
1
→
2
0
→
1
+
b
→
1
→
1
)
h
(
x
1
w
2
→
1
0
→
1
+
x
2
w
2
→
2
0
→
1
+
b
→
2
0
)
]
\begin{aligned} &=\left[\begin{array}{l} x_{1} w_{1 \rightarrow 1}^{0 \rightarrow 1}+x_{2} w_{1 \rightarrow 2}^{0 \rightarrow 1} \\ x_{1} w_{2 \rightarrow 1}^{0 \rightarrow 1}+x_{2} w_{2 \rightarrow 2}^{0 \rightarrow 1} \end{array}\right]+\left[\begin{array}{l} b_{\rightarrow 1}^{\rightarrow 1} \\ b \rightarrow 1 \\ b \rightarrow 2 \end{array}\right] \\ &=\left[\begin{array}{l} x_{1} w_{1 \rightarrow 1}^{0 \rightarrow 1}+x_{2} w_{1 \rightarrow 2}^{0 \rightarrow 1}+b_{\rightarrow 1}^{\rightarrow 1} \\ x_{1} w_{2 \rightarrow 1}^{0 \rightarrow 1}+x_{2} w_{2 \rightarrow 2}^{0 \rightarrow 1}+b_{\rightarrow 2}^{\rightarrow 1} \end{array}\right] \\ \left[\begin{array}{c} \sigma_{1}^{1} \\ \sigma_{2}^{1} \end{array}\right] &=\left[\begin{array}{l} h\left(x_{1} w_{1 \rightarrow 1}^{0 \rightarrow 1}+x_{2} w_{1 \rightarrow 2}^{0 \rightarrow 1}+b_{\rightarrow 1}^{\rightarrow 1}\right) \\ h\left(x_{1} w_{2 \rightarrow 1}^{0 \rightarrow 1}+x_{2} w_{2 \rightarrow 2}^{0 \rightarrow 1}+b_{\rightarrow 2}^{0}\right) \end{array}\right] \end{aligned}
[σ11σ21]=[x1w1→10→1+x2w1→20→1x1w2→10→1+x2w2→20→1]+⎣⎡b→1→1b→1b→2⎦⎤=[x1w1→10→1+x2w1→20→1+b→1→1x1w2→10→1+x2w2→20→1+b→2→1]=[h(x1w1→10→1+x2w1→20→1+b→1→1)h(x1w2→10→1+x2w2→20→1+b→20)]
相应的从中间层最下面的神经网络会得到的结果是
σ
1
2
\sigma_{1}^{2}
σ12(如果是阶跃函数则是直接得到y)。
σ
\sigma
σ会作为中间层的结果继续传入下一层。如果我们继续向下嵌套,则可以得到:
z
1
2
=
b
→
1
→
2
+
σ
1
1
w
1
→
1
1
→
2
+
σ
2
1
w
2
→
1
1
→
2
σ
1
2
=
g
(
z
1
2
)
σ
1
2
=
g
(
b
→
1
−
2
+
σ
1
1
w
1
→
1
1
→
2
+
σ
2
1
w
2
→
1
1
→
2
)
\begin{aligned} z_{1}^{2} &=b_{\rightarrow 1}^{\rightarrow 2}+\sigma_{1}^{1} w_{1 \rightarrow 1}^{1 \rightarrow 2}+\sigma_{2}^{1} w_{2 \rightarrow 1}^{1 \rightarrow 2} \\ \sigma_{1}^{2} &=g\left(z_{1}^{2}\right) \\ \sigma_{1}^{2} &=g\left(b_{\rightarrow 1}^{-2}+\sigma_{1}^{1} w_{1 \rightarrow 1}^{1 \rightarrow 2}+\sigma_{2}^{1} w_{2 \rightarrow 1}^{1 \rightarrow 2}\right) \end{aligned}
z12σ12σ12=b→1→2+σ11w1→11→2+σ21w2→11→2=g(z12)=g(b→1−2+σ11w1→11→2+σ21w2→11→2)
由于第二层就已经是输出层了,因此第二层使用的函数是
g
(
z
)
g(z)
g(z),在这里,第二层的表达和第一层几乎一 模一样。相信各种编号在这里已经让人感觉到有些头疼了,虽然公式本身并不复杂,但涉及到神经网络不同的层以及每层上的神经元之间的数据流动,公式的编号会让人有所混淆。如果神经网络的层数继续增加,或每一层上神经元数量继续增加,神经网络的嵌套和计算就会变得更加复杂。
在实际中,我们的真实数据可能有超过数百甚至数千个特征,所以真实神经网络的复杂度是非常高,计算非常缓慢的。所以,当神经网络长成如下所示的模样,我们就无法理解中间过程了。我们不知道究竟有多少个系数,如何相互作用产生了我们的预测结果,因此神经网络的过程是一个“黑箱”。
在PyTorch中实现神经网络的时候,我们一般用不到这些复杂的数学符号,也不需要考虑这些嵌套流程,毕竟这些计算非常底层。那为什么我们还要学习这些符号呢?有以下几点原因:
1、利用数学的嵌套,我们可以很容易就理解深层神经网络为什么会随着层数的增多、每层上神经元个数的增多而变得复杂,从而理解“黑箱”究竟是怎样形成的
2、多层神经网络与单层神经网络在许多关键点上其实有所区别,这种区别使用代数表示形式会更容易显示。比如,单层神经网络(线性回归、逻辑回归)中直线的表现形式都是
X
w
Xw
Xw,且
w
w
w是结构为(n_features,1)的列向量,但在多层神经网络中,随着“层”和神经元个数的增加,只有输入层与第一个隐藏层之间是特征与
w
w
w的关系,隐藏层与隐藏层、隐藏层与输出层之间都是
σ
\sigma
σ与
w
w
w的关系。并且,即便是在输入层与第一个隐藏层之间,单个特征所对应的
w
w
w不再是列向量,而是结构为(上层特征神经元个数,下层特征神经元个数)的矩阵。并且,每两层神经元之间,都会存在一个权重矩阵,权重将无法直接追踪到特征x上,这也是多层神经网络无法被解释的一个关键原因。同时,为了让输出结果和都保持列向量的形式(与神经网络的图像匹配),
X
X
X的结构也要顺应
w
w
w的变化而变化(上文的推导中我们展现的是单 一样本仅有两个特征的情况,想想看多个样本多个特征会是什么样吧),相乘公式也变化为
w
X
wX
wX。这些细节在由单层推广到多层时,都会成为新手容易掉入的坑,基础不牢固的情况下,新手很可能在2层推广 到4层,2个特征推广到N个特征,甚至是1个样本推广到多个样本时被卡住。(W的结构会改变——权重矩阵,X从行变成列,Z变成W在前X在后)
3、嵌套的数学公式可以帮助我们更好地理解反向传播,以及更好地阅读其他教材。
不过,即便神经网络是一个神秘黑箱,我们依然可以对它进行一系列的探索。基于我们已经熟悉的三层神经网络XOR的结构,我们来提出问题。
三、探索多层神经网络:层 vs h(z)
我们首先想要发问的隐藏层的作用。在之前的XOR函数中,我们提出”多层神经网络能够描绘出一条曲线作为决策边界,以此为基础处理单层神经网络无法处理的复杂问题“,这可能让许多人产生了“是增加层数帮助了神经网络”的错觉。实际上并非如此。
在神经网络的隐藏层中,存在两个关键的元素,一个是加和函数
∑
\sum
∑,另一个是
h
(
z
)
h(z)
h(z)。除了输入层之外,任何层的任何神经元上都会有加和的性质,因为神经元有“多进单出”的性质,可以一次性输入多个信号,但是输出只能有一个,因此输入神经元的信息必须以某种方式进行整合,否则神经元就无法将信息传递下去,而最容易的整合方式就是加和
∑
\sum
∑。因此我们可以认为加和
∑
\sum
∑是神经元自带的性质,只要增加更多的层,就会有更多的加和。但是
h
(
z
)
h(z)
h(z)的存在却不是如此,即便隐藏层上没有
h
(
z
)
h(z)
h(z)(或
h
(
z
)
h(z)
h(z)是一个恒等函数),神经网络依然可以从第一层走到最后一层。让我们来试试看,在XOR中,假设隐藏层上没有
h
(
z
)
h(z)
h(z)的话,会发生什么:
xorgate = torch.tensor([0,1,1,0],dtype=torch.float32)
def AND(X):
w = torch.tensor([-0.2,0.15, 0.15], dtype = torch.float32)
zhat = torch.mv(X,w)
andhat = torch.tensor([int(x) for x in zhat >= 0],dtype=torch.float32)
return andhat
def OR(X):
w = torch.tensor([-0.08,0.15,0.15], dtype = torch.float32)
zhat = torch.mv(X,w)
return zhat
def NAND(X):
w = torch.tensor([0.23,-0.15,-0.15], dtype = torch.float32)
zhat = torch.mv(X,w)
return zhat
def XOR(X):
input_1 = X
sigma_nand = NAND(input_1)
sigma_or = OR(input_1)
x0 = torch.tensor([[1],[1],[1],[1]],dtype=torch.float32)
input_2 = torch.cat((x0,sigma_nand.view(4,1),sigma_or.view(4,1)),dim=1)
y_and = AND(input_2)
return y_and
XOR(X)
很明显,此时XOR函数的预测结果与真实的xorgate不一致。当隐藏层的
h
(
z
)
h(z)
h(z)是恒等函数或不存在时,叠加层并不能够解决XOR这样的非线性问题。从数学上来看,这也非常容易理解。
从输入层到第1层:
从第1层到输出层:
不难发现,最终从输出层输出的结果和第一层的输出结果
x
1
w
11
1
+
x
2
w
12
1
+
b
1
1
x_{1} w_{11}^{1}+x_{2} w_{12}^{1}+b_{1}^{1}
x1w111+x2w121+b11是类似的,只不过是乘以特征
x
1
x_{1}
x1,
x
2
x_{2}
x2的具体数值不同。在没有
h
(
z
)
h(z)
h(z)时,在层中流动的数据被做了仿射变换(affine transformation),仿射变换后得到的依然是一个线性方程,而这样的方程不能解决非线性问题。可见,“层”本身不是神经网络解决非线性问题的关键,层上的
h
(
z
)
h(z)
h(z)才是。 从上面的例子和数学公式中可以看出,如果
h
(
z
)
h(z)
h(z)是线性函数,或不存在,那增加再多的层也没有用。
那是不是任意非线性函数作为
h
(
z
)
h(z)
h(z)都可以解决问题呢?让我们来试试看,在XOR例子中如果不使用阶跃函数,而使用sigmoid函数作为,会发生什么。
def AND(X):
w = torch.tensor([-0.2,0.15, 0.15], dtype = torch.float32)
zhat = torch.mv(X,w)
andhat = torch.tensor([int(x) for x in zhat >= 0],dtype=torch.float32)
return andhat
def OR(X):
w = torch.tensor([-0.08,0.15,0.15], dtype = torch.float32)
zhat = torch.mv(X,w)
sigma = torch.sigmoid(zhat)
return sigma
def NAND(X):
w = torch.tensor([0.23,-0.15,-0.15], dtype = torch.float32)
zhat = torch.mv(X,w)
sigma = torch.sigmoid(zhat)
return sigma
def XOR(X):
input_1 = X
sigma_nand = NAND(input_1)
sigma_or = OR(input_1)
x0 = torch.tensor([[1],[1],[1],[1]],dtype=torch.float32)
input_2 = torch.cat((x0,sigma_nand.view(4,1),sigma_or.view(4,1)),dim=1)
y_and = AND(input_2)
return y_and
XOR(X)
可以发现,如果将
h
(
z
)
h(z)
h(z)换成sigmoid函数,XOR结构的神经网络同样会失效!可见,即便是使用了
h
(
z
)
h(z)
h(z),也不一定能够解决曲线分类的问题。在不适合的非线性函数加持下,神经网络的层数再多也无法起效。所以,
h
(
z
)
h(z)
h(z)是真正能够让神经网络算法“活起来”的关键,没有搭配合适
h
(
z
)
h(z)
h(z)的神经网络结构是无用的,而
h
(
z
)
h(z)
h(z)正是神经网络中最关键的概念之一激活函数(activation function)。
四、激活函数
经过前面的介绍与铺垫,到这里相信大家已经充分理解激活函数的作用了。神经网络中可用的激活函数多达数十种(详情可以在激活函数的维基百科中找到:https://en.wikipedia.org/wiki/Activation_function),但机器学习中常用的激活函数只有恒等函数(identity function),阶跃函数(sign),sigmoid 函数,ReLU,tanh,softmax这六种,其中Softmax与恒等函数几乎不会出现在隐藏层上,Sign、Tanh 几乎不会出现在输出层上,ReLU与Sigmoid则是两种层都会出现,并且应用广泛。幸运的是,这6种函数我们在之前的课程中已经全部给大家介绍完毕。在这里,我们将总结性声明一下输出层的g(z)与隐藏层的h(z)之间的区别,以帮助大家获得更深的理解:
- 虽然都是激活函数,但隐藏层和输出层上的激活函数作用是完全不一样的。输出层的激活函数是为了让神经网络能够输出不同类型的标签而存在的。其中恒等函数用于回归,sigmoid函数用于二分类,softmax用于多分类。换句说,
g
(
z
)
g(z)
g(z)仅仅与输出结果的表现形式有关,与神经网络的效果无关,也因此它可以使用线性的恒等函数。但隐藏层的激活函数就不同了,如我们之前尝试的XOR,隐藏层上的激活函数
h
(
z
)
h(z)
h(z)的选择会影响神经网络的效果,而线性的是会让神经网络的结构失效的。
- 在同一个神经网络中,
g
(
z
)
g(z)
g(z)与
h
(
z
)
h(z)
h(z)可以是不同的,并且在大多数运行回归和多分类的神经网络时,他们也的确是不同的。每层上的
h
(
z
)
h(z)
h(z)可以是不同的,但是同一层上的激活函数必须一致。
我们可以通过下面的这段代码来实际体会一下,
h
(
z
)
h(z)
h(z)影响模型效果,而
g
(
z
)
g(z)
g(z)只影响模型输出结果的形式的事实。之前我们曾经尝试过以下几种情况:
现在我们来试试看,隐藏层上的
h
(
z
)
h(z)
h(z)是阶跃函数,而输出层的
g
(
z
)
g(z)
g(z)是sigmoid的情况。如果XOR网络依然有效,就证明了
g
(
z
)
g(z)
g(z)的变化对神经网络结果输出无影响。反之,则说明也影响神经网络输出结果。
def AND(X):
w = torch.tensor([-0.2,0.15, 0.15], dtype = torch.float32)
zhat = torch.mv(X,w)
sigma = torch.sigmoid(zhat)
andhat = torch.tensor([int(x) for x in sigma >= 0.5],dtype=torch.float32)
return andhat
def OR(X):
w = torch.tensor([-0.08,0.15,0.15], dtype = torch.float32)
zhat = torch.mv(X,w)
yhat = torch.tensor([int(x) for x in zhat >= 0],dtype=torch.float32)
return yhat
def NAND(X):
w = torch.tensor([0.23,-0.15,-0.15], dtype = torch.float32)
zhat = torch.mv(X,w)
yhat = torch.tensor([int(x) for x in zhat >= 0],dtype=torch.float32)
return yhat
def XOR(X):
input_1 = X
sigma_nand = NAND(input_1)
sigma_or = OR(input_1)
x0 = torch.tensor([[1],[1],[1],[1]],dtype=torch.float32)
input_2 = torch.cat((x0,sigma_nand.view(4,1),sigma_or.view(4,1)),dim=1)
y_and = AND(input_2)
return y_and
XOR(X)
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)