📐 优化理论¶
难度:⭐⭐⭐⭐⭐ | 预计学习时间:8-10小时 | 重要性:理解训练过程的核心
🎯 学习目标¶
- 掌握梯度下降及其变体(SGD/Momentum/Adam)的数学推导
- 理解凸优化与非凸优化的区别
- 掌握学习率调度策略
- 了解约束优化(拉格朗日/KKT条件)在SVM等模型中的应用
1. 梯度下降基础¶
1.1 梯度与方向导数¶
梯度 \(\nabla f(x)\) 指向函数增长最快方向。负梯度方向是局部下降最快方向。
import numpy as np
import matplotlib.pyplot as plt
def gradient_descent_2d(f, grad_f, x0, lr=0.1, n_iters=50):
"""2D梯度下降可视化"""
path = [x0.copy()]
x = x0.copy()
for _ in range(n_iters):
g = grad_f(x)
x = x - lr * g
path.append(x.copy())
return np.array(path) # np.array创建NumPy数组
# Rosenbrock函数(经典难优化函数)
def rosenbrock(x):
return (1 - x[0])**2 + 100*(x[1] - x[0]**2)**2
def rosenbrock_grad(x):
dx = -2*(1 - x[0]) - 400*x[0]*(x[1] - x[0]**2)
dy = 200*(x[1] - x[0]**2)
return np.array([dx, dy])
x0 = np.array([-1.0, 1.0])
path = gradient_descent_2d(rosenbrock, rosenbrock_grad, x0, lr=0.001, n_iters=200)
print(f"起点: {x0}, 终点: {path[-1]}, 最优: [1, 1]")
1.2 学习率的影响¶
| 学习率 | 效果 | 典型值 |
|---|---|---|
| 太大 | 震荡/发散 | — |
| 太小 | 收敛极慢 | — |
| 适中 | 稳定收敛 | \(10^{-3} \sim 10^{-1}\) |
1.3 梯度下降收敛性分析(凸函数情况)¶
假设: - \(f\) 是 \(L\)-smooth 凸函数(梯度Lipschitz连续):\(\|\nabla f(x) - \nabla f(y)\| \leq L\|x - y\|\) - \(f\) 是 \(\mu\)-强凸函数(可选,加速收敛):\(f(y) \geq f(x) + \nabla f(x)^T(y-x) + \frac{\mu}{2}\|y-x\|^2\)
收敛性证明:
由 \(L\)-smooth 性质,对凸函数有:
代入 \(x_{t+1} = x_t - \eta \nabla f(x_t)\):
当 \(\eta = \frac{1}{L}\) 时,\(1 - \frac{L\eta}{2} = \frac{1}{2}\),故:
由凸性 \(f(x^*) \geq f(x_t) + \nabla f(x_t)^T(x^* - x_t)\),结合Cauchy-Schwarz:
收敛速率: - 一般凸函数:\(f(x_T) - f(x^*) = O(1/T)\)(次线性收敛) - 强凸函数(\(\mu > 0\)):\(f(x_T) - f(x^*) = O((1 - \mu/L)^T)\)(线性收敛)
import numpy as np
def gradient_descent_convergence_demo():
"""演示梯度下降收敛性"""
# 二次函数 f(x) = 0.5 * x^T A x - b^T x
# 最优解 x* = A^{-1} b
A = np.array([[2.0, 0.5], [0.5, 1.0]]) # 正定矩阵
b = np.array([1.0, 1.0])
x_star = np.linalg.solve(A, b) # np.linalg线性代数运算
f_star = 0.5 * x_star @ A @ x_star - b @ x_star
L = np.linalg.eigvalsh(A).max() # Lipschitz常数
mu = np.linalg.eigvalsh(A).min() # 强凸参数
eta = 1.0 / L # 最优学习率
x = np.array([5.0, 5.0])
errors = []
for t in range(100):
grad = A @ x - b
x = x - eta * grad
f_val = 0.5 * x @ A @ x - b @ x
errors.append(f_val - f_star)
print(f"L = {L:.2f}, μ = {mu:.2f}, 条件数 = {L/mu:.2f}")
print(f"理论收敛率: O((1 - μ/L)^t) = O({1 - mu/L:.4f}^t)")
print(f"实际收敛: f(x_10) - f* = {errors[9]:.6f}, f(x_50) - f* = {errors[49]:.10f}")
gradient_descent_convergence_demo()
2. SGD及其变体¶
2.1 随机梯度下降¶
- 每次只用一个/一小批样本估计梯度
- 方差大,但能逃离局部最优
- Mini-batch SGD 是实际标配(batch_size=32~256)
2.2 Momentum(动量法)¶
物理直觉:小球沿坡滚下,积累速度。\(\gamma\) 典型值 \(0.9\)。
2.3 Adam(面试最常问)¶
结合Momentum(一阶矩估计)+ RMSProp(二阶矩估计):
偏差修正: $\(\hat{m}_t = \frac{m_t}{1-\beta_1^t}, \quad \hat{v}_t = \frac{v_t}{1-\beta_2^t}\)$
更新规则: $\(\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t\)$
class AdamOptimizer:
"""手写Adam优化器"""
def __init__(self, lr=0.001, beta1=0.9, beta2=0.999, eps=1e-8): # __init__构造方法,创建对象时自动调用
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.eps = eps
self.m = None # 一阶矩
self.v = None # 二阶矩
self.t = 0 # 时间步
def step(self, params, grads):
if self.m is None:
self.m = np.zeros_like(params)
self.v = np.zeros_like(params)
self.t += 1
self.m = self.beta1 * self.m + (1 - self.beta1) * grads
self.v = self.beta2 * self.v + (1 - self.beta2) * grads**2
# 偏差修正
m_hat = self.m / (1 - self.beta1**self.t)
v_hat = self.v / (1 - self.beta2**self.t)
params -= self.lr * m_hat / (np.sqrt(v_hat) + self.eps)
return params
# 对比SGD vs Momentum vs Adam
def compare_optimizers():
"""在Rosenbrock函数上对比优化器"""
x_sgd = np.array([-1.0, 1.0])
x_mom = np.array([-1.0, 1.0])
x_adam = np.array([-1.0, 1.0])
adam = AdamOptimizer(lr=0.01)
v_mom = np.zeros(2)
paths = {"SGD": [x_sgd.copy()], "Momentum": [x_mom.copy()], "Adam": [x_adam.copy()]}
for _ in range(500):
g = rosenbrock_grad(x_sgd)
x_sgd = x_sgd - 0.0005 * g
paths["SGD"].append(x_sgd.copy())
g = rosenbrock_grad(x_mom)
v_mom = 0.9 * v_mom + 0.0005 * g
x_mom = x_mom - v_mom
paths["Momentum"].append(x_mom.copy())
g = rosenbrock_grad(x_adam)
x_adam = adam.step(x_adam, g)
paths["Adam"].append(x_adam.copy())
for name, path in paths.items():
p = np.array(path)
print(f"{name:10s}: 终点=({p[-1,0]:.4f}, {p[-1,1]:.4f}), "
f"距最优={np.linalg.norm(p[-1] - [1, 1]):.6f}")
compare_optimizers()
2.4 优化器对比¶
| 优化器 | 特点 | 推荐场景 |
|---|---|---|
| SGD+Momentum | 泛化好,需调lr | CV任务(如ResNet训练) |
| Adam | 收敛快,默认好用 | NLP/Transformer/通用 |
| AdamW | Adam+权重衰减解耦 | 常用,常见于Transformer/LLM训练 |
| LAMB/LARS | 大batch训练 | 分布式训练 |
| Lion | 较新的优化器,更省内存 | 实验阶段 |
3. 学习率调度¶
3.1 常见策略¶
import numpy as np
import matplotlib.pyplot as plt
# 模拟不同学习率策略
steps = 100
lrs = {}
# Step Decay
lr = 0.1
lrs["StepDecay"] = []
for i in range(steps):
if i in [30, 60, 80]: lr *= 0.1
lrs["StepDecay"].append(lr)
# Cosine Annealing
lrs["Cosine"] = [0.1 * 0.5 * (1 + np.cos(np.pi * i / steps)) for i in range(steps)] # 列表推导式,简洁创建列表
# Warmup + Cosine (Transformer标配)
warmup = 10
lrs["Warmup+Cosine"] = []
for i in range(steps):
if i < warmup:
lrs["Warmup+Cosine"].append(0.1 * (i + 1) / warmup)
else:
progress = (i - warmup) / (steps - warmup)
lrs["Warmup+Cosine"].append(0.1 * 0.5 * (1 + np.cos(np.pi * progress)))
# 1/sqrt(t) decay (原始Transformer)
lrs["InvSqrt"] = [0.1 * min(1/(i+1)**0.5, (i+1)*warmup**(-1.5)) for i in range(steps)]
for name, lr_list in lrs.items():
plt.plot(lr_list, label=name)
plt.xlabel("Step"); plt.ylabel("LR"); plt.legend(); plt.title("学习率调度策略对比")
plt.show()
3.2 Warmup为什么重要?¶
- 训练初期参数随机,梯度方向不稳定
- 大学习率会导致前期loss爆炸
- Warmup让模型先"稳住",再加速训练
- 在大模型训练中常见 Warmup→Cosine Decay 组合
4. 凸优化与非凸优化¶
4.1 凸函数¶
\(f\) 是凸函数当且仅当:\(f(\lambda x + (1-\lambda)y) \leq \lambda f(x) + (1-\lambda)f(y)\)
等价条件:Hessian矩阵 \(H \succeq 0\)(半正定)
- 凸优化:局部最优 = 全局最优,SVM/逻辑回归是凸
- 非凸优化:DL都是非凸,有大量鞍点
4.2 深度学习的优化景观¶
# 可视化:高维空间中鞍点远多于局部最小值
# Hessian特征值:n维空间,每个方向独立为正/负(概率各50%)
# 所有方向都为正(局部最小)的概率 = 0.5^n → 指数衰减!
import matplotlib.pyplot as plt
dims = np.arange(1, 101)
p_local_min = 0.5 ** dims
plt.semilogy(dims, p_local_min)
plt.xlabel("参数维度")
plt.ylabel("随机临界点是局部最小值的概率")
plt.title("高维空间中鞍点远多于局部最小值")
plt.grid(True)
plt.show()
# 这就是为什么SGD实际效果好——大多数"卡住"是鞍点,SGD噪声能逃出
5. 约束优化¶
5.1 拉格朗日乘子法¶
拉格朗日函数: $\(L(x, \lambda, \nu) = f(x) + \sum_i \lambda_i g_i(x) + \sum_j \nu_j h_j(x)\)$
5.2 KKT条件(SVM推导核心)¶
- 原始可行:\(g_i(x^*) \leq 0\)
- 对偶可行:\(\lambda_i \geq 0\)
- 互补松弛:\(\lambda_i g_i(x^*) = 0\)(支持向量机名字的来源!)
- 稳定性:\(\nabla_x L = 0\)
5.3 SVM对偶问题推导¶
原始问题:\(\min_{w,b} \frac{1}{2}\|w\|^2 \quad \text{s.t.} \quad y_i(w \cdot x_i + b) \geq 1, \; \forall i\)
Step 1: 构造拉格朗日函数(引入乘子 \(\alpha_i \geq 0\)):
Step 2: 对原始变量求导并令其为0:
Step 3: 代回拉格朗日函数消去 \(w\) 和 \(b\):
将 \(w = \sum_i \alpha_i y_i x_i\) 代入 \(L\):
对偶问题:\(\max_\alpha \sum_i \alpha_i - \frac{1}{2}\sum_{i,j} \alpha_i \alpha_j y_i y_j (x_i \cdot x_j) \quad \text{s.t.} \; \alpha_i \geq 0, \; \sum_i \alpha_i y_i = 0\)
注意:对偶问题中只涉及 \(x_i \cdot x_j\)(内积),这正是核技巧的入口——将内积替换为核函数 \(K(x_i, x_j)\) 即可处理非线性分类。
from sklearn.svm import SVC
from sklearn.datasets import make_classification
X, y = make_classification(n_samples=100, n_features=2,
n_redundant=0, random_state=42)
svc = SVC(kernel='linear', C=1.0)
svc.fit(X, y)
print(f"支持向量数量: {svc.n_support_}")
print(f"总样本数: {len(X)}")
print(f"支持向量比例: {sum(svc.n_support_)/len(X):.2%}")
# 互补松弛条件: 只有支持向量的α>0,其余都=0
6. 深度学习优化技巧¶
6.1 梯度消失/爆炸¶
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 梯度消失 | sigmoid饱和/深层链式乘法 | ReLU/ResNet/LayerNorm/LSTM |
| 梯度爆炸 | 大权重连乘 | 梯度裁剪(Gradient Clipping) |
import torch
def gradient_clipping_demo():
"""梯度裁剪演示 — 大模型训练常用"""
model = torch.nn.Linear(10, 10)
# 模拟大梯度
x = torch.randn(1, 10) * 100
loss = model(x).sum()
loss.backward() # 反向传播计算梯度
# 裁剪前
total_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=float('inf'))
print(f"裁剪前梯度范数: {total_norm:.2f}")
# 重新计算
model.zero_grad()
loss = model(x).sum()
loss.backward()
# 裁剪(max_norm=1.0是LLM常用值)
# 注意: clip_grad_norm_ 返回裁剪前的梯度范数
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
post_clip_norm = sum(p.grad.norm()**2 for p in model.parameters() if p.grad is not None).sqrt()
print(f"裁剪后梯度范数: {post_clip_norm:.2f}")
gradient_clipping_demo()
6.2 权重初始化¶
🎓 面试常考题¶
- SGD vs Adam,什么时候用哪个? — CV用SGD+Momentum泛化好,NLP/LLM用AdamW
- Adam的超参数含义? — \(\beta_1\)=一阶矩(梯度均值)衰减(0.9),\(\beta_2\)=二阶矩(梯度平方均值)衰减(0.999),\(\epsilon\)=数值稳定(\(10^{-8}\))
- 为什么需要Warmup? — 初期参数随机梯度不稳,大lr会发散
- 梯度消失怎么解决? — ResNet(残差连接)、LayerNorm、ReLU、LSTM门控
- SVM为什么要转对偶? — 引入核函数、样本维度>特征维度时高效
- KKT互补松弛条件的物理含义? — 只有在约束边界上的样本(支持向量)才对决策有贡献
- 为什么DL不怕局部最优? — 高维空间鞍点远多于局部最小,且好的局部最小loss差不多
- BatchNorm为什么有效? — 减少Internal Covariate Shift,允许更大lr,有轻微正则化效果
📖 7. Adam优化器家族深度解析¶
7.1 从SGD到Adam的演进¶
| 优化器 | 更新规则 | 核心思想 | 适用场景 |
|---|---|---|---|
| SGD | \(\theta \leftarrow \theta - \eta g\) | 最朴素 | 凸优化 |
| Momentum | \(v \leftarrow \beta v + g; \theta \leftarrow \theta - \eta v\) | 指数滑动平均 | 加速收敛 |
| RMSProp | 自适应学习率 per-param | 二阶矩估计 | 非平稳目标 |
| Adam | 一阶矩+二阶矩+偏差校正 | 集大成者 | 通用默认 |
| AdamW | Adam + 解耦权重衰减 | 修正L2正则化 | Transformer标配 |
7.2 Adam vs AdamW 的关键区别¶
Adam with L2 regularization:
问题:L2正则化项也被自适应学习率缩放了,大梯度参数的正则化被削弱!
AdamW(解耦权重衰减):
权重衰减独立于自适应学习率,对每个参数的惩罚一致。
import torch
class AdamW:
"""手写AdamW优化器"""
def __init__(self, params, lr=1e-3, betas=(0.9, 0.999), eps=1e-8, weight_decay=0.01):
self.params = list(params)
self.lr = lr
self.beta1, self.beta2 = betas
self.eps = eps
self.wd = weight_decay
self.t = 0
self.m = [torch.zeros_like(p) for p in self.params]
self.v = [torch.zeros_like(p) for p in self.params]
def step(self):
self.t += 1
for i, p in enumerate(self.params): # enumerate同时获取索引和元素
if p.grad is None:
continue
g = p.grad.data
# 一阶矩(均值)
self.m[i] = self.beta1 * self.m[i] + (1 - self.beta1) * g
# 二阶矩(方差)
self.v[i] = self.beta2 * self.v[i] + (1 - self.beta2) * g ** 2
# 偏差校正
m_hat = self.m[i] / (1 - self.beta1 ** self.t)
v_hat = self.v[i] / (1 - self.beta2 ** self.t)
# 参数更新 = Adam步 + 解耦权重衰减
p.data -= self.lr * (m_hat / (v_hat.sqrt() + self.eps) + self.wd * p.data)
def zero_grad(self):
for p in self.params:
if p.grad is not None:
p.grad.zero_()
# 验证:对比PyTorch官方实现
model = torch.nn.Linear(10, 1)
opt_official = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)
print("AdamW手写实现完成")
7.3 学习率调度策略¶
import math
class CosineAnnealingWarmup:
"""Warmup + Cosine退火调度器"""
def __init__(self, optimizer, warmup_steps, total_steps, min_lr=1e-6):
self.optimizer = optimizer
self.warmup_steps = warmup_steps
self.total_steps = total_steps
self.min_lr = min_lr
self.base_lr = optimizer.param_groups[0]['lr']
self.step_count = 0
def step(self):
self.step_count += 1
if self.step_count <= self.warmup_steps:
# 线性Warmup
lr = self.base_lr * self.step_count / self.warmup_steps
else:
# Cosine退火
progress = (self.step_count - self.warmup_steps) / (self.total_steps - self.warmup_steps)
lr = self.min_lr + 0.5 * (self.base_lr - self.min_lr) * (1 + math.cos(math.pi * progress))
for pg in self.optimizer.param_groups:
pg['lr'] = lr
return lr
# 可视化调度曲线
model = torch.nn.Linear(10, 1)
opt = torch.optim.AdamW(model.parameters(), lr=3e-4)
scheduler = CosineAnnealingWarmup(opt, warmup_steps=1000, total_steps=50000)
lrs = [scheduler.step() for _ in range(50000)]
print(f"Warmup结束lr: {lrs[999]:.6f}")
print(f"中间lr: {lrs[25000]:.6f}")
print(f"最终lr: {lrs[-1]:.6f}")
为什么Warmup有效? Adam初期二阶矩估计不准(偏差校正不够),直接用大lr会导致参数剧烈震荡。Warmup给优化器"热身"时间。
📖 8. 分布式优化¶
8.1 数据并行(Data Parallel)梯度同步¶
\(N\) 个GPU各持有模型副本,每个GPU处理 \(\frac{B}{N}\) 个样本:
通过AllReduce操作同步梯度后统一更新参数。
8.2 梯度累积(穷人的大Batch)¶
def train_with_gradient_accumulation(model, dataloader, optimizer, accumulation_steps=4):
"""梯度累积:用小显存实现大batch效果"""
model.train() # train()开启训练模式
optimizer.zero_grad()
for step, (x, y) in enumerate(dataloader):
loss = model(x, y) / accumulation_steps # 除以累积步数
loss.backward() # 梯度累积
if (step + 1) % accumulation_steps == 0:
# 梯度裁剪(防梯度爆炸)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step() # 根据梯度更新模型参数
optimizer.zero_grad()
# 等价于batch_size = micro_batch * accumulation_steps * n_gpus
print("梯度累积: 4次micro_batch=8 ≈ 1次batch=32")
8.3 混合精度训练¶
# PyTorch AMP (Automatic Mixed Precision)
scaler = torch.amp.GradScaler('cuda')
for x, y in dataloader:
optimizer.zero_grad()
# 前向:自动选fp16/fp32
with torch.amp.autocast(device_type='cuda'):
output = model(x)
loss = criterion(output, y)
# 反向:loss scale防止fp16下溢
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
为什么混合精度有效? fp16计算速度2x,显存减半,但动态范围小(±65504)。通过loss scaling防止梯度下溢,master weight保持fp32精度。
🎯 面试高频题(扩展)¶
- Adam和SGD哪个泛化好? — SGD+Momentum通常泛化更好(更可能到平坦极小值),Adam收敛快但可能到尖锐极小值。实践中有时采用Adam预训练+SGD微调
- 梯度裁剪有几种方式? — clip_grad_norm_(按总范数缩放所有参数梯度)和clip_grad_value_(逐元素截断),前者更常用
- 为什么大batch需要线性缩放lr? — batch增大k倍,梯度方差减小k倍,可用更大lr(k倍)加速收敛。但有上限,超大batch需要LARS/LAMB
- 混合精度训练的loss scaling原理? — fp16动态范围小,小梯度会下溢为0。乘以scale放大到fp16可表示范围,更新时再除回来
- ZeRO优化是什么? — DeepSpeed的显存优化:Stage1分片优化器状态,Stage2再分片梯度,Stage3再分片参数。可线性减少显存
- 为什么Transformer常用AdamW? — 自适应学习率有助于训练稳定,解耦weight decay避免正则化被自适应缩放削弱
✅ 学习检查清单¶
- 能手写Adam优化器
- 能解释SGD/Momentum/Adam的区别和适用场景
- 能解释Warmup+Cosine调度的原因
- 能解释凸优化vs非凸优化
- 能写出SVM的拉格朗日函数和KKT条件
- 能解释梯度消失/爆炸的原因和解决方案

