第 7 章:参数更新与优化器
有了梯度,下一步是决定"如何用梯度更新参数"。不同的策略(优化器)对训练速度和最终效果影响巨大。
7.1 梯度下降的基本思想
想象你站在一座山上,目标是走到最低点(损失最小)。你的策略:
每次朝当前位置最陡的下坡方向走一步。
- 梯度 = 坡度(指向上坡方向)
- 负梯度 = 下坡方向
- 步长(学习率)= 每步走多远
损失
↑
| /\
| / \
| / \
|/ \___
| \___
+------------------→ 参数值
↑
当前位置
梯度<0(左侧是上坡)
所以应该向右走(增大参数)基本更新规则:
其中
7.2 SGD:随机梯度下降
7.2.1 公式
其中
7.2.2 代码
class SGD:
def __init__(self, learning_rate=0.1):
self.lr = learning_rate
def step(self, params, grads):
for key in params:
params[key] -= self.lr * grads[key]就这么简单:参数 = 参数 - 学习率 × 梯度。
7.2.3 问题
SGD 的问题在某些损失曲面上很明显:
等高线图(椭圆形损失曲面):
↑ 参数 W₂
| ___
| / \
| |
| × | ← 最优点
| |
| \___/
|
+----------→ 参数 W₁
SGD 的路径(锯齿状):
↑ ← 在 W₂ 方向震荡
| ↗↘↗↘↗
| → 在 W₁ 方向缓慢前进在梯度大的方向(短轴),SGD 来回震荡;在梯度小的方向(长轴),前进缓慢。
7.3 Momentum:动量法
7.3.1 物理比喻
想象把小球放在损失曲面上,让它自由滚动:
- 在陡峭方向,小球快速加速,但在震荡方向,来回的力相互抵消
- 小球会沿着"河谷"加速滑向最低点
Momentum 给梯度下降加入了"惯性"。
7.3.2 公式
其中:
:速度向量(初始为 0),记录"历史运动方向" :动量系数(通常 0.9),保留多少历史速度 :当前梯度
7.3.3 直觉
展开速度
第 1 步:v₁ = -α·g₁
第 2 步:v₂ = μ·v₁ - α·g₂ = -μα·g₁ - α·g₂
第 3 步:v₃ = μ·v₂ - α·g₃ = -μ²α·g₁ - μα·g₂ - α·g₃
...
第 t 步:vₜ = -α·Σₖ μ^(t-k) · gₖ当前速度 = 历史所有梯度的指数加权和(越近的梯度权重越大)。
效果:
- 如果多次梯度方向一致 → 速度累积,加速前进
- 如果梯度方向来回震荡 → 速度相互抵消,减弱震荡
class Momentum:
def __init__(self, learning_rate=0.01, momentum=0.9):
self.lr = learning_rate
self.momentum = momentum
self._velocity = {}
def step(self, params, grads):
if not self._velocity:
for key in params:
self._velocity[key] = np.zeros_like(params[key])
for key in params:
# 速度 = 保留历史速度 + 当前梯度带来的加速
self._velocity[key] = self.momentum * self._velocity[key] - self.lr * grads[key]
# 参数按速度方向更新
params[key] += self._velocity[key]7.3.4 与 SGD 对比
Momentum 的路径(比 SGD 平滑得多):
↑ ← 震荡被抑制
| →→→→→
| → 在主方向加速前进7.4 Adam:自适应矩估计
7.4.1 设计思路
Adam 结合了两个思想:
- Momentum 的思路:用历史梯度的均值(一阶矩)代替原始梯度
- AdaGrad 的思路:根据梯度的历史方差(二阶矩)自动调整每个参数的学习率
关键洞察: 不同参数的梯度幅度差异很大。对梯度大的参数用小学习率,对梯度小的参数用大学习率——这样所有参数都能以合适的速度更新。
7.4.2 公式
每步
一阶矩(梯度的指数移动均值):
二阶矩(梯度平方的指数移动均值):
偏差修正(修正初始阶段偏低的估计):
参数更新:
默认超参数(论文推荐值):
(一阶矩衰减) (二阶矩衰减) (防止除零) (学习率)
7.4.3 为什么需要偏差修正?
初始时
第一步更新后:
但真正的梯度均值估计应该接近
偏差修正:
随着训练步数
7.4.4 有效学习率的意义
Adam 的实际更新量(对单个参数):
用
Adam 的更新步长大约是固定的
但实际上用历史均值(而非瞬时值),所以比这更平滑——梯度一致的参数学习率大,震荡的参数学习率小。
7.4.5 代码实现
class Adam:
def __init__(self, learning_rate=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8):
self.lr = learning_rate
self.beta1 = beta1
self.beta2 = beta2
self.eps = epsilon
self._t = 0
self._m = {} # 一阶矩
self._v = {} # 二阶矩
def step(self, params, grads):
if not self._m:
for key in params:
self._m[key] = np.zeros_like(params[key])
self._v[key] = np.zeros_like(params[key])
self._t += 1
# 预计算偏差修正后的有效学习率
# lr_t = α · sqrt(1-β₂ᵗ) / (1-β₁ᵗ)
lr_t = self.lr * np.sqrt(1.0 - self.beta2 ** self._t) / (1.0 - self.beta1 ** self._t)
for key in params:
g = grads[key]
self._m[key] = self.beta1 * self._m[key] + (1.0 - self.beta1) * g # 一阶矩
self._v[key] = self.beta2 * self._v[key] + (1.0 - self.beta2) * g ** 2 # 二阶矩
params[key] -= lr_t * self._m[key] / (np.sqrt(self._v[key]) + self.eps)注意代码中把偏差修正合并进了学习率 lr_t,而不是单独修正 m 和 v:
然后直接用原始的
数学上等价,但只需一次修正计算,速度更快。
7.5 三种优化器对比
| 特性 | SGD | Momentum | Adam |
|---|---|---|---|
| 更新规则 | 引入速度 | 引入一、二阶矩 | |
| 参数自适应 | 无 | 无 | 有(每参数独立学习率) |
| 收敛速度 | 慢 | 较快 | 最快(一般情况) |
| 调参难度 | 简单(只有 | 较简单 | 相对复杂(但默认值通常够用) |
| 推荐学习率 | 0.01 ~ 0.1 | 0.01 | 0.001 |
| 适用场景 | 教学、简单问题 | 经典深度网络 | 大多数情况首选 |
7.6 学习率的影响
学习率
损失
↑
| α 太大:在最优点附近来回跳跃,无法收敛
| ↗↘↗↘↗
|
| α 合适:平滑收敛到最优点
| ↘___
|
| α 太小:收敛极慢,可能困在局部极小值
| ↘
| ↘
| ↘(很慢)
+------------------→ 训练步数本项目的建议:
- SGD:lr = 0.1
- Momentum:lr = 0.01
- Adam:lr = 0.001(我们的配置)
7.7 训练循环总结
把反向传播(第 6 章)和参数更新(本章)合并,完整的一次训练步骤是:
① 取一个 mini-batch:x_batch, t_batch
② 前向传播:y = model.forward(x_batch)
③ 计算损失:L = cross_entropy(y, t_batch)
④ 反向传播:grads = model.backprop(x_batch, t_batch)
⑤ 参数更新:for θ in params: θ -= lr * grads[θ] (SGD 情况)
⑥ 重复 ①-⑤,直到收敛7.8 小结
| 概念 | 关键点 |
|---|---|
| 梯度下降 | 沿负梯度方向更新: |
| SGD | 最简单,但在椭圆损失曲面上震荡 |
| Momentum | 引入速度项,历史梯度加权,减少震荡 |
| Adam | 自适应学习率,一阶矩 + 二阶矩,实践中最稳定 |
第一部分结束! 你现在已经有了理解这个神经网络项目所需的全部理论基础。接下来,我们进入第二部分:把这些理论翻译成代码。
← 第 6 章 | 返回目录 | 第 8 章:项目结构 →