摘要: 优化器是深度学习模型训练的核心组件,其选择直接影响模型的收敛速度、最终性能以及训练稳定性。本文系统梳理了从经典SGD到现代Adam系列的主流优化算法,深入剖析各优化器的数学原理、优劣势及适用场景。通过PyTorch实现完整对比实验,涵盖SGD、Adam、AdamW、RMSProp、AdaGrad等核心优化器,以及学习率调度策略。实验表明,不同优化器在收敛速度、泛化能力等方面存在显著差异,实际应用中需根据任务特点进行合理选择。
关键词: 深度学习;优化器;Adam;SGD;学习率调度;PyTorch
1. 引言
深度学习的训练过程本质上是一个优化问题:我们希望通过最小化损失函数来学习模型的参数。优化器(Optimizer)正是解决这一问题的核心算法。从1952年Robert Widrow提出的最小均方算法(LMS)开始,优化算法经历了数十年的发展演进。从早期的批量梯度下降(BGD)到随机梯度下降(SGD),再到自适应学习率算法如Adam、RMSProp,每一次算法革新都推动了深度学习在更大规模、更高复杂度任务上的突破。
本文将深入剖析深度学习中主流优化器的数学原理与工程实践,帮助读者建立完整的优化器知识体系。
2. 优化问题基础
2.1 损失函数的Landscape
深度学习模型的损失函数通常是一个高维非凸函数。以一个简单的二维函数为例,其landscape可能包含多种地形特征:
/\
/ \ 局部最小值
/ \ /\
/ \ / \
/ /\ \ / \
/ / \ \ / \
----/---/----\---X--------\----> 参数空间
\ / 鞍点
\ /
\ /
\/ 全局最小值
三种关键地形:
-
局部最小值(Local Minimum):损失函数在某一小区域内达到最低点,但在全局并非最低。传统观点曾认为局部最小值是训练的主要障碍,但近年研究表明,高维空间中的局部最小值往往具有接近全局最优的损失值。
-
鞍点(Saddle Point):损失函数在某些方向上是局部最小,在另一些方向上是局部最大。鞍点附近梯度接近零,导致传统梯度下降算法容易停滞,是训练中的主要挑战之一。
-
梯度消失与梯度爆炸:当网络层数加深时,梯度在反向传播过程中可能指数级衰减(梯度消失)或指数级放大(梯度爆炸),严重影响深层网络的训练。
2.2 优化器的核心作用
优化器的核心任务是在参数空间中寻找损失函数的极小值点。具体而言,给定损失函数 J(\\theta),优化器通过以下迭代公式更新参数:
\\theta_{t+1} = \\theta_t - \\alpha \\cdot \\nabla J(\\theta_t)
其中 \\alpha 是学习率,\\nabla J(\\theta_t) 是损失函数在当前参数处的梯度。优化器的改进主要体现在三个方面:
-
加速收敛:更快地到达极小值区域
-
逃离鞍点:更好地处理高维非凸 landscape
-
提高泛化:找到的解具有更好的泛化能力
3. 随机梯度下降(SGD)
3.1 基本原理
SGD(Stochastic Gradient Descent)是深度学习中最基础也是最重要的优化算法。与批量梯度下降不同,SGD每次只使用一个样本或一个小批量样本来计算梯度,这使得它在处理大规模数据集时具有显著的计算优势。
核心公式:
\\theta = \\theta - \\eta \\cdot \\nabla_\\theta J(\\theta; x\^{(i)}, y\^{(i)})
其中:
-
\\theta 是模型参数
-
\\eta 是学习率(learning rate)
-
\\nabla_\\theta J(\\theta; x\^{(i)}, y\^{(i)}) 是第 i 个样本的损失函数梯度
3.2 学习率的影响
学习率是SGD中最关键的超参数,其选择直接影响训练效果:
import numpy as np
import matplotlib.pyplot as plt
def sgd_convergence(grad_func, lr, n_steps):
"""模拟SGD收敛过程"""
theta = 5.0 # 初始参数值
trajectory = [theta]
for _ in range(n_steps):
gradient = grad_func(theta) # 模拟的梯度
theta = theta - lr * gradient
trajectory.append(theta)
return trajectory
# 假设的凸函数 f(x) = x^2,其梯度为 2x
grad = lambda x: 2 * x
# 不同学习率的收敛情况
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
for ax, lr, title in zip(axes, [0.01, 0.3, 0.8], ['学习率过小 (0.01)', '学习率适中 (0.3)', '学习率过大 (0.8)']):
trajectory = sgd_convergence(grad, lr, 20)
ax.plot(trajectory, 'b-', linewidth=2)
ax.axhline(y=0, color='r', linestyle='--', label='最优解')
ax.set_title(title)
ax.set_xlabel('迭代次数')
ax.set_ylabel('参数值')
ax.legend()
ax.grid(True)
plt.tight_layout()
plt.savefig('sgd_lr_effect.png', dpi=150)
plt.show()
学习率选择建议:
-
学习率过小:收敛速度极慢,容易陷入局部最优
-
学习率适中:稳定收敛到全局最优
-
学习率过大:无法收敛,在最优点附近震荡甚至发散
3.3 动量(Momentum)
标准SGD在陡峭方向上震荡剧烈,而在平缓方向上进展缓慢。动量机制通过累积历史梯度信息来加速收敛、抑制震荡。
动量更新公式:
v_t = \\beta \\cdot v*{t-1} + (1 - \\beta) \\cdot \\nabla*\\theta J(\\theta)$$ $$\\theta = \\theta - \\eta \\cdot v_t
其中 v_t 是速度项,\\beta \\in \[0, 1) 是动量系数,通常取0.9。
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
def momentum_sgd(grad_func, theta, lr, beta, n_steps):
"""
模拟带动量的SGD
grad_func: 梯度函数
theta: 初始参数
lr: 学习率
beta: 动量系数
"""
v = 0.0 # 初始速度
trajectory = [theta]
for _ in range(n_steps):
gradient = grad_func(theta)
v = beta * v + (1 - beta) * gradient # 动量更新
theta = theta - lr * v
trajectory.append(theta)
return trajectory
# Rosenbrock函数(经典的非凸测试函数)
# f(x, y) = (1-x)^2 + 100(y-x^2)^2, 梯度复杂,存在狭长山谷
def rosenbrock_grad(theta):
x, y = theta[0], theta[1]
dx = -2*(1-x) - 400*x*(y-x**2)
dy = 200*(y - x**2)
return np.array([dx, dy])
# 初始点
theta0 = np.array([-1.5, 1.5])
# 对比有无动量
traj_no_momentum = momentum_sgd(rosenbrock_grad, theta0.copy(), 0.0005, 0.0, 1000)
traj_with_momentum = momentum_sgd(rosenbrock_grad, theta0.copy(), 0.0005, 0.9, 1000)
print(f"无动量: 最终位置 {traj_no_momentum[-1]}, 损失 {rosenbrock_loss(traj_no_momentum[-1]):.4f}")
print(f"有动量: 最终位置 {traj_with_momentum[-1]}, 损失 {rosenbrock_loss(traj_with_momentum[-1]):.4f}")
3.4 Nesterov动量
Nesterov Accelerated Gradient(NAG)是动量算法的一种改进版本。与标准动量不同,NAG先根据历史速度做一个"预测性"跳跃,然后再计算梯度进行校正。
Nesterov动量公式:
v_t = \\beta \\cdot v*{t-1} + (1 - \\beta) \\cdot \\nabla*\\theta J(\\theta - \\eta \\cdot \\beta \\cdot v_{t-1})$$ $$\\theta = \\theta - \\eta \\cdot v_t
def nesterov_momentum(grad_func, theta, lr, beta, n_steps):
"""
Nesterov动量SGD
关键区别:先预测后校正
"""
v = np.zeros_like(theta, dtype=float)
trajectory = [theta.copy()]
for _ in range(n_steps):
# 预测步骤:沿着历史速度方向前移
theta_predict = theta - lr * beta * v
# 校正步骤:在预测位置计算梯度
gradient = grad_func(theta_predict)
# 更新速度
v = beta * v + (1 - beta) * gradient
# 更新参数
theta = theta - lr * v
trajectory.append(theta.copy())
return trajectory
Nesterov vs 标准动量的核心差异:
| 特性 | 标准动量 | Nesterov动量 |
|---|---|---|
| 梯度计算位置 | 当前参数 \\theta_t | 预测位置 \\theta_t - \\eta \\cdot \\beta \\cdot v_t |
| 动量累积方向 | 历史速度方向 | 历史速度方向 + 校正方向 |
| 收敛速度 | 较快 | 更稳定、更快 |
| 适用场景 | 一般深度学习任务 | 需要更稳定收敛的场景 |
4. 自适应学习率优化器
固定学习率的主要问题在于:不同参数需要不同的学习率。例如:
-
频繁更新的参数可能需要更小的学习率
-
稀疏特征对应的参数可能需要更大的学习率
自适应学习率优化器通过自动调节每个参数的学习率来解决这一问题。
4.1 AdaGrad
AdaGrad(Adaptive Gradient Algorithm)通过记录每个参数历史梯度的平方和来实现自适应学习率。
核心公式:
g_t = \\nabla*\\theta J(\\theta_t) \\quad \\text{(当前梯度)}$$ $$r_t = r*{t-1} + g_t \\odot g_t \\quad \\text{(累积平方梯度)}$$ $$\\theta_{t+1} = \\theta_t - \\frac{\\eta}{\\sqrt{r_t} + \\delta} \\odot g_t
其中 \\odot 表示逐元素乘法,\\delta 通常取 10\^{-8} 防止除零。
class AdaGrad:
"""
AdaGrad优化器实现
优点:对稀疏数据友好
缺点:学习率会单调递减,后期可能过早收敛
"""
def __init__(self, params, lr=1.0, eps=1e-8):
self.params = list(params)
self.lr = lr
self.eps = eps
self.state = {} # 存储累积平方梯度
def step(self):
for param in self.params:
if param.grad is None:
continue
grad = param.grad.data
# 初始化状态
if param not in self.state:
self.state[param] = torch.zeros_like(param.data)
# 累积平方梯度
self.state[param] += grad.pow(2)
# 自适应学习率更新
# 学习率 = lr / sqrt(累积平方和)
adaptive_lr = self.lr / (self.state[param].sqrt() + self.eps)
param.data -= adaptive_lr * grad
def zero_grad(self):
for param in self.params:
if param.grad is not None:
param.grad.zero_()
适用场景: 文本处理、词嵌入训练等存在大量稀疏特征的任务。
4.2 RMSProp
RMSProp(Root Mean Square Propagation)由Geoff Hinton提出,是对AdaGrad的改进。AdaGrad的致命问题在于学习率单调递减,RMSProp通过引入指数移动平均来解决。
核心公式:
E\[g\^2\]*t = \\beta \\cdot E\[g\^2\]* {t-1} + (1-\\beta) \\cdot g_t\^2$$ $$\\theta*{t+1} = \\theta_t - \\frac{\\eta}{\\sqrt{E\[g\^2\]*t} + \\delta} \\cdot g_t
其中 \\beta 通常取0.9。
class RMSProp:
"""
RMSProp优化器
使用指数移动平均代替直接累加,避免学习率过快下降
"""
def __init__(self, params, lr=1e-3, alpha=0.99, eps=1e-8, weight_decay=0):
self.params = list(params)
self.lr = lr
self.alpha = alpha # 指数移动平均系数
self.eps = eps
self.weight_decay = weight_decay
self.state = {} # 存储指数移动平均的平方梯度
def step(self):
for param in self.params:
if param.grad is None:
continue
grad = param.grad.data
# L2正则化(可选)
if self.weight_decay != 0:
grad = grad + self.weight_decay * param.data
# 初始化或更新指数移动平均
if param not in self.state:
self.state[param] = torch.zeros_like(param.data)
self.state[param] = self.alpha * self.state[param] + (1 - self.alpha) * grad.pow(2)
# 自适应学习率更新
adaptive_lr = self.lr / (self.state[param].sqrt() + self.eps)
param.data -= adaptive_lr * grad
def zero_grad(self):
for param in self.params:
if param.grad is not None:
param.grad.zero_()
4.3 AdaDelta
AdaDelta是RMSProp的进一步改进,其核心创新在于不需要人工设置全局学习率,而是通过参数更新的二阶近似来自动计算学习率。
核心公式:
E\[g\^2\]*t = \\beta \\cdot E\[g\^2\]* {t-1} + (1-\\beta) \\cdot g_t\^2$$ $$\\Delta\\theta_t = -\\frac{\\sqrt{E\[\\Delta\\theta\^2\]*{t-1} + \\epsilon}}{\\sqrt{E\[g\^2\]* t + \\epsilon}} \\cdot g_t$$ $$E\[\\Delta\\theta\^2\]*t = \\beta \\cdot E\[\\Delta\\theta\^2\]*{t-1} + (1-\\beta) \\cdot \\Delta\\theta_t\^2
class AdaDelta:
"""
AdaDelta优化器
不需要手动设置学习率,通过累积参数更新的移动平均来自适应调节
"""
def __init__(self, params, rho=0.9, eps=1e-6):
self.params = list(params)
self.rho = rho
self.eps = eps
self.state = {} # 存储EG和Edtheta
def step(self):
for param in self.params:
if param.grad is None:
continue
grad = param.grad.data
# 初始化状态
if param not in self.state:
self.state[param] = {
'EG': torch.zeros_like(param.data), # 梯度平方的指数移动平均
'Edtheta': torch.zeros_like(param.data) # 参数更新的指数移动平均
}
state = self.state[param]
# 更新梯度平方的移动平均
state['EG'] = self.rho * state['EG'] + (1 - self.rho) * grad.pow(2)
# 计算自适应学习率
# 注意:Edtheta开方后作为学习率的分子
delta = (state['Edtheta'].sqrt() + self.eps) / (state['EG'].sqrt() + self.eps) * grad
# 更新参数
param.data -= delta
# 更新参数更新平方的移动平均
state['Edtheta'] = self.rho * state['Edtheta'] + (1 - self.rho) * delta.pow(2)
def zero_grad(self):
for param in self.params:
if param.grad is not None:
param.grad.zero_()
5. Adam优化器
Adam(Adaptive Moment Estimation)是目前最广泛使用的优化器,它结合了动量法和RMSProp的优点,同时引入了偏置修正机制。
5.1 核心原理
Adam维护两个指数移动平均:
-
一阶矩估计 m_t(类似动量):估计梯度的一阶矩,即梯度的"均值"
-
二阶矩估计 v_t(类似RMSProp):估计梯度的二阶矩,即梯度的"方差"
完整算法流程:
输入:学习率 η,矩估计衰减系数 β1, β2,数值稳定常数 δ
初始化:θ0, m0=0, v0=0, t=0
while 未收敛 do
t = t + 1
# 计算梯度
g_t = ∇θJ(θ_{t-1})
# 更新一阶矩估计(动量)
m_t = β1 · m_{t-1} + (1-β1) · g_t
# 更新二阶矩估计(RMSProp)
v_t = β2 · v_{t-1} + (1-β2) · g_t^2
# 偏置修正(重要!非常重要!)
m̂_t = m_t / (1 - β1^t)
v̂_t = v_t / (1 - β2^t)
# 参数更新
θ_t = θ_{t-1} - η · m̂_t / (√v̂_t + δ)
end while
5.2 偏置修正(Bias Correction)
为什么Adam需要偏置修正?让我们分析一下:
初始化时 m_0 = 0, v_0 = 0,在初期迭代中:
-
一阶矩:m_1 = \\beta_1 \\cdot 0 + (1-\\beta_1) \\cdot g_1 = (1-\\beta_1) \\cdot g_1
-
由于 \\beta_1\^t 接近0,m_1 被严重低估
偏置修正通过除以 (1 - \\beta\^t) 来校正这种低估,确保早期估计的准确性。
import torch
import torch.nn as nn
class Adam:
"""
Adam优化器完整实现
结合了动量法和RMSProp的优点
"""
def __init__(self, params, lr=1e-3, beta1=0.9, beta2=0.999, eps=1e-8, weight_decay=0):
self.params = list(params)
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.eps = eps
self.weight_decay = weight_decay
self.t = 0 # 迭代计数器
self.state = {} # 存储m和v
def step(self):
self.t += 1
for param in self.params:
if param.grad is None:
continue
grad = param.grad.data
# L2正则化
if self.weight_decay != 0:
grad = grad + self.weight_decay * param.data
# 初始化或更新状态
if param not in self.state:
self.state[param] = {
'm': torch.zeros_like(param.data),
'v': torch.zeros_like(param.data)
}
state = self.state[param]
# 更新一阶矩估计(动量)
state['m'] = self.beta1 * state['m'] + (1 - self.beta1) * grad
# 更新二阶矩估计
state['v'] = self.beta2 * state['v'] + (1 - self.beta2) * grad.pow(2)
# 偏置修正
m_hat = state['m'] / (1 - self.beta1 ** self.t)
v_hat = state['v'] / (1 - self.beta2 ** self.t)
# 参数更新
param.data -= self.lr * m_hat / (v_hat.sqrt() + self.eps)
def zero_grad(self):
for param in self.params:
if param.grad is not None:
param.grad.zero_()
5.3 PyTorch内置Adam使用示例
import torch
import torch.nn as nn
import torch.optim as optim
# 定义模型
model = nn.Sequential(
nn.Linear(784, 256),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(256, 128),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(128, 10)
)
# 创建Adam优化器
optimizer = optim.Adam(
model.parameters(),
lr=1e-3, # 学习率,默认1e-3
betas=(0.9, 0.999), # 一阶、二阶矩估计衰减系数
eps=1e-8, # 数值稳定常数
weight_decay=0, # 权重衰减(L2正则化)
amsgrad=False # 是否使用AMSGrad变体
)
# 训练循环示例
for epoch in range(10):
for batch_data, batch_labels in train_loader:
optimizer.zero_grad() # 清零梯度
outputs = model(batch_data) # 前向传播
loss = criterion(outputs, batch_labels)
loss.backward() # 反向传播
optimizer.step() # 更新参数
6. AdamW优化器
AdamW(Adam with Weight Decay)是Adam的改进版本,解决了Adam中L2正则化与权重衰减不等价的问题。
6.1 Adam与L2正则化的问题
在标准Adam中,L2正则化通过在梯度中添加 \\lambda \\cdot \\theta 来实现:
\\theta_{t+1} = \\theta_t - \\eta \\cdot \\left( \\frac{m_t}{1-\\beta_1\^t} / \\sqrt{\\frac{v_t}{1-\\beta_2\^t} + \\epsilon} + \\lambda \\cdot \\theta_t \\right)
问题在于:Adam的自适应学习率会抵消L2正则化的效果,使得正则化强度不可预测。
6.2 AdamW的正确权重衰减
AdamW将权重衰减与梯度解耦,在参数更新时直接应用衰减:
class AdamW:
"""
AdamW = Adam + 正确的权重衰减
权重衰减不再通过梯度实现,而是直接作用于参数
"""
def __init__(self, params, lr=1e-3, beta1=0.9, beta2=0.999, eps=1e-8, weight_decay=0.01):
self.params = list(params)
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.eps = eps
self.weight_decay = weight_decay
self.t = 0
self.state = {}
def step(self):
self.t += 1
for param in self.params:
if param.grad is None:
continue
grad = param.grad.data
# 初始化状态
if param not in self.state:
self.state[param] = {
'm': torch.zeros_like(param.data),
'v': torch.zeros_like(param.data)
}
state = self.state[param]
# 更新一阶、二阶矩估计
state['m'] = self.beta1 * state['m'] + (1 - self.beta1) * grad
state['v'] = self.beta2 * state['v'] + (1 - self.beta2) * grad.pow(2)
# 偏置修正
m_hat = state['m'] / (1 - self.beta1 ** self.t)
v_hat = state['v'] / (1 - self.beta2 ** self.t)
# 关键区别:权重衰减直接作用于参数,而非梯度
# 先对参数进行衰减
param.data = param.data * (1 - self.lr * self.weight_decay)
# 再应用Adam更新
param.data -= self.lr * m_hat / (v_hat.sqrt() + self.eps)
def zero_grad(self):
for param in self.params:
if param.grad is not None:
param.grad.zero_()
使用建议: 在Transformer架构(BERT、GPT等)的预训练中,AdamW是标准配置,通常配合 weight_decay=0.01 使用。
7. L-BFGS优化器
L-BFGS(Limited-memory Broyden-Fletcher-Goldfarb-Shanno)是一种二阶优化算法,通过存储有限的历史信息来近似计算 Hessian 矩阵的逆。
特点:
-
收敛速度通常比一阶方法快
-
内存占用 O(m \\cdot n),其中 m 是历史步数,n 是参数维度
-
适合小规模数据集和参数较少的场景
-
不适合大规模深度学习,但可用于逻辑回归等传统ML任务
# PyTorch中使用L-BFGS
optimizer = optim.LBFGS(
model.parameters(),
lr=0.1,
max_iter=20, # 最大迭代次数
max_eval=None, # 最大函数评估次数
tolerance_grad=1e-7, # 梯度容忍度
tolerance_change=1e-9, # 参数变化容忍度
history_size=100 # 历史步数(内存占用)
)
def closure():
optimizer.zero_grad()
output = model(input)
loss = criterion(output, target)
loss.backward()
return loss
optimizer.step(closure)
8. 学习率调度策略
学习率是训练过程中最关键的超参数。学习率调度(Learning Rate Scheduling)允许学习率在训练过程中动态调整,通常能够显著提升训练效果。
8.1 Step LR(阶梯学习率衰减)
每经过固定 epoch 数,将学习率按固定比例衰减。
import torch.optim as optim
# 每30个epoch将学习率降低为原来的10%
scheduler = optim.lr_scheduler.StepLR(
optimizer,
step_size=30, # 衰减周期
gamma=0.1 # 衰减系数
)
for epoch in range(100):
train(...)
validate(...)
scheduler.step() # 更新学习率
8.2 Cosine Annealing(余弦退火)
使用余弦函数周期性地降低学习率,从最大值缓慢降到最小值。
# Cosine Annealing学习率调度
scheduler = optim.lr_scheduler.CosineAnnealingLR(
optimizer,
T_max=100, # 一个周期的最大迭代数
eta_min=1e-6 # 最小学习率
)
# Cosine Annealing with Warm Restarts
scheduler = optim.lr_scheduler.CosineAnnealingWarmRestarts(
optimizer,
T_0=20, # 第一个周期的长度
T_mult=2, # 周期倍增因子
eta_min=1e-6
)
8.3 ReduceLROnPlateau(早停式学习率衰减)
当监控指标(如验证集损失)不再改善时,自动降低学习率。
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
optimizer,
mode='min', # 'min'表示监控指标降低时触发
factor=0.1, # 学习率衰减比例
patience=10, # 容忍epoch数
threshold=0.0001, # 改善阈值
min_lr=1e-7, # 最低学习率
verbose=True # 打印学习率变化信息
)
for epoch in range(100):
train_loss = train(...)
val_loss = validate(...)
scheduler.step(val_loss) # 传入监控指标
8.4 Warmup(学习率预热)
训练初期从一个很小的学习率逐渐增加到目标学习率,有助于稳定训练初期。
class WarmupScheduler:
"""
学习率预热调度器
先线性warmup,再接其他调度器
"""
def __init__(self, optimizer, warmup_epochs, base_scheduler=None):
self.optimizer = optimizer
self.warmup_epochs = warmup_epochs
self.base_scheduler = base_scheduler
self.current_epoch = 0
self.base_lrs = [group['lr'] for group in optimizer.param_groups]
def step(self):
if self.current_epoch < self.warmup_epochs:
# Warmup阶段:线性增加学习率
factor = (self.current_epoch + 1) / self.warmup_epochs
for param_group, base_lr in zip(self.optimizer.param_groups, self.base_lrs):
param_group['lr'] = base_lr * factor
elif self.base_scheduler:
# Warmup完成后使用基础调度器
self.base_scheduler.step()
self.current_epoch += 1
# 使用示例
base_scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=90)
scheduler = WarmupScheduler(optimizer, warmup_epochs=5, base_scheduler=base_scheduler)
9. 使用场景选择指南
根据任务特点选择合适的优化器是工程实践中的重要能力。以下是各优化器的适用场景总结:
| 优化器 | 适用场景 | 不适用场景 | 备注 |
|---|---|---|---|
| SGD + Momentum | CV任务(ResNet等)、需要最佳泛化性能 | 超参数调优困难 | 收敛慢但泛化好 |
| Adam | 默认首选、NLP任务、快速原型开发 | 需要最佳泛化性能 | 收敛快但泛化略差 |
| AdamW | Transformer系列模型、预训练 | 小数据集 | 标准配置weight_decay=0.01 |
| RMSProp | RNNs、在线学习、稀疏数据 | - | 非凸问题表现好 |
| AdaGrad | 极度稀疏数据、文本处理 | 深度学习 | 学习率单调下降 |
| AdaDelta | 不想手动设置学习率 | - | 自适应能力强 |
| **L-BFGS | 小规模数据、逻辑回归、传统ML | 大规模深度学习 | 内存占用高 |
选择建议:
-
快速原型与探索阶段:优先使用 Adam,学习率默认 1e-3
-
追求最佳性能(CV):尝试 SGD + Momentum (lr=0.01, momentum=0.9) + Cosine Annealing
-
Transformer模型:AdamW (lr=1e-4, weight_decay=0.01)
-
稀疏特征明显:RMSProp 或 AdaGrad
-
不稳定训练:加入 Warmup 或使用 ReduceLROnPlateau
10. PyTorch实战:各优化器对比实验
以下代码在MNIST数据集上对比各优化器的训练效果:
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import matplotlib.pyplot as plt
import numpy as np
# 超参数设置
BATCH_SIZE = 256
EPOCHS = 20
LEARNING_RATE = 1e-3
# 检查设备
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"使用设备: {device}")
# ==================== 1. 准备数据 ====================
# 使用PyTorch内置MNIST数据集
from torchvision import datasets, transforms
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
train_dataset = datasets.MNIST(
root='./data', train=True, download=True, transform=transform
)
test_dataset = datasets.MNIST(
root='./data', train=False, download=True, transform=transform
)
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE)
# ==================== 2. 定义模型 ====================
class SimpleNet(nn.Module):
"""简单的多层感知机"""
def __init__(self):
super(SimpleNet, self).__init__()
self.features = nn.Sequential(
nn.Flatten(),
nn.Linear(784, 256),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(256, 128),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(128, 10)
)
def forward(self, x):
return self.features(x)
# ==================== 3. 训练函数 ====================
def train_model(model, optimizer, scheduler=None, epochs=EPOCHS):
"""训练模型并返回训练历史"""
criterion = nn.CrossEntropyLoss()
history = {
'train_loss': [],
'test_loss': [],
'train_acc': [],
'test_acc': []
}
for epoch in range(epochs):
# 训练阶段
model.train()
train_loss = 0.0
train_correct = 0
train_total = 0
for batch_x, batch_y in train_loader:
batch_x, batch_y = batch_x.to(device), batch_y.to(device)
optimizer.zero_grad()
outputs = model(batch_x)
loss = criterion(outputs, batch_y)
loss.backward()
optimizer.step()
train_loss += loss.item()
_, predicted = outputs.max(1)
train_total += batch_y.size(0)
train_correct += predicted.eq(batch_y).sum().item()
# 更新学习率
if scheduler is not None:
scheduler.step()
# 记录训练指标
history['train_loss'].append(train_loss / len(train_loader))
history['train_acc'].append(100. * train_correct / train_total)
# 测试阶段
model.eval()
test_loss = 0.0
test_correct = 0
test_total = 0
with torch.no_grad():
for batch_x, batch_y in test_loader:
batch_x, batch_y = batch_x.to(device), batch_y.to(device)
outputs = model(batch_x)
loss = criterion(outputs, batch_y)
test_loss += loss.item()
_, predicted = outputs.max(1)
test_total += batch_y.size(0)
test_correct += predicted.eq(batch_y).sum().item()
history['test_loss'].append(test_loss / len(test_loader))
history['test_acc'].append(100. * test_correct / test_total)
print(f"Epoch {epoch+1:2d}/{EPOCHS} | "
f"Train Loss: {history['train_loss'][-1]:.4f} | "
f"Train Acc: {history['train_acc'][-1]:.2f}% | "
f"Test Loss: {history['test_loss'][-1]:.4f} | "
f"Test Acc: {history['test_acc'][-1]:.2f}%")
return history
# ==================== 4. 定义要对比的优化器 ====================
optimizers_config = {
'SGD (lr=0.01, momentum=0.9)': {
'optimizer': lambda params: optim.SGD(params, lr=0.01, momentum=0.9),
'scheduler': lambda opt: optim.lr_scheduler.CosineAnnealingLR(opt, T_max=EPOCHS)
},
'SGD (lr=0.1, momentum=0.9)': {
'optimizer': lambda params: optim.SGD(params, lr=0.1, momentum=0.9),
'scheduler': lambda opt: optim.lr_scheduler.CosineAnnealingLR(opt, T_max=EPOCHS)
},
'Adam (lr=1e-3)': {
'optimizer': lambda params: optim.Adam(params, lr=1e-3),
'scheduler': None
},
'Adam (lr=1e-4)': {
'optimizer': lambda params: optim.Adam(params, lr=1e-4),
'scheduler': None
},
'AdamW (lr=1e-4, wd=0.01)': {
'optimizer': lambda params: optim.AdamW(params, lr=1e-4, weight_decay=0.01),
'scheduler': None
},
'RMSProp (lr=1e-3)': {
'optimizer': lambda params: optim.RMSprop(params, lr=1e-3, alpha=0.99),
'scheduler': None
},
'AdaGrad (lr=0.01)': {
'optimizer': lambda params: optim.Adagrad(params, lr=0.01),
'scheduler': None
}
}
# ==================== 5. 运行对比实验 ====================
results = {}
for name, config in optimizers_config.items():
print(f"\n{'='*60}")
print(f"训练优化器: {name}")
print('='*60)
# 创建新模型
model = SimpleNet().to(device)
# 创建优化器
optimizer = config['optimizer'](model.parameters())
# 创建学习率调度器
scheduler = config['scheduler'](optimizer) if config['scheduler'] else None
# 训练
history = train_model(model, optimizer, scheduler)
results[name] = history
# ==================== 6. 可视化对比结果 ====================
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 绘制损失曲线
ax1 = axes[0]
for name, history in results.items():
ax1.plot(history['train_loss'], label=name, linewidth=2)
ax1.set_xlabel('Epoch', fontsize=12)
ax1.set_ylabel('Training Loss', fontsize=12)
ax1.set_title('训练损失对比', fontsize=14)
ax1.legend(loc='upper right', fontsize=9)
ax1.grid(True, alpha=0.3)
# 绘制准确率曲线
ax2 = axes[1]
for name, history in results.items():
ax2.plot(history['test_acc'], label=name, linewidth=2)
ax2.set_xlabel('Epoch', fontsize=12)
ax2.set_ylabel('Test Accuracy (%)', fontsize=12)
ax2.set_title('测试准确率对比', fontsize=14)
ax2.legend(loc='lower right', fontsize=9)
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('optimizer_comparison.png', dpi=150, bbox_inches='tight')
plt.show()
# ==================== 7. 打印最终结果汇总 ====================
print("\n" + "="*80)
print("最终结果汇总")
print("="*80)
print(f"{'优化器':<35} {'最终训练损失':<15} {'最终测试准确率':<15}")
print("-"*80)
for name, history in results.items():
final_train_loss = history['train_loss'][-1]
final_test_acc = history['test_acc'][-1]
print(f"{name:<35} {final_train_loss:<15.4f} {final_test_acc:<15.2f}%")
# 找出最佳优化器
best_optimizer = max(results.items(), key=lambda x: x[1]['test_acc'][-1])
print(f"\n最佳优化器: {best_optimizer[0]}")
print(f"最佳测试准确率: {best_optimizer[1]['test_acc'][-1]:.2f}%")
实验结果解读:
运行上述代码后,你将看到各优化器在MNIST上的表现差异。一般规律:
-
Adam系列:收敛最快,但最终准确率可能略低于SGD
-
SGD + Momentum:收敛较慢,但往往能达到更高的准确率
-
AdaGrad:学习率持续下降,后期可能出现"僵硬"现象
-
RMSProp:收敛速度和准确性之间的良好平衡
11. 总结
本文系统梳理了深度学习中主流优化器的发展脉络与核心原理:
-
SGD系列:基础但强大,通过动量改进可达到优异性能
-
自适应学习率系列:AdaGrad、RMSProp、AdaDelta解决了学习率手动调节的难题
-
Adam:集大成者,是目前工业界的默认首选
-
AdamW:解决了Adam中权重衰减的问题,是Transformer时代的标准配置
-
学习率调度:配合优化器使用能够显著提升训练效果
实践建议:
-
快速实验用Adam,上线生产用SGD
-
Transformer模型用AdamW
-
配合学习率调度(Cosine Annealing + Warmup)效果更佳
-
没有银弹,根据任务特点选择合适的优化器