本文主要以异或门问题为例子,介绍多层神经网络。我们将从上一篇文章中介绍的单层神经网络出发,学习或门、非与门、异或门问题,了解单层神经网络在面对非线性问题(e.g. 异或门问题)时的局限性,从而引出多层神经网络强大及其复杂性。我们将介绍多层神经网络的编号方式、其嵌套运算的本质、以及激活函数对于一个多层神经网络的重要性。
或门、非与门问题
在介绍使用单层神经网络解决二分类问题的时候,我们用了与门(andgate)的例子,并使用符号函数转化线性模型结果 ,配合恰当设置的权重向量 ,实现了模型的正确分类。其代码如下:
x0 | x1 | x2 | andgate |
---|---|---|---|
1 | 0 | 0 | 0 |
1 | 1 | 0 | 0 |
1 | 0 | 1 | 0 |
1 | 1 | 1 | 1 |
在与门问题中,只有当x1和x2两个特征值同为1时,输出1;其他情况,输出0。
import torch
# 定义输入数据
X = torch.tensor([[1,0,0],[1,1,0],[1,0,1],[1,1,1]], dtype=torch.float32)
# 定义真实标签值
y = torch.tensor([0, 0, 0, 1], dtype=torch.float32)
def AND(X):
w = torch.tensor([-0.2,0.15,0.15], dtype=torch.float32) # 定义权重向量
z_hat = torch.mv(X, w)
y_hat = torch.tensor([int(x) for x in z_hat>0], dtype=torch.float32)
return y_hat
y_hat = AND(X)
print("预测标签:", y_hat)
print("真实标签:", y)
预测标签: tensor([0., 0., 0., 1.])
真实标签: tensor([0., 0., 0., 1.])
由于与门问题只有两个特征值,我们用可视化的方式呈现。我们将两个特征值分别用x轴和y轴表示,分类为1的点被显示为红色,而分类为0的点被显示为紫色。
import matplotlib.pyplot as plt
import seaborn as sns
plt.style.use("seaborn-v0_8-whitegrid")
sns.set_style("white")
plt.figure(figsize=(5,3))
plt.title("AND GATE", fontsize=12)
plt.scatter(X[:,1], X[:,2], c=y, cmap="rainbow")
plt.xlim(-1,3)
plt.ylim(-1,3)
plt.grid(alpha=0.4, axis="y") # 显示背景中的网格
plt.gca().spines["top"].set_alpha(.0) # r让上方和右侧的坐标轴被隐藏
plt.gca().spines["right"].set_alpha(.0)
回到我们的二分类模型,符号函数的公式是:
由于,上述公式还可以写成:
从可视化的角度来说,我们的分类模型是通过 这条直线将所要被预测的点分为两类:位于直线上方的点被预测为标签1,下方的点被预测为标签0。我们将这条直线也在图上展示:
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
plt.style.use("seaborn-v0_8-whitegrid")
sns.set_style("white")
plt.figure(figsize=(5,3))
plt.title("AND GATE", fontsize=12)
plt.scatter(X[:,1], X[:,2], c=y, cmap="rainbow")
plt.xlim(-1,3)
plt.ylim(-1,3)
plt.grid(alpha=0.4, axis="y") # 显示背景中的网格
plt.gca().spines["top"].set_alpha(.0) # r让上方和右侧的坐标轴被隐藏
plt.gca().spines["right"].set_alpha(.0)
x1 = np.arange(-1, 4)
plt.plot(x1, (0.2-0.15*x1)/0.15, c="k", linestyle="--")
从图上可以看出,我们设置的权重向量得到的分割直线(通常被称作决策边界),能够将与门问题中的两类标签完美的分开。下面我们尝试将类似的决策边界应用到或门、非与门、异或门问题中。
【或门】
x0 | x1 | x2 | orgate |
---|---|---|---|
1 | 0 | 0 | 0 |
1 | 1 | 0 | 1 |
1 | 0 | 1 | 1 |
1 | 1 | 1 | 1 |
在或门问题中,只要x1和x2两个特征值中有一个为1,就输出1;否则,输出0。
import torch
# 定义输入数据
X = torch.tensor([[1,0,0],[1,1,0],[1,0,1],[1,1,1]], dtype=torch.float32)
# 定义真实标签值
y = torch.tensor([0, 1, 1, 1], dtype=torch.float32)
def OR(X):
w = torch.tensor([-0.08,0.15,0.15], dtype=torch.float32) # 定义权重向量
z_hat = torch.mv(X, w)
y_hat = torch.tensor([int(x) for x in z_hat>0], dtype=torch.float32)
return y_hat
OR(X)
tensor([0., 1., 1., 1.])
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
plt.style.use("seaborn-v0_8-whitegrid")
sns.set_style("white")
plt.figure(figsize=(5,3))
plt.title("OR GATE", fontsize=12)
plt.scatter(X[:,1], X[:,2], c=y, cmap="rainbow")
plt.xlim(-1,3)
plt.ylim(-1,3)
plt.grid(alpha=0.4, axis="y") # 显示背景中的网格
plt.gca().spines["top"].set_alpha(.0) # r让上方和右侧的坐标轴被隐藏
plt.gca().spines["right"].set_alpha(.0)
x1 = np.arange(-1, 4)
plt.plot(x1, (0.08-0.15*x1)/0.15, c="k", linestyle="--")
【非与门】
x0 | x1 | x2 | nandgate |
---|---|---|---|
1 | 0 | 0 | 1 |
1 | 1 | 0 | 1 |
1 | 0 | 1 | 1 |
1 | 1 | 1 | 0 |
在非与门问题中,只有当x1和x2两个特征值同时为1时,输出0;其他情况,输出1。
import torch
# 定义输入数据
X = torch.tensor([[1,0,0],[1,1,0],[1,0,1],[1,1,1]], dtype=torch.float32)
# 定义真实标签值
y = torch.tensor([1, 1, 1, 0], dtype=torch.float32)
def NAND(X):
w = torch.tensor([0.2,-0.15,-0.15], dtype=torch.float32) # 定义权重向量
z_hat = torch.mv(X, w)
y_hat = torch.tensor([int(x) for x in z_hat>0], dtype=torch.float32)
return y_hat
NAND(X)
tensor([1., 1., 1., 0.])
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
plt.style.use("seaborn-v0_8-whitegrid")
sns.set_style("white")
plt.figure(figsize=(5,3))
plt.title("NAND GATE", fontsize=12)
plt.scatter(X[:,1], X[:,2], c=y, cmap="rainbow")
plt.xlim(-1,3)
plt.ylim(-1,3)
plt.grid(alpha=0.4, axis="y") # 显示背景中的网格
plt.gca().spines["top"].set_alpha(.0) # r让上方和右侧的坐标轴被隐藏
plt.gca().spines["right"].set_alpha(.0)
x1 = np.arange(-1, 4)
plt.plot(x1, (0.2-0.15*x1)/0.15, c="k", linestyle="--")
异或门:无法使用直线作为决策边界
在前面的二分类例子中我们可以看到,只要选取恰当的权重向量,使用直线的决策边界即可将两个类别完美的分割。但是,在很多分类问题中,直线的决策边界无法做到完美的分类,异或门问题就是其中之一。
【异或门】
x0 | x1 | x2 | xorgate |
---|---|---|---|
1 | 0 | 0 | 0 |
1 | 1 | 0 | 1 |
1 | 0 | 1 | 1 |
1 | 1 | 1 | 0 |
在异或门问题中,当x1和x2两个特征值取值一致时(同时为0或同时为1),输出0;否则,输出1。
异或门数据的可视化展示如下:
import torch
X = torch.tensor([[1,0,0],[1,1,0],[1,0,1],[1,1,1]], dtype=torch.float32)
y = torch.tensor([0, 1, 1, 0], dtype=torch.float32)
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
plt.style.use("seaborn-v0_8-whitegrid")
sns.set_style("white")
plt.figure(figsize=(5,3))
plt.title("XOR GATE", fontsize=12)
plt.scatter(X[:,1], X[:,2], c=y, cmap="rainbow")
plt.xlim(-1,3)
plt.ylim(-1,3)
plt.grid(alpha=0.4, axis="y") # 显示背景中的网格
plt.gca().spines["top"].set_alpha(.0) # r让上方和右侧的坐标轴被隐藏
plt.gca().spines["right"].set_alpha(.0)
从上图即可看出,没有一条直线能够将两类标签完美的分割,理想的决策边界是一条曲线。在神经网络中,可以通过增加神经网络的中间层来实现曲线的决策边界。
这是一个多层神经网络,除了输入层和输出层,还多了一层“中间层”。在这个网络中,数据依然是从左侧的输入层进入,特征会分别进入NAND
和OR
两个中间层的神经元,分别获得NAND函数
的结果和OR函数
的结果,接着,和会继续被输入下一层的神经元AND
,经过AND函数
的处理,成为最终结果。下面我们使用代码来实现这个结构:
import torch
X = torch.tensor([[1,0,0],[1,1,0],[1,0,1],[1,1,1]], dtype=torch.float32)
y = torch.tensor([0, 1, 1, 0], dtype=torch.float32)
def XOR(X):
sigma_or = OR(X) # 将数据输入OR函数
sigma_nand = NAND(X) # 将数据输入AND函数
x0 = torch.tensor([1,1,1,1], dtype=torch.float32)
X = torch.cat((x0.view(4,1), sigma_or.view(4,1), sigma_nand.view(4,1)), dim=1) # 中间层的数据
y_hat = AND(X) # 将中间层数据输入AND函数
return y_hat
XOR(X)
tensor([0., 1., 1., 0.])
从结果可以看出,在单层神经网络中增加了中间层之后,非线性的异或门问题被解决。叠加了多层的神经网络被称为多层神经网络。多层神经网络是神经网络在深度学习中的基本形态。
多层神经网络的不可解释性
多层神经网络的中间层通常被称为隐藏层(hidden layer)。
在一个神经网络中,更靠近输入层的层级相对于其他层级叫做"上层",更靠近输出层的则相对于其他层级叫做"下层"。若从输入层开始从左向右编号,则输入层为第0层,输出层为最后一层。除了输入层以外,每个神经元中都存在着对数据进行处理的数个函数。
在异或门例子中,隐藏层中的函数是NAND函数和OR函数(即线性回归的加和函数+符号函数),输出层上的函数是AND函数。对于所有神经元和所有层而言,加和函数的部分都是一致的(都得到一个值),因此我们需要关注的是加和之外的那部分函数。在隐藏层中,这个函数被称为激活函数,记作;在输出层中,这个函数只是普通的连接函数,定义为。我们的数据被逐层传递,每个下层的神经元都必须处理上层神经元中的处理完毕的数据,整个流程本质上是一个嵌套计算结果的过程。
在神经网络中,任意层上都有至少一个神经元,最上面的是常量神经元,连接常量神经元箭头上的参数是截距,剩余的是特征神经元,连接特征神经元的参数是权重,神经元从上至下进行编号,需注意的是,常量神经元与特征神经元是分别编号的,且神经元是从1开始编号的。
除了神经元和网络层,权重()、偏差()、神经元上的值(和)也存在编号。编号的规律如下:
- 神经元上的值(和):上标表示所在网络层数的编号,下标表示所在层神经元的编号;
- 举例:表示在神经网络第一层的第一个特征神经元;
- 连接箭头上的权重():上标表示该参数所指向的下一层神经网络的层数,下标表示所连接的两个前后层神经元分别的编号(后一层的编号在前);
- 举例:表示指向神经网络第1层的参数,连接了第1层第1个特征神经元和第0层第2个特征神经元;
- 连接箭头上的偏差():上标表示该参数所指向的下一层神经网络的层数,下标表示所连接的下层神经元的编号;
- 举例:表示指向神经网络第1层的参数,指向了第1层第2个特征神经元。
下图是神经网络编号示例(只编号了蓝色部分的神经元和参数):
根据编号说明,我们可以用数学公式来表示从输入层传入到第一层隐藏层的信号了。以XOR异或门为例子,对于仅有两个特征的单一样本而言,在第一层的第一个特征神经元中获得加和结果的式子可以表示为:
被该神经元中激活函数处理的公式可以写作为:
现在,我们用矩阵来表示数据从输入层传入到第一层,并在第一层的神经元中被处理成的情况:
从中间层到输出层:
随着神经网络的层数继续增加,或每一层上神经元数量继续增加,神经网络的嵌套和计算就会变得更加复杂。由于每两层神经网络之间就会存在一个权重矩阵,权重将无法直接追踪到特征上,这是多层神经网络无法被解释的一个重要原因。
注:
- 在多层神经网络中,每两层神经网络之间就会存在一个权重矩阵;
- 与单层神经网络不同,多层神经网络 公式中的是传统线性回归方程中的特征矩阵(design matrix)的转置(Transpose)。
非线性激活函数对于多层神经网络的重要性
在神经网络的隐藏层中,存在两个关键的元素,一个是加和函数,另一个是激活函数。除了输入层之外,任何层的任何神经元上都会有加和的性质,因为神经元有“多进单出”的性质,可以一次性输入多个信号,但是输出只能有一个,因此输入神经元的信息必须以某种方式进行整合,否则神经元就无法将信息传递下去,而最容易的整合方式就是加和。因此我们可以认为加和是神经元自带的性质,只要增加更多的层,就会有更多的加和。但是激活函数的存在却不是如此,即便隐藏层上没有激活函数(或是一个恒等函数),神经网络依然可以从第一层走到最后一层。
我们沿用上一小节中XOR异或门的例子,可知输出层神经元的经过加和后的值为:
当激活函数为恒等函数时,上述式子可以写为:
其中,
- ,
- ,
- .
从上述公式可以看出,在激活函数为恒等函数的情况下,输出层神经元的加和值仅是输入层特征值的加权和,该神经网络相当于一个单层神经网络,而根据我们前面介绍,单层神经网络是无法解决XOR问题的。
也就是说,当激活函数是一个线性函数时,无论给神经网络增加多少层数,都无法解决非线性问题。
当然,并非所有的非线性激活函数都有效,比如,在XOR问题中,当我们把隐藏层上的激活函数换成sigmoid时,我们会发现该神经网络失效了。因此,激活函数的选择同样也很重要。深度学习中常用的激活函数有恒等函数(identity function),符号函数(Sign),Sigmoid函数,ReLU,Tanh,Softmax这六种,其Softmax与恒等函数几乎不会出现在隐藏层上,Sign、Tanh几乎不会出现在输出层上,ReLU与Sigmoid则是两种层都会出现。
神经网络上位于同一层的激活函数必须是一样的,但不同层之间可以选择不同的激活函数。尽管都是激活函数,但隐藏层和输出层上的激活函数作用是不一样的。输出层的激活函数是为了让神经网络能够输出不同类型的标签而存在的。其中恒等函数用于回归,Sigmoid函数用于二分类,Softmax用于多分类。即,仅与输出结果的表现形式有关,与神经网络的效果无关。隐藏层的激活函数会影响神经网络的效果,因此的选择是构造一个有效神经网络的关键之一。