深度学习从入门到精通 - 梯度下降优化全攻略:SGD、Adam等七大优化器对比

发布于:2025-09-05 ⋅ 阅读:(14) ⋅ 点赞:(0)

深度学习从入门到精通 - 梯度下降优化全攻略: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 JJ较大时(比如遇到悬崖地形),步长η∇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=γvt1+ηJ(wt)=wtvt

物理意义解读γ\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=γvt1+ηJ(wtγvt1)=wtvt

关键差异:梯度计算位置从wtw_twt变为(wt−γvt−1)(w_t - \gamma v_{t-1})(wtγvt1)(预测位置)。实验证明在凸函数上收敛更快。

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=Gt1+(J(wt))2=wtGt+ϵ η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]t1+(1β)(J(wt))2=wtE[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]t1+ϵ J(wt)=γE[Δw2]t1+(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=β1mt1+(1β1)J(wt)(动量)=β2vt1+(1β2)(J(wt))2(自适应)=1β1tmt(偏差修正)=1β2tvt=wtv^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)

血泪经验

  1. 默认参数betas=(0.9,0.999)在90%场景表现良好
  2. 遇到震荡时调大β1\beta_1β1(如0.99)
  3. 稀疏数据(如推荐系统)慎用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论文的选择。但记住——没有银弹,持续实验才是王道!


网站公告

今日签到

点亮在社区的每一天
去签到