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),把核心术语嵌入每个环节:

  1. 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 ,查了两小时才发现是这一步漏了。

  2. 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%。

  3. 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)) ,否则模型可能根本学不动。

  4. 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)

  5. 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 一次,数值极不稳定。这个坑,我见过至少三个同事栽过。

  6. 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 以下。

  7. Backpropagation(反向传播) :这是整个流程的“引擎”。它用链式法则,从 loss 开始,逐层计算每个参数的梯度 ∂L/∂w 。关键术语是 gradient —— 它不是“梯度下降”的简称,而是损失函数对参数的偏导数,指示了参数该往哪边、走多远才能让 loss 变小。PyTorch 中 .backward() 就是触发这个计算,结果存在 .grad 属性里。注意: .backward() 默认只对 scalar loss 起作用;如果你的 loss 是向量(比如 per-sample loss),必须加 retain_graph=True 或先 loss.mean()

  8. 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 了。

  9. 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。
  • 实操解法
    1. 在每个 iteration 结束时,手动 del 所有大 tensor: del data, target, output, loss
    2. 强制 GC: import gc; gc.collect()
    3. 清空 CUDA 缓存: torch.cuda.empty_cache()
    4. 最狠一招:用 with torch.no_grad(): 包裹所有不需要梯度的计算,并在块内 del

我调一个 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 并不存在,得自己记。
  • 实操解法
    1. 统一定义 device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    2. model.to(device)
    3. 每次 data, target = data.to(device), target.to(device)
    4. (进阶)写一个 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) 时报错
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