---
## 一、从一次深夜调试说起
上周排查一个模型收敛问题,训练损失震荡下降,验证集指标却死活不动。盯着损失曲线看了半小时,突然意识到问题可能不在数据或模型结构——而是优化器的学习率策略设错了。这让我重新反思,很多看似玄学的训练问题,根源往往在最基础的优化机制上。今天我们就拆开黑盒,看看反向传播、梯度下降和优化器到底是怎么工作的。
---
## 二、反向传播:链式法则的工程实践
反向传播不是新数学,就是链式求导的巧妙实现。关键在“反向”二字:前向计算时保留中间结果,反向时从损失函数逐层回传梯度。
```python
# 一个极简的全连接层反向传播示例
def linear_backward(dZ, cache):
"""
cache 里存了前向传播的 (A_prev, W, b)
dZ 是当前层输出的梯度
"""
A_prev, W, b = cache
m = A_prev.shape[1]
dW = np.dot(dZ, A_prev.T) / m # 别忘记除以 m,这是批量梯度均值
db = np.sum(dZ, axis=1, keepdims=True) / m
dA_prev = np.dot(W.T, dZ) # 这个要传给前一层
# 这里踩过坑:梯度检查时发现没除 m,导致学习率实际放大了一万倍
return dA_prev, dW, db
```
注意几个细节:梯度是累加还是平均(通常取平均),矩阵乘法的顺序(维度要对上),以及中间结果的缓存是否完整。工业级框架会做更细的优化,比如融合操作、原地计算,但核心逻辑不变。
---
## 三、梯度下降:三个变种与实用选择
批量梯度下降(BGD)用全量数据算梯度,稳定但慢;随机梯度下降(SGD)用一个样本,快但震荡;小批量梯度下降(Mini-batch GD)是折中方案,也是现在的主流。
```python
# 小批量梯度下降的核心循环
for epoch in range(num_epochs):
np.random.shuffle(data) # 每个 epoch 打乱数据很重要
for i in range(0, num_samples, batch_size):
batch = data[i:i+batch_size]
grads = compute_gradients(batch, params)
params = params - learning_rate * grads # 就这一行是核心
# 实际项目这里会加梯度裁剪、权重衰减等
```
很多人以为 SGD 是“随机取一个样本”,其实现代实现里都是随机取一个小批量。batch size 的影响很大:太小则噪声大,太大则内存扛不住且容易陷进尖锐极小值。我们一般从 32 或 64 开始试。
---
## 四、优化器进化史:从动量到自适应
SGD 朴素,但问题不少:梯度方向震荡、学习率难调、不同参数更新尺度不同。于是优化器开始“打补丁”。
**动量(Momentum)** 引入物理惯性,缓解震荡:
```python
v = beta * v + (1 - beta) * grad # 指数移动平均,beta 常取 0.9
param = param - learning_rate * v
```
**AdaGrad** 自适应调整学习率,稀疏特征受益大,但分母累积会过早衰减学习率。
**RMSProp** 改进 AdaGrad,用滑动平均替代累加,缓解衰减问题。
**Adam** 现在最流行的方案,结合动量和自适应:
```python
m = beta1 * m + (1 - beta1) * grad # 一阶矩估计
v = beta2 * v + (1 - beta2) * (grad ** 2) # 二阶矩估计
m_hat = m / (1 - beta1 ** t) # 偏差校正,初期很重要
v_hat = v / (1 - beta2 ** t)
param = param - lr * m_hat / (np.sqrt(v_hat) + eps)
```
Adam 默认参数(beta1=0.9, beta2=0.999, eps=1e-8)在大多数场景表现良好,但有人批评其泛化能力不如带动量的 SGD。我的经验是:Adam 快速试错,SGD 精调上限。
---
## 五、那些不起眼但致命的细节
**学习率调度**:固定学习率是新手常见错误。试试余弦退火或 warmup:
```python
# 简单 warmup 示例
if step < warmup_steps:
lr = base_lr * step / warmup_steps # 慢慢爬坡
else:
lr = base_lr * 0.95 ** ((step - warmup_steps) // 1000) # 阶梯下降
```
**梯度裁剪**:防止梯度爆炸,尤其用在 RNN 和 Transformer:
```python
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# max_norm 一般设 1.0 或 5.0,别乱调太大
```
**权重初始化**:没初始化好,模型可能一开始就“死”了。Xavier 初始化适合 tanh,He 初始化适合 ReLU。
---
## 六、个人经验与建议
1. **不要迷信 Adam**:在图像分类、检测等任务上,SGD with Momentum 调好学习率调度,最终精度常优于 Adam。NLP 领域 Transformer 系模型用 AdamW 更多。
2. **学习率是最重要的超参数**:没有之一。先用学习率搜索(如 Leslie Smith 的 LR range test)找大致范围,再细调。
3. **监控梯度统计量**:训练时顺手记录梯度的 L2 范数、均值、方差。一旦发现梯度消失或爆炸,能快速定位到某层。
4. **优化器状态保存与恢复**:保存 checkpoint 时务必包含优化器的 state_dict,否则恢复训练后动量等状态丢失,可能引起震荡。
5. **新模型先从简单优化器开始**:遇到新结构,先用 SGD 调通,再换 Adam。复杂优化器可能掩盖模型本身的设计问题。
6. **分布式训练注意同步**:多卡时梯度求平均,batch size 要等比例放大学习率(线性缩放规则),但别死守公式,实际可能缩放要保守些。
---
优化器是训练过程的“驾驶控制器”,理解其原理就像老司机懂变速箱。调参时多问几个为什么:为什么震荡?为什么收敛慢?为什么验证集不涨?答案往往藏在梯度流动的细节里。下次遇到训练异常,不妨先画出各层梯度分布图——那才是模型最真实的脉搏。
602



被折叠的 条评论
为什么被折叠?



