深度之眼 PyTorch 训练营第 4 期 (7):torch.nn.init

15 篇文章 1 订阅
15 篇文章 3 订阅

1. torch.nn.init 概述

因为神经网络的训练过程其实是寻找最优解的过程,所以神经元的初始值非常重要。如果初始值恰好在最优解附近,神经网络的训练会非常简单。而当神经网络的层数增加以后,一个突出的问题就是梯度消失和梯度爆炸。前者指的是由于梯度接近 0,导致神经元无法进行更新;后者指的是误差梯度在更新中累积得到一个非常大的梯度,这样的梯度会大幅度更新网络参数,进而导致网络不稳定

torch.nn.init 模块提供了合理初始化初始值的方法。它一共提供了四类初始化方法:

  1. Xavier 分布初始化;
  2. Kaiming 分布初始化;
  3. 均匀分布、正态分布、常数分布初始化;
  4. 其它初始化。

有梯度边界的激活函数如 sigmoidtanhsoftmax 等被称为饱和函数,没有梯度边界的激活函数如 relu 被称为不饱和函数,它们对应的初始化方法不同。

2. 梯度消失和梯度爆炸

假设我们有一个 3 层的全连接网络:
在这里插入图片描述
对倒数第二层神经元的权重进行反向传播的公式为:
Δ W 3 = ∂ l o s s ∂ W 3 = ∂ l o s s ∂ o u t ∗ ∂ o u t ∂ H 3 ∗ ∂ H 3 ∂ W 3 \Delta W_3=\frac{\partial loss}{\partial W_3}=\frac{\partial loss}{\partial out}*\frac{\partial out}{\partial H_3}*\frac{\partial H_3}{\partial W_3} ΔW3=W3loss=outlossH3outW3H3
H 3 = H 2 ∗ W 3 H_3=H_2*W_3 H3=H2W3,所以
Δ W 3 = ∂ l o s s ∂ o u t ∗ ∂ o u t ∂ H 3 ∗ H 2 \Delta W_3=\frac{\partial loss}{\partial out}*\frac{\partial out}{\partial H_3}*H_2 ΔW3=outlossH3outH2
H 2 H_2 H2 ,即上一层的神经元的输出值,决定了 Δ W 3 \Delta W_3 ΔW3 的大小。如果 H 2 H_2 H2 太大或太小,即梯度消失或梯度爆炸,将导致神经网络无法训练。对于 sigmoidtanh 等梯度绝对值小于 1 的激活函数来说,神经元的值会越来越小;对于其它情况,假设我们构建了一个 100 层的全连接网络:

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 _ 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 init(self):
        for m in self.modules():
            if isinstance(m, nn.Linear):
                nn.init.normal_(m.weight.data)

layers=100
neural_num=256
batch_size=16

net = MLP(neural_num, layers)
net.init()

inputs = torch.randn(batch_size, neural_num)
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。这是为什么呢?

因为方差可以表征数据的离散程度,让我们来打印一下每次神经元的值的方差:

layers: 0, std: 15.7603178024292
layers: 1, std: 253.5698699951172
layers: 2, std: 4018.8212890625
layers: 3, std: 64962.9453125
layers: 4, std: 1050192.125
layers: 5, std: 16682177.0
...
layers: 28, std: 8.295319341711625e+34
layers: 29, std: 1.2787049888311946e+36
layers: 30, std: 2.0164275976565801e+37
layers: 31, std: nan

output is nan at 31th layers

tensor([[ 1.3354e+38, -2.0165e+38, -3.2402e+37,  ...,  1.0439e+37,
                -inf,  1.2574e+38],
        [       -inf,        -inf,         inf,  ...,        -inf,
                -inf,         inf],
        [ 1.2230e+37,        -inf,  5.6356e+37,  ..., -1.2776e+38,
                 inf,        -inf],
        ...,
        [ 2.1591e+37,  2.5838e+38, -2.9146e+38,  ...,         inf,
                -inf,        -inf],
        [        inf,  1.9056e+38,        -inf,  ...,         inf,
                -inf,        -inf],
        [       -inf,         inf, -1.7735e+38,  ...,  4.8110e+37,
                 inf,        -inf]], grad_fn=<MmBackward>)

可以看到,到第 30 层的时候,神经元的值已经非常大或非常小,终于在第 31 层的时候,神经元的值突破了存储精度的极限,只好变成了 nan

