TASK 1 训练一个网络识别手写数字
1、导入必要的库
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
- torch: PyTorch 的核心库,提供张量计算和自动微分功能。
- torch.nn: 包含神经网络层(如卷积层、全连接层)和损失函数。
- torch.optim: 提供优化算法(如 Adam、SGD)。
- torchvision: 提供数据集(如 MNIST)和图像预处理工具。
- DataLoader: 用于批量加载数据并支持多线程加速。
- matplotlib.pyplot: 用于可视化图像和结果
2、数据预处理和加载
transform = transforms.Compose([transforms.ToTensor(), # 将PIL图像或NumPy数组转为PyTorch张量,并归一化到[0,1]transforms.Normalize((0.1307,), (0.3081,)) # 标准化(均值0.1307,标准差0.3081)
])
-
ToTensor(): 这是一个将图像数据转换为PyTorch张量并进行标准化处理的转换器。
-
具体操作
-
数据类型转换
- 输入:PIL图像 或 NumPy数组(
uint8
类型,范围[0,255]) - 输出:PyTorch张量(
float32
类型,范围[0,1])
- 输入:PIL图像 或 NumPy数组(
-
数值范围缩放
- 原始:
0 ─────── 255
(整数) - 转换后:
0.0 ─── 1.0
(浮点数) - 公式:
像素值 / 255.0
- 原始:
-
维度顺序调整
- 原始图像格式:
(H, W, C)
← (高度, 宽度, 通道) - 转换后格式:
(C, H, W)
← (通道, 高度, 宽度) - 对于MNIST(灰度图):
(28, 28)
→(1, 28, 28)
- 对于彩色图像:
(H, W, 3)
→(3, H, W)
- 原始图像格式:
-
为什么需要这样做?
- 数值稳定性:将整数转换为浮点数,便于梯度计算
- 模型期望:PyTorch的卷积层期望输入格式为
(批次大小, 通道数, 高度, 宽度)
- 标准化基础:为后续的Normalize操作做准备
-
-
-
Normalize(): 对每个像素值进行标准化,使数据分布更加稳定。
-
具体操作
-
数学公式
- 对于每个像素:
(x - 0.1307) / 0.3081
- 对于每个像素:
-
参数解释
(0.1307,)
:MNIST数据集的平均像素值(均值)(0.3081,)
:MNIST数据集的标准差- MNIST的均值和是通过计算整个MNIST数据集的统计特性得到的
-
实际计算示例
- 如果一个像素原始值为
0.5
(经过ToTensor后): - 标准化后:
(0.5 - 0.1307) / 0.3081 ≈ 1.198
- 如果一个像素原始值为
-
为什么需要标准化?
- 加速收敛:
- 原始数据分布可能不均匀
- 标准化后数据均值为0,标准差为1,便于优化器快速收敛
- 数值稳定性:
- 防止梯度爆炸或消失
- 使激活函数工作在敏感区域
- 泛化能力:
- 使模型对不同亮度、对比度的图像更具鲁棒性
- 加速收敛:
-
-
train_data = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_data = datasets.MNIST(root='./data', train=False, download=True, transform=transform)
- datasets.MNIST: 下载 MNIST 数据集(训练集和测试集),存储在
./data
目录。 - transform: 对数据应用预处理流程。
train_loader = DataLoader(train_data, batch_size=64, shuffle=True)
test_loader = DataLoader(test_data, batch_size=1000, shuffle=False)
- DataLoader: 将数据集分批次加载(训练集每批64张,测试集每批1000张)。
- shuffle=True: 打乱训练数据顺序,避免模型学习到顺序偏差。
3、定义CNN模型
class CNN(nn.Module):def __init__(self):super(CNN, self).__init__()self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1)self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)self.fc1 = nn.Linear(64 * 7 * 7, 128)self.fc2 = nn.Linear(128, 10)
-
类定义和初始化
class CNN(nn.Module)
: 定义一个继承自nn.Module
的神经网络类(PyTorch要求所有自定义网络都必须继承此类)。super(CNN, self).__init__()
: 调用父类的构造函数,确保正确初始化。
-
卷积层定义
-
第一层卷积
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1)
in_channels=1
: 输入通道数(MNIST是灰度图,所以是1个通道)out_channels=32
: 输出通道数(即使用32个不同的卷积核)kernel_size=3
: 卷积核大小 3×3stride=1
: 滑动步长为1padding=1
: 边缘填充1圈(保持输入输出尺寸相同
-
公式:
\[输出尺寸 = (输入尺寸 + 2×填充 - 卷积核大小) ÷ 步长 + 1 \] -
计算过程
-
输入尺寸: 28(MNIST图像是28×28)
-
卷积核大小: 3
-
填充: 1(四周各补1圈0)
-
步长: 1(每次移动1个像素)
\[输出尺寸 = (28 + 2×1 - 3) ÷ 1 + 1= (28 + 2 - 3) ÷ 1 + 1= (27) ÷ 1 + 1= 27 + 1= 28 \]
-
-
-
池化层输出尺寸公式
\[输出尺寸 = 输入尺寸 ÷ 池化窗口大小 \]-
具体计算(第一次池化)
-
输入尺寸: 28
-
池化窗口: 2×2
-
步长: 默认为2(与窗口大小相同)
\[输出尺寸 = 28 ÷ 2 = 14 \]
-
-
-
全连接层
-
第一全连接层
self.fc1 = nn.Linear(64 * 7 * 7, 128)
- 参数详解:
in_features=64*7*7
: 输入特征数(64通道 × 7高度 × 7宽度 = 3136)out_features=128
: 输出特征数(128个神经元)
- 参数详解:
-
第二全连接层(输出层)
self.fc2 = nn.Linear(128, 10)
in_features=128
: 输入128维特征out_features=10
: 输出10个数字类别(0-9)的概率
-
-
前向传播
def forward(self, x):x = torch.relu(self.conv1(x)) # 卷积1 + ReLU激活x = torch.max_pool2d(x, 2) # 最大池化(2x2窗口,步长2)x = torch.relu(self.conv2(x)) # 卷积2 + ReLUx = torch.max_pool2d(x, 2) # 最大池化x = x.view(-1, 64 * 7 * 7) # 展平为向量x = torch.relu(self.fc1(x)) # 全连接层1 + ReLUx = self.fc2(x) # 全连接层2(输出未激活,因CrossEntropyLoss自带Softmax)return x
- torch.relu: ReLU激活函数,增加非线性。
- max_pool2d: 最大池化,降低特征图尺寸(28x28 → 14x14 → 7x7)。
- view(-1, 64*7*7): 将4D张量
[batch, channels, height, width]
展平为2D[batch, features]
-
整个网络的数据变化过程
一步一步来看:
- 开始: 输入图像
1×28×28
← (通道, 高, 宽)- 就像一张28×28的灰度照片
- 第一层卷积后:
32×28×28
- 用32个不同的3×3滤镜扫描图片
- 得到32张新的28×28特征图
- 第一次池化后:
32×14×14
- 每2×2的区域取最大值
- 尺寸减半,变成14×14
- 第二层卷积后:
64×14×14
- 用64个滤镜再次扫描
- 得到64张14×14特征图
- 第二次池化后:
64×7×7
- 再次取2×2区域最大值
- 尺寸再减半,变成7×7
- 展平后:
3136
个数字- 把64×7×7=3136个数字排成一长串
- 全连接层1:
3136 → 128
- 从3136个特征中选出128个最重要的
- 全连接层2:
128 → 10
- 从128个特征判断是哪个数字(0-9)
- 开始: 输入图像
-
参数数量计算(简单理解)
卷积层就像滤镜工厂
- 第一层: 32个3×3的滤镜 → 32 × 3 × 3 = 288个权重 + 32个偏置 = 320个参数
- 第二层: 64个3×3×32的滤镜 → 64 × 3 × 3 × 32 = 18,432个权重 + 64个偏置 = 18,496个参数
全连接层就像投票系统
- 第一全连接: 3136人每人给128个选项投票 → 3136 × 128 = 401,408个权重 + 128个偏置 = 401,536个参数
- 第二全连接: 128人每人给10个选项投票 → 128 × 10 = 1,280个权重 + 10个偏置 = 1,290个参数
总共: 320 + 18,496 + 401,536 + 1,290 = 421,642个可调节的"旋钮"
-
总结成一句话
这个CNN就像:
- 先用各种滤镜扫描图片(卷积层)
- 然后压缩重要信息(池化层)
- 最后投票决定是什么数字(全连接层)
每次卷积保持尺寸,每次池化尺寸减半,最终从28×28变成7×7!
4、定义损失函数和优化器
-
损失函数
criterion = nn.CrossEntropyLoss()
这是一个多分类交叉熵损失函数,专门用于多类别分类问题(比如10个数字的分类)。
它做什么?
- 计算预测误差:比较模型的预测结果和真实标签之间的差异
- 指导模型学习:告诉模型哪些预测是正确的,哪些是错误的
工作原理(简单版)
假设模型对一张"7"的图片预测:
- 输出:
[0.1, 0.2, 0.05, 0.1, 0.1, 0.1, 0.1, 0.6, 0.05, 0.1]
- 每个数字代表对应类别(0-9)的得分
- 第7个位置(索引7)的值0.6最大,表示预测为数字7
- 真实标签:
7
(不是one-hot编码,就是数字7)
损失函数会计算这个预测的"错误程度"
为什么适合数字识别?
- 直接处理类别标签(0,1,2,...,9)
- 内部自动包含Softmax,将得分转换为概率
- 非常适合多分类问题
-
优化器
optimizer = optim.Adam(model.parameters(), lr=0.001)
这是一个Adam优化器,负责根据损失函数的反馈来更新模型的参数(权重)。
参数详解
model.parameters()
:获取模型中所有需要训练的参数- 包括:卷积层的权重和偏置、全连接层的权重和偏置
- 总共约42万个参数(前面计算过)
lr=0.001
:学习率(Learning Rate)- 作用:控制每次参数更新的步长
- 为什么是0.001?:这是深度学习中常用的默认值
- 太大(如0.1):可能跳过最优解
- 太小(如0.00001):学习太慢
- 0.001:在大多数任务上效果良好
Adam优化器的特点
- 自适应学习率:为每个参数设置不同的学习率
- 动量机制:保持之前的更新方向,加速收敛
- 偏差校正:防止训练初期的不稳定
类比理解
- 模型:一个学生
- 损失函数:考试分数(告诉学生答错了多少题)
- 优化器:老师(根据错题指导学生如何改进)
- 学习率:老师的严格程度
- 太严格:学生不敢尝试新方法
- 太宽松:学生进步太慢
5、训练函数
def train(model, device, train_loader, optimizer, epoch):model.train() # 设置为训练模式(启用Dropout/BatchNorm)for batch_idx, (data, target) in enumerate(train_loader):data, target = data.to(device), target.to(device)optimizer.zero_grad() # 清空梯度output = model(data) # 前向传播loss = criterion(output, target) # 计算损失loss.backward() # 反向传播optimizer.step() # 更新权重
-
函数定义
def train(model, device, train_loader, optimizer, epoch):
- model: 要训练的神经网络模型(我们的CNN)
- device: 计算设备(CPU或GPU)
- train_loader: 数据加载器,提供批量训练数据
- optimizer: 优化器(Adam),负责更新模型参数
- epoch: 当前训练轮次,用于打印进度
-
设置为训练模式
model.train() # 设置为训练模式(启用Dropout/BatchNorm)
为什么需要这个?
- 训练模式:启用Dropout、BatchNorm等训练特有的层
- 评估模式:这些层在测试时会表现不同(
model.eval()
) - 就像学生:上课时(训练)要积极尝试,考试时(测试)要稳定发挥
-
遍历数据批次
for batch_idx, (data, target) in enumerate(train_loader):
- batch_idx: 当前批次的索引(0, 1, 2, ...)
- data: 一批图像数据,形状为
[64, 1, 28, 28]
← (批次大小, 通道, 高, 宽) - target: 对应的真实标签,形状为
[64]
← 64个数字标签 - enumerate: 同时获取索引和数据
-
数据转移到设备
data, target = data.to(device), target.to(device)
作用:将数据从CPU内存转移到GPU显存(如果可用)
- CPU:
data
在内存中 - GPU:
data
在显存中,计算速度更快 - 自动检测:
device
参数自动选择最佳设备
- CPU:
-
清空梯度
optimizer.zero_grad() # 清空梯度
为什么需要清空?
- PyTorch会累积梯度(默认行为)
- 如果不清空,每次
loss.backward()
都会累加梯度 - 就像记账:每次计算新账目前要清空旧账本
数学原理:
-
梯度:损失对每个权重的偏导数
\[\frac{\partial loss}{\partial w} \] -
需要从0开始计算新的梯度
-
前向传播
output = model(data) # 前向传播
发生了什么?
- 数据通过卷积层、池化层、全连接层
- 最终得到输出:
[64, 10]
← (批次大小, 10个类别的得分) - 比如:
output[0] = [2.1, -0.5, 1.3, ..., 0.8]
表示第一个样本的10个数字得分
-
计算损失
loss = criterion(output, target) # 计算损失
具体计算:
- output: 模型预测的10个得分
[64, 10]
- target: 真实标签
[64]
(如[7, 2, 1, ..., 9]
) - criterion: CrossEntropyLoss,计算预测与真实的差异
例子:
- 如果模型正确预测数字7(第7个得分最高),损失较小
- 如果错误预测,损失较大
- output: 模型预测的10个得分
-
反向传播
loss.backward() # 反向传播
这是最神奇的一步!
- 自动微分:PyTorch自动计算所有参数的梯度
- 链式法则:从输出层逐层反向计算到输入层
- 结果:每个参数都知道自己应该怎么调整才能减少损失
具体来说:
-
计算出:
\[\frac{\partial loss}{\partial w_1}, \frac{\partial loss}{\partial w_2}, ..., \frac{\partial loss}{\partial w_{421642}} \] -
总共计算42万多个梯度!
-
更新权重
optimizer.step() # 更新权重
根据梯度调整参数:
-
Adam优化器:使用自适应学习率更新每个参数
-
更新公式(简化):
\[w_{new} = w_{old} - \eta \cdot \frac{\partial loss}{\partial w} \]
就像走山路:
- 梯度:告诉你哪个方向是下山(减少损失)
- 优化器:沿着这个方向走一小步(学习率控制步长)
-
-
完整训练过程类比
学生学习比喻
- model.train():学生进入学习状态
- data.to(device):准备好学习资料
- optimizer.zero_grad():清空旧知识,准备学新的
- model(data):学生尝试解题(前向传播)
- criterion(output, target)老师批改作业,给出分数(损失)
- loss.backward():学生分析错题,知道哪里错了(反向传播)
- optimizer.step():学生改正错误,更新知识(参数更新)
工厂生产比喻
- 原料准备:
data.to(device)
- 清空生产线:
optimizer.zero_grad()
- 加工生产:
model(data)
- 质量检测:
criterion(output, target)
- 调整机器:
loss.backward()
- 优化生产:
optimizer.step()
-
总结
这8行代码完成了深度学习的核心训练循环:
- 准备:设置模式 + 转移数据 + 清空梯度
- 预测:前向传播获得输出
- 评估:计算损失衡量好坏
- 学习:反向传播计算梯度 + 优化器更新参数
6、测试函数
def test(model, device, test_loader):model.eval() # 设置为评估模式(关闭Dropout/BatchNorm)with torch.no_grad(): # 禁用梯度计算for data, target in test_loader:output = model(data)pred = output.argmax(dim=1) # 取概率最大的类别correct += (pred == target).sum().item()print(f'Test Accuracy: {100. * correct / len(test_loader.dataset):.2f}%')
-
函数定义
def test(model, device, test_loader):
- model: 已经训练好的神经网络模型
- device: 计算设备(CPU或GPU)
- test_loader: 测试数据加载器,提供批量测试数据
-
设置为评估模式
model.eval() # 设置为评估模式(关闭Dropout/BatchNorm)
为什么需要这个?
训练模式 vs 评估模式
模式 Dropout BatchNorm 用途 训练模式 ( model.train()
)启用 使用当前批次统计 训练时 评估模式 ( model.eval()
)关闭 使用训练期统计 测试时 具体影响:
- Dropout:在测试时应该关闭(否则会随机丢弃神经元,导致结果不稳定)
- BatchNorm:使用在训练期间学到的移动平均和方差,而不是当前批次的统计量
类比:
- 训练时:学生要做各种练习题(Dropout模拟不同情况)
- 考试时:学生要稳定发挥(关闭Dropout,用学到的知识)
-
禁用梯度计算
with torch.no_grad(): # 禁用梯度计算
这是关键优化!
为什么需要禁用梯度?
- 节省内存:梯度计算需要存储中间结果,占用大量内存
- 加速计算:避免不必要的梯度计算,提高速度
- 防止意外更新:确保测试时不会误修改模型参数
-
遍历测试数据
for data, target in test_loader:
- data: 一批测试图像,形状
[1000, 1, 28, 28]
- target: 对应的真实标签,形状
[1000]
- 这里使用较大的批次大小(1000),因为不需要计算梯度
- data: 一批测试图像,形状
-
模型预测
output = model(data) # 前向传播
输出形状:
[1000, 10]
← (批次大小, 10个数字的得分)示例输出:
output[0] = [1.2, -0.5, 0.8, 2.1, -1.0, 0.3, -0.2, 3.5, 0.1, 1.8]
- 每个数字代表对应类别(0-9)的得分
- 得分越高,模型认为属于该类别的可能性越大
-
获取预测结果
pred = output.argmax(dim=1) # 取概率最大的类别
具体操作:
-
dim=1: 在第二个维度(类别维度)上取最大值索引
-
输入:
output
形状[1000, 10]
-
输出:
pred
形状[1000]
,每个元素是预测的数字(0-9)例子:
- 如果
output[0] = [..., 3.5, ...]
且3.5是最大值 - 那么
pred[0] = 7
(因为索引7对应数字7)
- 如果
-
-
计算正确预测数
correct += (pred == target).sum().item()
逐步解析:
pred == target
: 逐元素比较预测和真实标签- 返回布尔张量,如
[True, False, True, ...]
True
表示预测正确,False
表示错误
- 返回布尔张量,如
.sum()
: 统计True
的数量(正确预测的样本数).item()
: 将单元素张量转换为Python数值correct += ...
: 累加所有批次的正确预测数
-
计算并打印准确率
print(f'Test Accuracy: {100. * correct / len(test_loader.dataset):.2f}%')
计算过程:
- 正确数:
correct
(所有批次累加的结果) - 总数:
len(test_loader.dataset)
= 10,000(MNIST测试集大小) - 准确率: \(\frac{\text{正确数}}{10000} \times 100%\)
格式化输出:
100. *
: 转换为百分比:.2f%
: 保留两位小数,加上百分号- 示例输出:
Test Accuracy: 98.76%
- 正确数:
-
完整流程
- 准备阶段: 设置评估模式 + 禁用梯度
- 预测阶段: 遍历所有测试数据,进行前向传播
- 评估阶段: 比较预测结果与真实标签
- 统计阶段: 计算总体准确率
-
总结
这段测试代码的核心思想是:
- 确保一致性:使用评估模式,保持测试结果稳定
- 提高效率:禁用梯度计算,节省资源和时间
- 准确评估:统计所有测试样本的预测准确率
7、主训练循环
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device) # 将模型移动到GPU(如果可用)for epoch in range(1, 6): # 训练5轮train(model, device, train_loader, optimizer, epoch)test(model, device, test_loader)
-
设备检测与分配
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
这行代码的作用:自动检测并选择最佳的计算设备
-
模型转移到设备
model.to(device) # 将模型移动到GPU(如果可用)
这行代码的作用:将模型的所有参数和缓冲区移动到指定设备
具体操作:
- 模型参数:权重矩阵、偏置向量
- 缓冲区:BatchNorm的running mean和variance
- 所有层:卷积层、全连接层等的参数
为什么需要这个?
- 数据与模型必须在同一设备:否则无法计算
- GPU加速:如果可用,将模型移到GPU显存中
- 内存管理:CPU内存和GPU显存是分开的
-
训练循环
for epoch in range(1, 6): # 训练5轮
epoch的概念:一个epoch表示模型完整遍历一次训练数据集
参数详解:
-
range(1, 6)
:生成序列[1, 2, 3, 4, 5]
-
为什么从1开始:更人性化的显示(Epoch 1而不是Epoch 0)
-
5个epoch:对于MNIST这样的简单数据集,通常足够收敛
epoch vs batch:
概念 含义 示例 Epoch 完整遍历一次训练集 60,000张图片都训练一次 Batch 一次处理的样本数 每次处理64张图片 Iteration 完成一个batch的训练 60,000/64 ≈ 938次迭代/epoch
-
-
训练过程
train(model, device, train_loader, optimizer, epoch)
调用训练函数,执行以下操作:
- 设置训练模式:
model.train()
- 遍历所有batch:使用
train_loader
- 数据转移:
data.to(device)
- 前向传播:计算输出和损失
- 反向传播:计算梯度
- 参数更新:
optimizer.step()
设备传递的重要性:
将
device
参数传递给train
函数,确保:- 数据转移到正确设备
- 模型和数据在同一设备上计算
- 设置训练模式:
-
测试评估
test(model, device, test_loader)
在每个epoch后评估模型性能:
为什么要在每个epoch后测试?
- 监控训练进度:查看准确率是否提升
- 检测过拟合:如果训练准确率上升但测试准确率下降,说明过拟合
- 选择最佳模型:保存测试准确率最高的模型参数
测试过程:
- 设置评估模式:
model.eval()
- 禁用梯度:
with torch.no_grad()
- 计算准确率:在10,000张测试图片上评估
8、可视化预测结果
data, target = next(iter(test_loader)) # 取一批测试数据
with torch.no_grad():output = model(data)pred = output.argmax(dim=1)plt.figure(figsize=(8, 4))
for i in range(6):plt.subplot(2, 3, i+1)plt.imshow(data[i].cpu().squeeze(), cmap='gray') # 显示图像plt.title(f'Pred: {pred[i].item()}, True: {target[i].item()}')
plt.show()
-
获取测试数据
data, target = next(iter(test_loader)) # 取一批测试数据
这行代码做了三件事:
a)
iter(test_loader)
- 作用:将DataLoader转换为迭代器
- 原理:
test_loader
本身是一个可迭代对象,iter()
使其变成可逐个获取数据的迭代器
b)
next()
- 作用:获取迭代器的下一个元素(第一个批次)
- 返回:一个批次的测试数据
- 数据形状:
data
:[1000, 1, 28, 28]
← (批次大小, 通道, 高, 宽)target
:[1000]
← 对应的真实标签
c) 赋值
- data: 测试图像数据
- target: 对应的真实数字标签
-
模型预测(无梯度模式)
with torch.no_grad():output = model(data)pred = output.argmax(dim=1)
a)
with torch.no_grad():
- 作用:在这个代码块内禁用梯度计算
- 为什么需要:
- 节省内存:不存储计算图的中间结果
- 加速计算:避免不必要的梯度计算
- 测试阶段不需要更新参数,所以不需要梯度
b)
output = model(data)
- 作用:模型对这批数据进行预测
- 输出形状:
[1000, 10]
← (批次大小, 10个数字的得分) - 示例:
output[0] = [1.2, -0.5, 0.8, ..., 3.2]
c)
pred = output.argmax(dim=1)
- 作用:获取预测结果(取得分最高的类别)
- dim=1:在第二个维度(类别维度)上找最大值
- 输出形状:
[1000]
← 预测的数字(0-9) - 数学运算:对于每个样本,找到10个得分中最大的那个的索引
-
创建画布
plt.figure(figsize=(8, 4))
- plt.figure():创建一个新的图形窗口
- figsize=(8, 4):设置图形大小为8英寸宽、4英寸高
- 为什么这个尺寸:适合显示2行3列共6个子图
-
绘制子布
for i in range(6):plt.subplot(2, 3, i+1)
a)
range(6)
- 循环6次,显示6张图片
i
的值:0, 1, 2, 3, 4, 5
b)
plt.subplot(2, 3, i+1)
- 作用:创建子图网格
- 参数:
(行数, 列数, 当前位置)
- 布局:位置编号: 1 2 3
4 5 6 - i+1:因为子图位置从1开始编号
-
显示图像
plt.imshow(data[i].cpu().squeeze(), cmap='gray')
a)
data[i]
- 获取第i个样本的图像数据
- 形状:
[1, 28, 28]
← (通道, 高, 宽)
b)
.cpu()
- 作用:如果数据在GPU上,将其移回CPU
- 为什么需要:
matplotlib
只能在CPU上显示图像 - 保险做法:确保数据在正确的设备上
c)
.squeeze()
- 作用:去除维度为1的维度
- 输入:
[1, 28, 28]
- 输出:
[28, 28]
- 为什么需要:
imshow
期望2D或3D数组,不需要通道维度
d)
cmap='gray'
- 作用:使用灰度色彩映射
- 为什么:MNIST是灰度图像,不是彩色
-
设置标题
plt.title(f'Pred: {pred[i].item()}, True: {target[i].item()}')
a)
pred[i].item()
- pred[i]:第i个样本的预测结果(张量)
- .item():将单元素张量转换为Python数值
- 示例:如果预测为数字7,返回
7
b)
target[i].item()
- 第i个样本的真实标签
- 同样转换为Python数值
c) f-string格式化
- 显示格式:
Pred: 7, True: 7
(预测正确) - 或:
Pred: 1, True: 7
(预测错误)
-
显示图形
plt.show()
- 作用:渲染并显示所有子图
- 效果:弹出图形窗口显示6张图片及其预测结果
-
完整的数据流
原始数据 → 数据加载器 → 一个批次 → 模型预测 → 获取预测结果
↓
选择6个样本 → 转换为显示格式 → 绘制子图 → 显示结果 -
总结
这段代码完成了从模型预测到结果可视化的完整流程:
- 数据获取:从测试集中取一个批次
- 模型预测:禁用梯度,获取预测结果
- 结果解析:转换为人类可读的格式
- 可视化:创建子图,显示图像和预测对比
- 结果展示:渲染并显示最终结果