12
PyTorch 深度学习实战
PyTorch in Practice: CNN / RNN
把神经网络的零件拼成完整训练流水线。三个案例:MNIST(MLP)、CIFAR-10(CNN)、IMDB 情感分析(RNN)。
核心 API 速查
| 组件 | 常用类 |
| 层 | nn.Linear, nn.Conv2d, nn.LSTM, nn.Embedding, nn.Transformer |
| 激活 | nn.ReLU, nn.GELU, nn.Softmax, nn.Sigmoid |
| 正则 | nn.Dropout, nn.BatchNorm2d, nn.LayerNorm |
| 损失 | nn.MSELoss, nn.CrossEntropyLoss, nn.BCEWithLogitsLoss |
| 优化器 | optim.SGD, optim.Adam, optim.AdamW |
| 调度 | StepLR, CosineAnnealingLR, ReduceLROnPlateau |
CNN 为什么比全连接强
论局部感受野 + 权值共享
全连接把图片拉成一维向量,丢掉了"像素之间的位置关系"。CNN 用卷积核在图片上滑动:局部感受野(每个神经元只看一小块)+ 权值共享(同一个卷积核在所有位置用同一组权重)。第一层学边缘,第二层学纹理,第三层学物体部件。参数比全连接少几个数量级。
ResNet:残差连接解决深度网络退化
论为什么网络不能无限深
网络加深后,梯度消失让深层学不动,甚至训练误差反而变大(退化问题)。ResNet 的残差连接:y = F(x) + x,让梯度可以"抄近路"直接传到前面。有了残差连接,100 层、1000 层网络都能训。这是现代深度网络的标配。
DataLoader 参数速查
| 参数 | 说明 |
| batch_size | 每批多少样本,常用 32/64/128。 |
| shuffle | 每个 epoch 打乱数据。训练集 True,验证/测试 False。 |
| num_workers | 多进程加载,Windows 上设 0。 |
| pin_memory | GPU 训练时设 True,加速数据到 GPU。 |
| drop_last | 最后不满一批的扔掉,BatchNorm 需要。 |
案例 1:MNIST 手写数字(LeNet 风格)
import torch, torch.nn as nn, torch.optim as optim
from torch.utils.data import DataLoader
from torchvision.datasets import MNIST
from torchvision import transforms
device = "cuda" if torch.cuda.is_available() else "cpu"
transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))])
train_ds = MNIST("./data", train=True, download=True, transform=transform)
test_ds = MNIST("./data", train=False, transform=transform)
train_loader = DataLoader(train_ds, batch_size=128, shuffle=True)
test_loader = DataLoader(test_ds, batch_size=256)
class LeNet(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 6, 5, padding=2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.pool = nn.MaxPool2d(2, 2)
self.fc1 = nn.Linear(16*5*5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = self.pool(torch.relu(self.conv1(x)))
x = self.pool(torch.relu(self.conv2(x)))
x = x.view(-1, 16*5*5)
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
return self.fc3(x)
model = LeNet().to(device)
opt = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)
crit = nn.CrossEntropyLoss()
for epoch in range(10):
model.train()
for x, y in train_loader:
x, y = x.to(device), y.to(device)
opt.zero_grad()
loss = crit(model(x), y)
loss.backward()
opt.step()
# 验证
model.eval(); correct=0; total=0
with torch.no_grad():
for x, y in test_loader:
x, y = x.to(device), y.to(device)
correct += (model(x).argmax(1) == y).sum().item()
total += y.size(0)
print(f"Epoch {epoch+1}: 测试准确率 {correct/total:.2%}")
# Epoch 1: 测试准确率 97.85%
# Epoch 2: 测试准确率 98.46%
# ...
案例 2:CIFAR-10(ResNet 风格 + 数据增强)
from torchvision import models, datasets
import torchvision.transforms as T
train_transform = T.Compose([
T.RandomCrop(32, padding=4),
T.RandomHorizontalFlip(),
T.ToTensor(),
T.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)),
])
test_transform = T.Compose([
T.ToTensor(),
T.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)),
])
# 直接用 torchvision 的 ResNet-18
model = models.resnet18(weights=None, num_classes=10)
# CIFAR 是 32×32,把第一层 7×7 改成 3×3
model.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False)
model.maxpool = nn.Identity()
model = model.to(device)
opt = optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4)
sched = optim.lr_scheduler.CosineAnnealingLR(opt, T_max=200)
# 跑 200 轮能到 94%+ 准确率
RNN / LSTM 为什么适合序列
论带"记忆"的网络
全连接/CNN 把每个输入独立处理,不记得前面说过什么。RNN 有一个隐藏状态 hₜ,每步更新:hₜ = tanh(W·[hₜ₋₁, xₜ])。这样它能记住序列的历史。但普通 RNN 长序列会梯度消失,记不住 100 步前的内容。LSTM 加了 cell state 和三个门(输入门/遗忘门/输出门),让梯度可以线性流动,能记住长距离依赖。GRU 是简化版(两个门)。
案例 3:IMDB 情感分析(LSTM)
class LSTMClassifier(nn.Module):
def __init__(self, vocab_size, embed_dim=128, hidden_dim=256, num_layers=2):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
self.lstm = nn.LSTM(embed_dim, hidden_dim, num_layers,
batch_first=True, bidirectional=True, dropout=0.5)
self.fc = nn.Linear(hidden_dim * 2, 1)
def forward(self, x):
emb = self.embedding(x) # (B, L, E)
out, (h, c) = self.lstm(emb)
# 双向 LSTM:把最后一层的前向和后向 hidden 拼起来
cat = torch.cat([h[-2], h[-1]], dim=1)
return self.fc(cat).squeeze(1)
训练技巧清单
| 技巧 | 一句话 |
| 梯度裁剪 | nn.utils.clip_grad_norm_(params, 1.0),防爆炸。 |
| 混合精度 AMP | torch.cuda.amp.autocast() + GradScaler,省显存快 2 倍。 |
| 权重初始化 | Xavier(sigmoid/tanh)、He/kaiming(ReLU)。 |
| BatchNorm | 每层输入标准化,收敛快。 |
| Dropout | 训练随机关神经元,防过拟合。 |
| 数据增强 | 翻转、裁剪、颜色抖动,免费加数据。 |
| 迁移学习 | 用预训练权重,只微调最后几层。 |
| 标签平滑 | 目标 1 改成 0.9,防过度自信。 |
混合精度 AMP 模板
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
for epoch in range(10):
model.train()
for x, y in train_loader:
x, y = x.to(device), y.to(device)
opt.zero_grad()
with autocast(device_type="cuda", dtype=torch.float16):
out = model(x)
loss = criterion(out, y)
scaler.scale(loss).backward()
scaler.step(opt)
scaler.update()
# 显存占用通常减半,速度快 1.5~2 倍
学习率调度策略对比
| 策略 | 说明 |
| StepLR | 每 step_size 轮乘 gamma,简单。 |
| CosineAnnealingLR | 按余弦曲线降到 0,训练 CV 任务默认。 |
| ReduceLROnPlateau | 验证 loss 不降就降 lr,稳妥。 |
| Warmup + Cosine | 前几百步线性升 lr 再余弦降,训练大模型标配。 |
本章面试题
面试 · PyTorch / 深度学习
Q1. CNN 为什么适合图像?
查看答案
局部感受野 + 权值共享,能识别边缘/纹理/形状,且参数比全连接少几个数量级。
Q2. BatchNorm 为什么有效?
查看答案
把每层输入标准化到均值 0 方差 1,缓解 Internal Covariate Shift,让学习率可以更大、收敛更快、对初始化不敏感。
Q3. LSTM 怎么解决梯度消失?
查看答案
门控机制(输入门/遗忘门/输出门)让梯度在 cell state 上可以"线性流动",不会被反复乘激活函数。
Q4. model.eval() 后为什么还要 torch.no_grad()?
查看答案
eval() 只改 Dropout/BatchNorm 行为,梯度仍会算(占显存)。no_grad() 关闭计算图,推理省显存快。