AI算法基础常识 - 从原理到应用

2 阅读49分钟

章节概述

本章将深入讲解AI的核心算法和数学原理。从机器学习的基础概念,到深度学习的数学基石,从CNN的卷积运算,到Transformer的注意力机制,从优化算法到训练技巧,我们将全面剖析AI算法的每一个细节。

无论你是想深入理解AI原理的开发者,还是希望掌握技术细节的研究者,本章都将为你提供扎实的理论基础和实践指导。


4.1 机器学习核心概念地图

学习范式的三叉路口

机器学习的核心任务可以分为三大类,每类都有独特的问题设定和解决方法。

                  机器学习
                     │
        ┌────────────┼────────────┐
        │            │            │
     监督学习      无监督学习    强化学习
        │            │            │
   ┌────┴────┐   ┌───┴───┐    ┌──┴──┐
 分类    回归   聚类  降维   价值  策略

监督学习(Supervised Learning)

问题设定

给定:训练数据集 D = {(x₁,y₁), (x₂,y₂), ..., (xₙ,yₙ)}
- xᵢ:输入特征(如图像、文本)
- yᵢ:标签(如类别、数值)

目标:学习函数 f: X → Y
使得 f(x) ≈ y(对新数据也成立)

分类 vs 回归

分类(Classification):
- 输出是离散的类别
- 例:邮件是否为垃圾邮件(是/否)
- 例:图像中的物体(猫/狗/鸟...)

回归(Regression):
- 输出是连续的数值
- 例:房价预测(具体金额)
- 例:温度预测(具体度数)

监督学习的本质

找到一个函数 f,使得损失函数最小:
L(f) = Σᵢ loss(f(xᵢ), yᵢ)

常用损失函数:
分类:交叉熵损失
回归:均方误差(MSE)

经典算法进化链

线性回归 → 逻辑回归 → SVM → 决策树 → 随机森林 → XGBoost → 深度学习
  ↓          ↓        ↓       ↓         ↓          ↓           ↓
简单      概率模型  最大间隔  规则      集成      梯度提升    端到端
低偏差    高偏差    中等      低偏差    低方差    低偏差      自动特征
高方差    低方差    低方差    高方差              低方差      学习

无监督学习(Unsupervised Learning)

问题设定

给定:只有输入数据 D = {x₁, x₂, ..., xₙ}
没有标签!

目标:发现数据的内在结构

聚类(Clustering)

任务:将数据分组
使得:
- 组内数据相似
- 组间数据不同

经典算法:
K-Means:
1. 随机初始化k个中心点
2. 分配每个点到最近的中心
3. 重新计算中心点
4. 重复2-3直到收敛

层次聚类:
- 自底向上合并相似簇
- 或自顶向下分裂簇

DBSCAN:
- 基于密度的聚类
- 能发现任意形状的簇
- 能识别噪声点

降维(Dimensionality Reduction)

任务:将高维数据映射到低维
目的:
- 可视化(降到2D/3D)
- 去噪
- 加速计算
- 特征提取

经典算法:
PCA(主成分分析):
- 找到方差最大的方向
- 线性降维
- 快速、可解释

t-SNE:
- 非线性降维
- 保持局部结构
- 适合可视化

UMAP:
- 比t-SNE更快
- 保持全局和局部结构

自编码器(Autoencoder)

深度学习的无监督方法:

结构:
输入 → 编码器 → 低维表示 → 解码器 → 重构输出

训练目标:
最小化重构误差
L = ||x - decoder(encoder(x))||²

应用:
- 降维
- 去噪
- 特征学习
- 生成模型(VAE)

强化学习(Reinforcement Learning)

问题设定

智能体(Agent)与环境(Environment)交互:

每个时间步t:
1. 观察状态 sₜ
2. 采取动作 aₜ
3. 获得奖励 rₜ
4. 转移到新状态 sₜ₊₁

目标:
最大化累积奖励 Σᵗ γᵗ rₜ
(γ是折扣因子)

与监督学习的区别

监督学习:
- 有正确答案(标签)
- 立即反馈
- 独立样本

强化学习:
- 没有正确答案,只有奖励信号
- 延迟反馈(奖励可能在很久之后)
- 序列决策(当前动作影响未来)

经典算法

基于价值(Value-based):
- Q-Learning
- DQN(Deep Q-Network)
- 学习动作价值函数Q(s,a)

基于策略(Policy-based):
- REINFORCE
- Actor-Critic
- 直接学习策略π(a|s)

模型预测:
- AlphaGo:MCTS + 深度学习
- MuZero:学习环境模型

偏差-方差权衡(Bias-Variance Tradeoff)

这是机器学习最核心的概念之一。

数学定义

总误差分解

对于预测值ŷ = f(x)和真实值y:

期望误差 = 偏差² + 方差 + 不可约误差

E[(y - ŷ)²] = Bias[ŷ]² + Var[ŷ] + σ²

其中:
偏差 = E[ŷ] - y(期望预测与真值的差)
方差 = E[(ŷ - E[ŷ])²](预测的波动程度)
不可约误差 = σ²(数据本身的噪声)

直觉理解

想象射箭:
真值 = 靶心

高偏差(High Bias):
- 箭都偏离靶心
- 系统性错误
- 模型太简单(欠拟合)
- 例:用直线拟合曲线数据

高方差(High Variance):
- 箭散布很广
- 对训练数据过于敏感
- 模型太复杂(过拟合)
- 例:用100次多项式拟合10个点

理想情况:
- 低偏差 + 低方差
- 箭集中在靶心附近

权衡关系

模型复杂度增加:
偏差 ↓(能拟合更复杂的函数)
方差 ↑(对数据变化更敏感)

不能同时最小化偏差和方差
需要找到平衡点

实践中的体现

欠拟合(Underfitting)= 高偏差

症状:
- 训练误差高
- 测试误差高
- 两者接近

原因:
- 模型太简单
- 特征不够
- 正则化过强

解决:
- 增加模型复杂度
- 添加特征
- 减少正则化
- 训练更久

过拟合(Overfitting)= 高方差

症状:
- 训练误差很低
- 测试误差高
- 两者差距大

原因:
- 模型太复杂
- 训练数据太少
- 训练太久

解决:
- 增加训练数据
- 正则化(L1/L2/Dropout)
- 减少模型复杂度
- 早停(Early Stopping)
- 数据增强

不同模型的偏差-方差特性

线性模型(如线性回归):
偏差:高(假设数据是线性的)
方差:低(稳定)

决策树(深度不限):
偏差:低(能拟合任意复杂函数)
方差:高(对数据微小变化敏感)

随机森林:
偏差:低(树的集成)
方差:低(通过集成降低)

神经网络(大规模):
偏差:低(通用逼近器)
方差:中(Dropout等正则化)

正则化(Regularization)

正则化是控制模型复杂度、防止过拟合的关键技术。

L1正则化(Lasso)

定义

损失函数:
L = L_data(w) + λ·||w||₁

其中:
||w||₁ = Σᵢ |wᵢ|(权重的绝对值和)
λ:正则化系数

特点

1. 稀疏性(Sparsity):
   - 很多权重会变为0
   - 自动特征选择

2. 不可微(在0点):
   - 需要特殊优化算法(如ISTA)

3. 几何解释:
   - 优化在L1球(菱形)上进行
   - 容易触碰坐标轴(权重=0)

应用场景

- 特征选择
- 压缩模型
- 可解释性(只保留重要特征)

L2正则化(Ridge)

定义

损失函数:
L = L_data(w) + λ·||w||₂²

其中:
||w||₂² = Σᵢ wᵢ²(权重的平方和)

特点

1. 权重衰减(Weight Decay):
   - 权重趋向于小值
   - 但不会变为0

2. 可微:
   - 梯度更新简单
   - w ← w - α·(∂L/∂w + 2λw)

3. 几何解释:
   - 优化在L2球(圆形)上进行
   - 不易触碰坐标轴

在神经网络中

实现方式:
1. 在损失中添加正则项
2. 或在优化器中实现权重衰减

PyTorch示例:
optimizer = torch.optim.SGD(
    model.parameters(),
    lr=0.01,
    weight_decay=0.0001  # L2正则化
)

等价于:
loss_total = loss_data + 0.0001 * sum(p.pow(2).sum() for p in model.parameters())

为什么L2有效?

贝叶斯观点:
L2正则化 = 权重的高斯先验

P(w) ∝ exp(-||w||²/2σ²)

MAP估计(最大后验):
max P(w|D) = max P(D|w)P(w)
          = max [log P(D|w) + log P(w)]
          = min [L_data(w) + λ||w||²]

Elastic Net(L1 + L2)

结合两者优点:
L = L_data(w) + λ₁·||w||₁ + λ₂·||w||₂²

特点:
- 稀疏性(来自L1)
- 分组效应(来自L2,相关特征一起保留或删除)

Dropout(深度学习中的正则化)

原理

训练时:
- 随机丢弃神经元(概率p,通常0.5)
- 相当于训练指数级数量的子网络

测试时:
- 使用所有神经元
- 输出乘以(1-p)(或训练时除以(1-p))

为什么有效?

1. 集成效应:
   n个神经元 → 2ⁿ个子网络
   测试时近似所有子网络的平均

2. 防止共适应:
   神经元不能依赖特定的其他神经元
   被迫学习更鲁棒的特征

3. 添加噪声:
   训练时的随机性增强泛化

实践技巧

位置:
- 全连接层:通常0.5
- 卷积层:0.1-0.2或不用
- 输入层:0.1-0.2

与BatchNorm的关系:
- 现代网络:Conv → BN → ReLU(不用Dropout)
- Dropout主要用在全连接层

数据增强(Data Augmentation)

图像增强

