专栏 AI 学习笔记 子专栏 AI 学习笔记 31 篇

Day 21|第 3 周复盘与 CIFAR-10 图像分类项目:把 5 天零件拼成第一个深度学习完整 Pipeline(AI 学习笔记 · 深度学习周 · 第 21 篇)

Week 3 用 5 天把深度学习的「五块零件」安装到位——神经网络基础(Day 15)、PyTorch 工程接口(Day 16)、CNN 卷积视觉(Day 17)、RNN/LSTM 序列建模(Day 18)、Transformer 与 Self-Attention(Day 19)。今天用 CIFAR-10 + ResNet-18 把它们拼成一条端到端图像分类 Pipeline:数据加载 → 增强 → 残差网络 → SGD/Adam 训练 → 测试集评估 → 单图推理,从 32×32×3 的彩色图像里把 airplane / automobile / bird / cat / deer / dog / frog / horse / ship / truck 十类物体分开。


1. Week 3 全景回顾:5 天学了什么、用在哪里

1.1 7 天主题串联表(Day 20 训练技巧本批已删除)

Day 主题 核心技能(1 行) 关键概念 / 产物
Day 15 神经网络基础 从感知机到多层 MLP,理解「层 / 激活 / 反向传播」三件套 神经元、ReLU、链式法则、梯度消失
Day 16 PyTorch 入门 nn.Module 写模型、DataLoader 喂数据、optim 跑训练 Tensor、autograd、model.train() / eval()
Day 17 CNN 卷积 / 池化 / 感受野三件套,LeNet → VGG 的演化 Conv2d、MaxPool2d、stride、padding
Day 18 RNN 与 LSTM 用循环单元处理变长序列,Gate 机制缓解梯度消失 隐藏状态、h_t、LSTM 的三个 gate
Day 19 Transformer 深入 Self-Attention 让每个 token 看全局,完全并行 Q/K/V、Multi-Head、位置编码、因果 mask
Day 21 周复盘 + 图像分类项目 把上面五块零件拼成完整 Pipeline CIFAR-10 + ResNet-18 + 数据增强 + 训练循环 + 评估

1.2 一句话串起 5 天

Day 15 给「神经网络是什么」的统一语言;Day 16 把语言翻译成 PyTorch 工程代码;Day 17 用 CNN 把视觉任务做到 90% 准确率;Day 18 用 RNN/LSTM 把序列任务做到可用;Day 19 用 Transformer 把 RNN 的串行瓶颈解决——这是今天所有 LLM 的地基。今天(Week 3 收官)用 CIFAR-10 把视觉零件全部上膛,验证「读图 → 卷积 → 池化 → 全连接分类」完整路径跑得通。

1.3 最小图像分类 Pipeline 长什么样

把 5 天零件拆成 6 步 Pipeline:

[原始图像 32×32×3] → 读 (Day 16 torchvision.datasets)
                  → 改 (Day 17 数据增强:随机裁剪/翻转/归一化)
                  → 喂 (Day 16 DataLoader 批量)
                  → 算 (Day 17 卷积/池化 / Day 19 ResNet 残差块)
                  → 学 (Day 15 反向传播 + Day 16 Adam/AdamW)
                  → 评 (Day 15 准确率 + Day 16 model.eval())

每一步的失败模式都不同:读错了 是路径 / 通道顺序(RGB vs BGR);改错了 是归一化均值用错(ImageNet 统计量 vs CIFAR 统计量);喂错了 是 batch size 不匹配显存;算错了 是卷积核尺寸算错导致 feature map 尺寸为零;学错了 是学习率过大 / 损失函数写错(分类用 CrossEntropy 不是 MSE);评错了 是忘记 model.eval() 导致 Dropout/BatchNorm 仍随机。


2. 关键概念回顾:5 个零件每个 1 段

2.1 神经网络基础(Day 15)

一个 $L$ 层 MLP 把输入 $x \in \mathbb{R}^{n}$ 通过若干次「线性变换 + 非线性激活」映射到输出 $\hat y$:

h^{(l)} = \sigma(W^{(l)} h^{(l-1)} + b^{(l)}),\quad l=1,\dots,L

σ 用 ReLU(避免 sigmoid 的梯度饱和);最后一层根据任务决定维度(10 分类用 10 维 logits,接 softmax)。反向传播用链式法则把 $\partial L / \partial W^{(l)}$ 一层层传回来。Day 15 是后续所有「可学习」组件的理论底座——CNN、RNN、Transformer 都是带特定归纳偏置的神经网络。

