Pytorch入门(一)
发表于 2026 年 10 月 16 日 · 次浏览
本篇整理我第一次用 Google Colab 和 PyTorch 训练 Fashion-MNIST 时用到的代码,按实际操作顺序记录,记录一些函数用途和数据形状。
这个系列计划写三篇。本篇先跑通服饰分类的训练与评测;第二篇结合手算和小模型,解释 Tensor、梯度与多层计算;第三篇再整理自定义模型、实验对照和权重保存恢复。后两篇随实际学习进度补充。
任务是识别十类服饰。训练集有 60000 张,测试集有 10000 张,每张都是 28×28 的灰度图。模型使用一个隐藏层,基准配置为 Adam、学习率 0.001、Batch Size 64、训练五轮,第一次测试准确率为 86.91%。
为了先把一次训练看明白,选择了 Fashion-MNIST 配合单隐藏层的全连接神经网络。Fashion-MNIST 是数据集,图片和正确标签都已准备好,省去了收集和标注数据的工作;图片尺寸小,配合这个简单模型,训练和验证都比较快。服饰图片也方便直接观察,能把预测结果与自己的判断对照起来。模型结构只涉及输入、中间特征和类别分数,较好理解。深度学习入门有很多路线,这里选择计算量小、结果直观的一条。
本文中的代码片段有前后依赖。需要在 Notebook 中按顺序执行,后面的单元才能使用前面创建的变量。
1. 导入库与检查设备
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
print(torch.__version__)
print(torch.cuda.is_available())
if torch.cuda.is_available():
print(torch.cuda.get_device_name(0))
device = "cuda" if torch.cuda.is_available() else "cpu"这里用到四组工具,torch 负责 Tensor 和训练,nn 提供网络层,torchvision 加载和转换图片,plt 负责画图。DataLoader 来自 torch.utils.data,用于分批取数据。
__version__ 查看版本,cuda.is_available() 检查 CUDA 是否可用,get_device_name(0) 查看第一张 GPU。实验在 Colab 云端分配到了 Tesla T4,与本机有没有独显无关。
from torch import nn 只在当前命名空间中引入 nn。如果后面要写 torch.cuda...,仍然需要 import torch。
2. 加载数据与转换图片
train_data = datasets.FashionMNIST(
root="data",
train=True,
download=True,
transform=transforms.ToTensor()
)
test_data = datasets.FashionMNIST(
root="data",
train=False,
download=True,
transform=transforms.ToTensor()
)root 是保存目录,download=True 在缺少数据时下载。train=True/False 分别选择训练集和测试集。
ToTensor() 在这里将图片转成 Tensor,并把 0~255 的亮度缩放到 0~1。这个缩放有输入类型条件,不适用于所有转换。
image, label = train_data[0]
print(image.shape)
print(train_data.classes[label])
print(image.min().item(), image.max().item())train_data[0] 返回图片和整数标签,classes[label] 查类别名。本次主要分析的编号是 Pullover(2,套头上衣)、Coat(4,外套)和 Shirt(6,衬衫),完整类别可查看 train_data.classes。
图片形状与像素值
单张图片是 [1, 28, 28],依次表示通道、高、宽。这里的灰度通道只有一个,普通 RGB 图片通常有三个通道。
像素值表示某个位置的亮度。模型拿到的是这些数字;图片的位置关系由这些数字在 Tensor 中的排列保留。
| 属性或方法 | 当前用途 |
|---|---|
image.shape | 查看各个维度的大小 |
image.min() / image.max() | 查看最小、最大像素值 |
.item() | 从只有一个元素的 Tensor 中取出普通 Python 数值 |
image[0] | 选出灰度通道,得到用于画图的 [28, 28] 数据 |
3. DataLoader 如何分批读取
train_loader = DataLoader(
train_data,
batch_size=64,
shuffle=True
)
test_loader = DataLoader(
test_data,
batch_size=64,
shuffle=False
)
images, labels = next(iter(train_loader))
print(images.shape)
print(labels.shape)输出通常是 [64, 1, 28, 28] 和 [64]。前者是一批 64 张图片,后者是对应的 64 个答案。默认保留最后不足 64 张的批次,本实验最后一批有 32 张。
batch_size 控制每批样本数量。shuffle=True 会在每次开始遍历训练数据时重新随机排列索引;测试时使用固定顺序,便于查找样本。
iter(train_loader) 创建迭代器,next(...) 取出它的下一批。反复写 next(iter(train_loader)) 会反复创建新迭代器,不能用它代替完整遍历。顺序取完所有批次用 for。
enumerate(train_loader) 可以给批次附加编号,方便检查。len()、range()、iter()、next() 和 zip() 都是 Python 自带功能,不属于 PyTorch。
4. 创建模型与执行预测
model = nn.Sequential(
nn.Flatten(),
nn.Linear(784, 128),
nn.ReLU(),
nn.Linear(128, 10)
).to(device)| 调用 | 处理的事情 | 一批数据的输出形状 |
|---|---|---|
nn.Sequential(...) | 按顺序连接各层 | 由内部层决定 |
nn.Flatten() | 默认保留批次维度,展开后面的维度 | [64, 784] |
nn.Linear(784, 128) | 将每张图片的 784 个输入线性组合成 128 个数 | [64, 128] |
nn.ReLU() | 正数保留,负数置为 0 | [64, 128] |
nn.Linear(128, 10) | 将中间特征组合成十个类别分数 | [64, 10] |
model.to(device) | 将模型参数等搬到指定设备 | 不改变数据形状 |
Linear 默认包含权重和偏置。每个输出由输入乘各自的权重、相加,再加上偏置得到。ReLU 加入非线性,使这两层组合能表达更复杂的关系。
128 是中间特征数量,最终类别数仍然是 10。这些特征在训练中形成,没有提前指定哪个代表袖子。
images = images.to(device)
outputs = model(images) # [64, 10]
predictions = outputs.argmax(dim=1) # [64]
print(train_data.classes[predictions[0].item()])model(images) 做前向计算,输出原始类别分数 logits,可以为负数,不要求和为 1。argmax(dim=1) 沿类别维度为每张图片选出最高分的索引,outputs[0].argmax() 则只处理第一张。
.to(device) 用于搬运 Tensor,.cpu() 将其放回 CPU。
模型参数和输入需要放在兼容的设备上。重新运行模型创建单元会生成新参数,之前训练的权重不会自动保留。
实验还用过 torch.allclose(model(images), model(images)),按容差比较两次输出。本模型没有随机层,相同参数和输入会得到一致结果。
5. Loss、梯度与优化器
loss_function = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(
model.parameters(),
lr=0.001
)nn.CrossEntropyLoss() 创建分类损失函数。在本实验里,调用时接收 [批次大小, 10] 的 logits,以及 [批次大小] 的整数类别标签,默认返回批次平均损失。这里直接输入原始分数,无需先手动转成概率,详见 CrossEntropyLoss 文档。
model.parameters() 提供模型的参数,Adam 用它们建立优化器。重新创建模型后,也要重新创建优化器,否则旧优化器管理的仍是旧参数。
一批数据的训练顺序
images = images.to(device)
labels = labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = loss_function(outputs, labels)
loss.backward()
optimizer.step()| 代码 | 实际作用 |
|---|---|
optimizer.zero_grad() | 清除之前累计的梯度,当前实现也可能将梯度设为 None |
model(images) | 使用当前参数预测 |
loss_function(outputs, labels) | 用预测和标签计算错误程度 |
loss.backward() | 反向计算梯度,并累加到参数的 .grad |
optimizer.step() | 根据梯度和优化器状态更新参数 |
backward() 算完梯度后,还需要执行 step() 才更新权重。反复运行这几行而不重新读取数据,会一直训练同一批图片。PyTorch 训练教程 也将每批训练拆成这些步骤。
学习率 lr 控制整体更新步幅。基础梯度下降可写为下面的式子。
新参数 = 旧参数 - 学习率 × 梯度实验使用的 Adam 还会根据历史梯度调整实际步幅,因此每个权重的变化量不固定等于 lr。对话中也提到过 torch.optim.SGD(...),用于说明更基础的更新方式,没有实际拿它跑对照实验。
检查当前优化器实际使用的学习率用下面这行。
print(optimizer.param_groups[0]["lr"])修改 Notebook 中的代码文本后,必须执行相应单元才能创建新对象。这次曾把文本改成 0.1,但内存里的优化器仍在使用 0.001。
6. 完整训练循环与统计
一个 Epoch 是完整遍历一次训练集。这里每批通常有 64 张,60000 张共分成 938 批,每批更新一次。
epochs = 5
model.train()
for epoch in range(epochs):
total_loss = 0
correct_count = 0
total_count = 0
for batch_images, batch_labels in train_loader:
batch_images = batch_images.to(device)
batch_labels = batch_labels.to(device)
optimizer.zero_grad()
outputs = model(batch_images)
loss = loss_function(outputs, batch_labels)
loss.backward()
optimizer.step()
total_loss += loss.item() * len(batch_labels)
predictions = outputs.argmax(dim=1)
correct_count += (predictions == batch_labels).sum().item()
total_count += len(batch_labels)
average_loss = total_loss / total_count
accuracy = correct_count / total_count
print(
f"Epoch {epoch + 1}/{epochs} | "
f"Loss: {average_loss:.4f} | "
f"Accuracy: {accuracy:.2%}"
)model.train() 切换训练模式。默认 CrossEntropyLoss 返回一批的平均值,乘批次样本数再累计,最后除以总样本数,得到按样本加权的整轮平均 Loss,最后一个小批次也能正确统计。
准确率统计先用 predictions == batch_labels 得到布尔 Tensor,sum().item() 数出答对张数。单批次也可写成 (predictions == labels).float().mean(),将对错转成 1、0 后求平均。
训练准确率统计的是模型边更新边预测的过程,与最终模型统一评测的口径不同。:.4f、:.2% 是 Python 的小数与百分比格式。
7. 测试模式与关闭梯度
model.eval()
correct_count = 0
total_count = 0
with torch.no_grad():
for test_images, test_labels in test_loader:
test_images = test_images.to(device)
test_labels = test_labels.to(device)
outputs = model(test_images)
predictions = outputs.argmax(dim=1)
correct_count += (predictions == test_labels).sum().item()
total_count += len(test_labels)
print(correct_count, "/", total_count)
print(f"{correct_count / total_count:.2%}")model.eval() 设置评测模式,对 Dropout、BatchNorm 等层有影响。当前模型只用了 Flatten、Linear 和 ReLU,切换模式不会改变它们的计算。
torch.no_grad() 关闭这段计算的梯度记录,减少开销。它与 eval() 职责不同,eval() 本身不会关闭自动求导。
这段代码保持参数不变,还因为没有执行参数更新。即使在 no_grad() 中,显式修改参数或使用已有梯度执行优化器更新,仍然可以改变权重。
这里的两个累计语句必须放在批次循环内部,才能统计整个测试集。评测完继续训练前,再执行 model.train()。
8. 筛选错题
在评测循环中,先用 predictions = outputs.argmax(dim=1).cpu() 将预测编号放回 CPU,再与 CPU 上的原始标签比较。对每张图片使用下面的条件,将图片和两个编号存进列表。
if predictions[i].item() != test_labels[i].item():
mistakes.append((
test_images[i],
test_labels[i].item(),
predictions[i].item()
))若只看 Coat 被认成 Shirt,条件改为 test_labels[i].item() == 4 and predictions[i].item() == 6。前六张错题只能帮助观察,最常见错误需要统计全部测试数据。
9. 混淆矩阵的创建与读法
confusion = torch.zeros(10, 10, dtype=torch.int64)
model.eval()
with torch.no_grad():
for test_images, test_labels in test_loader:
outputs = model(test_images.to(device))
predictions = outputs.argmax(dim=1).cpu()
for true_label, predicted_label in zip(test_labels, predictions):
confusion[true_label.item(), predicted_label.item()] += 1torch.zeros() 创建全零 Tensor,dtype=torch.int64 指定元素为 64 位整数,适合记录次数。
本实验规定行是真实类别、列是预测类别。confusion[4, 6] 就是真实 Coat 被认成 Shirt 的次数。对角线是真实与预测一致的计数,其他格子记录具体错误。
它在这里是一张计数表,用不到矩阵乘法或求逆。第一次分析时,Coat 与 Pullover 双向混淆共 174 次,Coat 与 Shirt 共 202 次,后者比我看几张图片形成的印象更严重。
10. 画图只记常用入口
plt.imshow(image[0], cmap="gray")
plt.title(train_data.classes[label])
plt.axis("off")
plt.show()imshow() 显示图片,title() 写标题,axis("off") 隐藏坐标轴。显示混淆矩阵时,换成 plt.imshow(confusion.numpy(), cmap="Blues") 即可,颜色越深表示计数越大。
用 plt.subplots(2, 3) 创建画框,再通过 axes.flat 遍历。各小图调用 ax.imshow() 和 ax.set_title(),最后 plt.tight_layout() 调间距。set_xticks()/set_yticks() 设置类别刻度,ax.text() 可在格子内标注计数,排版参数用到时再查。
.numpy() 将这里的 CPU 计数 Tensor 转成 NumPy 数组,没有另外编写 NumPy 运算。
11. 本次结果与执行顺序
| 配置 | 测试准确率 |
|---|---|
| 学习率 0.001,五轮 | 86.91% |
| 学习率 0.000001,五轮 | 62.94% |
| 学习率 0.001,一轮 | 84.38% |
大学习率 0.1 的实验明显变差,但经历多次重跑,训练输出和最终测试值配对不清楚,未放进这张对照表。实验没有固定随机种子,小学习率组还额外执行过单批次更新,以上是学习记录,不能当严格性能比较。
第三阶段又训练过一套五轮模型,随后继续训练五轮。按两张混淆矩阵计算,准确率从 86.68% 到 87.70%。Coat 正确数从 692 增至 826,Shirt 却从 701 降至 565,整体提高,但是伴随某一类退步。
希望以后有机会继续接触。