在之前的介绍中,我们已经学习了三种单层神经网络,分别为实现线性方程的回归网络,实现二分类的逻辑回归(二分类网络),以及实现多分类的softmax回归(多分类网络),我们已经基本了解了构成普通神经网络的诸多元素。从本节课开始,我们将从单层神经网络展开至深层神经网络,并深入理解层、以及层上的各类函数计算对于神经网络的意义。
从单层到多层是神经网络发展史上的重大变化,层的增加彻底将神经网络的性能提升到了另一个高度, 正确理解层的意义对于我们自主构建神经网络有很重要的作用,学会利用层是避免浪费计算资源以及提 升神经网络效果的关键。在本节的最后,我们还将继承nn.Module类实现一个完整的深层神经网络的前向传播的过程。
一、异或门问题
还记得之前的课程中我们提到的数据“与门”(andgate)吗?与门是一组只有两个特征(x1, x2)的分类数据,当两个特征下的取值都为1时,分类标签为1,其他时候分类标签为0。
之前我们使用tensor结构与nn.Linear类在“与门”数据上上实现了简单的二分类神经网络(逻辑回归),其中使用tensor结构定义时,我们自定义了权重向量w,并巧妙的让逻辑回归输出的结果与真实结果一 致,具体代码如下:
import torch
X = torch.tensor([[1,0,0],[1,1,0],[1,0,1],[1,1,1]], dtype = torch.float32)
andgate = torch.tensor([0,0,0,1], dtype = torch.float32)
#定义w
w = torch.tensor([-0.2,0.15,0.15], dtype = torch.float32)
def LogisticR(X,w):
zhat = torch.mv(X,w) #首先执行线性回归的过程,依然是mv函数,让矩阵与向量相乘得到z
sigma = torch.sigmoid(zhat) #执行sigmoid函数,你可以调用torch中的sigmoid函数,也可 以自己用torch.exp来写
andhat = torch.tensor([int(x) for x in sigma >= 0.5], dtype = torch.float32)
#设置阈值为0.5, 使用列表推导式将值转化为0和1
return sigma, andhat
sigma, andhat = LogisticR(X,w)
sigma
#tensor([0.4502, 0.4875, 0.4875, 0.5250])
andhat
#tensor([0., 0., 0., 1.])
andgate
#tensor([0., 0., 0., 1.])
andgate == andhat
#tensor([True, True, True, True])
考虑到与门的数据只有两维,我们可以通过python中的matplotlib代码将数据可视化,其中,特征 x 1 x_{1} x1为横坐标,特征 x 2 x_{2} x2为纵坐标,紫色点代表了类别0,红色点代表类别1。
import matplotlib.pyplot as plt
import seaborn as sns
plt.style.use('seaborn-whitegrid') #设置图像的风格
sns.set_style("white")
plt.figure(figsize=(5,3)) #设置画布大小
plt.title("AND GATE",fontsize=16) #设置图像标题
plt.scatter(X[:,1],X[:,2],c=andgate,cmap="rainbow") #绘制散点图
plt.xlim(-1,3) #设置横纵坐标尺寸
plt.ylim(-1,3)
plt.grid(alpha=.4,axis="y") #显示背景中的网格
plt.gca().spines["top"].set_alpha(.0) #让上方和右侧的坐标轴被隐藏
plt.gca().spines["right"].set_alpha(.0);
结果:
在机器学习中,存在一种对数据进行分类的简单方式:就是在两类数据中间绘制一条直线,并规定直线一侧的点属于一类标签,下方另一侧的点属于另一类标签。而在这个由
x
1
x_{1}
x1与
x
1
x_{1}
x1构成的空间中,二维平面上的任意一条线可以被表示为:
x
1
=
a
x
2
+
b
x_{1}=a x_{2}+b
x1=ax2+b
我们将此表达式变换一下:
0
=
b
−
x
1
+
a
x
2
0
=
[
1
,
x
1
,
x
2
]
∗
[
b
−
1
a
]
0
=
X
w
\begin{array}{l} 0=b-x_{1}+a x_{2} \\ 0=\left[1, x_{1}, x_{2}\right] *\left[\begin{array}{c} b \\ -1 \\ a \end{array}\right] \\ 0=\boldsymbol{X} \boldsymbol{w} \end{array}
0=b−x1+ax20=[1,x1,x2]∗⎣⎡b−1a⎦⎤0=Xw
其中[b, -1, a]就是我们的权重向量
w
w
w(默认列向量),
X
X
X就是我们的特征张量,
b
b
b是我们的截距。在一组数据下,给定固定的和,这个式子就可以是一条固定直线,在和不确定的状况下,这个表达式
X
w
=
0
Xw = 0
Xw=0就可以代表平面上的任意一条直线。在直线上方的点为1类(红色),在直线下方的点为0类(紫色),其颜色标注与刚才绘制的与门数据中的数据点一致。
如果在
w
w
w点
b
b
b和固定时,给定一个唯一的
X
X
X的取值,这个表达式就可以表示一个固定的点。如果任取一个紫色的点
X
p
X_{p}
Xp就可以被表示为:
X
p
w
=
p
\boldsymbol{X}_{p} \boldsymbol{w}=p
Xpw=p
由于紫色的点所代表的标签y是0,所以我们规定,p<=0。同样的,对于任意一个红色的点而言,我们可以将它表示为:
X
r
w
=
r
\boldsymbol{X}_{\boldsymbol{r}} \boldsymbol{w}=r
Xrw=r
由于红色点所表示的标签y是1,所以我们规定,r>0。由此,如果我们有新的测试数据
X
t
X_{t}
Xt,则
X
t
X_{t}
Xt的标签就可以根据以下式子来判定:
y
=
{
1
,
if
X
t
w
>
0
0
,
if
X
t
w
≤
0
y=\left\{\begin{array}{ll} 1, & \text { if } \boldsymbol{X}_{\boldsymbol{t}} \boldsymbol{w}>0 \\ 0, & \text { if } \boldsymbol{X}_{\boldsymbol{t}} \boldsymbol{w} \leq 0 \end{array}\right.
y={1,0, if Xtw>0 if Xtw≤0
在我们只有两个特征的时候,实际上这个式子就是:
y
=
{
1
,
if
w
1
x
1
+
w
2
x
2
+
b
>
0
0
,
if
w
1
x
1
+
w
2
x
2
+
b
≤
0
y=\left\{\begin{array}{ll} 1, &\text { if } w_{1} x_{1}+w_{2} x_{2}+b>0 \\ 0,&\text { if } w_{1} x_{1}+w_{2} x_{2}+b \leq 0 \end{array}\right.
y={1,0, if w1x1+w2x2+b>0 if w1x1+w2x2+b≤0
而
w
1
x
1
+
w
2
x
2
+
b
w_{1} x_{1}+w_{2} x_{2}+b
w1x1+w2x2+b就是我们在神经网络中表示
z
z
z的式子,所以这个式子实际上就是:
y
=
{
1
if
z
>
0
0
if
z
≤
0
y=\left\{\begin{array}{ll} 1 & \text { if } \boldsymbol{z}>0 \\ 0 & \text { if } \boldsymbol{z} \leq 0 \end{array}\right.
y={10 if z>0 if z≤0
你发现了吗?用线划分点的数学表达和我们之前学习的阶跃函数的表示方法一模一样!在Lesson 8中,我们试着使用阶跃函数作为联系函数替代sigmoid来对与门数据进行分类,最终的结果发现阶跃函数也可以轻松实现与sigmoid函数作为联系函数时的分类效果。我们现在把代码改写一下:
import torch
X = torch.tensor([[1,0,0],[1,1,0],[1,0,1],[1,1,1]], dtype = torch.float32)
andgate = torch.tensor([0,0,0,1], 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
andhat = AND(X)
andgate
#tensor([0., 0., 0., 1.])
实际上,阶跃函数的本质也就是用直线划分点的过程,而这条具有分类功能直线被我们称为“决策边界”。在机器学习中,任意分类算法都可以绘制自己的决策边界,且依赖决策边界来进行分类。(逻辑回归自然也有,不过因为sigmoid函数的决策边界绘制起来没有阶跃函数那么直接,因此我们没有选择逻辑回归的决策边界来进行绘制)。不难看出,既然决策边界的方程就是
z
z
z的表达式,那在相同的数据
X
X
X下(即在相同的数据空间中),决策边界具体在哪里就是由我们定义的
w
w
w决定的。
在之前的代码中,我们定义了
w
1
w_{1}
w1 = 0.15,
w
2
w_{2}
w2 = 0.15 ,
b
b
b = -0.23,这些参数对应的直线就是
0.15
x
1
+
0.15
x
2
−
0.23
0.15 x_{1}+0.15 x_{2}-0.23
0.15x1+0.15x2−0.23。我们可以用以下代码,将这条线绘制到样本点的图像上:
import numpy as np
x = np.arange(-1,3,0.5)
plt.plot(x,(0.23-0.15*x)/0.15,color="k",linestyle="--"); #这里是从直线的表达式变型出的x2 = 的式子
可以看到,这是一条能够将样本点完美分割的直线。这说明,我们所设置的权重和截距绘制出的直线,可以将与门数据中的两类点完美分开。所以对于任意的数据,我们只需要找到适合的
w
w
w和
b
b
b就能够确认相应的决策边界,也就可以自由进行分类了。
现在,让我们使用阶跃函数作为线性结果
z
z
z之后的函数,在其他典型数据上试试看使用决策边界进行分类的方式。比如下面的“或门”(OR GATE),特征一或特征二为1的时候标签就为1的数据。
以及”非与门“(NAND GATE),特征一和特征二都是1的时候标签就为0,其他时候标签则为1的数据。
用以下代码,可以很容易就实现对这两种数据的拟合:
#定义数据
X = torch.tensor([[1,0,0],[1,1,0],[1,0,1],[1,1,1]], dtype = torch.float32)
#或门,或门的图像
#定义或门的标签
orgate = torch.tensor([0,1,1,1], dtype = torch.float32)
#或门的函数(基于阶跃函数)
def OR(X):
w = torch.tensor([-0.08,0.15,0.15], dtype = torch.float32) #在这里我修改了b的数值
zhat = torch.mv(X,w)
yhat = torch.tensor([int(x) for x in zhat >= 0],dtype=torch.float32)
return yhat
OR(X)
#tensor([0., 1., 1., 1.])
#绘制直线划分散点的图像
x = np.arange(-1,3,0.5)
plt.figure(figsize=(5,3))
plt.title("OR GATE",fontsize=16)
plt.scatter(X[:,1],X[:,2],c=orgate,cmap="rainbow")
plt.plot(x,(0.08-0.15*x)/0.15,color="k",linestyle="--")
plt.xlim(-1,3)
plt.ylim(-1,3)
plt.grid(alpha=.4,axis="y")
plt.gca().spines["top"].set_alpha(.0)
plt.gca().spines["right"].set_alpha(.0)
#非与门、非与门的图像
nandgate = torch.tensor([1,1,1,0], dtype = torch.float32)
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
NAND(X) #图像
#tensor([1., 1., 1., 0.])
x = np.arange(-1,3,0.5)
plt.figure(figsize=(5,3))
plt.title("NAND GATE",fontsize=16)
plt.scatter(X[:,1],X[:,2],c=nandgate,cmap="rainbow")
plt.plot(x,(0.23-0.15*x)/0.15,color="k",linestyle="--")
plt.xlim(-1,3)
plt.ylim(-1,3)
plt.grid(alpha=.4,axis="y")
plt.gca().spines["top"].set_alpha(.0)
plt.gca().spines["right"].set_alpha(.0)
可以看到,或门和非与门的情况都可以被很简单地解决。现在,来看看下面这一组数据:
和之前的数据相比,这组数据的特征没有变化,不过标签变成了[0,1,1,0]。这是一组被称为“异或门”(XOR GATE)的数据,可以看出,当两个特征的取值一致时,标签为0,反之标签则为1。我们同样把这组数据可视化看看:
xorgate = torch.tensor([0,1,1,0], dtype = torch.float32)
plt.figure(figsize=(5,3))
plt.title("XOR GATE",fontsize=16)
plt.scatter(X[:,1],X[:,2],c=xorgate,cmap="rainbow")
plt.xlim(-1,3)
plt.ylim(-1,3)
plt.grid(alpha=.4,axis="y")
plt.gca().spines["top"].set_alpha(.0)
plt.gca().spines["right"].set_alpha(.0)
问题出现了——很容易注意到,现在没有任意一条直线可以将两类点完美分开,所以无论我们如何调整
w
w
w和
b
b
b的取值都无济于事。这种情况在机器学习中非常常见,而现实中的大部分数据都是无法用直线完美分开的(相似的是,线性回归可以拟合空间中的直线,但大部分变量之间的关系都无法用直线来拟合,这也是线性模型的局限性)。此时我们会需要类似如下的曲线来对数据进行划分:
在神经网络诞生初期,无法找出曲线的决策边界是神经网络的痛,但是现在的神经网络已经可以轻松解决这个问题。那我们如何把直线的决策边界变成曲线呢?答案是将单层神经网络变成多层。来看下面的网格结构:
这是一个多层神经网络,除了输入层和输出层,还多了一层“中间层”。在这个网络中,数据依然是从左侧的输入层进入,特征会分别进入NAND和OR两个中间层的神经元,分别获得NAND函数的结果
y
n
a
n
d
y_{nand}
ynand和OR函数的结果
y
o
r
y_{or}
yor,接着,
y
n
a
n
d
y_{nand}
ynand和
y
o
r
y_{or}
yor会继续被输入下一层的神经元AND,经过AND函数的处理,成为最终结果y。让我们来使用代码实现这个结构,来看看这样的结构是否能够解决异或门的问题:
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)
#input_2第一列x0 第二列sigma_nand 第三列sigma_or 按照列拼接
y_and = AND(input_2)
#print("NANE:",y_nand)
#print("OR:",y_or)
return y_and
XOR(X)
#tensor([0., 1., 1., 0.])
print(NAND(X))
OR(X)
#tensor([1., 1., 1., 0.])
#tensor([0., 1., 1., 1.])
可以看到,最终输出的结果和异或门要求的结果是一致的。可见,拥有更多的”层“帮助我们解决了单层神经网络无法处理的非线性问题。叠加了多层的神经网络也被称为”多层神经网络“。多层神经网络是神经网络在深度学习中的基本形态,接下来,我们就来认识多层神经网络。