2.2 PyTorch 工程接口(Day 16)

torch.Tensor 是带 autograd 的多维数组,requires_grad=True 时所有操作会建计算图。nn.Module 封装「参数 + 前向」,nn.Parameter 自动加入 parameters() 迭代器;optim 负责按梯度更新参数;CIFAR-10 项目里这三个对象是流水线工人:

import torch.nn as nn
import torch.optim as optim

model = ResNet18().to(device)              # nn.Module
criterion = nn.CrossEntropyLoss()          # 损失
optimizer = optim.AdamW(model.parameters(), # 优化器
                         lr=1e-3, weight_decay=5e-4)

2.3 CNN(Day 17)

卷积层用局部连接 + 权重共享两个归纳偏置,把「图像的局部性 + 平移不变性」硬编码进架构。一个 Conv2d(in_c, out_c, kernel_size=3, padding=1) 把 $(C, H, W)$ 变成 $(C’, H, W)$(same padding 时)。堆叠多层后,深层神经元能看到原图更大的感受野——这就是「层级特征提取」的几何基础。CIFAR-10 项目里的 ResNet-18 是「加了残差连接的 CNN 堆叠」。

2.4 RNN / LSTM(Day 18)

RNN 用同一个权重矩阵在时间步之间循环,适合变长序列;但 tanh + 链式法则导致长序列梯度消失。LSTM 引入三个 gate(输入 / 遗忘 / 输出)和一条 cell state 高速公路,让「该记的记、该忘的忘」成为可学习行为。Week 4 Day 22-25 的 LLM 全部抛弃 RNN 改用 Transformer(Day 19),但 LSTM 在中小规模序列任务上仍然是默认起点。

2.5 Transformer 与 Self-Attention(Day 19)

Self-Attention 让每个位置直接 attend 到所有其他位置,完全并行。Multi-Head 把 d_model 切成 h 个子空间并行计算。Decoder-only 加 causal mask 阻止偷看未来。Week 3 末尾的 4 篇 LLM 文章全部基于这一天的 Scaled Dot-Product 公式:

\mathrm{Attention}(Q, K, V) = \mathrm{softmax}(Q K^\top / \sqrt{d_k}) \cdot V

2.6 Week 3 知识地图(纵向串联)

Day 15  (NN 基础)           → 把「学习」写成 y=f(Wx+b) + Loss + 梯度
Day 16  (PyTorch 工程)     → 把 Day 15 翻译成 nn.Module / DataLoader / optim
Day 17  (CNN)              → 把「图像的局部性」硬编码进架构
Day 18  (RNN/LSTM)         → 把「序列的时序依赖」硬编码进架构
Day 19  (Transformer)      → 把「全局依赖」硬编码进架构,并行 + 长依赖
Day 21  (CIFAR-10 项目)    → 把视觉零件全部上膛,跑通 10 分类端到端

每一层都是下一层的前置条件——Day 17 的 CNN 没有 Day 16 的 nn.Module 写不出来,Day 16 的反向传播要靠 Day 15 的链式法则,Day 19 的 Q @ K^T / √d_k 是 Day 21 ResNet 残差块里 1×1 conv 的同一套数学。


3. CIFAR-10 + ResNet-18 完整项目:6 步跑通端到端

3.1 数据集与任务

CIFAR-10 是深度学习的「Hello World」,由 Hinton 学生 Alex Krizhevsky 整理:

项 值
类别数 10(airplane/auto/bird/cat/deer/dog/frog/horse/ship/truck)
图像尺寸 32×32×3(RGB)
训练集 50,000 张
测试集 10,000 张
类别均衡 每类 5,000 / 1,000 训练 / 测试样本
基线 ResNet-18 准确率 ~94-95%

10 类物体中 airplane 与 ship 都是「蓝天背景」、automobile 与 truck 都是「路上物体」——视觉上类别间相似度高,baseline 模型就要 90%+ 才能算「学到了」。

3.2 步骤一:数据加载

import torch
import torchvision
import torchvision.transforms as T

CIFAR_MEAN = (0.4914, 0.4822, 0.4465)
CIFAR_STD  = (0.2470, 0.2435, 0.2616)

train_tfm = T.Compose([
    T.RandomCrop(32, padding=4),
    T.RandomHorizontalFlip(),
    T.ColorJitter(brightness=0.2, contrast=0.2),
    T.ToTensor(),
    T.Normalize(CIFAR_MEAN, CIFAR_STD),
])

