深度学习从入门到精通 - 梯度下降优化全攻略:SGD、Adam等七大优化器对比
各位,今天咱们要深度剖析模型训练中最关键的引擎——优化器。说个掏心窝的话,我见过太多初学者在模型调参时疯狂调整网络结构,却对优化器的选择一知半解… 这就像给跑车加92号汽油还纳闷为啥跑不快!本文将手把手带大家拆解七大主流优化器的核心原理,更重要的是——分享那些我踩过的坑和深夜Debug的血泪经验。
第一章 为什么梯度下降需要优化?
想象你被困在云雾缭绕的山谷里(损失函数曲面),只能靠脚下的坡度(梯度)摸索下山路径。最朴素的方案就是沿着最陡方向前进——这就是批量梯度下降(BGD):
# 传统BGD伪代码
for epoch in range(epochs):
grad = compute_gradient(entire_dataset) # 计算全量梯度
weights = weights - learning_rate * grad # 沿负梯度更新
致命缺陷来了:当数据集达到百万级时,单次梯度计算就能让GPU冒烟。更糟的是——这里藏着我踩过的第一个大坑:局部最优陷阱。下图展示了几种典型地形:
特别是鞍点区域——梯度接近于零却非全局最优,传统BGD会完全停滞。为了解决这些痛点,优化器进化史拉开帷幕…
第二章 随机梯度下降(SGD):最基础的引擎
直接抛弃全量数据,每次随机采样一个小批次(mini-batch)计算梯度:
# SGD核心代码(PyTorch版)
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
for batch in dataloader:
loss = model(batch)
loss.backward() # 计算梯度
optimizer.step() # 参数更新
optimizer.zero_grad() # 清零梯度!这是新手常忘的坑
学习率(lr)的选择艺术:这里必须插播血泪史——去年在CIFAR-10实验时,我设了lr=0.5,结果损失值直接NaN!原因在于:
wt+1=wt−η∇J(wt) w_{t+1} = w_t - \eta \nabla J(w_t) wt+1=wt−η∇J(wt)
当梯度∇J\nabla J∇J较大时(比如遇到悬崖地形),步长η∇J\eta \nabla Jη∇J会击穿数值范围。补救方案:添加梯度裁剪:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
第三章 动量法(Momentum):让优化拥有惯性
SGD在山谷中容易走"之字形"(梯度方向剧烈震荡)。动量法引入物理中的惯性概念:
vt=γvt−1+η∇J(wt)wt+1=wt−vt \begin{aligned} v_t &= \gamma v_{t-1} + \eta \nabla J(w_t) \\ w_{t+1} &= w_t - v_t \end{aligned} vtwt+1=γvt−1+η∇J(wt)=wt−vt
物理意义解读:γ\gammaγ(通常取0.9)像摩擦系数,vtv_tvt是速度矢量。即使当前梯度为零,动量仍推动参数前进——对逃离鞍点奇效!
optimizer = torch.optim.SGD(model.parameters(),
lr=0.01,
momentum=0.9) # 启用动量
踩坑记录:在RNN训练中,动量过大会导致文本生成结果崩坏。建议NLP任务中γ≤0.7\gamma \leq 0.7γ≤0.7
第四章 NAG(Nesterov):更聪明的动量
NAG改写了动量规则:“既然有速度惯性,何不先看看下一位置?”
vt=γvt−1+η∇J(wt−γvt−1)wt+1=wt−vt \begin{aligned} v_t &= \gamma v_{t-1} + \eta \nabla J(w_t - \gamma v_{t-1}) \\ w_{t+1} &= w_t - v_t \end{aligned} vtwt+1=γvt−1+η∇J(wt−γvt−1)=wt−vt
关键差异:梯度计算位置从wtw_twt变为(wt−γvt−1)(w_t - \gamma v_{t-1})(wt−γvt−1)(预测位置)。实验证明在凸函数上收敛更快。
optimizer = torch.optim.SGD(model.parameters(),
lr=0.01,
momentum=0.9,
nesterov=True) # 启用NAG
第五章 自适应学习率三巨头
5.1 AdaGrad:为参数定制学习率
核心思想:频繁更新的参数应减小步幅(学习率),稀疏参数则增大步幅。数学实现:
Gt=Gt−1+(∇J(wt))2wt+1=wt−ηGt+ϵ∇J(wt) \begin{aligned} G_t &= G_{t-1} + (\nabla J(w_t))^2 \\ w_{t+1} &= w_t - \frac{\eta}{\sqrt{G_t + \epsilon}} \nabla J(w_t) \end{aligned} Gtwt+1=Gt−1+(∇J(wt))2=wt−Gt+ϵη∇J(wt)
致命缺陷:GtG_tGt单调递增导致后期学习率趋近于零!我在训练ResNet34时,epoch>50后loss纹丝不动…
5.2 RMSProp:滑动平均救场
针对AdaGrad的缺陷,引入衰减系数β\betaβ(通常0.9):
E[g2]t=βE[g2]t−1+(1−β)(∇J(wt))2wt+1=wt−ηE[g2]t+ϵ∇J(wt) \begin{aligned} E[g^2]_t &= \beta E[g^2]_{t-1} + (1-\beta) (\nabla J(w_t))^2 \\ w_{t+1} &= w_t - \frac{\eta}{\sqrt{E[g^2]_t + \epsilon}} \nabla J(w_t) \end{aligned} E[g2]twt+1=βE[g2]t−1+(1−β)(∇J(wt))2=wt−E[g2]t+ϵη∇J(wt)
代码实现差异:
# AdaGrad (逐渐淘汰)
optimizer = torch.optim.Adagrad(model.parameters(), lr=0.01)
# RMSProp (推荐替代)
optimizer = torch.optim.RMSprop(model.parameters(),
lr=0.001,
alpha=0.9) # alpha即β
5.3 Adadelta:连学习率都省了
进一步魔改RMSProp,动态计算η\etaη:
Δwt=−E[Δw2]t−1+ϵE[g2]t+ϵ∇J(wt)E[Δw2]t=γE[Δw2]t−1+(1−γ)(Δwt)2 \begin{aligned} \Delta w_t &= -\frac{\sqrt{E[\Delta w^2]_{t-1} + \epsilon}}{\sqrt{E[g^2]_t + \epsilon}} \nabla J(w_t) \\ E[\Delta w^2]_t &= \gamma E[\Delta w^2]_{t-1} + (1-\gamma) (\Delta w_t)^2 \end{aligned} ΔwtE[Δw2]t=−E[g2]t+ϵE[Δw2]t−1+ϵ∇J(wt)=γE[Δw2]t−1+(1−γ)(Δwt)2
优势:完全摆脱手动调学习率。坑点:初期更新量极小,图像分类任务前10epoch几乎无进展。
第六章 Adam:动量+自适应的终极融合
终于轮到今天的明星——Adam(Adaptive Moment Estimation)。它结合了动量法和RMSProp:
mt=β1mt−1+(1−β1)∇J(wt)(动量)vt=β2vt−1+(1−β2)(∇J(wt))2(自适应)m^t=mt1−β1t(偏差修正)v^t=vt1−β2twt+1=wt−ηv^t+ϵm^t \begin{aligned} m_t &= \beta_1 m_{t-1} + (1-\beta_1) \nabla J(w_t) \quad \text{(动量)} \\ v_t &= \beta_2 v_{t-1} + (1-\beta_2) (\nabla J(w_t))^2 \quad \text{(自适应)} \\ \hat{m}_t &= \frac{m_t}{1-\beta_1^t} \quad \text{(偏差修正)} \\ \hat{v}_t &= \frac{v_t}{1-\beta_2^t} \\ w_{t+1} &= w_t - \frac{\eta}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t \end{aligned} mtvtm^tv^twt+1=β1mt−1+(1−β1)∇J(wt)(动量)=β2vt−1+(1−β2)(∇J(wt))2(自适应)=1−β1tmt(偏差修正)=1−β2tvt=wt−v^t+ϵηm^t
偏差修正的必要性:初期mt,vtm_t,v_tmt,vt接近零会导致更新量过小。修正项1/(1−βt)1/(1-\beta^t)1/(1−βt)在t较小时放大数值。
# Adam经典配置
optimizer = torch.optim.Adam(model.parameters(),
lr=3e-4,
betas=(0.9, 0.999)) # (β1, β2)
血泪经验:
- 默认参数betas=(0.9,0.999)在90%场景表现良好
- 遇到震荡时调大β1\beta_1β1(如0.99)
- 稀疏数据(如推荐系统)慎用Adam!可能不如SGD
第七章 优化器性能横评
我在MNIST和CIFAR-10上做了对比实验(3层CNN),结果如下:
优化器 | 训练速度 | 收敛稳定性 | 超参敏感度 | 推荐场景 |
---|---|---|---|---|
SGD | 慢 | 高 | 极高 | 凸问题、微调 |
Momentum | 中等 | 高 | 高 | 图像分类 |
Adam | 快 | 中等 | 低 | NLP、GAN、默认选 |
Adadelta | 慢 | 低 | 极低 | 强化学习 |
个人建议:
- 新手首选Adam (lr=3e-4)
- 微调预训练模型用SGD+动量
- RNN/LSTM可尝试RMSProp
终章 优化器选择决策树
遇到新任务时,按此流程图决策:
graph TB
A[新任务] --> B{数据稀疏?}
B -->|是| C[SGD with Momentum]
B -->|否| D{计算资源受限?}
D -->|是| E[RMSProp]
D -->|否| F{需要快速原型?}
F -->|是| G[Adam]
F -->|否| H[NAG or SGD]
最后说个反直觉的结论:在足够长的训练时间和精心调参下,SGD往往能达到比Adam更优的泛化性能——这也是ResNet论文的选择。但记住——没有银弹,持续实验才是王道!