罗四强头像
【AI 开发从入门到进阶 · 04】PyTorch 第一课:Tensor、自动求导与那套万能的「五步训练循环」封面图

【AI 开发从入门到进阶 · 04】PyTorch 第一课:Tensor、自动求导与那套万能的「五步训练循环」

目录

一、为什么选 PyTorch

安装

二、Tensor:会记账的 NumPy 数组

三、四块积木:搭一个神经网络只需要这四个类

四、完整实战:MNIST 手写数字识别

五、把「五步训练循环」背下来

六、可视化:看看模型究竟看对了什么

七、排错清单

八、本篇小结与下篇预告


一、为什么选 PyTorch

维度PyTorch说明
编程范式动态图(Define-by-Run)像写普通 Python 一样调试,print(tensor.shape) 随时可用
生态HuggingFace / PEFT / vLLM 全线优先支持第 08~10 篇的大模型工具链几乎都建立在它之上
学习成本nn.Module + 训练循环,概念少学会一次,CNN / RNN / Transformer 全通用

一句话:研究和大模型生态事实上已经统一到 PyTorch,先把这一篇吃透,后面几篇会非常顺。

安装

# CPU 版(本系列示例 CPU 均可运行)
pip install torch torchvision

# NVIDIA GPU 版:请到 pytorch.org 官网按 CUDA 版本选择对应命令
# 例如 CUDA 12.1: pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121

验证:

import torch
print(torch.__version__)
print("CUDA 可用:", torch.cuda.is_available())
print("设备数:", torch.cuda.device_count())

别在 conda install pytorch 和 pip install torch 之间混着来,容易装出两个版本互相覆盖。选定一种,一路走到底。


二、Tensor:会记账的 NumPy 数组

Tensor 的用法和 ndarray 几乎一致,但多了一个关键属性 requires_grad:

import torch

x = torch.tensor([[1., 2.], [3., 4.]], requires_grad=True)
w = torch.tensor([[2.], [1.]], requires_grad=True)

y = (x @ w).sum()     # 前向计算,PyTorch 同时在后台记录「计算图」
print(y)               # tensor(16., grad_fn=<SumBackward0>)

y.backward()           # 自动反向传播,求出所有叶子节点的梯度
print(x.grad)          # ∂y/∂x
print(w.grad)          # ∂y/∂w

输出里那个 grad_fn=<SumBackward0> 就是线索:每个由运算产生的 Tensor 都记得自己是怎么来的。调用 backward() 时,PyTorch 沿这张图反向走一遍链式法则,把梯度累加到 .grad 上。

三个必须记住的细节:

  1. 梯度是累加的,所以每个 batch 前要 optimizer.zero_grad(),否则上一批的梯度会混进来。
  2. loss.item() 取标量值用于打印/记录;直接对 loss 做统计会一直持有计算图,显存慢慢爆掉。
  3. 推理时用 torch.no_grad()(或 model.eval() 配 with torch.no_grad()),不再建图,省显存也更快。

常用对照表,从 NumPy 迁移零成本:

NumPyPyTorch
np.array(...)torch.tensor(...)
x.shapex.shape / x.size()
x.Tx.T / x.transpose(0, 1)
x @ wx @ w / torch.matmul(x, w)
np.concatenatetorch.cat
x.reshape(...)x.view(...) / x.reshape(...)
np.float32torch.float32

设备迁移统一用 .to(device):

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


三、四块积木:搭一个神经网络只需要这四个类

组件作用常用类
层 / 模型存放可训练参数nn.Module、nn.Linear、nn.ReLU、nn.Sequential
损失函数定义「错得多离谱」nn.CrossEntropyLoss(多分类)、nn.MSELoss(回归)、nn.BCEWithLogitsLoss(二分类)
优化器按梯度更新参数torch.optim.Adam、SGD
数据管道批量、打乱、并行加载Dataset、DataLoader

其中 nn.Module 是唯一需要自己继承的:

from torch import nn

class MLP(nn.Module):
    def __init__(self):
        super().__init__()                 # 必须调用,否则参数注册不上
        self.net = nn.Sequential(
            nn.Flatten(),                  # (B,1,28,28) -> (B,784)
            nn.Linear(28 * 28, 128),
            nn.ReLU(),                     # 非线性,没有它多层线性层等价于一层
            nn.Linear(128, 10),           # 输出 10 类的 logits,不用自己写 softmax
        )

    def forward(self, x):
        return self.net(x)                 # 只写前向,反向由 autograd 自动生成

新手高频坑:nn.CrossEntropyLoss 内部已经包含 log-softmax,所以模型最后一层绝不要再加 Softmax。加了等于做两次,训练会变得极难收敛。


四、完整实战:MNIST 手写数字识别

新建 mnist_mlp.py。首次运行会自动下载数据集(约 12MB);如果网络不通,可手动下载后放到 ./data 目录。

import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

# ---------- 1. 超参数与设备 ----------
BATCH_SIZE = 64
EPOCHS = 5
LR = 1e-3
device = "cuda" if torch.cuda.is_available() else "cpu"
torch.manual_seed(42)

# ---------- 2. 数据 ----------
transform = transforms.Compose([
    transforms.ToTensor(),                                  # PIL/HWC 0~255 -> CHW 0~1
    transforms.Normalize((0.1307,), (0.3081,)),            # MNIST 常用均值/标准差
])
train_set = datasets.MNIST("./data", train=True,  download=True, transform=transform)
test_set  = datasets.MNIST("./data", train=False, download=True, transform=transform)