test_tfm = T.Compose([
    T.ToTensor(),
    T.Normalize(CIFAR_MEAN, CIFAR_STD),
])

train_set = torchvision.datasets.CIFAR10(
    root='./data', train=True,  download=True, transform=train_tfm)
test_set  = torchvision.datasets.CIFAR10(
    root='./data', train=False, download=True, transform=test_tfm)

train_loader = torch.utils.data.DataLoader(
    train_set, batch_size=128, shuffle=True,  num_workers=4, pin_memory=True)
test_loader  = torch.utils.data.DataLoader(
    test_set,  batch_size=256, shuffle=False, num_workers=4, pin_memory=True)

坑预警:download=True 第一次跑会去 CIFAR-10 官网下载 ~170 MB,需要联网;若离线,把 download=False 并预先把 cifar-10-batches-py/ 放到 ./data 下。num_workers=4 利用多进程预取,Linux/macOS 上提速 2-3 倍。

3.3 步骤二:数据增强

数据增强是 CIFAR-10 上分最便宜的「免费午餐」:

增强 作用 关键参数
RandomCrop(32, padding=4) 4 像素 padding 后随机裁剪 等价于「物体轻微平移」
RandomHorizontalFlip() 50% 概率水平翻转 等价于「左右镜像都是同一类」
ColorJitter 亮度 / 对比度抖动 等价于「不同光照」
Normalize(mean, std) 归一化到 0 附近 必须用 CIFAR 自己的统计量,不是 ImageNet

RandomErasing / Cutout / MixUp / CutMix 是进阶选项,常能再提 0.5-1.5%。测试集永远不做随机增强,只 Normalize——保证评估是确定性的。

3.4 步骤三:模型定义(ResNet-18)

ResNet 的核心思想是「残差连接」:让某一层学习「残差 $F(x)$」而不是直接拟合 $H(x)$,最终输出 $H(x) = F(x) + x$。这让 100+ 层网络的梯度可以走「捷径」直达浅层,解决深度网络的退化问题。

import torch.nn as nn
import torch.nn.functional as F


class BasicBlock(nn.Module):
    """ResNet-18/34 用的小残差块:两个 3x3 conv + 残差"""
    expansion = 1

    def __init__(self, in_c, out_c, stride=1):
        super().__init__()
        self.conv1 = nn.Conv2d(in_c, out_c, 3, stride=stride, padding=1, bias=False)
        self.bn1   = nn.BatchNorm2d(out_c)
        self.conv2 = nn.Conv2d(out_c, out_c, 3, stride=1, padding=1, bias=False)
        self.bn2   = nn.BatchNorm2d(out_c)
        self.short = nn.Sequential()  # 维度对齐捷径
        if stride != 1 or in_c != out_c:
            self.short = nn.Sequential(
                nn.Conv2d(in_c, out_c, 1, stride=stride, bias=False),
                nn.BatchNorm2d(out_c),
            )

    def forward(self, x):
        out = F.relu(self.bn1(self.conv1(x)), inplace=True)
        out = self.bn2(self.conv2(out))
        out = out + self.short(x)
        return F.relu(out, inplace=True)