# 几何变换
transforms.RandomCrop(224)          # 随机裁剪
transforms.RandomHorizontalFlip()   # 随机水平翻转
transforms.RandomRotation(15)       # 随机旋转±15度
transforms.RandomAffine(0, translate=(0.1, 0.1))  # 平移

# 颜色变换
transforms.ColorJitter(
    brightness=0.2,  # 亮度
    contrast=0.2,    # 对比度
    saturation=0.2,  # 饱和度
    hue=0.1          # 色调
)

# 高级增强
Mixup:线性插值两张图像
CutMix:剪切粘贴图像块
RandAugment:随机组合增强策略

文本增强

- 同义词替换
- 回译(翻译成其他语言再翻译回来)
- 随机插入/删除/交换词语
- EDA(Easy Data Augmentation)

作用

1. 增加有效训练数据
2. 提高模型鲁棒性
3. 防止过拟合
4. 相当于正则化

早停(Early Stopping)

方法

监控验证集误差:
1. 每个epoch后评估验证误差
2. 如果验证误差不再下降(连续n个epoch)
3. 停止训练,恢复最佳模型

伪代码:
best_val_loss = inf
patience = 10
counter = 0

for epoch in range(max_epochs):
    train(...)
    val_loss = evaluate(val_set)

    if val_loss < best_val_loss:
        best_val_loss = val_loss
        save_model()
        counter = 0
    else:
        counter += 1
        if counter >= patience:
            break  # 早停

load_best_model()

优点

- 简单有效
- 防止过拟合
- 节省训练时间

注意

- 需要独立的验证集
- patience需要调整
- 可能停得太早(验证误差波动)

4.2 深度学习的数学基石

反向传播:深度学习的发动机

反向传播(Backpropagation)是训练神经网络的核心算法。虽然概念简单(链式法则),但理解其细节对于调试和优化模型至关重要。

前向传播(Forward Propagation)

单层感知机

输入:x ∈ ℝᵈ
权重:W ∈ ℝᵈˣʰ, b ∈ ℝʰ

线性变换:
z = Wx + b

激活:
a = σ(z)

其中σ是激活函数(如ReLU、Sigmoid)

多层网络

对于L层网络:

层1:
z⁽¹⁾ = W⁽¹⁾x + b⁽¹⁾
a⁽¹⁾ = σ(z⁽¹⁾)

层2:
z⁽²⁾ = W⁽²⁾a⁽¹⁾ + b⁽²⁾
a⁽²⁾ = σ(z⁽²⁾)

...

层L(输出层):
z⁽ᴸ⁾ = W⁽ᴸ⁾a⁽ᴸ⁻¹⁾ + b⁽ᴸ⁾
ŷ = σ(z⁽ᴸ⁾)

损失:
L = loss(ŷ, y)

反向传播推导

目标

计算损失L对所有参数的梯度:
∂L/∂W⁽ˡ⁾, ∂L/∂b⁽ˡ⁾  (对所有层l)

链式法则(Chain Rule)

如果 y = f(u) 且 u = g(x)
那么 dy/dx = (dy/du) · (du/dx)

扩展到多层:
∂L/∂W⁽¹⁾ = (∂L/∂z⁽ᴸ⁾) · (∂z⁽ᴸ⁾/∂a⁽ᴸ⁻¹⁾) · ... · (∂a⁽¹⁾/∂z⁽¹⁾) · (∂z⁽¹⁾/∂W⁽¹⁾)

输出层梯度

设损失函数为L = (ŷ - y)²/2(均方误差)

∂L/∂z⁽ᴸ⁾ = ∂L/∂ŷ · ∂ŷ/∂z⁽ᴸ⁾
         = (ŷ - y) · σ'(z⁽ᴸ⁾)

定义:δ⁽ᴸ⁾ = ∂L/∂z⁽ᴸ⁾(输出层的"误差")

隐藏层梯度(反向传播)

对于层l:
δ⁽ˡ⁾ = ∂L/∂z⁽ˡ⁾

利用链式法则:
δ⁽ˡ⁾ = ∂L/∂z⁽ˡ⁺¹⁾ · ∂z⁽ˡ⁺¹⁾/∂a⁽ˡ⁾ · ∂a⁽ˡ⁾/∂z⁽ˡ⁾
     = δ⁽ˡ⁺¹⁾ · W⁽ˡ⁺¹⁾ᵀ · σ'(z⁽ˡ⁾)

这就是"反向传播"!
从输出层的δ⁽ᴸ⁾开始,逐层向前传播

参数梯度

权重W⁽ˡ⁾的梯度:
∂L/∂W⁽ˡ⁾ = ∂L/∂z⁽ˡ⁾ · ∂z⁽ˡ⁾/∂W⁽ˡ⁾
          = δ⁽ˡ⁾ · (a⁽ˡ⁻¹⁾)ᵀ

偏置b⁽ˡ⁾的梯度:
∂L/∂b⁽ˡ⁾ = δ⁽ˡ⁾

反向传播算法总结

前向传播:
for l = 1 to L:
    z⁽ˡ⁾ = W⁽ˡ⁾a⁽ˡ⁻¹⁾ + b⁽ˡ⁾
    a⁽ˡ⁾ = σ(z⁽ˡ⁾)
L = loss(a⁽ᴸ⁾, y)

反向传播:
δ⁽ᴸ⁾ = ∂L/∂a⁽ᴸ⁾ ⊙ σ'(z⁽ᴸ⁾)

for l = L-1 to 1:
    δ⁽ˡ⁾ = (W⁽ˡ⁺¹⁾ᵀδ⁽ˡ⁺¹⁾) ⊙ σ'(z⁽ˡ⁾)

参数梯度:
for l = 1 to L:
    ∂L/∂W⁽ˡ⁾ = δ⁽ˡ⁾(a⁽ˡ⁻¹⁾)ᵀ
    ∂L/∂b⁽ˡ⁾ = δ⁽ˡ⁾

参数更新:
for l = 1 to L:
    W⁽ˡ⁾ ← W⁽ˡ⁾ - α·∂L/∂W⁽ˡ⁾
    b⁽ˡ⁾ ← b⁽ˡ⁾ - α·∂L/∂b⁽ˡ⁾

计算图视角

计算图(Computation Graph)

前向传播 = 构建计算图
反向传播 = 在图上传播梯度

例子:y = (x + 2) * 3

计算图:
x → [+2] → a → [×3] → y
    ↑           ↑
    2           3

前向:
a = x + 2
y = a * 3

反向(假设∂L/∂y = 1):
∂L/∂a = ∂L/∂y · ∂y/∂a = 1 · 3 = 3
∂L/∂x = ∂L/∂a · ∂a/∂x = 3 · 1 = 3

现代深度学习框架

自动微分(Automatic Differentiation):
- PyTorch、TensorFlow自动构建计算图
- 自动计算梯度
- 用户只需定义前向传播

PyTorch示例:
x = torch.tensor([2.0], requires_grad=True)
y = (x + 2) * 3
y.backward()  # 自动计算梯度
print(x.grad)  # 输出:tensor([3.])

梯度消失与梯度爆炸

梯度消失(Vanishing Gradient)

问题

Sigmoid激活函数:
σ(z) = 1/(1 + e⁻ᶻ)
σ'(z) = σ(z)(1 - σ(z)) ≤ 0.25

反向传播:
δ⁽ˡ⁾ = (W⁽ˡ⁺¹⁾ᵀδ⁽ˡ⁺¹⁾) ⊙ σ'(z⁽ˡ⁾)
     ≤ ||W⁽ˡ⁺¹⁾|| · ||δ⁽ˡ⁺¹⁾|| · 0.25

10层网络:
||δ⁽¹⁾|| ≤ ||δ⁽¹⁰⁾|| · ∏ᵢ ||W⁽ⁱ⁾|| · 0.25⁹
        ≈ ||δ⁽¹⁰⁾|| · 4×10⁻⁶  (假设||W||≈1)

前几层梯度几乎为0

后果

- 前几层学不到东西
- 网络深度受限
- 训练缓慢

解决方案

1. ReLU激活函数:
   ReLU'(x) = 1 (x>0)
   不会衰减梯度

2. 残差连接(ResNet):
   y = F(x) + x
   梯度可以直接传播

3. BatchNorm:
   归一化激活值,保持梯度

4. LSTM(针对RNN):
   门控机制,选择性传播梯度

5. 更好的初始化:
   Xavier、He初始化

梯度爆炸(Exploding Gradient)

问题

如果权重W的特征值>1:
梯度可能指数增长

10层网络,||W||=2:
||δ⁽¹⁾|| ≈ ||δ⁽¹⁰⁾|| · 2⁹ = 512·||δ⁽¹⁰⁾||

后果

- 权重更新过大
- 数值溢出(NaN)
- 训练不稳定

解决方案

1. 梯度裁剪(Gradient Clipping):
   if ||g|| > threshold:
       g = g / ||g|| * threshold

2. 权重正则化:
   L2正则化限制权重大小

3. BatchNorm:
   归一化激活值

4. 更小的学习率

优化算法的演进

神经网络训练的核心是优化:找到最小化损失函数的参数。

梯度下降(Gradient Descent)

批量梯度下降(Batch GD)

# 计算整个数据集的梯度
for epoch in range(num_epochs):
    grad = 0
    for (x, y) in dataset:
        grad += compute_gradient(x, y, w)

    grad = grad / len(dataset)
    w = w - learning_rate * grad

优点

- 梯度准确(整个数据集)
- 收敛稳定

缺点

- 计算慢(每次更新需要遍历全部数据)
- 内存消耗大
- 容易卡在局部最优

随机梯度下降(SGD)

算法

for epoch in range(num_epochs):
    shuffle(dataset)  # 打乱数据
    for (x, y) in dataset:
        grad = compute_gradient(x, y, w)
        w = w - learning_rate * grad

优点

- 更新频繁,收敛快
- 内存友好
- 噪声有助于跳出局部最优

