趋近智
大师班
虽然矩阵乘法或微分等运算的数学定义是精确的,但它们在计算机上的实现涉及有限精度的浮点数(如32位float或16位half)。这种有限精度可能在深度神经网络训练过程中引出一些不易察觉,有时却又十分显著的问题,尤其是在大型语言模型中常见的那些非常深的架构。若不妥善处理这些数值稳定性问题,它们可能极大地妨碍甚至完全停止训练过程。
在反向传播过程中,梯度利用链式法则计算,从输出层反向传播通过网络。每一步都涉及到乘以该层操作的局部梯度(包括激活函数导数)和该层的权重。设想一个具有许多层的深层网络。如果这些梯度(特别是激活函数导数或权重矩阵)的模值持续小于1,那么梯度信号在反向传播时会呈指数级缩小。
∂W1∂L=∂outN∂L…∂in3∂out3∂out2∂in3∂in2∂out2∂W1∂in2如果这个链式乘积中的许多项的模值都小于1,那么对于早期层(如W1)的最终梯度 ∂W1∂L 会变得非常小,接近于零。
这种现象被称为梯度消失问题。当梯度消失时,网络早期层的权重几乎得不到更新,网络也就无法有效地从这些层的数据中学习到有意义的表示。这在训练早期深层网络时是一个很大的难题,特别是对于使用Sigmoid或Tanh等激活函数的网络,这些函数的导数在输入值较大或较小时会饱和(接近于零)。
import torch
import matplotlib.pyplot as plt
# Sigmoid导数饱和示例
x = torch.linspace(-10, 10, 200)
sigmoid_x = torch.sigmoid(x)
sigmoid_grad = sigmoid_x * (1 - sigmoid_x) # Sigmoid函数的导数
# 绘图
fig, ax1 = plt.subplots()
color = '#4263eb' # 蓝色
ax1.set_xlabel('输入值 (x)')
ax1.set_ylabel('Sigmoid激活', color=color)
ax1.plot(x, sigmoid_x, color=color, label='Sigmoid(x)')
ax1.tick_params(axis='y', labelcolor=color)
ax1.grid(True, linestyle=':')
ax2 = ax1.twinx() # 创建一个共享x轴的第二个坐标轴
color = '#f03e3e' # 红色
ax2.set_ylabel('Sigmoid导数', color=color)
ax2.plot(
x,
sigmoid_grad,
color=color,
linestyle='--',
label='d(Sigmoid)/dx'
)
ax2.tick_params(axis='y', labelcolor=color)
ax2.set_ylim(bottom=0) # 导数非负
fig.tight_layout() # 否则右侧y轴标签可能略有裁剪
#plt.title("Sigmoid Activation and its Derivative") # 为直接显示而移除标题
#plt.show() # 为直接JSON输出而注释掉
# 创建Plotly JSON表示
plotly_json = {
"data": [
{
"x": x.tolist()[::10], # 采样点以保持JSON小巧
"y": sigmoid_x.tolist()[::10],
"type": "scatter",
"mode": "lines",
"name": "Sigmoid(x)",
"line": {"color": "#4263eb"}
},
{
"x": x.tolist()[::10],
"y": sigmoid_grad.tolist()[::10],
"type": "scatter",
"mode": "lines",
"name": "d(Sigmoid)/dx",
"yaxis": "y2",
"line": {"color": "#f03e3e", "dash": "dash"}
}
],
"layout": {
"xaxis": {"title": "输入值 (x)"},
"yaxis": {
"title": "Sigmoid激活",
"titlefont": {"color": "#4263eb"},
"tickfont": {"color": "#4263eb"},
"gridcolor": "#e9ecef"
},
"yaxis2": {
"title": "Sigmoid导数",
"titlefont": {"color": "#f03e3e"},
"tickfont": {"color": "#f03e3e"},
"overlaying": "y",
"side": "right",
"range": [0, 0.3],
"gridcolor": "#e9ecef"
},
"legend": {"x": 0.1, "y": 0.9},
"margin": {"l": 50, "r": 50, "t": 20, "b": 40}
}
}
Sigmoid函数的导数很小(最大0.25),并且在输入值很大(正或负)时接近于零。在反向传播过程中将许多小数值相乘会导致梯度消失。
反之,如果链式法则乘积中的项(梯度、权重)的模值持续大于1,那么梯度信号在反向传播时会呈指数级增长。这会引发梯度爆炸问题。
梯度爆炸会导致模型权重(θt+1=θt−η∇θJ(θ))的更新值过大。这些过大的更新可能使优化过程变得不稳定,导致剧烈震荡或完全发散。在实践中,这通常表现为在训练过程中损失函数突然飙升至NaN(非数字)或Inf(无穷大),因为数值超过了浮点数的表示范围。这对于循环连接或重复多次乘以相同权重矩阵的非常深层网络来说,尤为突出。
标准的深度学习通常使用32位浮点数(FP32或float)。然而,训练大型语言模型常采用低精度格式,如16位浮点数(FP16或half)或BFloat16(BF16),以减少内存消耗并加速计算,尤其是在配备NVIDIA Tensor Cores等专用硬件的设备上。
这些低精度格式的表示范围明显小于FP32,并且精度也较低。
Inf)或下溢(值变为零)。在梯度消失的情况下常见的小梯度,在FP16中很容易变为零,从而停止学习。大梯度或中间激活值可能超出最大可表示值,导致Inf或NaN。使用这些格式需要谨慎处理以保持数值稳定性,我们将在第20章中对此进行更详细的阐述。
幸运的是,已经发展出多种技术来应对这些稳定性问题,它们构成了训练深度模型的一套标准方法:
细致的初始化: 适当初始化权重有助于从一开始就避免梯度消失或爆炸。Xavier/Glorot和Kaiming初始化(第12章)等技术根据层维度设定初始权重比例。
归一化层: 诸如批归一化(Batch Normalization)或在Transformer中更为常见的层归一化(Layer Normalization)(第4章)等层会将层内的激活值重新缩放,使其具有零均值和单位方差。这有助于将激活值和梯度保持在合理范围内,从而稳定训练。
梯度裁剪: 这种技术通过在权重更新步骤之前,限制梯度的最大模值或范数来直接解决梯度爆炸问题。如果梯度范数超过设定的阈值,它会被向下重新缩放。(第17章)。
import torch
import torch.nn as nn
# 示例参数和梯度(请替换为您的模型参数)
param1 = torch.randn(100, 100, requires_grad=True)
param2 = torch.randn(50, 100, requires_grad=True)
parameters = [param1, param2]
# 模拟梯度(例如,在loss.backward()之后)
if param1.grad is None: # 如果梯度不存在,则创建虚拟梯度
param1.grad = torch.randn_like(param1) * 100
if param2.grad is None:
param2.grad = torch.randn_like(param2) * 50
# 计算总梯度范数
total_norm = 0
for p in parameters:
if p.grad is not None:
param_norm = p.grad.data.norm(2)
total_norm += param_norm.item() ** 2
total_norm = total_norm ** 0.5
print(f"Original Gradient Norm: {total_norm:.2f}")
# 应用梯度裁剪(使用PyTorch工具)
max_norm = 1.0
nn.utils.clip_grad_norm_(parameters, max_norm)
# 裁剪后计算范数
clipped_total_norm = 0
for p in parameters:
if p.grad is not None:
param_norm = p.grad.data.norm(2)
clipped_total_norm += param_norm.item() ** 2
clipped_total_norm = clipped_total_norm ** 0.5
print(f"Clipped Gradient Norm: {clipped_total_norm:.2f}")
# 预期输出将显示原始范数可能 > 1.0
# 裁剪后的范数非常接近 1.0
激活函数: 使用非饱和激活函数,如ReLU(修正线性单元)或其变体(GeLU、SwiGLU),与Sigmoid或Tanh相比,有助于缓解梯度消失问题(第11章)。
混合精度训练技术: 诸如损失缩放(loss scaling)等方法专用于FP16,以动态调整损失函数的规模,从而在反向传播过程中有效放大梯度以防止下溢,然后在权重更新前将梯度重新缩放回来(第20章)。
了解这些潜在的数值难题以及应对它们的策略,对于处理现代大型语言模型的规模和深度特点来说是必不可少的。若不仔细考量数值稳定性,训练这些强大的模型在实际中将无法实现。
这部分内容有帮助吗?
© 2026 ApX Machine Learning用心打造