Aller au contenu principal

深度学习 3 — 卷积网络 (1/3)

:::tip Kaggle 笔记本 本章的完整可执行代码在 Kaggle 上:打开 →

法语和英语版本可在首页查看。 :::

第一次接触卷积神经网络(CNN)。本章的主要概念性挑战:PyTorch 的 BCHW 张量约定。一旦理解这个规则,其余内容自然跟上。

为什么要学这一章?

您将学到:

  • 为什么用 CNN 而不是 MLP 处理图像;
  • PyTorch 对图像期望的 (B, C, H, W) 轴心规则
  • 构件 Conv2dMaxPool2dFlatten
  • 如何堆叠多个卷积层;
  • nn.Module 语法(编写模型的专业方式);
  • 使用 .to(device) 使用 GPU

为什么用 CNN?

MLP 将图像视为 1D 向量(将 (28,28)(28, 28) 展平为 (784,)(784,))。三个问题:

  1. 空间邻近性的丢失:对于 MLP,像素 (1,1)(1,1) 和像素 (1,2)(1,2) 与像素 (1,1)(1,1) 和像素 (28,28)(28,28) 一样陌生。
  2. 巨大的参数数量:对于 224×224 RGB 图像,约 150,000 个输入特征。100 个隐藏神经元,仅第一层就约 1500 万个权重。
  3. 没有平移不变性:如果对象移动几个像素,网络就不再识别它。

CNN 通过利用两个想法解决这三个问题:

  • 局部连接:神经元只看小邻域(3×3 像素),不是整个图像。
  • 权重共享:同一个 3×3 滤波器应用于所有位置。参数大大减少,平移不变性免费。

黄金法则:(B, C, H, W)

PyTorch 在卷积时总是期望 4D 张量 (batch, channels, height, width)。就是这样。

维度细分:

维度含义MNISTCIFAR-10
B并行处理的图像数6464
C通道数1(灰度)3(RGB)
H像素高度2832
W像素宽度2832

这种约定称为 BCHW(或 NCHW)。出于 GPU 性能原因,PyTorch 强制使用它。

:::warning matplotlib 使用不同的约定 matplotlib(和一般的 numpy)期望图像采用 HWCheight, 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=1kernel_size=3 保持 H×W 不变。

形状:(B,Cin,H,W)(B,Cout,Hout,Wout)(B, C_\text{in}, H, W) \to (B, C_\text{out}, H_\text{out}, W_\text{out})

构件 2:nn.ReLU

如同 MLP 中,我们在每次卷积后插入非线性激活。没有它,堆叠的 Conv2d 会塌缩成单个等价的 Conv2d。ReLU 是标准激活。

nn.ReLU()

构件 3:nn.MaxPool2d

池化减小图像的空间大小同时保留有用信息。MaxPool2d(2) 通过保留每个 2×2 块的最大值,将高度和宽度减半。

nn.MaxPool2d(kernel_size=2)

效果:(B,C,H,W)(B,C,H/2,W/2)(B, C, H, W) \to (B, C, H/2, W/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()

Kaggle 上的完整笔记本(可分叉)→