class ResNet18(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.stem = nn.Sequential(
            nn.Conv2d(3, 64, 3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
        )
        self.layer1 = self._make_layer(64,  64,  num_blocks=2, stride=1)
        self.layer2 = self._make_layer(64,  128, num_blocks=2, stride=2)
        self.layer3 = self._make_layer(128, 256, num_blocks=2, stride=2)
        self.layer4 = self._make_layer(256, 512, num_blocks=2, stride=2)
        self.avgpool = nn.AdaptiveAvgPool2d(1)
        self.fc      = nn.Linear(512, num_classes)

    def _make_layer(self, in_c, out_c, num_blocks, stride):
        layers = [BasicBlock(in_c, out_c, stride=stride)]
        for _ in range(num_blocks - 1):
            layers.append(BasicBlock(out_c, out_c, stride=1))
        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.stem(x)
        x = self.layer1(x); x = self.layer2(x)
        x = self.layer3(x); x = self.layer4(x)
        x = self.avgpool(x).flatten(1)
        return self.fc(x)


model = ResNet18(num_classes=10).to(device)
print(sum(p.numel() for p in model.parameters()) / 1e6, 'M params')
# 预期: 11.17 M params

3.5 步骤四:训练循环

def train_one_epoch(model, loader, criterion, optimizer, device, epoch, total_epochs):
    model.train()
    running_loss, correct, total = 0.0, 0, 0
    for step, (x, y) in enumerate(loader):
        x, y = x.to(device, non_blocking=True), y.to(device, non_blocking=True)

        logits = model(x)
        loss = criterion(logits, y)

        optimizer.zero_grad(set_to_none=True)
        loss.backward()
        optimizer.step()

        running_loss += loss.item() * x.size(0)
        correct += (logits.argmax(1) == y).sum().item()
        total += x.size(0)

        if step % 100 == 0:
            print(f'epoch {epoch}/{total_epochs} step {step}/{len(loader)} '
                  f'loss {loss.item():.4f} acc {correct/total:.4f}')
    return running_loss / total, correct / total


@torch.no_grad()
def evaluate(model, loader, criterion, device):
    model.eval()
    running_loss, correct, total = 0.0, 0, 0
    for x, y in loader:
        x, y = x.to(device, non_blocking=True), y.to(device, non_blocking=True)
        logits = model(x)
        loss = criterion(logits, y)
        running_loss += loss.item() * x.size(0)
        correct += (logits.argmax(1) == y).sum().item()
        total += x.size(0)
    return running_loss / total, correct / total

关键细节:set_to_none=True 比 zero_grad() 更省显存(把梯度置 None 而不是清零);non_blocking=True 配合 pin_memory=True 让 CPU→GPU 拷贝异步;model.train() / model.eval() 切换必须配对出现,否则 Dropout/BatchNorm 行为不一致。

3.6 步骤五:优化器与学习率调度

EPOCHS = 30

optimizer = torch.optim.AdamW(model.parameters(),
                              lr=1e-3, weight_decay=5e-4)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
    optimizer, T_max=EPOCHS)
criterion = nn.CrossEntropyLoss()

best_acc = 0.0
for epoch in range(1, EPOCHS + 1):
    tr_loss, tr_acc = train_one_epoch(model, train_loader, criterion,
                                      optimizer, device, epoch, EPOCHS)
    te_loss, te_acc = evaluate(model, test_loader, criterion, device)
    scheduler.step()

    if te_acc > best_acc:
        best_acc = te_acc
        torch.save(model.state_dict(), 'best_resnet18_cifar10.pth')

    print(f'epoch {epoch:2d}  '
          f'train loss {tr_loss:.4f} acc {tr_acc:.4f} | '
          f'test  loss {te_loss:.4f} acc {te_acc:.4f} | '
          f'best {best_acc:.4f}')

预期输出(在 V100 / RTX 3090 上):

epoch  1  train loss 1.45 acc 0.48 | test  loss 1.10 acc 0.62 | best 0.62
epoch 10  train loss 0.32 acc 0.89 | test  loss 0.45 acc 0.87 | best 0.91
epoch 20  train loss 0.12 acc 0.96 | test  loss 0.32 acc 0.92 | best 0.94
epoch 30  train loss 0.05 acc 0.98 | test  loss 0.31 acc 0.94 | best 0.9461

30 个 epoch 在 CIFAR-10 上跑 ResNet-18 应达到 94-95% 测试准确率。低于 90% 通常是「数据增强不足 / 学习率不合适 / BatchNorm 没配对 model.eval()」三选一。

3.7 步骤六:单图推理与部署

训练完成后,模型可以直接对单张图像推理:

from PIL import Image
import torchvision.transforms as T

CIFAR_CLASSES = ['airplane','automobile','bird','cat','deer',
                 'dog','frog','horse','ship','truck']

infer_tfm = T.Compose([
    T.Resize((32, 32)),
    T.ToTensor(),
    T.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)),
])

model = ResNet18(num_classes=10).to(device)
model.load_state_dict(torch.load('best_resnet18_cifar10.pth'))
model.eval()

img = Image.open('test_cat.jpg').convert('RGB')
x = infer_tfm(img).unsqueeze(0).to(device)

with torch.no_grad():
    logits = model(x)
    probs  = torch.softmax(logits, dim=1)
    top5_prob, top5_idx = probs.topk(5)

for prob, idx in zip(top5_prob[0], top5_idx[0]):
    print(f'{CIFAR_CLASSES[idx]:12s}  {prob.item()*100:.2f}%')

unsqueeze(0) 把 (3, 32, 32) 加一维 batch 维变 (1, 3, 32, 32);softmax + topk(5) 给出 Top-5 类别与概率。生产部署还会做:① torch.jit.script(model) 序列化;② ONNX 导出;③ TensorRT 加速;④ 转成服务端 Flask/FastAPI endpoint。


