📑 本页目录(点开跳转)
15 / PyTorch 实战 · 边做边查
先跑通一次训练,
再按需要查写法。
这是一份实战手册,不是需要顺序背诵的 API 列表。先确认输入、输出和你要检查的结果。
训练循环只记三件事
预测与算损失,
反传与更新,独立验证。
每一步都有可观察的结果;不靠“没有报错”判断训练成功。
- 1看输入与输出
维度、类型、设备一致;损失是有限数。
- 2检查是否在学习
先用极小训练集检查,再正式训练。
- 3用验证集做选择
保留最佳权重,最后才报告测试结果。
第一次接触
我想先看一个能跑的小例子
从张量形状与一次自动求导开始。只需要 PyTorch,不需要 GPU。
去张量与求导示例 →已经会基本写法
我需要完整训练模板
乳腺癌二分类示例:数据划分、标准化、模型、训练和最佳权重恢复。依赖 NumPy、scikit-learn 和 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) # 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() 返回不可求导的 Python 标量,但不会销毁原张量的计算图
- .detach() 显式切断梯度(想阻断反传时用)
- 梯度默认累加 → 独立更新前清零;有意做梯度累积时则按累积周期清零
🏗️ 二、定义模型:两种写法
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) # 可以在这加残差、多分支等
损失函数配对表(⭐ 最高频的错误来源):
接口依据:CrossEntropyLoss 官方说明;求导与模式区别见 Autograd 官方说明。
| 任务 | 损失函数 | 输出层 | 注意 |
|---|---|---|---|
| 二分类 | BCEWithLogitsLoss |
1 个数,不加 Sigmoid | 内部含 sigmoid,数值更稳 |
| 多分类 | CrossEntropyLoss |
C 个数,不加 Softmax | 内部含 log_softmax;常规标签用整数索引,也支持与输出同形的浮点概率分布 |
| 回归 | MSELoss / L1Loss |
1 个数,不加激活 | 有离群值用 L1 或 SmoothL1 |
📦 三、数据加载
先用小张量演示内存数据。自定义 Dataset 的例子采用 NumPy 文件,每个文件存一条样本;使用时传入自己的文件路径。
from torch.utils.data import TensorDataset, DataLoader, Dataset
import torch
import numpy as np
# 简单情况:数据已在内存
X_tensor = torch.tensor([[1., 2.], [3., 4.], [5., 6.], [7., 8.]])
y_tensor = torch.tensor([[0.], [0.], [1.], [1.]])
ds = TensorDataset(X_tensor, y_tensor)
loader = DataLoader(ds, batch_size=32, shuffle=True,
num_workers=0) # 先跑通,再按瓶颈调整
print(next(iter(loader))[0].shape) # torch.Size([4, 2])
# 自定义 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 = torch.as_tensor(np.load(self.paths[i]), dtype=torch.float32)
if self.transform: x = self.transform(x)
return x, self.labels[i]
💡 GPU 利用率忽高忽低,可能来自加载瓶颈,也可能来自同步或小模型。先定位,再比较
num_workers=0/2/4。 CPU 到 CUDA 传输可测试pin_memory=True。Windows 多进程加载要正确使用主入口保护;小数据增加进程反而可能更慢。
🔁 四、完整训练模板(包含数据与模型准备)
运行条件:已安装 NumPy、scikit-learn 与 PyTorch;使用库自带数据,无需下载数据、无需 GPU。先划分训练/验证/测试,再只用训练集拟合标准化器。
本次校验范围:已做语法检查;当前预览环境没有 PyTorch 和 scikit-learn,未重跑此训练。下方历史日志不作为补全后模板逐项一致的承诺。
import numpy as np, torch, torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42)
X_train, X_val, y_train, y_val = train_test_split(
X_train, y_train, test_size=0.25, stratify=y_train, random_state=42)
scaler = StandardScaler().fit(X_train)
Xtr, Xva, Xte = [torch.tensor(scaler.transform(a), dtype=torch.float32)
for a in (X_train, X_val, X_test)]
ytr, yva, yte = [torch.tensor(a, dtype=torch.float32).reshape(-1, 1)
for a in (y_train, y_val, y_test)]
loader = DataLoader(TensorDataset(Xtr, ytr), batch_size=32,
shuffle=True, num_workers=0)
def make_model():
return nn.Sequential(nn.Linear(30, 64), nn.ReLU(),
nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, 1))
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 '待排查:容量、学习率、标签与训练步数'}")
# ========== 正式训练 + 早停 ==========
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:
# 跳出循环后统一恢复最佳权重
print(f"早停于 ep {ep}"); break
# 早停或跑满轮数,都恢复验证损失最小的一份权重
model.load_state_dict(best_state)
model.eval()
with torch.no_grad():
test_logits = model(Xte.to(device))
test_acc = ((test_logits > 0) == yte.to(device)).float().mean().item()
print(f"恢复最佳权重后的测试准确率:{test_acc:.1%}")
原文保留的历史日志(乳腺癌;原记录未完整保留模型与划分配置,不要求上面的模板输出相同数字):
历史小样本检查:10 个样本,loss=0.00000,记录为通过。这里只保留原文观察值。
| 轮数 | 训练损失 | 验证损失 | 验证准确率 | 学习率 | 末批梯度范数 |
|---|---|---|---|---|---|
| 0 | 0.6616 | 0.5970 | 84.8% | 9.99e-04 | 0.47 |
| 10 | 0.0677 | 0.0814 | 96.5% | 9.19e-04 | 0.38 |
| 30 | 0.0340 | 0.0684 | 96.5% | 4.74e-04 | 1.11 |
| 50 | 0.0324 | 0.0674 | 96.5% | 5.45e-05 | 0.03 |
原记录早停于第 51 轮、恢复最佳权重;最终测试准确率 96.5%。这不是补全后模板的新实测结果。
💡 注意
train一直在降而val在 ep 40 后基本不动 —— 早停正是在这时候起作用。
🚨 五、六个高频 Bug
| Bug | 症状 | 修 |
|---|---|---|
无意中漏掉 optimizer.zero_grad() |
梯度跨步累加,更新偏离预期 | 独立更新前清零;有意累积则按周期清零 |
忘了 model.eval() |
Dropout 仍随机丢弃,BN 可能使用/更新批统计 | 验证前切换 eval;它不自动关闭梯度记录 |
| 验证时仍记录梯度 | 可能增加内存占用和开销 | 按需使用 no_grad 或 inference mode |
| 输出层多加了 Sigmoid/Softmax | loss 下降异常缓慢 | 用 WithLogits 版损失,删掉激活 |
忘了 .to(device) |
报错 "expected all tensors on same device" | 模型和数据都要 to |
CrossEntropyLoss 标签类型/形状不匹配 |
可能报错或算错任务 | 常规用整数索引;概率标签须为同形浮点分布,各类和为 1 |
⚡ 六、提速三件套
下面是接入已有训练循环的写法片段,不是独立训练脚本。先定位瓶颈,再比较速度、显存与稳定性。梯度累积还需处理最后不足一个周期的批次,不能丢掉它们的梯度。
# 1. CUDA 混合精度:收益取决于硬件和算子,不保证显存减半
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 统计)。
- no_grad 关闭区域内的反向梯度记录,常用于验证推理以降低开销;不等于 eval,需要输入梯度的任务不能盲目关闭求导。
- 先判断是否是数据加载瓶颈,再比较进程数、预处理与传输设置;利用率低并不总靠增加 workers 解决。
🛑 可以停在这里
⚡ 走神救援
张量承载数据,模块定义运算,DataLoader 提供批次,训练循环负责更新。
- 二分类用 BCEWithLogitsLoss 时,输出层不要另加 Sigmoid;多分类 CrossEntropyLoss 直接接原始分数。
- 先用少量样本检查能否拟合;拟合不佳是排查线索,不是单凭 loss 阈值证明有 bug。
- 独立更新前清梯度,训练/验证切换模式,普通验证不记录梯度。
- 用验证集选最佳权重,再报告测试成绩;六类问题与三种提速写法都在上方,按需回查。
下一节 👉 16-特征工程基础.md