实用技巧
学习目标
完成本节后,你将能够:
- 正确处理 CPU / GPU 设备切换
- 使用随机种子提升实验可复现性
- 理解混合精度训练和梯度裁剪的作用
- 会保存和恢复模型 checkpoint
- 建立一份 PyTorch 调试检查清单
一、先解决最常见的工程问题
1.1 设备切换:先别假设你一定有 GPU
很多初学者会直接把代码写死成 cuda(),结果在没有 GPU 的机器上直接报错。
更稳妥的写法是:
import torch
device = "cuda" if torch.cuda.is_available() else "cpu"
print("当前设备:", device)
x = torch.tensor([[1.0, 2.0], [3.0, 4.0]]).to(device)
print(x)
print("张量所在设备:", x.device)
你可以把 device 理解成“训练发生在哪张工作台上”:
- CPU:普通桌面
- GPU:并行运算的大工作台
1.2 固定随机种子:让实验尽量可复现
训练不稳定时,第一件事往往不是改模型,而是先固定随机性。
import random
import numpy as np
import torch
def set_seed(seed=42):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
set_seed(42)
print(torch.randn(3))
set_seed(42)
print(torch.randn(3))
如果两次打印结果一样,说明这部分随机性被固定住了。
为什么“尽量”而不是“绝对”?
有些 GPU 算子和并行细节仍然可能引入微小差异,所以可复现通常是“更接近”,不是“绝对一模一样”。
二、让训练过程更稳
2.1 train()、eval() 和 no_grad() 要形成肌肉记忆
训练与验证最容易写乱的地方,不是模型结构,而是模式切换。
标准习惯:
model.train() # 训练前
...
model.eval() # 验证 / 推理前
with torch.no_grad():
...
你可以把它理解成:
train():模型进入“练习模式”eval():模型进入“考试模式”no_grad():考试时不做反向传播草稿,节省内存
2.2 梯度裁剪:防止梯度突然爆掉
在 RNN、Transformer 或较深网络里,梯度有时会变得很大,导致训练不稳定。
梯度裁剪就是“给梯度设一个上限”。
import torch
from torch import nn
torch.manual_seed(42)
model = nn.Sequential(
nn.Linear(10, 20),
nn.ReLU(),
nn.Linear(20, 1)
)
x = torch.randn(32, 10)
y = torch.randn(32, 1) * 50
loss_fn = nn.MSELoss()
pred = model(x)
loss = loss_fn(pred, y)
loss.backward()
def grad_norm(model):
total = 0.0
for p in model.parameters():
if p.grad is not None:
total += p.grad.norm(2).item() ** 2
return total ** 0.5
before = grad_norm(model)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
after = grad_norm(model)
print("裁剪前梯度范数:", round(before, 4))
print("裁剪后梯度范数:", round(after, 4))
这就像给下坡的自行车加个限速器,避免冲得太猛。
三、让训练更快
3.1 混合精度训练(AMP):更省显存、更快
AMP 的核心思想是:
在合适的地方用更低精度计算,以换取更快速度和更低显存占用。
它尤其适合 GPU 训练。
为了保证下面代码在没有 GPU 的机器上也能直接运行,我们写成“有 GPU 就启用,没有就正常训练”。
import torch
from torch import nn
device = "cuda" if torch.cuda.is_available() else "cpu"
model = nn.Sequential(nn.Linear(16, 32), nn.ReLU(), nn.Linear(32, 1)).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
loss_fn = nn.MSELoss()
x = torch.randn(64, 16).to(device)
y = torch.randn(64, 1).to(device)
if device == "cuda":
scaler = torch.amp.GradScaler("cuda")
for _ in range(3):
optimizer.zero_grad()
with torch.amp.autocast("cuda"):
pred = model(x)
loss = loss_fn(pred, y)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
print("已使用 AMP 在 GPU 上完成 3 步训练")
else:
for _ in range(3):
optimizer.zero_grad()
pred = model(x)
loss = loss_fn(pred, y)
loss.backward()
optimizer.step()
print("当前无 GPU,使用普通精度完成 3 步训练")
3.2 Batch 太大怎么办?
如果你经常遇到显存不够:
- 先减小
batch_size - 再考虑 AMP
- 再考虑梯度累积
梯度累积的直觉是:
虽然一次吃不下大 batch,但可以分几口吃完,再统一更新一次。
四、保存和恢复训练进度
4.1 为什么 checkpoint 很重要?
训练随时可能因为这些原因中断:
- 断电
- Notebook 超时
- GPU 被回收
- 程序报错
checkpoint 就像“游戏存档”。
4.2 一个最小可运行示例
import torch
from torch import nn
model = nn.Linear(2, 1)
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
checkpoint_path = "demo_checkpoint.pt"
# 保存
torch.save({
"model_state_dict": model.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
"epoch": 5
}, checkpoint_path)
print("checkpoint 已保存:", checkpoint_path)
# 恢复
new_model = nn.Linear(2, 1)
new_optimizer = torch.optim.SGD(new_model.parameters(), lr=0.1)
ckpt = torch.load(checkpoint_path, map_location="cpu")
new_model.load_state_dict(ckpt["model_state_dict"])
new_optimizer.load_state_dict(ckpt["optimizer_state_dict"])
print("恢复的 epoch:", ckpt["epoch"])
真实项目里,通常会额外保存:
- 最优验证集指标
- 训练配置
- tokenizer / label mapping
五、调试时先看哪里?
5.1 形状(shape)永远排第一
PyTorch 里很多 bug,本质上都不是“模型太难”,而是:
- shape 不对
- dtype 不对
- device 不一致
训练前建议多打几行:
print("x shape:", x.shape)
print("y shape:", y.shape)
print("x dtype:", x.dtype)
print("x device:", x.device)
5.2 训练不下降时的检查顺序
可以按这个顺序查:
- 数据有没有读对
- 标签有没有对齐
- loss 有没有算对
optimizer.zero_grad()有没有写backward()和step()顺序对不对- 学习率是不是太大或太小
5.3 看到 nan 怎么办?
常见原因有:
- 学习率太大
- 输入尺度过大
- 梯度爆炸
- 除零或
log(0)等数值问题
最实用的第一反应:
- 降低学习率
- 打印 loss 和参数范围
- 打开梯度裁剪
六、一份适合保存的训练模板
model.train()
for batch_x, batch_y in train_loader:
batch_x, batch_y = batch_x.to(device), batch_y.to(device)
pred = model(batch_x)
loss = loss_fn(pred, batch_y)
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
model.eval()
with torch.no_grad():
for batch_x, batch_y in val_loader:
batch_x, batch_y = batch_x.to(device), batch_y.to(device)
pred = model(batch_x)
val_loss = loss_fn(pred, batch_y)
这个模板不花哨,但非常实用。
小结
这节课最重要的不是新 API,而是训练工程直觉:
- 设备别写死
- 随机种子先固定
train / eval / no_grad要分清- 大梯度要会裁
- 训练进度要会存
很多模型训练卡住,不是因为算法不会,而是这些“小工程细节”没处理好。