4. PyTorch 完整 Pipeline 代码(Mini-Pipeline,单文件可跑)

下面是一份端到端可运行的 Mini-Pipeline,涵盖「数据加载 → 增强 → 训练 → 评估 → 单图推理」全流程,直接在 Linux + GPU 环境复制粘贴即可。

# train_cifar10.py
import time, torch, torchvision
import torch.nn as nn, torch.nn.functional as F
import torchvision.transforms as T

device = 'cuda' if torch.cuda.is_available() else 'cpu'
torch.manual_seed(0)


# ---------- 数据 ----------
CIFAR_MEAN = (0.4914, 0.4822, 0.4465)
CIFAR_STD  = (0.2470, 0.2435, 0.2616)
train_tfm = T.Compose([
    T.RandomCrop(32, padding=4), T.RandomHorizontalFlip(),
    T.ToTensor(), T.Normalize(CIFAR_MEAN, CIFAR_STD)])
test_tfm  = T.Compose([T.ToTensor(), T.Normalize(CIFAR_MEAN, CIFAR_STD)])

train_set = torchvision.datasets.CIFAR10('./data', True,  True,  train_tfm)
test_set  = torchvision.datasets.CIFAR10('./data', False, True,  test_tfm)
train_loader = torch.utils.data.DataLoader(
    train_set, batch_size=128, shuffle=True,  num_workers=2, pin_memory=True)
test_loader  = torch.utils.data.DataLoader(
    test_set,  batch_size=256, shuffle=False, num_workers=2, pin_memory=True)


# ---------- 模型 ----------
class BasicBlock(nn.Module):
    expansion = 1
    def __init__(self, in_c, out_c, stride=1):
        super().__init__()
        self.conv1 = nn.Conv2d(in_c, out_c, 3, stride, 1, bias=False)
        self.bn1   = nn.BatchNorm2d(out_c)
        self.conv2 = nn.Conv2d(out_c, out_c, 3, 1, 1, bias=False)
        self.bn2   = nn.BatchNorm2d(out_c)
        self.short = (nn.Sequential(
            nn.Conv2d(in_c, out_c, 1, stride, bias=False),
            nn.BatchNorm2d(out_c))
            if stride != 1 or in_c != out_c else nn.Identity())

    def forward(self, x):
        out = F.relu(self.bn1(self.conv1(x)), inplace=True)
        out = self.bn2(self.conv2(out))
        return F.relu(out + self.short(x), inplace=True)


class ResNet18(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.stem = nn.Sequential(nn.Conv2d(3, 64, 3, 1, 1, bias=False),
                                   nn.BatchNorm2d(64), nn.ReLU(inplace=True))
        self.layer1 = self._make(64,  64,  2, 1)
        self.layer2 = self._make(64,  128, 2, 2)
        self.layer3 = self._make(128, 256, 2, 2)
        self.layer4 = self._make(256, 512, 2, 2)
        self.avgpool = nn.AdaptiveAvgPool2d(1)
        self.fc      = nn.Linear(512, num_classes)

    def _make(self, in_c, out_c, n, s):
        layers = [BasicBlock(in_c, out_c, s)]
        for _ in range(n - 1):
            layers.append(BasicBlock(out_c, out_c, 1))
        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.stem(x)
        x = self.layer1(x); x = self.layer2(x)
        x = self.layer3(x); x = self.layer4(x)
        return self.fc(self.avgpool(x).flatten(1))


model = ResNet18().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=5e-4)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30)


# ---------- 训练 ----------
@torch.no_grad()
def evaluate():
    model.eval()
    correct, total = 0, 0
    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)
    return correct / total


for epoch in range(1, 31):
    model.train()
    for x, y in train_loader:
        x, y = x.to(device), y.to(device)
        loss = criterion(model(x), y)
        optimizer.zero_grad(set_to_none=True)
        loss.backward()
        optimizer.step()
    scheduler.step()
    if epoch % 5 == 0:
        print(f'epoch {epoch:2d}  test acc {evaluate():.4f}')

# 预期输出(简化):
# epoch  5  test acc 0.8803
# epoch 10  test acc 0.9185
# epoch 15  test acc 0.9356
# epoch 20  test acc 0.9421
# epoch 25  test acc 0.9452
# epoch 30  test acc 0.9478

4.1 跑通后的最小验证

