深度学习术语实操地图:从训练流程理解核心概念
1. 这不是术语表,而是一张深度学习的“操作地图”
刚入行那会儿,我花三天背完了一整页的深度学习术语表——activation、backpropagation、convolution、dropout、epoch、batch、learning rate……结果第一次跑模型时,导师问:“你调 learning rate 是按什么逻辑调的?”我卡住了。不是不会改数字,是根本没想明白这个数字背后连着梯度下降的步长、损失曲面的陡峭程度、权重更新的震荡风险,甚至和 batch size 共同决定了每轮迭代看到多少数据、走了多远、稳不稳。后来我才懂:术语不是用来背的,是用来“用”的;每一个词,都是你在模型世界里移动、观察、干预的一个坐标点。
这篇内容,就是为你画一张“可操作的术语地图”。它不按字母排序,不堆砌定义,而是以真实训练流程为轴线,把 deep learning terms 拆解成你每天在代码里敲、在日志里看、在实验中调、在故障里查的活生生的零件。你会看到:为什么 bias 不是“偏见”而是“截距项”,为什么 vanishing gradient 在 LSTM 里被门控机制“物理拦截”,为什么 overfitting 的典型症状不是训练 loss 高,而是验证 loss 突然掉头向上——这些不是教科书里的静态描述,而是我在调试 ResNet-50 分类器、调参 BERT 微调任务、排查 Transformer 解码器卡顿问题时,反复验证过的现场信号。
适合谁?如果你正在写第一行 model = Sequential() ,或者已经能手写 nn.Module 子类但总在 torch.nn.functional.cross_entropy 的 reduction 参数上犹豫,又或者你读论文时看到 “the attention weights are masked to prevent future token leakage” 仍需停顿三秒才反应过来这是防止信息穿越——那你就是这张地图的目标用户。它不假设你懂微积分,但默认你愿意在 PyTorch 或 TensorFlow 的终端里敲命令;它不回避数学符号,但每个公式旁边都配了实操注释,比如告诉你 ∂L/∂w = ∂L/∂a * ∂a/∂z * ∂z/∂w 这串链式法则,在 PyTorch 中对应 .backward() 调用后, layer.weight.grad 里存的就是最终结果。接下来的内容,全部基于真实项目场景展开,所有术语解释都锚定在你明天就要面对的代码、日志、曲线图上。
2. 术语不是孤立的单词,而是训练流程中的关键节点
2.1 从数据输入到预测输出:一条不可跳过的流水线
深度学习模型不是黑箱,而是一条精密装配线。理解术语,必须把它放回这条流水线里看它在哪一环起作用、怎么起作用、出错时在哪一环露马脚。我们以一个标准图像分类任务为例,从原始图片开始,走完一次前向传播(forward pass),再倒推一次反向传播(backward pass),把核心术语嵌入每个环节:
-
Input Layer(输入层) :这不是一个“层”,而是一个数据接口。你传入
(N, C, H, W)的张量,N 是 batch size,C 是通道数(RGB=3),H/W 是高宽。这里的关键术语是 tensor —— 它不是数学课本里的抽象概念,而是 GPU 显存里一块连续的、带 shape 和 dtype 的内存块。torch.tensor([1,2,3], dtype=torch.float32)创建的是 CPU 张量;tensor.cuda()才把它搬进显存。我踩过最深的坑,就是忘了.cuda(),模型在 GPU 上跑,数据还在 CPU,报错Expected all tensors to be on the same device,查了两小时才发现是这一步漏了。 -
Preprocessing(预处理) :在数据进模型前,必须做标准化(normalization)。常见操作是
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])。这里的 mean/std 不是随便选的,它们来自 ImageNet 数据集所有像素的统计均值和标准差。为什么必须用这个?因为预训练模型(如 ResNet)的权重,是在这个分布下收敛的。如果你用自己的 mean/std,相当于让模型看“戴了有色眼镜”的图,特征提取直接失真。实测过:用错 std,ResNet-18 在 CIFAR-10 上准确率从 94% 掉到 72%。 -
Hidden Layers(隐藏层) :这是模型的“肌肉”。每一层执行
z = Wx + b,其中 W 是权重矩阵(weight), b 是偏置向量(bias)。注意: bias 绝不是“偏见”,它是线性变换的截距项,让模型能拟合不过原点的函数。没有 bias,所有神经元输出都强制过零点,表达能力大打折扣。PyTorch 中nn.Linear(784, 128)默认bias=True,但如果你手动初始化nn.Linear(784, 128, bias=False),就得自己加nn.Parameter(torch.zeros(128)),否则模型可能根本学不动。 -
Activation Function(激活函数) :
z算出来后,必须过一个非线性函数,否则再多层也等价于单层线性变换。最常用的是 ReLU(Rectified Linear Unit) :f(z) = max(0, z)。它简单、计算快、缓解梯度消失。但它的致命缺陷是 dying ReLU problem :当z < 0时,导数为 0,该神经元永久死亡,不再更新。我调参时遇到过一个现象:训练几轮后,某一层的输出直方图里,一半是精确的 0,且再也不变——这就是大量神经元“死”了。解决方案不是换函数,而是用 Leaky ReLU :f(z) = max(0.01z, z),给负区间留个微小斜率,让梯度能漏过去。代码就一行:nn.LeakyReLU(negative_slope=0.01)。 -
Loss Function(损失函数) :前向走完,得到预测 logits,和真实标签比对,算出一个标量 loss 。分类任务常用 Cross-Entropy Loss 。它的数学形式是
-Σ y_i * log(p_i),其中y_i是 one-hot 标签,p_i是 softmax 后的概率。但 PyTorch 的nn.CrossEntropyLoss是个“组合技”:它内部先做log_softmax,再算nll_loss(Negative Log Likelihood),所以你传给它的输入是 raw logits,不是 softmax 后的概率!如果误传softmax(logits),loss 会爆炸,因为log(softmax)再log一次,数值极不稳定。这个坑,我见过至少三个同事栽过。 -
Optimization(优化) :最小化 loss,靠的是 optimizer 。最基础的是 SGD(Stochastic Gradient Descent) :
w = w - lr * ∂L/∂w。这里的 lr(learning rate) 是灵魂参数。它太大,loss 剧烈震荡甚至发散;太小,收敛慢如蜗牛。一个经验法则是:从1e-3开始试,用 learning rate finder (如 fastai 的lr_find)画出 loss vs lr 曲线,取曲线下降最快那段的中点。我调一个 ViT 模型时,初始 lr=1e-4,loss 十轮不降;换成 3e-3,三轮就降到 0.1 以下。 -
Backpropagation(反向传播) :这是整个流程的“引擎”。它用链式法则,从 loss 开始,逐层计算每个参数的梯度
∂L/∂w。关键术语是 gradient —— 它不是“梯度下降”的简称,而是损失函数对参数的偏导数,指示了参数该往哪边、走多远才能让 loss 变小。PyTorch 中.backward()就是触发这个计算,结果存在.grad属性里。注意:.backward()默认只对 scalar loss 起作用;如果你的 loss 是向量(比如 per-sample loss),必须加retain_graph=True或先loss.mean()。 -
Weight Update(权重更新) :optimizer 拿到梯度后,执行更新。SGD 就是
w -= lr * w.grad;Adam 更复杂,维护了梯度的一阶矩(momentum)和二阶矩(RMSProp)估计。这里的关键是 gradient clipping :当梯度异常大(如 RNN 训练中梯度爆炸),直接更新会让权重崩坏。所以必须加torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0),把所有梯度的 L2 范数裁剪到 1.0 以内。没加这句,我的一个 seq2seq 模型训到第三轮就nan了。 -
Evaluation(评估) :训练时看 loss,但最终看 accuracy 、 precision 、 recall 、 F1-score 。特别注意 validation set 和 test set 的区别:validation 用于调参(如早停、lr 调整),test 只能用一次,用于最终报告性能。混用它们,会导致模型在 test set 上“过拟合”,结果虚高。我曾因把 validation 当 test 用,论文被 reviewer 质疑泛化性,返修重跑实验两周。
这条流水线,就是所有 deep learning terms 的“主场”。每个术语都不是孤岛,而是流水线上一个有明确输入、输出、功能、故障模式的工位。接下来,我们聚焦几个最常被误解、也最影响实操效果的核心术语,拆开它的“内部结构”。
2.2 核心术语深度解剖:从定义到故障信号
2.2.1 Epoch, Batch, Iteration:别再数错你的训练次数
这三个词,是新手最容易混淆的“时间单位”。它们的关系,就像“年、月、日”:
-
Batch(批) :一次送进模型的数据量。比如你有 1000 张图,设
batch_size=32,那么一个 batch 就是 32 张图及其标签。它决定了每次forward和backward处理多少样本,直接影响 GPU 显存占用和梯度估计的噪声水平。batch_size太小(如 1),梯度噪声大,训练不稳定;太大(如 512),显存爆满,且梯度方向过于“平滑”,可能错过更优解。经验:从 32 或 64 开始,根据显存和 loss 曲线调整。 -
Iteration(迭代) :一次完整的
forward + backward + optimizer.step()。处理一个 batch,就算一次 iteration。继续上面的例子,1000 张图 / 32 = 31.25 → 实际 32 次 iteration 走完一个 epoch(最后一轮可能 padding 补足)。 -
Epoch(轮) :把整个训练集(1000 张图)完整过一遍,叫一个 epoch。它不等于 iteration 数,而是
total_samples / batch_size向上取整。所以 epoch 是一个“数据遍历”概念,iteration 是一个“计算步骤”概念。监控训练时,横轴用 epoch 更合理,因为它代表了模型“看了多少遍数据”;但写日志时,iteration 编号更精确,因为不同 epoch 的 iteration 数可能因数据 shuffle 而微调。
提示:PyTorch 的
DataLoader里shuffle=True是每个 epoch 开始前打乱数据顺序,确保模型每轮看到的数据分布一致。但如果你在__getitem__里写了random.random(),就会导致同一个 epoch 内数据顺序也乱,loss 曲线锯齿状抖动,这不是模型问题,是数据加载 bug。
2.2.2 Overfitting vs Underfitting:从曲线形状读懂模型健康状态
这两个术语,是模型诊断的“听诊器”。它们不靠肉眼判断,而靠训练 loss 和验证 loss 的两条曲线走势:
-
Underfitting(欠拟合) :训练 loss 高,验证 loss 也高,且两者差距小(< 0.01)。说明模型太“笨”,连训练数据都没学好。原因通常是:模型太浅(层数少)、激活函数太弱(如全用 sigmoid)、正则太强(如 dropout=0.8)。对策:加深网络、换 ReLU、减小 dropout、增大学习率。
-
Overfitting(过拟合) :训练 loss 持续下降,验证 loss 先降后升,形成一个“U”形谷底。谷底位置就是最佳训练轮次。此时模型把训练数据的噪声和细节都记住了,但泛化不了新数据。原因:模型太复杂、训练太久、数据太少、正则太弱。对策:加 dropout、L2 权重衰减(
weight_decay)、数据增强(transforms.RandomHorizontalFlip())、早停(early stopping)。
我调一个医学影像分割模型时,验证 Dice 系数在第 42 轮达到峰值 0.87,之后缓慢下降;而训练 Dice 一直升到 0.93。我果断在 42 轮保存模型,测试集得分 0.85;如果硬训到 100 轮,测试集跌到 0.79。这就是 overfitting 的典型代价。
2.2.3 Regularization(正则化):不是“加点东西”,而是“控制复杂度”
正则化不是魔法,它的数学本质是:在 loss 函数里,给模型复杂度加一个惩罚项。标准 cross-entropy loss 是 L_ce ,加 L2 正则后变成 L = L_ce + λ * Σw² 。 λ (lambda)就是正则强度, Σw² 是所有权重的平方和。 λ 越大,模型越“怕”让权重变大,被迫用更小的权重去拟合,从而降低复杂度。
-
Dropout :训练时,随机“关掉”一部分神经元(设其输出为 0),强迫网络不依赖特定神经元,提升鲁棒性。注意:它只在
train()模式生效;eval()模式下自动关闭。如果你在验证时忘了model.eval(),dropout 还开着,验证 loss 会虚高,误判模型不行。 -
Batch Normalization(BN) :不是正则化,但常被误用。它对每层输入做归一化:
x' = (x - μ) / √(σ² + ε),再学两个参数γ, β做缩放和平移。BN 的核心作用是稳定训练,让每层输入分布更一致,从而允许用更大的 learning rate。但它有个陷阱:BN 的μ和σ²在训练时用当前 batch 统计,在推理时用整个训练集的移动平均。所以model.eval()不仅关 dropout,还切换 BN 的统计量来源。漏掉它,推理结果完全错误。
2.2.4 Optimizer(优化器):SGD、Adam、RMSProp 的选择逻辑
别被名字吓住,它们都是 SGD 的变种,核心区别在于“怎么用梯度”:
-
SGD :最朴素,
w = w - lr * g(g 是梯度)。优点:简单、内存省、有时泛化更好;缺点:需要精细调 lr,容易卡在鞍点。 -
Momentum SGD :加了“惯性”,
v = β*v + (1-β)*g; w = w - lr*v。β通常 0.9,让更新方向更平滑,加速穿过平坦区。它像骑自行车下坡,有惯性,不易停。 -
Adam :融合了 Momentum(一阶矩估计)和 RMSProp(二阶矩估计)。
m = β1*m + (1-β1)*g; v = β2*v + (1-β2)*g²; w = w - lr * m/(√v + ε)。β1=0.9,β2=0.999是默认值。它几乎成了默认选择,因为自适应 lr,对超参不敏感。但它的缺点是:在稀疏梯度场景(如 NLP embedding),二阶矩估计可能不准,导致后期收敛慢。这时可换 AdamW ,它把 weight decay 从 loss 里拿出来,单独加在权重更新上,避免了 Adam 原生实现中 weight decay 和梯度更新耦合的问题。
注意:Adam 的
lr不能设得太大。因为它的自适应机制会让有效学习率在初期放大。我试过lr=1e-2,loss 直接 nan;换成1e-3,一切正常。所以,即使用了 Adam,也要从1e-3开始试。
3. 实操过程与核心环节实现:从零搭建一个可调试的训练循环
3.1 构建一个“透明”的训练循环:每一步都可监控、可打断、可复现
一个健壮的训练循环,不是 for epoch in range(epochs): 然后一堆 model.train() 就完事。它必须像一台精密仪器,每个部件的状态都可读、可写、可验。下面是我用 PyTorch 写的标准模板,已用于 5 个以上项目,核心是“三可”: 可监控、可打断、可复现 。
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
import numpy as np
import random
# 1. 固定随机种子:保证可复现性
def set_seed(seed=42):
torch.manual_seed(seed)
np.random.seed(seed)
random.seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True # 确保 CUDA 卷积确定性
torch.backends.cudnn.benchmark = False # 关闭 benchmark,避免不同次运行算法不同
set_seed(42)
# 2. 数据加载:带进度条和错误捕获
def create_dataloaders(train_dataset, val_dataset, batch_size=32, num_workers=4):
train_loader = DataLoader(
train_dataset,
batch_size=batch_size,
shuffle=True,
num_workers=num_workers,
pin_memory=True, # 加速 GPU 数据传输
drop_last=True # 防止最后一个 batch size 不足,导致 BN 统计异常
)
val_loader = DataLoader(
val_dataset,
batch_size=batch_size,
shuffle=False,
num_workers=num_workers,
pin_memory=True
)
return train_loader, val_loader
# 3. 模型、损失、优化器初始化
model = YourModel() # e.g., ResNet18()
criterion = nn.CrossEntropyLoss(label_smoothing=0.1) # 加 label smoothing 防 overfitting
optimizer = optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.01)
scheduler = optim.lr_scheduler.OneCycleLR( # 用 OneCycleLR,比 step LR 更高效
optimizer, max_lr=3e-4, epochs=100, steps_per_epoch=len(train_loader)
)
# 4. 主训练循环:核心是“状态字典”和“日志”
def train_epoch(model, train_loader, criterion, optimizer, scheduler, device):
model.train()
total_loss = 0
correct = 0
total = 0
# tqdm 进度条,显示实时 loss
from tqdm import tqdm
pbar = tqdm(train_loader, desc="Training")
for batch_idx, (data, target) in enumerate(pbar):
data, target = data.to(device), target.to(device)
# 前向传播
output = model(data)
loss = criterion(output, target)
# 反向传播
optimizer.zero_grad() # 清空上一轮梯度
loss.backward()
# 梯度裁剪:防止 RNN/LSTM 梯度爆炸
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# 参数更新
optimizer.step()
scheduler.step() # OneCycleLR 每 step 更新 lr
# 统计
total_loss += loss.item()
_, predicted = output.max(1)
total += target.size(0)
correct += predicted.eq(target).sum().item()
# 实时更新进度条
pbar.set_postfix({
'loss': f'{loss.item():.4f}',
'acc': f'{100.*correct/total:.2f}%'
})
return total_loss / len(train_loader), 100. * correct / total
def validate(model, val_loader, criterion, device):
model.eval() # 关键!关闭 dropout 和 BN 的训练模式
val_loss = 0
correct = 0
total = 0
with torch.no_grad(): # 关闭梯度计算,省显存
for data, target in val_loader:
data, target = data.to(device), target.to(device)
output = model(data)
loss = criterion(output, target)
val_loss += loss.item()
_, predicted = output.max(1)
total += target.size(0)
correct += predicted.eq(target).sum().item()
return val_loss / len(val_loader), 100. * correct / total
# 5. 完整训练主函数:集成早停、模型保存、日志记录
def main_train():
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
# 初始化最佳验证精度和早停计数器
best_val_acc = 0.0
patience_counter = 0
patience = 10 # 连续 10 轮没提升,就早停
# 日志列表,用于画图
train_losses, val_losses = [], []
train_accs, val_accs = [], []
for epoch in range(100):
print(f'\nEpoch {epoch+1}/100')
# 训练
train_loss, train_acc = train_epoch(model, train_loader, criterion, optimizer, scheduler, device)
train_losses.append(train_loss)
train_accs.append(train_acc)
# 验证
val_loss, val_acc = validate(model, val_loader, criterion, device)
val_losses.append(val_loss)
val_accs.append(val_acc)
print(f'Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.2f}%')
print(f'Val Loss: {val_loss:.4f} | Val Acc: {val_acc:.2f}%')
# 早停逻辑
if val_acc > best_val_acc:
best_val_acc = val_acc
patience_counter = 0
# 保存最佳模型
torch.save({
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'val_acc': val_acc,
}, 'best_model.pth')
print('=> Saved new best model')
else:
patience_counter += 1
print(f'=> Patience counter: {patience_counter}/{patience}')
if patience_counter >= patience:
print(f'Early stopping at epoch {epoch+1}')
break
# 训练结束,画图
import matplotlib.pyplot as plt
plt.figure(figsize=(12,4))
plt.subplot(1,2,1)
plt.plot(train_losses, label='Train Loss')
plt.plot(val_losses, label='Val Loss')
plt.legend()
plt.title('Loss Curve')
plt.subplot(1,2,2)
plt.plot(train_accs, label='Train Acc')
plt.plot(val_accs, label='Val Acc')
plt.legend()
plt.title('Accuracy Curve')
plt.savefig('training_curves.png')
plt.show()
if __name__ == "__main__":
main_train()
这段代码的价值,不在于它多炫酷,而在于它把所有 deep learning terms 都落到了具体操作上:
label_smoothing=0.1:这是正则化的一种,让模型不要对某个类别 100% 确信,防 overfitting;pin_memory=True:利用 CUDA 的 pinned memory,加速 CPU 到 GPU 的数据搬运;drop_last=True:确保每个 batch size 严格一致,避免 BN 层因 batch size 变小而统计失效;torch.no_grad():明确告诉 PyTorch 这段代码不参与反向传播,省显存、提速度;model.eval():不只是“不训练”,更是切换 BN 和 Dropout 的行为模式;OneCycleLR:一种高级学习率调度,先升后降,比传统 step LR 收敛更快。
3.2 关键环节调试实战:从 loss 曲线定位问题根源
训练不是“跑起来就行”,而是持续诊断的过程。loss 曲线就是你的“心电图”,不同形态指向不同病因:
| Loss 曲线形态 | 典型表现 | 最可能原因 | 快速验证方法 | 我的实操对策 |
|---|---|---|---|---|
| Loss = nan | 第一轮 loss 就是 nan |
学习率过大、梯度爆炸、数据含 inf / nan |
print(torch.isnan(data).any()) , print(torch.isnan(model(data)).any()) |
1. 降 lr 至 1e-5 ;2. 加 clip_grad_norm_ ;3. 检查数据 pipeline, transforms.ToTensor() 前加 np.clip(img, 0, 255) |
| Loss 不降 | 几十轮后 loss 仍 > 5.0(分类) | 模型未启动(权重全零)、学习率过小、数据标签错乱 | print(model.layer1[0].weight.mean()) 看权重是否更新; print(target[:5]) 看标签是否在 [0,C-1] |
1. 检查 optimizer.step() 是否被注释;2. 用 lr_find 找合适 lr;3. assert target.min() >= 0 and target.max() < num_classes |
| Loss 剧烈震荡 | loss 在 0.5~3.0 间无规律跳变 | batch_size 太小、lr 过大、数据噪声大 | 计算 loss.std() / loss.mean() ,若 > 0.3 则震荡严重 |
1. batch_size 翻倍;2. lr 降半;3. 加 Dropout(p=0.2) |
| Train loss ↓, Val loss ↑ | 典型 U 形,val loss 在第 25 轮达最低 | 过拟合 | print('Train acc:', train_acc, 'Val acc:', val_acc) ,若 gap > 5% 即过拟合 |
1. 加 weight_decay=1e-4 ;2. Dropout(p=0.5) ;3. 早停在 val loss 最低点 |
| Train & Val loss 都高且平稳 | loss 停在 2.3 左右不降(CIFAR-10) | 欠拟合、模型容量不足、数据未归一化 | print(data.mean(), data.std()) ,应接近 (0.485, 0.229) |
1. 换更深模型(ResNet34→50);2. 检查 transforms.Normalize 参数;3. 增大学习率 |
我调一个目标检测模型时,loss 曲线呈剧烈锯齿状, std/mean ≈ 0.8 。我第一反应是 lr 太大,但降了三次 lr 效果甚微。最后发现是数据增强里的 RandomRotation 角度设成了 90 ,导致大量图像被旋转 90 度后,目标框坐标超出图像边界, target 里出现负坐标, loss 计算时 log(0) 导致 nan 。修复方法:在 collate_fn 里加坐标裁剪 target[:, [0,2]] = target[:, [0,2]].clamp(0, w); target[:, [1,3]] = target[:, [1,3]].clamp(0, h) 。这个教训告诉我: 术语要落地到数据 pipeline 的每一行代码里 。
3.3 模型保存与加载:不只是 torch.save() ,而是状态的完整快照
新手常犯的错误是:只保存 model.state_dict() ,加载时却忘了 model.eval() ,或忘了 set_seed() ,导致结果不一致。一个生产级的保存/加载,必须包含:
- 模型结构 :
model.__class__或重新 import 模型类; - 模型权重 :
model.state_dict(); - 优化器状态 :
optimizer.state_dict()(含 momentum 缓存); - 学习率调度器状态 :
scheduler.state_dict(); - 随机种子 :
torch.get_rng_state(); - 当前 epoch 和最佳指标 :用于断点续训。
# 保存完整状态
def save_checkpoint(model, optimizer, scheduler, epoch, val_acc, filename):
checkpoint = {
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'scheduler_state_dict': scheduler.state_dict(),
'val_acc': val_acc,
'rng_state': torch.get_rng_state(), # 保存当前随机状态
}
torch.save(checkpoint, filename)
# 加载完整状态
def load_checkpoint(model, optimizer, scheduler, filename):
checkpoint = torch.load(filename)
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
scheduler.load_state_dict(checkpoint['scheduler_state_dict'])
torch.set_rng_state(checkpoint['rng_state']) # 恢复随机状态
start_epoch = checkpoint['epoch'] + 1
best_val_acc = checkpoint['val_acc']
return start_epoch, best_val_acc
# 使用示例
start_epoch, best_acc = load_checkpoint(model, optimizer, scheduler, 'checkpoint.pth')
for epoch in range(start_epoch, 100):
# ... 训练代码
注意:
torch.save()保存的是 Python 对象,如果模型类定义在__main__里(即脚本里直接写的 class),加载时会报AttributeError: Can't get attribute 'YourModel' on <module '__main__'>。解决方法:把模型类定义在独立的models.py文件里,然后from models import YourModel。这是工程实践的铁律。
4. 常见问题与排查技巧实录:那些文档里不会写的“血泪经验”
4.1 “CUDA out of memory”:不是显存不够,而是显存没管好
这个报错,90% 的情况不是你 GPU 显存真不够,而是 PyTorch 没释放。根本原因是:Python 的垃圾回收(GC)不及时, tensor 对象虽然没用了,但显存没被释放。
- 现象 :训练几轮后突然报
CUDA out of memory,但nvidia-smi显示显存占用只有 80%,且torch.cuda.memory_allocated()返回值持续增长。 - 根因 :你在循环里创建了中间 tensor(如
loss = criterion(output, target)后,又做了loss.item(),但loss对象本身还在内存里);或者用了torch.no_grad()但忘了del掉大 tensor。 - 实操解法 :
- 在每个 iteration 结束时,手动
del所有大 tensor:del data, target, output, loss; - 强制 GC:
import gc; gc.collect(); - 清空 CUDA 缓存:
torch.cuda.empty_cache(); - 最狠一招:用
with torch.no_grad():包裹所有不需要梯度的计算,并在块内del。
- 在每个 iteration 结束时,手动
我调一个 3D 医学影像模型时,显存从 12G 涨到 24G 报错。加了 del 和 gc.collect() 后,稳定在 14G。记住: PyTorch 不是自动内存管家,你是 。
4.2 “RuntimeError: Input type (torch.cuda.FloatTensor) and weight type (torch.FloatTensor) should be the same”:设备不一致的隐形杀手
这个报错,表面是 tensor 类型不匹配,深层是数据和模型没放在同一设备。
- 现象 :
model.cuda()了,但data还在 CPU;或者model.cpu()了,data.cuda()了。 - 根因 :
DataLoader返回的data默认在 CPU,你必须显式.to(device);而model的device是属性,不是方法,model.device并不存在,得自己记。 - 实操解法 :
- 统一定义
device = torch.device("cuda" if torch.cuda.is_available() else "cpu"); model.to(device);- 每次
data, target = data.to(device), target.to(device); - (进阶)写一个
to_device()函数,递归把dict或list里的所有 tensor 移到 device。
- 统一定义
def to_device(data, device):
if isinstance(data, torch.Tensor):
return data.to(device)
elif isinstance(data, dict):
return {k: to_device(v, device) for k, v in data.items()}
elif isinstance(data, list):
return [to_device(v, device) for v in data]
else:
return data
# 使用
data = to_device(data, device)
4.3 “The size of tensor a (32) must match the size of tensor b (16) at non-singleton dimension 0”:batch size 不匹配的连锁反应
这个报错,源头往往是 DataLoader 的 drop_last 设置不当,或 collate_fn 写错了。
- 现象 :在
output = model(data)时报错
更多推荐


所有评论(0)