‹ 目录
#AI#PyTorch#学习记录

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. 导入库与检查设备

python
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. 加载数据与转换图片

python
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。这个缩放有输入类型条件,不适用于所有转换。

python
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 如何分批读取

python
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. 创建模型与执行预测

python
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。这些特征在训练中形成,没有提前指定哪个代表袖子。

python
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、梯度与优化器

python
loss_function = nn.CrossEntropyLoss()

optimizer = torch.optim.Adam(
    model.parameters(),
    lr=0.001
)

nn.CrossEntropyLoss() 创建分类损失函数。在本实验里,调用时接收 [批次大小, 10] 的 logits,以及 [批次大小] 的整数类别标签,默认返回批次平均损失。这里直接输入原始分数,无需先手动转成概率,详见 CrossEntropyLoss 文档。

model.parameters() 提供模型的参数,Adam 用它们建立优化器。重新创建模型后,也要重新创建优化器,否则旧优化器管理的仍是旧参数。

一批数据的训练顺序

python
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 控制整体更新步幅。基础梯度下降可写为下面的式子。

text
新参数 = 旧参数 - 学习率 × 梯度

实验使用的 Adam 还会根据历史梯度调整实际步幅,因此每个权重的变化量不固定等于 lr。对话中也提到过 torch.optim.SGD(...),用于说明更基础的更新方式,没有实际拿它跑对照实验。

检查当前优化器实际使用的学习率用下面这行。

python
print(optimizer.param_groups[0]["lr"])

修改 Notebook 中的代码文本后,必须执行相应单元才能创建新对象。这次曾把文本改成 0.1,但内存里的优化器仍在使用 0.001。

6. 完整训练循环与统计

一个 Epoch 是完整遍历一次训练集。这里每批通常有 64 张,60000 张共分成 938 批,每批更新一次。

python
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. 测试模式与关闭梯度

python
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 上的原始标签比较。对每张图片使用下面的条件,将图片和两个编号存进列表。

python
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. 混淆矩阵的创建与读法

python
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()] += 1

torch.zeros() 创建全零 Tensor,dtype=torch.int64 指定元素为 64 位整数,适合记录次数。

本实验规定行是真实类别、列是预测类别。confusion[4, 6] 就是真实 Coat 被认成 Shirt 的次数。对角线是真实与预测一致的计数,其他格子记录具体错误。

它在这里是一张计数表,用不到矩阵乘法或求逆。第一次分析时,Coat 与 Pullover 双向混淆共 174 次,Coat 与 Shirt 共 202 次,后者比我看几张图片形成的印象更严重。

10. 画图只记常用入口

python
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,整体提高,但是伴随某一类退步。

希望以后有机会继续接触。

评论 支持 Markdown · 邮箱选填(仅用于接收回复提醒)
正在连接终端评论服务...