🏠 总目录📚 本教程 15 · PyTorch 实战手册
📑 本页目录(点开跳转)

15 · PyTorch 实战手册

34 分钟 | ⭐ 可直接抄的模板 | 🔨 有完整代码


🎯 一句话

PyTorch 只需要理解四个东西:张量、模块、数据加载器、训练循环。 这一章给你一份验证过能跑的完整模板,以后所有项目从它开始改。


🧱 一、张量:会自动求导的 numpy

import torch

x = torch.tensor([[1., 2.], [3., 4.]])
print(x.shape, x.dtype, x.device)          # torch.Size([2,2]) torch.float32 cpu

# 和 numpy 几乎一样
x @ x.T          # 矩阵乘
x.sum(dim=0)     # 沿维度求和
x.view(-1)       # reshape(view 要求内存连续,不确定就用 reshape)

# 三个高频操作
x.unsqueeze(1)   # 加一个维度 (2,2) → (2,1,2)
x.squeeze()      # 去掉所有大小为1的维度
torch.cat([x, x], dim=0)   # 拼接

自动求导(第 8 章的框架版)

import torch
w = torch.tensor([2.0], requires_grad=True)
loss = (w * 3 - 6) ** 2
loss.backward()          # 自动算梯度
print(w.grad)            # tensor([0.]) —— w=2 时正好是最优

⚠️ 三个坑: - .item() 取标量值(会切断梯度,只在记录日志时用) - .detach() 显式切断梯度(想阻断反传时用) - 梯度默认累加 → 每步必须 optimizer.zero_grad()


🏗️ 二、定义模型:两种写法

import torch.nn as nn

# 写法1:Sequential —— 简单直筒结构
model = nn.Sequential(
    nn.Linear(30, 64), nn.ReLU(), nn.Dropout(0.2),
    nn.Linear(64, 32), nn.ReLU(),
    nn.Linear(32, 1),               # ⭐ 二分类不加 Sigmoid
)

# 写法2:继承 Module —— 需要控制前向逻辑时
class Net(nn.Module):
    def __init__(self, in_dim, n_class):
        super().__init__()
        self.backbone = nn.Sequential(nn.Linear(in_dim,64), nn.ReLU())
        self.head = nn.Linear(64, n_class)
    def forward(self, x):
        h = self.backbone(x)
        return self.head(h)          # 可以在这加残差、多分支等

损失函数配对表(⭐ 最高频的错误来源):

任务 损失函数 输出层 注意
二分类 BCEWithLogitsLoss 1 个数,不加 Sigmoid 内部含 sigmoid,数值更稳
多分类 CrossEntropyLoss C 个数,不加 Softmax 内部含 log_softmax;标签是整数不是 one-hot
回归 MSELoss / L1Loss 1 个数,不加激活 有离群值用 L1 或 SmoothL1

📦 三、数据加载

from torch.utils.data import TensorDataset, DataLoader, Dataset

# 简单情况:数据已在内存
ds = TensorDataset(X_tensor, y_tensor)
loader = DataLoader(ds, batch_size=32, shuffle=True,
                    num_workers=4, pin_memory=True)   # ⭐ 加速关键

# 自定义 Dataset:数据要现读现处理时
class MyDataset(Dataset):
    def __init__(self, paths, labels, transform=None):
        self.paths, self.labels, self.transform = paths, labels, transform
    def __len__(self):
        return len(self.paths)
    def __getitem__(self, i):
        x = load_and_process(self.paths[i])       # 读一条
        if self.transform: x = self.transform(x)
        return x, self.labels[i]

💡 第 11 章说的「GPU 利用率忽高忽低」 就是这里没配好: 加 num_workers(一般 4–8)、pin_memory=True,重活提前预处理。


🔁 四、完整训练模板(实测可跑,直接抄)

import numpy as np, torch, torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader

def set_seed(s=42):
    import random
    random.seed(s); np.random.seed(s); torch.manual_seed(s)
    torch.cuda.manual_seed_all(s)
set_seed()

device = "cuda" if torch.cuda.is_available() else "cpu"
model = make_model().to(device)
criterion = nn.BCEWithLogitsLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=60)

# ========== 第0步:先证明能过拟合 10 个样本(第11章)⭐ ==========
tx, ty = Xtr[:10].to(device), ytr[:10].to(device)
model.train()
for step in range(301):
    optimizer.zero_grad()
    loss = criterion(model(tx), ty)
    loss.backward(); optimizer.step()
print(f"10样本过拟合检查: loss={loss.item():.5f} "
      f"{'✅通过' if loss.item()<0.01 else '❌有bug,先别往下走'}")

# ========== 正式训练 + 早停 ==========
set_seed(); model = make_model().to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=60)

best, wait, patience, best_state = float("inf"), 0, 10, None
for ep in range(60):
    # --- 训练 ---
    model.train()                     # ⭐ 开启 Dropout/BN 训练模式
    total = 0.0
    for xb, yb in loader:
        xb, yb = xb.to(device), yb.to(device)
        optimizer.zero_grad()                                   # ⭐ 必须
        loss = criterion(model(xb), yb)
        loss.backward()
        gn = torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0).item()
        optimizer.step()
        total += loss.item() * len(xb)
    scheduler.step()

    # --- 验证 ---
    model.eval()                      # ⭐ 关闭 Dropout,BN 用running统计
    with torch.no_grad():             # ⭐ 不建计算图,省显存加速
        val_loss = criterion(model(Xva.to(device)), yva.to(device)).item()

    print(f"ep {ep:<3} train {total/len(Xtr):.4f} | val {val_loss:.4f} "
          f"| lr {optimizer.param_groups[0]['lr']:.2e} | grad {gn:.2f}")   # 第11章六个数字

    # --- 早停 ---
    if val_loss < best:
        best, wait = val_loss, 0
        best_state = {k: v.clone() for k, v in model.state_dict().items()}
    else:
        wait += 1
        if wait >= patience:
            model.load_state_dict(best_state)      # ⭐ 回滚最佳
            print(f"早停于 ep {ep}"); break

