知识点卡片:梯度下降与SGD
基本信息
| 属性 | 内容 |
|---|---|
| 知识点 | 梯度下降 (GD/SGD/Momentum) |
| 掌握程度 | ★★★★★ |
| 学习优先级 | P0 |
| 预估时间 | 6小时 |
| 面试频率 | ★★★★★ |
核心原理
梯度下降 (GD)
θ_{t+1} = θ_t - η ∇_θ L(θ_t)
使用全部数据计算梯度。
优点:梯度精确
缺点:每步需要计算所有样本,太慢随机梯度下降 (SGD)
θ_{t+1} = θ_t - η ∇_θ L(θ_t; x_i, y_i)
每次使用一个样本计算梯度。
优点:更新快,可逃离局部最优
缺点:梯度噪声大,收敛震荡Mini-batch SGD
θ_{t+1} = θ_t - η * (1/|B|) Σ_{i∈B} ∇_θ L(θ_t; x_i, y_i)
折中方案,实际使用。Momentum
v_t = β v_{t-1} + (1-β) ∇_θ L(θ_t)
θ_{t+1} = θ_t - η v_t
类似物理动量:维持之前的方向,加速收敛
β通常=0.9代码实现
python
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
# 1. 从零实现SGD
class SGD:
def __init__(self, lr=0.01):
self.lr = lr
def update(self, params, grads):
for param, grad in zip(params, grads):
param -= self.lr * grad
# 2. 从零实现Momentum
class Momentum:
def __init__(self, lr=0.01, momentum=0.9):
self.lr = lr
self.momentum = momentum
self.velocities = None
def update(self, params, grads):
if self.velocities is None:
self.velocities = [np.zeros_like(p) for p in params]
for i, (param, grad) in enumerate(zip(params, grads)):
self.velocities[i] = self.momentum * self.velocities[i] + (1 - self.momentum) * grad
param -= self.lr * self.velocities[i]
# 3. PyTorch使用
model = nn.Linear(10, 1)
optimizer_sgd = optim.SGD(model.parameters(), lr=0.01)
optimizer_momentum = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
optimizer_nesterov = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, nesterov=True)三种方法对比
| 方法 | 梯度来源 | 更新频率 | 收敛速度 | 噪声 |
|---|---|---|---|---|
| GD | 全数据集 | 1次/epoch | 慢 | 无 |
| SGD | 单样本 | N次/epoch | 快 | 大 |
| Mini-batch | 小批量 | N/B次/epoch | 最快 | 可控 |
面试高频问题
Q1: Batch Size对训练的影响?
答:
| 方面 | 小Batch | 大Batch |
|---|---|---|
| 梯度精度 | 噪声大 | 精确 |
| 收敛速度 | 快(更新频繁) | 慢(更新少) |
| 泛化能力 | 通常更好 | 可能更差 |
| 显存占用 | 低 | 高 |
| 并行效率 | 低 | 高 |
实践:通常选32/64/128,根据显存调整。小batch的噪声有助于逃离局部最优,是一种隐式正则化。
Q2: Momentum为什么加速收敛?
答:
- 在梯度方向一致时,速度累积(加速)
- 在梯度方向震荡时,震荡互相抵消(稳定)
- 类似物理中的惯性:保持运动方向,不容易被单个样本的梯度带偏
Q3: Nesterov Momentum与普通Momentum的区别?
答:
普通Momentum:先计算当前位置梯度,再沿速度方向移动
v = βv + (1-β)∇L(θ)
θ = θ - ηv
Nesterov:先在速度方向移动,再在新位置计算梯度("前瞻")
θ_lookahead = θ - ηβv
v = βv + (1-β)∇L(θ_lookahead)
θ = θ - ηv
Nesterov更智能:在移动后再看梯度,可以"提前减速"
理论上有更好的收敛率(O(1/t²) vs O(1/t))学习率的影响
python
# 学习率过大 → 震荡甚至发散
# 学习率过小 → 收敛太慢
# 学习率适中 → 平滑收敛
# 学习率折线图
lrs = [0.001, 0.01, 0.1, 1.0]
for lr in lrs:
losses = []
x = 5.0
for _ in range(20):
grad = 2 * (x - 3) # f(x) = (x-3)²
x = x - lr * grad
losses.append((x - 3) ** 2)
print(f"lr={lr}: final loss={losses[-1]:.6f}")
# lr=1.0震荡,lr=0.01正常收敛练习题
python
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
# 模拟不同优化器的轨迹
def rosenbrock(x, y):
"""香蕉函数:经典优化测试函数"""
return (1 - x)**2 + 100 * (y - x**2)**2
# 梯度
def rosenbrock_grad(x, y):
dx = -2*(1-x) - 400*x*(y - x**2)
dy = 200*(y - x**2)
return np.array([dx, dy])
# GD vs SGD vs Momentum
x_gd = np.array([-1.0, 1.0])
x_sgd = np.array([-1.0, 1.0])
x_mom = np.array([-1.0, 1.0])
lr = 0.001
v = np.zeros(2)
hist = {'gd': [], 'sgd': [], 'mom': []}
for i in range(1000):
# GD
grad_full = rosenbrock_grad(*x_gd)
x_gd -= lr * grad_full
hist['gd'].append(x_gd.copy())
# SGD(加噪声模拟)
grad_noisy = grad_full + np.random.randn(2) * 0.5
x_sgd -= lr * grad_noisy
hist['sgd'].append(x_sgd.copy())
# Momentum
grad_mom = rosenbrock_grad(*x_mom)
v = 0.9 * v + (1 - 0.9) * grad_mom
x_mom -= lr * v
hist['mom'].append(x_mom.copy())
print(f"GD final: {x_gd}")
print(f"SGD final: {x_sgd}")
print(f"Momentum final: {x_mom}")