当前位置: 首页 > article >正文

深度学习 Pytorch 深层神经网络

在之前已经学习了三种单层神经网络,分别为实现线性方程的回归网络,实现二分类的逻辑回归(二分类网络),以及实现多分类的softmax回归(多分类网络)。从本节开始,我们将从单层神经网络展开至深层神经网络,并深入理解层、以及层上的各类函数计算对于神经网络的意义。


32 异或门问题

回忆一下,上节我们提到的数据“与门”(andgate)是一组只有两个特征(x1,x2)的分类数据,当两个特征下的取值都为1时,分类标签为1,其他时候分类标签为0

x0x1x2andgate
1000
1100
1010
1111

之前我们使用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 = torch.tensor([-0.2,0.15,0.15], dtype = torch.float32)
def LogisticR(X,w):
    zhat = torch.mv(X,w)
    sigma = torch.sigmoid(zhat)
    andhat = torch.tensor([int(x) for x in sigma >= 0.5], dtype=torch.float32)
    return sigma, andhat
sigma, andhat = LogisticR(X,w)
sigma
# output :
tensor([0.4502, 0.4875, 0.4875, 0.5250])

andhat
# output :
tensor([0., 0., 0., 1.])

andgate
# output :
tensor([0., 0., 0., 1.])

考虑到与门的数据只有两维,我们可以通过python中的matploblib代码将数据可视化,其中,特征 x 1 x_1 x1为横坐标,特征 x 2 x_2 x2为纵坐标,紫色点代表类别0,红色点代表类别1

import matplotlib.pyplot as plt
import seaborn as sns
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);
# output :

在这里插入图片描述

在机器学习中,存在一种对数据进行分类的简单方式:就是在两类数据中间绘制一条直线,并规定直线一侧的点属于一类标签,另一侧的点属于另一类标签。

而在这个由 x 1 x_1 x1 x 2 x_2 x2构成的空间中,二维平面的任意一条线可以被表示为:
x 1 = a x 2 + b x_1=ax_2+b x1=ax2+b
我们将此表达式变换一下:
0 = b − x 1 + a x 2 0 = [ 1 , x 1 , x 2 ] ∗ [ b − 1 a ] 0 = X w 0 = b - x_1 + ax_2 \\ 0 = [1, x_1, x_2] * \begin{bmatrix} b \\ -1 \\ a \end{bmatrix} \\ 0 = Xw 0=bx1+ax20=[1,x1,x2] b1a 0=Xw
其中[b, -1, a]就是我们的权重向量 w w w(默认列向量), X X X就是我们的特征张量, b b b是我们的截距。在一组数据下,给定固定的 w w w b b b,这个式子就可以是一条固定直线。在 w w w 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 X_pw=p Xpw=p
由于紫色的点所代表的标签y0,所以我们规定, p < = 0 p<=0 p<=0

同样的,对于任意一个红色的点 X r X_r Xr而言,我们可以将它表示为:
X r w = r X_rw=r Xrw=r
由于红色点所代表的标签y1,所以我们规定, r > 0 r>0 r>0