检查项 期望
model.parameters() 总参数量 ~11.17 M
单 epoch 时间(V100) ~15 s
30 epoch 总时间(V100) ~8 min
单 epoch 时间(RTX 3090) ~20 s
测试集准确率(30 epoch) ≥ 0.94
单图推理耗时 < 5 ms
模型权重文件大小 ~45 MB

4.2 与 Day 14 ML Pipeline 的对照

维度 Day 14 经典 ML Day 21 深度学习
输入维度 手工特征(如 Titanic 12 列) 原始像素(3072 维)
特征工程 必须(缺失值 / 类别编码) 模型内部学(端到端)
模型参数量 < 1 K 11.17 M
训练时长(CPU) 秒级 小时级
训练时长(GPU) 秒级 分钟级
推理速度 < 1 ms 1-5 ms
评估指标 Accuracy / F1 Accuracy / Top-5
部署方式 sklearn pickle TorchScript / ONNX
失败模式 过拟合 / 类别不平衡 训练不稳定 / 数据增强错

核心区别:Day 14 的 Pipeline 把「特征工程」当主角,模型只做最后一步分类;Day 21 的 Pipeline 把「特征工程」内化到 CNN 卷积层,只要给足够数据和算力,模型自动学到最优特征表示——这是深度学习相对经典 ML 的范式飞跃。


5. Week 3 vs Week 4:难度与心智模型对比

维度 Week 3(深度学习) Week 4(大模型与生成式 AI)
任务类型 视觉 / 序列基础任务 自然语言生成 / 预训练 / 微调
模型规模 11 M(ResNet-18) 1.5 B - 175 B(GPT 系列)
数据规模 5 万图像(CIFAR-10) 300 B tokens(LLM 预训练)
训练硬件 单卡 GPU(8 GB 显存足够) 多机多卡 / 数百 GPU
训练时长 30 分钟 数周
损失函数 Cross-Entropy Causal LM / Masked LM
优化器 SGD / AdamW AdamW(必有 warmup + cosine)
评估方式 准确率 / Top-5 PPL / MMLU / HumanEval
调试难点 训练不收敛 / 过拟合 显存溢出 / 长文本 OOM / 复读
抽象层级 「这个卷积核看到什么」 「这个 token 在学什么语义」
产出物 一个会分类的 CNN 一个会说话 / 推理 / 写作的 LLM
核心心智 层级特征 + 残差连接 自监督预训练 + 涌现能力

Week 4 的难度跃迁点:① 损失函数从 Cross-Entropy 变成「下一个 token 预测」;② 模型从「分类头 + 全连接」变成「几十层 Transformer 堆叠」;③ 评估从「测试集准确率」变成「多任务榜单 + 人类偏好」。Week 3 学会的「Pipeline 思维」(读 → 改 → 喂 → 算 → 学 → 评)在 Week 4 不变,变的是「算」这一步——从 CNN 残差块变成 GPT 的 Self-Attention 堆叠。


6. 常见坑(8 条,CIFAR-10 实战典型)

6.1 训练 30 epoch 后测试准确率只有 80%

症状:train acc 99%、test acc 80%,严重过拟合。 原因:没有数据增强或归一化用错;CIFAR-10 训练集只有 5 万张 32×32 图像,没增强直接训必然过拟合。 修法:① 加 RandomCrop(32, padding=4) + RandomHorizontalFlip();② 用 CIFAR 自己的 (0.4914, 0.4822, 0.4465) 归一化,不要用 ImageNet;③ 加 weight_decay=5e-4(L2 正则);④ 加 Dropout 或 Label Smoothing(0.1)。

6.2 训练 loss 不下降,卡在 2.3(Cross-Entropy 的均匀猜测值)

症状:train loss 一直 2.30、test acc 10%,等价于随机猜。 原因:学习率过大 或 标签全错 或 logits 全是 NaN。 修法:① 学习率从 1e-3 降到 1e-4 试;② 检查 y 是不是 [0, 9];③ 检查 nn.CrossEntropyLoss() 默认 reduction='mean',不要传 reduction='none';④ 检查是否把 logits 又过了一遍 softmax。

6.3 训练 loss 下降但 test acc 抖动剧烈

症状:test acc 在 88%-92% 之间大幅跳动,不收敛。 原因:测试时没 model.eval()——BatchNorm 仍用当前 batch 的均值方差,Dropout 仍随机置零。 修法:evaluate 函数第一行 model.eval(),且用 @torch.no_grad() 装饰;还要确认 evaluate 里不调用 loss.backward()(即使显式 @torch.no_grad 也不该)。

