摘要:
深度学习还没学完,怎么图深度学习又来了?别怕,这里有份系统教程,可以将0基础的你直接送到图深度学习。还会定期更新哦。
本教程是一个系列免费教程,争取每月更新2到4篇。
主要是基于图深度学习的入门内容。讲述最基本的基础知识,其中包括深度学习、数学、图神经网络等相关内容。该教程由代码医生工作室出版的全部书籍混编节选而成。偏重完整的知识体系和学习指南。在实践方面不会涉及太多基础内容 (实践和经验方面的内容,请参看原书)。
文章涉及使用到的框架以PyTorch和TensorFlow为主。默认读者已经掌握Python和TensorFlow基础。如有涉及到PyTorch的部分,会顺带介绍相关的入门使用。
本教程主要针对的人群:
本篇文章主要介绍训练模型的原理。在训练模型中,图神经网络所使用的技术是与深度学习是完全一样的。
本篇文章以介绍深度学习中训练模型的原理为主,顺便介绍一下PyTorch基础中与梯度计算相关的接口。
在模型的训练环节中,只有一个目的,就是找到模型中各个参数应该被赋予的最合适的值。基于这个目的,人们研究了有很多方法,有遗传算法、Bp算法、动态路由、常微分方程等等。其中最为主流的应该数反向链式求导。
本文也主要基于Bp算法的技术进行展开。
首先我们来回顾一下教程(三)中的内容:
在教程(三)中,介绍了计算机界中的神经元数学模型。如图所示。
该模型可以写成公式:
其中W和b都是具体的值。通过这些值的计算,得到了最终结果z。
如果这部分内容你已经掌握了。那么就请往下继续阅读。
在编写代码中,我们常习惯给W和b一个初始值,然后通过训练,一点点的对W和b进行调整。最终得到合适的值。
反向传播的意义就是:告诉模型每次训练时,需要将w和b调整多少。
在刚开始没有得到合适的权重时,正向传播生成的结果与实际的标签是有误差,反向传播就是要把这个误差传递给权重,让权重做适当的调整来达到一个合适的输出。最终的目的,是要让正向传播的输出结果与标签间的误差最小化,这就是反向传播的核心思想。
Bp算法又称“误差反向传播算法”。它是反向传播的过程中的常用方法。
正向传播的模型是清晰的,所以很容易得出一个关于由b和w组成的对于输出的表达式。接着,也可以得出一个描述损失值的表达式(将输出值与标签直接相减,或是做平方差等运算)。
为了要让这个损失值变得最小化,我们运用数学知识,选择一个损失值的表达式让这个表达式有最小值,接着通过对其求导的方式,找到最小值时刻的函数切线斜率(也就是梯度),从而让w和b的值沿着这个梯度来调整。
这里再进一步介绍其工作原理,具体步骤如图所示。
假设有一个包含一个隐藏层的神经网络。隐藏层只有一个节点。该神经网络在Bp算法中具体的过程如下:
(1)有一个批次含有三个数据A、B、C,批次中每个样本有两个数(X1、x2)通过权重(w1、w2)来到隐藏层H并生成批次h,如图中w1和w2所在的两条直线方向;
(2)该批次的h通过隐藏层权重P1生成最终的输出结果y;
(3)y与最终的标签P比较,生成输出层误差less(y,p);
(4)less(y,p)与生成y的导数相乘,得到Del_y。Del_y为输出层所需要的修改值;
(5)将h的转置与Del_y相乘得到Del_p1。这是源于h与p1相等得到的y(见第二步);
(6)最终将该批次的Del_p1求和并更新到P1上;
(7)同理,在将误差反向传递到上一层:计算Del_h。得到Del_h后再计算权重(w1、w2)的Del值并更新。
在PyTorch中,通过Variable类型与自动微分模块的组合,实现了对模型的反向传播。
Variable类是PyTorch中的另一个变量类型。它是由Autograd模块对张量进一步封装实现的。一旦张量Tensor被转化成Variable对象,便可以实现自动求导的功能。
自动微分模块(Autograd)是构成神经网络训练的必要模块。它主要是在神经网络的反向传播过程中,对基于正向计算的结果对当前参数进行微分计算,从而实现网络权重的更新。Autograd模块与张量相同,也是建立在 ATen 框架上。
Autograd提供了所有张量操作的自动求微分功能。 它的灵活性体现在可以通过代码的运行来决定反向传播的过程, 这样就使得每一次的迭代都可以让权重参数向着目标结果进行更新。
Variable对象与普通的张量(Tensor)对象之间的转化方法如下:
import torch#引入PyTorch库
from torch.autograd import Variable
a = torch.FloatTensor([4]) #定义一个张量,值是[4]
print(Variable(a)) #张量转成Variable对象,输出:tensor([4.])
#张量转成支持梯度计算的Variable对象
print(Variable(a,requires_grad=True))#输出: tensor([4.], requires_grad=True)
print(a.data) # Variable对象转成张量,输出:tensor([4.])
在使用Variable对张量进行转化时,可以使用requires_grad参数指定该张量是否需要梯度计算。
Variable类中的requires_grad属性还会受到函数no_grad(设置Variable对象不需要梯度计算)和enable_grad(重新使Variable对象的梯度计算属性生效)的影响。其中:
不需要梯度计算后,enable_grad可以重新使其恢复具有需要梯度计算的属性。
提示:
enable_grad函数只是对具有需要梯度计算属性的Variable对象有效。如果定义Variable对象时,没有设置requires_grad属性为True,则enable_grad函数也不能使其具有需要梯度计算的属性。
函数no_grad会使其作用区域中的Variable对象requires_grad属性失效。具体如下:
(1)用函数no_grad配合with语句限制requires_grad的作用域。
import torch#引入PyTorch库
from torch.autograd import Variable
x=torch.ones(2,2,requires_grad=True) #定义一个需要梯度计算的Variable对象
with torch.no_grad():
y = x * 2
print(y.requires_grad)#输出:False
上面代码可以看出,即便Variable对象在定义时,声明了需要计算梯度。在函数no_grad作用域下通过计算生成的Variable对象也一样没有需要计算梯度的属性。
(2)用函数no_grad装饰器限制requires_grad的作用域。接上面代码,具体如下:
@torch.no_grad()#用装饰器的方式修饰函数
def doubler(x):#将张量的计算封装到函数中
return x * 2
z = doubler(x)#调用函数,得到张量
print(z.requires_grad)#输出:False
上面代码使用装饰器来限制函数级别的运算梯度属性,同样可以使需要计算梯度的属性失效。
提示:
在神经网络模型的开发中,常会将搭建网络结构的过程封装起来,例如上面代码的doubler函数。在有些模型在某种情况是不需要进行训练的情况下,使用装饰器会给开发带来便捷。有关装饰器的语法细节,请参考《python带我起飞——入门、进阶、商业实战》一书6.10.3小节。这里不再展开。
在函数enable_grad的作用区域中,Variable对象的requires_grad属性将变为True。enable_grad常常与函数no_grad嵌套使用。具体如下:
(1)用函数enable_grad配合with语句限制requires_grad的作用域。
import torch#引入PyTorch库
x=torch.ones(2,2,requires_grad=True) #定义一个需要梯度计算的Variable对象
with torch.no_grad():#调用函数no_grad将需要梯度计算属性失效
with torch.enable_grad():#嵌套函数enable_grad使需要梯度计算属性生效
y = x * 2
print(y.requires_grad)#输出:True
上面代码可以看出,在函数no_grad的with语句内层又用了enable_grad的with语句使Variable对象恢复需要计算梯度的属性。于是打印y.requires_grad时输出了True。
提示:
有关函数的with调用方式,以及with语句的作用域,属于Python语法基础。不清楚该知识点的读者可以参考《python带我起飞——入门、进阶、商业实战》一书9.9小节。
(2)用函数enable_grad也可以支持装饰器的方式对函数进行修饰。接上面代码,具体如下:
@torch.enable_grad()#用装饰器的方式修饰函数
def doubler(x):#将张量的计算封装到函数中
return x * 2
with torch.no_grad():#调用函数no_grad将需要梯度计算属性失效
z = doubler(x)#调用函数,得到Variable对象
print(z.requires_grad)#输出:True
上面代码使用装饰器的方式用函数enable_grad来修饰Variable对象计算函数。该函数被修饰后将不再受到with torch.no_grad语句的影响。
(3)当enable_grad函数作用在没有requires_grad属性的Variable对象上时,将会失效。具体如下:
import torch#引入PyTorch库
x=torch.ones(2,2) #定义一个不需要梯度计算的Variable对象
with torch.enable_grad():#调用enable_grad函数
y = x * 2
print(y.requires_grad)#输出:False
在上面代码中,定义Variable对象x时,没有设置requires_grad属性。于是即使调用了enable_grad函数,通过张量x所计算出来的y仍没有需要计算梯度的属性。
在前向传播的计算过程中,每个通过计算得到的Variable对象都会有一个grad_fn属性。该属性会随着变量的backward方法进行自动的梯度计算。但是没有经过计算得到的Variable对象是没有grad_fn属性的。例如:
(1)没有经过计算的变量,没有grad_fn属性。
import torch#引入PyTorch库
from torch.autograd import Variable
x=Variable(torch.ones(2,2),requires_grad=True) #定义一个Variable对象
print(x,x.grad_fn)#输出:tensor([[1., 1.], [1., 1.]], requires_grad=True) None
如上面代码,因为x是通过定义生成的,并不是通过计算生成的,所以x的grad_fn属性为None。
(2)经过计算得到的变量,有grad_fn属性。接上面代码,具体如下:
m = x+2#经过计算得到m
print(m.grad_fn)#输出:<AddBackward0 object at 0x0000026913263D30>
该梯度函数是可以调用的,见如下代码:
#对x变量求梯度
print(m.grad_fn(x))#输出(tensor([[1., 1.], [1., 1.]], requires_grad=True), None)
上面这种情况,求出了x的导数为1。该结果便是x变量关于m的梯度。
(3)对于下面这种情况,所得的变量也是没有grad_fn属性。接上面代码具体如下:
x2=torch.ones(2,2)#定义一个不需要梯度计算的张量
m = x2+2 #经过计算得到m
print(m.grad_fn)#输出:None
上面代码中,变量m是经过计算得到的。但是参与计算的x2是一个不需要梯度计算的变量。所以m也是没有grad_fn属性。
在自定义Variable对象时,如果将属性requires_grad 设为True,则该Variable对象就被称为叶子节点,其is_leaf属性为True。
如果Variable对象不是通过自定义生成,而是通过其它张量计算得到,不是叶子节点,则该Variable对象不是叶子节点,其is_leaf属性为False。
具体代码如下:
import torch#引入PyTorch库
x=torch.ones(2,2,requires_grad=True) #定义一个Variable对象
print(x.is_leaf)#输出:True
m = x+2#经过计算得到m
print(m.is_leaf)#输出:False
上面代码中,变量x为直接定义的Variable对象,其is_leaf属性为True。变量m为通过计算得到的Variable对象,其is_leaf属性为False。
PyTorch会在模型的正向运行过程中,记录每个张量的由来,最终在内存中形成一棵树型结构。该结构可帮助神经网络在优化参数时进行反向链式求导。叶子节点的属性主要用于反向链式求导过程中,为递归循环提供信号指示。当反向链式求导遇到叶子节点时,则终止递归循环。
当带有需求梯度计算的张量经过一系列计算最终生成一个标量(具体的一个数)时,便可以使用该标量的backward方法进行自动求导。该方法的会自动调用每个需要求导变量的grad_fn函数,并将结果放到该变量的grad属性中。
例如:
import torch#引入PyTorch库
x=torch.ones(2,2,requires_grad=True) #定义一个Variable对象
m = x+2#通过计算得到m变量
f = m.mean()#通过m的mean方法,得到一个标量
f.backward()#调用标量f的backward进行自动求导
print(f,x.grad)#输出f与x的梯度:tensor(3., grad_fn=<MeanBackward1>)
tensor([[0.2500, 0.2500], [0.2500, 0.2500]])
在上面代码中,张量f调用backward方法后,便会得到x的梯度tensor([[0.2500, 0.2500], [0.2500, 0.2500]])。
提示:
backward方法一定要在当前变量内容是标量的情况下使用,否则会报错。
PyTorch正是通过backward方法实现了自动求导的功能,使得在复杂的神经网络计算中,自动的将每一层中每个参数的梯度计算出来,实现训练过程中的反向传播。该功能大大减化了开发者的工作。
损失函数是绝对网络学习质量的关键。无论什么样的网络结构,如果使用的损失函数不正确,最终都将难以训练出正确的模型。
在训练过程中,模型的会根据每次预测的结果与真实值比较,并计算出预测误差。这个误差被用于调整模型中的权重参数,同时也用于衡量模型的训练质量(误差越小模型越好)。
均值平方差(Mean Squared Error,MSE),也称“均方误差”,在神经网络中主要是表达预测值与真实值之间的差异,在数理统计中均方误差是指参数估计值与参数真值之差平方的期望值。主要是对每一个真实值与预测值相减的平方取平均值:
MSE的值越小,表明模型越好。类似的损失算法还有均方根误差RMSE(将MSE开平方)、平均绝对值误差MAD(对一个真实值与预测值相减的绝对值取平均值)等。
PyTorch中,MSE损失函数是以类的形式封装的。需要先对其进行实例化,再进行使用,具体代码如下:
import torch
loss = torch.nn.MSELoss ()(pre,label)
注意:在神经网络计算时,预测值要与真实值控制在同样的数据分布内,假设将预测经过Sigmoid激活函数得到取值范围在0~1之间,那么真实值也归一化成0~1之间。这样在做loss计算时才会有较好的效果。
交叉熵(CrossEntropy)也是Loss算法的一种,一般用在互斥多分类任务上。
y代表真实值分类(0或1),a代表预测值。整个公式的含义是计算预测结果属于某一类的概率。
PyTorch中,CrossEntropyLoss损失函数是以类的形式封装的。需要先对其进行实例化,再进行使用,具体代码如下:
import torch
loss = torch.nn.CrossEntropyLoss ()(pre,label)
注意
这里用于计算的a代表预测属于某类的概率,其取值范围是0~1。如果真实值和预测值都是1,前面一项yln(a)就是1×ln(1)等于0,后一项(1-y)×ln(1-a)也就是0×ln(0)等于0,loss为0,反之loss函数为其他数。
加权交叉熵是指在交叉熵的基础上给第一项乘了个系数(加权),是增加或减少正样本在计算交叉熵时的损失值。
在训练一个多类分类器时,如果训练样本很不均衡的话,可以通过加权交叉熵有效的控制训练模型分类的平衡性。
PyTorch中,具体做法如下:
import torch
loss = torch.nn.CrossEntropyLoss (weight)(pre,label)
tf.keras接口是TensorFlow 2.x主推的使用接口,在该接口中包含了一些常用的损失函数。具体如下:
常用的回归损失函数有:
公式中的各个项的含义如下。
在PyTorch中还有封装了其它的损失函数。这些损失函数相对不如前文中介绍的几款常用,但是作为知识扩展,也建议了解一下。具体如下:
损失函数的选取取决于输入标签数据的类型:如果输入是实数,无界的值,损失函数使用平方差,如果输入标签是位矢量(分类标志),使用交叉熵会更适合。
例如,损失函数根据任务的性质进行选取。
上文提到的交叉熵到底是什么 ? 如果这部分你不清楚,那么,请看一下这部分的知识。
信息熵 (information entropy)是一个度量单位,用来对信息进行量化。比如可以用信息熵来量化一本书所含有的信息量。它就好比用米、厘米对长度进行量化一样。
信息熵这个词是克劳德·艾尔伍德·香农从热力学中借用过来的。在热力学中,用热熵来表示分子状态混乱程度的物理量。克劳德·艾尔伍德·香农用信息熵的概念来描述信源的不确定度。
任何信息都存在冗余,冗余大小与信息中每个符号(数字、字母或单词)的出现概率或者说不确定性有关。
信息熵是指去掉冗余信息后的平均信息量。其值与信息中每个符号的概率密切相关。
一个信源发送出什么符号是不确定的,衡量它可以根据其出现的概率来度量。概率大,出现机会多,不确定性小;反之不确定性就大,则信息熵就越大。
假设计算信息熵的函数是I,计算概率的函数是P,则信息熵的特点可以有如下表示:
(1)I是P的减函数。
(2)两个独立符号所产生的不确定性(信息熵)应等于各自不确定性之和,即I(P1,P2)=I(P1)+I(P2)。
信息熵属于一个抽象概念,其计算方法本没有固定公式。任何符合信息熵特点的公式都可以被用作信息熵的计算。
对数函数是一个符合信息熵特性的函数。具体解释如下:
(1)假设两个是独立不相关事件的概率为P(x,y),则P(x,y)=P(x)P(y)。
(2)如果将对数公式引入信息熵计算的计算,则I(x,y)= log(P(x,y))=log(P(x))+log(P(y))。
(3)因为I(x)=log(P(x)),I(y)=log(P(y)),则I(x,y)=I(x)+I(y),正好符合信息熵的可加性。
为了满足I是P的减函数,则直接对P取倒数即可。于是,引入对数函数的信息熵其公式可以写成公式7-3。
公式7-3中的I(x) 也被称为随机变量 xx 的自信息 (self-information),描述的是随机变量的某个事件发生所带来的信息量。该函数在坐标轴上的曲线如图7-46所示。
图7-46 自信息公式
由图7-46可以看出,因为概率p的取值范围为0~1,公式7-3中的负号也可以用来保证信息量是非负数。
在信源中,假如一个符号U可以有n种取值:U1…Ui…Un,对应概率为:P1…Pi…Pn,且各种符号的出现彼此独立。则该信源所表达的信息量可以通过求 I(x)=−logp(U)关于概率分布 p(U) 的期望得到。那么U的信息熵便可以写成公式7-4。
目前,信息熵大多都是通过公式7-4进行计算的。在数学中对数一般取2为底,单位为比特。在神经网络中,对数一般以自然对数e为底,单位常常被称为奈特(nats)。
由公式7-4可以看出,随机变量的取值个数越多,状态数也就越多,信息熵就越大,说明混乱程度就越大。
以一个最简单的单符号二元信源为例,该信源中的符号U仅可以取值为a或b。其中,取a的概率为p,则取b的概率为1-p。该信源的信息熵可以记为H(U)=pI(p)+(1-p)I(1-p)。所形成的曲线如图7-47所示。
图7-47 二元信源的信息熵
图7-47中,x轴代表符号U取值为a的概率值P,y轴代表符号U的信息熵H(U)。由图7-47可以看出信息熵有如下几个特性:
(1)确定性:当符号U取值为a的概率值P=0和P=1时,U的值是确定的,没有任何变化量,所以信息熵为0。
(2)极值性:当P=0.5时,U的信息熵达到了最大。这表明当变量U的取值为均匀分布时(所有的取值的概率都相同),熵最大。
(3)对称性:即对称于P=0.5
(4)非负性:即收到一个信源符号所获得的信息量应为正值,H(U)≥0。
在“3 信息熵的计算公式”中所介绍公式适用于离散信源,即信源中的变量都是从离散数据中取值。
在信息论中,还有一种连续信源,即信源中的变量是从连续数据中取值。连续信源可以取值无限,信息量是无限大,对其求信息熵已无意义。一般常会已其它的连续信源做参照,用相对熵的值进行度量。此时连续信息熵可以用来表示,它是一个有限的相对值,又称相对熵。
连续信息熵与离散信源的信息熵特性相似,仍具有可加性。不同的是,连续信源的信息熵不具非负性。但是,在取两熵的差值为互信息时,它仍具有非负性。这与力学中势能的定义相仿。
联合熵 (Joint entropy)是将一维随机变量分布推广到多维随机变量分布。设两个变量X和Y ,它们的联合信息熵也可以由联合概率P(X,Y)进行计算得来。如公式7-5。
公式7-5中的联合概率分布P(X,Y)是指X,Y同时满足某一条件的概率。还可以被记作P(XY)或者P(X∩Y)。
条件熵 H(Y|X)表示在已知随机变量X的条件下随机变量Y的不确定性。条件熵 H() 可以由联合概率P(x,y)和条件概率P(y|x)进行计算得来。见公式7-6。
公式7-6中的条件概率分布P(Y|X)是指Y基于X的条件概率,即在X的条件下Y出现的概率。它与联合概率的关系见公式7-7。
公式7-7中的P(X)是指X的边际概率(也叫边缘概率)。整个公式可以描述为:“XY的联合概率”等于“Y基于X的条件概率”乘以“X的边际概率”。
条件熵 H(Y|X)的计算公式与条件概率非常相似,也可以由X和Y 的联合信息熵计算而来。见公式7-8。
公式7-8可以描述为:条件熵 H(Y|X)=联合熵 H(X,Y) 减去X单独的熵(边缘熵) H(X)。即描述X和Y所需的信息是描述X自己所需的信息,加上给定X的条件下具体化Y所需的额外信息。
交叉熵在神经网络中常用于计算分类模型的损失。其数学意义可以有如下解释:
假设样本集的概率分布为p(x),模型预测结果的概率分布为q(x),则真实样本集的信息熵如公式7-9
如果使用模型预测结果的概率分布为q(x)来表示来数据集中样本分类的信息熵,则公式可以写成7-10
公式7-10则为q(x)与p(x)的交叉熵。因为分类的概率来自于样本集,所以式中的概率部分用q(x),而熵部分则是神经网络的计算结果,所以用q(x)。
在上文曾经介绍过交叉熵损失,如式8-9所示
从交叉熵角度理解,交叉熵损失的公式是模型对正向样本预测的交叉熵(第一项)和负向样本预测的交叉熵(第二项)之和。
提示:
预测正向样本的概率为a,预测负向样本的概率为1-a。