pytorch8——模型权值初始化和损失函数

1. 权值初始化

        正确的权重初始化非常重要,可以影响模型的训练速度和效果。如果初始化到最优解附近,模型训练速度就会很快,否则就需要更多次迭代,并且可能引发梯度消失和爆炸现象。

1.1  梯度消失和梯度爆炸

        梯度消失和爆炸现象是深度神经网络中常见的问题。梯度消失指的是在反向传播过程中,由于梯度值太小而导致无法更新神经网络的参数;梯度爆炸则是指梯度值过大,导致网络权重更新过于剧烈,甚至变得不稳定。这些问题都可能导致模型无法收敛或者收敛缓慢。

      因为梯度连乘,如果上一层神经元的输出值非常小,那么在反向传播过程中,梯度也会变得非常小,从而导致梯度消失的问题。而如果上一层神经元的输出值非常大,梯度也会变得非常大,导致梯度爆炸的问题。

        一旦发生梯度消失或者爆炸, 就会导致模型无法训练,为了避免这些问题,我们就得控制网络输出层的一个尺度范围,也就是不能让它太大或者太小

        我们建立一个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)

        从结果来看,31层的时候,输出成nan,我们还没有反向传播, 根据上面的权重推导的公式,后面的这些如果为nan了之后,反向传播的时候,这些权重根本就没法进行更新,会发生梯度爆炸现象。这就是有时候我们在训练网络的时候,最后结果全是nan的原因,这往往可能是权重初始化的不当导致的。 我们推导一下上面这个过程中每一层输出的方差是如何变化的就明白了。

E ( X ) = 0 , E ( Y ) = 0,则D(XY) = D(X)D(Y)。那么神经网络里面每一层输出的方差计算,第一个神经元的方差计算为:

H_{11} = \sum_{i=0}^{n} X_i \times W_{1i}

D(H_{11}) = \sum_{i=0}^{n} D(X_i) \times D(W_{1i})= n \times (1 \times 1) = n

\text{std}(H_{11}) = \sqrt{D(H_{11})} = \sqrt{n}

这里我们的输入数据和权重都初始化的均值为0,方差为1的标准正态。 这样经过一个网络层就发现方差扩大了n 倍。 而我们上面用了100个网络层, 那么这个方差会指数增长,所以我们后面才会出现输出层方差nan的情况。

那么我们怎么解决这种情况呢? 那很简单,让网络层的输出方差保持尺度不变就可以了, 可是怎么做呢? 我们发现每一层输出方差会和每一层神经元个数,

,前一层输出方差和本层权重的方差有关,如果想让方差的尺度不变,因为这里都是连乘,有个方法就是让每一层输出方差都是1, 因为神经元个数变不了,所以将权重的方差改为D(W)={\frac{1}{n}},标准差为std(W)=\sqrt{\frac{1}{n}}。所以只需要将上面代码改为:

    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))         # 把权重方差改了

这样就不会出现nan的现象,并且方差变化的尺度是1,所以推导正确:

这个例子只有线性操作,那么若加上激活函数会怎样?加入tanh()激活函数如下:

    def forward(self, x):
        for (i, linear) in enumerate(self.linears):
            x = linear(x)
            x = torch.tanh(x)
            print("layer:{}, std:{}".format(i, x.std()))
            if torch.isnan(x.std()):
                print("output is nan in {} layers".format(i))
                break
        return x

结果发现方差越来愈小,最后可能梯度消失。

那么如何对权重初始化?

1.2 Xavier初始化(sigmoid,tanh)

方差一致性:保持数据尺度范围维持在恰当范围, 通常方差为1。 

对于饱和激活函数, 如sigmoid, tanh。在论文Xavier中,我们权重方差:

n_i * D(W)=1\\ n_{i+1} * D(W)=1\\ D(W)=\frac{2}{n_{i}+n_{i+1}}

这里的n _in_{i+1}分别指的输入层和输出层神经元个数。通常Xavier采用均匀分布对权重进行初始化,那么我们可以推导一下均匀分布的上限和下限。

W - U[-a,a]\\ D(W) = \frac{(-a-a)^2}{12}=\frac{a^2}{3}

让上面的两个D ( W ) 相等就会得到:

\frac{2}{n_{i}+n_{i+1}} =\frac{a^2}{3} => a=\frac{ \sqrt{6}}{\sqrt{n_{i}+n_{i+1}}}\\ W~U[\frac{- \sqrt{6}}{\sqrt{n_{i}+n_{i+1}}}, \frac{ \sqrt{6}}{\sqrt{n_{i}+n_{i+1}}}]

如何在代码中实现?

    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。 (这里不用也行,但得知道这个方法)。结果为:

1.3 Kaiming初始化(ReLU系列

这个依然是考虑的方差一致性原则,针对的激活函数是ReLU及其变种。经过公示推导,最后的权值标准差是这样的:

D(W)=\frac{2}{n_{i}}\\ D(W)=\frac{2}{(1+a^2)+n_{i}}\\ std(W) = \sqrt \frac{2}{(1+a^2)+n_{i}}\\

代码如下:

    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))     # 这两句话其实作用一样,不过自己写还得计算出标准差

结果可以看出标准差稳定在0.5左右

        我们了解到权重初始化对于模型训练的重要性。不好的权重初始化方法会导致输出层的输出值过大或过小,引发梯度消失或爆炸,从而无法训练模型。为了缓解这种现象,我们需要控制输出层的值的范围尺度,采取合理的权重初始化方法。常见的方法包括:Xavier初始化、Kaiming初始化和均匀分布初始化等。正确的权重初始化方法可以提高模型的训练效果和稳定性,从而获得更好的性能。

1.4 总结

Pytorch里面提供了很多权重初始化的方法,可以分为下面的四大类:

针对饱和激活函数(sigmoid, tanh):Xavier均匀分布, Xavier正态分布

torch.nn.init.xavier_uniform_(tensor: torch.Tensor, gain: float = 1.0) → torch.Tensor
torch.nn.init.xavier_normal_(tensor, gain=1.0)

针对非饱和激活函数(relu及变种):Kaiming均匀分布, Kaiming正态分布

torch.nn.init.kaiming_uniform_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu')
torch.nn.init.kaiming_normal_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu')

三个常用的分布初始化方法:均匀分布,正态分布,常数分布。

#均匀分布初始化
torch.nn.init.uniform_(tensor, a=0.0, b=1.0)
# 正态分布初始化
torch.nn.init.normal_(tensor, a=0.0, b=1.0)
# 常数初始化
torch.nn.init.constant_(tensor, val)

三个特殊的矩阵初始化方法:0初始化,正交矩阵初始化,单位矩阵初始化。

#0初始化
torch.nn.init.zeros_(tensor)
#单位矩阵初始化
torch.nn.init.eye_(tensor)
# 正交初始化
torch.nn.init.orthogonal_(tensor, gain=1)

2. 损失函数

损失函数: 衡量模型输出与真实标签的差异。 损失函数, 代价函数, 目标函数的区别是什么?

  • Loss Function: 计算一个样本的一个差异。 L o s s = f ( \widehat{y} , y )
  • Cost Function: 计算整个训练集Loss的一个平均值。cos t = \frac{1}{N}\sum {f( \widehat{y_i}, y_i )}
  • Objective Function: 这是一个更广泛的概念,在机器学习模型训练中,这是最终的一个目标,过拟合和欠拟合之间进行一个权衡。Obj=Cost+Regularization

2.1 交叉熵损失CrossEntropyLoss

torch.nn.CrossEntropyLoss(weight=Nonesize_average=Noneignore_index=-100reduce=Nonereduction='mean'label_smoothing=0.0)

  • weight:各类别的loss设置权值, 如果类别不均衡的时候这个参数很有必要了。
  • ignore_index: 忽略某个类别。
  • reduction: 计算模式,可为none/sum/mean, none表示逐个元素计算,这样有多少个样本就会返回多少个loss。 sum表示所有元素的loss求和,返回标量, mean所有元素的loss求加权平均(加权平均的含义下面会提到),返回标量。

        交叉熵是衡量两个分布之间的距离,所以在使用交叉熵之前需要通过softmax将输出值转换到概率取值的一个范围。

H (p,q) = - \sum_{i=1}^{n} p(x_i)logq(x_i),将 p(x_i) 设为一个独热向量,表示目标类别的真实概率分布。由于只有一个类别的概率为 1,其他类别的概率都为 0,所以可以将交叉熵公式简化为:

H (p,q) = - p(class)logq(class), 因为p(class)=1,将softmax代入公式,得:

loss(x, class) = - log(\frac{exp(x[class])} {\sum_{j} {exp({x[j])}}})=-x[class]+log(\sum_{j}exp(x[j]))

利用代码实现以上操作如下,对比结果相同:

import torch
import torch.nn as nn
import math

loss = nn.CrossEntropyLoss()
input = torch.randn(1, 5, requires_grad= True)
target = torch.empty(1, dtype = torch.long).random_(5)
output = loss(input, target)

print('输入为5类:', input,'要计算loss的真实类别', target)
print('pytorch计算loss:', output)

#自己计算的结果
x_class = sum_exp = loss = 0
x_class -= input[0][target[0]]
for i in range(5):
    sum_exp += math.exp(input[0][i])
loss = 0
loss += x_class + math.log(sum_exp)
print('自己计算的loss:', loss)

结果如下:

输入为5类: tensor([[ 0.4899,  0.1627, -0.8963,  1.1090, -0.3719]], requires_grad=True) 要计算loss的真实类别 tensor([3])
pytorch计算loss: tensor(0.8280, grad_fn=<NllLossBackward>)
自己计算的loss: tensor(0.8280, grad_fn=<AddBackward0>)

2.2 交叉熵损失特例

2.2.1 nn.NLLoss

        上面的交叉熵损失是softmax和NLLoss的组合。nn.NLLoss是实现负对数似然函数里面的负号功能, 结果相同,如下:

m = nn.LogSoftmax(dim=1)
loss = nn.NLLLoss()
# input is of size N x C = 3 x 5
inputs = torch.randn(3, 5, requires_grad=True)
# each element in target has to have 0 <= value < C
target = torch.tensor([1, 0, 4])
output = loss(m(inputs), target)
print(output)

loss_cross = nn.CrossEntropyLoss()
output2 = loss_cross(inputs, target)
print(output2)

2.2.2 nn.BCELos

torch.nn.BCELoss(weight=Nonesize_average=Nonereduce=Nonereduction='mean')

二分类交叉熵,输入值取值在[0,1]。公式如下:

l_n = - w_n \left[ y_n \cdot \log x_n + (1 - y_n) \cdot \log (1 - x_n) \right]

这里用sigmoid进行转化0-1之间。

m = nn.Sigmoid()
loss = nn.BCELoss()
inputs = torch.randn(3, 2, requires_grad=True)
target = torch.rand(3, 2, requires_grad=False)
output = loss(m(inputs), target)
output.backward()

2.2.3 BCEWithLogitsLoss

torch.nn.BCEWithLogitsLoss(weight=Nonesize_average=Nonereduce=Nonereduction='mean'pos_weight=None)

这个结合了sigmoid和二分类交叉熵,参数多了一个pow_weight,这个是平衡正负样本的权值用的, 对正样本进行一个权值设定。比如我们正样本有100个,负样本有300个,那么这个数可以设置为3,在类别不平衡的时候可以用。相当于将上面的合成一步完成。

loss = nn.BCEWithLogitsLoss()
inputs = torch.randn(3, requires_grad=True)
target = torch.empty(3).random_(2)
output = loss(inputs, target)

m = nn.Sigmoid()
loss = nn.BCELoss()
output2 = loss(m(inputs), target)
output, output2
(tensor(0.7211, grad_fn=<BinaryCrossEntropyWithLogitsBackward>),
 tensor(0.7211, grad_fn=<BinaryCrossEntropyBackward>))

2.3 其他损失

  • 分类问题

二分类单标签问题: 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

参考文章:

pytorch学习笔记十:权值初始化的十种方法_nn.conv2d 零初始化-CSDN博客

torch.nn.init — PyTorch 2.1 documentation

https://zhongqiang.blog.csdn.net/article/details/105590118?ydreferer=aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d1emhvbmdxaWFuZy9jYXRlZ29yeV8xMDAyNDUzOC5odG1s?ydreferer=aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d1emhvbmdxaWFuZy9jYXRlZ29yeV8xMDAyNDUzOC5odG1s

  • 11
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值