由此,如果我们有新的测试数据 X t X_t Xt,则 X t X_t Xt的标签可以根据以下式子来判定:
y = { 1 , if  X i w > 0 0 , if  X i w ≤ 0 y = \begin{cases} 1, & \text{if } X_i w > 0 \\ 0, & \text{if } X_i w \leq 0 \end{cases} y={1,0,if Xiw>0if Xiw0
在外面只有两个特征的时候,实际上这个式子就是:
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 = \begin{cases} 1, & \text{if } w_1x_1 + w_2x_2 + b > 0 \\ 0, & \text{if } w_1x_1 + w_2x_2 + b \leq 0 \end{cases} y={1,0,if w1x1+w2x2+b>0if w1x1+w2x2+b0
w 1 x 1 + w 2 x 2 + b w_1x_1+w_2x_2+b w1x1+w2x2+b就是我们在神经网络中表示 z z z的式子,所以这个式子实际上就是:
y = { 1 , if  z > 0 0 , if  z ≤ 0 y = \begin{cases} 1, & \text{if } z > 0 \\ 0, & \text{if } z \leq 0 \end{cases} y={1,0,if z>0if z0
你发现了吗?用线划分的数学表达和我们之前学习的阶跃函数的表示方法一模一样!

在上节中,我们试着使用阶跃函数作为联系函数替代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)
andhat
# output :
tensor([0., 0., 0., 1.])

andgate
# output :
tensor([0., 0., 0., 1.])

实际上,阶跃函数的本质也就是用直线划分点的过程,而这条具有分类功能的直线被我们称为 “决策边界”

在机器学习中,任意 分类 算法都可以绘制自己的决策边界,且依赖决策边界来进行分类。

不难看出,既然决策边界的方程就是 z z z的表达式,那在相同的数据 X X X下(即在相同的数据空间中),决策边界具体在哪里就是由我们定义的 w w w决定的。

在之前的代码中,我们定义了 w 1 = 0.15 , w 2 = 0.15 , b = − 0.23 w_1=0.15,w_2=0.15,b=-0.23 w1=0.15,w2=0.15,b=0.23,这些参数对应的直线就是 0.15 x 1 + 0.15 x 2 − 0.23 0.15x_1+0.15x_2-0.23 0.15x1+0.15x20.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="--")
# output :

在这里插入图片描述

可以看出,这是一条能够将样本点完美分割的直线。这说明,我们所设置的权重和截距绘制出的直线,可以将与门数据中的两类点完美分开。所以对于任意的数据,我们只需要找到适合的 w w w b b b就能够确认相应的决策边界,也就可以自由进行分类了。

现在,让我们使用阶跃函数作为线性结果 z z z之后的函数,在其他典型数据上试试看使用决策边界进行分类的方式。

比如下面的 “或门”OR GATE),特征一或特征二为1的时候标签就为1的数据。

x0x1x2orgate
1000
1101
1011
1111

以及 “非与门”NAND GATE),特征一和特征二都是1的时候标签就为0,其他时候标签则为1的数据。

x0x1x2andgate
1001
1101
1011
1110

用以下代码,可以很容易就实现对这两种数据的拟合:

import torch
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)
    zhat = torch.mv(X,w)
    yhat = torch.tensor([int(x) for x in zhat > 0], dtype=torch.float32)
    return yhat
OR(X)
# output :
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)
# output :

在这里插入图片描述


注意:非与门、或门的权重都不同

# 定义与非门的标签
X = torch.tensor([[1,0,0],[1,1,0],[1,0,1],[1,1,1]],dtype=torch.float32)
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)
# output :
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)
# output :

在这里插入图片描述

可以看到,或门和与非门的情况都可以被很简单地解决。现在来看下面这一组数据:

x0x1x2XOR
1000
1101
1011
1110

和之前的数据相比,这组数据的特征没有变化,不过标签 y y y变成了[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)
# output :

在这里插入图片描述

问题出现了——没有任意一条直线可以将两类点完美分开,所以无论我们如何调整 w w w b b b的取值都无济于事。

这种情况在机器学习中非常常见,而现实中的大部分数据都是无法用直线完美分开的(相似的是,线性回归可以拟合空间中的直线,但大部分变量之间的关系都无法用直线来拟合,这也是线性模型的局限性)。此时我们会需要类似如下的曲线来对数据进行划分:

在这里插入图片描述

如何把直线的决策边界变成曲线呢?答案是将单层神经网络变成多层

来看下面的网格结构:

在这里插入图片描述

这是一个多层神经网络,除了输入层和输出层,还多了一层“中间层”。在这个网络中,数据依然是从左侧的输入层进入,特征会分别进入NANDOR两个中间层的神经元,分别获得NAND函数的结果ynandOR函数的结果yor,接着,ynand和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)
    y_and = AND(input_2)
    #print("NANE:",y_nand)
    #print("OR:",y_or)
    return y_and