我们知道,一组数的方差 D D D 和 期望 E E E X X X Y Y Y 相互独立的条件下满足下面的性质:
E ( X ∗ Y ) = E ( X ) ∗ E ( Y ) E(X*Y)=E(X)*E(Y) E(XY)=E(X)E(Y)
D ( X ) = E ( X 2 ) − [ E ( X ) ] 2 D(X)=E(X^2)-[E(X)]^2 D(X)=E(X2)[E(X)]2
D ( X + Y ) = D ( X ) + D ( Y ) D(X+Y)=D(X)+D(Y) D(X+Y)=D(X)+D(Y)
所以有:
D ( X ∗ Y ) = D ( X ) ∗ D ( Y ) + D ( X ) ∗ [ E ( Y ) ] 2 + D ( Y ) ∗ [ E ( X ) ] 2 D(X*Y)=D(X)*D(Y)+D(X)*[E(Y)]^2+D(Y)*[E(X)]^2 D(XY)=D(X)D(Y)+D(X)[E(Y)]2+D(Y)[E(X)]2
E ( X ) = 0 E(X)=0 E(X)=0 E ( Y ) = 0 E(Y)=0 E(Y)=0 的时候:
D ( X ∗ Y ) = D ( X ) ∗ D ( Y ) D(X*Y)=D(X)*D(Y) D(XY)=D(X)D(Y)
在神经网络中,由于全连接层的性质
H 11 = ∑ i = 0 n X I ∗ W 1 i H_{11}=\sum^n_{i=0}X_I*W_{1i} H11=i=0nXIW1i

D ( H 11 ) = ∑ i = 0 n D ( X i ) ∗ D ( W 1 i ) = n ∗ ( 1 ∗ 1 ) = n D(H_{11})=\sum^n_{i=0}D(X_i)*D(W_{1i})\\ =n*(1*1)\\ =n D(H11)=i=0nD(Xi)D(W1i)=n(11)=n
因为 X i X_i Xi 服从一个方差为 1 的正态分布,而 W i W_i Wi 也服从一个方差为 1 的分布,所以 D ( H 11 ) D(H_{11}) D(H11) 的值就是神经元的个数,因此标准差就是 n \sqrt{n} n 。而全连接的性质决定了第 k k k 层的神经元的标准差为 n k \sqrt{n^k} nk ,与上面例子中 256 个神经元的情况基本吻合。

为了让神经网络的神经元值稳定,我们希望将每一层神经元的方差维持在 1,这样每一次前向传播后的方差仍然是 1,使模型保持稳定。这被称为“方差一致性准则”。因为 D ( H 11 ) = n ∗ D ( X i ) ∗ D ( W 1 i ) D(H_{11})=n*D(X_i)*D(W_{1i}) D(H11)=nD(Xi)D(W1i),为了让 D ( H i ) = 1 D(H_i)=1 D(Hi)=1,我们只需要让 D ( W i ) = 1 n D(W_i)=\frac{1}{n} D(Wi)=n1 s t d ( W ) = 1 n std(W)=\sqrt{\frac{1}{n}} std(W)=n1 。我们验证一下:

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 _ in range(layers)])
        self.neural_num = neural_num
        
    def forward(self, x):
        for (i, linear) in enumerate(self.linears):
            x = linear(x)
            print(f'layers: {i}, std: {x.std()}')
            if torch.isnan(x.std()):
                print(f'output is nan at {i}th layers')
                break
                
        return x
            
    def init(self):
        for m in self.modules():
            if isinstance(m, nn.Linear):
                nn.init.normal_(m.weight.data, std=np.sqrt(1/self.neural_num))
layers=100
neural_num=256
batch_size=16

net = MLP(neural_num, layers)
net.init()

inputs = torch.randn(batch_size, neural_num)
output = net(inputs)

打印一下神经网络的神经元值:

layers: 0, std: 0.9983504414558411
layers: 1, std: 0.9868919253349304
layers: 2, std: 0.9728540778160095
layers: 3, std: 0.9823500514030457
layers: 4, std: 0.9672497510910034
layers: 5, std: 0.9902626276016235
...
layers: 95, std: 1.0507267713546753
layers: 96, std: 1.0782362222671509
layers: 97, std: 1.1384222507476807
layers: 98, std: 1.1450780630111694
layers: 99, std: 1.138461709022522
tensor([[-0.6622,  0.4439,  0.5704,  ..., -2.2066, -1.1012,  0.0450],
        [-0.1037, -0.3485, -0.0313,  ..., -0.1562, -0.0520,  0.6481],
        [ 0.3136, -0.0966, -1.5647,  ..., -0.8760, -0.7498,  0.6339],
        ...,
        [-0.6644, -0.4354,  0.8103,  ...,  1.1510,  0.7699,  0.0607],
        [-0.7511, -0.1086,  0.4008,  ...,  1.5456,  0.6027, -0.0303],
        [-0.5602, -0.1664, -0.9711,  ..., -1.0884, -0.7040,  0.7415]],
       grad_fn=<MmBackward>)

神经元的值果然是稳定的。

3. torch.nn.init.calculate_gain

这个函数计算激活函数之前和之后的方差的比例变化。比如 D ( X ) = 1 D(X)=1 D(X)=1 经过 rlue 以后还是 1,所以它的增益是 1。PyTorch 给了常见的激活函数的变化增益:

激活函数变化增益
Linearity1
ConvND1
Sigmoid1
Tanh 5 3 \frac{5}{3} 35
ReLU 2 \sqrt{2} 2
Leaky ReLU 2 1 + n e g a t i v e _ s l o p e 2 \sqrt{\frac{2}{1+negative\_slope^2}} 1+negative_slope22

这个函数的参数如下:torch.nn.init.calculate_gain(nonlinearity, param=None)

  • nonlinearity:激活函数;
  • param激活函数的参数。

4. Xavier initialization

为了解决饱和激活函数里的权重初始化问题,2010 年 Glorot 和 Bengio 发表了《Understanding the difficulty of training deep feedforward neural networks》 论文,正式提出了 Xavier 初始化。Xavier 初始化通常使用均匀分布。由论文得,初始化后的张量中的值采样自 U [ − a , a ] U[-a,a] U[a,a]
a = gain × 6 n i + n i + 1 a=\text{gain}\times\sqrt{\frac{6}{n_i+n{i+1}}} a=gain×ni+ni+16
均匀分布下的 Xavier 初始化函数为 torch.nn.init.xavier_uniform_(tensor, gain=1)

Xavier 初始化也可以采用正态分布的方式。其初始化后的张量中的值采样自 U [ − a , a ] U[-a,a] U[a,a]
a = gain × 2 n i + n i + 1 a=\text{gain}\times\sqrt{\frac{2}{n_i+n{i+1}}} a=gain×ni+ni+12

5. Kaiming initialization

2011 年 ReLU 函数横空出世,Xavier 初始化对 ReLU 函数不再适用。2015 年,Kaiming He 提出了另一种初始化方法来适应 ReLU:
a = 2 ( 1 + a 2 ) ∗ n i a=\frac{2}{(1+a^2)*n_i} a=(1+a2)ni2
a 是 ReLU 上 x < 0 x<0 x<0 时的斜率。同样的,Kaiming 初始化也有均匀分布和正态分布两种:
torch.nn.init.kaiming_uniform_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu'):均匀分布的 Kaiming 初始化函数;

torch.nn.init.kaiming_normal_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu'):正态分布的 Kaiming 初始化函数。

6. 其它初始化方法

  • torch.nn.init.uniform_(tensor, a=0.0, b=1.0):初始化服从 [a, b] 范围的均匀分布;
  • torch.nn.init.normal_(tensor, mean=0.0, std=1.0):初始化服从 mean=0std=1 时的正态分布;
  • torch.nn.init.constant_(tensor, val):初始化为任一常数;
  • torch.nn.init.ones_(tensor):初始化为 1;
  • torch.nn.init.zeros_(tensor)初始化为 0;
  • torch.nn.init.eye_(tensor):初始化对角线为 1,其它为 0;
  • torch.nn.init.orthogonal_(tensor, gain=1):对张量的矩形区域进行初始化。由于张量都是矩形,个人理解是这个函数会将整个张量进行初始化。
  • torch.nn.init.sparse_(tensor, sparsity, std=0.01):以 sparsity 为概率将张量填充 0,剩余的元素的标准差为 std

欢迎关注我的微信公众号“花解语 NLP”:
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值