优化算法总结
机器学习与深度学习优化算法体系专题¶
摘要 (Abstract)¶
在机器学习与深度学习领域,优化算法的目标是寻找模型参数 $\theta$,使得损失函数 $L(\theta)$ 最小化。根据求解策略的不同,优化算法可划分为解析法(Analytical Methods)与数值分析法(Numerical Methods)。解析法追求通过数学闭式解(Closed-form Solution)一步到位,但在深度学习的高维非线性空间中受限于计算复杂度和非凸性。数值分析法则通过迭代逼近,从二阶牛顿法降维到一阶梯度下降(GD),并演化出动量(Momentum)与自适应(Adaptive)等变体。本文将系统性地梳理这一体系,阐述各阶段算法的演进动机与应用场景。
一、 优化算法体系概览¶
优化算法的底层逻辑在于如何处理目标函数的导数信息(梯度)。其分类标准主要依据是否依赖迭代过程以及利用了几阶导数信息。
| 分类层级 | 求解策略 | 核心逻辑 | 典型算法 |
|---|---|---|---|
| 解析法 | 闭式求解 | 令 $\nabla L(\theta) = 0$ 直接求根 | 正规方程 (Normal Equation), KKT 条件 |
| 数值分析法 | 迭代搜索 | $\theta_{t+1} = \theta_t + \Delta \theta$ 逐步逼近 | 梯度下降, 牛顿法, 拟牛顿法 |
二、 解析法:理论基石及其局限性¶
解析法的核心在于利用微积分原理,通过代数变换直接锁定平稳点。
2.1 典型应用:正规方程 (Normal Equation)¶
对于线性回归模型 $y = X\theta + \epsilon$,其最小二乘损失函数的解析解为: $$\theta = (X^TX)^{-1}X^Ty$$
2.2 深度学习弃用解析法的原因¶
- 非线性障碍:神经网络中的激活函数(ReLU, Sigmoid)使得损失函数的导数方程成为复杂的超越方程,不存在代数闭式解。
- 计算复杂度:矩阵求逆 $(X^TX)^{-1}$ 的复杂度为 $O(M^3)$,$M$ 为参数量。在千万级参数的模型中,内存与算力均无法支撑。
- 秩亏损与病态矩阵:高维数据常导致 $X^TX$ 不可逆或条件数过大,导致数值解极不稳定。
三、 数值分析法:从二阶到一阶的降维¶
当无法通过解析法直接“看到”终点时,数值法选择在损失曲面上进行局部搜索。
3.1 牛顿法(二阶数值法)¶
牛顿法利用二阶泰勒展开近似局部曲面: $$\theta_{t+1} = \theta_t - H^{-1} \nabla L(\theta_t)$$ 其中 $H$ 为 Hessian 矩阵。虽然收敛速度快(二阶收敛),但在深度学习中面临 Hessian 存储成本 ($O(M^2)$) 和 计算逆矩阵代价 巨大的问题,且极易陷入鞍点。
3.2 梯度下降(一阶数值法)¶
为了适应大规模参数,深度学习退而求其次,仅利用一阶梯度信息: $$\theta_{t+1} = \theta_t - \eta \nabla L(\theta_t)$$ 其 $O(M)$ 的空间复杂度是神经网络得以训练的工程基础。
四、 深度学习优化器的演化路径¶
在进入具体算法之前,必须先理解深度学习的损失曲面与经典优化理论的核心差异。这三个挑战,直接驱动了后续每一个算法改进的动机。
凸优化的价值正是在此处体现:凸函数保证"局部最小值就是全局最小值",因此所有算法的收敛性可以被严格数学证明。深度学习损失函数虽然非凸,但研究者发现,在实践中大量的局部极小值质量相当接近全局最优(尤其是在高维空间中),真正危险的是鞍点和平坦区域。凸优化理论提供的直觉和算法框架,是分析非凸场景的基础语言。
以下逐步展示从原始梯度下降出发,每个算法如何针对性地修复前一个算法的缺陷,形成一条清晰的技术演进链。
4.1 基础:批量梯度下降(GD)¶
每次用全部训练样本计算梯度,更新参数:
$$\theta \leftarrow \theta - \eta \cdot \frac{1}{N}\sum_{i=1}^{N} \nabla_\theta L_i(\theta)$$
缺陷是显而易见的:当数据集有百万条样本时,每更新一次参数就要遍历全部数据,计算代价极高,且无法实时响应数据分布的变化。
4.2 修复计算代价:随机梯度下降(SGD)与 Mini-batch SGD¶
用单个样本(SGD)或一小批样本(Mini-batch SGD)的梯度近似替代全量梯度:
$$\theta \leftarrow \theta - \eta \cdot \nabla_\theta L_{i}(\theta) \quad \text{(随机取第 } i \text{ 个样本)}$$
Mini-batch SGD 是两者的折中,也是现代深度学习的实际标准。其中 batch size 的选取直接影响梯度估计的噪声水平和训练速度,通常取 32~512。
引入的新问题:梯度方向噪声大,在狭窄的"峡谷型"损失曲面中会剧烈震荡,收敛路径迂回。
4.3 修复震荡问题:动量法(Momentum)¶
核心思想:给参数更新加入"惯性",让历史梯度的方向也参与决策,而不是每步都完全跟随当前噪声梯度。
$$v_t = \beta v_{t-1} + (1-\beta)\nabla_\theta L(\theta) \qquad \theta \leftarrow \theta - \eta v_t$$
其中 $v_t$ 是梯度的指数加权移动平均,$\beta$ 通常取 0.9。效果相当于在"峡谷"中,横向震荡被历史动量相互抵消,纵向前进方向被不断累积放大,整体路径变得顺滑。
引入的新问题:所有参数共享同一个学习率 $\eta$,但实际情况中不同参数的梯度尺度差异巨大——稀疏特征(如 NLP 中的罕见词)出现频率低、梯度累积少,应当得到更大的更新步幅;高频特征则相反。
4.4 修复学习率问题:AdaGrad / RMSProp¶
AdaGrad 为每个参数维护独立的累积梯度平方 $s_t$,并用它来缩放学习率:
$$s_t = s_{t-1} + g_t^2 \qquad \theta \leftarrow \theta - \frac{\eta}{\sqrt{s_t + \epsilon}} g_t$$
参数更新越频繁,$s_t$ 越大,有效学习率越小;冷门参数的 $s_t$ 较小,学习率较大,正好弥补了稀疏梯度的问题。
AdaGrad 的缺陷:$s_t$ 单调递增,训练后期有效学习率趋近于零,模型停止学习。
RMSProp 用指数加权移动平均替代历史梯度的全量累积,让 $s_t$ 保持动态:
$$s_t = \gamma s_{t-1} + (1-\gamma) g_t^2 \qquad \theta \leftarrow \theta - \frac{\eta}{\sqrt{s_t + \epsilon}} g_t$$
$\gamma$ 通常取 0.9,近期梯度的权重更高,$s_t$ 不再单调增长,修复了 AdaGrad 的过度衰减问题。
4.5 终点:Adam(Adaptive Moment Estimation)¶
Adam 将 Momentum(一阶矩)与 RMSProp(二阶矩)合并到一个算法中,并加入偏差修正以消除训练初期动量估计的偏差:
$$m_t = \beta_1 m_{t-1} + (1-\beta_1) g_t \qquad \text{(一阶矩,动量)}$$
$$v_t = \beta_2 v_{t-1} + (1-\beta_2) g_t^2 \qquad \text{(二阶矩,自适应学习率)}$$
$$\hat{m}_t = \frac{m_t}{1-\beta_1^t} \qquad \hat{v}_t = \frac{v_t}{1-\beta_2^t} \qquad \text{(偏差修正)}$$
$$\theta \leftarrow \theta - \frac{\eta}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t$$
默认超参数 $\beta_1 = 0.9$,$\beta_2 = 0.999$,$\epsilon = 10^{-8}$,$\eta = 10^{-3}$。Adam 在绝大多数任务上开箱即用,是工程首选。
五、 工程实践建议与常见误区¶
5.1 决策框架¶
- 稀疏数据(如推荐系统 Embedding):首选 Adam/AdaGrad,利用其自适应特性处理低频特征。
- 计算机视觉(CV)精度追求:首选 SGD + Momentum,虽然收敛慢,但其寻找的极小值通常更“平坦”,泛化性能更好。
- 大模型(NLP/Transformer):首选 AdamW + Warmup,确保训练初期的稳定性。
5.2 核心误区提示¶
- 陷阱 1:梯度为 0 就是最优解。在深度学习中,梯度为 0 的点极有可能是鞍点而非极小值点。
- 陷阱 2:学习率越小越稳。过小的学习率会导致模型陷入局部平坦区域无法跳出,配合 Cosine Annealing(余弦退火) 往往比恒定学习率效果更好。
- 陷阱 3:Adam 优于一切。Adam 在某些场景下存在不收敛或泛化差的问题,甚至可能不如经过精细调参的 SGD。
六、 小结¶
优化算法体系是从数学上的解析精确解向工程上的数值近似解妥协的过程。在深度学习中,我们通过放弃昂贵的二阶 Hessian 信息,转而使用高效的一阶梯度,并通过动量和自适应技术,在复杂的非凸高维空间中寻找“足够好”的局部最优解。理解这一演化过程,有助于我们在面对不同规模、不同稀疏度的数据集时,做出最合理的算法选择。
举例子¶
面试官给出 $y = \sigma(x)$(其中 $\sigma$ 为 sigmoid),已知 $y$,求 $x$。
这道题实际上有两层考法:
考法 A(解析法):这个函数是单调可逆的,直接取 logit:
$$x = \ln\frac{y}{1-y}$$
考法 B(梯度下降/函数逼近):假装不知道逆函数,把它转化为一个优化问题,用迭代法求解。面试官想考察的正是这个——你是否理解"逼近"的本质。
如何用梯度下降求解?¶
第一步:构造损失函数。 将"找 $x$ 使 $\sigma(x) \approx y$"转化为最小化:
$$L(x) = \bigl(\sigma(x) - y\bigr)^2$$
第二步:求梯度。 对 $x$ 求导:
$$\frac{dL}{dx} = 2\bigl(\sigma(x) - y\bigr) \cdot \sigma(x)\bigl(1 - \sigma(x)\bigr)$$
其中 $\sigma'(x) = \sigma(x)(1-\sigma(x))$ 是 sigmoid 导数的经典结论,面试中必须能直接写出。
第三步:梯度下降迭代。
$$x_{t+1} = x_t - \eta \cdot \frac{dL}{dx}\bigg|_{x=x_t}$$
import math
def sigmoid(x):
return 1 / (1 + math.exp(-x))
def solve_by_gradient_descent(y_target, eta=0.1, steps=1501):
x = 0.0 # 初始化(任意值均可,因为损失函数是凸的)
for i in range(steps):
sig = sigmoid(x)
grad = 2 * (sig - y_target) * sig * (1 - sig)
x -= eta * grad
if i % 500 == 0:
print(f"step {i:3d}: x={x:.6f}, σ(x)={sigmoid(x):.6f}, loss={( sigmoid(x)-y_target)**2:.2e}")
return x
# 验证:令 y=0.8,解析解为 ln(0.8/0.2) = ln(4) ≈ 1.3863
x_solved = solve_by_gradient_descent(0.8)
print(f"\n解析解: {math.log(0.8/0.2):.6f}")
print(f"梯度下降解: {x_solved:.6f}")
step 0: x=0.015000, σ(x)=0.503750, loss=8.78e-02 step 500: x=1.338294, σ(x)=0.792209, loss=6.07e-05 step 1000: x=1.382751, σ(x)=0.799432, loss=3.22e-07 step 1500: x=1.386023, σ(x)=0.799957, loss=1.88e-09 解析解: 1.386294 梯度下降解: 1.386023
import torch
import torch.nn as nn
import torch.nn.functional as F
class BaseOptimizer:
def zero_grad(self, params):
for p in params:
if p.grad is not None:
p.grad.zero_()
class Adam(BaseOptimizer):
def __init__(self, lr=1e-3, beta1=0.9, beta2=0.999, eps=1e-8):
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.eps = eps
self.m = {}
self.v = {}
self.t = 0
@torch.no_grad()
def step(self, params):
self.t += 1
for p in params:
if p.grad is None:
continue
key = id(p)
if key not in self.m:
self.m[key] = torch.zeros_like(p)
self.v[key] = torch.zeros_like(p)
m = self.m[key]
v = self.v[key]
g = p.grad
m.mul_(self.beta1).add_(g, alpha=1 - self.beta1)
v.mul_(self.beta2).addcmul_(g, g, value=1 - self.beta2)
m_hat = m / (1 - self.beta1 ** self.t)
v_hat = v / (1 - self.beta2 ** self.t)
p.addcdiv_(m_hat, v_hat.sqrt().add(self.eps), value=-self.lr)
def sigmoid(x):
return 1 / (1 + torch.exp(-x))
target = torch.tensor([0.8])
x = torch.tensor([0.0], requires_grad=True)
opt = Adam(lr=0.1)
for step in range(200):
loss = 0.5 * (sigmoid(x) - target).pow(2).sum()
if x.grad is not None:
x.grad.zero_()
loss.backward()
opt.step([x])
print("optimized x:", x.item())
print("true x:", torch.logit(target).item())
optimized x: 1.3863083124160767 true x: 1.3862944841384888
常用优化器的实现和在MLP中的应用¶
import torch
import torch.nn.functional as F
from torch.utils.data import DataLoader, TensorDataset
torch.manual_seed(42)
# ══════════════════════════════════════════════════════════════════
# 基类:所有优化器的公共接口
# ══════════════════════════════════════════════════════════════════
class BaseOptimizer:
def zero_grad(self, params):
"""
每次 backward() 之后,梯度会累积在 p.grad 上。
若不清零,下一次 backward() 的梯度会叠加在旧梯度上,导致更新错误。
因此每次 step 结束(或开始)前必须手动清零。
"""
for p in params:
if p.grad is not None:
p.grad.zero_()
# ══════════════════════════════════════════════════════════════════
# 梯度下降 GD / SGD / MiniBatchSGD
# ══════════════════════════════════════════════════════════════════
class GD(BaseOptimizer):
"""
── 数学公式 ──────────────────────────────
θ_t = θ_{t-1} - lr * g_t
其中 g_t = ∂L/∂θ 为当前损失对参数的梯度
── 三种变体的区别(仅在于 g_t 的计算范围)──
GD : g_t 由全部数据计算(全量梯度,稳定但慢)
SGD : g_t 由单条样本计算(噪声大,但有正则化效果)
MiniBatchSGD: g_t 由一个 batch 计算(工程中最常用的折中方案)
三者的参数更新公式完全一致,差异仅在调用方传入的数据量,
故 SGD 和 MiniBatchSGD 直接继承 GD,无需重写任何逻辑。
"""
def __init__(self, lr=1e-2):
self.lr = lr # 学习率:控制每步更新的幅度
@torch.no_grad() # 禁用自动微分,节省内存,参数更新不需要被记录进计算图
def step(self, params):
for p in params:
if p.grad is None:
continue
# p = p - lr * grad
# add_(tensor, alpha=k) 等价于 p += k * tensor
p.add_(p.grad, alpha=-self.lr)
class SGD(GD):
pass # 完全复用 GD,语义上表示"单样本梯度下降"
class MiniBatchSGD(GD):
pass # 完全复用 GD,语义上表示"小批量梯度下降"
# ══════════════════════════════════════════════════════════════════
# Momentum(动量优化器)
# ══════════════════════════════════════════════════════════════════
class Momentum(BaseOptimizer):
"""
── 动机 ────────────────────────────────────────────────────────
纯 SGD 在梯度方向剧烈震荡时(如狭长山谷),收敛极慢。
动量的思想:引入"速度"变量 v,对历史梯度做指数加权平均,
使更新方向更平滑,同时在一致方向上加速。
── 数学公式 ──────────────────────────────
v_t = β * v_{t-1} + (1-β) * g_t # 速度 = 衰减旧速度 + 新梯度贡献
θ_t = θ_{t-1} - lr * v_t # 参数沿速度方向更新
── 参数说明 ──────────────────────────────
β(beta): 动量系数,典型值 0.9
β 越大 → 历史梯度权重越高 → 方向更平滑,但对新梯度响应更迟钝
β=0 → 退化为普通 SGD
"""
def __init__(self, lr=1e-2, beta=0.9):
self.lr = lr
self.beta = beta
# 用字典惰性存储每个参数的速度缓冲
# key = id(p):参数对象的内存地址,作为唯一标识
# 惰性初始化:第一次 step 时才为该参数创建零向量
self.v = {}
@torch.no_grad()
def step(self, params):
for p in params:
if p.grad is None:
continue
key = id(p)
if key not in self.v:
# 首次出现:初始化速度为全零(形状与参数相同)
self.v[key] = torch.zeros_like(p)
v = self.v[key] # 取出该参数对应的速度(引用,原地修改会持久化)
# v = β * v + (1-β) * g
# mul_(β) → v = β * v (衰减旧速度)
# add_(g, α=1-β) → v = v + (1-β) * g (融入新梯度)
v.mul_(self.beta).add_(p.grad, alpha=1 - self.beta)
# θ = θ - lr * v
p.add_(v, alpha=-self.lr)
# ══════════════════════════════════════════════════════════════════
# AdaGrad(自适应梯度优化器)
# ══════════════════════════════════════════════════════════════════
class AdaGrad(BaseOptimizer):
"""
── 动机 ────────────────────────────────────────────────────────
不同参数的梯度尺度差异可能很大(如 NLP 中稀疏特征 vs 高频特征)。
AdaGrad 为每个参数维护一个**累积历史梯度平方和** s,
用它来自适应地缩放每个参数的学习率:
历史梯度大的参数 → s 大 → 有效学习率小(防止过冲)
历史梯度小的参数 → s 小 → 有效学习率大(加速学习)
── 数学公式 ──────────────────────────────
s_t = s_{t-1} + g_t² # 累积梯度平方(只增不减)
θ_t = θ_{t-1} - (lr / √(s_t + ε)) * g_t
── 缺陷 ────────────────────────────────────────────────────────
s_t 单调递增,训练后期有效学习率趋近于 0,导致参数停止更新。
RMSProp 通过引入衰减系数解决此问题。
── 参数说明 ──────────────────────────────
ε(eps): 防止除以零的小量,典型值 1e-8
"""
def __init__(self, lr=1e-2, eps=1e-8):
self.lr = lr
self.eps = eps
self.s = {} # 累积梯度平方,key=id(p),惰性初始化
@torch.no_grad()
def step(self, params):
for p in params:
if p.grad is None:
continue
key = id(p)
if key not in self.s:
self.s[key] = torch.zeros_like(p)
s = self.s[key]
# s = s + g²
# pow(2) 为逐元素平方
s.add_(p.grad.pow(2))
# θ = θ - lr * g / (√s + ε)
# addcdiv_(num, denom, value=k) 等价于 p += k * (num / denom)
p.addcdiv_(p.grad, s.sqrt().add(self.eps), value=-self.lr)
# ══════════════════════════════════════════════════════════════════
# RMSProp(均方根传播)
# ══════════════════════════════════════════════════════════════════
class RMSProp(BaseOptimizer):
"""
── 动机 ────────────────────────────────────────────────────────
AdaGrad 的改进版:将"累积梯度平方"改为"指数移动平均梯度平方",
引入衰减系数 β,使 s 不再单调递增,从而在训练后期仍保持有效学习率。
── 数学公式 ──────────────────────────────
s_t = β * s_{t-1} + (1-β) * g_t² # 梯度平方的指数移动平均
θ_t = θ_{t-1} - (lr / √(s_t + ε)) * g_t
── 与 AdaGrad 的对比 ────────────────────
AdaGrad: s_t = s_{t-1} + g² → s 只增不减,最终 lr→0
RMSProp: s_t = β*s_{t-1} + (1-β)*g² → s 跟踪近期梯度,lr 稳定
── 参数说明 ──────────────────────────────
β(beta): 衰减系数,典型值 0.9,控制历史梯度的"遗忘速度"
"""
def __init__(self, lr=1e-3, beta=0.9, eps=1e-8):
self.lr = lr
self.beta = beta
self.eps = eps
self.s = {} # 梯度平方的指数移动平均,key=id(p)
@torch.no_grad()
def step(self, params):
for p in params:
if p.grad is None:
continue
key = id(p)
if key not in self.s:
self.s[key] = torch.zeros_like(p)
s = self.s[key]
# s = β * s + (1-β) * g²
# mul_(β) → s = β * s
# addcmul_(g, g, v=1-β)→ s = s + (1-β)*g*g (逐元素 g²)
s.mul_(self.beta).addcmul_(p.grad, p.grad, value=1 - self.beta)
# θ = θ - lr * g / (√s + ε)
p.addcdiv_(p.grad, s.sqrt().add(self.eps), value=-self.lr)
# ══════════════════════════════════════════════════════════════════
# Adam(自适应矩估计)= Momentum + RMSProp + 偏差修正
# ══════════════════════════════════════════════════════════════════
class Adam(BaseOptimizer):
"""
── 动机 ────────────────────────────────────────────────────────
Adam 综合了两种思想:
1. Momentum → 一阶矩 m:对梯度做指数移动平均,平滑更新方向
2. RMSProp → 二阶矩 v:对梯度平方做指数移动平均,自适应学习率
额外引入"偏差修正":训练初期 m/v 从零开始,会系统性地低估真实均值,
除以 (1-β^t) 可以补偿这一偏差(t 越大,修正系数越趋近于 1,影响消失)。
── 完整数学公式 ──────────────────────────
# Step 1: 更新一阶矩(梯度的指数移动平均)
m_t = β1 * m_{t-1} + (1-β1) * g_t
# Step 2: 更新二阶矩(梯度平方的指数移动平均)
v_t = β2 * v_{t-1} + (1-β2) * g_t²
# Step 3: 偏差修正(消除初期零初始化带来的低估)
m̂_t = m_t / (1 - β1^t)
v̂_t = v_t / (1 - β2^t)
# Step 4: 参数更新(自适应学习率)
θ_t = θ_{t-1} - lr * m̂_t / (√v̂_t + ε)
── 参数说明 ──────────────────────────────
β1=0.9 : 一阶矩衰减系数,控制梯度均值的平滑程度
β2=0.999: 二阶矩衰减系数,控制梯度方差的平滑程度(更大 → 更稳定)
ε=1e-8 : 防止除零,同时对极小梯度参数起到下界保护作用
"""
def __init__(self, lr=1e-3, beta1=0.9, beta2=0.999, eps=1e-8):
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.eps = eps
self.m = {} # 一阶矩缓冲(梯度均值),key=id(p)
self.v = {} # 二阶矩缓冲(梯度方差),key=id(p)
self.t = 0 # 全局时间步,用于偏差修正系数 (1-β^t)
@torch.no_grad()
def step(self, params):
self.t += 1 # 每调用一次 step,时间步 +1
# 提前计算偏差修正系数,避免在循环内重复计算(微小优化)
bc1 = 1 - self.beta1 ** self.t # bias correction for m
bc2 = 1 - self.beta2 ** self.t # bias correction for v
for p in params:
if p.grad is None:
continue
key = id(p)
if key not in self.m:
# 惰性初始化:首次遇到该参数时,创建全零的一/二阶矩缓冲
self.m[key] = torch.zeros_like(p)
self.v[key] = torch.zeros_like(p)
m = self.m[key] # 该参数的一阶矩(引用,原地修改会持久化)
v = self.v[key] # 该参数的二阶矩(引用)
g = p.grad # 当前梯度
# ── Step 1: 更新一阶矩 ──────────────────────
# m = β1 * m + (1-β1) * g; alpha表示g的缩放系数
m.mul_(self.beta1).add_(g, alpha=1 - self.beta1)
m.mul_(self.beta1).add_(g, alpha=1 - self.beta1)
# ── Step 2: 更新二阶矩 ──────────────────────
# v = β2 * v + (1-β2) * g²
# addcmul_(g, g, value=k) → v += k * g * g(逐元素平方,无临时张量)
v.mul_(self.beta2).addcmul_(g, g, value=1 - self.beta2)
# ── Step 3: 偏差修正 ────────────────────────
# 注意:m_hat / v_hat 是新张量(不带下划线),不修改 m/v 本身
# 必须保留未修正的 m/v 用于下一个 step 的递推
m_hat = m / bc1 # m̂ = m / (1-β1^t)
v_hat = v / bc2 # v̂ = v / (1-β2^t)
# ── Step 4: 参数更新 ────────────────────────
# θ = θ - lr * m̂ / (√v̂ + ε)
# addcdiv_(num, denom, value=k) → p += k * (num/denom) 原地操作
# v_hat.sqrt().add(self.eps):先开根号,再加 ε(均不带下划线,产生临时张量)
p.addcdiv_(m_hat, v_hat.sqrt().add(self.eps), value=-self.lr)
# ══════════════════════════════════════════════════════════════════
# 优化器族谱:演进关系一览
# ══════════════════════════════════════════════════════════════════
#
# SGD ──(加动量)──→ Momentum
# │
# └──(自适应lr)──→ AdaGrad ──(改累积为EMA)──→ RMSProp
# │
# Momentum ──────────────────┘
# └──(两者结合+偏差修正)──→ Adam
#
# 每种优化器解决的核心问题:
# SGD : 最基础,方向即梯度,无任何自适应
# Momentum : 解决震荡,在一致方向上加速
# AdaGrad : 解决特征梯度尺度差异,但后期 lr→0
# RMSProp : 修复 AdaGrad 的 lr 衰减问题
# Adam : 综合 Momentum + RMSProp,工程中最常用
# ══════════════════════════════════════════════════════════════════
# self.m和self.v的设计逻辑
# ══════════════════════════════════════════════════════════════════
# 一个简单的两层网络,有 4 个参数
# model = Linear(2 -> 4) + Linear(4 -> 1)
# # 对应 4 个 key
# self.m = {
# id(W1): tensor(shape=[2,4]), # W1 的一阶矩
# id(b1): tensor(shape=[4]), # b1 的一阶矩
# id(W2): tensor(shape=[4,1]), # W2 的一阶矩
# id(b2): tensor(shape=[1]), # b2 的一阶矩
# }
# self.v 结构完全相同
# 用 id(p) 而不是直接用 p 作为 key,原因是:
# # 张量本身不能做字典 key(不可哈希)
# self.m[p] = ... # ❌ 报错
# # id(p) 是整数(内存地址),可以做 key
# self.m[id(p)] = ... # ✅ 正常
# ```
# 所以整个逻辑链条是:
# ```
# params 里的每个参数 p
# → id(p) 作为唯一标识
# → 对应字典里独立的 m[key] 和 v[key]
# → 各自维护自己的递推历史
# → 各自计算自己的自适应学习率
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, TensorDataset
torch.manual_seed(42)
class MLP(nn.Module):
def __init__(self, input_dim, hidden_dims, output_dim, activation='relu', dropout=0.1, layernorm=True):
super().__init__()
layer_dims = [input_dim] + hidden_dims + [output_dim]
layers = []
for i in range(len(layer_dims) - 1):
layers.append(nn.Linear(layer_dims[i], layer_dims[i+1]))
if i < len(layer_dims) - 2: # 最后一层不要激活/Dropout/Norm
if layernorm:
layers.append(nn.LayerNorm(layer_dims[i+1])) # nn.LayerNorm 必须指定 特征维度大小(即输入最后一维的大小)
if activation == 'relu':
layers.append(nn.ReLU())
elif activation == 'sigmoid':
layers.append(nn.Sigmoid())
elif activation == 'tanh':
layers.append(nn.Tanh())
# Dropout接收的输入是经过ReLU激活后的向量,然后对这些激活值进行随机屏蔽,直接影响的是传递给下一层的信息
if dropout > 0:
layers.append(nn.Dropout(dropout))
# *被称为"解包操作符"(unpacking operator),它的作用是将一个可迭代对象(如列表、元组)中的元素逐个取出,作为独立的参数传递给函数。
self.network = nn.Sequential(*layers)
self.initialize_weight()
def initialize_weight(self):
for module in self.modules(): # 调用self.modules()会依次返回整个MLP模型、第一个Linear层、LayerNorm层、ReLU层、第二个Linear层等等
if isinstance(module, nn.Linear): # Linear层有权重矩阵和偏置向量需要初始化
nn.init.xavier_uniform_(module.weight) # PyTorch的设计体系中,函数名末尾的下划线表示这是一个"就地操作"(in-place operation),意思是它会直接修改传入的张量,而不是返回一个新的张量。
if module.bias is not None:
nn.init.constant_(module.bias, 0)
def forward(self, x):
return self.network(x)
def train(model, optimizer, loader, epochs=100):
history = []
for epoch in range(epochs+1):
total_loss = 0.0
for xb, yb in loader:
pred = model(xb)
loss = F.mse_loss(pred, yb)
model.zero_grad(set_to_none=True)
loss.backward()
optimizer.step(model.parameters())
total_loss += loss.item() * xb.size(0)
if epoch % 100 == 0:
print(f"{epoch}: total_loss损失为{total_loss}")
avg_loss = total_loss / len(loader.dataset)
history.append(avg_loss)
return history
N = 2000
X = torch.rand(N, 2) * 4 - 2
y = torch.sin(X[:, :1]) + 0.3 * X[:, 1:2] ** 2 + 0.1 * torch.randn(N, 1)
dataset = TensorDataset(X, y)
model = MLP(2, [8, 16], 1)
optimizer = AdaGrad(lr=1e-1)
loader = DataLoader(dataset, batch_size=32, shuffle=True)
history = train(model, optimizer, loader, epochs=500)
0: total_loss损失为507.2723476886749 100: total_loss损失为103.4418934583664 200: total_loss损失为92.72998484969139 300: total_loss损失为84.88741222023964 400: total_loss损失为79.32599598169327 500: total_loss损失为75.8373444378376