XOR(X)
# output :
tensor([0., 1., 1., 0.])

可以看到,最终输出的结果和异或门要求的结果是一致的。可见,拥有更多的”层“帮助我们解决了单层神经网络无法处理的非线性问题。叠加了多层的神经网络也被称为”多层神经网络“。多层神经网络是神经网络在深度学习中的基本形态,接下来,我们就来认识多层神经网络。


33 黑箱:深层神经网络的不可解释性

在这里插入图片描述

首先从结构上来看,多层神经网络比单层神经网络多出了“中间层”。中间层常常被称为隐藏层(hidden layer),理论上来说可以有无限层,所以在图像表示中经常被省略。

层数越多,神经网络的模型复杂度越高,一般也认为更深的神经网络可以解决更加复杂的问题。在学习中,通常我们只会设置3~5个隐藏层,但在实际工业场景中会更多。

当数据量够大时,现代神经网络层数越深,效果越好。

在这里插入图片描述

在一个神经网络中,更靠近输入层的层级相对于其他层级叫做"上层",更靠近输出层的则相对于其他层级叫做"下层"。若从输入层开始从左向右编号,则输入层为第0层,输出层为最后一层。除了输入层以外,每个神经元中都存在着对数据进行处理的数个函数。在我们的例子异或门(XOR)中,隐藏层中的函数是NAND函数和OR函数(也就是线性回归的加和函数+阶跃函数),输出层上的函数是AND函数。

对于所有神经元和所有层而言,加和函数的部分都是一致的(都得到结果 ),因此我们需要关注的是加和之外的那部分函数。在隐藏层中这个函数被称为激活函数,符号为 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号偏差神经元,含有特征 x 1 x_1 x1的神经元则是1号特征神经元。

除了神经元和网络层,权重、偏差、神经元上的取值也存在编号。这些编号规律分别如下:

在这里插入图片描述

这些编号实在很复杂,所以将编号改写为如下:

在这里插入图片描述

需要记住的是, z z z是加和之后的结果, σ \sigma σ是经过激活函数的结果,无论是什么激活函数。

w w w b b b是位于层与层之间的连线上,所以有箭头。 z z z σ \sigma σ是神经元上面的,无箭头。

上标表示的是层的编号,下标表示的是层中的神经元编号。

b b b由于每一层之后会有一个新的神经元,所以没有起点,只有终点。

有了这些编号说明,我们就可以用数学公式来表示从输入层传入到第一层隐藏层的信号了。以上一节中说明的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_{\rightarrow1}^{\rightarrow1} + x_1 w_{1 \rightarrow 1}^{0 \rightarrow 1} + x_2 w_{2 \rightarrow 1}^{0 \rightarrow 1} z11=b11+x1w1101+x2w2101
而隐藏层中被 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 ) \sigma_1^1 = h(z_1^1) \\ = h(b_{-1}^{-1} + x_1 w_{1 \rightarrow 1}^{0 \rightarrow 1} + x_2 w_{2 \rightarrow 1}^{0 \rightarrow 1}) σ11=h(z11)=h(b11+x1w1101+x2w2101)
根据我们之前写的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=W1X+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 → 1 ] \begin{bmatrix} z_1^1 \\ z_2^1 \end{bmatrix}= \begin{bmatrix} 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{bmatrix} * \begin{bmatrix} x_1 \\ x_2 \end{bmatrix} + \begin{bmatrix} b_{\rightarrow1}^{\rightarrow1} \\ b_{\rightarrow2}^{\rightarrow1} \end{bmatrix} [z11z21]=[w1101w2101w1201w2201][x1x2]+[b11b21]

