深度学习 3 — 卷积网络 (1/3)
:::tip Kaggle 笔记本 本章的完整可执行代码在 Kaggle 上:打开 →
法语和英语版本可在首页查看。 :::
第一次接触卷积神经网络(CNN)。本章的主要概念性挑战:PyTorch 的 BCHW 张量约定。一旦理解这个规则,其余内容自然跟上。
为什么要学这一章?
您将学到:
- 为什么用 CNN 而不是 MLP 处理图像;
- PyTorch 对图像期望的
(B, C, H, W)轴心规则; - 构件
Conv2d、MaxPool2d、Flatten; - 如何堆叠多个卷积层;
nn.Module语法(编写模型的专业方式);- 使用
.to(device)使用 GPU。
为什么用 CNN?
MLP 将图像视为 1D 向量(将 展平为 )。三个问题:
- 空间邻近性的丢失:对于 MLP,像素 和像素 与像素 和像素 一样陌生。
- 巨大的参数数量:对于 224×224 RGB 图像,约 150,000 个输入特征。100 个隐藏神经元,仅第一层就约 1500 万个权重。
- 没有平移不变性:如果对象移动几个像素,网络就不再识别它。
CNN 通过利用两个想法解决这三个问题:
- 局部连接:神经元只看小邻域(3×3 像素),不是整个图像。
- 权重共享:同一个 3×3 滤波器应用于所有位置。参数大大减少,平移不变性免费。
黄金法则:(B, C, H, W)
PyTorch 在卷积时总是期望 4D 张量
(batch, channels, height, width)。就是这样。
维度细分:
| 维度 | 含义 | MNIST | CIFAR-10 |
|---|---|---|---|
| B | 并行处理的图像数 | 64 | 64 |
| C | 通道数 | 1(灰度) | 3(RGB) |
| H | 像素高度 | 28 | 32 |
| W | 像素宽度 | 28 | 32 |
这种约定称为 BCHW(或 NCHW)。出于 GPU 性能原因,PyTorch 强制使用它。
:::warning matplotlib 使用不同的约定
matplotlib(和一般的 numpy)期望图像采用 HWC(height, width, channels)。这种差异将迫使我们对 RGB 图像进行一些置换。
:::
每次操作图像张量,用 print(X.shape) 打印其形状。这是避免 90% bug 的反射动作。
重塑图像
MNIST(1 通道,扁平存储)
X = df.drop(columns='label').to_numpy() # (N, 784)
X = X.reshape(-1, 1, 28, 28) / 255.0 # (N, 1, 28, 28) — 已经是 BCHW
X_t = torch.tensor(X, dtype=torch.float32)
CIFAR-10(3 通道,以 HWC 存储)
X = df.drop(columns='label').to_numpy() # (N, 3072)
X = X.reshape(-1, 32, 32, 3) / 255.0 # (N, H, W, C) — 图像约定
X = X.transpose(0, 3, 1, 2) # (N, C, H, W) — 为 PyTorch 置换
X_t = torch.tensor(X, dtype=torch.float32)
唯一的区别:对于 RGB,我们将轴从 HWC 置换到 CHW。
构件 1:nn.Conv2d
卷积应用一个小尺寸的滤波器(kernel),在图像上滑动到所有位置。在每个位置,它计算覆盖像素的加权和。每个滤波器学习检测一个空间模式——边缘、角点、纹理。
nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0)
in_channels:输入通道数(MNIST 为 1,CIFAR 为 3)。out_channels:学习的滤波器数(= 输出通道数)。kernel_size:滤波器大小(通常 3 或 5)。padding=1和kernel_size=3保持 H×W 不变。
形状:。
构件 2:nn.ReLU
如同 MLP 中,我们在每次卷积后插入非线性激活。没有它,堆叠的 Conv2d 会塌缩成单个等价的 Conv2d。ReLU 是标准激活。
nn.ReLU()
构件 3:nn.MaxPool2d
池化减小图像的空间大小同时保留有用信息。MaxPool2d(2) 通过保留每个 2×2 块的最大值,将高度和宽度减半。
nn.MaxPool2d(kernel_size=2)
效果:。通道数不变。
好处:后续层的参数更少、对小平移的稳健性、聚焦于强模式。
构件 4:nn.Flatten + nn.Linear
经过几个 Conv → ReLU → Pool 块后,我们将 4D 张量展平为 2D 以应用密集分类层:
nn.Flatten() # (B, C, H, W) → (B, C*H*W)
nn.Linear(C*H*W, n_classes) # logits
最终 Linear 后没有激活——CrossEntropyLoss 在内部处理 softmax。
典型 CNN 架构
model = nn.Sequential(
nn.Conv2d(1, 8, kernel_size=3, padding=1), # (N, 1, 28, 28) → (N, 8, 28, 28)
nn.ReLU(),
nn.MaxPool2d(2), # → (N, 8, 14, 14)
nn.Flatten(), # → (N, 8*14*14)
nn.Linear(8 * 14 * 14, 10), # → (N, 10) logits
)
堆叠多个块
为了得到更强的模型,堆叠 Conv → ReLU → Pool 块。规则:一层的 out_channels 成为下一层的 in_channels。
nn.Sequential(
nn.Conv2d(3, 16, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # → 16×16
nn.Conv2d(16, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # → 8×8
nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # → 4×4
nn.Flatten(),
nn.Linear(64 * 4 * 4, 10),
)
通道数通常增加(16 → 32 → 64)——以分辨率为代价获得表达力。
nn.Module 语法
对于更丰富的架构,定义一个继承 nn.Module 的类:
class SimpleCNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 16, 3, padding=1)
self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
self.pool = nn.MaxPool2d(2)
self.relu = nn.ReLU()
self.fc = nn.Linear(32 * 7 * 7, 10)
def forward(self, x):
x = self.pool(self.relu(self.conv1(x)))
x = self.pool(self.relu(self.conv2(x)))
x = torch.flatten(x, start_dim=1)
x = self.fc(x)
return x
这是实践中的标准模式。它允许分支、残差连接、层共享等。
使用 GPU
PyTorch 从不自动移动东西。一切必须显式。
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
在训练循环中,在 fit 时移动批次:
for Xb, yb in train_loader:
Xb = Xb.to(device)
yb = yb.to(device)
optimizer.zero_grad()
logits = model(Xb)
loss = criterion(logits, yb)
loss.backward()
optimizer.step()
最后,要使用 scikit-learn 或 matplotlib 计算指标,将张量带回 CPU:
y_hat_np = y_hat.cpu().numpy()