前言

本篇是我训练营的第二次学习,主要目标是使用 PyTorch 实现 CIFAR10 彩色图片识别。如果说 P1 周主要是用 MNIST 手写数字识别来跑通“数据导入 → 模型构建 → 模型训练 → 结果可视化”的完整流程,那么 P2 周就是在这个流程基础上,把任务从 灰度图数字识别 推进到 彩色图像分类
这周的数据集变成了 CIFAR10。它不再是简单的黑白手写数字,而是包含飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车等 10 类彩色图片。图片虽然也不大,只有 32 × 32,但是因为它是 RGB 三通道彩色图,而且图像内容更复杂,所以识别难度比 MNIST 明显更高。
P1 周是“先跑通深度学习图像分类流程”,P2 周是“在彩色图像上继续跑通流程,并重点理解 CNN 网络结构和尺寸变化”。
本周学习的重点不是单纯把代码跑完,而是要真正理解 CNN 网络结构,尤其是卷积层、池化层之后图像尺寸如何变化,以及为什么最后全连接层中会出现 512 这样的数字。对我来说,这一周最重要的学习目标是:

  1. 复习 P1 周已经学过的 PyTorch 图像分类完整流程;
  2. 理解 MNIST 与 CIFAR10 在数据格式上的区别;
  3. 掌握彩色图片可视化时为什么要使用 transpose((1, 2, 0))
  4. 手动推导 CNN 中每一层输出的 shape;
  5. 理解 self.fc1 = nn.Linear(512, 256) 中的 512 是怎么来的;
  6. 对比 P1 与 P2 的训练结果,理解为什么 CIFAR10 的准确率比 MNIST 低很多。

P1 周与 P2 周整体对比

在正式学习 P2 之前,先把 P1 和 P2 放在一起对比,这样更容易看清楚本周到底新增了什么内容。

对比项目 P1 周:MNIST 手写数字识别 P2 周:CIFAR10 彩色图片识别 不同点
数据集 MNIST CIFAR10 都是图像分类经典数据集,但 CIFAR10 更接近真实图像
图像类型 灰度图 彩色图 P1 只有 1 个通道,P2 有 RGB 3 个通道
输入 shape [32, 1, 28, 28] [32, 3, 32, 32] batch_size 都是 32,但通道数和图像大小不同
类别数 10 类数字:0-9 10 类物体 输出层仍然是 10 个分类分数
可视化处理 np.squeeze() transpose((1, 2, 0)) 灰度图是去掉通道维;彩色图是调整通道顺序
CNN 层数 2 层卷积 + 2 层池化 3 层卷积 + 3 层池化 图像更复杂,所以网络更深一点
Flatten 后维度 64 × 5 × 5 = 1600 128 × 2 × 2 = 512 全连接层输入维度由最后一层特征图决定
训练轮数 5 个 epoch 10 个 epoch CIFAR10 更难,训练更多轮准确率也不一定很高
最终测试准确率 可达到 98% 左右 约 58% 左右 CIFAR10 难度明显更高,简单 CNN 不够强

感谢K同学啊老师的教学,以及ChatGPT和豆包。


一、准备工作

1. 设置运行设备:GPU 或 CPU

每一次都先选择环境,如果电脑支持 CUDA,就使用 GPU;否则使用 CPU。

import torch
import torch.nn as nn
import matplotlib.pyplot as plt
import torchvision

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

device
device(type='cpu')

程序会自动判断当前电脑能不能使用 CUDA。如果可以使用,就把模型和数据放到 GPU 上运行;如果不可以,就放到 CPU 上运行。我自己安装的是 CPU 版本 PyTorch,所以只有CPU


2. 关于 CIFAR10 数据集

CIFAR10 是一个经典的彩色图像分类数据集,一共有 10 个类别:airplane, automobile, bird, cat, deer, dog, frog, horse, ship, truck
每张图片大小是 32 × 32,并且是 RGB 彩色图,所以每张图片有 3 个通道。
MNIST 图片是:1 × 28 × 28,CIFAR10 图片是:3 × 32 × 32
因此模型第一层卷积要写成 nn.Conv2d(3, 64, kernel_size=3) ,因为输入通道数变成了 3。


