📑 本页目录(点开跳转)
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、梯度累加 |
✅ 检查点
.item()和.detach()的区别?为什么每步要zero_grad()?- 二分类的输出层该不该加 Sigmoid?为什么?
model.train()和model.eval()影响哪些层?torch.no_grad()的作用?- GPU 利用率忽高忽低怎么办?
👀 答案
.item()取出 Python 标量(切断梯度,日志用);.detach()返回张量但切断梯度图。梯度默认累加,不清零会把上一步的梯度混进来。- 不加。用
BCEWithLogitsLoss,它内部合并了 sigmoid + 交叉熵,数值更稳定(避免 log(0))。 - Dropout(train 时随机丢,eval 时全保留)和 BatchNorm(train 用当前 batch 统计,eval 用累积的 running 统计)。
- 不构建计算图 → 省显存、加速。验证/推理时必用。
- 数据加载是瓶颈。加
num_workers=4~8、pin_memory=True,把重的预处理提前做好。
🛑 可以停在这里
⚡ 走神救援
PyTorch 只需理解四件事:张量、模块、数据加载器、训练循环。张量=会自动求导的 numpy,⚠️三个坑:
.item()会切断梯度(记日志才用)、.detach()显式切断、⭐梯度默认累加,每步必须清零。⭐损失配对表是最高频的错误来源:二分类用BCEWithLogitsLoss,不加 Sigmoid;多分类用CrossEntropyLoss,不加 Softmax,标签传整数而不是 one-hot。💡"GPU 利用率忽高忽低"就是数据加载没配好——num_workers4–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