21-图像分类实战:从 MNIST 到 CIFAR-10

概述

第 20 篇已经用 Paddle 构建了一个基础 CNN。那一篇重点是理解 Conv2DMaxPool2DFlattenLinear 如何组成图像分类模型。本篇继续做完整图像分类实战:先用 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.MNISTpaddle.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 对比跑一遍

  1. 打印 MNIST 和 CIFAR-10 的 batch shape。
  2. 把 MNIST CNN 的第一层改成 Conv2D(3, 16, 3),观察错误。
  3. 把 CIFAR-10 模型中的 Dropout 去掉,观察过拟合。
  4. 给 CIFAR-10 添加学习率调度器。
  5. 保存最佳模型参数。
  6. 对比训练集和测试集 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,不只是换数据集,更是从简单灰度图分类进入真实图像建模思维。

相关推荐
青 春 记 忆1 小时前
LeetCode 121. 买卖股票的最佳时机|Python 解法详解
python·算法·leetcode
网易云信1 小时前
7 天,52 个 AI 员工“上岗”了|帝王蟹 AI 实战训练营首期收官
人工智能·线下活动
Logintern091 小时前
Celery 的底层架构正是进程池、事件循环、epoll 和协程全部组合在了一起
python·架构·消息队列·进程·celery·事件循环
RobinDevNotes1 小时前
K8s+DeepSpeed+vLLM打通大模型训练到上线全链路
人工智能
盖伦发发1 小时前
RAG 能跑≠能用:用 EDD 把 Eval 做成基础设施 (附源码)
人工智能·后端·python·功能测试
宋哥转AI1 小时前
深入理解 AI Agent · MEMORY #03:从设计到落地
人工智能·agent·ai编程
_codeOH1 小时前
大模型上下文窗口管理:从滑动窗口到 RAG
人工智能·ai编程
Vuji1 小时前
Pi 插件解剖|custom-compaction.ts:117 行,自定义上下文压缩
前端·人工智能·agent
MistyStar1 小时前
一切皆插件,DeepSeek Harness 来了,模型终于配上自己的马具
人工智能·github