章节概述
本章将深入讲解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.0001)
beta1 = 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):
(-1)·0 + 0·0 + 1·0 +
(-1)·0 + 0·0 + 1·0 +
(-1)·0 + 0·0 + 1·0 = 0
位置(1,2):
(-1)·0 + 0·0 + 1·1 +
(-1)·0 + 0·0 + 1·1 +
(-1)·0 + 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 × 32 ≈ 2.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) × 2 → FC(1000)
总层数:13个卷积层 + 3个全连接层 = 16层
参数:1.38亿
为什么3×3?
2个3×3 = 1个5×5的感受野
3个3×3 = 1个7×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|| > 1 且 tanh' ≈ 1 → 梯度爆炸
- ||W_hh|| < 1 或 tanh' ≈ 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_t ⊙ tanh(C_t)
其中:
- σ:sigmoid函数(输出0-1)
- ⊙:逐元素乘法
- [h_{t-1}, x_t]:拼接
直觉理解:
遗忘门f_t:
作用:决定保留多少C_{t-1}
f_t ≈ 1:完全保留旧记忆
f_t ≈ 0:完全丢弃旧记忆
例:
句子"我喜欢吃苹果,它很甜"
处理"它"时:
- 如果需要记住"苹果",f_t ≈ 1
- 如果"苹果"不重要,f_t ≈ 0
输入门i_t:
作用:决定添加多少新信息
i_t ≈ 1:完全添加C̃_t
i_t ≈ 0:不添加新信息
与遗忘门配合:
- 遗忘旧信息(f_t)
- 添加新信息(i_t)
输出门o_t:
作用:决定输出多少C_t
o_t ≈ 1:完全输出
o_t ≈ 0:几乎不输出
例:
某些时刻只需要记住信息(C_t更新)
但不需要输出(o_t ≈ 0)
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_t ≈ 1
- 梯度不会消失!
对比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])
每行是一个概率分布(和为1)
A[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 > 0:
Q(s,a)被低估了,增加它
如果TD_error < 0:
Q(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×4(4帧灰度图)
→ 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 > 0:
a比平均好,应该增加概率
如果A < 0:
a比平均差,应该减少概率
估计(用价值网络):
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对社会的深远影响。