楼层: 首页/ 软件技术/ Python 机器学习与深度学习/ PyTorch 深度学习实战
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_memoryGPU 训练时设 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),防爆炸。
混合精度 AMPtorch.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() 关闭计算图,推理省显存快。