train_loader = DataLoader(train_set, batch_size=BATCH_SIZE, shuffle=True,  num_workers=2)
test_loader  = DataLoader(test_set,  batch_size=256,       shuffle=False, num_workers=2)

# ---------- 3. 模型 / 损失 / 优化器 ----------
class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Flatten(),
            nn.Linear(28 * 28, 128), nn.ReLU(),
            nn.Linear(128, 64),      nn.ReLU(),
            nn.Linear(64, 10),
        )
    def forward(self, x):
        return self.net(x)

model = MLP().to(device)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=LR)

# ---------- 4. 训练 ----------
for epoch in range(EPOCHS):
    model.train()                       # 开启训练模式(Dropout/BN 生效)
    running_loss = 0.0
    for x, y in train_loader:
        x, y = x.to(device), y.to(device)

        # ↓↓↓ 五步训练循环 ↓↓↓
        optimizer.zero_grad()           # ① 清空上一批梯度
        logits = model(x)               # ② 前向传播
        loss = loss_fn(logits, y)       # ③ 计算损失
        loss.backward()                 # ④ 反向传播,autograd 求梯度
        optimizer.step()                # ⑤ 更新参数
        # ↑↑↑ 五步训练循环 ↑↑↑

        running_loss += loss.item() * y.size(0)

    print(f"Epoch {epoch+1}/{EPOCHS}  train_loss = {running_loss / len(train_set):.4f}")

# ---------- 5. 评估 ----------
model.eval()                            # 切换评估模式
correct = 0
with torch.no_grad():                   # 不建计算图
    for x, y in test_loader:
        x, y = x.to(device), y.to(device)
        correct += (model(x).argmax(dim=1) == y).sum().item()

print(f"测试集准确率: {correct / len(test_set):.4f}")

# ---------- 6. 保存 / 加载 ----------
torch.save(model.state_dict(), "mnist_mlp.pth")
# 加载: model.load_state_dict(torch.load("mnist_mlp.pth", map_location=device))

CPU 上 5 个 epoch 大约几分钟,测试集准确率通常在 97% 上下。一个三层全连接网络,什么都没手工设计,就能认出手写数字——这就是第 02 篇那个「下山」过程在几万个参数上同时发生的结果。


五、把「五步训练循环」背下来

上面代码里框出来的五行,是所有 PyTorch 项目的公共骨架:

optimizer.zero_grad()   →  logits = model(x)  →  loss = criterion(logits, y)
                        →  loss.backward()     →  optimizer.step()

第 05 篇的 CNN、第 06 篇的 LSTM、第 07 篇手写的 Transformer、第 10 篇的 LoRA 微调,改的都只是「模型长什么样」和「数据怎么来」,这个循环原封不动。建议直接存成编辑器代码片段。

配套的三个「模式开关」也一并记牢:

场景必须做的事
训练model.train()
验证 / 推理model.eval() + with torch.no_grad():
取损失数值loss.item(),不要用 loss 本身累加

六、可视化:看看模型究竟看对了什么

import matplotlib.pyplot as plt

images, labels = next(iter(test_loader))
model.eval()
with torch.no_grad():
    preds = model(images.to(device)).argmax(1).cpu()

fig, axes = plt.subplots(2, 5, figsize=(10, 4))
for ax, img, true, pred in zip(axes.ravel(), images[:10], labels[:10], preds[:10]):
    ax.imshow(img.squeeze(), cmap="gray")
    ax.set_title(f"true={true.item()} pred={pred.item()}",
                 color="green" if true == pred else "red")
    ax.axis("off")
plt.tight_layout()
plt.savefig("mnist_pred.png", dpi=120)
plt.show()

把分错的样本单独挑出来看,往往能发现标签噪声、书写歧义之类的问题——看错误样本比看准确率涨得多得多,这个习惯后面做 NLP 和大模型评估时同样重要。


七、排错清单

现象大概率原因
loss 一直是 nan学习率过大;输入没归一化;标签越界(分类标签要从 0 开始连续编号)
loss 完全不降忘了 zero_grad();最后一层误加 Softmax;学习率过小;标签与输出 shape 不匹配
训练好但测试差忘了 model.eval()(Dropout 还在随机丢弃);数据划分有泄漏
显存 OOMbatch_size 调小;累计 loss 时误用了 loss 而非 loss.item();推理没关 grad
Expected all tensors on same device某个输入或模型没 .to(device),逐项打印 .device 排查

八、本篇小结与下篇预告

本篇完成了从 NumPy 到 PyTorch 的迁移:Tensor 与 autograd 计算图、nn.Module 四件套、DataLoader 批量管道、以及那套万能的五步训练循环。同时明确了 train()/eval()、no_grad()、CrossEntropyLoss 内含 softmax 这几个最容易出错的细节。

不过全连接网络把 28×28 的图片直接拉平成一维,等于丢掉了「相邻像素有关系」这个关键先验。下一篇《CNN 卷积神经网络与图像分类实战》,我们引入卷积、池化、BN、数据增强,用 CIFAR-10 把准确率从"能用"推到"像样",并系统讲一遍训练调参技巧。


本系列持续更新,建议收藏 + 关注。代码基于 PyTorch 2.x 验证。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/weixin_42350212/article/details/170349754

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--