深度学习 4 — 卷积网络 (2/3)
:::tip Kaggle 笔记本 本章的完整可执行代码在 Kaggle 上:打开 →
法语和英语版本可在首页查看。 :::
在上一章中,我们在 CSV 存储的数据集上训练了 CNN(MNIST、CIFAR-10)。便于入门,但实际上图像几乎总是来自 PNG/JPG 文件。我们还发现稳定和正则化深层 CNN 的两种关键技术:Batch Normalization 和 Dropout。
为什么要学这一章?
您将学到:
- 使用
ImageFolder从磁盘加载图像; - 加快数据流水线(
num_workers、pin_memory); - Batch Normalization:为什么以及在哪里放置;
- Dropout 减少过拟合。
ImageFolder:按文件夹组织
PyTorch 期望非常简单的组织:一个类别一个文件夹。
dataset/
├── train/
│ ├── 0/
│ │ ├── img_001.png
│ │ └── img_002.png
│ ├── 1/
│ └── ...
└── test/
├── 0/
└── ...
子文件夹名称作为标签。torchvision.datasets.ImageFolder 完成所有工作。
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
transform = transforms.Compose([
transforms.Resize((28, 28)),
transforms.ToTensor() # PIL [0,255] → tensor [0,1] (C, H, W)
])
train_dataset = datasets.ImageFolder(root='dataset/train', transform=transform)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
ToTensor() 的魔力
ToTensor() 一次完成三件事:
- 将 PIL Image 转换为 PyTorch 张量;
- 除以 255 → 值在 中;
- 从 HWC 重新排列为 CHW。
实际后果:使用 ImageFolder + ToTensor(),您再也不需要 reshape 或 transpose。批次直接以 格式到达。
| 来源 | 需要 reshape 吗? |
|---|---|
| CSV(第 3 章) | 是 |
ImageFolder(这里) | 否,由 ToTensor() 已经是 BCHW |
加快加载
当 GPU 上训练"似乎慢"时,原因很少是模型。通常是数据流水线慢。四个杠杆:
num_workers
默认 num_workers=0:所有事情都在主进程中发生。num_workers > 0 时,单独的进程并行准备批次。
DataLoader(train_dataset, batch_size=256, shuffle=True, num_workers=4)
通常值:Kaggle 上 2-4,在强力机器上 8。
pin_memory
使用 pin_memory=True,张量在固定(page-locked)内存中分配,加速到 GPU 的传输。
DataLoader(..., pin_memory=True)
for Xb, yb in train_loader:
Xb = Xb.to(device, non_blocking=True)
persistent_workers
避免在每个 epoch 重新创建 worker。当 epoch 短时有用。
DataLoader(..., num_workers=4, persistent_workers=True)
总结
DataLoader(
train_dataset,
batch_size=128,
shuffle=True,
num_workers=4,
persistent_workers=True,
pin_memory=True,
)
目标:GPU 永远不应等待批次。
Batch Normalization
问题: 在每个梯度步骤,权重发生变化,因此馈送到下一层的激活分布也发生变化。模型必须不断重新调整——训练缓慢且不稳定。
解决方案: 按批次归一化激活。对于每个通道,重新居中(均值 ≈ 0)和重新缩放(方差 ≈ 1),然后应用学习的仿射变换(每个通道两个参数 和 )。
好处
- 训练更稳定;
- 更快收敛(通常允许
lr加倍或加三倍); - 对初始化的敏感性降低;
- 轻微的正则化效果。
在哪里放 BN
标准位置:在卷积之后,激活之前。
Conv2d → BatchNorm2d → ReLU
self.conv = nn.Conv2d(32, 64, kernel_size=3, padding=1)
self.bn = nn.BatchNorm2d(64) # conv 输出 64 通道
self.relu = nn.ReLU()
PyTorch 自动处理双模式:
- 在
model.train():在当前批次上计算的统计; - 在
model.eval():训练期间累积的平均统计。
:::warning 评估时总是调用 eval()
没有 model.eval(),模型会继续使用当前批次的统计——改变其预测。养成习惯。
:::
Dropout
问题: 深层网络可能通过过度依赖某些神经元组合而过拟合,创建脆弱的路径。
解决方案: 在训练期间,每一步随机禁用 比例的神经元(密集层通常 ,conv 层 -)。网络学习不依赖任何特定的神经元。
在评估时,dropout 由 model.eval() 自动禁用。
在哪里放 Dropout
主要在网络末端的密集层(Linear)中。
Linear → ReLU → Dropout
self.fc = nn.Linear(512, 256)
self.dropout = nn.Dropout(p=0.5)
更少在 conv 层之后(BN 在该级别已经发挥正则化作用)。
BN,Dropout,还是两者?
| 技术 | 何时使用 |
|---|---|
| 仅 BatchNorm | 现代 CNN 中的标准。许多情况下足够。 |
| 仅 Dropout | 当 BatchNorm 出现问题时(小批次、RNN、某些 transformer)。 |
| 两者 | 兼容。BN 在 conv 部分,Dropout 在密集部分。 |
这两种技术与 ReLU 同样重要,是深度学习工具包的必备。
典型现代 CNN 架构
class CNN(nn.Module):
def __init__(self):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(32, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(64, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2),
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(128 * 4 * 4, 256), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(256, 10),
)
def forward(self, x):
x = self.features(x)
x = self.classifier(x)
return x
这是大多数现代"自制"架构中的骨架。