一、前言

在刚开始学习深度学习时,很多人都会遇到这样几个问题:

  • 模型刚训练几轮,loss 直接变成 nan

  • 学习率明明不大,但参数更新后结果越来越离谱

  • 网络层数一深,梯度不是特别大,就是特别小

  • 同样的代码,别人能跑,我一跑就崩

一开始我以为这是代码写错了,后来学习了李沐老师《动手学深度学习》中关于**数值稳定性(Numerical Stability)**的内容后,才明白:
很多深度学习训练问题,不一定是模型设计错了,而是数值计算过程本身不稳定。

这篇博客就来系统讲清楚:

  1. 什么是数值稳定性

  2. 为什么深度学习中会出现数值问题

  3. 梯度爆炸与梯度消失是怎么来的

  4. 如何从初始化、激活函数、归一化等角度解决问题

  5. 训练时出现 naninf 应该怎么排查

这部分内容非常重要,它直接关系到后面神经网络能不能稳定训练。


二、什么是数值稳定性

所谓数值稳定性,本质上是指:

在计算过程中,数值不会因为层层运算而变得过大、过小,或者产生严重误差,从而影响最终结果。

在深度学习里,模型训练依赖大量矩阵乘法、加法、指数运算和反向传播。如果某一步计算结果过大或者过小,就可能出现:

  • 上溢(overflow)

  • 下溢(underflow)

  • 梯度爆炸(gradient explosion)

  • 梯度消失(gradient vanishing)

  • naninf

这会导致模型无法继续训练,或者训练效果非常差。


三、为什么深度学习特别容易出现数值问题

深度学习模型通常具有以下特点:

  • 层数多

  • 参数多

  • 计算链条长

  • 反向传播要经过很多层

  • 经常涉及乘法连乘

连乘恰恰是数值不稳定的主要来源。

这说明:

  • 连乘很多个大于 1 的数,容易爆炸

  • 连乘很多个小于 1 的数,容易消失

而神经网络的前向传播和反向传播,本质上都存在类似的连乘结构。


四、从一个简单例子理解数值不稳定

1. 前向传播中的问题


2. 反向传播中的问题

反向传播更容易出问题。

根据链式法则,梯度需要不断相乘:

如果这些偏导数:

  • 大多大于 1,梯度越来越大,发生梯度爆炸

  • 大多小于 1,梯度越来越小,发生梯度消失

这也是深层网络难训练的重要原因。


五、梯度爆炸与梯度消失

1. 梯度爆炸

梯度爆炸指的是:

在反向传播过程中,梯度值越来越大,导致参数更新幅度过大,训练发散。

常见现象:

  • loss 突然变得非常大

  • 参数值异常大

  • 出现 infnan

  • 模型根本收敛不了

例如,若每层梯度都乘以 2,经过 20 层后:

这已经非常大了。


2. 梯度消失

梯度消失指的是:

在反向传播过程中,梯度越来越接近 0,导致前面层几乎无法更新。

常见现象:

  • 网络训练很慢

  • 深层参数几乎不变

  • loss 下降特别困难

  • 模型效果上不去

例如,若每层梯度都乘以 0.5,经过 20 层后:

这个梯度已经非常小了,前面层基本学不到东西。


六、激活函数为什么会影响数值稳定性

激活函数不仅决定网络的非线性表达能力,还会影响梯度传播。


1. Sigmoid 的问题

如果层数一多,梯度会迅速变得极小,因此 Sigmoid 很容易导致梯度消失

另外,Sigmoid 在输入绝对值很大时,会进入饱和区:

  • 输入很大时,输出接近 1

  • 输入很小时,输出接近 0

此时导数接近 0,训练更加困难。


2. Tanh 的问题


3. ReLU 的优势


七、参数初始化为什么重要

1. 为什么不能随便初始化

如果参数初始化过大,那么前向传播时输出会越来越大,容易爆炸。
如果参数初始化过小,那么输出会越来越小,梯度也容易消失。

所以,初始化的目标并不是“随机就行”,而是:

让每一层的输出方差和梯度方差尽量保持在合理范围内。


2. Xavier 初始化


3. He 初始化

He 初始化更适合 ReLU。

因为 ReLU 会把一部分输入变成 0,所以需要稍微更大的初始化方差来补偿。

常见思想是:

He 初始化在深层 ReLU 网络中非常常见。


八、为什么深层网络更难训练

随着层数增加,网络会面临几个问题:

1. 连乘效应更严重

无论是前向传播还是反向传播,只要层数加深,连乘次数就增加。
这会让数值更容易爆炸或消失。

2. 梯度传递路径更长

越靠前的层,梯度需要经过更多层才能传回来。
一旦中间某些层的梯度很小,前面层几乎就收不到有效更新信号。

3. 激活值分布不断漂移

每一层输出的分布可能都不同,导致后续层训练困难。
这也是后来 Batch Normalization 被提出的重要原因。


九、如何解决数值稳定性问题

这一部分是重点,也是训练神经网络时最实用的内容。


1. 合理的参数初始化

不要用过大或过小的随机数初始化参数。
一般建议:

  • Sigmoid / Tanh:Xavier 初始化

  • ReLU:He 初始化

在 PyTorch 中常见写法:

import torch
from torch import nn

linear = nn.Linear(128, 64)
nn.init.xavier_uniform_(linear.weight)   # Xavier 初始化
nn.init.zeros_(linear.bias)

或者:

nn.init.kaiming_uniform_(linear.weight, nonlinearity='relu')  # He 初始化

2. 使用合适的激活函数

通常优先考虑:

  • ReLU

  • Leaky ReLU

  • GELU

尽量少在深层网络中大量使用普通 Sigmoid,除非你明确知道为什么要用它。


3. 数据归一化

输入数据尺度差异过大,也会导致训练不稳定。

例如图像数据通常会做:

  • 除以 255

  • 标准化到均值 0、方差 1 附近

如果输入数据本身就特别大,那么模型前几层的输出也容易特别大。


4. 批量归一化 Batch Normalization

BatchNorm 的作用可以简单理解为:

把每一层的中间输出拉回到较稳定的分布范围内。

好处包括:

  • 加快收敛

  • 缓解梯度消失和梯度爆炸

  • 允许使用更大学习率

  • 提高训练稳定性

PyTorch 写法:

net = nn.Sequential(
    nn.Linear(784, 256),
    nn.BatchNorm1d(256),
    nn.ReLU(),
    nn.Linear(256, 10)
)

5. 梯度裁剪 Gradient Clipping

当梯度过大时,可以对梯度进行裁剪,避免更新过猛。

这在 RNN、LSTM 等序列模型中尤其常见。

torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

它的意思是:
如果梯度范数超过 1.0,就按比例缩小。


6. 控制学习率

学习率过大,也会让参数更新一步跨太远,导致 loss 震荡甚至爆炸。

常见建议:

  • 模型不稳定时,先尝试减小学习率

  • 配合学习率调度器逐步下降

  • 不要一上来就设置很大的学习率


7. 使用残差连接

残差连接是 ResNet 成功的关键之一。

核心思想:

[
y = F(x) + x
]

它可以让梯度更顺畅地从后面层传回前面层,从而缓解深层网络中的梯度消失问题。


十、softmax 和交叉熵中的数值稳定性

这一块非常经典,也特别容易考。



3. 为什么框架里常把 softmax 和交叉熵合在一起

因为:

  • 单独算 softmax 可能不稳定

  • 再手动取对数还可能产生额外误差

所以很多框架提供了更稳定的封装版本,比如 PyTorch 的:

loss_fn = nn.CrossEntropyLoss()

它内部已经把这些数值稳定性问题处理好了。
因此平时训练分类模型时,不要自己先做 softmax 再送入 CrossEntropyLoss


十一、训练中出现 nan 怎么办

这是最常见的实际问题之一。

如果训练时出现了 nan,可以从下面几个方向排查。

1. 学习率是不是太大

先把学习率调小,很多问题都会立刻缓解。