3. 导入 CIFAR10 数据集

与上一周的导入方法一样,只不过是把MNIST换成了CIFAR10,下载速度就能看出来数据量比MNIST大多了,用了差不多11min.

train_ds = torchvision.datasets.CIFAR10('data', 
                                      train=True, 
                                      transform=torchvision.transforms.ToTensor(), # 将数据类型转化为Tensor
                                      download=True)

test_ds  = torchvision.datasets.CIFAR10('data', 
                                      train=False, 
                                      transform=torchvision.transforms.ToTensor(), # 将数据类型转化为Tensor
                                      download=True)

这一段的意思是:从 torchvision.datasets 中读取 CIFAR10 数据集,把数据放到 data 文件夹里;train=True 表示读取训练集,train=False 表示读取测试集;读取图片时使用 ToTensor() 把图片转换成 PyTorch 可以处理的 Tensor 格式;如果本地没有数据,就自动下载。

100.0%
d:\Anaconda\envs\torchstudy\Lib\site-packages\torchvision\datasets\cifar.py:83: VisibleDeprecationWarning: dtype(): align should be passed as Python or NumPy boolean but got `align=0`. Did you mean to pass a tuple to create a subarray type? (Deprecated NumPy 2.4)
  entry = pickle.load(f, encoding="latin1")

问了豆包警报信息,应该没有太大的问题,先继续运行
在这里插入图片描述


4. 创建 DataLoader 数据加载器

方法都和之前基本一样,没有任何变化,PyTorch 的图像分类流程几乎都是一样的

batch_size = 32

train_dl = torch.utils.data.DataLoader(train_ds, 
                                       batch_size=batch_size, 
                                       shuffle=True)

test_dl  = torch.utils.data.DataLoader(test_ds, 
                                       batch_size=batch_size)

这一段的意思是

将 CIFAR10 训练集和测试集包装成 DataLoader,每次取出 32 张图片组成一个 batch。训练集设置 shuffle=True,表示每一轮训练前打乱数据顺序,这样可以减少模型按固定顺序的可能。


5. 查看一个 batch 的数据格式

imgs, labels = next(iter(train_dl))
imgs.shape

运行结果:

torch.Size([32, 3, 32, 32])

这个 shape 可以拆开理解:

torch.Size([32, 3, 32, 32])
             ↑   ↑   ↑   ↑
             N   C   H   W
             │   │   │   └── 宽度:32 像素
             │   │   └────── 高度:32 像素
             │   └────────── 通道数:3(RGB 彩色图)
             └────────────── 批次大小:这一批有 32 张图

MNIST取出一个 batch 后,图片 shape 是:torch.Size([32, 1, 28, 28]),现在是torch.Size([32, 3, 32, 32]),所以最重要的差异是:

  1. MNIST是灰度图,通道数为 1
  2. CIFAR是彩色图,通道数为 3
  3. MNIST图片大小是 28 × 28
  4. CIFAR图片大小是 32 × 32

6. 数据可视化

import numpy as np

# 指定图片大小,图像大小为20宽、5高的绘图,单位为英寸 inch
plt.figure(figsize=(20, 5)) 
for i, imgs in enumerate(imgs[:20]):
    # 进行轴变换
    npimg = imgs.numpy().transpose((1, 2, 0))
    # 将整个figure分成2行10列,绘制第i+1个子图
    plt.subplot(2, 10, i+1)
    plt.imshow(npimg, cmap=plt.cm.binary)
    plt.axis('off')
    
# plt.show()  如果你使用的是 Pycharm 编译器,请加上这行代码

输出如下:
在这里插入图片描述
这一段的意思是
从当前 batch 中取出前 20 张图片,将每张图片从 Tensor 转换成 NumPy 数组,然后把图片的维度顺序从 PyTorch 习惯的 [C, H, W] 调整成 matplotlib 显示图片习惯的 [H, W, C],最后用 2 行 × 10 列 的方式画出来。

