PyTorch教程-5.4. 数值稳定性和初始化

电子说

1.2w人已加入

描述

到目前为止,我们实现的每个模型都需要我们根据一些预先指定的分布来初始化它的参数。直到现在,我们都认为初始化方案是理所当然的,掩盖了如何做出这些选择的细节。您甚至可能觉得这些选择并不是特别重要。相反,初始化方案的选择在神经网络学习中起着重要作用,对于保持数值稳定性至关重要。此外,这些选择可以以有趣的方式与非线性激活函数的选择联系起来。我们选择哪个函数以及我们如何初始化参数可以决定我们的优化算法收敛的速度。这里的错误选择可能会导致我们在训练时遇到梯度爆炸或消失的情况。在这个部分,

 

%matplotlib inline
import torch
from d2l import torch as d2l

 

 

%matplotlib inline
from mxnet import autograd, np, npx
from d2l import mxnet as d2l

npx.set_np()

 

 

%matplotlib inline
import jax
from jax import grad
from jax import numpy as jnp
from jax import vmap
from d2l import jax as d2l

 

 

No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)

 

 

%matplotlib inline
import tensorflow as tf
from d2l import tensorflow as d2l

 

5.4.1. 消失和爆炸梯度

考虑一个深度网络L图层,输入x 和输出o. 每层l由转换定义fl权重参数化 W(l), 隐藏层输出为 h(l)(让h(0)=x),我们的网络可以表示为:

(5.4.1)h(l)=fl(h(l−1)) and thus o=fL∘…∘f1(x).

如果所有隐藏层的输出和输入都是向量,我们可以写出梯度为o关于任何一组参数 W(l)如下:

(5.4.2)∂W(l)o=∂h(L−1)h(L)⏟M(L)=def⋅…⋅∂h(l)h(l+1)⏟M(l+1)=def∂W(l)h(l)⏟v(l)=def.

换句话说,这个梯度是L−l矩阵 M(L)⋅…⋅M(l+1)和梯度向量v(l). 因此,当将太多概率相乘时,我们很容易遇到同样的数值下溢问题。在处理概率时,一个常见的技巧是切换到对数空间,即将压力从尾数转移到数值表示的指数。不幸的是,我们上面的问题更严重:最初矩阵 M(l)可能有各种各样的特征值。它们可能很小或很大,它们的产品可能很大或 很小。

不稳定梯度带来的风险超出了数值表示。不可预测的梯度也会威胁到我们优化算法的稳定性。我们可能面临以下参数更新:(i) 过大,破坏了我们的模型( 梯度爆炸问题);或 (ii) 过小(梯度消失问题),由于参数几乎不会在每次更新时移动,因此无法进行学习。

5.4.1.1。消失的渐变

导致梯度消失问题的一个常见罪魁祸首是激活函数的选择σ在每一层的线性操作之后附加。从历史上看,sigmoid 函数1/(1+exp⁡(−x))(在第 5.1 节中介绍)很受欢迎,因为它类似于阈值函数。由于早期的人工神经网络受到生物神经网络的启发,神经元要么完全放电要么根本不放电(就像生物神经元一样)的想法似乎很有吸引力。让我们仔细看看 sigmoid,看看它为什么会导致梯度消失。

 

x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = torch.sigmoid(x)
y.backward(torch.ones_like(x))

d2l.plot(x.detach().numpy(), [y.detach().numpy(), x.grad.numpy()],
     legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5))

 

pytorch

 

x = np.arange(-8.0, 8.0, 0.1)
x.attach_grad()
with autograd.record():
  y = npx.sigmoid(x)
y.backward()

d2l.plot(x, [y, x.grad], legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5))

 

 

[07:15:48] src/base.cc:49: GPU context requested, but no GPUs found.

 

pytorch

 

x = jnp.arange(-8.0, 8.0, 0.1)
y = jax.nn.sigmoid(x)
grad_sigmoid = vmap(grad(jax.nn.sigmoid))
d2l.plot(x, [y, grad_sigmoid(x)],
     legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5))

 

pytorch

 

x = tf.Variable(tf.range(-8.0, 8.0, 0.1))
with tf.GradientTape() as t:
  y = tf.nn.sigmoid(x)
d2l.plot(x.numpy(), [y.numpy(), t.gradient(y, x).numpy()],
     legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5))

 

pytorch

如您所见,S 形函数的梯度在其输入较大和较小时都消失了。此外,当通过多层反向传播时,除非我们处于 Goldilocks 区,其中许多 sigmoid 的输入接近于零,否则整个产品的梯度可能会消失。当我们的网络拥有很多层时,除非我们小心,否则梯度可能会在某一层被切断。事实上,这个问题曾经困扰着深度网络训练。因此,更稳定(但在神经上不太可信)的 ReLU 已成为从业者的默认选择。

5.4.1.2. 爆炸梯度