2. 输入数据有没有异常值

检查:

  • 是否有缺失值

  • 是否有极大值

  • 是否归一化

  • 标签是否正确


3. loss 函数使用方式是否正确

例如:

  • CrossEntropyLoss 前不要自己做 softmax

  • BCEWithLogitsLoss 前不要自己做 sigmoid

因为这些损失函数已经集成了更稳定的实现。


4. 初始化是否不合理

如果初始化过大,第一轮前向传播就可能把数值放大到失控。


5. 是否需要梯度裁剪

特别是 RNN、Transformer、深层网络中,梯度裁剪很有必要。


6. 打印张量范围

可以在训练时打印:

print(x.min(), x.max())
print(loss.item())

看看是不是某一层输出已经非常夸张。


十二、一个直观总结:数值稳定性到底在解决什么

其实可以把数值稳定性理解成一句话:

让网络中的信号和梯度,在层与层之间传播时,既不要无限放大,也不要迅速消失。

它解决的不是“模型有没有想法”,而是“模型能不能活着训练完”。

如果没有数值稳定性保障,那么再好的网络结构,也可能训练不起来。


十三、我对这一节的理解

学习这一节之后,我最大的感受是:

以前总觉得深度学习难,是因为模型结构复杂;
后来才发现,很多问题其实出在底层数值计算机制上。

比如:

  • 为什么激活函数不能乱选

  • 为什么初始化方法不是随便写个 random

  • 为什么 BatchNorm、残差连接这么重要

  • 为什么 loss 会变成 nan

这些问题都和数值稳定性高度相关。

也就是说,数值稳定性虽然听起来像“理论细节”,但它实际上决定了:

  • 模型能不能训

  • 训练快不快

  • 深层网络能不能收敛

  • 最终效果能不能出来

所以这部分内容绝对不是可有可无,而是深度学习的基础。


十四、代码小实验:观察梯度消失与爆炸

下面给一个简单小实验,用来体会连乘导致的数值问题。

import torch

# 模拟多层连乘
x = torch.tensor(1.0)

# 情况1:不断放大
for i in range(20):
    x = x * 1.5
print("连续乘1.5,结果为:", x.item())

# 情况2:不断缩小
x = torch.tensor(1.0)
for i in range(20):
    x = x * 0.5
print("连续乘0.5,结果为:", x.item())

运行后你会发现:

  • 连续乘 1.5,值会越来越大

  • 连续乘 0.5,值会越来越接近 0

这和深度网络中的前向传播、反向传播本质上是类似的。


十五、结语

数值稳定性是学习深度学习时绕不开的一关。
它不像卷积、注意力机制那样“看起来很酷”,但它决定了一个模型能否正常训练。

在实际学习和写代码时,我觉得应该重点记住下面几点:

  • 深层网络容易出现梯度爆炸和梯度消失

  • 激活函数会影响梯度传播

  • 参数初始化非常关键

  • BatchNorm、梯度裁剪、残差连接都在帮助稳定训练

  • 出现 nan 时,先查学习率、初始化、数据和 loss 使用方式

只有把这些基础问题弄明白,后面学 CNN、RNN、Transformer 时才不会总被训练问题卡住。


十六、重点速记版

1. 什么是数值稳定性

训练过程中,数值不能过大、过小,也不能出现严重误差。

2. 为什么会不稳定

神经网络中存在大量连乘,层数一深就容易爆炸或消失。

3. 两大典型问题

  • 梯度爆炸:梯度过大,训练发散

  • 梯度消失:梯度过小,前层学不到东西

4. 常见解决方法

  • 合理初始化(Xavier、He)

  • 选择合适激活函数(ReLU)

  • 数据归一化

  • BatchNorm

  • 梯度裁剪

  • 控制学习率

  • 残差连接

如果你和我一样,刚开始学深度学习时总遇到 loss=nan、模型不收敛、训练发散的问题,那么一定要认真理解“数值稳定性”这一节。
它不仅仅是理论内容,更是后续写好深度学习代码的重要基础。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