系统学习Pytorch笔记五:模型的权值初始化与损失函数介绍
模型的权值初始化与损失函数介绍
背景
学习知识先有框架(至少先知道有啥东西)然后再通过实战(各个东西具体咋用)来填充这个框架。 而这个系列的目的就是在脑海中先建一个Pytorch的基本框架出来。
通过上一次的学习,基本上完成了模型模块,对模型的创建和模型容器的知识和网络搭建及常见的网络层知识有了基本认识,接下来主要是进行权值初始化与损失函数介绍的学习。权值初始化是网络模型搭建好之后的一个非常重要的步骤,正确的权值初始化可以加速模型的收敛, 不恰当的权值初始化导致输出层的输出过大或者过小,最终导致梯度爆炸或者消失,使得模型无法训练, 这里会深层剖析权重初始化的重要性,会学习适用于饱和激活函数tanh等的Xavier初始化方法和非饱和激活函数relu等的Kaiming初始化方法(这些在实践中非常常用,但是有时候并不知道用这个背后的原因)。学习完了这个,然后再正式整理关于各种损失函数的一些知识,这里会学习多种损失函数的原理及使用,最后会对损失函数梳理一下,得知道什么样的任务有哪些损失函数可用。
权值初始化
在网络模型搭建完成之后,对网络中的权重进行合适的初始化是非常重要的一个步骤, 初始化好了,比如正好初始化到模型的最优解附近,那么模型训练起来速度也会非常的快, 但如果初始化不好,离最优解很远,那么模型就需要更多次迭代,有时候还会引发梯度消失和爆炸现象, 所以正确的权值初始化还是非常重要的,下面我们就来看看常用的权值初始化的方法,但是在这之前,先了解一下什么是梯度消失和梯度爆炸现象。
梯度的消失和爆炸
来看一下梯度消失和爆炸现象
假设我们要算
W
2
W 2
W2 的梯度,我们根据链式法则
W
2
W2
W2梯度的求解过程中会用到上一层神经元的输出值
H
1
H1
H1 , 那么这时候,如果
H
1
H1
H1的输出值非常小,那么
W
2
W2
W2的梯度也会非常小,这时候就有可能造成梯度消失的现象。当网络层很多的时候,这种连乘一个数非常小,就会导致网络的输出越乘越小,后面的网络输出层越来越小,而反向传播又会用到输出层的值,此时越后面的层就越有可能出现梯度消失。当
H
1
H1
H1非常大的时候,当然也就会发生梯度爆炸。
一旦发生梯度消失或者爆炸, 就会导致模型无法训练,而如果想避免这个现象,我们就得控制网络输出层的一个尺度范围,也就是不能让它太大或者太小。那么我们怎么控制这个网络输出层的尺度呢? 那就是通过合理的初始化权重了。
建立一个100层的多层感知机,每一层256个神经元,看看效果。
class MLP(nn.Module):
def __init__(self, neural_num, layers):
super(MLP, self).__init__()
self.linears = nn.ModuleList([nn.Linear(neural_num, neural_num, bias=False) for i in range(layers)])
self.neural_num = neural_num
# 正向传播
def forward(self, x):
for (i, linear) in enumerate(self.linears):
x = linear(x)
print("layer:{}, std:{}".format(i, x.std()))
if torch.isnan(x.std()):
print('output is nan in {} layers".format(i))
break
return x
# 权值初始化,我们这里使用标准正态
def initialize(self):
for m in self.modules():
if isinstance(m, nn.Linear):
nn.init.normal_(m.weight.data) # normal: mean=0, std=1
# 用一下网络
layer_nums = 100
neural_nums = 256
batch_size = 16
net = MLP(neural_nums, layer_nums)
net.initialize()
inputs = torch.randn((batch_size, neural_nums)) # normal: mean=0, std=1
output = net(inputs)
print(output)
每一层输出的方差越来越大,在35层的时候,神经网络的输出就成了nan, 这说明网络出现了问题,导致后面输出的值太大了, 当然我们还没有反向传播, 根据上面的权重推导的公式,后面的这些如果为nan了之后,反向传播的时候,这些权重根本就没法进行更新,会发生梯度爆炸现象。
这就是有时候我们在训练网络的时候,最后结果全是nan的原因,这往往可能是权重初始化的不当导致的
为啥初始化权重不当了会影响到网络的输出呢? 刚才不是还说是网络的输出影响的权重梯度吗? 那是反向传播的时候, 而正向传播的时候,权重肯定要影响到每一层的输出啊。 推导一下正向传播中每一层输出的方差是如何变化的。
看第一层第一个神经元的方差
输入数据和权重都初始化的均值为0,方差为1的标准正态。 这样经过一个网络层就发现方差扩大了n倍。所以用了100个网络层, 那么这个方差会指数增长,所以才会出现输出层方差nan的情况。
每一层的输出方差会和每一层神经元个数,前一层输出方差和本层权重的方差有关,如果想让方差的尺度不变,即
D
(
H
11
)
=
1
D(H_{11})=1
D(H11)=1,可以对权重进行初始化。
每一层的输出方差都是1, 这样方差就不会导致nan的情况发生了。在上面代码中改一句话:
def initialize(self):
# 遍历模型的子模块
for m in self.modules():
if isinstance(m, nn.Linear):
nn.init.normal_(m.weight.data, std=np.sqrt(1/self.neural_num)) # 把权重方差改了
所以我们只要采用恰当的权值初始化方法,就可以实现多层神经网络的输出值的尺度维持在一定范围内, 这样在反向传播的时候,就有利于缓解梯度消失或者爆炸现象的发生。
上面的网络只是一个线性网络,在实际中我们还得考虑激活函数的存在,我们从上面的前向传播中加一个激活函数后,发现输出层的方差越来越小,随着层数的增加,最后可能会出现梯度消失。
那么,具有激活函数的时候,怎么对权重进行初始化呢?
Xavier初始化
方差一致性:保持数据尺度范围维持在恰当范围, 通常方差为1。
如果有了激活函数之后,我们应该怎么对权重初始化呢?
2010年Xavier发表了一篇文章,详细探讨了如果有激活函数的时候,如何进行权重初始化, 当然它也是运用的方差一致性原则, 但是它这里考虑的是饱和激活函数, 如sigmoid, tanh
文章中有个这样的公式推导,从而得到我们权重的方差:
这里的
n
i
n_{i}
ni 、
n
i
+
1
n_{i+1}
ni+1分别指的输入层和输出层神经元个数。通常Xavier采用均匀分布对权重进行初始化,那么我们可以推导一下均匀分布的上限和下限:
Xavier初始化方法代码,在参数初始化里面用Xavier初始化权重
def initialize(self):
for m in self.modules():
if isinstance(m, nn.Linear):
# Xavier初始化权重
tanh_gain = nn.init.calculate_gain('tanh')
nn.init.xavier_uniform_(m.weight.data, gain=tanh_gain)
这里面用到了一个函数nn.init.calculate_gain(nonlinearity, param=None)
这个函数的作用是计算激活函数的方差变化尺度, 怎么理解这个方差变化尺度呢?其实就是输入数据的方差除以经过激活函数之后的输出数据的方差。nonlinearity表示激活函数的名称,如tanh, param表示激活函数的参数,如Leaky ReLU的negative_slop
。
Xavier权重初始化,有利于缓解带有sigmoid,tanh的这样的饱和激活函数的神经网络的梯度消失和爆炸现象。
Kaiming初始化
依然是考虑的方差一致性原则,针对的激活函数是ReLU及其变种。经过公式推导,最后的权值标准差
没看懂!!! 第二个公式下面加
n
i
n_{i}
ni应该是相乘。
Kaiming初始化权重方法代码
def initialize(self):
for m in self.modules():
if isinstance(m, nn.Linear):
nn.init.kaiming_normal_(m.weight.data)
# nn.init.normal_(m.weight.data, std=np.sqrt(2 / self.neural_num)) # 这两句话其实作用一样,不过自己写还得计算出标准差
我们对权值的初始化有了一些认识,发现了权重初始化对于模型的重要性,不好的权重初始化方法会引起输出层的输出值过大过小,从而引发梯度的消失或者爆炸,最终导致我们的模型无法训练。所以我们如果想缓解这种现象,就得控制输出层的值的范围尺度,就得采取合理的权重初始化方法。
其他权重初始化方法
Pytorch里面提供了很多权重初始化的方法,可以分为下面的四大类:
- 针对饱和激活函数(sigmoid, tanh):Xavier均匀分布, Xavier正态分布
- 针对非饱和激活函数(relu及变种):Kaiming均匀分布, Kaiming正态分布
- 三个常用的分布初始化方法:均匀分布,正态分布,常数分布
- 三个特殊的矩阵初始化方法:正交矩阵初始化,单位矩阵初始化,稀疏矩阵初始化
好了,到了这里,模型模块才算得上结束, 下面我们就进行下一个模块的学习,损失函数模块,在这里面学习各种损失函数的原理及应用场景。
损失函数
机器模型学习的五大模块,分别是数据,模型,损失函数,优化器,迭代训练
. 下面学习损失函数模块,在这里面学习各种损失函数的原理及应用场景。
损失函数初步介绍
损失函数: 衡量模型输出与真实标签的差异。而我们谈损失函数的时候,往往会有三个概念: 损失函数, 代价函数, 目标函数。 你知道这仨到底啥区别吗?
我们一般都是在衡量模型输出和真实标签的差异的时候,往往都直接说成损失函数。
我们发现原来_Loss
也是继承于Module
,这个在模型创建的时候就已经很熟悉了,也具体介绍过, 既然_Loss
也是继承于这个类,那么就得先想起来肯定_Loss
也有那8个参数字典了,然后这里面是设置一个reduction这个参数。
损失函数的初始化和使用
损失函数的初始化方法和模型其实类似,也是调用Module的初始化方法,最终会有8个属性字典, 然后就是设置了一个reduction这个参数。
发现了损失函数其实也是一个Module, 使用的方法依然是定义在了forward函数中。
交叉熵损失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()
把模型的输出值归一化成了概率分布的形式,然后是单个样本的输出,并且没有求和符号。
具体的下面会解释,但是解释之前,得先明白一个问题,就是为什么交叉熵可以衡量两个分布的差异,这个到底是个什么东西? 这就不得不提到相对熵, 而想了解相对熵,就得先明白熵的概念,而如果想明白熵,就得先知道自信息,可以参考博客原文,或者我写的另一篇关于熵的基础知识。
交叉熵=相对熵(又称为KL散度)+信息熵,即
在机器学习模型中,我们最小化交叉熵,其实就是最小化相对熵,因为我们训练集取出来之后就是固定的了,熵就是一个常数。
好了,我们已经知道了交叉熵是衡量两个分布之间的距离,一个差异。所以这里使用softmax,就可以将一个输出值转换到概率取值的一个范围。我们看看这里的交叉熵损失函数是怎么计算的:
这里的x就是我们输出的概率值,class就是某一个类别,在括号里面执行了一个softmax,把某个神经元的输出归一化成了概率取值,然后-log一下,就得到了交叉熵损失函数.
对比一下我们的交叉熵公式:
由于是某个样本,那么P ( x i )已经是1了,毕竟取出来了已经。 而是某个样本,所以也不用求和符号。 发现这么说反而不好理解,还是直接回归本身即可。 就看交叉熵损失函数前面的公式。loss(x, class)=-log(Q(x_i)), 相比较于交叉熵公式,只是对于某个样本进行的计算交叉熵。
这就是用softmax的原因了,把模型的输出值转成概率分布的形式,这样就得到了交叉熵损失函数。
这里就可以说一说那些参数的作用了, 第一个参数weight, 各类别的loss设置权值, 如果类别不均衡的时候这个参数很有必要了,加了之后损失函数变成这样
就是如果我们想让模型更关注某一类的话,就可以把这一类的权值设置的大一点。第二个参数ignore_index, 这个是表示某个类别不去计算loss。而关于第三个参数reduction, 有三个计算模式none/sum/mean, 上面已经说了,下面我们从代码中看看这三个的区别:
# fake data
inputs = torch.tensor([[1, 2], [1, 3], [1, 3]], dtype=torch.float) # 这里就是模型预测的输出, 这里是两个类,可以看到模型输出是数值,我们得softmax一下转成分布
target = torch.tensor([0, 1, 1], dtype=torch.long) # 这里的类型必须是long, 两个类0和1
# 三种模式的损失函数
loss_f_none = nn.CrossEntropyLoss(weight=None, reduction='none')
loss_f_sum = nn.CrossEntropyLoss(weight=None, reduction='sum')
loss_f_mean = nn.CrossEntropyLoss(weight=None, reduction='mean')
# forward
loss_none = loss_f_none(inputs, target)
loss_sum = loss_f_sum(inputs, target)
loss_mean = loss_f_mean(inputs, target)
# view
print("Cross Entropy Loss:\n ", loss_none, loss_sum, loss_mean)
## 结果:
Cross Entropy Loss:
tensor([1.3133, 0.1269, 0.1269]) tensor(1.5671) tensor(0.5224)
再通过代码看看加上weight的损失
给类别加上权值之后,对应样本的损失就会相应的加倍,这里重点是了解一下这个加上权之后,mean模式下怎么计算的损失: 其实也很简单,我们三个样本,第一个权值为1, 后两个权值为2, 所以分母不再是3个样本,而是1+2+2, 毕竟后两个样本权为2, 一个样本顶第一个的这样的2个。所以mean模式下求平均不是除以样本的个数,而是样本所占的权值的总份数。
几个交叉熵损失函数的特例
1、nn.NLLLoss
实现负对数似然函数里面的负号功能
这个损失函数,就是根据真实类别去获得相应的softmax之后的概率结果,然后取反就是最终的损失
2、nn.BCELoss
这个是交叉熵损失函数的特例,二分类交叉熵。这用于测量例如自动编码器中的重构的误差。注意:输入值取值在[0,1]
这里的参数和上面的一样,计算公式如下:
这里首先注意的点就是target, 这里可以发现和交叉熵那里的标签就不一样了,首先是类型是float, 每个样本属于哪一类的时候要写成独热的那种形式,这是因为看损失函数的计算公式也能看到,每个神经元一一对应的去计算loss,而不是一个整的神经元向量去计算loss。
3、nn.BCEWithLogitsLoss
这个函数结合了Sigmoid与二分类交叉熵,注意事项: 网络最后不加sigmoid函数
这里的参数多了一个pow_weight
, 这个是平衡正负样本的权值用的, 对正样本进行一个权值设定。比如我们正样本有100个,负样本有300个,那么这个数可以设置为3,在类别不平衡的时候可以用。
计算公式如下:
其他损失函数
其他损失函数可根据任务分类选择损失函数,具体可查阅官方文档。
https://pytorch.org/docs/stable/nn.html#loss-functions
- 分类问题
- 二分类单标签问题: nn.BCELoss, nn.BCEWithLogitsLoss, nn.SoftMarginLoss
- 二分类多标签问题:nn.MultiLabelSoftMarginLoss
- 多分类单标签问题: nn.CrossEntropyLoss, nn.NLLLoss, nn.MultiMarginLoss
- 多分类多标签问题: nn.MultiLabelMarginLoss,
- 不常用:nn.PoissonNLLLoss, nn.KLDivLoss
- 回归问题: nn.L1Loss, nn.MSELoss, nn.SmoothL1Loss
- 时序问题:nn.CTCLoss
- 人脸识别问题:nn.TripletMarginLoss
- 半监督Embedding问题(输入之间的相似性): nn.MarginRankingLoss, nn.HingeEmbeddingLoss, nn.CosineEmbeddingLoss
总结
今天的内容主要是分为2大块, 第一块就是模型模块的收尾部分,解决了模型模块的小尾巴, 权重的初始化方法,学习了梯度消失和梯度爆炸的原理,权重初始化的重要性,针对各种情况学习了不同的初始化方法,重要的是Xavier初始化和Kaiming初始化方法, 分别针对非饱和激活函数和包含激活函数的网络。
然后学习了损失函数的相关知识,通过损失函数的初步介绍,我们知道了损失函数也是一个Module,那么初始化和运行机制就基本了解。 然后学习了交叉熵损失函数及特例。
关于Pytorch的损失函数模块,到这里就基本结束, 我们的逻辑就是按照机器学习的那五大步骤进行的查看, 数据模块 -> 模型模块 -> 损失函数 -> 优化器 -> 训练
等,后面进入优化器部分。
参考:
[1]: https://zhongqiang.blog.csdn.net/article/details/105590118
[2]: https://pytorch.org/docs/stable/torch.html
[3]: https://blog.csdn.net/weixin_45653897/article/details/130195505