写点什么

PyTorch: 权值初始化

作者:timerring
  • 2023-07-16
    山东
  • 本文字数:4297 字

    阅读完需:约 14 分钟

文章和代码已经归档至【Github 仓库:https://github.com/timerring/dive-into-AI 】或者公众号【AIShareLab】回复 pytorch 教程 也可获取。

Pytorch:权值初始化

在搭建好网络模型之后,首先需要对网络模型中的权值进行初始化。权值初始化的作用有很多,通常,一个好的权值初始化将会加快模型的收敛,而比较差的权值初始化将会引发梯度爆炸或者梯度消失。下面将具体解释其中的原因:

梯度消失与梯度爆炸

考虑一个 3 层的全连接网络。


,如下图所示,



其中第 2 层的权重梯度如下:


$\begin{array}{l}\mathrm{H}{2}=\mathrm{H}{1} * \mathrm{~W}{\mathbf{2}} \\Delta \mathrm{W}{\mathbf{2}}=\frac{\partial \text { Loss }}{\partial \mathrm{W}{2}}=\frac{\partial \mathrm{Loss}}{\partial \text { out }} * \frac{\partial \text { out }}{\partial \mathrm{H}{2}} * \frac{\partial \mathrm{H}{2}}{\partial \mathrm{W}{2}} \ =\frac{\partial \text { Loss }}{\partial \text { out }} * \frac{\partial \text { out }}{\partial \mathrm{H}{2}} * \mathrm{H}{1} \\end{array}$


由上式化简可知,如果 H_1 发生以下变化,那么对应的梯度也就会发生变化:


  • 梯度消失: $\mathrm{H}{1} \rightarrow 0 \Rightarrow \Delta \mathrm{W}{2} \rightarrow 0$

  • 梯度爆炸: $\mathrm{H}{1} \rightarrow \infty \Rightarrow \Delta \mathrm{W}{2} \rightarrow \infty $


因此,为了避免以上两种情况,就必须严格控制网络层输出的数值范围。


具体可以通过构建 100 层全连接网络,先不使用非线性激活函数,每层的权重初始化为服从 的正态分布,输出数据使用随机初始化的数据,这样的例子来直观地感受影响:


import torchimport torch.nn as nnfrom common_tools import set_seed
set_seed(1) # 设置随机种子
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) 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 = 256batch_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)
复制代码


输出为:


tensor([[nan, nan, nan,  ..., nan, nan, nan],        [nan, nan, nan,  ..., nan, nan, nan],        [nan, nan, nan,  ..., nan, nan, nan],        ...,        [nan, nan, nan,  ..., nan, nan, nan],        [nan, nan, nan,  ..., nan, nan, nan],        [nan, nan, nan,  ..., nan, nan, nan]], grad_fn=<MmBackward>)
复制代码


通过输出可知,输出值均为 nan,即非数字类型,原因可能是数据太大(梯度爆炸)或者太小(梯度消失)。


为了具体知道是在哪一层开始出现 nan 的,我们可以在 forward 函数中添加判断得知,查看每一次前向转播的标准差是否是 nan,若是,则停止前向传播并输出。


这里判断是否为 nan 时采用了 torch.isnan 函数


    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
复制代码


输出如下:


layer:0, std:15.959932327270508layer:1, std:256.6237487792969layer:2, std:4107.24560546875...layer:29, std:1.322983152787379e+36layer:30, std:2.0786820453988485e+37layer:31, std:nanoutput is nan in 31 layers
复制代码


可见,之际上输出的标准差是逐层递增的,具体为什么会导致这种情况:


  • :两个相互独立的随机变量的乘积的期望等于它们的期望的乘积。

  • :一个随机变量的方差等于它的平方的期望减去期望的平方

  • :两个相互独立的随机变量之和的方差等于它们的方差的和。


可以推导出两个随机变量的乘积的方差如下:


又由于输入变量是符合标准的正态分布的,因此 ,可知


我们以输入层第一个神经元为例:


$\mathrm{H}{11}=\sum{i=0}^{n} X_{i} * W_{1 i}$


其中输入 X 和权值 W 都是服从 的正态分布,且由公式, 因此这个神经元的方差为:


$\begin{aligned}\mathbf{D}\left(\mathrm{H}{11}\right) &=\sum{i=0}^{n} D\left(X_{i}\right) * D\left(W_{1 i}\right) \&=n *(1 * 1) \&=n\end{aligned}$


可以求其标准差:$\operatorname{std}\left(\mathrm{H}{11}\right)=\sqrt{\mathrm{D}\left(\mathrm{H}{11}\right)}=\sqrt{n}$


可见,经过第一层网络,方差就会扩大 n 倍,标准差就扩大 倍,n 为每层神经元个数,直到超出数值表示范围。


从前面的输出中也可以看出来,n = 256,因此每一层的标准差输出都是 16 倍。再由公式可知,每一层网络输出的方差与神经元个数、输入数据的方差、权值方差有关(见上式),通过观察可知,比较好改变的是权值的方差 ,要控制每一层输出的方差仍然为 1 左右,因此需要 ,可知标准差为 。因此修改权值初始化代码为nn.init.normal_(m.weight.data, std=np.sqrt(1/self.neural_num))