矩阵结构表示为: ( 2 , 1 ) = ( 2 , 2 ) ∗ ( 2 , 1 ) + ( 2 , 1 ) 矩阵结构表示为: (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 → 2 → 1 ] = \begin{bmatrix} 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{bmatrix} + \begin{bmatrix} b_{\rightarrow 1}^{\rightarrow 1} \\ b_{\rightarrow 2}^{\rightarrow 1} \end{bmatrix} =[x1w1101+x2w1201x1w2101+x2w2201]+[b11b21]

= [ 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 ] = \begin{bmatrix} 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{bmatrix} =[x1w1101+x2w1201+b11x1w2101+x2w2201+b21]

[ σ 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 → 1 ) ] \begin{bmatrix} \sigma_1^1 \\ \sigma_2^1 \end{bmatrix}= \begin{bmatrix} h(x_1 w_{1 \rightarrow 1}^{0 \rightarrow 1} + x_2 w_{1 \rightarrow 2}^{0 \rightarrow 1} + b_{\rightarrow1}^{\rightarrow1}) \\ h(x_1 w_{2 \rightarrow 1}^{0 \rightarrow 1} + x_2 w_{2 \rightarrow 2}^{0 \rightarrow 1} + b_{\rightarrow2}^{\rightarrow1}) \end{bmatrix} [σ11σ21]=[h(x1w1101+x2w1201+b11)h(x1w2101+x2w2201+b21)]

相应的从中间层最下面的神经网络会得到的结果是 σ 1 2 \sigma_1^2 σ12(如果是阶跃函数则是直接得到 y y 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 ) 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(z_1^2) \\ \sigma_1^2 = g(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}) z12=b12+σ11w1112+σ21w2112σ12=g(z12)σ12=g(b12+σ11w1112+σ21w2112)

由于第二层就已经是输出层了,因此第二层使用的函数是 g ( z ) g(z) g(z),在这里,第二层的表达和第一层几乎一模一样。相信各种编号在这里已经让人感觉到有些头疼了,虽然公式本身并不复杂,但涉及到神经网络不同的层以及每层上的神经元之间的数据流动,公式的编号会让人有所混淆。如果神经网络的层数继续增加,或每一层上神经元数量继续增加,神经网络的嵌套和计算就会变得更加复杂。

在实际中,我们的真实数据可能有超过数百甚至数千个特征,所以真实神经网络的复杂度是非常高,计算非常缓慢的。所以,当神经网络长成如下所示的模样,我们就无法理解中间过程了。我们不知道究竟有多少个系数,如何相互作用产生了我们的预测结果,因此神经网络的过程是一个“黑箱”。

在这里插入图片描述


34 探索多层神经网络:层 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)的话,会发生什么:

#回忆一下XOR数据的真实标签
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)
    #下面这一行就是阶跃函数的表达式,注意AND函数是在输出层,所以保留输出层的阶跃函数g(z)
    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)
    #注释掉阶跃函数,相当于h(z)是恒等函数
    #yhat = torch.tensor([int(x) for x in zhat >= 0],dtype=torch.float32)
    return zhat

def NAND(X):
    w = torch.tensor([0.23,-0.15,-0.15], dtype = torch.float32) 
    zhat = torch.mv(X,w)
    #注释掉阶跃函数,相当于h(z)是恒等函数
    #yhat = torch.tensor([int(x) for x in zhat >= 0],dtype=torch.float32)
    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)
    #print("NANE:",y_nand)
    #print("OR:",y_or)
    return y_and

XOR(X)
# output :
tensor([0., 0., 0., 0.])

很明显,此时XOR函数的预测结果与真实的xorgate不一致。当隐藏层的 h ( z ) h(z) h(z)是恒等函数或不存在时,叠加层并不能够解决XOR这样的非线性问题。从数学上来看,这也非常容易理解。

