Pytorch入门P2周学习打卡:CIFAR10彩色图片识别
- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
前言
本篇是我训练营的第二次学习,主要目标是使用 PyTorch 实现 CIFAR10 彩色图片识别。如果说 P1 周主要是用 MNIST 手写数字识别来跑通“数据导入 → 模型构建 → 模型训练 → 结果可视化”的完整流程,那么 P2 周就是在这个流程基础上,把任务从 灰度图数字识别 推进到 彩色图像分类。
这周的数据集变成了 CIFAR10。它不再是简单的黑白手写数字,而是包含飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车等 10 类彩色图片。图片虽然也不大,只有 32 × 32,但是因为它是 RGB 三通道彩色图,而且图像内容更复杂,所以识别难度比 MNIST 明显更高。
P1 周是“先跑通深度学习图像分类流程”,P2 周是“在彩色图像上继续跑通流程,并重点理解 CNN 网络结构和尺寸变化”。
本周学习的重点不是单纯把代码跑完,而是要真正理解 CNN 网络结构,尤其是卷积层、池化层之后图像尺寸如何变化,以及为什么最后全连接层中会出现 512 这样的数字。对我来说,这一周最重要的学习目标是:
- 复习 P1 周已经学过的 PyTorch 图像分类完整流程;
- 理解 MNIST 与 CIFAR10 在数据格式上的区别;
- 掌握彩色图片可视化时为什么要使用
transpose((1, 2, 0)); - 手动推导 CNN 中每一层输出的 shape;
- 理解
self.fc1 = nn.Linear(512, 256)中的512是怎么来的; - 对比 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]),所以最重要的差异是:
- MNIST是灰度图,通道数为
1; - CIFAR是彩色图,通道数为
3; - MNIST图片大小是
28 × 28; - 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 网络来说,通常可以分为两部分:
- 特征提取网络:用卷积层、池化层提取图片特征;
- 分类网络:用全连接层根据提取到的特征进行分类。
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多,主要原因是:
- CIFAR10 是彩色图,输入通道数从 1 变成 3;
- P2 网络多了一层卷积层;
- 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-2:1e-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
测试函数和训练函数很像,但是有两个关键区别:
- 测试时不调用
optimizer.step(),所以不会更新模型参数; - 测试时使用
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 使用训练时记录下来的
训练结果理解,从整体趋势看:
- 训练准确率从 14.7% 提升到 59.0%;
- 测试准确率从 19.6% 提升到 58.5%;
- 训练损失整体下降;
为什么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 只能达到较基础的效果。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)