概述
第 20 篇已经用 Paddle 构建了一个基础 CNN。那一篇重点是理解 Conv2D、MaxPool2D、Flatten 和 Linear 如何组成图像分类模型。本篇继续做完整图像分类实战:先用 MNIST 跑通灰度图分类,再迁移到 CIFAR-10 彩色图分类。
这两个数据集非常适合入门:
| 数据集 | 图像类型 | 类别数 | 典型输入 shape | 难度 |
|---|---|---|---|---|
| MNIST | 灰度手写数字 | 10 | [N, 1, 28, 28] |
低 |
| CIFAR-10 | 彩色自然图像 | 10 | [N, 3, 32, 32] |
中 |
从 MNIST 到 CIFAR-10,最重要的变化是:
- 输入通道从 1 变成 3。
- 图像内容从简单数字变成自然物体。
- 数据增强更重要。
- CNN 需要更强表达能力。
- 验证指标更容易暴露模型容量不足。
读完本文,你应该能用 Paddle 原生 API 训练 MNIST 和 CIFAR-10 分类模型,并理解灰度图和彩色图训练流程的差异。
图像分类流程总览
图像分类训练流程可以抽象为:
text
Dataset
|
Transform
|
DataLoader
|
CNN model
|
logits
|
cross_entropy
|
backward + optimizer
|
evaluate accuracy
对应 Paddle 组件:
| 环节 | Paddle 组件 |
|---|---|
| 数据集 | paddle.vision.datasets.MNIST、paddle.vision.datasets.Cifar10 |
| 预处理 | paddle.vision.transforms |
| 批读取 | paddle.io.DataLoader |
| 模型 | paddle.nn.Layer |
| 损失 | paddle.nn.functional.cross_entropy |
| 优化器 | paddle.optimizer.Adam |
| 评估 | 手写 accuracy |
入门阶段建议先完整跑通 MNIST,再进入 CIFAR-10。
MNIST:准备数据
MNIST 是灰度手写数字数据集,类别为 0 到 9。
python
import paddle
from paddle.vision import datasets, transforms
mnist_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(mean=[0.5], std=[0.5]),
])
train_dataset = datasets.MNIST(mode="train", transform=mnist_transform)
test_dataset = datasets.MNIST(mode="test", transform=mnist_transform)
train_loader = paddle.io.DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = paddle.io.DataLoader(test_dataset, batch_size=128, shuffle=False)
检查一个 batch:
python
images, labels = next(iter(train_loader))
print(images.shape, images.dtype)
print(labels.shape, labels.dtype)
期望:
text
images: [64, 1, 28, 28]
labels: [64]
这里 Normalize(mean=[0.5], std=[0.5]) 用于把输入大致映射到更适合训练的范围。灰度图只有 1 个通道,因此 mean 和 std 都只写 1 个值。
MNIST:定义 CNN 模型
python
import paddle.nn as nn
import paddle.nn.functional as F
class MnistCNN(nn.Layer):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2D(1, 16, kernel_size=3, padding=1)
self.conv2 = nn.Conv2D(16, 32, kernel_size=3, padding=1)
self.pool = nn.MaxPool2D(kernel_size=2, stride=2)
self.flatten = nn.Flatten()
self.fc = nn.Linear(32 * 7 * 7, 10)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = self.flatten(x)
return self.fc(x)
shape 推导:
text
[N, 1, 28, 28]
-> Conv2D(1, 16, padding=1)
[N, 16, 28, 28]
-> MaxPool2D(2)
[N, 16, 14, 14]
-> Conv2D(16, 32, padding=1)
[N, 32, 14, 14]
-> MaxPool2D(2)
[N, 32, 7, 7]
-> Flatten
[N, 1568]
-> Linear
[N, 10]
训练前先验证:
python
model = MnistCNN()
sample_logits = model(images[:4])
print(sample_logits.shape)
应该输出:
text
[4, 10]
通用训练与评估函数
python
def accuracy(logits, labels):
pred = paddle.argmax(logits, axis=1)
return paddle.mean((pred == labels).astype("float32"))
def train_one_epoch(model, loader, optimizer):
model.train()
total_loss = 0.0
total_acc = 0.0
count = 0
for images, labels in loader:
logits = model(images)
loss = F.cross_entropy(logits, labels)
acc = accuracy(logits, labels)
loss.backward()
optimizer.step()
optimizer.clear_grad()
total_loss += float(loss.numpy())
total_acc += float(acc.numpy())
count += 1
return total_loss / count, total_acc / count
def evaluate(model, loader):
model.eval()
total_loss = 0.0
total_acc = 0.0
count = 0
with paddle.no_grad():
for images, labels in loader:
logits = model(images)
loss = F.cross_entropy(logits, labels)
acc = accuracy(logits, labels)
total_loss += float(loss.numpy())
total_acc += float(acc.numpy())
count += 1
return total_loss / count, total_acc / count
训练 MNIST:
python
model = MnistCNN()
optimizer = paddle.optimizer.Adam(learning_rate=0.001, parameters=model.parameters())
for epoch in range(5):
train_loss, train_acc = train_one_epoch(model, train_loader, optimizer)
test_loss, test_acc = evaluate(model, test_loader)
print(
"epoch:", epoch,
"train_loss:", train_loss,
"train_acc:", train_acc,
"test_loss:", test_loss,
"test_acc:", test_acc,
)
MNIST 通常很容易收敛。如果效果很差,大概率是数据 shape、标签 dtype 或训练循环有问题。
CIFAR-10:数据集和类别
Paddle 官方 paddle.vision.datasets.Cifar10 提供 CIFAR-10 数据集封装。官方文档说明它包含 10 类图像,训练集长度通常为 50000,测试集长度通常为 10000。
CIFAR-10 是彩色图像,因此输入通道数是 3。
python
from paddle.vision.datasets import Cifar10
import paddle.vision.transforms as T
cifar_train_transform = T.Compose([
T.Resize(32),
T.RandomHorizontalFlip(prob=0.5),
T.ToTensor(),
T.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]),
])
cifar_test_transform = T.Compose([
T.Resize(32),
T.ToTensor(),
T.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]),
])
train_dataset = Cifar10(mode="train", transform=cifar_train_transform, download=True)
test_dataset = Cifar10(mode="test", transform=cifar_test_transform, download=True)
train_loader = paddle.io.DataLoader(train_dataset, batch_size=128, shuffle=True)
test_loader = paddle.io.DataLoader(test_dataset, batch_size=256, shuffle=False)
检查:
python
images, labels = next(iter(train_loader))
print(images.shape, images.dtype)
print(labels.shape, labels.dtype)
期望:
text
images: [128, 3, 32, 32]
labels: [128]
注意:训练集可以用随机翻转,测试集不要用随机增强。
CIFAR-10:定义更强一点的 CNN
MNIST 的 CNN 对 CIFAR-10 往往不够强。这里写一个稍微更完整的 CNN:
python
class CifarCNN(nn.Layer):
def __init__(self):
super().__init__()
self.features = nn.Sequential(
nn.Conv2D(3, 32, kernel_size=3, padding=1),
nn.BatchNorm2D(32),
nn.ReLU(),
nn.Conv2D(32, 32, kernel_size=3, padding=1),
nn.BatchNorm2D(32),
nn.ReLU(),
nn.MaxPool2D(kernel_size=2, stride=2),
nn.Dropout(p=0.25),
nn.Conv2D(32, 64, kernel_size=3, padding=1),
nn.BatchNorm2D(64),
nn.ReLU(),
nn.Conv2D(64, 64, kernel_size=3, padding=1),
nn.BatchNorm2D(64),
nn.ReLU(),
nn.MaxPool2D(kernel_size=2, stride=2),
nn.Dropout(p=0.25),
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(64 * 8 * 8, 256),
nn.ReLU(),
nn.Dropout(p=0.5),
nn.Linear(256, 10),
)
def forward(self, x):
x = self.features(x)
return self.classifier(x)
shape 推导:
text
[N, 3, 32, 32]
-> 两个 Conv2D,尺寸不变
[N, 32, 32, 32]
-> MaxPool2D
[N, 32, 16, 16]
-> 两个 Conv2D
[N, 64, 16, 16]
-> MaxPool2D
[N, 64, 8, 8]
-> Flatten
[N, 4096]
-> Linear
[N, 10]
训练前检查:
python
model = CifarCNN()
logits = model(images[:4])
print(logits.shape)
应该输出:
text
[4, 10]
CIFAR-10:训练建议
CIFAR-10 比 MNIST 难,建议:
python
model = CifarCNN()
optimizer = paddle.optimizer.Adam(
learning_rate=0.001,
parameters=model.parameters(),
weight_decay=1e-4,
)
训练:
python
for epoch in range(20):
train_loss, train_acc = train_one_epoch(model, train_loader, optimizer)
test_loss, test_acc = evaluate(model, test_loader)
print(
"epoch:", epoch,
"train_loss:", train_loss,
"train_acc:", train_acc,
"test_loss:", test_loss,
"test_acc:", test_acc,
)
如果只训练 1 到 2 个 epoch,CIFAR-10 指标可能不高,这是正常的。它比 MNIST 更依赖模型容量、训练轮数、数据增强和学习率策略。
从 MNIST 到 CIFAR-10 的关键差异
| 方面 | MNIST | CIFAR-10 |
|---|---|---|
| 通道数 | 1 | 3 |
| 图片大小 | 28x28 | 32x32 |
| 内容 | 手写数字 | 自然图像 |
| 背景复杂度 | 低 | 高 |
| 数据增强 | 可少量使用 | 更重要 |
| 模型容量 | 小 CNN 足够 | 需要更强 CNN |
| 过拟合风险 | 相对低 | 更明显 |
迁移时最容易忘记的是 in_channels:
python
nn.Conv2D(1, 16, 3)
用于 MNIST。
python
nn.Conv2D(3, 32, 3)
用于 CIFAR-10。
常见错误:图像分类实战排查清单
错误一:Normalize 通道数不匹配
灰度图:
python
T.Normalize(mean=[0.5], std=[0.5])
彩色图:
python
T.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
错误二:Conv2D 的 in_channels 写错
MNIST 输入是 1 通道,CIFAR-10 输入是 3 通道。
错误三:Flatten 后 Linear 维度写错
先打印特征图 shape:
python
features = model.features(images[:4])
print(features.shape)
再计算 Linear 输入维度。
错误四:测试集用了随机增强
测试集不要使用 RandomHorizontalFlip、随机裁剪等随机 transform。
错误五:只看 train_acc
CIFAR-10 上更容易过拟合,必须同时看 test 或 val 指标。
建议练习:把 MNIST 和 CIFAR-10 对比跑一遍
- 打印 MNIST 和 CIFAR-10 的 batch shape。
- 把 MNIST CNN 的第一层改成
Conv2D(3, 16, 3),观察错误。 - 把 CIFAR-10 模型中的 Dropout 去掉,观察过拟合。
- 给 CIFAR-10 添加学习率调度器。
- 保存最佳模型参数。
- 对比训练集和测试集 accuracy 曲线。
总结
这一篇完成了从 MNIST 到 CIFAR-10 的图像分类实战:
- MNIST 是灰度图,输入
[N, 1, 28, 28]。 - CIFAR-10 是彩色图,输入
[N, 3, 32, 32]。 - 图像预处理必须区分训练和测试。
- CNN 的第一层
in_channels必须匹配输入通道数。 - CIFAR-10 比 MNIST 更需要数据增强、正则化和更强模型。
- 训练分类模型时,最后输出 logits,再交给
cross_entropy。
如果只能记住一句话,那就是:
从 MNIST 迁移到 CIFAR-10,不只是换数据集,更是从简单灰度图分类进入真实图像建模思维。