WARNING
🧪 Beta公测版本提示:教程主体已完成,正在优化细节,欢迎大家提Issue反馈问题或建议。
s08 优化器:从SGD到Adam — demo.py 代码详解
运行方式
cd docs/nn-decision/dl/optimizers/code
python demo.py代码逐段详解
第1步:导入库 — 每个库是做什么的
import numpy as np
import matplotlib.pyplot as plt
from typing import Tuple, List, Dict, Callable
import osnumpy:提供数组操作、随机数生成(np.random.randn用于模拟梯度噪声)、数学运算(np.sqrt、np.linalg.norm)。matplotlib:绘制损失地形等高线图、优化器轨迹、损失曲线、超参数游乐场子图。typing:类型注解,标注函数签名中的参数和返回值类型。
第2步:损失地形 — 狭长峡谷形的二维二次型
class LossLandscape:
def __init__(self, a: float = 20.0, b: float = 1.0):
self.a = a # 陡峭方向曲率
self.b = b # 平缓方向曲率
def __call__(self, theta: np.ndarray) -> float:
theta1, theta2 = theta[0], theta[1]
return 0.5 * (self.a * theta1 ** 2 + self.b * theta2 ** 2)
def gradient(self, theta: np.ndarray) -> np.ndarray:
theta1, theta2 = theta[0], theta[1]
return np.array([self.a * theta1, self.b * theta2])损失函数定义:
梯度:
条件数(Condition Number)
方向(系数 ):陡峭,梯度 ,稍微偏离原点就产生很大梯度 方向(系数 ):平缓,梯度 ,偏离较多才有较大梯度
全局最优解在原点
第3步:SGD — 最朴素的优化器
class SGDOptimizer:
def __init__(self, lr: float = 0.02):
self.lr = lr
def step(self, theta: np.ndarray, grad: np.ndarray) -> np.ndarray:
return theta - self.lr * grad更新公式:
特点:不记忆任何历史信息。每一步只看当前梯度,直接往反方向走。这是最纯粹的梯度下降,也是所有改进的基准线(baseline)。
SGD 存储开销:0 个额外向量——只需要存储参数本身。
第4步:Momentum — 给优化器加"惯性"
class MomentumOptimizer:
def __init__(self, lr: float = 0.02, beta: float = 0.9):
self.lr = lr
self.beta = beta
self.m = None # 速度向量
def step(self, theta: np.ndarray, grad: np.ndarray) -> np.ndarray:
if self.m is None:
self.m = np.zeros_like(theta) # m_0 = 0
self.m = self.beta * self.m + (1 - self.beta) * grad
return theta - self.lr * self.m更新公式:
直觉:
Momentum 存储开销:1 个额外向量(
为什么 (1-beta) 而非直接 beta?这是"凸组合"的标准写法:确保所有权重之和为 1。展开后
第5步:RMSProp — 给每个参数自适应步长
class RMSPropOptimizer:
def __init__(self, lr: float = 0.05, beta: float = 0.9, eps: float = 1e-8):
self.lr = lr
self.beta = beta
self.eps = eps
self.v = None
def step(self, theta: np.ndarray, grad: np.ndarray) -> np.ndarray:
if self.v is None:
self.v = np.zeros_like(theta)
self.v = self.beta * self.v + (1 - self.beta) * (grad ** 2)
return theta - self.lr * grad / (np.sqrt(self.v) + self.eps)更新公式:
直觉:
RMSProp 存储开销:1 个额外向量(
第6步:Adam — Momentum + RMSProp + 偏差修正
class AdamOptimizer:
def __init__(self, lr: float = 0.1, beta1: float = 0.9,
beta2: float = 0.999, eps: float = 1e-8):
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.eps = eps
self.m = None # 一阶矩
self.v = None # 二阶矩
self.t = 0 # 迭代步数计数器
def step(self, theta: np.ndarray, grad: np.ndarray) -> np.ndarray:
if self.m is None:
self.m = np.zeros_like(theta)
self.v = np.zeros_like(theta)
self.t += 1
# 一阶矩(Momentum 部分)
self.m = self.beta1 * self.m + (1 - self.beta1) * grad
# 二阶矩(RMSProp 部分)
self.v = self.beta2 * self.v + (1 - self.beta2) * (grad ** 2)
# 偏差修正
m_hat = self.m / (1 - self.beta1 ** self.t)
v_hat = self.v / (1 - self.beta2 ** self.t)
# 参数更新
return theta - self.lr * m_hat / (np.sqrt(v_hat) + self.eps)完整数学公式:
一阶矩(方向):
二阶矩(尺度):
偏差修正:
参数更新(Adam 核心公式):
偏差修正为什么必要?
Adam 存储开销:2 个额外向量(
第7步:运行优化器 — 记录完整轨迹
def run_optimizer(optimizer, landscape, theta_init, n_steps=100,
add_noise=False, noise_std=0.0):
theta = theta_init.copy()
trajectory = [theta.copy()]
losses = [landscape(theta)]
for _ in range(n_steps):
grad = landscape.gradient(theta)
if add_noise:
grad = grad + np.random.randn(*grad.shape) * noise_std
theta = optimizer.step(theta, grad)
trajectory.append(theta.copy())
losses.append(landscape(theta))
return np.array(trajectory), lossesadd_noise 参数用于模拟 mini-batch 梯度噪声——真实训练中,我们只能用一个 mini-batch 估计梯度,估计值总是带有噪声。这个演示让你直观地看到:Adam 对噪声的鲁棒性远优于 SGD。
第8步:可视化 — 三种对比视角
视角1:等高线图上的轨迹对比
def plot_contour_comparison(landscape, all_trajectories, filename):
# 生成等高线
Z = 0.5 * (landscape.a * X**2 + landscape.b * Y**2)
levels = np.logspace(-2, 2, 15) # 对数间隔等高线
ax.contour(X, Y, Z, levels=levels, cmap='Blues')
ax.contourf(X, Y, Z, levels=levels, cmap='Blues', alpha=0.15)
# 绘制每条优化器轨迹
for name, traj in all_trajectories.items():
ax.plot(traj[:, 0], traj[:, 1], '-', color=color, label=name)
ax.plot(traj[0, 0], traj[0, 1], 'o') # 起点
ax.plot(traj[-1, 0], traj[-1, 1], 's') # 终点np.logspace(-2, 2, 15) 生成对数间隔的 15 个高度值——近距离处(接近原点)等高线密集,远处稀疏,配合狭长峡谷的损失地形。起点用小圆点标记,终点用方块标记,清晰展示每种优化器从
视角2:损失下降曲线
对数纵轴的折线图,直观对比收敛速度。Adam 的曲线通常下降最快,SGD 在陡峭方向上震荡导致损失下降缓慢。
视角3:超参数游乐场
4 张子图分别对应 lr = 0.01, 0.05, 0.1, 0.5,每张绘制四种优化器的轨迹。展示学习率对优化器的影响:
- 小 lr = 0.01:所有优化器都收敛缓慢
- 大 lr = 0.5:SGD 剧烈震荡甚至发散,Adam 仍能稳定收敛
这直观展示了 Adam 对学习率的鲁棒性——在较宽的 lr 范围内都能稳定工作。
第9步:噪声鲁棒性对比
noisy_sgd = SGDOptimizer(lr=0.02)
noisy_adam = AdamOptimizer(lr=0.1)
traj_sgd_noisy, loss_sgd_noisy = run_optimizer(
noisy_sgd, landscape, theta_init, n_steps=100,
add_noise=True, noise_std=1.0 # 标准差为 1.0 的高斯噪声
)梯度中加入 noise_std=1.0 的噪声后,SGD 的路径剧烈抖动(每一步的梯度方向都受噪声影响),而 Adam 由于
关键概念速查表
| 优化器 | 核心记忆 | 更新公式 | 存储 | 解决的痛点 |
|---|---|---|---|---|
| SGD | 无 | 0 | —(基线) | |
| Momentum | 1x | 方向抖动 | ||
| RMSProp | 1x | 步长不统一 | ||
| Adam | 2x | 方向 + 步长 + 初始化偏差 | ||
| 条件数 | — | — | 衡量损失地形"狭长度" | |
| 指数滑动平均 | — | — | Adam 的基础运算 |
源码位置
clone 后打开(相对仓库根目录):
docs/nn-decision/dl/optimizers/code/demo.py