PyTorch 中单张 CIFAR10 图片的 shape 是:[3, 32, 32]
其中:

3  → C,通道数,代表 RGB 三个颜色通道
32 → H,图片高度
32 → W,图片宽度

但是 matplotlib.pyplot.imshow() 更习惯接收的彩色图片格式是:[32, 32, 3],也就是:H, W, C

所以需要使用:

imgs.numpy().transpose((1, 2, 0))

它的意思是重新排列维度:

原顺序:(0, 1, 2) = (C, H, W)
新顺序:(1, 2, 0) = (H, W, C)

也就是:[3, 32, 32] → [32, 32, 3]
与 P1 周的 np.squeeze() 对比
MNIST 是灰度图,单张图片 shape 是:[1, 28, 28],当时我们使用npimg = np.squeeze(imgs.numpy()),它的作用是去掉多余的通道维度[1, 28, 28] → [28, 28]
但是因为 CIFAR10 是彩色图,3 个通道不能被随便挤掉。所以真正需要的是调整维度顺序:[3, 32, 32] → [32, 32, 3]
所以本周的处理重点是保留 RGB 三个通道,只是把通道位置挪到最后


二、构建简单的 CNN 网络

对于一般的 CNN 网络来说,通常可以分为两部分:

  1. 特征提取网络:用卷积层、池化层提取图片特征;
  2. 分类网络:用全连接层根据提取到的特征进行分类。
    CIFAR10 是彩色物体分类,比 MNIST 更难,所以网络也稍微深一些。

1. torch.nn.Conv2d() 卷积层

函数原型:

torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, 
                dilation=1, groups=1, bias=True, padding_mode='zeros', 
                device=None, dtype=None)

常用参数解释:

参数 含义 本周代码中的体现
in_channels 输入图像的通道数 CIFAR10 是 RGB 图,所以第一层是 3
out_channels 输出特征图的通道数 第一层输出 64 个特征图
kernel_size 卷积核大小 本周都是 3,即 3 × 3
stride 卷积步长 默认是 1
padding 是否在图像边缘补 0 默认是 0,所以卷积后尺寸会变小
dilation 卷积核元素之间的间隔 默认是 1
groups 分组卷积 默认是 1,初学阶段先不改

例如:

self.conv1 = nn.Conv2d(3, 64, kernel_size=3)

这一句表示:

输入通道数:3,因为 CIFAR10 是 RGB 彩色图
输出通道数:64,表示提取 64 组不同特征
卷积核大小:3 × 3

MNIST第一层卷积是:

self.conv1 = nn.Conv2d(1, 32, kernel_size=3)

CIFAR10第一层卷积是:

self.conv1 = nn.Conv2d(3, 64, kernel_size=3)

原因是:MNIST 是灰度图,输入通道数为 1;CIFAR10 是彩色图,输入通道数为 3。


2. torch.nn.MaxPool2d() 池化层

函数原型:

torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, 
                   return_indices=False, ceil_mode=False)

常用参数解释:

参数 含义
kernel_size 池化窗口大小
stride 池化窗口移动步长,默认等于 kernel_size
padding 是否填充边缘
dilation 窗口元素之间的间隔

例如:

self.pool1 = nn.MaxPool2d(kernel_size=2)

这一句表示使用 2 × 2 的窗口做最大池化。池化层的作用是缩小特征图尺寸,减少计算量,同时保留比较明显的特征。
卷积层:负责找特征;
池化层:负责压缩特征图,保留重点。


3. torch.nn.Linear() 全连接层

函数原型:

torch.nn.Linear(in_features, out_features, bias=True, device=None, dtype=None)

常用参数解释:

参数 含义
in_features 输入特征数量
out_features 输出特征数量
self.fc1 = nn.Linear(512, 256)
self.fc2 = nn.Linear(256, num_classes)

这一段的意思是:
先把卷积和池化提取出来的 512 个特征输入全连接层,变成 256 个特征;最后再输出 10 个类别分数。这里的 num_classes = 10,因为 CIFAR10 有 10 个类别。


4. 定义CNN 模型

