【Pytorch梯度爆炸】梯度、loss在反向传播过程中变为nan解决方法

0. 遇到大坑

笔者在最近的项目中用到了自定义loss函数,代码一切都准备就绪后,在训练时遇到了梯度爆炸的问题,每次训练几个step后,梯度/loss都会变为nan。一般情况下,梯度变为nan都是出现了log(0), x/0等情况,导致结果变为+inf,也就成了nan。

1. 问题分析

笔者需要的loss函数如下:
L = 1 N ∑ i = 0 N − 1 ( x i − Γ ( x i ) ) 2 \mathscr{L}=\frac{1}{N} \sum_{i=0}^{N-1}{\left(x_i - \Gamma(x_i)\right)^2} L=N1i=0N1(xiΓ(xi))2
其中, Γ ( x i ) = x i γ \Gamma(x_i)=x_i^\gamma Γ(xi)=xiγ, 0 &lt; γ &lt; 1 0&lt;\gamma&lt;1 0<γ<1

从理论上分析,这个loss函数在反向传播过程中很可能会遇到梯度爆炸,这是为什么呢?反向传播的过程是对loss链式求一阶导数的过程,那么, Γ ( x i ) \Gamma(x_i) Γ(xi)的导数为:
d Γ ( x i ) d x i = γ x i γ − 1 \frac{d\Gamma(x_i)}{dx_i}=\gamma x_i^{\gamma-1} dxidΓ(xi)=γxiγ1
由于 0 &lt; γ &lt; 1 0&lt;\gamma&lt;1 0<γ<1,这个导数又可以表示为:
d Γ ( x i ) d x i = γ x i 1 − γ \frac{d\Gamma(x_i)}{dx_i}=\frac{\gamma}{x_i^{1-\gamma}} dxidΓ(xi)=xi1γγ
这样的话,出现了类似于 1 / x 1/x 1/x的表达式,也就会出现典型的 0 / 1 0/1 0/1问题了。为了避免这个问题,首先进行了如下的 Γ ( x i ) \Gamma(x_i) Γ(xi)改变:
Γ ( x i ) = { 12.9 × x i , x i &lt; 0.003 x i γ , x i ≥ 0.003 \Gamma(x_i)=\left\{ \begin{aligned} 12.9 \times x_i, &amp;x_i &lt; 0.003\\ x_i^\gamma, &amp; x_i \geq 0.003 \end{aligned} \right. Γ(xi)={12.9×xi,xiγ,xi<0.003xi0.003
经过改变,在 x i = 0 x_i=0 xi=0时,不再是 1 / 0 1/0 1/0问题了,而是转换为了一个线性函数,梯度成为了恒定的12.9,从理论上来看,避免了梯度爆炸的问题。

2. PyTorch初步实现

在实现这一过程时,依旧…遇到了大坑,下面通过示例代码来说明:

"""
loss = mse(X, gamma_inv(X))
"""
def loss_function(x):
    mask = (x < 0.003).float()
    gamma_x = mask * 12.9 * x + (1-mask) * (x ** 0.5)
    loss = torch.mean((x - gamma_x) ** 2)
    return loss

if __name__ == '__main__':
    x = Variable(torch.FloatTensor([0, 0.0025, 0.5, 0.8, 1]), requires_grad=True)
    loss = loss_function(x)
    print('loss:', loss)
    loss.backward()
    print(x.grad)

改进后的 Γ ( x i ) \Gamma(x_i) Γ(xi)是一个分支结构,在实现时,就采用了类似于Matlab中矩阵计算的mask方式,mask定义为 x i &lt; 0.003 x_i&lt;0.003 xi<0.003,满足条件的 x i x_i xi在mask中对应位置的值为1,因此,mask * 12.9 * x的结构只会保留 x i &lt; 0.003 x_i&lt;0.003 xi<0.003的结果,同样的道理,gamma_x = mask * 12.9 * x + (1-mask) * (x ** 0.5)就实现了上述改进后的 Γ ( x i ) \Gamma(x_i) Γ(xi)公式。

按理来说,此时,在反向传播过程中的梯度应该是正确的,但是,上面代码的输出结果为:

loss: tensor(0.0105, grad_fn=<MeanBackward1>)
tensor([    nan,  0.1416, -0.0243, -0.0167,  0.0000])

emmm…依旧为nan,问题在理论层面得到了解决,但是,在实现层面依旧没能解决…

3. 源码调试分析

上面源码的问题依旧在 Γ ( x i ) \Gamma(x_i) Γ(xi)的实现,这个过程,在Python解释器解释的过程或许是这样的:

  1. 计算mask * 12.9,对mask进行广播式的乘法,结果为:原本为1的位置变为了12.9,原本为0的位置依旧为0;
  2. 将1.的结果继续与x相乘,本质上仍然是与x的每个元素相乘,只是mask中不满足条件的 x i x_i xi位置为0,表现出的结果是仅对满足条件的 x i x_i xi进行了计算;
  3. 按照2.所述的原理, Γ ( x i ) \Gamma(x_i) Γ(xi)公式的后半部分也是同样的计算过程,即, x x x中的每个值依旧会进行 x γ x^\gamma xγ的计算;

按照上述过程进行前向传播,在反向传播时,梯度不是从某一个分支得到的,而是两个分支的题目相加得到的,换句话说,依旧没能解决梯度变为nan的问题。

4. 源码改进及问题解决

经过第三部分的分析,知道了梯度变为nan的根本原因是当 x i = 0 x_i=0 xi=0时依旧参与了 x i γ x_i^\gamma xiγ的计算,导致在反向传播时计算出的梯度为nan。

要解决这个问题,就要保证在 x i = 0 x_i=0 xi=0时不会进行这样的计算。

新的PyTorch代码如下:

def loss_function(x):
    mask = x < 0.003
    gamma_x = torch.FloatTensor(x.size()).type_as(x)
    gamma_x[mask] = 12.9 * x[mask]
    mask = x >= 0.003
    gamma_x[mask] = x[mask] ** 0.5
    loss = torch.mean((x - gamma_x) ** 2)
    return loss

if __name__ == '__main__':
    x = Variable(torch.FloatTensor([0, 0.0025, 0.5, 0.8, 1]), requires_grad=True)
    loss = loss_function(x)
    print('loss:', loss)
    loss.backward()
    print(x.grad)

改变的地方位于loss_function,改变了对于 Γ ( x i ) \Gamma(x_i) Γ(xi)分支的处理方式,控制并保住每次计算仅有满足条件的值可以参与。此时输出为:

loss: tensor(0.0105, grad_fn=<MeanBackward1>)
tensor([ 0.0000,  0.1416, -0.0243, -0.0167,  0.0000])

就此,问题解决!

*原创博客,转载请附加本文链接。

  • 9
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
PyTorch 梯度爆炸是指在反向传播过程梯度值变得非常大,导致权重更新的变化非常大,从而无法收敛。以下是几种可以解决梯度爆炸方法: 1. 梯度裁剪(Gradient Clipping):梯度裁剪是一种简单但有效的方法,通过对梯度进行限制,使其不超过一个阈值。在 PyTorch ,可以使用 `torch.nn.utils.clip_grad_norm_` 函数来实现梯度裁剪。该函数将所有参数的梯度连接成单个向量,然后计算该向量的范数,并将梯度除以该范数(如果小于阈值)或者将梯度除以阈值(如果大于阈值),从而限制梯度的大小。 2. 使用更小的学习率:梯度爆炸通常发生在学习率过大的情况下,因此可以尝试减小学习率,这样梯度变化就会更加平缓。 3. 权重初始化:权重的初始值可能会影响模型的训练效果。如果权重的初始值过大,那么在反向传播过程梯度也会变得非常大。因此,可以尝试使用更小的初始权重来避免梯度爆炸。 4. 使用 Batch Normalization:Batch Normalization 是一种常用的正则化方法,可以通过规范化每个批次的输入来减少内部协变量偏移(Internal Covariate Shift)。Batch Normalization 已经得到广泛应用,而且可以防止梯度爆炸。 5. 简化模型:模型太复杂可能会导致梯度爆炸,因此可以尝试简化模型,例如减少模型的层数或者减少每层的神经元数量。 6. 使用更好的优化器:优化器的选择也会影响梯度的变化。例如,Adam 优化器可以自适应地调整学习率和梯度的大小,从而可以避免梯度爆炸。 需要注意的是,梯度爆炸的原因可能有很多,不同的模型或者数据集可能需要采用不同的方法解决梯度爆炸问题。因此,需要根据具体情况进行调整。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值