Pytorch官方英文文档:https://pytorch.org/docs/stable/torch.html?
Pytorch中文文档:https://pytorch-cn.readthedocs.io/zh/latest/
1. 权值初始化
在网络模型搭建完成之后,对网络中的权重进行合适的初始化是非常重要的一个步骤, 初始化好了,比如正好初始化到模型的最优解附近,那么模型训练起来速度也会非常的快, 但如果初始化不好,离最优解很远,那么模型就需要更多次迭代,有时候还会引发梯度消失和爆炸现象, 所以正确的权值初始化还是非常重要的,下面我们就来看看常用的权值初始化的方法,但是在这之前,先了解一下什么是梯度消失和梯度爆炸现象。
1.1 梯度的消失和爆炸
我们以上一篇的一个图来看一下梯度消失和爆炸现象
一旦发生梯度消失或者爆炸, 就会导致模型无法训练,而如果想避免这个现象,我们就得控制网络输出层的一个尺度范围,也就是不能让它太大或者太小。那么我们怎么控制这个网络输出层的尺度呢? 那就是通过合理的初始化权重了。我们下面从代码切入,进行理解吧:
我们建立一个100层的多层感知机,每一层256个神经元,我们使用上面学习的ModuleList进行建立:
1 | class MLP(nn.Module): |
这个结果可以发现,在35层的时候,神经网络的输出就成了nan, 这说明网络出现了问题,导致后面输出的值太大了, 当然我们还没有反向传播, 根据上面的权重推导的公式,后面的这些如果为nan了之后,反向传播的时候,这些权重根本就没法进行更新,会发生梯度爆炸现象。
这就是有时候我们在训练网络的时候,最后结果全是nan的原因,这往往可能是权重初始化的不当导致的。
可是,这是为啥呢? 为啥我初始化权重不当了会影响到网络的输出呢? 刚才不是还说是网络的输出影响的权重梯度吗? 那是反向传播的时候, 而正向传播的时候,权重肯定要影响到每一层的输出啊。 我们推导一下上面这个过程中每一层输出的方差是如何变化的就明白了。
好了, 那么我们看看神经网络里面每一层输出的方差计算:
这里我们的输入数据和权重都初始化的均值为0,方差为1的标准正态。 这样经过一个网络层就发现方差扩大了n倍。 而我们上面用了100个网络层, 那么这个方差会指数增长,所以我们后面才会出现输出层方差nan的情况。
那么我们怎么解决这种情况呢? 那很简单,让网络层的输出方差保持尺度不变就可以了, 可是怎么做呢? 分析一下网络层的输出方差:
1 | def initialize(self): |
这样就会发现,不会出现nan的情况了:
所以我们只要采用恰当的权值初始化方法,就可以实现多层神经网络的输出值的尺度维持在一定范围内, 这样在反向传播的时候,就有利于缓解梯度消失或者爆炸现象的发生
当然,上面的网络只是一个线性网络,在实际中我们还得考虑激活函数的存在,我们从上面的前向传播中加一个激活函数再看一下结果:
那么,具有激活函数的时候,怎么对权重进行初始化呢?
1.2 Xavier初始化
方差一致性:保持数据尺度范围维持在恰当范围, 通常方差为1。 如果有了激活函数之后,我们应该怎么对权重初始化呢?
2010年Xavier发表了一篇文章,详细探讨了如果有激活函数的时候,如何进行权重初始化, 当然它也是运用的方差一致性原则, 但是它这里考虑的是饱和激活函数, 如sigmoid, tanh。 文章中有个这样的公式推导,从而得到我们权重的方差:
这就是Xavier初始化方法, 那么在代码中怎么用呢? 还是上面的那个代码例子,我们在参数初始化里面用Xavier初始化权重:
1 | def initialize(self): |
这里面用到了一个函数nn.init.calculate_gain(nonlinearity, param=None)
这个函数的作用是计算激活函数的方差变化尺度, 怎么理解这个方差变化尺度呢?其实就是输入数据的方差除以经过激活函数之后的输出数据的方差。nonlinearity表示激活函数的名称,如tanh
, param表示激活函数的参数,如Leaky ReLU的negative_slop
。 (这里不用也行,但得知道这个方法)。这时候再来看一下最后的结果:
所以Xavier权重初始化,有利于缓解带有sigmoid,tanh的这样的饱和激活函数的神经网络的梯度消失和爆炸现象。
但是,2012年AlexNet出现之后,非饱和函数relu也用到了神经网络中,而Xavier初始化对于relu就不好使了,不信我们看看:
1.3 Kaiming初始化
这个依然是考虑的方差一致性原则,针对的激活函数是ReLU及其变种。经过公示推导,最后的权值标准差是这样的:
那么Kaiming初始化权重方法怎么用呢?
1 | def initialize(self): |
我们可以看一下结果:
所以从上面的学习中,我们对权值的初始化有了清晰的认识,发现了权重初始化对于模型的重要性,不好的权重初始化方法会引起输出层的输出值过大过小,从而引发梯度的消失或者爆炸,最终导致我们的模型无法训练。所以我们如果想缓解这种现象,就得控制输出层的值的范围尺度,就得采取合理的权重初始化方法。
1.4 十种权重初始化方法
Pytorch里面提供了很多权重初始化的方法,可以分为下面的四大类:
针对饱和激活函数(sigmoid, tanh):Xavier均匀分布, Xavier正态分布
针对非饱和激活函数(relu及变种):Kaiming均匀分布, Kaiming正态分布
三个常用的分布初始化方法:均匀分布,正态分布,常数分布
三个特殊的矩阵初始化方法:正交矩阵初始化,单位矩阵初始化,稀疏矩阵初始化:
好了,到了这里,模型模块才算得上结束, 下面我们就进行下一个模块的学习,损失函数模块,在这里面学习各种损失函数的原理及应用场景。
2. 损失函数
这一部分分为三大块, 首先看一下损失函数到底是干嘛的? 然后学习非常常用的损失函数交叉熵,最后再看看其他的几个重要损失函数。
2.1 损失函数初步介绍
损失函数: 衡量模型输出与真实标签的差异。而我们谈损失函数的时候,往往会有三个概念: 损失函数, 代价函数, 目标函数。 你知道这仨到底啥区别吗? 还是以为这仨就是一个概念?
我们发现了啥? 原来_Loss
也是继承于Module
,这个在模型创建的时候就已经很熟悉了,也具体介绍过, 既然_Loss
也是继承于这个类,那么就得先想起来肯定_Loss
也有那8个参数字典了,然后这里面是设置一个reduction
这个参数。 下面我们再以人民币二分类的实验中的交叉熵损失为例子,看看损失函数是如何创建和使用的,背后的运行机制又是什么?哈哈哈, 下面就得来一波调试了。 这次是损失函数的学习,所以我们在定义损失函数和使用损失函数的地方打上断点,并且开始debug:
程序运行到第一个断点处,我们步入,就到了loss.py文件中的一个class CrossEntropyLoss(_WeightedLoss):
交叉熵损失类的__init__
方法, 这里发现交叉熵损失函数继承_WeightedLoss
这个类:
我们继续步入,就到了class _WeightedLoss(_Loss)
:这个类里面,就会发现这个类继承_Loss
, 那么我们继续步入,就到了_Loss
这个类里面去,会发现这个继承Module
,那么现在就明白了,损失函数的初始化方法和模型其实类似,也是调用Module
的初始化方法,最终会有8个属性字典, 然后就是设置了一个reduction
这个参数。 初始化就是这样子了, 学过了nn.Module之后,这里都比较好理解。
那么下面看看使用过程中的运行机制:我们到第二个断点,然后步入,我们知道既然这个损失函数也是一个Module,那么在调用的时候肯定也是调用的forward方法了, 还真的是这样, 它也有一个forward的函数的:
看这里也是调用的forward函数,我们把程序运行到547行,再次步入,看看损失函数的forward长啥样:
我们模型构建里面forward里面写的是各个模块的拼接方式,而损失函数的forward里面调用了F里面的各种函数,我们Ctrl然后点击这个函数,看看这个交叉熵损失函数到底长啥样:
这个是底层计算了,不再往下了,我们退回去。
这就是损失函数的初始化和使用方法的内部运行机制了。从上面我们发现了损失函数其实也是一个Module, 那么既然是Module,初始化依然是有8个属性字典,使用的方法依然是定义在了forward函数中。 下面我们就详细的学习一个非常重要的函数,也是上面例子里面的函数nn.CrossEntropyLoss
, 这个在分类任务中很常用, 所以下面得详细的说说。
2.2 交叉熵损失CrossEntropyLoss
nn.CrossEntropyLoss: nn.LogSortmax()与nn.NLLLoss()结合,进行交叉熵计算。
- weight:各类别的loss设置权值
- ignore_index: 忽略某个类别
- reduction: 计算模式,可为none/sum/mean, none表示逐个元素计算,这样有多少个样本就会返回多少个loss。 sum表示所有元素的loss求和,返回标量, mean所有元素的loss求加权平均(加权平均的含义下面会提到),返回标量。看了下面的原理就懂了。
在详细介绍这些参数用法之前,得先说说这里的交叉熵损失函数,这个并不是公式意义上的交叉熵损失函数,而是有一些不同之处。还记得普通的交叉熵损失函数吗?
P表示数据的原始分布,Q表示模型输出的分布,交叉熵损失衡量两个分布之间的差异程度,交叉熵越低,说明两个分布越近。这里的一个不同就是先用nn.LogSoftmax()把模型的输出值归一化成了概率分布的形式,然后是单个样本的输出,并且没有求和符号。
具体的下面会解释,但是解释之前,得先明白一个问题,就是为什么交叉熵可以衡量两个分布的差异,这个到底是个什么东西? 这就不得不提到相对熵, 而想了解相对熵,就得先明白熵的概念,而如果想明白熵,就得先知道自信息,好吧,成功懵逼。 下面我们先看看这些都是啥吧:
在机器学习模型中,我们最小化交叉熵,其实就是最小化相对熵,因为我们训练集取出来之后就是固定的了,熵就是一个常数。
好了,我们已经知道了交叉熵是衡量两个分布之间的距离,一个差异。所以这里使用softmax,就可以将一个输出值转换到概率取值的一个范围。我们看看这里的交叉熵损失函数是怎么计算的:
这样,就是如果我们想让模型更关注某一类的话,就可以把这一类的权值设置的大一点。第二个参数ignore_index
, 这个是表示某个类别不去计算loss。而关于第三个参数reduction
, 有三个计算模式none/sum/mean, 上面已经说了,下面我们从代码中看看这三个的区别:
1 |
|
这样可以看到, none模式下是输出三个损失, sum下是三个损失求和,mean下是三个损失求平均。这里还要注意一下这里的target, 这个是每个样本给出属于哪一个类即可,类型是torch.long, 为什么要强调这个,我们下面会学习二分类交叉熵损失,是交叉熵损失函数的特例,那里的target更要注意,对比起来更容易理解
下面我们再通过代码看看加上weight的损失:
这里可以发现,给类别加上权值之后,对应样本的损失就会相应的加倍,这里重点是了解一下这个加上权之后,mean模式下怎么计算的损失: 其实也很简单,我们三个样本,第一个权值为1, 后两个权值为2, 所以分母不再是3个样本,而是1+2+2, 毕竟后两个样本权为2, 一个样本顶第一个的这样的2个。所以mean模式下求平均不是除以样本的个数,而是样本所占的权值的总份数。
2.2.1 还有几个交叉熵损失函数的特例
nn.NLLoss
在上面的交叉熵损失中,我们发现这个是softmax和NLLoss的组合,那么这里的nn.NLLLoss
是何物啊? 交叉熵损失里面还有个这个东西,其实这个东西不要被这个名字给迷惑了, 这个就是实现了一个负号的功能:
nn.NLLoss
: 实现负对数似然函数里面的负号功能
这个损失函数,就是根据真实类别去获得相应的softmax之后的概率结果,然后取反就是最终的损失。 还别说,真能反应模型好坏, 因为第一个类分错了,所以损失就大,看到没。
nn.BCELoss
这个是交叉熵损失函数的特例,二分类交叉熵。注意:输入值取值在[0,1]
这里的参数和上面的一样,也不说了, 看看这个计算公式吧:
逻辑回归的时候,是不是就是这个公式啊? 我们看看代码中这个怎么用:
这里首先注意的点就是target, 这里可以发现和交叉熵那里的标签就不一样了,首先是类型是float, 每个样本属于哪一类的时候要写成独热的那种形式,这是因为看损失函数的计算公式也能看到,每个神经元一一对应的去计算loss,而不是一个整的神经元向量去计算loss, 看结果也会发现有8个loss,因为每个神经元都一一去计算loss,根据inputs,这里是两个神经元的。
nn.BCEWithLogitsLoss
这个函数结合了Sigmoid与二分类交叉熵,注意事项: 网络最后不加sigmoid函数
这里的参数多了一个pow_weight
, 这个是平衡正负样本的权值用的, 对正样本进行一个权值设定。比如我们正样本有100个,负样本有300个,那么这个数可以设置为3,在类别不平衡的时候可以用。
计算公式如下:
这里了就是加了个sigmoid。
2.3 剩余的14种损失函数介绍
1.nn.L1Loss
这个用于回归问题,用来计算inputs与target之差的绝对值
上面的size_average和reduce不用再关注,即将淘汰。 而reduction这个三种模式,其实和上面的一样。
2.nn.MSE
这个也是用于回归问题,计算inputs与target之差的平方
3.nn.SmoothL1Loss
这是平滑的L1Loss(回归问题)
那么这个平滑到底是怎么体现的呢?
采用这种平滑的损失函数可以减轻离群点带来的影响。
4.nn.PoissonNLLLoss
功能: 泊松分布的负对数似然损失函数, 分类里面如果发现数据的类别服从泊松分布,可以使用这个损失函数
5.nn.KLDivLoss
功能:计算KLD, KL散度,相对熵,注意: 需要提前将输入计算log-probabilities, 如通过nn.logsoftmax()
其实这个已经在上面交叉熵的时候说完了。上面的Pytorch里面的计算和我们原来公式里面的计算还有点不太一样,所以我们得自己先logsoftmax(),完成转换为分布然后转成对数才可以。
这里的reduction还多了一种计算模式叫做batchmean, 是按照batchsize的大小求平均值。
6.nn.MarginRankingLoss
功能:计算两个向量之间的相似度,用于排序任务。 特别说明, 该方法计算两组数据之间的差异,也就是每个元素两两之间都会计算差异,返回一个n*n的loss矩阵。类似于相关性矩阵那种。
这个地方看一下代码理解吧还是:
7.nn.MultiLabelMarginLoss
功能:多标签边界损失函数, 这是一个多标签分类,就是一个样本可能属于多个类,和多分类任务还不一样。(多标签问题)
这里的i取值从0到输出的维度减1, j取值也是0到y的维度减1, 对于所有的i和j, i不等于y[j],也就是标签所在的神经元去减掉那些非标签所在的神经元,这说的啥? 一脸懵逼,还是看代码理解一下吧:
应该差不多明白这个过程了,可以为啥要这么做呢? 这个意思就是说我们希望标签所在的神经元要比非标签所在的神经元的输出值要尽量的大,当这个差大于1了, 我们根据max(0, 1-差值)
, 才发现不会有损失产生, 当这个差值小或者非标签所在的神经元比标签所在神经元大的时候,都会产生损失。 所以上面那个例子,我们想让第0个神经元的值要比第1个,第二个大一些,第3个神经元的值要比第1个,第2个大一些,这才能说明这个样本属于第0类和第3类,才是我们想要的结果啊。 有没有一点hinge loss的意思? 只不过那里是多分类,而这里是多标签分类,感觉思想差不多。
8.nn.SoftMarginLoss
功能: 计算二分类的logistic损失(二分类问题)
9.nn.MultiLabelSortMarginLoss
这个理解起来也不是那么好理解,也是看看代码怎么计算:我们这里是一个三分类的任务, 输入的这个样本属于第二类和第三类:
10.nn.MultiMarginLoss(hingLoss)
功能: 计算多分类的折页损失(多分类问题)
这个其实和多标签边界损失函数的原理差不多,只不过那里是一个样本属于多个类,需要每个类都这样算算,而这里一个样本属于1个类,只计算一次即可。这个其实就是我们的hinge loss损失,我们可以看一下
11.nn.TripletMarginLoss
功能: 计算三元组损失, 人脸验证中常用
我们想让模型把A和P看成一样的,也就是争取让A和P之间的距离小,而A和N之间的距离大,那么我们的模型就能够进行人脸识别任务了。
12.nn.HingeEmbeddingLoss
功能: 计算两个输入的相似性, 常用于非线性embedding和半监督学习。 特别注意, 输入的x应为两个输入之差的绝对值, 也就是手动计算两个输入的差值
13.nn.CosineEmbeddingLoss
功能: 采用余弦相似度计算两个输入的相似性,常用于半监督学习和embedding
14.nn.CTCLoss
功能: 计算CTC损失, 解决时序类数据的分类
到这里,18种损失函数就介绍完了,哇,太多了,这哪能记得住啊, 所以我们可以对这些损失函数从任务的角度分分类,到时候看看是什么任务,然后看看有哪些损失函数可以用,再去查具体用法就可以啦。 我这边是这样分的:
3. 总结
今天的内容就到这里了,这次整理的内容还是比较多的, 主要分为两大块:权重初始化和损失函数的介绍, 第一块里面有10中权重初始化方法,而第二块里面18种损失函数。 哇,这个知识量还是很大的,当然我们其实并不需要都记住,只知道有哪些方法,具体什么时候用就行了,这个系列的目的也不是要求一下子都会了, 而是先有个框架出来。 快速梳理一遍吧:
首先,我们解决了模型模块的小尾巴, 权重的初始化方法,我们学习了梯度消失和梯度爆炸的原理,也知道了权重初始化的重要性,针对各种情况学习了不同的初始化方法,重要的是Xavier初始化和Kaiming初始化方法, 分别针对非饱和激活函数和包含激活函数的网络。
然后学习了损失函数的相关知识,通过损失函数的初步介绍,我们知道了损失函数也是一个Module,那么初始化和运行机制就基本了解。 然后学习了交叉熵损失函数及四个特例, 交叉熵损失函数比较重要,所以学习了一下原理,从自信息,熵,相对熵到交叉熵都过了一遍。 最后又根据场景的不同学习了其他14种损失函数。
下面依然是一个思维导图把知识拎起来,方便后面的速查:
好了,损失函数模块到这里就结束了,后面进入优化器部分, 我们还是那个流程:数据模块 -> 模型模块 -> 损失函数模块 -> 优化器 -> 迭代训练。 我们已经完成了3个模块的学习,马上就要看到曙光,再坚持一下, rush 😉