import torch.nn.functional as F

num_classes = 10  # 图片的类别数

class Model(nn.Module):
    def __init__(self):
        super().__init__()
        
        # 特征提取网络
        self.conv1 = nn.Conv2d(3, 64, kernel_size=3)   # 第一层卷积:输入3通道,输出64通道
        self.pool1 = nn.MaxPool2d(kernel_size=2)       # 第一层池化:2×2
        self.conv2 = nn.Conv2d(64, 64, kernel_size=3)  # 第二层卷积:输入64通道,输出64通道
        self.pool2 = nn.MaxPool2d(kernel_size=2) 
        self.conv3 = nn.Conv2d(64, 128, kernel_size=3) # 第三层卷积:输入64通道,输出128通道
        self.pool3 = nn.MaxPool2d(kernel_size=2) 
                                      
        # 分类网络
        self.fc1 = nn.Linear(512, 256)          
        self.fc2 = nn.Linear(256, num_classes)
        
    # 前向传播
    def forward(self, x):
        x = self.pool1(F.relu(self.conv1(x)))     
        x = self.pool2(F.relu(self.conv2(x)))
        x = self.pool3(F.relu(self.conv3(x)))
        
        x = torch.flatten(x, start_dim=1)

        x = F.relu(self.fc1(x))
        x = self.fc2(x)
       
        return x

三、卷积层与池化层的 shape 变化

CIFAR10 彩色图,单张图片 shape 是[3, 32, 32],表示3 个颜色通道,图片高 32,宽 32

1. 卷积层输出尺寸公式

对于普通卷积层,输出尺寸公式是:

输出尺寸 = floor((输入尺寸 + 2 × padding - dilation × (kernel_size - 1) - 1) / stride + 1)

本周代码中,卷积层基本使用默认参数:

kernel_size = 3
stride = 1
padding = 0
dilation = 1

所以公式可以简化成:

输出尺寸 = 输入尺寸 - 3 + 1 = 输入尺寸 - 2

也就是说,每经过一个 3 × 3 且不加 padding 的卷积层,高和宽都会减少 2。


2. 池化层输出尺寸公式

池化层是:

nn.MaxPool2d(kernel_size=2)

stride 不设置时,默认等于 kernel_size,所以这里相当于:

kernel_size = 2
stride = 2

可以简单理解为:每经过一次 2 × 2 最大池化,高和宽大约变成原来的一半。


3. 完整 shape 推导

输入图片

[3, 32, 32]

第一层卷积 conv1

self.conv1 = nn.Conv2d(3, 64, kernel_size=3)

输入通道数从 3 变成输出通道数 64
图片大小变化:

32 × 32 → 30 × 30

所以输出变成:

[64, 30, 30]

第一层池化 pool1

self.pool1 = nn.MaxPool2d(kernel_size=2)

图片大小减半:

30 × 30 → 15 × 15

所以输出变成:

[64, 15, 15]

第二层卷积 conv2

self.conv2 = nn.Conv2d(64, 64, kernel_size=3)

通道数仍然是 64
图片大小变化:

15 × 15 → 13 × 13

所以输出变成:

[64, 13, 13]

第二层池化 pool2

图片大小变化:

13 × 13 → 6 × 6

这里不是 6.5 × 6.5,因为输出尺寸要取整数,默认向下取整。
所以输出变成:

[64, 6, 6]

第三层卷积 conv3

self.conv3 = nn.Conv2d(64, 128, kernel_size=3)

通道数从 64 变成 128
图片大小变化:

6 × 6 → 4 × 4

所以输出变成:

[128, 4, 4]

第三层池化 pool3

图片大小变化:

4 × 4 → 2 × 2

所以输出变成:

[128, 2, 2]

Flatten 展平

进入全连接层之前,需要把多维特征图展平成一维向量:

128 × 2 × 2 = 512

所以:

self.fc1 = nn.Linear(512, 256)

这里的 512 就是这样来的。


4. 完整结构汇总