缺点

- 梯度估计有噪声
- 收敛不稳定(震荡)

小批量梯度下降(Mini-batch GD)

折中方案

batch_size = 32  # 常用:32, 64, 128, 256

for epoch in range(num_epochs):
    shuffle(dataset)
    for batch in get_batches(dataset, batch_size):
        grad = 0
        for (x, y) in batch:
            grad += compute_gradient(x, y, w)

        grad = grad / batch_size
        w = w - learning_rate * grad

优点

- 平衡速度和稳定性
- 利用向量化计算(GPU加速)
- 梯度估计较准确

批次大小的选择

小批次(16-32):
- 噪声大,正则化效果
- 泛化能力好
- 收敛快(更新频繁)

大批次(256-1024):
- 噪声小,稳定
- 需要更大学习率
- 硬件利用率高

实践:
- 通常从32或64开始
- 根据GPU内存调整
- 大模型:可能用到数千或数万

动量(Momentum)

动机

SGD问题:
- 在平坦方向震荡
- 在陡峭方向缓慢前进

想法:
像物理中的惯性
累积过去梯度的方向

算法

v = 0  # 速度
beta = 0.9  # 动量系数

for iteration:
    grad = compute_gradient(w)
    v = beta * v + (1 - beta) * grad
    w = w - learning_rate * v

或者(Polyak's momentum)

v = 0
beta = 0.9

for iteration:
    grad = compute_gradient(w)
    v = beta * v + grad
    w = w - learning_rate * v

效果

在一致的方向上加速
在震荡的方向上减速

可视化:
无动量:锯齿形路径
有动量:更平滑的路径

Nesterov加速梯度(NAG)

改进:先预测一步,再计算梯度

v = beta * v + grad(w - learning_rate * beta * v)
w = w - learning_rate * v

效果:更智能的"预测"

AdaGrad(自适应梯度)

动机

不同参数应该有不同的学习率:
- 稀疏特征:学习率大
- 频繁特征:学习率小

算法

G = 0  # 梯度平方的累积
eps = 1e-8  # 防止除以0

for iteration:
    grad = compute_gradient(w)
    G = G + grad ** 2
    w = w - learning_rate * grad / (sqrt(G) + eps)

特点

自适应学习率:
lr_effective = lr / sqrt(G + eps)

好处:
- 稀疏参数(G小)→ 学习率大
- 频繁参数(G大)→ 学习率小

问题:
- G单调递增
- 学习率不断下降
- 可能过早停止学习

RMSprop

改进AdaGrad

不累积所有历史梯度
用指数移动平均(EMA)

算法

v = 0
beta = 0.9
eps = 1e-8

for iteration:
    grad = compute_gradient(w)
    v = beta * v + (1 - beta) * grad ** 2
    w = w - learning_rate * grad / (sqrt(v) + eps)

优点

- v不会无限增长
- 学习率不会过早衰减
- 适合非平稳目标

Adam(自适应矩估计)

结合Momentum + RMSprop

算法

m = 0  # 一阶矩估计(均值)
v = 0  # 二阶矩估计(未中心化的方差)
beta1 = 0.9
beta2 = 0.999
eps = 1e-8
t = 0  # 时间步

for iteration:
    t += 1
    grad = compute_gradient(w)

    # 更新矩估计
    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)

    # 更新参数
    w = w - learning_rate * m_hat / (sqrt(v_hat) + eps)

为什么需要偏差修正?

初始时m=0, v=0
前几步:
m ≈ 0(偏向0)
v ≈ 0(偏向0)

偏差修正:
m_hat = m / (1 - beta1^t)
当t→∞时,(1 - beta1^t) → 1,修正消失
当t=1时,(1 - beta1^1) = 0.1,放大10倍

效果:前几步学习率更大

超参数默认值

learning_rate = 0.001(或0.0001beta1 = 0.9
beta2 = 0.999
eps = 1e-8

这些默认值在大多数情况下都很好

为什么Adam如此流行?

1. 鲁棒性强:
   默认参数通常就很好

2. 收敛快:
   结合了Momentum和RMSprop的优点

3. 内存效率:
   只需额外存储m和v(与参数同维度)

4. 适应性强:
   每个参数独立调整学习率

5. 适用广泛:
   几乎所有深度学习任务

Adam的变体

AdamW(Adam with Weight Decay)

修正Adam中L2正则化的实现

标准Adam + L2:
grad = grad + lambda * w
(错误:L2项也被自适应缩放)

AdamW:
grad_from_loss = compute_gradient(w)
m = beta1 * m + (1 - beta1) * grad_from_loss
v = beta2 * v + (1 - beta2) * grad_from_loss ** 2
w = w - lr * (m_hat / (sqrt(v_hat) + eps) + lambda * w)
(正确:L2项独立于自适应缩放)

效果:更好的泛化

AMSGrad

解决Adam可能不收敛的问题

修改:使用v的最大值
v_max = max(v_max, v)
w = w - lr * m_hat / (sqrt(v_max) + eps)

RAdam(Rectified Adam)

改进Adam的warmup

自动调整前几步的学习率
不需要手动warmup

优化器选择指南

默认选择:Adam或AdamW
- 适用范围广
- 默认参数通常有效
- 适合大多数任务

训练Transformer:AdamW
- 几乎是标准配置
- beta1=0.9, beta2=0.98或0.999

计算机视觉:SGD + Momentum
- 有时泛化更好
- 需要仔细调学习率
- 配合学习率调度

小数据集:Adam
- 收敛快
- 对超参数不敏感

大批次训练:LARS或LAMB
- 专门为大批次设计

4.3 CNN:视觉智能的基石

卷积神经网络(CNN)是计算机视觉的基础架构。理解卷积运算的原理对于设计和调试视觉模型至关重要。

卷积的数学本质

一维卷积

连续信号

(f * g)(t) = ∫_{-∞}^{∞} f(τ)g(t-τ) dτ

离散信号

(f * g)[n] = Σ_{m=-∞}^{∞} f[m]g[n-m]

直觉理解

卷积 = 加权平均
- 滤波器g在信号f上滑动
- 每个位置计算加权和

例子(平滑滤波器)

信号:f = [1, 2, 3, 4, 5]
滤波器:g = [1/3, 1/3, 1/3]3点平均)

卷积结果:
(f * g)[0] = 无定义(需要f[-1])
(f * g)[1] = (1·1/3 + 2·1/3 + 3·1/3) = 2
(f * g)[2] = (2·1/3 + 3·1/3 + 4·1/3) = 3
(f * g)[3] = (3·1/3 + 4·1/3 + 5·1/3) = 4
...

效果:平滑了信号

二维卷积(图像)

定义

(I * K)(i, j) = ΣΣ I(i-m, j-n) K(m, n)
                m n

其中:
- I:输入图像
- K:卷积核(kernel/filter)
- (m,n):卷积核内的坐标

互相关 vs 卷积

严格的卷积:K需要翻转
互相关:K不翻转

深度学习中:
通常说"卷积",实际是"互相关"
因为K是学习的,翻不翻转无所谓

互相关:
(I ⊗ K)(i, j) = ΣΣ I(i+m, j+n) K(m, n)
                  m n

例子(边缘检测)

输入图像(5×5):
0 0 0 1 1
0 0 0 1 1
0 0 0 1 1
0 0 0 1 1
0 0 0 1 1

垂直边缘检测核(3×3):
-1  0  1
-1  0  1
-1  0  1

