优化算法是训练深度学习模型的核心引擎。本章将系统讲解从经典SGD到最新自适应优化器的完整知识体系。
环境声明
- Python版本:Python 3.12+
- 核心依赖:NumPy 1.24+、Matplotlib 3.7+、PyTorch 2.0+ / TensorFlow 2.13+
- 开发工具:PyCharm / VS Code / Jupyter Notebook
- 操作系统:Windows / macOS / Linux(通用)
bash
# 安装依赖
pip install numpy matplotlib torch torchvision
学习目标
完成本章学习后,你将能够:
- 理解优化问题的数学本质,区分凸优化与非凸优化
- 掌握梯度下降家族的三种变体及其适用场景
- 深入理解动量方法的物理直觉与数学原理
- 推导并应用自适应学习率优化器(AdaGrad、RMSprop、Adam)
- 了解2024-2026年最新优化器进展(AdamW改进、Lion、Sophia等)
- 通过可视化对比不同优化器的收敛行为
- 在MNIST数据集上实战对比各类优化器的性能
1. 优化问题概述
1.1 什么是优化问题
训练机器学习模型的本质是一个优化问题:寻找一组模型参数,使得损失函数达到最小值。
数学定义 :给定损失函数 L(θ)L(\theta)L(θ),其中 θ\thetaθ 表示模型参数,优化目标是:
θ∗=argminθL(θ) \theta^* = \arg\min_{\theta} L(\theta) θ∗=argθminL(θ)
1.2 损失函数的几何意义
损失函数描述了模型预测值与真实值之间的差异。从几何角度看:
| 维度 | 损失函数形态 | 可视化特征 |
|---|---|---|
| 1D | 曲线 | 波峰与波谷 |
| 2D | 曲面 | 山谷与盆地 |
| 高维 | 超曲面 | 复杂的"地形" |
1.3 凸优化 vs 非凸优化
凸优化(Convex Optimization)
当损失函数满足凸函数性质时,我们称其为凸优化问题:
f(λx+(1−λ)y)≤λf(x)+(1−λ)f(y),∀λ∈[0,1] f(\lambda x + (1-\lambda)y) \leq \lambda f(x) + (1-\lambda)f(y), \quad \forall \lambda \in [0,1] f(λx+(1−λ)y)≤λf(x)+(1−λ)f(y),∀λ∈[0,1]
凸优化的优势:
- 局部最优即全局最优
- 梯度下降保证收敛到全局最优解
- 理论分析相对简单
典型例子:线性回归的均方误差损失
L(θ)=1n∑i=1n(yi−θTxi)2 L(\theta) = \frac{1}{n}\sum_{i=1}^{n}(y_i - \theta^T x_i)^2 L(θ)=n1i=1∑n(yi−θTxi)2
非凸优化(Non-convex Optimization)
深度学习中的优化问题几乎都是非凸的:
- 存在多个局部最优解
- 鞍点问题严重
- 收敛性难以保证
补充:神经网络损失函数的非凸性来源于激活函数的非线性组合以及参数空间的复杂结构。
1.4 优化算法的分类
根据利用信息的不同,优化算法可分为:
| 类别 | 利用信息 | 代表算法 | 计算成本 |
|---|---|---|---|
| 零阶优化 | 仅函数值 | 遗传算法、模拟退火 | 高 |
| 一阶优化 | 梯度信息 | SGD、Adam、RMSprop | 低 |
| 二阶优化 | 梯度+海森矩阵 | 牛顿法、L-BFGS | 高 |
2. 梯度下降家族
2.1 梯度下降的核心思想
梯度下降基于一个核心观察:函数在某点的梯度指向函数增长最快的方向。因此,沿梯度的反方向移动可以减小函数值。
参数更新公式:
θt+1=θt−η∇L(θt) \theta_{t+1} = \theta_t - \eta \nabla L(\theta_t) θt+1=θt−η∇L(θt)
其中 η\etaη 为学习率(learning rate),控制每次更新的步长。
2.2 批量梯度下降(BGD)
算法原理
批量梯度下降使用整个训练集计算梯度:
∇L(θ)=1n∑i=1n∇Li(θ) \nabla L(\theta) = \frac{1}{n}\sum_{i=1}^{n}\nabla L_i(\theta) ∇L(θ)=n1i=1∑n∇Li(θ)
Python实现
python
import numpy as np
def batch_gradient_descent(X, y, lr=0.01, epochs=1000):
"""
批量梯度下降实现
参数:
X: 特征矩阵 (n_samples, n_features)
y: 目标向量 (n_samples,)
lr: 学习率
epochs: 迭代轮数
"""
n_samples, n_features = X.shape
theta = np.zeros(n_features) # 初始化参数
losses = []
for epoch in range(epochs):
# 计算预测值
predictions = X @ theta
# 计算梯度 (使用整个数据集)
gradient = (2 / n_samples) * X.T @ (predictions - y)
# 参数更新
theta = theta - lr * gradient
# 记录损失
loss = np.mean((predictions - y) ** 2)
losses.append(loss)
return theta, losses
优缺点分析
| 优点 | 缺点 |
|---|---|
| 梯度估计准确,收敛稳定 | 大数据集计算成本极高 |
| 适合凸优化问题 | 内存消耗大 |
| 易于并行化 | 无法在线学习 |
2.3 随机梯度下降(SGD)
算法原理
SGD每次只使用一个样本计算梯度:
θt+1=θt−η∇Li(θt) \theta_{t+1} = \theta_t - \eta \nabla L_i(\theta_t) θt+1=θt−η∇Li(θt)
Python实现
python
def stochastic_gradient_descent(X, y, lr=0.01, epochs=1000):
"""
随机梯度下降实现
参数:
X: 特征矩阵 (n_samples, n_features)
y: 目标向量 (n_samples,)
lr: 学习率
epochs: 迭代轮数
"""
n_samples, n_features = X.shape
theta = np.zeros(n_features)
losses = []
for epoch in range(epochs):
# 随机打乱样本顺序
indices = np.random.permutation(n_samples)
for i in indices:
# 取单个样本
xi = X[i:i+1]
yi = y[i:i+1]
# 计算单样本梯度
prediction = xi @ theta
gradient = 2 * xi.T @ (prediction - yi)
# 参数更新
theta = theta - lr * gradient.flatten()
# 记录本轮损失
loss = np.mean((X @ theta - y) ** 2)
losses.append(loss)
return theta, losses
优缺点分析
| 优点 | 缺点 |
|---|---|
| 计算速度快,适合大数据集 | 梯度估计噪声大 |
| 内存占用小 | 收敛过程震荡剧烈 |
| 可能跳出局部最优 | 需要仔细调参 |
2.4 小批量梯度下降(Mini-batch GD)
算法原理
Mini-batch GD是BGD和SGD的折中,每次使用一小批样本(通常32-512个):
θt+1=θt−η⋅1m∑i=1m∇Li(θt) \theta_{t+1} = \theta_t - \eta \cdot \frac{1}{m}\sum_{i=1}^{m}\nabla L_i(\theta_t) θt+1=θt−η⋅m1i=1∑m∇Li(θt)
其中 mmm 为batch size。
Python实现
python
def minibatch_gradient_descent(X, y, lr=0.01, epochs=1000, batch_size=32):
"""
小批量梯度下降实现
参数:
X: 特征矩阵 (n_samples, n_features)
y: 目标向量 (n_samples,)
lr: 学习率
epochs: 迭代轮数
batch_size: 批量大小
"""
n_samples, n_features = X.shape
theta = np.zeros(n_features)
losses = []
for epoch in range(epochs):
# 随机打乱
indices = np.random.permutation(n_samples)
for start in range(0, n_samples, batch_size):
end = min(start + batch_size, n_samples)
batch_idx = indices[start:end]
# 取小批量样本
X_batch = X[batch_idx]
y_batch = y[batch_idx]
# 计算批量梯度
predictions = X_batch @ theta
gradient = (2 / len(batch_idx)) * X_batch.T @ (predictions - y_batch)
# 参数更新
theta = theta - lr * gradient
# 记录损失
loss = np.mean((X @ theta - y) ** 2)
losses.append(loss)
return theta, losses
2.5 三种方法的对比
| 特性 | BGD | SGD | Mini-batch GD |
|---|---|---|---|
| 每次迭代样本数 | n | 1 | m (32-512) |
| 梯度估计方差 | 低 | 高 | 中 |
| 收敛速度 | 慢 | 快但震荡 | 快且稳定 |
| 内存需求 | 高 | 低 | 中 |
| 实际应用 | 很少 | 特定场景 | 主流选择 |
3. 动量方法
3.1 物理直觉:小球滚下山坡
想象一个小球从山坡滚下:
- 初始阶段:坡度陡峭,小球加速
- 平缓区域:小球依靠惯性继续前进
- 局部最优:动量帮助小球越过"小坑"
这就是动量方法的核心思想:积累历史梯度信息,加速收敛并减少震荡。
3.2 经典动量(Momentum)
数学推导
动量方法引入速度变量 vvv,模拟物理中的动量:
vt=γvt−1+η∇L(θt) v_t = \gamma v_{t-1} + \eta \nabla L(\theta_t) vt=γvt−1+η∇L(θt)
θt+1=θt−vt \theta_{t+1} = \theta_t - v_t θt+1=θt−vt
其中 γ\gammaγ 为动量系数(通常取0.9),控制历史梯度的衰减速度。
Python实现
python
def momentum_sgd(X, y, lr=0.01, gamma=0.9, epochs=1000, batch_size=32):
"""
带动量的SGD实现
参数:
gamma: 动量系数 (通常0.9)
"""
n_samples, n_features = X.shape
theta = np.zeros(n_features)
v = np.zeros(n_features) # 初始化速度
losses = []
for epoch in range(epochs):
indices = np.random.permutation(n_samples)
for start in range(0, n_samples, batch_size):
end = min(start + batch_size, n_samples)
batch_idx = indices[start:end]
X_batch = X[batch_idx]
y_batch = y[batch_idx]
# 计算当前梯度
predictions = X_batch @ theta
gradient = (2 / len(batch_idx)) * X_batch.T @ (predictions - y_batch)
# 更新速度: v = gamma * v + lr * gradient
v = gamma * v + lr * gradient
# 更新参数
theta = theta - v
loss = np.mean((X @ theta - y) ** 2)
losses.append(loss)
return theta, losses
3.3 Nesterov加速梯度(NAG)
核心思想
Nesterov动量是一种"前瞻性"的动量方法。它不是在当前位置计算梯度,而是在动量预测的下一个位置计算梯度:
vt=γvt−1+η∇L(θt−γvt−1) v_t = \gamma v_{t-1} + \eta \nabla L(\theta_t - \gamma v_{t-1}) vt=γvt−1+η∇L(θt−γvt−1)
θt+1=θt−vt \theta_{t+1} = \theta_t - v_t θt+1=θt−vt
直观理解
- 经典动量:先计算梯度,再沿更新方向大步前进
- Nesterov动量:先沿动量方向"试探"一步,再在该位置计算梯度进行修正
Python实现
python
def nesterov_sgd(X, y, lr=0.01, gamma=0.9, epochs=1000, batch_size=32):
"""
Nesterov加速梯度实现
"""
n_samples, n_features = X.shape
theta = np.zeros(n_features)
v = np.zeros(n_features)
losses = []
for epoch in range(epochs):
indices = np.random.permutation(n_samples)
for start in range(0, n_samples, batch_size):
end = min(start + batch_size, n_samples)
batch_idx = indices[start:end]
X_batch = X[batch_idx]
y_batch = y[batch_idx]
# 前瞻性位置: theta_lookahead = theta - gamma * v
theta_lookahead = theta - gamma * v
# 在lookahead位置计算梯度
predictions = X_batch @ theta_lookahead
gradient = (2 / len(batch_idx)) * X_batch.T @ (predictions - y_batch)
# 更新速度
v = gamma * v + lr * gradient
# 更新参数
theta = theta - v
loss = np.mean((X @ theta - y) ** 2)
losses.append(loss)
return theta, losses
3.4 动量方法的对比
| 特性 | 经典Momentum | Nesterov Momentum |
|---|---|---|
| 梯度计算位置 | 当前参数 | 前瞻性参数 |
| 收敛速度 | 快 | 更快 |
| 震荡抑制 | 好 | 更好 |
| 实际应用 | 广泛 | 更推荐 |
4. 自适应学习率方法
4.1 为什么需要自适应学习率
不同参数的重要性不同,应该使用不同的学习率:
- 稀疏特征:需要较大的学习率
- 频繁特征:需要较小的学习率
4.2 AdaGrad
核心思想
AdaGrad为每个参数维护一个累积的梯度平方和,用其来调整学习率:
gt=∇L(θt) g_t = \nabla L(\theta_t) gt=∇L(θt)
rt=rt−1+gt⊙gt r_t = r_{t-1} + g_t \odot g_t rt=rt−1+gt⊙gt
θt+1=θt−ηrt+ϵ⊙gt \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{r_t + \epsilon}} \odot g_t θt+1=θt−rt+ϵ η⊙gt
其中 ⊙\odot⊙ 表示逐元素乘法,ϵ\epsilonϵ 是防止除零的小常数(通常 10−810^{-8}10−8)。
Python实现
python
def adagrad(X, y, lr=0.01, epsilon=1e-8, epochs=1000, batch_size=32):
"""
AdaGrad优化器实现
"""
n_samples, n_features = X.shape
theta = np.zeros(n_features)
r = np.zeros(n_features) # 累积梯度平方
losses = []
for epoch in range(epochs):
indices = np.random.permutation(n_samples)
for start in range(0, n_samples, batch_size):
end = min(start + batch_size, n_samples)
batch_idx = indices[start:end]
X_batch = X[batch_idx]
y_batch = y[batch_idx]
# 计算梯度
predictions = X_batch @ theta
g = (2 / len(batch_idx)) * X_batch.T @ (predictions - y_batch)
# 累积梯度平方
r = r + g ** 2
# 自适应更新
theta = theta - (lr / (np.sqrt(r) + epsilon)) * g
loss = np.mean((X @ theta - y) ** 2)
losses.append(loss)
return theta, losses
优缺点
| 优点 | 缺点 |
|---|---|
| 自动调整学习率 | 累积平方导致学习率过早衰减 |
| 适合稀疏数据 | 后期学习率接近零,训练停滞 |
4.3 RMSprop
核心思想
RMSprop解决了AdaGrad学习率单调递减的问题,使用指数移动平均代替累积和:
rt=ρrt−1+(1−ρ)gt⊙gt r_t = \rho r_{t-1} + (1-\rho)g_t \odot g_t rt=ρrt−1+(1−ρ)gt⊙gt
θt+1=θt−ηrt+ϵ⊙gt \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{r_t + \epsilon}} \odot g_t θt+1=θt−rt+ϵ η⊙gt
其中 ρ\rhoρ 为衰减系数(通常0.9)。
Python实现
python
def rmsprop(X, y, lr=0.001, rho=0.9, epsilon=1e-8, epochs=1000, batch_size=32):
"""
RMSprop优化器实现
"""
n_samples, n_features = X.shape
theta = np.zeros(n_features)
r = np.zeros(n_features)
losses = []
for epoch in range(epochs):
indices = np.random.permutation(n_samples)
for start in range(0, n_samples, batch_size):
end = min(start + batch_size, n_samples)
batch_idx = indices[start:end]
X_batch = X[batch_idx]
y_batch = y[batch_idx]
# 计算梯度
predictions = X_batch @ theta
g = (2 / len(batch_idx)) * X_batch.T @ (predictions - y_batch)
# 指数移动平均
r = rho * r + (1 - rho) * (g ** 2)
# 自适应更新
theta = theta - (lr / (np.sqrt(r) + epsilon)) * g
loss = np.mean((X @ theta - y) ** 2)
losses.append(loss)
return theta, losses
4.4 Adam(Adaptive Moment Estimation)
核心思想
Adam结合了动量方法和自适应学习率的优点,维护两个移动平均:
- 一阶矩估计(动量):mtm_tmt - 梯度的均值
- 二阶矩估计(自适应):vtv_tvt - 梯度平方的均值
完整推导
步骤1:计算梯度
gt=∇L(θt) g_t = \nabla L(\theta_t) gt=∇L(θt)
步骤2:更新一阶矩估计(动量)
mt=β1mt−1+(1−β1)gt m_t = \beta_1 m_{t-1} + (1-\beta_1)g_t mt=β1mt−1+(1−β1)gt
步骤3:更新二阶矩估计
vt=β2vt−1+(1−β2)gt⊙gt v_t = \beta_2 v_{t-1} + (1-\beta_2)g_t \odot g_t vt=β2vt−1+(1−β2)gt⊙gt
步骤4:偏差修正
由于初始值为0,估计值在早期迭代中有偏差,需要进行修正:
m^t=mt1−β1t \hat{m}_t = \frac{m_t}{1-\beta_1^t} m^t=1−β1tmt
v^t=vt1−β2t \hat{v}_t = \frac{v_t}{1-\beta_2^t} v^t=1−β2tvt
步骤5:参数更新
θt+1=θt−ηv^t+ϵm^t \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t θt+1=θt−v^t +ϵηm^t
Python实现
python
def adam(X, y, lr=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8,
epochs=1000, batch_size=32):
"""
Adam优化器实现
参数:
lr: 学习率 (默认0.001)
beta1: 一阶矩衰减系数 (默认0.9)
beta2: 二阶矩衰减系数 (默认0.999)
epsilon: 数值稳定性常数
"""
n_samples, n_features = X.shape
theta = np.zeros(n_features)
m = np.zeros(n_features) # 一阶矩
v = np.zeros(n_features) # 二阶矩
losses = []
t = 0 # 时间步
for epoch in range(epochs):
indices = np.random.permutation(n_samples)
for start in range(0, n_samples, batch_size):
t += 1
end = min(start + batch_size, n_samples)
batch_idx = indices[start:end]
X_batch = X[batch_idx]
y_batch = y[batch_idx]
# 计算梯度
predictions = X_batch @ theta
g = (2 / len(batch_idx)) * X_batch.T @ (predictions - y_batch)
# 更新一阶矩估计
m = beta1 * m + (1 - beta1) * g
# 更新二阶矩估计
v = beta2 * v + (1 - beta2) * (g ** 2)
# 偏差修正
m_hat = m / (1 - beta1 ** t)
v_hat = v / (1 - beta2 ** t)
# 参数更新
theta = theta - (lr / (np.sqrt(v_hat) + epsilon)) * m_hat
loss = np.mean((X @ theta - y) ** 2)
losses.append(loss)
return theta, losses
4.5 AdamW
核心改进
AdamW将权重衰减(L2正则化)与梯度更新解耦:
传统Adam+L2:
gt=∇L(θt)+λθt g_t = \nabla L(\theta_t) + \lambda \theta_t gt=∇L(θt)+λθt
AdamW:
θt+1=θt−η(m^tv^t+ϵ+λθt) \theta_{t+1} = \theta_t - \eta \left(\frac{\hat{m}_t}{\sqrt{\hat{v}_t}+\epsilon} + \lambda \theta_t\right) θt+1=θt−η(v^t +ϵm^t+λθt)
这种解耦使得权重衰减的效果与学习率解耦,实验表明AdamW在很多任务上优于Adam。
4.6 自适应优化器对比
| 优化器 | 动量 | 自适应学习率 | 偏差修正 | 推荐场景 |
|---|---|---|---|---|
| AdaGrad | 无 | 有 | 无 | 稀疏特征 |
| RMSprop | 无 | 有 | 无 | RNN训练 |
| Adam | 有 | 有 | 有 | 通用首选 |
| AdamW | 有 | 有 | 有 | 带正则化的训练 |
5. 2024-2026年最新优化器进展
5.1 Lion优化器(2023-2024)
核心思想
Lion(EvoLved Sign Momentum)是Google在2023年提出的优化器,通过符号操作和二阶动量来减少内存占用。
更新规则:
ct=β1mt−1+(1−β1)gt c_t = \beta_1 m_{t-1} + (1-\beta_1) g_t ct=β1mt−1+(1−β1)gt
mt=β2mt−1+(1−β2)gt m_t = \beta_2 m_{t-1} + (1-\beta_2) g_t mt=β2mt−1+(1−β2)gt
θt+1=θt−η⋅sign(ct) \theta_{t+1} = \theta_t - \eta \cdot \text{sign}(c_t) θt+1=θt−η⋅sign(ct)
特点:
- 只使用动量,不使用自适应学习率
- 通过符号操作减少内存占用
- 在大规模语言模型训练上表现出色
python
def lion(X, y, lr=0.001, beta1=0.9, beta2=0.99, epochs=1000, batch_size=32):
"""
Lion优化器实现
"""
n_samples, n_features = X.shape
theta = np.zeros(n_features)
m = np.zeros(n_features) # 一阶矩
losses = []
for epoch in range(epochs):
indices = np.random.permutation(n_samples)
for start in range(0, n_samples, batch_size):
end = min(start + batch_size, n_samples)
batch_idx = indices[start:end]
X_batch = X[batch_idx]
y_batch = y[batch_idx]
# 计算梯度
predictions = X_batch @ theta
g = (2 / len(batch_idx)) * X_batch.T @ (predictions - y_batch)
# 计算更新方向
c = beta1 * m + (1 - beta1) * g
# 符号更新
theta = theta - lr * np.sign(c)
# 更新动量
m = beta2 * m + (1 - beta2) * g
loss = np.mean((X @ theta - y) ** 2)
losses.append(loss)
return theta, losses
5.2 Sophia优化器(2024)
核心思想
Sophia(Second-order Clipped Optimizer)是2024年提出的二阶优化器,通过海森矩阵对角线的近似来加速收敛。
更新规则:
θt+1=θt−η⋅clip(mtmax(ht,ϵ),ρ) \theta_{t+1} = \theta_t - \eta \cdot \text{clip}\left(\frac{m_t}{\max(h_t, \epsilon)}, \rho\right) θt+1=θt−η⋅clip(max(ht,ϵ)mt,ρ)
其中:
- mtm_tmt 是梯度的一阶矩估计
- hth_tht 是海森矩阵对角线的估计
- ρ\rhoρ 是裁剪阈值
特点:
- 结合一阶和二阶信息
- 通过裁剪防止更新过大
- 在大模型训练上收敛速度显著快于Adam
5.3 Muon优化器(2024-2025)
核心思想
Muon(Momen tum Orthogonalization)是2024年底提出的优化器,通过对梯度进行正交化处理来提高收敛稳定性。
核心步骤:
- 计算梯度 gtg_tgt
- 对梯度进行正交化:g~t=Orthogonalize(gt)\tilde{g}_t = \text{Orthogonalize}(g_t)g~t=Orthogonalize(gt)
- 更新参数:θt+1=θt−η⋅g~t\theta_{t+1} = \theta_t - \eta \cdot \tilde{g}_tθt+1=θt−η⋅g~t
特点:
- 正交化操作提高了参数更新的多样性
- 在Transformer训练上表现出色
- 与AdamW结合使用效果最佳
5.4 2024-2026年优化器发展趋势
| 趋势 | 描述 | 代表工作 |
|---|---|---|
| 内存效率 | 减少优化器状态内存占用 | Lion、Adafactor |
| 二阶近似 | 高效利用二阶信息 | Sophia、Shampoo |
| 自适应裁剪 | 动态调整更新幅度 | Sophia、AdamClip |
| 分布式优化 | 支持大规模并行训练 | Zero-1/2/3、FSDP |
| 学习率调度 | 更智能的学习率调整 | Warmup-Stable-Decay、Cosine |
6. 二阶优化方法
6.1 牛顿法
核心思想
牛顿法利用损失函数的二阶信息(海森矩阵)来指导优化方向。
泰勒展开
在 θt\theta_tθt 处对损失函数进行二阶泰勒展开:
L(θ)≈L(θt)+∇L(θt)T(θ−θt)+12(θ−θt)TH(θt)(θ−θt) L(\theta) \approx L(\theta_t) + \nabla L(\theta_t)^T(\theta-\theta_t) + \frac{1}{2}(\theta-\theta_t)^T H(\theta_t)(\theta-\theta_t) L(θ)≈L(θt)+∇L(θt)T(θ−θt)+21(θ−θt)TH(θt)(θ−θt)
其中 H(θt)H(\theta_t)H(θt) 是海森矩阵:
Hij=∂2L∂θi∂θj H_{ij} = \frac{\partial^2 L}{\partial \theta_i \partial \theta_j} Hij=∂θi∂θj∂2L
最优条件
令导数等于零,得到最优更新方向:
θt+1=θt−H−1∇L(θt) \theta_{t+1} = \theta_t - H^{-1}\nabla L(\theta_t) θt+1=θt−H−1∇L(θt)
优缺点
| 优点 | 缺点 |
|---|---|
| 二次收敛,收敛极快 | 海森矩阵计算复杂度 O(n2)O(n^2)O(n2) |
| 不需要学习率调参 | 海森矩阵求逆复杂度 O(n3)O(n^3)O(n3) |
| 对病态条件数问题鲁棒 | 内存消耗巨大 |
6.2 拟牛顿法
核心思想
拟牛顿法通过近似海森矩阵或其逆矩阵来避免直接计算和求逆。
BFGS算法
BFGS维护一个近似海森逆矩阵的矩阵 BtB_tBt:
st=θt+1−θt s_t = \theta_{t+1} - \theta_t st=θt+1−θt
yt=∇L(θt+1)−∇L(θt) y_t = \nabla L(\theta_{t+1}) - \nabla L(\theta_t) yt=∇L(θt+1)−∇L(θt)
Bt+1=(I−stytTytTst)Bt(I−ytstTytTst)+ststTytTst B_{t+1} = \left(I - \frac{s_t y_t^T}{y_t^T s_t}\right)B_t\left(I - \frac{y_t s_t^T}{y_t^T s_t}\right) + \frac{s_t s_t^T}{y_t^T s_t} Bt+1=(I−ytTststytT)Bt(I−ytTstytstT)+ytTstststT
6.3 L-BFGS
核心改进
L-BFGS(Limited-memory BFGS)不存储完整的近似矩阵,而是只存储最近的 mmm 个 (st,yt)(s_t, y_t)(st,yt) 对(通常 m=10−20m=10-20m=10−20):
算法特点
- 内存消耗:O(mn)O(mn)O(mn) 而非 O(n2)O(n^2)O(n2)
- 适合中等规模问题
- 在深度学习中使用较少(batch size受限)
7. 优化算法可视化对比
7.1 测试函数
我们使用经典的Rosenbrock函数来测试各优化器的性能:
f(x,y)=(1−x)2+100(y−x2)2 f(x, y) = (1-x)^2 + 100(y-x^2)^2 f(x,y)=(1−x)2+100(y−x2)2
该函数有一个全局最小值在 (1,1)(1, 1)(1,1),但收敛路径呈狭长的抛物线形,对优化器是极大的挑战。
7.2 完整可视化代码
python
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
def rosenbrock(x, y):
"""Rosenbrock函数 (香蕉函数)"""
return (1 - x)**2 + 100 * (y - x**2)**2
def rosenbrock_grad(x, y):
"""Rosenbrock函数的梯度"""
dx = -2 * (1 - x) - 400 * x * (y - x**2)
dy = 200 * (y - x**2)
return np.array([dx, dy])
class OptimizerComparison:
"""优化器对比可视化类"""
def __init__(self, lr=0.001, n_iterations=5000):
self.lr = lr
self.n_iterations = n_iterations
self.start_point = np.array([-1.5, 2.0])
def sgd(self):
"""标准SGD"""
path = [self.start_point.copy()]
theta = self.start_point.copy()
for _ in range(self.n_iterations):
grad = rosenbrock_grad(theta[0], theta[1])
theta = theta - self.lr * grad
path.append(theta.copy())
# 早停条件
if rosenbrock(theta[0], theta[1]) < 1e-6:
break
return np.array(path)
def momentum(self, gamma=0.9):
"""带动量的SGD"""
path = [self.start_point.copy()]
theta = self.start_point.copy()
v = np.zeros(2)
for _ in range(self.n_iterations):
grad = rosenbrock_grad(theta[0], theta[1])
v = gamma * v + self.lr * grad
theta = theta - v
path.append(theta.copy())
if rosenbrock(theta[0], theta[1]) < 1e-6:
break
return np.array(path)
def adam_optimizer(self, beta1=0.9, beta2=0.999, epsilon=1e-8):
"""Adam优化器"""
path = [self.start_point.copy()]
theta = self.start_point.copy()
m = np.zeros(2)
v = np.zeros(2)
t = 0
for _ in range(self.n_iterations):
t += 1
grad = rosenbrock_grad(theta[0], theta[1])
m = beta1 * m + (1 - beta1) * grad
v = beta2 * v + (1 - beta2) * (grad ** 2)
m_hat = m / (1 - beta1 ** t)
v_hat = v / (1 - beta2 ** t)
theta = theta - (self.lr / (np.sqrt(v_hat) + epsilon)) * m_hat
path.append(theta.copy())
if rosenbrock(theta[0], theta[1]) < 1e-6:
break
return np.array(path)
def rmsprop_optimizer(self, rho=0.9, epsilon=1e-8):
"""RMSprop优化器"""
path = [self.start_point.copy()]
theta = self.start_point.copy()
r = np.zeros(2)
for _ in range(self.n_iterations):
grad = rosenbrock_grad(theta[0], theta[1])
r = rho * r + (1 - rho) * (grad ** 2)
theta = theta - (self.lr / (np.sqrt(r) + epsilon)) * grad
path.append(theta.copy())
if rosenbrock(theta[0], theta[1]) < 1e-6:
break
return np.array(path)
def visualize_all(self):
"""可视化所有优化器的收敛路径"""
# 生成等高线数据
x = np.linspace(-2, 2, 400)
y = np.linspace(-1, 3, 400)
X, Y = np.meshgrid(x, y)
Z = rosenbrock(X, Y)
# 运行各优化器
paths = {
'SGD': self.sgd(),
'Momentum': self.momentum(),
'RMSprop': self.rmsprop_optimizer(),
'Adam': self.adam_optimizer()
}
# 创建图形
fig, axes = plt.subplots(2, 2, figsize=(14, 12))
axes = axes.flatten()
colors = ['red', 'blue', 'green', 'purple']
for idx, (name, path) in enumerate(paths.items()):
ax = axes[idx]
# 绘制等高线
contour = ax.contour(X, Y, Z, levels=np.logspace(-1, 3, 20),
cmap='viridis', alpha=0.6)
ax.clabel(contour, inline=True, fontsize=8)
# 绘制优化路径
ax.plot(path[:, 0], path[:, 1],
color=colors[idx], linewidth=2, label=f'{name} Path')
ax.scatter(path[0, 0], path[0, 1],
color='green', s=100, marker='o', label='Start', zorder=5)
ax.scatter(path[-1, 0], path[-1, 1],
color='red', s=100, marker='*', label='End', zorder=5)
# 标记全局最小值
ax.scatter([1], [1], color='gold', s=150, marker='X',
label='Global Min', zorder=5)
ax.set_xlabel('x', fontsize=12)
ax.set_ylabel('y', fontsize=12)
ax.set_title(f'{name} (Iterations: {len(path)-1})', fontsize=14)
ax.legend()
ax.grid(True, alpha=0.3)
ax.set_xlim(-2, 2)
ax.set_ylim(-1, 3)
plt.tight_layout()
plt.savefig('optimizer_comparison.png', dpi=150, bbox_inches='tight')
plt.show()
return paths
# 运行可视化
if __name__ == "__main__":
comparison = OptimizerComparison(lr=0.001, n_iterations=3000)
paths = comparison.visualize_all()
# 打印最终收敛结果
print("优化器收敛结果对比:")
print("-" * 50)
for name, path in paths.items():
final = path[-1]
final_loss = rosenbrock(final[0], final[1])
print(f"{name:12s}: 位置=({final[0]:.6f}, {final[1]:.6f}), 损失={final_loss:.8f}")
7.3 可视化结果分析
运行上述代码后,你将观察到:
| 优化器 | 收敛速度 | 路径特征 | 推荐场景 |
|---|---|---|---|
| SGD | 慢 | 震荡明显 | 简单问题 |
| Momentum | 中等 | 惯性滑行 | 存在平坦区域 |
| RMSprop | 快 | 直接指向最优 | 非平稳目标 |
| Adam | 最快 | 平滑高效 | 通用首选 |
8. 实战案例:MNIST手写数字识别优化对比
8.1 实验设计
我们将使用PyTorch实现一个简单的神经网络,对比不同优化器在MNIST数据集上的表现。
8.2 完整实战代码
python
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import time
# 设置随机种子保证可复现
torch.manual_seed(42)
# 设备配置
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"使用设备: {device}")
# 数据预处理
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,)) # MNIST标准化参数
])
# 加载MNIST数据集
train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST('./data', train=False, transform=transform)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=128, shuffle=True)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=1000, shuffle=False)
# 定义神经网络模型
class SimpleNN(nn.Module):
"""简单的三层全连接神经网络"""
def __init__(self):
super(SimpleNN, self).__init__()
self.fc1 = nn.Linear(28*28, 256)
self.fc2 = nn.Linear(256, 128)
self.fc3 = nn.Linear(128, 10)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(0.2)
def forward(self, x):
x = x.view(-1, 28*28) # 展平
x = self.relu(self.fc1(x))
x = self.dropout(x)
x = self.relu(self.fc2(x))
x = self.dropout(x)
x = self.fc3(x)
return x
def train_model(optimizer_name, optimizer_fn, epochs=10):
"""
训练模型并记录指标
参数:
optimizer_name: 优化器名称
optimizer_fn: 创建优化器的函数
epochs: 训练轮数
"""
model = SimpleNN().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optimizer_fn(model)
train_losses = []
train_accuracies = []
test_accuracies = []
times = []
print(f"\n{'='*50}")
print(f"训练使用优化器: {optimizer_name}")
print('='*50)
start_time = time.time()
for epoch in range(epochs):
model.train()
epoch_loss = 0
correct = 0
total = 0
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
# 前向传播
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
# 反向传播
loss.backward()
optimizer.step()
# 统计
epoch_loss += loss.item()
_, predicted = output.max(1)
total += target.size(0)
correct += predicted.eq(target).sum().item()
# 计算训练集指标
train_loss = epoch_loss / len(train_loader)
train_acc = 100. * correct / total
train_losses.append(train_loss)
train_accuracies.append(train_acc)
# 测试集评估
model.eval()
test_correct = 0
test_total = 0
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
_, predicted = output.max(1)
test_total += target.size(0)
test_correct += predicted.eq(target).sum().item()
test_acc = 100. * test_correct / test_total
test_accuracies.append(test_acc)
elapsed = time.time() - start_time
times.append(elapsed)
print(f"Epoch [{epoch+1}/{epochs}] "
f"Loss: {train_loss:.4f} | "
f"Train Acc: {train_acc:.2f}% | "
f"Test Acc: {test_acc:.2f}% | "
f"Time: {elapsed:.2f}s")
return {
'name': optimizer_name,
'train_losses': train_losses,
'train_accuracies': train_accuracies,
'test_accuracies': test_accuracies,
'times': times,
'final_test_acc': test_accuracies[-1]
}
def run_comparison():
"""运行所有优化器的对比实验"""
# 定义要测试的优化器
optimizers = {
'SGD': lambda m: optim.SGD(m.parameters(), lr=0.01),
'SGD+Momentum': lambda m: optim.SGD(m.parameters(), lr=0.01, momentum=0.9),
'AdaGrad': lambda m: optim.Adagrad(m.parameters(), lr=0.01),
'RMSprop': lambda m: optim.RMSprop(m.parameters(), lr=0.001),
'Adam': lambda m: optim.Adam(m.parameters(), lr=0.001),
'AdamW': lambda m: optim.AdamW(m.parameters(), lr=0.001, weight_decay=1e-4)
}
results = []
for name, opt_fn in optimizers.items():
result = train_model(name, opt_fn, epochs=10)
results.append(result)
return results
def plot_results(results):
"""绘制对比结果"""
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
colors = ['blue', 'green', 'orange', 'red', 'purple', 'brown']
# 绘制训练损失
ax = axes[0]
for idx, r in enumerate(results):
ax.plot(r['train_losses'], label=r['name'], color=colors[idx], linewidth=2)
ax.set_xlabel('Epoch', fontsize=12)
ax.set_ylabel('Training Loss', fontsize=12)
ax.set_title('Training Loss Comparison', fontsize=14)
ax.legend()
ax.grid(True, alpha=0.3)
# 绘制训练准确率
ax = axes[1]
for idx, r in enumerate(results):
ax.plot(r['train_accuracies'], label=r['name'], color=colors[idx], linewidth=2)
ax.set_xlabel('Epoch', fontsize=12)
ax.set_ylabel('Training Accuracy (%)', fontsize=12)
ax.set_title('Training Accuracy Comparison', fontsize=14)
ax.legend()
ax.grid(True, alpha=0.3)
# 绘制测试准确率
ax = axes[2]
for idx, r in enumerate(results):
ax.plot(r['test_accuracies'], label=r['name'], color=colors[idx], linewidth=2)
ax.set_xlabel('Epoch', fontsize=12)
ax.set_ylabel('Test Accuracy (%)', fontsize=12)
ax.set_title('Test Accuracy Comparison', fontsize=14)
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('mnist_optimizer_comparison.png', dpi=150, bbox_inches='tight')
plt.show()
# 打印最终对比表
print("\n" + "="*70)
print("优化器性能对比总结")
print("="*70)
print(f"{'优化器':<20} {'最终测试准确率':<20} {'训练时间(秒)':<15}")
print("-"*70)
for r in results:
print(f"{r['name']:<20} {r['final_test_acc']:<20.2f} {r['times'][-1]:<15.2f}")
print("="*70)
# 主程序
if __name__ == "__main__":
results = run_comparison()
plot_results(results)
8.3 实验结果分析
通过运行上述代码,你通常会发现:
| 优化器 | 收敛速度 | 最终准确率 | 稳定性 |
|---|---|---|---|
| SGD | 慢 | 中等 | 一般 |
| SGD+Momentum | 中等 | 高 | 好 |
| AdaGrad | 中等 | 中等 | 一般 |
| RMSprop | 快 | 高 | 好 |
| Adam | 最快 | 高 | 很好 |
| AdamW | 快 | 最高 | 很好 |
一句话总结:对于MNIST这样的标准数据集,自适应优化器(Adam、AdamW)通常收敛更快且最终性能更好;SGD配合动量虽然收敛慢,但泛化性能往往不差。
9. 避坑小贴士
9.1 学习率设置
| 问题 | 表现 | 解决方案 |
|---|---|---|
| 学习率过大 | 损失震荡甚至发散 | 减小学习率,或使用学习率衰减 |
| 学习率过小 | 收敛极慢,陷入局部最优 | 增大学习率,或使用自适应优化器 |
| 学习率固定 | 前期收敛慢,后期震荡 | 使用学习率调度器(Scheduler) |
推荐学习率:
- SGD: 0.01 - 0.1
- Adam/AdamW: 0.0001 - 0.001
- RMSprop: 0.001 - 0.01
9.2 Batch Size选择
| Batch Size | 优点 | 缺点 |
|---|---|---|
| 小 (32-64) | 泛化好,内存占用小 | 训练慢,梯度噪声大 |
| 中 (128-256) | 平衡选择 | 需要调参 |
| 大 (512+) | 训练快,梯度稳定 | 泛化可能下降,内存需求大 |
9.3 优化器选择建议
根据任务类型选择:
| 任务类型 | 推荐优化器 | 理由 |
|---|---|---|
| 计算机视觉 | SGD+Momentum / AdamW | 泛化性能好 |
| 自然语言处理 | Adam / AdamW | 处理稀疏梯度效果好 |
| 强化学习 | RMSprop / Adam | 适应非平稳目标 |
| 生成模型 | Adam | 训练稳定 |
| 大语言模型(2024+) | AdamW / Lion / Sophia | 内存效率和收敛速度 |
9.4 常见错误
- 混用权重衰减和学习率衰减:AdamW已经解耦了权重衰减,不需要额外的L2正则
- 忽视梯度裁剪:RNN训练时梯度爆炸很常见,应使用梯度裁剪
- Batch Size与学习率不匹配:增大batch size时应同比增大学习率
10. 本章小结
核心知识点回顾
-
优化问题本质:寻找损失函数的最小值点,深度学习多为非凸优化
-
梯度下降家族:
- BGD:全批量,稳定但慢
- SGD:单样本,快但震荡
- Mini-batch:折中方案,实际最常用
-
动量方法:
- 经典Momentum:积累历史梯度,加速收敛
- Nesterov:前瞻性梯度计算,效果更好
-
自适应学习率:
- AdaGrad:累积梯度平方,适合稀疏数据
- RMSprop:指数移动平均,解决AdaGrad过早衰减问题
- Adam:结合动量和自适应,通用首选
- AdamW:解耦权重衰减,效果更好
-
2024-2026最新进展:
- Lion:符号动量,内存高效
- Sophia:二阶裁剪,收敛更快
- Muon:正交化梯度,Transformer友好
-
二阶方法:牛顿法收敛快但计算成本高,L-BFGS是折中选择
选择优化器的决策流程
开始
|
v
数据规模小 + 凸问题? --> 是 --> 使用L-BFGS
| 否
v
深度学习任务? --> 否 --> 使用SGD或牛顿法
| 是
v
大语言模型(2024+)? --> 是 --> 使用AdamW/Lion/Sophia
| 否
v
需要快速原型? --> 是 --> 使用Adam/AdamW
| 否
v
追求最佳泛化性能? --> 是 --> 使用SGD+Momentum + 仔细调参
| 否
v
使用AdamW + 学习率调度
11. 思考与练习
基础练习
-
梯度下降推导:从泰勒展开出发,推导梯度下降的更新公式。
-
动量分析:解释为什么动量方法能够加速收敛并减少震荡。
-
Adam偏差修正:推导Adam中偏差修正的公式,解释为什么需要偏差修正。
进阶练习
-
优化器实现:完整实现Lion优化器,并在MNIST数据集上与Adam对比。
-
学习率调度:实现余弦退火(Cosine Annealing)学习率调度,观察其对收敛的影响。
-
二阶近似:实现使用对角海森近似的优化器,比较其与Adam的性能。
挑战练习
-
Sophia实现:基于论文实现Sophia优化器的简化版本。
-
优化器组合:设计一个自适应选择优化器的策略,根据训练阶段动态切换。
-
大规模实验:在CIFAR-10或ImageNet上对比不同优化器的性能。
学习建议:优化算法需要理论与实践结合。建议读者动手实现各个优化器,并在不同数据集上观察其行为差异。
本系列教程持续更新中,欢迎关注专栏获取更多机器学习深度内容。