输入:3, 32, 32
↓
卷积层1:Conv2d(3, 64, kernel_size=3)
输出:64, 30, 30
↓
池化层1:MaxPool2d(2)
输出:64, 15, 15
↓
卷积层2:Conv2d(64, 64, kernel_size=3)
输出:64, 13, 13
↓
池化层2:MaxPool2d(2)
输出:64, 6, 6
↓
卷积层3:Conv2d(64, 128, kernel_size=3)
输出:128, 4, 4
↓
池化层3:MaxPool2d(2)
输出:128, 2, 2
↓
Flatten 展平:128 × 2 × 2 = 512
↓
全连接层1:Linear(512, 256)
↓
全连接层2:Linear(256, 10)
↓
输出:10 个类别分数

四、模型参数量理解

打印模型结构:

from torchinfo import summary

model = Model().to(device)
summary(model)
=================================================================
Layer (type:depth-idx)                   Param #
=================================================================
Model                                    --
├─Conv2d: 1-1                            1,792
├─MaxPool2d: 1-2                         --
├─Conv2d: 1-3                            36,928
├─MaxPool2d: 1-4                         --
├─Conv2d: 1-5                            73,856
├─MaxPool2d: 1-6                         --
├─Linear: 1-7                            131,328
├─Linear: 1-8                            2,570
=================================================================
Total params: 246,474
Trainable params: 246,474
Non-trainable params: 0
=================================================================

运行结果中总参数量为:Total params: 246,474

1. 卷积层参数量怎么算

卷积层参数量公式:

参数量 = 输出通道数 × (输入通道数 × 卷积核高 × 卷积核宽 + bias)

其中 + bias 是因为每个输出通道通常有一个偏置项。

conv1 参数量

nn.Conv2d(3, 64, kernel_size=3)
64 × (3 × 3 × 3 + 1) = 64 × 28 = 1792

conv2 参数量

nn.Conv2d(64, 64, kernel_size=3)
64 × (64 × 3 × 3 + 1) = 64 × 577 = 36928

conv3 参数量

nn.Conv2d(64, 128, kernel_size=3)
128 × (64 × 3 × 3 + 1) = 128 × 577 = 73856

2. 全连接层参数量怎么算

全连接层参数量公式:

参数量 = 输入特征数 × 输出特征数 + 输出特征数对应的 bias

fc1 参数量

nn.Linear(512, 256)
512 × 256 + 256 = 131328

fc2 参数量

nn.Linear(256, 10)
256 × 10 + 10 = 2570

3. 与MNIST参数量对比

模型 网络结构 总参数量
MNIST CNN 2 个卷积层 + 2 个池化层 + 2 个全连接层 121,930
CIFAR10 CNN 3 个卷积层 + 3 个池化层 + 2 个全连接层 246,474

CIFAR的模型参数量比MNIST多,主要原因是:

  1. CIFAR10 是彩色图,输入通道数从 1 变成 3;
  2. P2 网络多了一层卷积层;
  3. P2 中间特征通道数更多,例如 64、128。

五、训练模型

1. 设置损失函数、学习率和优化器

loss_fn    = nn.CrossEntropyLoss() # 创建损失函数
learn_rate = 1e-2                  # 学习率
opt        = torch.optim.SGD(model.parameters(), lr=learn_rate)

这部分和上一周基本一样。

nn.CrossEntropyLoss() 常用于多分类任务。CIFAR10 有 10 个类别,所以可以使用交叉熵损失函数。损失值越大,说明模型预测和真实标签差距越大;损失值越小,说明模型预测越来越接近真实标签。
learn_rate = 1e-21e-2 就是 0.01,表示学习率。学习率可以理解为模型每次更新参数时迈出的步子大小,学习率太大:可能走过头,训练不稳定;学习率太小:走得太慢,训练速度慢。
SGD优化器的作用是根据梯度更新模型参数。model.parameters() 表示把模型中所有可训练参数交给优化器。


2. 编写训练函数