卷积结果(3×3,valid padding):
位置(1,1):
(-10 + 0·0 + 1·0 +
(-10 + 0·0 + 1·0 +
(-10 + 0·0 + 1·0 = 0

位置(1,2):
(-10 + 0·0 + 1·1 +
(-10 + 0·0 + 1·1 +
(-10 + 0·0 + 1·1 = 3

完整结果:
0  3  3
0  3  3
0  3  3

解释:
在垂直边缘处有强响应(3)
其他地方响应弱(0

经典卷积核

Sobel边缘检测

水平边缘:
-1 -2 -1
 0  0  0
 1  2  1

垂直边缘:
-1  0  1
-2  0  2
-1  0  1

高斯模糊

1/16 * [1  2  1]
       [2  4  2]
       [1  2  1]

效果:平滑图像,去噪

锐化

 0 -1  0
-1  5 -1
 0 -1  0

效果:增强边缘

CNN的核心组件

卷积层

参数

输入:H×W×C_in(高×宽×通道数)
卷积核:K×K×C_in×C_out
- K:核大小(如3×3)
- C_in:输入通道数
- C_out:输出通道数(卷积核数量)

偏置:C_out

计算

对于每个输出通道c_out:
    对于每个空间位置(i, j):
        输出[i,j,c_out] = Σ(输入[i:i+K, j:j+K, :] * 核[:,:,:,c_out]) + 偏置[c_out]

每个输出通道使用不同的卷积核
每个卷积核在所有输入通道上操作

输出尺寸计算

输出高度 = (H + 2P - K) / S + 1
输出宽度 = (W + 2P - K) / S + 1

其中:
- P:padding(填充)
- S:stride(步长)
- K:核大小

Padding(填充)

Same padding(保持尺寸)

输出尺寸 = 输入尺寸

需要的padding:
P = (K - 1) / 2  (假设S=1)

例:K=3 → P=1
    K=5 → P=2

Valid padding(无填充)

P = 0
输出尺寸 = H - K + 1

Stride(步长)

S=1:卷积核每次移动1个像素(标准)
S=2:卷积核每次移动2个像素(下采样)

S=2的效果:
- 输出尺寸减半
- 替代池化层进行下采样

参数量和计算量

参数量

参数 = K × K × C_in × C_out + C_out
     = K² × C_in × C_out + C_out

例:
K=3, C_in=64, C_out=128
参数 = 9 × 64 × 128 + 128 = 73,856

计算量(FLOPs)

FLOPs = K² × C_in × C_out × H_out × W_out

例:
K=3, C_in=64, C_out=128, H_out=W_out=32
FLOPs = 9 × 64 × 128 × 32 × 322.4亿

1×1卷积的妙用

作用

1. 降维/升维

输入:56×56×256
1×1卷积(64个)
输出:56×56×64

参数:256 × 64 = 16,384(远小于3×3卷积的147,456

2. 增加非线性

1×1 Conv → ReLU
在不改变空间尺寸的情况下增加网络深度

3. 跨通道信息融合

1×1卷积在通道维度上做全连接
可以学习通道间的关系

应用

- GoogLeNet的Inception模块(降维)
- ResNet的瓶颈结构
- MobileNet的深度可分离卷积

池化层(Pooling)

最大池化(Max Pooling)

输入:4×4
池化核:2×2,stride=2

[1  2  | 3  4]       [6  8]
[5  6  | 7  8][------|------]      [14 16]
[9  10 | 11 12]
[13 14 | 15 16]

每个2×2区域取最大值

平均池化(Average Pooling)

每个区域取平均值

[1  2  | 3  4]       [3.5  5.5]
[5  6  | 7  8][------|------]      [11.5 13.5]
[9  10 | 11 12]
[13 14 | 15 16]

全局池化(Global Pooling)

输入:H×W×C
输出:1×1×C

对每个通道,整个空间尺寸池化为1个值

全局平均池化:
输出[c] = mean(输入[:,:,c])

用途:
- 替代全连接层
- 减少参数(ResNet、GoogLeNet使用)

池化的作用

1. 降低维度:
   减少计算量和参数

2. 增加感受野:
   池化后,每个神经元"看到"的区域更大

3. 平移不变性:
   小的平移不影响池化结果

4. 特征选择:
   最大池化保留最强响应

池化 vs 步长卷积

趋势:用步长卷积替代池化

优点:
- 可学习的下采样
- 不丢失信息(池化是不可逆的)

缺点:
- 参数增加

批归一化(Batch Normalization)

动机

内部协变量转移(Internal Covariate Shift):
- 每层输入的分布在训练中不断变化
- 导致训练不稳定、速度慢

算法

# 对每个batch归一化
def batch_norm(x, gamma, beta, eps=1e-5):
    # x: (N, C, H, W)

    # 计算均值和方差(对N, H, W维度)
    mu = x.mean(dim=(0, 2, 3), keepdim=True)  # (1, C, 1, 1)
    var = x.var(dim=(0, 2, 3), keepdim=True)  # (1, C, 1, 1)

    # 归一化
    x_norm = (x - mu) / sqrt(var + eps)

    # 缩放和平移(可学习参数)
    out = gamma * x_norm + beta

    return out

可学习参数

γ(gamma):缩放参数,初始化为1
β(beta):平移参数,初始化为0

为什么需要?
- 归一化后分布固定(均值0,方差1)
- γ和β恢复网络的表达能力
- 如果网络需要,可以学习还原原始分布

训练 vs 推理

训练时:
- 使用当前batch的均值和方差

推理时:
- 使用训练时的移动平均统计量
- running_mean = momentum * running_mean + (1 - momentum) * batch_mean
- running_var = momentum * running_var + (1 - momentum) * batch_var

BatchNorm的位置

常见顺序:
Conv → BatchNorm → ReLU

或:
Conv → ReLU → BatchNorm

经验:
第一种更常用(ResNet等)

效果

1. 加速训练:
   - 可以用更大的学习率
   - 收敛速度快2-10倍

2. 正则化作用:
   - batch内的噪声提供正则化
   - 可以减少Dropout

3. 对初始化不敏感:
   - 即使初始化不好,BN也能稳定训练

4. 允许更深的网络:
   - 缓解梯度消失/爆炸

BatchNorm的问题

1. 依赖batch size:
   - batch太小(如1-2)性能下降
   - 对比学习等任务困难

2. 训练/推理不一致:
   - 训练用batch统计
   - 推理用全局统计

替代方案:
- Layer Normalization(Transformer常用)
- Group Normalization
- Instance Normalization(风格迁移)

经典CNN架构演进

LeNet-5(1998)

结构

输入:32×32灰度图像
→ Conv5×5(6) → AvgPool → Conv5×5(16) → AvgPool
→ FC(120) → FC(84) → FC(10)

参数:约6

创新

- 首次成功应用CNN
- 卷积 + 池化的模式
- 用于手写数字识别

局限

- 很浅(只有2个卷积层)
- 激活函数:tanh(不是ReLU)
- 硬件限制(当时)

AlexNet(2012)

结构

输入:224×224×3
→ Conv11×11(96, stride=4) → ReLU → MaxPool → LRN
→ Conv5×5(256) → ReLU → MaxPool → LRN
→ Conv3×3(384) → ReLU
→ Conv3×3(384) → ReLU
→ Conv3×3(256) → ReLU → MaxPool
→ FC(4096) → Dropout → FC(4096) → Dropout → FC(1000)

参数:6000

创新

1. ReLU激活函数(首次大规模使用)
2. Dropout防止过拟合
3. 数据增强
4. GPU训练
5. LRN(后来被BatchNorm取代)

影响

- ImageNet 2012冠军
- 开启深度学习时代

VGGNet(2014)

哲学

更深 + 更简单
只用3×3卷积核

VGG-16结构

输入:224×224×3

Block 1:Conv3×3(64) × 2 → MaxPool
Block 2:Conv3×3(128) × 2 → MaxPool
Block 3:Conv3×3(256) × 3 → MaxPool
Block 4:Conv3×3(512) × 3 → MaxPool
Block 5:Conv3×3(512) × 3 → MaxPool

FC(4096) × 2FC(1000)

总层数:13个卷积层 + 3个全连接层 = 16层
参数:1.38亿

为什么3×3?

2个3×3 = 15×5的感受野
3个3×3 = 17×7的感受野

但参数更少:
3个3×3:3×(9C²) = 27C²
1个7×7:49C²

节省:45%参数
更多非线性:3个ReLU vs 1个ReLU

优点

- 结构简单统一
- 容易实现
- 迁移学习效果好

缺点

- 参数量巨大(90%在FC层)
- 训练慢
- 内存占用高

GoogLeNet / Inception v1(2014)

Inception模块

                输入
    ┌───────┬───────┬───────┬───────┐
    │       │       │       │       │
  1×1     1×1     1×1      3×3
 Conv    Conv    Conv    MaxPool
  │       │       │       │
  │      3×3     5×5     1×1
  │     Conv    Conv    Conv
  │       │       │       │
  └───────┴───────┴───────┴───────┘
              拼接(Concat)
                输出

多尺度特征并行捕获

网络结构

9个Inception模块 + 辅助分类器
22层深度
700万参数(比VGG少20倍)

1×1卷积降维

输入:28×28×192

不降维:
5×5 Conv(32) → 参数:5×5×192×32 = 153,600

降维:
1×1 Conv(16) → 5×5 Conv(32)
→ 参数:1×1×192×16 + 5×5×16×32 = 15,872

节省:90%

贡献

- Inception模块(多尺度)
- 1×1卷积降维
- 全局平均池化(替代FC)
- 辅助分类器(缓解梯度消失)

ResNet(2015)

残差块(Residual Block)

      输入x
        │
    ┌───┴───┐
    │       │
    │     Conv
    │       │
    │     ReLU
    │       │
    │     Conv
    │       │
    │      F(x)
    │       │
    └───┬───┘
        +  ← 加法(element-wise)
        │
       ReLU
        │
      输出

输出 = F(x) + x

为什么有效?

1. 恒等映射容易学习:
   如果最优解是恒等函数
   ResNet:学习F(x)=0即可(容易)
   普通网络:学习H(x)=x(难)

2. 梯度高速公路:
   ∂L/∂x = ∂L/∂output · (∂F(x)/∂x + 1)
   有常数项1,梯度可以直接传播

3. 集成效应:
   n个残差块 → 2ⁿ条路径
   类似网络集成

ResNet-50结构

输入:224×224×3

Conv1:7×7 Conv(64) → MaxPool

Conv2_x(56×56):
[1×1 Conv(64)3×3 Conv(64)1×1 Conv(256)] × 3

Conv3_x(28×28):
[1×1 Conv(128)3×3 Conv(128)1×1 Conv(512)] × 4

Conv4_x(14×14):
[1×1 Conv(256)3×3 Conv(256)1×1 Conv(1024)] × 6

Conv5_x(7×7):
[1×1 Conv(512)3×3 Conv(512)1×1 Conv(2048)] × 3

全局平均池化 → FC(1000)

总层数:50层
参数:2500

瓶颈结构(Bottleneck)

标准残差块(ResNet-34及以下):
3×3 Conv → 3×3 Conv

瓶颈结构(ResNet-50及以上):
1×1 Conv(降维)→ 3×3 Conv → 1×1 Conv(升维)

例:
256通道 → 64通道 → 64通道 → 256通道

参数对比:
标准:3×3×256×256 × 2 = 1,179,648
瓶颈:1×1×256×64 + 3×3×64×64 + 1×1×64×256 = 69,632

节省:94%

维度匹配

问题:x和F(x)的通道数不同

解决方案:

方案A:零填充
x' = zero_pad(x)

方案B:投影(1×1卷积)
x' = 1×1 Conv(x)

实践:
- 通道数不变:恒等映射
- 通道数改变:1×1卷积投影

成就

- ImageNet 2015冠军
- Top-5错误率:3.57%(超过人类5%)
- 训练152层甚至1000层网络
- 成为标准骨干网络

4.4 RNN与序列建模

循环神经网络(RNN)是处理序列数据的经典架构。虽然现在Transformer更流行,但理解RNN仍然重要。

RNN的记忆机制

基本RNN

结构

在每个时间步t:
隐藏状态:h_t = tanh(W_hh · h_{t-1} + W_xh · x_t + b_h)
输出:y_t = W_hy · h_t + b_y

其中:
- x_t:时间步t的输入
- h_t:时间步t的隐藏状态("记忆")
- y_t:时间步t的输出

展开视角

t=1: x_1 → [RNN] → h_1 → y_1
              ↑
             h_0

t=2: x_2 → [RNN] → h_2 → y_2
              ↑
             h_1

t=3: x_3 → [RNN] → h_3 → y_3
              ↑
             h_2

每个时间步共享参数W_hh, W_xh, W_hy

前向传播

def rnn_forward(x_sequence, h0, Whh, Wxh, Why, bh, by):
    """
    x_sequence: (T, D) - T个时间步,每步D维
    h0: (H,) - 初始隐藏状态
    """
    T = len(x_sequence)
    H = len(h0)

    h = h0
    h_sequence = []
    y_sequence = []

    for t in range(T):
        # 更新隐藏状态
        h = np.tanh(Whh @ h + Wxh @ x_sequence[t] + bh)
        h_sequence.append(h)

        # 计算输出
        y = Why @ h + by
        y_sequence.append(y)

    return y_sequence, h_sequence

RNN的应用模式

1. 一对一(标准神经网络)

输入:单个向量
输出:单个向量

例:图像分类

2. 一对多

输入:单个向量
输出:序列

例:图像描述生成
图像 → RNN → "a", "dog", "is", "running"

3. 多对一

输入:序列
输出:单个向量

例:情感分析
"I love this movie" → RNN → 正面

4. 多对多(同步)

输入序列和输出序列长度相同
每个时间步都有输出

例:视频分类(每帧标注)

5. 多对多(异步)

输入序列和输出序列长度不同
先编码,再解码

例:机器翻译
"Hello" → RNN(编码) → RNN(解码) → "Bonjour"

RNN的训练:BPTT

时间反向传播(Backpropagation Through Time)

问题

RNN在时间上展开
如何计算梯度?

答案

把展开的RNN看作前馈网络
应用标准反向传播

梯度计算

损失:L = Σ_t L_t(每个时间步的损失和)

对于输出层:
∂L/∂y_t = ∂L_t/∂y_t

对于隐藏状态:
∂L/∂h_T = ∂L/∂y_T · ∂y_T/∂h_T

∂L/∂h_t = ∂L/∂y_t · ∂y_t/∂h_t + ∂L/∂h_{t+1} · ∂h_{t+1}/∂h_t

反向传播梯度:
从t=T到t=1

梯度消失/爆炸

数学分析

h_t/∂h_{t-1} = ∂(tanh(W_hh·h_{t-1} + W_xh·x_t))/∂h_{t-1}
               = diag(tanh'(...)) · W_hh

∂h_T/∂h_1 = ∂h_T/∂h_{T-1} · ∂h_{T-1}/∂h_{T-2} · ... · ∂h_2/∂h_1
          = ∏_{t=2}^T (diag(tanh'(...)) · W_hh)

如果:
- ||W_hh|| > 1tanh'1 → 梯度爆炸
- ||W_hh|| < 1tanh'0 → 梯度消失

后果

梯度消失:
- 长距离依赖学不到
- RNN实际只能记住10-20步

梯度爆炸:
- 梯度过大
- 权重更新不稳定
- 训练发散

缓解方法

梯度裁剪(针对梯度爆炸):
if ||grad|| > threshold:
    grad = grad / ||grad|| * threshold

LSTM/GRU(针对梯度消失):
门控机制,选择性传播梯度

LSTM:长短期记忆网络

动机

RNN问题:
- 梯度消失 → 长期依赖学不到
- 所有信息混在一起 → 无法选择性记忆

LSTM解决:
- 细胞状态(cell state):信息高速公路
- 门控机制:控制信息流动

LSTM结构

三个门

1. 遗忘门(Forget Gate)f_t:
   决定丢弃多少旧信息

2. 输入门(Input Gate)i_t:
   决定添加多少新信息

3. 输出门(Output Gate)o_t:
   决定输出多少信息

数学表达

遗忘门:
f_t = σ(W_f · [h_{t-1}, x_t] + b_f)

输入门:
i_t = σ(W_i · [h_{t-1}, x_t] + b_i)
C̃_t = tanh(W_C · [h_{t-1}, x_t] + b_C)(候选值)

细胞状态更新:
C_t = f_t ⊙ C_{t-1} + i_t ⊙ C̃_t
      丢弃旧信息    添加新信息

输出门:
o_t = σ(W_o · [h_{t-1}, x_t] + b_o)
h_t = o_ttanh(C_t)

其中:
- σ:sigmoid函数(输出0-1)
- ⊙:逐元素乘法
- [h_{t-1}, x_t]:拼接

直觉理解

遗忘门f_t

作用:决定保留多少C_{t-1}

f_t1:完全保留旧记忆
f_t0:完全丢弃旧记忆

例:
句子"我喜欢吃苹果,它很甜"
处理"它"时:
- 如果需要记住"苹果"f_t1
- 如果"苹果"不重要,f_t0

输入门i_t

作用:决定添加多少新信息

i_t1:完全添加C̃_t
i_t0:不添加新信息

与遗忘门配合:
- 遗忘旧信息(f_t)
- 添加新信息(i_t

输出门o_t

作用:决定输出多少C_t

o_t1:完全输出
o_t0:几乎不输出

例:
某些时刻只需要记住信息(C_t更新)
但不需要输出(o_t0

LSTM为什么能解决梯度消失?

细胞状态的梯度流

C_t = f_t ⊙ C_{t-1} + i_t ⊙ C̃_t

∂C_t/∂C_{t-1} = f_t(逐元素)

∂C_T/∂C_1 = ∏_{t=2}^T f_t

关键:
- f_t由网络学习
- 如果需要长期记忆,网络会学习f_t1
- 梯度不会消失!

对比RNN:
∂h_t/∂h_{t-1} = tanh'(...) · W_hh
tanh'1,无法控制

信息高速公路

细胞状态C:
- 只经过简单的线性操作(加法、逐元素乘法)
- 信息可以几乎无损地流动

对比:
- RNN的h:每步都经过tanh
- 信息被非线性变换多次,容易丢失

GRU:门控循环单元

简化的LSTM

只有两个门:
- 重置门(Reset Gate)r_t
- 更新门(Update Gate)z_t

数学表达

重置门:
r_t = σ(W_r · [h_{t-1}, x_t])

更新门:
z_t = σ(W_z · [h_{t-1}, x_t])

候选隐藏状态:
h̃_t = tanh(W · [r_t ⊙ h_{t-1}, x_t])

最终隐藏状态:
h_t = (1 - z_t) ⊙ h_{t-1} + z_t ⊙ h̃_t
      保留旧信息          添加新信息

与LSTM对比

LSTM:
- 3个门(遗忘、输入、输出)
- 细胞状态C和隐藏状态h分离
- 参数更多

GRU:
- 2个门(重置、更新)
- 只有隐藏状态h
- 参数更少,训练更快

性能:
- 大多数任务上相近
- GRU稍快,LSTM稍灵活

双向RNN(Bidirectional RNN)

动机

单向RNN:只看过去
例:处理"apple"时,只看到"I love"

双向RNN:同时看过去和未来
处理"apple"时,看到"I love""very much"

结构

前向RNN:
→h_1 →h_2 →h_3 →h_4 →h_5

后向RNN:
←h_1 ←h_2 ←h_3 ←h_4 ←h_5

每个时间步t的表示:
h_t = [→h_t; ←h_t](拼接)

应用

- BERT(Transformer的双向)
- 语音识别
- 命名实体识别
- 机器翻译(编码器)

不适用:
- 实时任务(无法等到未来)
- 语言模型(作弊!)

4.5 Transformer:注意力的数学美学

Transformer已经在第三章详细介绍,这里补充一些数学细节和实现技巧。

自注意力的矩阵形式详解

输入

X ∈ ℝ^{n×d_model}
n:序列长度
d_model:模型维度(如512)

投影矩阵

W_Q ∈ ℝ^{d_model × d_k}
W_K ∈ ℝ^{d_model × d_k}
W_V ∈ ℝ^{d_model × d_v}

通常 d_k = d_v = d_model / h
h:注意力头数

计算步骤

1. 线性投影

Q = XW_Q  ∈ ℝ^{n×d_k}
K = XW_K  ∈ ℝ^{n×d_k}
V = XW_V  ∈ ℝ^{n×d_v}

2. 计算注意力分数

S = QK^T ∈ ℝ^{n×n}

S[i,j] = Q[i] · K[j]
       = 位置i对位置j的"查询-键"匹配分数

3. 缩放

S = S / √d_k

为什么?
避免softmax饱和(详见第三章)

4. Softmax归一化

A = softmax(S)  ∈ ℝ^{n×n}

A[i,j] = exp(S[i,j]) / Σ_k exp(S[i,k])

每行是一个概率分布(和为1A[i,j]:位置i对位置j的注意力权重

5. 加权求和

output = AV  ∈ ℝ^{n×d_v}

output[i] = Σ_j A[i,j] · V[j]

位置i的输出是所有V[j]的加权平均
权重由注意力A[i,j]决定

完整公式

Attention(Q,K,V) = softmax(QK^T / √d_k)V

掩码(Masking)的实现

Padding Mask

def create_padding_mask(seq):
    """
    seq: (batch, seq_len)
    返回: (batch, 1, 1, seq_len)
    """
    # seq中padding位置为0
    mask = (seq == 0).float()

    # 扩展维度以匹配注意力矩阵
    mask = mask.unsqueeze(1).unsqueeze(2)

    # 将padding位置设为-∞(softmax后为0)
    mask = mask * -1e9

    return mask

# 使用
scores = scores + padding_mask
attention = softmax(scores)

Look-ahead Mask(Causal Mask)

def create_look_ahead_mask(size):
    """
    size: 序列长度
    返回: (size, size)
    """
    # 上三角矩阵(对角线以上为1)
    mask = torch.triu(torch.ones(size, size), diagonal=1)

    # 1的位置设为-∞
    mask = mask.masked_fill(mask == 1, float('-inf'))

    return mask

# 使用
look_ahead_mask = create_look_ahead_mask(seq_len)
scores = scores + look_ahead_mask
attention = softmax(scores)

组合Mask

def create_masks(src, tgt):
    """
    src: 源序列(编码器输入)
    tgt: 目标序列(解码器输入)
    """
    # 编码器padding mask
    src_mask = create_padding_mask(src)

    # 解码器padding mask
    tgt_padding_mask = create_padding_mask(tgt)

    # 解码器look-ahead mask
    tgt_len = tgt.size(1)
    tgt_look_ahead_mask = create_look_ahead_mask(tgt_len)

    # 组合(取最大值,即最严格的mask)
    tgt_mask = torch.maximum(tgt_padding_mask, tgt_look_ahead_mask)

    return src_mask, tgt_mask

位置编码的数学

正弦/余弦编码详解

def get_positional_encoding(max_len, d_model):
    """
    max_len: 最大序列长度
    d_model: 模型维度
    """
    PE = torch.zeros(max_len, d_model)

    position = torch.arange(0, max_len).unsqueeze(1).float()
    div_term = torch.exp(
        torch.arange(0, d_model, 2).float() *
        -(np.log(10000.0) / d_model)
    )

    PE[:, 0::2] = torch.sin(position * div_term)
    PE[:, 1::2] = torch.cos(position * div_term)

    return PE

为什么用这个公式?

1. 不同频率

维度0,1:高频(快速变化)
维度d_model-2, d_model-1:低频(慢速变化)

类似傅里叶级数:
用不同频率的正弦波叠加,可以表示任意函数

2. 相对位置

定理:
PE(pos+k) 可以表示为 PE(pos) 的线性函数

即:存在矩阵M_k,使得
PE(pos+k) = M_k · PE(pos)

这意味着:
模型可以学习到相对位置关系

3. 外推能力

训练时序列长度n
推理时序列长度n+k(k>0)
仍然可以计算PE(n+k)

对比可学习位置嵌入:
只能处理训练时见过的长度

多头注意力的实现

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads):
        super().__init__()
        assert d_model % num_heads == 0

        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model // num_heads

        # 投影矩阵
        self.W_Q = nn.Linear(d_model, d_model)
        self.W_K = nn.Linear(d_model, d_model)
        self.W_V = nn.Linear(d_model, d_model)
        self.W_O = nn.Linear(d_model, d_model)

    def split_heads(self, x):
        """
        输入: (batch, seq_len, d_model)
        输出: (batch, num_heads, seq_len, d_k)
        """
        batch_size = x.size(0)
        x = x.view(batch_size, -1, self.num_heads, self.d_k)
        return x.transpose(1, 2)

    def forward(self, Q, K, V, mask=None):
        batch_size = Q.size(0)

        # 线性投影
        Q = self.W_Q(Q)  # (batch, seq_len, d_model)
        K = self.W_K(K)
        V = self.W_V(V)

        # 分头
        Q = self.split_heads(Q)  # (batch, num_heads, seq_len, d_k)
        K = self.split_heads(K)
        V = self.split_heads(V)

        # 计算注意力
        scores = torch.matmul(Q, K.transpose(-2, -1)) / np.sqrt(self.d_k)

        if mask is not None:
            scores = scores + mask

        attention = torch.softmax(scores, dim=-1)
        output = torch.matmul(attention, V)

        # 合并头
        output = output.transpose(1, 2).contiguous()
        output = output.view(batch_size, -1, self.d_model)

        # 最终投影
        output = self.W_O(output)

        return output, attention

4.6 训练深度学习模型的实践技巧

数据预处理

归一化(Normalization)

为什么需要?

问题:
特征尺度差异大
例:年龄(0-100)vs 收入(0-1000000)

后果:
- 梯度下降收敛慢
- 某些特征主导训练
- 数值不稳定

方法1:Min-Max归一化

X_norm = (X - X.min()) / (X.max() - X.min())

结果:X_norm ∈ [0, 1]

优点:
- 简单
- 保留分布形状

缺点:
- 对异常值敏感
- 测试集可能超出[0,1]

方法2:Z-score标准化

X_norm = (X - X.mean()) / X.std()

结果:均值0,标准差1

优点:
- 对异常值较鲁棒
- 适合正态分布数据

实践:
深度学习中最常用

图像归一化

# ImageNet标准化
mean = [0.485, 0.456, 0.406]
std = [0.229, 0.224, 0.225]

transform = transforms.Normalize(mean, std)
img_normalized = transform(img)

# 等价于
img_normalized = (img - mean) / std

为什么用ImageNet统计量?

迁移学习:
预训练模型在ImageNet上训练
使用相同的归一化保证分布一致

数据增强(Data Augmentation)

图像增强(详细)

几何变换

# 随机裁剪
transforms.RandomCrop(224)
transforms.RandomResizedCrop(224, scale=(0.08, 1.0))

# 随机翻转
transforms.RandomHorizontalFlip(p=0.5)  # 水平翻转
transforms.RandomVerticalFlip(p=0.5)    # 垂直翻转(某些任务)

# 随机旋转
transforms.RandomRotation(degrees=15)

# 随机仿射变换
transforms.RandomAffine(
    degrees=0,
    translate=(0.1, 0.1),  # 平移±10%
    scale=(0.9, 1.1),      # 缩放90%-110%
    shear=10               # 剪切
)

# 随机透视变换
transforms.RandomPerspective(distortion_scale=0.5, p=0.5)

颜色变换

# 颜色抖动
transforms.ColorJitter(
    brightness=0.2,  # 亮度±20%
    contrast=0.2,    # 对比度±20%
    saturation=0.2,  # 饱和度±20%
    hue=0.1          # 色调±10%
)

# 随机灰度化
transforms.RandomGrayscale(p=0.1)

# 随机反转颜色
transforms.RandomInvert(p=0.1)

# 自动对比度
transforms.AutoAugment()

高级增强

Mixup

def mixup(x1, y1, x2, y2, alpha=1.0):
    """
    混合两个样本
    """
    lambda_ = np.random.beta(alpha, alpha)

    x = lambda_ * x1 + (1 - lambda_) * x2
    y = lambda_ * y1 + (1 - lambda_) * y2

    return x, y

# 例子
# 图像1:猫(标签[1, 0])
# 图像2:狗(标签[0, 1])
# lambda=0.6
# 混合图像:0.6*猫 + 0.4*狗
# 混合标签:[0.6, 0.4]

好处:
- 正则化效果
- 更平滑的决策边界

CutMix

def cutmix(x1, y1, x2, y2, alpha=1.0):
    """
    剪切粘贴图像块
    """
    lambda_ = np.random.beta(alpha, alpha)

    # 随机选择矩形区域
    W, H = x1.size()[-2:]
    cut_w = int(W * np.sqrt(1 - lambda_))
    cut_h = int(H * np.sqrt(1 - lambda_))

    cx = np.random.randint(W)
    cy = np.random.randint(H)

    x1_left, x1_top = max(0, cx - cut_w // 2), max(0, cy - cut_h // 2)
    x1_right, x1_bottom = min(W, cx + cut_w // 2), min(H, cy + cut_h // 2)

    # 剪切粘贴
    x = x1.clone()
    x[..., x1_top:x1_bottom, x1_left:x1_right] = x2[..., x1_top:x1_bottom, x1_left:x1_right]

    # 标签混合(根据面积比例)
    lambda_ = 1 - (cut_w * cut_h) / (W * H)
    y = lambda_ * y1 + (1 - lambda_) * y2

    return x, y

RandAugment

# 随机选择增强操作并随机确定强度
transforms.RandAugment(
    num_ops=2,         # 随机选择2个操作
    magnitude=9        # 强度(0-30)
)

操作池:
- 旋转、平移、剪切
- 颜色变换
- 锐化、模糊
- 对比度、亮度
- ...

每次随机选择,组合爆炸式多样性

文本增强

1. 同义词替换(Synonym Replacement):
   "I love this movie" → "I adore this film"

2. 随机插入(Random Insertion):
   "I love this" → "I really love this"

3. 随机交换(Random Swap):
   "I love this movie" → "I this love movie"

4. 随机删除(Random Deletion):
   "I love this movie" → "I love movie"

5. 回译(Back Translation):
   英语 → 法语 → 英语
   "I love movies" → "J'aime les films" → "I like films"

学习率调度(Learning Rate Scheduling)

为什么需要?

训练初期:
- 损失高,需要快速下降
- 大学习率,快速逼近最优

训练后期:
- 接近最优,需要精细调整
- 小学习率,避免震荡

常用策略

1. 步长衰减(Step Decay)

scheduler = torch.optim.lr_scheduler.StepLR(
    optimizer,
    step_size=30,  # 每30个epoch
    gamma=0.1      # 学习率乘以0.1
)

# 例子
epoch 0-29:lr = 0.1
epoch 30-59:lr = 0.01
epoch 60-89:lr = 0.001

2. 多步衰减(MultiStep Decay)

scheduler = torch.optim.lr_scheduler.MultiStepLR(
    optimizer,
    milestones=[30, 60, 90],  # 在这些epoch衰减
    gamma=0.1
)

# 例子
epoch 0-29:lr = 0.1
epoch 30-59:lr = 0.01
epoch 60-89:lr = 0.001
epoch 90+:lr = 0.0001

3. 指数衰减(Exponential Decay)

scheduler = torch.optim.lr_scheduler.ExponentialLR(
    optimizer,
    gamma=0.95  # 每个epoch乘以0.95
)

# lr_t = lr_0 * gamma^t

平滑下降

4. 余弦退火(Cosine Annealing)

scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
    optimizer,
    T_max=100,      # 周期
    eta_min=0.0001  # 最小学习率
)

# 公式
lr_t = eta_min + (lr_0 - eta_min) * (1 + cos(π * t / T_max)) / 2

# 特点
# - 平滑下降(余弦曲线)
# - 可以多周期重启(CosineAnnealingWarmRestarts)

5. 预热(Warmup)+ 衰减

def get_lr(step, warmup_steps, d_model):
    """
    Transformer论文使用的学习率调度
    """
    arg1 = step ** (-0.5)
    arg2 = step * (warmup_steps ** (-1.5))

    return d_model ** (-0.5) * min(arg1, arg2)

# 或PyTorch实现
scheduler = torch.optim.lr_scheduler.LambdaLR(
    optimizer,
    lr_lambda=lambda step: min(
        step ** (-0.5),
        step * warmup_steps ** (-1.5)
    )
)

# 效果
# 0 - warmup_steps:线性增长
# warmup_steps后:按幂次衰减

为什么需要Warmup?

训练初期:
- 参数随机初始化
- 梯度可能很大
- 大学习率 → 不稳定

Warmup:
- 前几步用小学习率
- 让模型"热身"
- 稳定训练

6. ReduceLROnPlateau(基于指标)

scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
    optimizer,
    mode='min',      # 最小化指标(如损失)
    factor=0.1,      # 衰减因子
    patience=10,     # 容忍多少个epoch不改善
    verbose=True
)

# 使用
for epoch in range(num_epochs):
    train(...)
    val_loss = validate(...)

    scheduler.step(val_loss)  # 根据验证损失调整

7. 循环学习率(Cyclic LR)

scheduler = torch.optim.lr_scheduler.CyclicLR(
    optimizer,
    base_lr=0.001,      # 最小学习率
    max_lr=0.01,        # 最大学习率
    step_size_up=2000,  # 上升步数
    mode='triangular'   # 模式
)

# 学习率在base_lr和max_lr之间循环
# 有时能跳出局部最优

8. 一周期学习率(OneCycleLR)

scheduler = torch.optim.lr_scheduler.OneCycleLR(
    optimizer,
    max_lr=0.01,
    total_steps=num_epochs * steps_per_epoch,
    pct_start=0.3,  # 前30%时间增长,后70%衰减
    anneal_strategy='cos'
)

# 流行于fast.ai
# 先快速增长,再缓慢衰减

梯度裁剪(Gradient Clipping)

为什么需要?

问题:
RNN等模型容易梯度爆炸
梯度很大 → 权重更新过大 → 训练不稳定

解决:
限制梯度的范数

方法1:按值裁剪

torch.nn.utils.clip_grad_value_(
    model.parameters(),
    clip_value=0.5
)

# 每个梯度元素:
# if g > 0.5: g = 0.5
# if g < -0.5: g = -0.5

方法2:按范数裁剪(推荐)

torch.nn.utils.clip_grad_norm_(
    model.parameters(),
    max_norm=1.0
)

# 如果 ||grad|| > max_norm:
#     grad = grad / ||grad|| * max_norm
# 保持方向,缩放大小

使用示例

for epoch in range(num_epochs):
    for batch in dataloader:
        optimizer.zero_grad()

        loss = model(batch)
        loss.backward()

        # 梯度裁剪
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

        optimizer.step()

何时使用?

必须:
- RNN、LSTM、GRU(梯度爆炸常见)
- Transformer(大模型训练)

可选:
- CNN(通常不需要,但不会有害)

推荐值:
- RNN:0.25 - 5.0
- Transformer:1.0

4.7 强化学习基础

强化学习(RL)是AI的第三大范式,也是训练ChatGPT等模型的关键技术(RLHF)。

MDP框架(马尔可夫决策过程)

五元组

MDP = (S, A, P, R, γ)

S:状态空间(State Space)
   - 所有可能的状态集合
   - 例(游戏):棋盘配置

A:动作空间(Action Space)
   - 所有可能的动作集合
   - 例:上下左右

P:状态转移概率(Transition Probability)
   - P(s'|s,a):在状态s执行动作a后转移到s'的概率
   - 环境动态

R:奖励函数(Reward Function)
   - R(s, a, s'):执行动作后获得的即时奖励
   - 例:游戏得分

γ:折扣因子(Discount Factor)
   - γ ∈ [0, 1]
   - 未来奖励的折扣

马尔可夫性质

P(s_{t+1} | s_t, a_t, s_{t-1}, a_{t-1}, ..., s_0, a_0) = P(s_{t+1} | s_t, a_t)

即:
下一状态只依赖于当前状态和动作
与历史无关

这是一个简化假设
实际中有时需要记忆(LSTM等)

目标

学习策略π(a|s):
在状态s时选择动作a的概率

最大化累积折扣奖励:
G_t = Σ_{k=0}^{∞} γ^k R_{t+k+1}

其中γ的作用:
- γ接近0:短视(只看眼前奖励)
- γ接近1:远视(重视长期奖励)
- γ=0.9-0.99常用

价值函数

状态价值函数V^π(s)

定义:
V^π(s) = E_π[G_t | s_t = s]
       = E_π[Σ_{k=0}^{∞} γ^k R_{t+k+1} | s_t = s]

意义:
从状态s开始,遵循策略π,期望的累积奖励

Bellman方程:
V^π(s) = Σ_a π(a|s) Σ_{s'} P(s'|s,a) [R(s,a,s') + γV^π(s')]
       = E_π[R_{t+1} + γV^π(s_{t+1}) | s_t = s]

动作价值函数Q^π(s,a)

定义:
Q^π(s,a) = E_π[G_t | s_t = s, a_t = a]

意义:
在状态s执行动作a,然后遵循策略π,期望的累积奖励

Bellman方程:
Q^π(s,a) = Σ_{s'} P(s'|s,a) [R(s,a,s') + γ Σ_{a'} π(a'|s') Q^π(s',a')]
         = E[R_{t+1} + γQ^π(s_{t+1}, a_{t+1}) | s_t=s, a_t=a]

最优价值函数

V*(s) = max_π V^π(s)
Q*(s,a) = max_π Q^π(s,a)

Bellman最优方程:
V*(s) = max_a Σ_{s'} P(s'|s,a) [R(s,a,s') + γV*(s')]
Q*(s,a) = Σ_{s'} P(s'|s,a) [R(s,a,s') + γ max_{a'} Q*(s',a')]

最优策略:
π*(s) = argmax_a Q*(s,a)

Q-Learning算法

核心思想

学习动作价值函数Q(s,a)
无需知道环境模型P和R(model-free)

算法

# 初始化
Q = {}  # Q表,Q[s][a]存储Q值
alpha = 0.1  # 学习率
gamma = 0.9  # 折扣因子
epsilon = 0.1  # 探索率

for episode in range(num_episodes):
    s = env.reset()

    while not done:
        # ε-贪心选择动作
        if random() < epsilon:
            a = random_action()  # 探索
        else:
            a = argmax_a Q[s][a]  # 利用

        # 执行动作
        s', r, done = env.step(a)

        # Q-Learning更新
        Q[s][a] = Q[s][a] + alpha * (r + gamma * max_a' Q[s'][a'] - Q[s][a])
                           ↑
                        TD误差(Temporal Difference Error)

        s = s'

TD误差

TD_error = r + γ·max_a' Q(s',a') - Q(s,a)
           目标值              当前估计

目标值:r + γ·max_a' Q(s',a')
- r:即时奖励
- γ·max_a' Q(s',a'):下一状态的最大Q值(折扣后)

如果TD_error > 0Q(s,a)被低估了,增加它

如果TD_error < 0Q(s,a)被高估了,减少它

探索 vs 利用(Exploration vs Exploitation)

ε-贪心策略

以概率ε:随机动作(探索)
以概率1-ε:选择最优动作(利用)

为什么需要探索?
- 如果只利用,可能陷入次优策略
- 探索才能发现更好的动作

ε的衰减:
ε_t = max(ε_min, ε_0 * decay^t)

训练初期:ε大,多探索
训练后期:ε小,多利用

Q-Learning的收敛性

定理:
如果所有(s,a)对被无限次访问
且学习率满足:
- Σ_t α_t = ∞
- Σ_t α_t² < ∞

那么Q收敛到Q*

实践中:
- 固定小学习率(如0.1)也能工作
- 但可能不收敛到精确最优

深度Q网络(DQN)

动机

Q-Learning问题:
- Q表太大(状态空间大)
- 例:围棋,状态数 > 宇宙原子数
- 无法存储所有Q(s,a)

解决:
用神经网络近似Q函数
Q(s,a) ≈ Q(s,a;θ)(θ是网络参数)

DQN算法(2013,DeepMind)

创新1:经验回放(Experience Replay)

# 经验池
replay_buffer = []
max_buffer_size = 100000

# 收集经验
for episode in episodes:
    s = env.reset()
    while not done:
        a = select_action(s)
        s', r, done = env.step(a)

        # 存储经验
        replay_buffer.append((s, a, r, s', done))

        if len(replay_buffer) > max_buffer_size:
            replay_buffer.pop(0)  # 删除旧经验

        # 从经验池随机采样训练
        batch = random.sample(replay_buffer, batch_size)
        train_on_batch(batch)

为什么?
1. 打破样本相关性:
   连续样本高度相关 → 训练不稳定
   随机采样 → 独立同分布

2. 重复利用数据:
   每个经验可以被用多次
   数据效率高

创新2:目标网络(Target Network)

# 两个网络
Q_network = QNetwork()  # 主网络,频繁更新
Q_target = QNetwork()   # 目标网络,定期更新

# 训练
for step in range(training_steps):
    # 采样batch
    batch = replay_buffer.sample(batch_size)

    for (s, a, r, s', done) in batch:
        # 计算目标(使用目标网络)
        if done:
            y = r
        else:
            y = r + gamma * max_a' Q_target(s', a')

        # 更新主网络
        loss = (Q_network(s, a) - y)²
        Q_network.update(loss)

    # 定期同步目标网络
    if step % update_freq == 0:
        Q_target.load(Q_network.parameters())

为什么?
稳定训练:
- 如果用同一个网络计算目标和当前Q
- 目标在不断变化(因为网络在训练)
- "追逐移动的目标" → 不稳定

目标网络:
- 目标相对固定(定期更新)
- 训练更稳定

网络结构(Atari游戏)

输入:84×84×44帧灰度图)
→ Conv 8×8(32, stride=4) → ReLU
→ Conv 4×4(64, stride=2) → ReLU
→ Conv 3×3(64) → ReLU
→ Flatten → FC(512) → ReLU
→ FC(num_actions)

输出:每个动作的Q

成就

2013年:
DQN在多个Atari游戏上达到人类水平

2015年(Nature论文):
在49个Atari游戏中:
- 29个超过人类
- 平均得分是人类的75%

历史意义:
证明深度学习可以用于强化学习
开启Deep RL时代

策略梯度(Policy Gradient)

与Q-Learning的区别

Q-Learning(value-based):
- 学习Q(s,a)
- 策略隐式:π(s) = argmax_a Q(s,a)

策略梯度(policy-based):
- 直接学习策略π(a|s;θ)
- 参数化策略(如神经网络)

为什么需要策略梯度?

1. 连续动作空间:
   Q-Learning需要max_a Q(s,a)
   如果动作连续(如机器人关节角度)
   无法穷举所有a

   策略梯度:
   直接输出动作(或动作分布)

2. 随机策略:
   有些问题需要随机性(如石头剪刀布)
   Q-Learning是确定性的

3. 更容易收敛:
   在某些问题上,策略梯度更稳定

目标函数

最大化期望回报:
J(θ) = E_π[G_t] = E_π[Σ_t γ^t r_t]

策略梯度定理:
∇_θ J(θ) = E_π[∇_θ log π(a|s;θ) · Q^π(s,a)]

直觉:
- 如果Q^π(s,a) > 0:好动作,增加π(a|s)
- 如果Q^π(s,a) < 0:坏动作,减少π(a|s)
- 幅度正比于Q

REINFORCE算法

def reinforce(env, policy_network, num_episodes):
    optimizer = Adam(policy_network.parameters())

    for episode in range(num_episodes):
        states, actions, rewards = [], [], []

        # 采样轨迹
        s = env.reset()
        while not done:
            # 策略网络输出动作概率
            probs = policy_network(s)
            a = sample(probs)  # 根据概率采样

            s', r, done = env.step(a)

            states.append(s)
            actions.append(a)
            rewards.append(r)

            s = s'

        # 计算回报(从后往前)
        G = []
        g = 0
        for r in reversed(rewards):
            g = r + gamma * g
            G.insert(0, g)

        # 策略梯度更新
        for t in range(len(states)):
            s, a, g = states[t], actions[t], G[t]

            # 计算损失(负的对数似然,加权by回报)
            loss = -log(policy_network(s)[a]) * g

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

优点

- 适合连续动作
- 收敛性好(在某些问题上)
- 可以学习随机策略

缺点

- 高方差:
  回报G是随机的,方差大
  → 训练不稳定

- 样本效率低:
  每个轨迹只用一次(on-policy)

改进:
- Actor-Critic(降低方差)
- PPO(提高样本效率)

PPO(Proximal Policy Optimization)

动机

策略梯度问题:
- 高方差
- 样本效率低
- 步长难以选择(太大→策略崩溃,太小→收敛慢)

PPO:
OpenAI 2017年提出
简单、稳定、高效
成为当前主流算法

核心思想

限制策略更新幅度
确保新策略不会偏离旧策略太远

重要性采样(Importance Sampling)

问题:
策略梯度需要从当前策略π_θ采样
每次更新θ后,需要重新采样
→ 样本效率低

解决:
用旧策略π_old采样的数据训练新策略π_θ

重要性采样比:
ρ_t(θ) = π_θ(a_t|s_t) / π_old(a_t|s_t)

期望修正:
E_π_θ[f(a)] = E_π_old[ρ(θ) · f(a)]

PPO-Clip目标函数

L^{CLIP}(θ) = E[min(ρ_t(θ)·A_t, clip(ρ_t(θ), 1-ε, 1+ε)·A_t)]

其中:
- ρ_t(θ) = π_θ(a_t|s_t) / π_old(a_t|s_t)
- A_t:优势函数(Advantage)
- ε:裁剪范围(通常0.1-0.2)
- clip(x, a, b):将x裁剪到[a, b]

直觉:
- 如果A_t > 0(好动作):
  希望增加π_θ(a|s),即ρ > 1
  但限制在(1, 1+ε],防止过大

- 如果A_t < 0(坏动作):
  希望减少π_θ(a|s),即ρ < 1
  但限制在[1-ε, 1),防止过小

优势函数(Advantage Function)

定义:
A^π(s,a) = Q^π(s,a) - V^π(s)

意义:
动作a相对于平均水平的优势

如果A > 0a比平均好,应该增加概率

如果A < 0a比平均差,应该减少概率

估计(用价值网络):
A_t ≈ r_t + γV(s_{t+1}) - V(s_t)  (TD error)
或用GAE(Generalized Advantage Estimation)

PPO算法流程

def ppo(env, policy, value_function, num_iterations):
    for iteration in range(num_iterations):
        # 1. 收集轨迹(用当前策略)
        trajectories = collect_trajectories(env, policy, num_steps)

        # 2. 计算优势
        for traj in trajectories:
            compute_advantages(traj, value_function)

        # 3. 多轮更新(重复使用数据)
        for epoch in range(K):  # K=3-10
            for batch in get_batches(trajectories):
                # 计算重要性采样比
                ratio = policy(a|s) / policy_old(a|s)

                # PPO-Clip损失
                L_clip = min(
                    ratio * advantage,
                    clip(ratio, 1-eps, 1+eps) * advantage
                )

                # 价值函数损失
                L_value = (value_function(s) - return# 熵正则化(鼓励探索)
                L_entropy = -entropy(policy(·|s))

                # 总损失
                loss = -L_clip + c1*L_value - c2*L_entropy

                # 更新
                optimizer.zero_grad()
                loss.backward()
                optimizer.step()

        # 4. 更新旧策略
        policy_old = copy(policy)

PPO的优点

1. 简单:
   只需裁剪,没有复杂的KL惩罚

2. 稳定:
   限制策略更新幅度

3. 样本效率:
   重复使用数据(K轮更新)

4. 通用:
   适用于连续和离散动作

5. 实现友好:
   超参数对性能不敏感

应用

- OpenAI Five(Dota 2)
- OpenAI GPT的RLHF训练
- ChatGPT的奖励模型优化
- 机器人控制

RLHF:强化学习与人类反馈

ChatGPT的秘密武器

动机

语言模型问题:
- 预训练:只是预测下一个词
- 不一定生成"好"的回复
  - 有害内容
  - 不遵循指令
  - 不符合人类偏好

需要:
对齐人类偏好(Alignment)

RLHF三阶段

阶段1:监督微调(SFT)

收集高质量示例:
问题:"如何做披萨?"
答案:(人工撰写的详细回复)

微调语言模型:
在这些示例上训练
→ 得到SFT模型

阶段2:奖励模型训练

收集比较数据:
对同一问题,生成多个回复(用SFT模型)
人工排序:哪个回复更好?

回复A:清晰、有帮助  → 排名1
回复B:正确但啰嗦    → 排名2
回复C:错误          → 排名3

训练奖励模型RM(x, y):
输入:问题x,回复y
输出:分数(越高越好)

损失:
使排名高的回复得分高
L = -E[log σ(RM(x, y_好) - RM(x, y_差))]

阶段3:PPO优化

把语言模型当作RL智能体:

状态s:问题 + 已生成的文本
动作a:下一个token
奖励r:RM(问题, 完整回复) - KL惩罚

KL惩罚:
防止模型偏离SFT模型太远
r_total = RM(x, y) - β·KL(π_RL || π_SFT)

用PPO更新策略:
最大化奖励

效果

ChatGPT = GPT-3.5 + SFT + RLHF

对比:
GPT-3:强大但不可控
ChatGPT:有帮助、无害、诚实

本章总结

本章深入讲解了AI的核心算法和数学原理,覆盖了:

机器学习基础

  • 三大学习范式(监督、无监督、强化学习)
  • 偏差-方差权衡
  • 正则化技术(L1/L2/Dropout/数据增强)

深度学习数学

  • 反向传播算法
  • 优化算法演进(SGD → Adam)
  • 梯度消失/爆炸问题

核心架构

  • CNN:卷积运算、池化、经典架构(LeNet-VGG-ResNet)
  • RNN:记忆机制、BPTT、LSTM/GRU
  • Transformer:自注意力、多头注意力、位置编码

训练技巧

  • 数据预处理与增强
  • 学习率调度
  • 梯度裁剪
  • BatchNorm等归一化技术

强化学习

  • MDP框架
  • Q-Learning与DQN
  • 策略梯度与PPO
  • RLHF(ChatGPT的关键)

这些算法和技术构成了现代AI的基石。掌握它们不仅能帮助你理解已有模型,更能让你设计和调试新模型。


下一章预告第五章:未来展望 - 通往AGI之路

我们将探讨AI的未来方向:当前AI的局限性、走向AGI的技术路径、前沿研究方向、伦理与安全问题,以及AI对社会的深远影响。