实测输出(乳腺癌数据集):

10样本过拟合检查: loss=0.00000 ✅通过

ep 0   train 0.6616 | val 0.5970 | acc 84.8% | lr 9.99e-04 | grad 0.47
ep 10  train 0.0677 | val 0.0814 | acc 96.5% | lr 9.19e-04 | grad 0.38
ep 30  train 0.0340 | val 0.0684 | acc 96.5% | lr 4.74e-04 | grad 1.11
ep 50  train 0.0324 | val 0.0674 | acc 96.5% | lr 5.45e-05 | grad 0.03
早停于 ep 51,回滚到最佳权重

最终测试准确率:96.5%

💡 注意 train 一直在降而 val 在 ep 40 后基本不动 —— 早停正是在这时候起作用。


🚨 五、六个高频 Bug

Bug 症状
忘了 optimizer.zero_grad() 训练完全不收敛 加上
忘了 model.eval() 验证/测试结果每次不同、偏低 加上
忘了 torch.no_grad() 验证时显存暴涨甚至 OOM 加上
输出层多加了 Sigmoid/Softmax loss 下降异常缓慢 WithLogits 版损失,删掉激活
忘了 .to(device) 报错 "expected all tensors on same device" 模型和数据都要 to
CrossEntropyLoss 传了 one-hot 报错或结果错 整数类别索引

⚡ 六、提速三件套

# 1. 混合精度(显存减半、速度快 1.5-3 倍,需要 GPU)
import torch
scaler = torch.amp.GradScaler()
with torch.amp.autocast("cuda"):
    loss = criterion(model(xb), yb)
scaler.scale(loss).backward()
scaler.step(optimizer); scaler.update()

# 2. 梯度累积(小显存模拟大 batch)
loss = criterion(model(xb), yb) / accum
loss.backward()
if (i+1) % accum == 0:
    optimizer.step(); optimizer.zero_grad()

# 3. 编译加速(PyTorch 2.0+)
model = torch.compile(model)

🔗 这一章连到哪里

去哪 为什么
AI基础设施 09 ⭐ 撞上 OOM 之后的下一步:梯度检查点、offload、累加,各省多少各花多少
AI基础设施 07 ⚠️ torch.compile 为什么快,以及随手加的 .item() / print 会怎么把图切断
Kaggle 02 同一份模板在竞赛里的加料版:混合精度、EMA、梯度累加

✅ 检查点

  1. .item().detach() 的区别?为什么每步要 zero_grad()
  2. 二分类的输出层该不该加 Sigmoid?为什么?
  3. model.train()model.eval() 影响哪些层?
  4. torch.no_grad() 的作用?
  5. GPU 利用率忽高忽低怎么办?
👀 答案
  1. .item() 取出 Python 标量(切断梯度,日志用);.detach() 返回张量但切断梯度图。梯度默认累加,不清零会把上一步的梯度混进来。
  2. 不加。用 BCEWithLogitsLoss,它内部合并了 sigmoid + 交叉熵,数值更稳定(避免 log(0))。
  3. Dropout(train 时随机丢,eval 时全保留)和 BatchNorm(train 用当前 batch 统计,eval 用累积的 running 统计)。
  4. 不构建计算图 → 省显存、加速。验证/推理时必用。
  5. 数据加载是瓶颈。加 num_workers=4~8pin_memory=True,把重的预处理提前做好。

🛑 可以停在这里

走神救援

PyTorch 只需理解四件事:张量、模块、数据加载器、训练循环张量=会自动求导的 numpy,⚠️三个坑:.item() 会切断梯度(记日志才用)、.detach() 显式切断、⭐梯度默认累加,每步必须清零。⭐损失配对表是最高频的错误来源二分类用 BCEWithLogitsLoss,不加 Sigmoid多分类用 CrossEntropyLoss,不加 Softmax,标签传整数而不是 one-hot。💡"GPU 利用率忽高忽低"就是数据加载没配好——num_workers 4–8、开 pin_memory训练模板五要素:⭐第 0 步先证明能过拟合 10 个样本(跑 301 步,loss 要降到 0.01 以下,否则就是有 bug、先别往下走)、每步清梯度、训练用 train / 验证用 eval 模式、验证包在 no_grad() 里、早停并回滚最佳权重实测(乳腺癌):10 样本检查 loss=0.00000 通过 → 第 10 轮 val 0.0814、准确率 96.5%早停于第 51 轮、回滚最佳权重,最终测试 96.5%;💡train 一直在降而 val 从第 40 轮后不动,早停正是这时起作用。⚠️六个高频 bug:忘清梯度→完全不收敛;忘 eval→验证每次不同且偏低;忘 no_grad()显存暴涨甚至 OOM输出层多加 Sigmoid/Softmax→loss 降得异常慢;忘 .to(device)→张量不在同一设备;交叉熵传 one-hot→报错。提速三件套:混合精度(显存减半、快 1.5–3 倍)、梯度累积、torch.compile

下一节 👉 16-特征工程基础.md

打卡记录保存在你的浏览器里,首页能看到总进度