从输入层到第1层:
Z 1 = W 1 ⋅ X + B 1 Σ 1 = h ( Z 1 ) 因为 h ( z ) 为恒等函数, 所以 Σ 1 = [ σ 1 1 σ 2 1 ] = [ z 1 1 z 2 1 ] \mathbf{Z}^1 = \mathbf{W}^1 \cdot \mathbf{X} + \mathbf{B}^1 \\ \boldsymbol{\Sigma}^1 = h(\mathbf{Z}^1) \\ 因为 h(z) 为恒等函数, \\ 所以 \boldsymbol{\Sigma}^1 = \begin{bmatrix} \sigma_1^1 \\ \sigma_2^1 \end{bmatrix} = \begin{bmatrix} z_1^1 \\ z_2^1 \end{bmatrix} Z1=W1X+B1Σ1=h(Z1)因为h(z)为恒等函数,所以Σ1=[σ11σ21]=[z11z21]
从第1层到输出层:
Z 2 = W 2 ⋅ Σ + B 2 [ z 1 2 ] = [ w 1 → 1 1 → 2 w 1 → 2 1 → 2 ] ∗ [ σ 1 1 σ 2 1 ] + [ b → 1 → 2 ] \mathbf{Z}^2 = \mathbf{W}^2 \cdot \boldsymbol{\Sigma} + \mathbf{B}^2 \\ \begin{bmatrix} z_1^2 \end{bmatrix} = \begin{bmatrix} w_{1\rightarrow 1}^{1\rightarrow 2} & w_{1\rightarrow 2}^{1\rightarrow 2} \end{bmatrix} \ast \begin{bmatrix} \sigma_1^1 \\ \sigma_2^1 \end{bmatrix} + \begin{bmatrix} b_{\rightarrow 1}^{\rightarrow 2} \end{bmatrix} Z2=W2Σ+B2[z12]=[w1112w1212][σ11σ21]+[b12]
由于公式太长 ,不再给 s s s写箭头角标 ,而是直接写普通数字角标:
[ z 1 2 ] = σ 1 1 w 11 2 + σ 2 1 w 12 2 + b 1 2 = z 1 1 w 11 2 + z 2 1 w 12 2 + b 1 2 = ( x 1 w 11 1 + x 2 w 12 1 + b 1 1 ) w 11 2 + ( x 1 w 21 1 + x 2 w 22 1 + b 2 1 ) w 12 2 + b 1 2 = x 1 ( w 11 1 w 11 2 + w 21 1 w 12 2 ) + x 2 ( w 12 1 w 11 2 + w 22 1 w 12 2 ) + b 1 1 w 11 2 + b 2 1 w 12 2 + b 1 2 = x 1 W 1 + x 2 W 2 + B , 其中  W 1 , W 2  和  B  都是常数 \begin{bmatrix} z_1^2 \end{bmatrix} = \sigma_1^1 w_{11}^2 + \sigma_2^1 w_{12}^2 + b_1^2 \\ = z_1^1 w_{11}^2 + z_2^1 w_{12}^2 + b_1^2 \\ = (x_1 w_{11}^1 + x_2 w_{12}^1 + b_1^1) w_{11}^2 + (x_1 w_{21}^1 + x_2 w_{22}^1 + b_2^1) w_{12}^2 + b_1^2 \\ = x_1 (w_{11}^1 w_{11}^2 + w_{21}^1 w_{12}^2) + x_2 (w_{12}^1 w_{11}^2 + w_{22}^1 w_{12}^2) + b_1^1 w_{11}^2 + b_2^1 w_{12}^2 + b_1^2 \\ = x_1 W_1 + x_2 W_2 + B, \text{其中 } W_1, W_2 \text{ 和 } B \text{ 都是常数} [z12]=σ11w112+σ21w122+b12=z11w112+z21w122+b12=(x1w111+x2w121+b11)w112+(x1w211+x2w221+b21)w122+b12=x1(w111w112+w211w122)+x2(w121w112+w221w122)+b11w112+b21w122+b12=x1W1+x2W2+B,其中 W1,W2  B 都是常数
不难发现,最终从输出层输出的结果和第一层的输出结果 x 1 w 11 1 + x 2 w 12 1 + b 1 1 x_1w_{11}^1+x_2w_{12}^1+b_1^1 x1w111+x2w121+b11是类似的,只不过是乘以特征 x 1 , x 2 x_1,x_2 x1,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函数作为 h ( z ) h(z) h(z),会发生什么。

