目录
一、为什么选 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()) |
| 别在 |
二、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 上。
三个必须记住的细节:
- 梯度是累加的,所以每个 batch 前要
optimizer.zero_grad(),否则上一批的梯度会混进来。 loss.item()取标量值用于打印/记录;直接对loss做统计会一直持有计算图,显存慢慢爆掉。- 推理时用
torch.no_grad()(或model.eval()配with torch.no_grad()),不再建图,省显存也更快。
常用对照表,从 NumPy 迁移零成本:
| NumPy | PyTorch |
|---|---|
np.array(...) | torch.tensor(...) |
x.shape | x.shape / x.size() |
x.T | x.T / x.transpose(0, 1) |
x @ w | x @ w / torch.matmul(x, w) |
np.concatenate | torch.cat |
x.reshape(...) | x.view(...) / x.reshape(...) |
np.float32 | torch.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 自动生成 |
| 新手高频坑: |
四、完整实战: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 还在随机丢弃);数据划分有泄漏 |
| 显存 OOM | batch_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




