文章目录
-
- 一、为什么需要梯度下降
- 二、算法原理
-
- [2.1 损失函数与梯度](#2.1 损失函数与梯度)
- [2.2 SGD:随机梯度下降](#2.2 SGD:随机梯度下降)
- [2.3 Momentum:动量加速](#2.3 Momentum:动量加速)
- [2.4 Nesterov Accelerated Gradient (NAG)](#2.4 Nesterov Accelerated Gradient (NAG))
- [2.5 学习率调度策略](#2.5 学习率调度策略)
- [2.6 收敛性分析](#2.6 收敛性分析)
- [2.7 鞍点逃逸](#2.7 鞍点逃逸)
- [三、Python 实现](#三、Python 实现)
-
- [3.1 基础优化器实现](#3.1 基础优化器实现)
- [3.2 完整训练示例:在二次函数上对比三种优化器](#3.2 完整训练示例:在二次函数上对比三种优化器)
- [3.3 学习率调度器实现](#3.3 学习率调度器实现)
- 四、参数调优与变体对比
-
- [4.1 核心参数说明](#4.1 核心参数说明)
- [4.2 SGD / Momentum / NAG 对比](#4.2 SGD / Momentum / NAG 对比)
- [4.3 学习率调度策略对比](#4.3 学习率调度策略对比)
- 五、在推荐系统中的实际应用
- 六、常见陷阱
- 七、总结
一、为什么需要梯度下降
在机器学习和深度学习中,模型训练的本质是求解一个优化问题:找到一组参数 θ \theta θ,使得损失函数 L ( θ ) L(\theta) L(θ) 最小。当参数维度达到百万甚至十亿级别时,传统的解析解(如最小二乘法)完全不可行------计算 Hessian 矩阵的逆需要 O ( n 3 ) O(n^3) O(n3) 时间复杂度。
梯度下降是解决这个问题的核心方法:它只需要一阶导数信息,每次迭代计算量为 O ( n ) O(n) O(n),通过沿梯度反方向逐步更新参数来逼近最优解。但它远非完美------学习率太大导致震荡发散,太小则收敛极慢;在鞍点附近会停滞;在非凸优化中可能陷入局部最优。
从最基础的 SGD 到 Momentum 再到 Nesterov Accelerated Gradient,每一次改进都针对了梯度下降的一个具体缺陷。理解这些算法的数学原理和工程权衡,是所有深度学习实践者的必修课。
二、算法原理
2.1 损失函数与梯度
给定训练集 D = { ( x 1 , y 1 ) , ( x 2 , y 2 ) , ... , ( x N , y N ) } D = \{(x_1, y_1), (x_2, y_2), \ldots, (x_N, y_N)\} D={(x1,y1),(x2,y2),...,(xN,yN)},模型的损失函数定义为:
L ( θ ) = 1 N ∑ i = 1 N ℓ ( f θ ( x i ) , y i ) L(\theta) = \frac{1}{N} \sum_{i=1}^{N} \ell(f_\theta(x_i), y_i) L(θ)=N1i=1∑Nℓ(fθ(xi),yi)
其中 θ \theta θ 是模型参数, f θ ( x i ) f_\theta(x_i) fθ(xi) 是模型预测值, ℓ \ell ℓ 是单样本损失(如均方误差或交叉熵)。
梯度是损失函数对参数的偏导数向量:
∇ L ( θ ) = ∂ L ∂ θ 1 , ∂ L ∂ θ 2 , ... , ∂ L ∂ θ d T \nabla L(\theta) = \left\\frac{\\partial L}{\\partial \\theta_1}, \\frac{\\partial L}{\\partial \\theta_2}, \\ldots, \\frac{\\partial L}{\\partial \\theta_d}\\right^T ∇L(θ)=∂θ1∂L,∂θ2∂L,...,∂θd∂LT
梯度的几何意义是函数值上升最快的方向,因此负梯度方向是函数值下降最快的方向。
2.2 SGD:随机梯度下降
标准梯度下降(BGD)每次使用全部训练数据计算梯度,计算开销大。SGD 每次随机抽取一个小批量(mini-batch)计算梯度近似:
g t = ∇ L B t ( θ t ) = 1 ∣ B t ∣ ∑ i ∈ B t ∇ ℓ ( f θ t ( x i ) , y i ) g_t = \nabla L_{B_t}(\theta_t) = \frac{1}{|B_t|} \sum_{i \in B_t} \nabla \ell(f_{\theta_t}(x_i), y_i) gt=∇LBt(θt)=∣Bt∣1i∈Bt∑∇ℓ(fθt(xi),yi)
其中 B t B_t Bt 是第 t t t 步的 mini-batch, ∣ B t ∣ |B_t| ∣Bt∣ 是批量大小。
参数更新规则:
θ t + 1 = θ t − η ⋅ g t \theta_{t+1} = \theta_t - \eta \cdot g_t θt+1=θt−η⋅gt
其中 η \eta η 是学习率。
SGD 的关键性质 : E g t = ∇ L ( θ t ) Eg_t = \nabla L(\theta_t) Egt=∇L(θt),即小批量梯度是全梯度的无偏估计; V a r ( g t ) ∝ 1 ∣ B t ∣ Var(g_t) \propto \frac{1}{|B_t|} Var(gt)∝∣Bt∣1,批量越大方差越小。
为什么 SGD 能工作? 虽然每一步的梯度方向可能不准确,但期望方向是正确的。噪声反而有助于逃离局部最优和鞍点。
2.3 Momentum:动量加速
SGD 在两个场景下收敛慢:
- 高曲率方向:损失函数在某些方向变化剧烈,SGD 在这些方向反复震荡
- 小梯度方向:在平坦区域,梯度很小导致 step size 极小
Momentum 的思路:累积历史梯度方向,平滑震荡方向、加速一致方向。
v t = γ ⋅ v t − 1 + η ⋅ g t v_t = \gamma \cdot v_{t-1} + \eta \cdot g_t vt=γ⋅vt−1+η⋅gt
θ t + 1 = θ t − v t \theta_{t+1} = \theta_t - v_t θt+1=θt−vt
其中 γ ∈ [ 0 , 1 ) \gamma \in [0, 1) γ∈[0,1) 是动量系数,通常设为 0.9。
展开递推式理解 Momentum:
v t = η ∑ i = 0 t γ t − i g i = η ⋅ g t + γ ⋅ η ⋅ g t − 1 + γ 2 ⋅ η ⋅ g t − 2 + ⋯ v_t = \eta \sum_{i=0}^{t} \gamma^{t-i} g_i = \eta \cdot g_t + \gamma \cdot \eta \cdot g_{t-1} + \gamma^2 \cdot \eta \cdot g_{t-2} + \cdots vt=ηi=0∑tγt−igi=η⋅gt+γ⋅η⋅gt−1+γ2⋅η⋅gt−2+⋯
可以看到, v t v_t vt 是历史梯度的指数加权移动平均。在梯度方向一致的方向上,历史梯度累加形成"惯性",有效 step size 最大可达 η 1 − γ \frac{\eta}{1-\gamma} 1−γη;在梯度方向反复震荡的方向上,正负梯度互相抵消,有效 step size 趋近于 0。
当 γ = 0.9 \gamma = 0.9 γ=0.9 时,有效学习率扩大为 η 1 − 0.9 = 10 η \frac{\eta}{1-0.9} = 10\eta 1−0.9η=10η,这就是 Momentum 加速的本质。
2.4 Nesterov Accelerated Gradient (NAG)
Momentum 有一个缺陷:它在"看到"当前梯度之前就已经"冲"了一段距离。如果当前位置已经过了最优点,Momentum 会因为惯性继续冲过头。
NAG 的改进:先按动量方向"前瞻"一步,在展望的位置计算梯度。
v t = γ ⋅ v t − 1 + η ⋅ ∇ L ( θ t − γ ⋅ v t − 1 ) v_t = \gamma \cdot v_{t-1} + \eta \cdot \nabla L(\theta_t - \gamma \cdot v_{t-1}) vt=γ⋅vt−1+η⋅∇L(θt−γ⋅vt−1)
θ t + 1 = θ t − v t \theta_{t+1} = \theta_t - v_t θt+1=θt−vt
对比 Momentum:
- Momentum:先计算当前梯度,再加动量 → γ v t − 1 + η g ( θ t ) \gamma v_{t-1} + \eta g(\theta_t) γvt−1+ηg(θt)
- NAG:先加动量到"展望位置",再计算梯度 → γ v t − 1 + η g ( θ t − γ v t − 1 ) \gamma v_{t-1} + \eta g(\theta_t - \gamma v_{t-1}) γvt−1+ηg(θt−γvt−1)
NAG 在凸优化中的理论优势 :在凸函数上,NAG 的收敛率为 O ( 1 / t 2 ) O(1/t^2) O(1/t2),而 Momentum 为 O ( 1 / t ) O(1/t) O(1/t)。但在深度学习的非凸场景中,NAG 和 Momentum 的差异通常不大,NAG 稍快一些。
2.5 学习率调度策略
固定学习率无法适应训练全程的需求。常见调度策略:
| 策略 | 公式 | 特点 |
|---|---|---|
| Step Decay | η t = η 0 ⋅ γ ⌊ t / s ⌋ \eta_t = \eta_0 \cdot \gamma^{\lfloor t/s \rfloor} ηt=η0⋅γ⌊t/s⌋ | 每 s s s 步衰减为 γ \gamma γ 倍,简单实用 |
| Cosine Annealing | η t = η m i n + 1 2 ( η m a x − η m i n ) ( 1 + cos ( π t T ) ) \eta_t = \eta_{min} + \frac{1}{2}(\eta_{max} - \eta_{min})(1 + \cos(\frac{\pi t}{T})) ηt=ηmin+21(ηmax−ηmin)(1+cos(Tπt)) | 平滑衰减,末期收敛更稳 |
| OneCycle | 先从 η m i n \eta_{min} ηmin 线性升到 η m a x \eta_{max} ηmax,再余弦退火到 η m i n \eta_{min} ηmin | 前期快速探索,后期精细收敛 |
| Warmup | 前 T w a r m u p T_{warmup} Twarmup 步线性增长,之后按其他策略 | 避免训练初期梯度方差大导致发散 |
Cosine Annealing 推导:
η t = η m i n + 1 2 ( η m a x − η m i n ) ( 1 + cos ( π t T ) ) \eta_t = \eta_{min} + \frac{1}{2}(\eta_{max} - \eta_{min})\left(1 + \cos\left(\frac{\pi t}{T}\right)\right) ηt=ηmin+21(ηmax−ηmin)(1+cos(Tπt))
当 t = 0 t=0 t=0 时, cos ( 0 ) = 1 \cos(0) = 1 cos(0)=1, η 0 = η m a x \eta_0 = \eta_{max} η0=ηmax;当 t = T t=T t=T 时, cos ( π ) = − 1 \cos(\pi) = -1 cos(π)=−1, η T = η m i n \eta_T = \eta_{min} ηT=ηmin。整个过程从最大学习率平滑下降到最小学习率。
2.6 收敛性分析
对于凸函数 L ( θ ) L(\theta) L(θ) 满足 β \beta β-Lipschitz 连续梯度(即 ∥ ∇ L ( θ ) − ∇ L ( θ ′ ) ∥ ≤ β ∥ θ − θ ′ ∥ \|\nabla L(\theta) - \nabla L(\theta')\| \leq \beta \|\theta - \theta'\| ∥∇L(θ)−∇L(θ′)∥≤β∥θ−θ′∥):
BGD 收敛率 :当 η ≤ 1 β \eta \leq \frac{1}{\beta} η≤β1 时,
L ( θ T ) − L ( θ ∗ ) ≤ ∥ θ 0 − θ ∗ ∥ 2 2 η T L(\theta_T) - L(\theta^*) \leq \frac{\|\theta_0 - \theta^*\|^2}{2\eta T} L(θT)−L(θ∗)≤2ηT∥θ0−θ∗∥2
收敛率为 O ( 1 / T ) O(1/T) O(1/T)。
SGD 收敛率:
E L ( θ T ) − L ( θ ∗ ) ≤ ∥ θ 0 − θ ∗ ∥ 2 2 η T + η σ 2 2 EL(\\theta_T) - L(\theta^*) \leq \frac{\|\theta_0 - \theta^*\|^2}{2\eta T} + \frac{\eta \sigma^2}{2} EL(θT)−L(θ∗)≤2ηT∥θ0−θ∗∥2+2ησ2
其中 σ 2 \sigma^2 σ2 是梯度估计方差。SGD 的收敛率同样是 O ( 1 / T ) O(1/T) O(1/T),但多了一个与方差相关的噪声项。这说明:学习率需要随时间衰减以消除噪声项;增大 batch size 可以减小 σ 2 \sigma^2 σ2,但收益递减。
2.7 鞍点逃逸
在非凸优化中,鞍点比局部最优更常见。鞍点处 ∇ L ( θ ) = 0 \nabla L(\theta) = 0 ∇L(θ)=0,但 Hessian 矩阵既有正特征值也有负特征值。
SGD 如何逃逸鞍点? 靠梯度噪声。在鞍点附近,真实梯度为零,但小批量梯度的随机噪声会把参数推向负曲率方向(下降方向)。逃逸时间取决于 Hessian 负特征值的大小和梯度方差。
Momentum 如何帮助逃逸? 动量累积了历史梯度方向,如果历史梯度在某个方向有分量,即使当前梯度为零,动量也会推动参数继续移动,帮助穿越鞍点区域。
三、Python 实现
3.1 基础优化器实现
python
import numpy as np
from typing import Callable, Optional
from dataclasses import dataclass
class SGD:
"""随机梯度下降优化器"""
def __init__(self, lr: float = 0.01):
self.lr = lr
def step(self, params: np.ndarray, grads: np.ndarray) -> np.ndarray:
"""单步参数更新:θ = θ - η·g"""
return params - self.lr * grads
class Momentum:
"""带动量的随机梯度下降"""
def __init__(self, lr: float = 0.01, momentum: float = 0.9):
self.lr = lr
self.momentum = momentum
self.v: Optional[np.ndarray] = None
def step(self, params: np.ndarray, grads: np.ndarray) -> np.ndarray:
if self.v is None:
self.v = np.zeros_like(params)
# 动量累积:v = γ·v + η·g
self.v = self.momentum * self.v + self.lr * grads
return params - self.v
class NAG:
"""Nesterov Accelerated Gradient"""
def __init__(self, lr: float = 0.01, momentum: float = 0.9):
self.lr = lr
self.momentum = momentum
self.v: Optional[np.ndarray] = None
def step(self, params: np.ndarray,
grads_fn: Callable[[np.ndarray], np.ndarray]) -> np.ndarray:
if self.v is None:
self.v = np.zeros_like(params)
# 在展望位置计算梯度
look_ahead = params - self.momentum * self.v
grads = grads_fn(look_ahead)
self.v = self.momentum * self.v + self.lr * grads
return params - self.v
3.2 完整训练示例:在二次函数上对比三种优化器
python
@dataclass
class OptimResult:
"""优化过程记录"""
name: str
trajectory: list # 参数轨迹
losses: list # 损失轨迹
def optimize(optimizer_name: str, fn: Callable, grad_fn: Callable,
x0: np.ndarray, n_iter: int = 200,
lr: float = 0.01) -> OptimResult:
"""通用优化函数,支持 SGD / Momentum / NAG"""
x = x0.copy()
trajectory = [x.copy()]
losses = [fn(x)]
if optimizer_name == "sgd":
opt = SGD(lr=lr)
for _ in range(n_iter):
g = grad_fn(x)
x = opt.step(x, g)
trajectory.append(x.copy())
losses.append(fn(x))
elif optimizer_name == "momentum":
opt = Momentum(lr=lr, momentum=0.9)
for _ in range(n_iter):
g = grad_fn(x)
x = opt.step(x, g)
trajectory.append(x.copy())
losses.append(fn(x))
elif optimizer_name == "nag":
opt = NAG(lr=lr, momentum=0.9)
for _ in range(n_iter):
x = opt.step(x, grad_fn)
trajectory.append(x.copy())
losses.append(fn(x))
return OptimResult(optimizer_name, trajectory, losses)
if __name__ == "__main__":
# 椭圆型损失函数(条件数大):f(x, y) = 10x² + y²
def loss_fn(p: np.ndarray) -> float:
return 10 * p[0]**2 + p[1]**2
def grad_fn(p: np.ndarray) -> np.ndarray:
return np.array([20 * p[0], 2 * p[1]])
x0 = np.array([5.0, 5.0])
for name in ["sgd", "momentum", "nag"]:
result = optimize(name, loss_fn, grad_fn, x0,
n_iter=100, lr=0.01)
print(f"{name:10s}: final_loss={result.losses[-1]:.6f}, "
f"final_pos=({result.trajectory[-1][0]:.4f}, "
f"{result.trajectory[-1][1]:.4f})")
3.3 学习率调度器实现
python
import math
class StepLR:
"""阶梯衰减:每 step_size 步乘以 gamma"""
def __init__(self, initial_lr: float, step_size: int = 30,
gamma: float = 0.1):
self.initial_lr = initial_lr
self.step_size = step_size
self.gamma = gamma
def get_lr(self, epoch: int) -> float:
return self.initial_lr * (self.gamma ** (epoch // self.step_size))
class CosineAnnealingLR:
"""余弦退火"""
def __init__(self, eta_max: float, eta_min: float = 0.0,
T_max: int = 200):
self.eta_max = eta_max
self.eta_min = eta_min
self.T_max = T_max
def get_lr(self, epoch: int) -> float:
return self.eta_min + 0.5 * (self.eta_max - self.eta_min) * \
(1 + math.cos(math.pi * epoch / self.T_max))
class OneCycleLR:
"""OneCycle 调度:warmup + cosine annealing"""
def __init__(self, eta_max: float, eta_min: float = 0.0,
total_steps: int = 1000, warmup_pct: float = 0.3):
self.eta_max = eta_max
self.eta_min = eta_min
self.total_steps = total_steps
self.warmup_steps = int(total_steps * warmup_pct)
self.anneal_steps = total_steps - self.warmup_steps
def get_lr(self, step: int) -> float:
if step < self.warmup_steps:
# 线性 warmup
return self.eta_min + (self.eta_max - self.eta_min) * \
step / self.warmup_steps
else:
# cosine annealing
t = step - self.warmup_steps
return self.eta_min + 0.5 * (self.eta_max - self.eta_min) * \
(1 + math.cos(math.pi * t / self.anneal_steps))
if __name__ == "__main__":
print("StepLR:")
for e in [0, 29, 30, 59, 60, 99]:
print(f" epoch {e:3d}: lr = {StepLR(0.1).get_lr(e):.6f}")
print("\nCosineAnnealingLR:")
for e in [0, 50, 100, 150, 200]:
print(f" epoch {e:3d}: lr = "
f"{CosineAnnealingLR(0.1, T_max=200).get_lr(e):.6f}")
print("\nOneCycleLR:")
for e in [0, 75, 150, 300, 700, 999]:
print(f" step {e:4d}: lr = "
f"{OneCycleLR(0.1, total_steps=1000).get_lr(e):.6f}")
四、参数调优与变体对比
4.1 核心参数说明
| 参数 | 典型范围 | 作用 | 调参建议 |
|---|---|---|---|
| 学习率 η \eta η | 0.0001 ~ 1.0 | 控制 step size | 最重要参数,从 0.01 开始试 |
| 动量系数 γ \gamma γ | 0.5 ~ 0.99 | 历史梯度权重 | 默认 0.9,高曲率用 0.99 |
| Batch Size | 16 ~ 256 | 梯度估计精度 | 显存允许越大越好 |
| Weight Decay | 1e-5 ~ 1e-3 | L2 正则化 | 防过拟合,与学习率联动 |
4.2 SGD / Momentum / NAG 对比
| 特性 | SGD | Momentum | NAG |
|---|---|---|---|
| 计算量/步 | 1×梯度 | 1×梯度 + 1×向量乘加 | 1×梯度(展望位置)+ 1×向量乘加 |
| 内存开销 | 无额外 | 额外 v v v(同参数量) | 额外 v v v(同参数量) |
| 震荡抑制 | 无 | 强 | 强 |
| 收敛速度 | 慢 | 快(~ 1 1 − γ \frac{1}{1-\gamma} 1−γ1×加速) | 略快于 Momentum |
| 凸函数收敛率 | O ( 1 / T ) O(1/T) O(1/T) | O ( 1 / T ) O(1/T) O(1/T) | O ( 1 / T 2 ) O(1/T^2) O(1/T2) |
| 超参敏感度 | 高(对学习率敏感) | 中(动量缓冲了学习率波动) | 中 |
| 适用场景 | 简单模型、大 batch | 通用默认选择 | 理论偏好、凸优化 |
4.3 学习率调度策略对比
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Step Decay | 简单、可解释 | 衰减点突变、需人工设定 | 通用、快速实验 |
| Cosine | 平滑衰减、末期稳定 | 需预设 T m a x T_{max} Tmax | 训练周期固定 |
| OneCycle | 自动 warmup、收敛快 | 超参多、不灵活 | 迁移学习、微调 |
| Warmup + Cosine | 稳定启动、平滑收敛 | 需调 warmup 步数 | 大模型训练 |
五、在推荐系统中的实际应用
以推荐系统的 Embedding 训练为例。假设有用户特征 u ∈ R d u \in \mathbb{R}^d u∈Rd 和物品特征 v ∈ R d v \in \mathbb{R}^d v∈Rd,预测点击概率为 y ^ = σ ( u T v ) \hat{y} = \sigma(u^T v) y^=σ(uTv),损失函数为交叉熵:
L = − 1 N ∑ i = 1 N y i log y \^ i + ( 1 − y i ) log ( 1 − y \^ i ) L = -\frac{1}{N} \sum_{i=1}^{N} y_i \\log \\hat{y}_i + (1-y_i) \\log(1-\\hat{y}_i) L=−N1i=1∑Nyilogy\^i+(1−yi)log(1−y\^i)
实际工程中的配置选择:
- Embedding 维度 d=64,batch_size=256:用 Momentum(lr=0.01, momentum=0.9),训练 100 epoch + Cosine Annealing
- 冷启动特征多、梯度方差大:加 Warmup(前 5% steps 线性升温),避免初期梯度噪声导致 Embedding 分布崩坏
- 特征稀疏、部分 ID 出现频率低:用 SGD 而非 Momentum(稀疏特征下动量几乎没有累积效果),或用自适应学习率方法如 Adam
python
# 推荐系统 Embedding 训练示例(简化版)
import numpy as np
class EmbeddingModel:
"""简化版双塔推荐模型"""
def __init__(self, n_users: int, n_items: int, dim: int = 64):
self.user_emb = np.random.randn(n_users, dim) * 0.01
self.item_emb = np.random.randn(n_items, dim) * 0.01
self.dim = dim
def forward(self, user_ids: np.ndarray,
item_ids: np.ndarray) -> np.ndarray:
u = self.user_emb[user_ids] # (batch, dim)
v = self.item_emb[item_ids] # (batch, dim)
return 1.0 / (1.0 + np.exp(-np.sum(u * v, axis=1)))
def loss(self, y_pred: np.ndarray, y_true: np.ndarray) -> float:
eps = 1e-7
return -np.mean(y_true * np.log(y_pred + eps) +
(1 - y_true) * np.log(1 - y_pred + eps))
def grad(self, user_ids: np.ndarray, item_ids: np.ndarray,
y_pred: np.ndarray, y_true: np.ndarray):
"""计算 Embedding 梯度"""
diff = (y_pred - y_true).reshape(-1, 1) # (batch, 1)
u_grad = np.zeros_like(self.user_emb)
v_grad = np.zeros_like(self.item_emb)
for i in range(len(user_ids)):
u_grad[user_ids[i]] += diff[i] * self.item_emb[item_ids[i]]
v_grad[item_ids[i]] += diff[i] * self.user_emb[user_ids[i]]
return u_grad, v_grad
if __name__ == "__main__":
n_users, n_items, dim = 1000, 5000, 64
model = EmbeddingModel(n_users, n_items, dim)
np.random.seed(42)
train_users = np.random.randint(0, n_users, 10000)
train_items = np.random.randint(0, n_items, 10000)
train_labels = (train_users + train_items) % 2
opt = Momentum(lr=0.01, momentum=0.9)
opt.v = np.zeros(n_users * dim + n_items * dim)
batch_size = 256
for epoch in range(50):
idx = np.random.permutation(len(train_users))
epoch_loss = 0
for start in range(0, len(train_users), batch_size):
batch_idx = idx[start:start + batch_size]
u_ids = train_users[batch_idx]
i_ids = train_items[batch_idx]
y_true = train_labels[batch_idx].astype(float)
y_pred = model.forward(u_ids, i_ids)
epoch_loss += model.loss(y_pred, y_true) * len(batch_idx)
u_grad, v_grad = model.grad(u_ids, i_ids, y_pred, y_true)
params = np.concatenate([model.user_emb.ravel(),
model.item_emb.ravel()])
grads = np.concatenate([u_grad.ravel(),
v_grad.ravel()])
params = opt.step(params, grads)
model.user_emb = params[:n_users * dim].reshape(n_users, dim)
model.item_emb = params[n_users * dim:].reshape(n_items, dim)
if (epoch + 1) % 10 == 0:
print(f"Epoch {epoch + 1:3d}, "
f"Loss: {epoch_loss / len(train_users):.4f}")
六、常见陷阱
陷阱 1:学习率设置过大导致 Loss 爆炸
--- 当 Loss 突然变成 NaN 或 inf,通常是学习率过大。梯度爆炸后参数更新到极端值,激活函数进入饱和区或数值溢出。解决方案:加梯度裁剪(gradient clipping),或用 warmup 从小学习率开始。
陷阱 2:动量系数设为 1.0 导致不收敛--- γ = 1.0 \gamma=1.0 γ=1.0 意味着动量不衰减,历史梯度永远累积。在震荡方向上梯度无法抵消,参数飞发散。 γ \gamma γ 必须严格小于 1,通常 0.9 或 0.99。
陷阱 3:NAG 实现错误--- 常见错误是在 Momentum 代码上"加一点"改动但搞错了展望位置。正确的 NAG 是在 θ t − γ v t − 1 \theta_t - \gamma v_{t-1} θt−γvt−1 处计算梯度,而不是在 θ t − v t − 1 \theta_t - v_{t-1} θt−vt−1 处。注意 PyTorch 中
Nesterov=True的实现方式做了等价变形,并非直接照搬原公式。
陷阱 4:忽略了 batch size 与学习率的 scaling--- 线性缩放规则(Linear Scaling Rule):当 batch size 扩大 k k k 倍,学习率也扩大 k k k 倍。但这只在 batch size 不太大时成立。大 batch 训练(>8K)通常需要更精细的 warmup 策略。
陷阱 5:在稀疏梯度场景下用 Momentum--- 当大部分参数每步梯度为零(如 NLP 中低频词的 Embedding),Momentum 的动量向量也全为零,等于退化为 SGD。此时应使用自适应学习率方法(如 Adam),或对稀疏参数单独设置学习率。
七、总结
| 维度 | SGD | Momentum | NAG |
|---|---|---|---|
| 核心公式 | θ t + 1 = θ t − η g t \theta_{t+1} = \theta_t - \eta g_t θt+1=θt−ηgt | v t = γ v t − 1 + η g t v_t = \gamma v_{t-1} + \eta g_t vt=γvt−1+ηgt; θ t + 1 = θ t − v t \theta_{t+1} = \theta_t - v_t θt+1=θt−vt | v t = γ v t − 1 + η ∇ L ( θ t − γ v t − 1 ) v_t = \gamma v_{t-1} + \eta \nabla L(\theta_t - \gamma v_{t-1}) vt=γvt−1+η∇L(θt−γvt−1) |
| 关键参数 | η \eta η(学习率) | η \eta η、 γ \gamma γ(动量系数) | η \eta η、 γ \gamma γ |
| 优势 | 简单、噪声有助于逃离鞍点 | 震荡抑制、加速收敛 | 理论收敛率更优、前瞻减少冲过头 |
| 降级策略 | 学习率衰减 + 梯度裁剪 | 降低 γ \gamma γ 或退回 SGD | 退回 Momentum |
| 选型建议 | 稀疏梯度、大 batch | 通用默认选择 | 凸优化、理论偏好 |
| 常用学习率 | 0.01 ~ 0.1 | 0.001 ~ 0.01 | 0.001 ~ 0.01 |
核心建议:没有"最优"的优化器,只有"最适合当前场景"的优化器。从 SGD + Momentum(lr=0.01, momentum=0.9) + Cosine Annealing 开始是 90% 场景下的安全选择。遇到不收敛先查学习率,遇到收敛慢再调调度策略。