相反的问题,当梯度爆炸时,可能同样令人烦恼。为了更好地说明这一点,我们绘制了 100 个高斯随机矩阵并将它们与某个初始矩阵相乘。对于我们选择的尺度(方差的选择σ2=1), 矩阵乘积爆炸。当由于深度网络的初始化而发生这种情况时,我们没有机会让梯度下降优化器收敛。

 

M = torch.normal(0, 1, size=(4, 4))
print('a single matrix n',M)
for i in range(100):
  M = M @ torch.normal(0, 1, size=(4, 4))
print('after multiplying 100 matricesn', M)

 

 

a single matrix
 tensor([[ 0.0837, -0.9784, -0.5752, -0.0418],
    [ 2.0032, 2.0948, -1.4284, -1.5950],
    [-0.9720, -2.1672, -0.2809, 0.2282],
    [-0.7581, 0.0328, -0.2364, -0.5804]])
after multiplying 100 matrices
 tensor([[ 7.5119e+24, -9.2313e+24, -2.1761e+24, 7.0456e+23],
    [-1.3462e+24, 1.6544e+24, 3.8999e+23, -1.2627e+23],
    [ 1.4648e+25, -1.8001e+25, -4.2433e+24, 1.3739e+24],
    [ 8.9242e+24, -1.0967e+25, -2.5852e+24, 8.3702e+23]])

 

 

M = np.random.normal(size=(4, 4))
print('a single matrix', M)
for i in range(100):
  M = np.dot(M, np.random.normal(size=(4, 4)))
print('after multiplying 100 matrices', M)

 

 

a single matrix [[ 2.2122064  1.1630787  0.7740038  0.4838046 ]
 [ 1.0434403  0.29956347 1.1839255  0.15302546]
 [ 1.8917114 -1.1688148 -1.2347414  1.5580711 ]
 [-1.771029  -0.5459446 -0.45138445 -2.3556297 ]]
after multiplying 100 matrices [[ 3.4459747e+23 -7.8040759e+23 5.9973355e+23 4.5230040e+23]
 [ 2.5275059e+23 -5.7240258e+23 4.3988419e+23 3.3174704e+23]
 [ 1.3731275e+24 -3.1097129e+24 2.3897754e+24 1.8022945e+24]
 [-4.4951091e+23 1.0180045e+24 -7.8232368e+23 -5.9000419e+23]]

 

 

get_key = lambda: jax.random.PRNGKey(d2l.get_seed()) # Generate PRNG keys
M = jax.random.normal(get_key(), (4, 4))
print('a single matrix n', M)
for i in range(100):
  M = jnp.matmul(M, jax.random.normal(get_key(), (4, 4)))
print('after multiplying 100 matricesn', M)

 

 

a single matrix
 [[-1.531857  -1.6248469 -1.7896363  0.01071274]
 [-0.83422506 -1.583117  -0.04581776 -0.6887173 ]
 [ 0.5935193 -1.4750035  0.5265016 -1.0061077 ]
 [-0.6028679 -0.3464505  1.1737709 -1.3659075 ]]
after multiplying 100 matrices
 [[-2.7093967e+25 6.4777160e+24 -1.1368576e+25 -7.9616848e+25]
 [ 9.1084851e+24 -2.1776870e+24 3.8219019e+24 2.6765695e+25]
 [ 2.6918674e+25 -6.4358044e+24 1.1295020e+25 7.9101732e+25]
 [ 3.1859349e+25 -7.6170382e+24 1.3368117e+25 9.3620141e+25]]

 

 

M = tf.random.normal((4, 4))
print('a single matrix n', M)
for i in range(100):
  M = tf.matmul(M, tf.random.normal((4, 4)))
print('after multiplying 100 matricesn', M.numpy())

 

 

a single matrix
 tf.Tensor(
[[ 0.6401031 -0.30267003 -1.2595664 -0.09789732]
 [-1.7108601  0.5364894 -0.02595425 0.32053235]
 [-0.40997565 -0.6582443 -0.7346871  1.9668812 ]
 [ 0.16821837 -0.41409513 -0.5977446 -0.38071403]], shape=(4, 4), dtype=float32)
after multiplying 100 matrices
 [[1.5536073e+21 5.0625842e+21 2.7629899e+21 3.1974600e+21]
 [6.6411150e+20 2.1640736e+21 1.1810791e+21 1.3667998e+21]
 [3.0877614e+21 1.0061778e+22 5.4913826e+21 6.3548842e+21]
 [3.9506273e+20 1.2873513e+21 7.0259335e+20 8.1307368e+20]]

 

5.4.1.3. 打破对称

神经网络设计中的另一个问题是其参数化中固有的对称性。假设我们有一个带有一个隐藏层和两个单元的简单 MLP。在这种情况下,我们可以置换权重 W(1)第一层的权重,同样置换输出层的权重以获得相同的函数。第一个隐藏单元与第二个隐藏单元没有什么特别的区别。换句话说,我们在每一层的隐藏单元之间具有排列对称性。