def train(dataloader, model, loss_fn, optimizer):
    size = len(dataloader.dataset)  # 训练集大小
    num_batches = len(dataloader)   # batch 数量

    train_loss, train_acc = 0, 0
    
    for X, y in dataloader:
        X, y = X.to(device), y.to(device)
        
        # 计算预测误差
        pred = model(X)
        loss = loss_fn(pred, y)
        
        # 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        # 记录准确率和损失
        train_acc  += (pred.argmax(1) == y).type(torch.float).sum().item()
        train_loss += loss.item()
            
    train_acc  /= size
    train_loss /= num_batches

    return train_acc, train_loss

训练函数的核心仍然是三步:

第一步:optimizer.zero_grad() 清空梯度

PyTorch 中梯度默认会累加,所以每个 batch 开始训练前,需要先把上一轮的梯度清空。

第二步:loss.backward() 反向传播

根据当前损失值,自动计算每个参数的梯度。

第三步:optimizer.step() 更新参数

优化器根据梯度更新模型参数。


3. 编写测试函数

def test(dataloader, model, loss_fn):
    size        = len(dataloader.dataset)
    num_batches = len(dataloader)
    test_loss, test_acc = 0, 0
    
    with torch.no_grad():
        for imgs, target in dataloader:
            imgs, target = imgs.to(device), target.to(device)
            
            target_pred = model(imgs)
            loss        = loss_fn(target_pred, target)
            
            test_loss += loss.item()
            test_acc  += (target_pred.argmax(1) == target).type(torch.float).sum().item()

    test_acc  /= size
    test_loss /= num_batches

    return test_acc, test_loss

测试函数和训练函数很像,但是有两个关键区别:

  1. 测试时不调用 optimizer.step(),所以不会更新模型参数;
  2. 测试时使用 torch.no_grad(),关闭梯度计算,节省内存和计算量。

4. 正式训练

epochs     = 10
train_loss = []
train_acc  = []
test_loss  = []
test_acc   = []

for epoch in range(epochs):
    model.train()
    epoch_train_acc, epoch_train_loss = train(train_dl, model, loss_fn, opt)
    
    model.eval()
    epoch_test_acc, epoch_test_loss = test(test_dl, model, loss_fn)
    
    train_acc.append(epoch_train_acc)
    train_loss.append(epoch_train_loss)
    test_acc.append(epoch_test_acc)
    test_loss.append(epoch_test_loss)
    
    template = ('Epoch:{:2d}, Train_acc:{:.1f}%, Train_loss:{:.3f}, Test_acc:{:.1f}%,Test_loss:{:.3f}')
    print(template.format(epoch+1, epoch_train_acc*100, epoch_train_loss, epoch_test_acc*100, epoch_test_loss))
print('Done')
Epoch: 1, Train_acc:14.7%, Train_loss:2.292, Test_acc:19.6%,Test_loss:2.244
Epoch: 2, Train_acc:24.0%, Train_loss:2.053, Test_acc:28.3%,Test_loss:1.956
Epoch: 3, Train_acc:32.8%, Train_loss:1.833, Test_acc:36.0%,Test_loss:1.767
Epoch: 4, Train_acc:40.3%, Train_loss:1.638, Test_acc:43.7%,Test_loss:1.545
Epoch: 5, Train_acc:44.4%, Train_loss:1.529, Test_acc:46.1%,Test_loss:1.494
Epoch: 6, Train_acc:48.1%, Train_loss:1.434, Test_acc:50.3%,Test_loss:1.402
Epoch: 7, Train_acc:51.7%, Train_loss:1.348, Test_acc:49.2%,Test_loss:1.384
Epoch: 8, Train_acc:54.5%, Train_loss:1.279, Test_acc:55.4%,Test_loss:1.250
Epoch: 9, Train_acc:56.7%, Train_loss:1.220, Test_acc:56.8%,Test_loss:1.209
Epoch:10, Train_acc:59.0%, Train_loss:1.164, Test_acc:58.5%,Test_loss:1.184
Done