def AND(X):
    w = torch.tensor([-0.2,0.15, 0.15], dtype = torch.float32)
    zhat = torch.mv(X,w)
    #下面这一行就是阶跃函数的表达式,注意AND函数是在输出层,所以保留输出层的阶跃函数g(z)
    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)
    #h(z), 使用sigmoid函数
    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)
    #h(z), 使用sigmoid函数
    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)
    #print("NANE:",y_nand)
    #print("OR:",y_or)
    return y_and
    
XOR(X)
# output :
tensor([0., 0., 0., 0.])

可以发现,如果将 h ( z ) h(z) h(z)换成sigmoid函数,XOR结构的神经网络同样会失效!可见,即便是使用了,也不一定能够解决曲线分类的问题。在不适合的非线性函数加持下,神经网络的层数再多也无法起效。所以, h ( z ) h(z) h(z)是真正能够让神经网络算法“活起来”的关键,没有搭配合适 的神经网络结构是无用的,而 h ( z ) h(z) h(z)正是神经网络中最关键的概念之一激活函数(activation function)


35 激活函数

在人工神经网络的神经元上,根据一组输入定义该神经元的输出结果的函数,就是激活函数。激活函数一般都是非线性函数,它出现在神经网络中除了输入层以外的每层的每个神经元上。

经过前面的介绍与铺垫,到这里相信大家已经充分理解激活函数的作用了。神经网络中可用的激活函数多达数十种(详情可以在激活函数的维基百科中找到,但机器学习中常用的激活函数只有恒等函数(identity function),阶跃函数(sign),sigmoid函数,ReLUtanhsoftmax这六种,其中Softmax与恒等函数几乎不会出现在隐藏层上,SignTanh几乎不会出现在输出层上,ReLUSigmoid则是两种层都会出现,并且应用广泛。

在这里,我们将总结性声明一下输出层的g(z)与隐藏层的h(z)之间的区别,以帮助大家获得更深的理解:

  1. 虽然都是激活函数,但隐藏层和输出层上的激活函数作用是完全不一样的。输出层的激活函数 g ( z ) g(z) g(z)是为了让神经网络能够输出不同类型的标签而存在的。其中恒等函数用于回归,sigmoid函数用于二分类,softmax用于多分类。换句说, g ( z ) g(z) g(z)仅仅与输出结果的表现形式有关,与神经网络的效果无关,也因此它可以使用线性的恒等函数。但隐藏层的激活函数就不同了,如我们之前尝试的XOR,隐藏层上的激活函数 h ( z ) h(z) h(z)的选择会影响神经网络的效果,而线性的 h ( z ) h(z) h(z)是会让神经网络的结构失效的。

  2. 在同一个神经网络中, 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)XOR网络是否有效
阶跃函数阶跃函数有效
恒等函数阶跃函数无效
sigmoid函数阶跃函数无效
阶跃函数sigmoid函数?

现在我们来试试看,隐藏层上的 h ( z ) h(z) h(z)是阶跃函数,而输出层的 g ( z ) g(z) g(z)sigmoid的情况。如果XOR网络依然有效,就证明了 g ( z ) g(z) g(z)的变化对神经网络结果输出无影响。反之,则说明 g ( z ) g(z) g(z)也影响神经网络输出结果。

#如果g(z)是sigmoid函数,而h(z)是阶跃函数

#输出层,以0.5为sigmoid的阈值
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

#隐藏层,OR与NAND都使用阶跃函数作为h(z)
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)
    #print("NANE:",y_nand)
    #print("OR:",y_or)
    return y_and