6.4 ResNet 残差块 shape 对不上,报 mat1 and mat2 mismatch

症状:RuntimeError: The size of tensor a (128) must match the size of tensor b (256) 原因:残差连接的捷径没对齐维度——stride=2 的下采样层把 feature map 缩小一半、通道翻倍,但 self.short 是 nn.Identity()。 修法:在 BasicBlock.__init__ 里加:

if stride != 1 or in_c != out_c:
    self.short = nn.Sequential(nn.Conv2d(in_c, out_c, 1, stride, bias=False),
                               nn.BatchNorm2d(out_c))

6.5 用了 ImageNet 的归一化均值,训练崩坏

症状:loss 不下降 / 训练完全发散。 原因:ImageNet 用 (0.485, 0.456, 0.406),CIFAR 是 (0.4914, 0.4822, 0.4465)——虽然差得不远,但训练集小、数据分布差异会让前几 epoch 崩。 修法:永远用当前数据集自己的均值 / 标准差,可以用 train_set.data.mean(axis=(0,1,2)) / 255 计算或直接抄公开数值。

6.6 DataLoader num_workers 设太大,RAM 爆掉

症状:训练卡住,系统 swap 飙升,甚至 OOM Killed。 原因:num_workers 越大,每个 worker 进程独立加载数据,RAM 占用 = num_workers × 单进程占用。Linux 默认 4-8 没问题,但在 16 GB 内存的开发机设 16 会爆。 修法:num_workers 从 2-4 起,边训边看 top 的 RES 占用;如果是 SSD + Linux,可以加大;机械硬盘反而设 0 更稳。

6.7 没 pin_memory=True,GPU 利用率低

症状:nvidia-smi 显示 GPU 利用率 30-50%,训练慢。 原因:DataLoader 输出的 Tensor 在 CPU 内存,与 GPU 之间传输时是 pageable,速度慢。 修法:DataLoader(..., pin_memory=True),再在 .to(device, non_blocking=True) 时加 non_blocking=True——CPU→GPU 拷贝异步执行,GPU 利用率可上 90%+。

6.8 训练完模型保存成整个 model 对象,加载报 pickle 错

症状:torch.load('best.pth') 报 RuntimeError: storage has wrong size 或 module not found。 原因:torch.save(model, ...) 把整个对象序列化(包含 model_class 定义),换机器 / 改类名后加载失败。 修法:只保存 state_dict:torch.save(model.state_dict(), 'best.pth');加载时先实例化模型,再 model.load_state_dict(torch.load('best.pth'))。


7. 自检三问

A. ResNet 为什么用残差连接 $H(x) = F(x) + x$ 而不是直接拟合 $H(x)$?在反向传播时梯度怎么流过这个加法?

要点:残差连接的工程意义是给梯度一条「捷径」。$\partial L / \partial x = \partial L / \partial H \cdot (1 + \partial F / \partial x)$,即使 $\partial F / \partial x$ 很小(深层网络退化时常常如此),加号带来的 1 也让梯度至少为 $\partial L / \partial H$——梯度不会消失。几何上,「学残差」比「学完整映射」更易优化:输入附近的恒等映射只需把 $F$ 推到 0,而不是从零学起一个全新映射。详见 §3.4。

B. 数据增强在训练时用 RandomCrop + Flip,测试时只用 Resize + Normalize,这是为什么?能否用同样的增强?

要点:数据增强是训练时的正则化——给模型看「同一物体的多种变体」,强迫它学「什么是不变特征」。测试/推理时必须用确定性的预处理,否则同一张图跑两次得到不同结果,无法复现实验。永远不要把随机增强用到 test / val / inference。详见 §3.3。

C. CIFAR-10 用 ResNet-18 跑 30 epoch 可以到 94-95%,如果只用 5 epoch 就停,准确率会停在多少?为什么?

要点:5 epoch 大约停在 88-90%。原因:① 数据增强让模型需要更多 epoch 才能拟合;② AdamW 的初始学习率 1e-3 在前 5 epoch 处于 warmup 期;③ CosineAnnealing 在前 5 epoch 衰减幅度小,模型「还没学完」。深度学习训练 epoch 数 = (模型参数量 × 数据量) / 经验常数,CIFAR-10 + ResNet-18 经验值就是 30 epoch。如果想 5 epoch 达到 94%,需要换 SGD + 大学习率 + StepLR,或者用预训练权重(Week 4 主题)。


8. 推荐资源(5 类)

