一、前言在刚开始学习深度学习时很多人都会遇到这样几个问题模型刚训练几轮loss直接变成nan学习率明明不大但参数更新后结果越来越离谱网络层数一深梯度不是特别大就是特别小同样的代码别人能跑我一跑就崩一开始我以为这是代码写错了后来学习了李沐老师《动手学深度学习》中关于**数值稳定性Numerical Stability**的内容后才明白很多深度学习训练问题不一定是模型设计错了而是数值计算过程本身不稳定。这篇博客就来系统讲清楚什么是数值稳定性为什么深度学习中会出现数值问题梯度爆炸与梯度消失是怎么来的如何从初始化、激活函数、归一化等角度解决问题训练时出现nan、inf应该怎么排查这部分内容非常重要它直接关系到后面神经网络能不能稳定训练。二、什么是数值稳定性所谓数值稳定性本质上是指在计算过程中数值不会因为层层运算而变得过大、过小或者产生严重误差从而影响最终结果。在深度学习里模型训练依赖大量矩阵乘法、加法、指数运算和反向传播。如果某一步计算结果过大或者过小就可能出现上溢overflow下溢underflow梯度爆炸gradient explosion梯度消失gradient vanishingnan或inf这会导致模型无法继续训练或者训练效果非常差。三、为什么深度学习特别容易出现数值问题深度学习模型通常具有以下特点层数多参数多计算链条长反向传播要经过很多层经常涉及乘法连乘而连乘恰恰是数值不稳定的主要来源。这说明连乘很多个大于 1 的数容易爆炸连乘很多个小于 1 的数容易消失而神经网络的前向传播和反向传播本质上都存在类似的连乘结构。四、从一个简单例子理解数值不稳定1. 前向传播中的问题2. 反向传播中的问题反向传播更容易出问题。根据链式法则梯度需要不断相乘如果这些偏导数大多大于 1梯度越来越大发生梯度爆炸大多小于 1梯度越来越小发生梯度消失这也是深层网络难训练的重要原因。五、梯度爆炸与梯度消失1. 梯度爆炸梯度爆炸指的是在反向传播过程中梯度值越来越大导致参数更新幅度过大训练发散。常见现象loss 突然变得非常大参数值异常大出现inf或nan模型根本收敛不了例如若每层梯度都乘以 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 / TanhXavier 初始化ReLUHe 初始化在 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, nonlinearityrelu) # He 初始化2. 使用合适的激活函数通常优先考虑ReLULeaky ReLUGELU尽量少在深层网络中大量使用普通 Sigmoid除非你明确知道为什么要用它。3. 数据归一化输入数据尺度差异过大也会导致训练不稳定。例如图像数据通常会做除以 255标准化到均值 0、方差 1 附近如果输入数据本身就特别大那么模型前几层的输出也容易特别大。4. 批量归一化 Batch NormalizationBatchNorm 的作用可以简单理解为把每一层的中间输出拉回到较稳定的分布范围内。好处包括加快收敛缓解梯度消失和梯度爆炸允许使用更大学习率提高训练稳定性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_norm1.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前不要自己做 softmaxBCEWithLogitsLoss前不要自己做 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梯度裁剪控制学习率残差连接如果你和我一样刚开始学深度学习时总遇到lossnan、模型不收敛、训练发散的问题那么一定要认真理解“数值稳定性”这一节。它不仅仅是理论内容更是后续写好深度学习代码的重要基础。
动手学深度学习笔记:数值稳定性
一、前言在刚开始学习深度学习时很多人都会遇到这样几个问题模型刚训练几轮loss直接变成nan学习率明明不大但参数更新后结果越来越离谱网络层数一深梯度不是特别大就是特别小同样的代码别人能跑我一跑就崩一开始我以为这是代码写错了后来学习了李沐老师《动手学深度学习》中关于**数值稳定性Numerical Stability**的内容后才明白很多深度学习训练问题不一定是模型设计错了而是数值计算过程本身不稳定。这篇博客就来系统讲清楚什么是数值稳定性为什么深度学习中会出现数值问题梯度爆炸与梯度消失是怎么来的如何从初始化、激活函数、归一化等角度解决问题训练时出现nan、inf应该怎么排查这部分内容非常重要它直接关系到后面神经网络能不能稳定训练。二、什么是数值稳定性所谓数值稳定性本质上是指在计算过程中数值不会因为层层运算而变得过大、过小或者产生严重误差从而影响最终结果。在深度学习里模型训练依赖大量矩阵乘法、加法、指数运算和反向传播。如果某一步计算结果过大或者过小就可能出现上溢overflow下溢underflow梯度爆炸gradient explosion梯度消失gradient vanishingnan或inf这会导致模型无法继续训练或者训练效果非常差。三、为什么深度学习特别容易出现数值问题深度学习模型通常具有以下特点层数多参数多计算链条长反向传播要经过很多层经常涉及乘法连乘而连乘恰恰是数值不稳定的主要来源。这说明连乘很多个大于 1 的数容易爆炸连乘很多个小于 1 的数容易消失而神经网络的前向传播和反向传播本质上都存在类似的连乘结构。四、从一个简单例子理解数值不稳定1. 前向传播中的问题2. 反向传播中的问题反向传播更容易出问题。根据链式法则梯度需要不断相乘如果这些偏导数大多大于 1梯度越来越大发生梯度爆炸大多小于 1梯度越来越小发生梯度消失这也是深层网络难训练的重要原因。五、梯度爆炸与梯度消失1. 梯度爆炸梯度爆炸指的是在反向传播过程中梯度值越来越大导致参数更新幅度过大训练发散。常见现象loss 突然变得非常大参数值异常大出现inf或nan模型根本收敛不了例如若每层梯度都乘以 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 / TanhXavier 初始化ReLUHe 初始化在 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, nonlinearityrelu) # He 初始化2. 使用合适的激活函数通常优先考虑ReLULeaky ReLUGELU尽量少在深层网络中大量使用普通 Sigmoid除非你明确知道为什么要用它。3. 数据归一化输入数据尺度差异过大也会导致训练不稳定。例如图像数据通常会做除以 255标准化到均值 0、方差 1 附近如果输入数据本身就特别大那么模型前几层的输出也容易特别大。4. 批量归一化 Batch NormalizationBatchNorm 的作用可以简单理解为把每一层的中间输出拉回到较稳定的分布范围内。好处包括加快收敛缓解梯度消失和梯度爆炸允许使用更大学习率提高训练稳定性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_norm1.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前不要自己做 softmaxBCEWithLogitsLoss前不要自己做 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梯度裁剪控制学习率残差连接如果你和我一样刚开始学深度学习时总遇到lossnan、模型不收敛、训练发散的问题那么一定要认真理解“数值稳定性”这一节。它不仅仅是理论内容更是后续写好深度学习代码的重要基础。