XOR(X)  
# output :
tensor([0., 1., 1., 0.])

从结果可以看出,只要隐藏层的 h ( z ) h(z) h(z)是阶跃函数,XOR网络就一定能有效,这与输出层 g ( z ) g(z) g(z)是什么函数完全无关。

从这里开始,若没有特别说明,当我们提到“激活函数”时,特指隐藏层上的激活函数 。当需要表达输出层上的激活函数时,我们需称其为“输出层激活函数”(out_activation)。

隐藏层 h ( z ) h(z) h(z)输出层 g ( z ) g(z) g(z)XOR网络是否有效
阶跃函数阶跃函数有效
恒等函数阶跃函数无效
sigmoid函数阶跃函数无效
阶跃函数sigmoid函数有效

36 从0实现深度神经网络的正向传播

在这里插入图片描述

学到这里,我们已经学完了一个普通深度神经网络全部的基本元素——用来构筑神经网络的结构的层与激活函数,输入神经网络的数据(特征、权重、截距),并且我们了解从左向右的过程是神经网络的正向传播(也叫做前向传播,或者向前传播)。在过去的课程中我们所学习的内容都是在torch.nn这个模块下,现在我们就使用封装好的torch.nn模块来实现一个完整、多层的神经网络的正向传播。


假设我们有500条数据,20个特征,标签为3分类。我们现在要实现一个三层神经网络,这个神经网络的架构如下:第一层有13个神经元,第二层有8个神经元,第三层是输出层。其中,第一层的激活函数是relu,第二层是sigmoid。我们要如何实现它呢?来看代码:

# 继承nn.Module类完成正向传播
import torch
import torch.nn as nn
from torch.nn import functional as F

# 确定数据
# 控制X和y的随机性
torch.manual_seed(420)
X = torch.rand((500, 20), dtype = torch.float32)
y = torch.randint(low = 0, high = 3, size = (500, 1), dtype = torch.float32)

# 继承nn.Modules类来定义神经网络的架构
class Model(nn.Module):
    # init : 定义类本身,__init__函数是在类被实例化的瞬间就会执行的函数
    # in_features为特征,out_features为标签
    def __init__(self,in_features = 10, out_features = 2):
        # 用父类替换现在的类
        # 子类只默认继承除了父类的init函数之外的所有函数,如果要继承父类的init函数,必须使用super关键字
        super(Model,self).__init__()
        # 输入层不用写,这里是隐藏层的第一层
        self.linear1 = nn.Linear(in_features, 13, bias = True)
        # 层的第一个参数都是上一层的神经元个数,第二个参数是这一层的神经元个数
        self.linear2 = nn.Linear(13, 8, bias = True)
        self.output = nn.Linear(8, out_features, bias = True)
    
    # __init__之外的函数,是在__init__被执行完毕后,就可以被调用的函数
    def forward(self, x):
        # 输入数据,会自动生成w和b
        z1 = self.linear1(x)
        sigma1 = torch.relu(z1)
        z2 = self.linear2(sigma1)
        sigma2 = torch.sigmoid(z2)
        z3 = self.output(sigma2)
        # 因为我们输入的是二维张量,dim=1就表示要对每一行都进行softmax计算
        sigma3 = F.softmax(z3, dim = 1)
        return sigma3
    
input_ = X.shape[1]	# 特征的数目
output_ = len(y.unique())	# 分类的数目

# 实例化神经网络
# 控制每次实例化后w和b的随机性
torch.manual_seed(420)
net = Model(in_features = input_, out_features = output_)
# 在这一瞬间,所有的层就已经被实例化好了,所有随机的w和b也都被建立好了
# 前向传播
net(X)		# 等同于net.forward(X)
# output :
tensor([[0.4140, 0.3496, 0.2365],
        [0.4210, 0.3454, 0.2336],
        [0.4011, 0.3635, 0.2355],
        ...,
        [0.4196, 0.3452, 0.2352],
        [0.4153, 0.3455, 0.2392],
        [0.4153, 0.3442, 0.2405]], grad_fn=<SoftmaxBackward0>)

