元数据卡
- 前置知识:ch05 线性模型、Vol 10 微积分(链式法则)
- 预计时间:60 分钟
- 核心难度:进阶/深入
- 阅读模式:高度专注
- 完成标志:能手动推算反向传播的梯度流、理解各激活函数的区别、使用 PyTorch 训练一个两层网络
你的进度
你在模型工坊里试过树模型、线性模型——在表格数据上它们很能打。但你从口袋里掏出一张手写数字的图片,扔到工坊的测试台上,模型完全不会处理。
树模型不知道像素是什么,线性模型参数量大到爆炸。你需要在工坊里开一条新的生产线。
你想起数学塔里学过的链式法则,想起计算图——它们在这里找到了归宿。
你的任务
神经网络把线性层堆叠起来,每层后加非线性激活函数,再用反向传播端到端训练。本章从感知机构建到多层网络,手算反向传播,理解优化器的演变。
本章分层
- 必读:感知机 → 多层感知机、反向传播手动推导、常见激活函数
- 选读:SGD vs Adam vs RMSProp 的直观区别
- 进阶:梯度消失与梯度爆炸的理论分析
破局 · 溯源
回顾 ch05 的逻辑回归:它本质是一个"输入 → 加权求和 → sigmoid"的流水线。它能做线性分类,但 XOR 问题就把它难住了——你无法用一条直线把 XOR 的两个类别分开。
解决方案:再加一层。首层学习一些中间特征(比如"两个输入相等吗"),第二层在这些中间特征上做分类。这就是神经网络的本质——多层非线性变换的组合。
从感知机到多层网络
单个感知机就是一个线性分类器:
y = sigmoid(w1*x1 + w2*x2 + ... + b)堆叠起来:第一层学低层特征,第二层学特征组合,更多层学更抽象的模式。
import torch
import torch.nn as nn
import torch.optim as optim
class TwoLayerNet(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
super().__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.activation = nn.ReLU()
self.fc2 = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
h = self.activation(self.fc1(x))
return self.fc2(h) # 分类任务通常用 CrossEntropyLoss(自带 softmax)
model = TwoLayerNet(784, 128, 10)
print(model)网络的可学习参数全部在 Linear 层:每个 Linear 层有两组参数——权重 W(shape: [in_features, out_features])和偏置 b(shape: [out_features])。参数总数是 in_features * out_features + out_features。
反向传播
反向传播是训练的核心机制。它用链式法则计算损失对每个参数的梯度。
手动追踪一个简单网络的正向和反向过程:
# 极小网络:一个隐藏神经元 + 一个输出神经元
# 正向:
# z1 = w1*x + b1
# a1 = relu(z1)
# z2 = w2*a1 + b2
# loss = 0.5 * (z2 - y)^2
# 反向: 从 loss 往回推
# d_loss/d_z2 = z2 - y
# d_loss/d_w2 = d_loss/d_z2 * a1
# d_loss/d_b2 = d_loss/d_z2
# d_loss/d_a1 = d_loss/d_z2 * w2
# d_loss/d_z1 = d_loss/d_a1 * (1 if z1 > 0 else 0) [relu 导数]
# d_loss/d_w1 = d_loss/d_z1 * x
# d_loss/d_b1 = d_loss/d_z1
# PyTorch 自动帮你做这一切
x = torch.randn(4, 784)
y = torch.randint(0, 10, (4,))
output = model(x)
loss = nn.CrossEntropyLoss()(output, y)
loss.backward() # 自动计算所有参数的梯度反向传播的核心洞察是"梯度共享"——中间变量 a1 出现在前向路径的多个下游节点中。反向传播通过拓扑顺序遍历计算图,保证了每个中间变量的梯度只计算一次。
激活函数 vs 线性:为什么不能只用线性层?
两个线性层叠加相当于一个线性层(W2*(W1x) = (W2W1)*x),非线性激活函数打破这种退化,让网络能够表示任意复杂函数。
# 常用激活函数
activation_fns = {
'sigmoid': nn.Sigmoid(), # [0,1],但梯度在两端几乎为零(梯度消失)
'tanh': nn.Tanh(), # [-1,1],梯度消失稍好
'ReLU': nn.ReLU(), # [0,∞),简单快速,但负半轴死区
'LeakyReLU': nn.LeakyReLU(0.01), # 负半轴有小梯度,解决"神经元死亡"
'GELU': nn.GELU(), # 对 ReLU 的光滑近似,Transformer 标准
}ReLU 是实际使用最广泛的激活函数。它的梯度在正半轴恒为 1——不增不减——避免了深度网络中梯度的指数级衰减。
优化器
梯度下降告诉"往哪走"(方向),优化器决定"走多大步"(学习率)和"怎么走得更聪明"。
# SGD:最原始的阶梯下降
optimizer = optim.SGD(model.parameters(), lr=0.01)
# Adam:自适应学习率 + 动量
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 训练循环
for epoch in range(100):
optimizer.zero_grad()
output = model(x)
loss = nn.CrossEntropyLoss()(output, y)
loss.backward()
optimizer.step()关键优化器对比:
| 优化器 | 核心机制 | 适用场景 |
|---|---|---|
| SGD | 原始梯度 | 简单任务,需要精细调参 |
| SGD+Momentum | 累加历史梯度方向 | 避免鞍点停滞 |
| RMSProp | 按参数自适应学习率 | RNN、非平稳目标 |
| Adam | Momentum + RMSProp | 通用首选,默认推荐 |
Adam 用两个统计量调整每个参数的学习率:梯度的一阶矩估计(动量)和二阶矩估计(自适应缩放)。对于绝大多数深度学习任务,Adam(Ir=0.001) 是一个可用的起点。
常见陷阱
- 权重初始化很重要:全零初始化导致所有神经元学到相同特征(对称性)。推荐 Xavier/Glorot 或 He 初始化。
- 损失函数的选择:回归用 MSELoss,二分类用 BCEWithLogitsLoss,多分类用 CrossEntropyLoss(自带 softmax,不需要单独加)。
- 学习率过高会导致 loss 发散(NaN),过低则几乎不收敛。观察 loss 曲线——如果振荡,降低学习率。
- 批归一化(BatchNorm)放在激活函数之前还是之后?实践上有争论——建议放在激活函数之前,全连接层之后。
- 小批量(batch size)的影响:太大收敛快但泛化差,太小梯度波动大。32~256 是常见选择。
通关挑战
- 热身(15 分钟):在纸上手动计算一个 2-2-1 网络(2 输入 → 隐藏层 2 个 ReLU → 1 输出)的反向传播。给定输入 (x1, x2)=(1,0),目标 y=1,计算 w11 的梯度。
- 挑战(40 分钟):用 PyTorch 在 MNIST 上训练一个三层网络(784→512→256→10),使用 ReLU + Adam。达到 97%+ 测试准确率。
- 观察:在训练过程中保持学习率不变,分别用 0.1、0.01、0.001、0.0001 训练,画出 loss 曲线——观察哪个学习率导致振荡、哪个太慢、哪个最佳。
旅人笔记
神经网络等于线性变换嵌套非线性激活。反向传播用链式法则让整个网络作为一个整体端到端学习。优化器的选择决定了到达"好解"的速度和质量。三者在深度学习的迭代中不断演化,但核心框架始终未变。
-> 下一站预告
全连接网络在图像和序列上的表现有限。下一章介绍深度学习的两大突破:CNN 和 Transformer。