再次输出时,结果如下:


layer:0, std:0.9974957704544067layer:1, std:1.0024365186691284layer:2, std:1.002745509147644...layer:94, std:1.031973123550415layer:95, std:1.0413124561309814layer:96, std:1.0817031860351562
复制代码


修改之后,没有出现梯度消失或者梯度爆炸的情况,每层神经元输出的方差均在 1 左右。通过恰当的权值初始化,可以保持权值在更新过程中维持在一定范围之内


但是上述的实验前提为未使用非线性函数的前提下,如果在forward()中添加非线性变换例如tanh,每一层的输出方差会越来越小,会导致梯度消失。


为了解决这个问题,进一步有了著名的 Xavier 初始化与 Kaiming 初始化。

Xavier 方法与 Kaiming 方法

Xavier 方法

Xavier 是 2010 年提出的,针对有非线性激活函数时的权值初始化方法。


  • 目标是保持数据的方差维持在 1 左右

  • 针对饱和激活函数如 sigmoid 和 tanh 等。


同时考虑前向传播和反向传播,需要满足两个等式


$\begin{array}{l}\boldsymbol{n}{\boldsymbol{i}} * \boldsymbol{D}(\boldsymbol{W})=\mathbf{1} \\boldsymbol{n}{\boldsymbol{i}+\mathbf{1}} * \boldsymbol{D}(\boldsymbol{W})=\mathbf{1} \\end{array}$


通过计算可知:


为了使 Xavier 方法初始化的权值服从均匀分布,假设 服从均匀分布 ,那么方差 ,令 ,解得:,所以 服从分布


所以初始化方法改为:


a = np.sqrt(6 / (self.neural_num + self.neural_num))# 把 a 变换到 tanh,计算增益tanh_gain = nn.init.calculate_gain('tanh')a *= tanh_gain
nn.init.uniform_(m.weight.data, -a, a)
复制代码


并且每一层的激活函数都使用 tanh,输出如下:


layer:0, std:0.7571136355400085layer:1, std:0.6924336552619934layer:2, std:0.6677976846694946...layer:97, std:0.6426210403442383layer:98, std:0.6407480835914612layer:99, std:0.6442216038703918
复制代码


可以看到每层输出的方差都维持在 0.6 左右。


也可以直接调用 PyTorch 中 Xavier 初始化方法:


tanh_gain = nn.init.calculate_gain('tanh')nn.init.xavier_uniform_(m.weight.data, gain=tanh_gain)
复制代码

nn.init.calculate_gain()

这里重点介绍一下nn.init.calculate_gain(nonlinearity,param=**None**)方法。


主要功能是经过一个分布的方差经过激活函数后的变化尺度,主要有两个参数:


  • nonlinearity:激活函数名称

  • param:激活函数的参数,如 Leaky ReLU 的 negative_slop 等等。


下面是计算标准差经过激活函数的变化尺度的代码。


x = torch.randn(10000)out = torch.tanh(x)# 计算变化尺度(也可以称为变化倍数)gain = x.std() / out.std()print('gain:{}'.format(gain))
tanh_gain = nn.init.calculate_gain('tanh')print('tanh_gain in PyTorch:', tanh_gain)
复制代码


输出如下:


gain:1.5982500314712524tanh_gain in PyTorch: 1.6666666666666667
复制代码


结果表示,原有数据分布的方差经过 tanh 之后,标准差会变小 1.6 倍左右。

Kaiming 方法

虽然 Xavier 方法提出了针对饱和激活函数的权值初始化方法,但是 AlexNet 出现后,大量网络开始使用非饱和的激活函数如 ReLU 等,这时 Xavier 方法不再适用。2015 年针对 ReLU 及其变种等激活函数提出了 Kaiming 初始化方法。


针对 ReLU,方差应该满足:


针对 ReLu 的变种,方差应该满足:,a 表示负半轴的斜率,如 PReLU 方法,标准差满足


代码如下:nn.init.normal_(m.weight.data, std=np.sqrt(2 / self.neural_num)),或者使用 PyTorch 提供的初始化方法:nn.init.kaiming_normal_(m.weight.data)

常用初始化方法

PyTorch 中提供了 10 中初始化方法


  1. Xavier 均匀分布

  2. Xavier 正态分布

  3. Kaiming 均匀分布

  4. Kaiming 正态分布

  5. 均匀分布

  6. 正态分布

  7. 常数分布

  8. 正交矩阵初始化

  9. 单位矩阵初始化

  10. 稀疏矩阵初始化


综上, 常用初始化的目标就是要保证每一层输出的方差不能太大,也不能太小,维持在一个稳定的范围内。

发布于: 刚刚阅读数: 5
用户头像

timerring

关注

公众号【AIShareLab】 2022-07-14 加入

他日若遂凌云志

评论

发布
暂无评论
PyTorch: 权值初始化_PyTorch_timerring_InfoQ写作社区