model.train() 表示把模型切换到训练模式。如果模型里有 Dropout 或 BatchNorm 这类层,训练模式会影响它们的行为,Dropout:训练时随机丢弃部分神经元;BatchNorm:训练时使用当前 batch 的均值和方差,并更新内部统计量。
虽然本周这个简单 CNN 没有明显用到 Dropout 和 BatchNorm,但养成训练前写 model.train() 的习惯是很重要的。
model.eval() 表示把模型切换到评估模式。Dropout 会关闭;BatchNorm 使用训练时记录下来的

训练结果理解,从整体趋势看:

  1. 训练准确率从 14.7% 提升到 59.0%;
  2. 测试准确率从 19.6% 提升到 58.5%;
  3. 训练损失整体下降;

为什么CIFAR10准确率比 MNIST低很多?
MNIST 的测试准确率可以达到 98% 左右,但 CIFAR10 只有 58% 左右。原因主要有以下几点:
① CIFAR10 图像内容更复杂
MNIST 是黑白手写数字,背景干净,目标单一;CIFAR10 是真实彩色物体图片,背景、颜色、形状都更复杂。

②CIFAR10 是彩色图,有 3 个通道
模型需要同时学习 RGB 三个通道中的颜色、纹理和形状信息,输入信息更复杂。

③简单 CNN 表达能力有限
本周的 CNN 只有三层卷积层,没有使用 BatchNorm、Dropout、数据增强,也没有使用更强的模型结构,所以准确率不会特别高。

④训练轮数仍然偏少
CIFAR10 通常需要更多 epoch、更合适的优化器、更细致的数据预处理,才能获得更高准确率。


六、结果可视化

训练结束后,可以把训练准确率、测试准确率、训练损失、测试损失画出来。

import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")               # 忽略警告信息
plt.rcParams['font.sans-serif']    = ['SimHei'] # 正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False      # 正常显示负号
plt.rcParams['figure.dpi']         = 100        # 分辨率

from datetime import datetime
current_time = datetime.now() # 获取当前时间

epochs_range = range(epochs)

plt.figure(figsize=(12, 3))
plt.subplot(1, 2, 1)

plt.plot(epochs_range, train_acc, label='Training Accuracy')
plt.plot(epochs_range, test_acc, label='Test Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')
plt.xlabel(current_time) # 打卡请带上时间戳,否则代码截图无效

plt.subplot(1, 2, 2)
plt.plot(epochs_range, train_loss, label='Training Loss')
plt.plot(epochs_range, test_loss, label='Test Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()

这一部分和上周几乎一样,主要作用是观察训练过程,准确率曲线:看模型分类能力是否提高;损失曲线:看模型预测误差是否下降。
这是我的结果
在这里插入图片描述


总结

本周学习的是 PyTorch 入门第 P2 周:彩色图片识别。相比 P1 周 MNIST 手写数字识别,P2 周的任务明显更复杂。P1 的重点是让我跑通一个完整的深度学习程序,理解 Dataset、DataLoader、CNN、训练、测试和可视化的基本流程;而 P2 的重点则是在这个流程基础上进一步理解 CNN 网络本身,尤其是卷积层和池化层之后图像尺寸的变化。
这周我最重要的收获是理解了彩色图像和灰度图像在 Tensor 形状上的区别。P1 周 MNIST 是 [1, 28, 28],所以可视化时可以用 np.squeeze() 去掉通道维;P2 周 CIFAR10 是 [3, 32, 32],RGB 三个通道不能去掉,只能使用 transpose((1, 2, 0)) 把它转换成 matplotlib 能显示的 [32, 32, 3]
另一个重要收获是学会了手动推导 CNN 的 shape。输入图片从 [3, 32, 32] 进入三组卷积和池化后,最后变成 [128, 2, 2],展平后就是 128 × 2 × 2 = 512,所以全连接层才写成 nn.Linear(512, 256)。这一点比单纯背代码更重要,因为以后只要模型结构改变,Flatten 后的维度也会跟着变化,。
最后,通过对比 P1 和 P2 的训练结果,我也理解了为什么 CIFAR10 的准确率远低于 MNIST。MNIST 图像简单、背景干净,而 CIFAR10 是彩色真实物体图片,类别之间差异更复杂,简单 CNN 只能达到较基础的效果。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