# 查看输出结果的结构
net.forward(X).shape
# output : 对应五百个数据的三个类别的概率
torch.Size([500, 3])

# 查看每一层上的权重w和截距b的形状
net.linear.weight.shape
# output :
torch.Size([13, 20])

net.linear2.weight.shape
# output :
torch.Size([8, 13])

net.output.weight.shape
# output :
torch.Size([3, 8])

每一层的权重矩阵都是反过来的结构:第一个数表示当前层的神经元个数,第二个数表示前一层的神经元个数。

比如说我们的X结构是(500,20),对于多层神经网络来说,输入的X会自动的转换成(20,500)的结构,并且w会自动的放在前面和X相乘。

第一层: w (13,20) * (20,500) -> (13,500)

第二层: w (8, 13) * (13,500) -> (8,500)

第三层:w(3, 8) * (8, 500) -> (3,500) -> (500,3)

最后softmax输出的时候,又会把行和列给调过来,变成(500,3)

# 查看截距形状
net.linear1.bias.shape
# output : 因为第一层有13个神经元
torch.Size([13])

通过使用super函数,我们的神经网络模型从nn.Module那里继承了哪些有用的属性和方法呢?首先,如果不加super函数,神经网络的向前传播是无法运行的:

# 注释掉super函数就会报错:
AttributeError: cannot assign module before Module.__init__() call

nn.Module类的定义代码有一千多行,其__init__函数中有许多复杂的内容需要被继承,因此super函数一定不能漏下。

在这里插入图片描述

我们从nn.Module中继承了这些有用的方法:

# 属性的继承
net.training # 是否用于训练

# 方法的继承
net.cuda()

net.cpu()

net.apply() # 对__init__中的所有对象(全部层)都执行同样的操作
# 比如,令所有线性层的初始权重w都为0
def initial_0(m):
    print(m)
    if type(m) == nn.Linear:
        m.weight.data.fill_(0)
        print(m.weight)
net.apply(initial_0)

# 一个特殊的方法
net.parameters() # 一个迭代器,我们可以通过循环的方式查看里面究竟是什么内容
for param in net.parameters():
    print(param)

http://www.kler.cn/a/524178.html

相关文章:

  • 【UE插件】Sphinx关键词语音识别
  • JavaScript_02 表单
  • Linux 非阻塞IO
  • Airflow:精通Airflow任务依赖
  • 使用 Redis List 和 Pub/Sub 实现简单的消息队列
  • 解读隐私保护工具 Fluidkey:如何畅游链上世界而不暴露地址?
  • 虚幻浏览器插件 UE与JS通信
  • 《活出人生的厚度》
  • 【Docker】快速部署 Nacos 注册中心
  • AlertDialog组件的功能与用法
  • 电信骨干网络
  • 世上本没有路,只有“场”et“Bravo”
  • kaggle比赛入门 - House Prices - Advanced Regression Techniques(第四部分)
  • c++ 定点 new
  • WGCLOUD使用详解 - 如何监控文件防篡改
  • 计算机的错误计算(二百二十四)
  • 【玩转全栈】----靓号管理系统实现
  • Qt5.14.2如何设置编译链
  • matlab中,fill命令用法
  • 简单的SQL语句的快速复习
  • 【Validator】universal-translator,实现动态多语言切换,go案例根据Accept-Language 动态设置 locale
  • 论文笔记(六十三)Understanding Diffusion Models: A Unified Perspective(六)(完结)
  • 基于SpringBoot的中老年人文化活动管理系统
  • 图论——单源最短路的扩展应用
  • 【漫话机器学习系列】064.梯度下降小口诀(Gradient Descent rule of thume)
  • RAG技术:通过向量检索增强模型理解与生成能力