这不仅仅是理论上的麻烦。考虑前面提到的具有两个隐藏单元的单隐藏层 MLP。为了说明,假设输出层将两个隐藏单元转换为仅一个输出单元。想象一下,如果我们将隐藏层的所有参数初始化为 W(1)=c对于一些常数c. 在这种情况下,在前向传播过程中,任一隐藏单元采用相同的输入和参数,产生相同的激活,并将其馈送到输出单元。在反向传播期间,根据参数区分输出单元W(1)给出一个梯度,其元素都取相同的值. 因此,在基于梯度的迭代(例如,小批量随机梯度下降)之后, W(1)仍然取相同的值。这样的迭代永远不会自行破坏对称性,我们可能永远无法实现网络的表达能力。隐藏层的行为就好像它只有一个单元。请注意,虽然小批量随机梯度下降不会破坏这种对称性,但 dropout 正则化(稍后介绍)会!

5.4.2. 参数初始化

解决或至少减轻上述问题的一种方法是通过仔细初始化。正如我们稍后将看到的,在优化和适当的正则化过程中额外注意可以进一步提高稳定性。

5.4.2.1. 默认初始化

在前面的部分中,例如,在第 3.5 节中,我们使用正态分布来初始化我们的权重值。如果我们不指定初始化方法,框架将使用默认的随机初始化方法,这在实践中通常适用于中等规模的问题。

5.4.2.2. 泽维尔初始化

让我们看一下输出的尺度分布oi对于一些没有非线性的完全连接层。和 nin输入xj及其相关权重 wij对于这一层,输出由下式给出

(5.4.3)oi=∑j=1ninwijxj.

权重wij都是从同一个分布中独立抽取的。此外,我们假设此分布的均值和方差为零σ2. 请注意,这并不意味着分布必须是高斯分布,只是意味着均值和方差需要存在。现在,我们假设层的输入 xj也有零均值和方差γ2并且它们独立于wij并且相互独立。在这种情况下,我们可以计算的均值和方差oi如下:

(5.4.4)E[oi]=∑j=1ninE[wijxj]=∑j=1ninE[wij]E[xj]=0,Var[oi]=E[oi2]−(E[oi])2=∑j=1ninE[wij2xj2]−0=∑j=1ninE[wij2]E[xj2]=ninσ2γ2.

保持方差固定的一种方法是设置 ninσ2=1. 现在考虑反向传播。我们面临着类似的问题,尽管梯度是从更靠近输出的层传播的。使用与前向传播相同的推理,我们看到梯度的方差会爆炸,除非 noutσ2=1, 在哪里nout是这一层的输出个数。这让我们进退两难:我们不可能同时满足这两个条件。相反,我们只是尝试满足:

(5.4.5)12(nin+nout)σ2=1 or equivalently σ=2nin+nout.

这就是现在标准且实际有益的Xavier 初始化背后的推理,以其创建者的第一作者命名(Glorot 和 Bengio,2010)。通常,Xavier 初始化从具有零均值和方差的高斯分布中采样权重 σ2=2nin+nout. 当从均匀分布中采样权重时,我们也可以调整它来选择方差。注意均匀分布U(−a,a)有方差a23. 堵漏a23进入我们的条件σ2产生根据初始化的建议

(5.4.6)U(−6nin+nout,6nin+nout).

尽管在神经网络中很容易违反上述数学推理中不存在非线性的假设,但 Xavier 初始化方法在实践中表现良好。

5.4.2.3. 超过

上面的推理仅仅触及了现代参数初始化方法的皮毛。深度学习框架通常会实施十几种不同的启发式方法。此外,参数初始化仍然是深度学习基础研究的热点领域。其中包括专门用于绑定(共享)参数、超分辨率、序列模型和其他情况的启发式方法。例如, 肖等人。( 2018 )通过使用精心设计的初始化方法证明了在没有架构技巧的情况下训练 10000 层神经网络的可能性。

如果您对该主题感兴趣,我们建议您深入研究该模块的内容,阅读提出和分析每个启发式的论文,然后探索有关该主题的最新出版物。也许您会偶然发现甚至发明一个聪明的想法并为深度学习框架贡献一个实现。

5.4.3. 概括

梯度消失和爆炸是深度网络中的常见问题。需要非常小心地进行参数初始化,以确保梯度和参数得到很好的控制。需要初始化启发式方法来确保初始梯度既不过大也不过小。随机初始化是确保优化前对称性被破坏的关键。Xavier 初始化表明,对于每一层,任何输出的方差都不受输入数量的影响,并且任何梯度的方差都不受输出数量的影响。ReLU 激活函数减轻了梯度消失问题。这可以加速收敛。

5.4.4. 练习

除了 MLP 层中的置换对称性之外,您能否设计神经网络可能表现出需要破坏的对称性的其他情况?

我们能否将线性回归或 softmax 回归中的所有权重参数初始化为相同的值?

查找两个矩阵乘积的特征值的解析界限。这告诉您有关确保梯度条件良好的什么信息?

如果我们知道某些术语存在分歧,我们能否在事后解决这个问题?查看有关分层自适应速率缩放的论文以获取灵感 (You等人,2017 年)。

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分