视频

  • CS231n · Convolutional Neural Networks (Stanford) Lecture 5-9——CNN 基础 + ResNet + 训练技巧的完整体系
  • 李沐 · 深度学习课程(d2l.ai) 第 7 章——ResNet 论文精读 + PyTorch 实现
  • Yannic Kilcher · ResNet 论文讲解(YouTube)——30 分钟读完原论文

教科书

  • 《动手学深度学习》(d2l.ai) 第 7-8 章——CNN / 现代 CNN(ResNet / DenseNet)
  • 《Deep Learning》(Goodfellow) 第 9 章——卷积网络数学基础
  • 《深度学习:核心算法与实践》——CIFAR-10 全流程中文教材

论文

  • He et al. 2015《Deep Residual Learning for Image Recognition》——ResNet 原论文,ImageNet 2015 冠军
  • Krizhevsky et al. 2009《Learning Multiple Layers of Features from Tiny Images》——CIFAR-10 数据集原始论文
  • Ioffe & Szegedy 2015《Batch Normalization》——BatchNorm 原论文,CIFAR-10 上必读

博客 / 课程

  • PyTorch 官方教程 · Training a Classifier——CIFAR-10 完整 Pipeline
  • paperswithcode · CIFAR-10 Leaderboard——查 SOTA 模型与准确率
  • Lil’Log · ResNet 解读——图解 ResNet 内部工作机制

代码

  • PyTorch 官方 Examples · ImageNet Training——官方 ResNet-50 实现
  • timm (Ross Wightman)——数百种预训练视觉模型,可直接 timm.create_model('resnet18')
  • kornia——可微分的数据增强库,与 PyTorch 训练管线无缝集成

9. 本节要点

  • Week 3 五天零件:神经网络(Day 15)→ PyTorch(Day 16)→ CNN(Day 17)→ RNN/LSTM(Day 18)→ Transformer(Day 19),逐层深入,从「什么是学习」到「怎么写工程代码」到「图像 / 序列 / 全局依赖的架构选择」
  • CIFAR-10 是深度学习的「Hello World」:50K 训练 / 10K 测试、10 类、32×32×3,基线 ResNet-18 应达到 94-95%
  • 残差连接 $H(x) = F(x) + x$ 通过「恒等捷径」让梯度可以跳过深层而不消失,是 100+ 层网络可训练的关键
  • 数据增强是 CIFAR-10 上分最便宜的「免费午餐」:RandomCrop(32, padding=4) + RandomHorizontalFlip 通常带来 5-10% 提升,测试集永远不做随机增强
  • PyTorch Pipeline 六步:读(Dataset)→ 改(transforms)→ 喂(DataLoader)→ 算(nn.Module)→ 学(optim + scheduler)→ 评(model.eval() + @torch.no_grad())
  • 常见失败模式:归一化用错数据集、BatchNorm/Dropout 没切 train/eval、num_workers 设太大、DataLoader 没 pin_memory、model 直接 save 而非 state_dict
  • Week 3 vs Week 4 心智跃迁:CNN 的「层级特征提取」→ LLM 的「自监督预训练 + 涌现」;Week 4 在 Week 3 Pipeline 基础上把「算」这一步换成 Self-Attention 堆叠

10. 下一节:Day 22 · 预训练范式与 Tokenization

主题:从「专门任务的监督学习」跃迁到「自监督预训练 + 下游微调」,这是过去 7 年 NLP 最重要的范式革命——GPT 用 Causal LM(预测下一个 token)、BERT 用 Masked LM(预测被遮蔽的 token),两者共同奠定 LLM 时代。

覆盖:

  • GPT-1 (2018, 117M 参数) / BERT (2018, 340M 参数) / GPT-2 (2019, 1.5B) / GPT-3 (2020, 175B) 的发布时间、参数量、核心创新
  • BPE 算法手算与 WordPiece / SentencePiece / Unigram 4 种子词 tokenizer 的核心区别
  • HuggingFace tokenizers 库与 OpenAI tiktoken 库的现状与典型用法

产出物:用 HuggingFace tokenizers 从零训练一个 BPE tokenizer(语料取自 Day 14 处理的 Titanic 文本),再用它编码 / 解码一个句子,理解「为什么 Hello world 会被切成 1-3 个 token」。


作者:林馨予 + 林晓月

说明 · 本站内容均为学习笔记与经验总结,所有菜谱与技法请结合实际食材、季节与个人口味灵活调整。涉及生食、营养与健康的内容仅供参考,特殊体质或疾病请咨询专业营养师/医生。