PyTorch深度学习实战:从环境搭建到模型训练的全流程避坑指南
1. 从“动手学”到“动手做”我的PyTorch学习路径反思“动手学PyTorch”这个系列相信很多刚入门深度学习的同学都接触过。我自己也是从这个系列开始一步步从零搭建环境到跑通第一个手写数字识别模型。但回过头看我发现了一个普遍问题很多笔记和教程包括我自己早期的记录都过于“按部就班”了。它们记录了“如何做”——比如torch.nn.Linear怎么用DataLoader怎么配——但却很少深入去问“为什么这么做”以及“不这么做会怎样”。这导致学完之后面对一个全新的项目依然无从下手或者代码跑起来了但对其中的隐患一无所知。这篇笔记我想跳出单纯的代码复现结合我踩过的无数个坑来聊聊PyTorch学习中的那些“潜规则”和“为什么”。我们不只关心forward函数怎么写更要关心数据在GPU和CPU之间搬家的代价、损失函数选择背后的逻辑、以及为什么别人的模型收敛了而你的没有。这不仅仅是第六篇笔记更像是一个从“学习者”到“实践者”的思维转换记录。无论你是刚刚配置好PyTorch GPU环境的新手还是在纠结TensorFlow和PyTorch选哪个的初学者希望这些从实战中提炼的经验能帮你少走弯路。2. 环境搭建不只是“能跑起来”就行几乎所有教程的第一步都是环境搭建安装Anaconda用清华源装PyTorch。但这一步埋藏的坑足以让后续所有努力白费。一个稳定的、可复现的、与你的硬件匹配的环境是项目成功的基石。2.1 CUDA、驱动与PyTorch版本的“三角关系”这是GPU用户的第一道坎。错误信息五花八门最常见的就是CUDA error: no kernel image is available for execution或者直接提示CUDA版本不匹配。其核心在于三个组件必须兼容NVIDIA显卡驱动版本决定了你最高能支持到哪个版本的CUDA。CUDA Toolkit版本PyTorch的GPU版本是基于特定的CUDA版本编译的。PyTorch版本你通过conda install或pip install下载的安装包。它们的兼容关系是自上而下的。你的PyTorch版本必须明确指定并匹配其编译所用的CUDA版本如cu118代表CUDA 11.8而这个CUDA版本必须被你的显卡驱动所支持。实操心得不要盲目安装最新版。先去 NVIDIA官网 查看CUDA版本所需的最低驱动版本。然后用nvidia-smi命令查看当前驱动版本。例如CUDA 12.1要求驱动版本525.60.13。确保你的驱动满足要求后再去 PyTorch官网 使用安装命令生成器选择对应的CUDA版本。对于像Jetson这类嵌入式平台如JetPack 6.2.2情况更特殊。PyTorch需要从NVIDIA官方渠道下载为特定JetPack版本和架构aarch64预编译的whl包直接用pip install torch大概率会失败。2.2 Conda环境管理为每个项目建立“隔离舱”很多新手喜欢在base环境里安装所有包这很快会导致依赖地狱——项目A需要PyTorch 1.9项目B需要PyTorch 2.0互相冲突。# 创建新环境指定Python版本 conda create -n pytorch_project python3.9 # 激活环境 conda activate pytorch_project # 在新环境中安装PyTorch conda install pytorch torchvision torchaudio pytorch-cuda11.8 -c pytorch -c nvidia为什么这么做每个Conda环境都有独立的Python解释器和包安装路径。pytorch_project环境里的改动不会影响base或其他环境。当项目完成后你可以轻松地导出环境配置conda env export environment.yml其他人或未来的你可以通过conda env create -f environment.yml完美复现这个环境确保代码运行结果一致。这是工程化实践的第一步。2.3 验证安装关键的几步检查安装完成后不要急着跑复杂模型。先进行基础验证import torch print(torch.__version__) # 查看PyTorch版本 print(torch.cuda.is_available()) # 查看CUDA是否可用 print(torch.cuda.get_device_name(0)) # 查看GPU型号 x torch.rand(5, 3).cuda() # 在GPU上创建一个张量 print(x) # 确认张量被创建在GPU上显示 devicecuda:0如果torch.cuda.is_available()返回False但你的显卡明明是NVIDIA的那大概率是上述的“三角关系”没匹配好。对于Mac用户尤其是Apple Silicon芯片则需要关注是否安装了支持Metal加速的PyTorch版本它利用MPSMetal Performance Shaders进行GPU加速验证方式类似但设备名称为mps。3. 张量Tensor操作理解“计算图”的起点张量是PyTorch的基石但它的核心价值在于自动微分Autograd。很多初学者把张量当成NumPy数组来用这没错但没用到精髓。3.1 张量的“三要素”数据、设备与梯度一个PyTorch张量不仅仅存储数据它还关联了三个关键属性data: 存储具体的数值。device: 数据所在设备cpu,cuda:0,mps。不合理的设备间数据转移是性能瓶颈的主要来源。requires_grad: 布尔值默认为False。如果设置为TruePyTorch会开始跟踪在该张量上的所有操作用于后续的梯度计算。import torch # 创建一个需要计算梯度的张量 x torch.ones(2, 2, requires_gradTrue) print(x.requires_grad) # True print(x.device) # cpu y x 2 print(y.requires_grad) # True! 因为y是由x计算而来的继承了梯度追踪 print(y.grad_fn) # AddBackward0 object 记录了创建y的操作加法 z y * y * 3 out z.mean() print(out) # tensor(27., grad_fnMeanBackward0) # 反向传播计算梯度 out.backward() # 等价于 out.backward(torch.tensor(1.)) print(x.grad) # 输出 d(out)/dx 在 x1 处的值为什么需要requires_grad在训练神经网络时我们需要计算损失函数相对于网络参数权重和偏置的梯度然后用梯度下降法更新参数。只有将参数张量的requires_grad设为TruePyTorch的Autograd引擎才会构建一个动态计算图记录从参数到损失的所有操作从而在调用.backward()时自动计算并累积梯度。3.2 设备管理避免隐式的CPU-GPU“交通堵塞”这是影响训练速度的关键。不假思索的数据传输会让强大的GPU“饿着”。# 不好的做法在循环中反复移动数据 model MyModel() model.cuda() # 将模型参数移到GPU for data, target in dataloader: data, target data.cuda(), target.cuda() # 每个batch都执行.cuda()有开销 output model(data) ... # 好的做法在数据加载时指定设备使用pin_memory加速 train_loader DataLoader(dataset, batch_size64, shuffleTrue, num_workers4, pin_memoryTrue) # pin_memory加速CPU到GPU传输 model.cuda() for data, target in train_loader: # DataLoader在后台已将数据放入锁页内存.cuda()异步传输更快 data, target data.cuda(non_blockingTrue), target.cuda(non_blockingTrue) output model(data) ...核心原则尽可能让数据和模型呆在同一个设备上。将整个模型.cuda()一次而不是每次前向传播都做。使用DataLoader的pin_memoryTrue和.cuda(non_blockingTrue)可以显著减少数据从CPU到GPU的传输延迟。3.3 就地操作In-place的陷阱以_结尾的操作如x.add_(y)或直接赋值x[0] 10会直接修改原张量而不是创建新的张量。x torch.tensor([1., 2.], requires_gradTrue) y x 2 y.add_(10) # 就地操作修改了y # 此时尝试计算梯度会报错或得到错误结果 # RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation.为什么危险因为Autograd通过计算图追踪张量的历史。就地操作会破坏这个历史记录使得梯度计算无法进行或结果错误。在需要计算梯度的张量上尽量避免使用就地操作除非你非常清楚自己在做什么例如在自定义优化器或某些内存优化场景中。4. 数据集Dataset与数据加载器DataLoader数据管道的艺术模型训练的时间大部分其实花在数据准备和加载上。一个高效、健壮的数据管道至关重要。4.1 自定义Dataset不只是__len__和__getitem__PyTorch的torch.utils.data.Dataset是一个抽象类。自定义Dataset需要实现__len__和__getitem__两个方法。但这里有几个容易忽略的细节from torch.utils.data import Dataset, DataLoader from PIL import Image import os class CustomImageDataset(Dataset): def __init__(self, img_dir, transformNone): self.img_dir img_dir self.img_names os.listdir(img_dir) # 假设全是图片 self.transform transform # 数据增强变换 def __len__(self): return len(self.img_names) def __getitem__(self, idx): img_path os.path.join(self.img_dir, self.img_names[idx]) # 使用PIL.Image打开而不是torchvision.io.read_image因为后者可能不支持所有格式 image Image.open(img_path).convert(RGB) # 确保三通道 label ... # 根据文件名或其他方式获取标签 if self.transform: image self.transform(image) # 应用变换如ToTensor, Normalize # 确保返回的是Tensor # ToTensor()变换会自动将PIL Image或numpy.ndarray转换为torch.Tensor并缩放到[0.0, 1.0] return image, label注意事项__getitem__中执行IO每次调用__getitem__都会从磁盘读取图片这在HDD上是巨大瓶颈。对于小数据集可以在__init__中一次性将所有数据加载到内存self.images [...]。对于大数据集考虑使用更快的存储SSD或使用torch.multiprocessing预加载。转换Transform的应用时机数据增强如随机裁剪、翻转必须在__getitem__中完成这样每个epoch、每个样本的增强都是随机的增加了数据多样性。而像ToTensor()和Normalize()这类确定性变换放在这里也没问题。返回类型一致性__getitem__必须返回相同结构的数据如图像张量和整数标签。DataLoader会默认使用torch.stack来合并多个样本这就要求每个返回的张量形状必须一致除了batch维度。4.2 DataLoader的参数调优加速训练的关键DataLoader负责从Dataset中抽取样本组合成batch并可能将其送至GPU。几个关键参数loader DataLoader(dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue, persistent_workersTrue, # PyTorch 1.7 prefetch_factor2) # PyTorch 1.7num_workers: 用于数据加载的子进程数。默认是0即在主进程加载数据。增加此值可以利用多核CPU并行加载数据从而让GPU永不“断粮”。经验法则是设置为CPU核心数但不宜超过核心数太多否则进程切换开销会抵消收益。常见坑在Windows或使用spawn启动方式的场景下如果__getitem__中包含了复杂的初始化或全局变量可能会引发问题。有时需要将主要逻辑封装在if __name__ __main__:下。pin_memoryTrue: 将加载到CPU的数据张量放入“锁页内存”pinned memory。这使得从CPU到GPU的数据传输通过cuda()可以使用DMA直接内存访问加速速度更快。通常只要使用GPU就应设置此选项为True。persistent_workersTrue: 保持工作进程存活而不是在每个epoch结束后销毁。这避免了反复创建和销毁进程的开销对于迭代周期短的训练有明显加速。但会稍微增加内存占用。prefetch_factor: 每个工作进程预加载的batch数量。默认是2。增加此值可以让工作进程提前准备好更多数据减少GPU等待时间但也会增加内存消耗。调试技巧如果训练时GPU利用率很低例如使用nvidia-smi查看发现GPU使用率波动很大第一个怀疑对象就是数据加载瓶颈。可以尝试逐步增加num_workers观察GPU利用率是否提升。同时监控CPU使用率如果某个核心长期100%也可能是数据预处理逻辑如__getitem__中的图像解码太慢需要考虑优化预处理代码或使用更快的图像库如opencv。5. 模型构建nn.Module的哲学与细节torch.nn.Module是所有神经网络模块的基类。理解它的工作方式是构建复杂模型的基础。5.1 注册参数与子模块让Autograd管理一切在__init__中你必须使用self.xxx的方式将子模块nn.Linear,nn.Conv2d或参数nn.Parameter赋值给类属性。import torch.nn as nn import torch.nn.functional as F class SimpleNet(nn.Module): def __init__(self, input_size, hidden_size, num_classes): super(SimpleNet, self).__init__() # 必须调用父类初始化 # 注册子模块 self.fc1 nn.Linear(input_size, hidden_size) self.fc2 nn.Linear(hidden_size, num_classes) # 注册自定义参数例如一个可学习的缩放因子 self.scale nn.Parameter(torch.ones(1)) # 非参数属性不会被Autograd追踪也不在.parameters()中 self.some_buffer torch.zeros(1) def forward(self, x): out self.fc1(x) out F.relu(out) out self.fc2(out) out out * self.scale # 使用可学习参数 return out为什么必须用self.xxx赋值当调用super().__init__()时nn.Module会初始化一个内部的_modules有序字典和一个_parameters字典。当你使用self.fc1 nn.Linear(...)时Python的描述符协议会触发nn.Module的__setattr__方法。这个方法会检查你赋值对象的类型如果是nn.Module类型它会被注册到_modules中。如果是nn.Parameter类型它会被注册到_parameters中。只有被注册到_parameters中的张量才会在调用model.parameters()时被返回进而被优化器如torch.optim.SGD更新。self.some_buffer只是一个普通的Tensor属性不会被优化器看到。5.2 模型状态管理train()与eval()的本质区别这是影响模型行为如Dropout和BatchNorm的关键开关。model SimpleNet(784, 256, 10) model.train() # 切换到训练模式 # 此时model.training True # 所有子模块如nn.Dropout, nn.BatchNorm2d都会感知到这个状态。 # 训练循环... for data, target in train_loader: optimizer.zero_grad() output model(data) # Dropout层会随机丢弃神经元BatchNorm会使用当前batch的统计量 loss criterion(output, target) loss.backward() optimizer.step() model.eval() # 切换到评估模式 # 此时model.training False with torch.no_grad(): # 关闭梯度计算节省内存和计算 for data, target in val_loader: output model(data) # Dropout层不起作用BatchNorm使用训练阶段累积的运行均值/方差 # 计算准确率...核心区别训练模式model.train()Dropout按照设定的概率p随机将神经元的输出置零。BatchNorm使用当前mini-batch的均值和方差进行归一化并更新其内部维护的运行均值running_mean和运行方差running_var。这两个统计量是随着训练过程指数移动平均更新的。评估模式model.eval()Dropout不起作用所有神经元都参与计算。BatchNorm停止更新运行均值和方差固定使用训练阶段最终累积下来的运行均值和方差进行归一化。这保证了评估结果的确定性。常见错误在验证或测试时忘记调用model.eval()导致Dropout仍然生效BatchNorm使用当前batch的统计量而验证集的batch统计量与训练集分布可能不同这会使得评估指标如准确率波动且不可靠。同样在训练前忘记调用model.train()会导致BatchNorm无法更新其统计量可能影响收敛。5.3 模型保存与加载不仅仅是torch.save保存模型不仅是为了中断后继续训练更是为了部署。# 保存推荐只保存模型的state_dict状态字典 torch.save({ epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), loss: loss, # ... 其他你想保存的信息如学习率调度器状态 }, checkpoint.pth) # 加载先实例化模型结构再加载状态字典 model SimpleNet(784, 256, 10) # 必须使用完全相同的模型类定义 checkpoint torch.load(checkpoint.pth) model.load_state_dict(checkpoint[model_state_dict]) model.eval() # 或 model.train()取决于你接下来要做什么 # 如果只需要用于推理部署可以保存整个模型不推荐因为与代码绑定 torch.save(model, entire_model.pth) loaded_model torch.load(entire_model.pth)state_dictvs 整个模型state_dict一个Python字典将每个层或参数映射到其对应的Tensor。它只包含可学习参数权重、偏置和Buffer如BatchNorm的运行均值。它不包含模型类的定义。这意味着加载时你必须先有完全相同的模型类代码。这种方式灵活、安全是工业界的标准做法。保存整个模型使用Python的pickle序列化整个模型对象。这包含了模型类定义和state_dict。缺点pickle与保存时使用的确切类定义和文件路径绑定。如果你修改了模型类的代码或者移动了文件位置再加载就可能失败。这种方式通常用于快速原型验证不适用于长期保存或部署。跨设备加载如果你在GPU上训练并保存了state_dict但想在只有CPU的机器上加载可以使用torch.load(checkpoint.pth, map_locationtorch.device(cpu))PyTorch会自动将Tensor转移到CPU上。6. 训练循环从标准流程到性能调优训练循环的代码结构大同小异但魔鬼藏在细节里。一个健壮、高效的训练循环需要处理好梯度、损失、日志和验证。6.1 梯度归零zero_grad()、set_to_none与内存在PyTorch中梯度是累积的。这意味着每次调用loss.backward()计算出的梯度会加到现有梯度上.grad属性。如果不手动清零下一次反向传播的梯度就会和上一次的加在一起这显然不是我们想要的。# 标准做法 optimizer.zero_grad() loss.backward() optimizer.step() # PyTorch 1.7 的高效做法 optimizer.zero_grad(set_to_noneTrue) loss.backward() optimizer.step()set_to_noneTrue的好处默认情况下zero_grad()会将所有参数的.grad属性置为零张量。而set_to_noneTrue会直接将.grad属性设置为None。这有两个好处1) 内存分配器可以更高效地复用内存减少内存碎片2) 对于稀疏梯度很多参数梯度为0的模型可以轻微提升性能。建议在内存紧张或训练大模型时使用。6.2 损失计算与选择不仅仅是交叉熵损失函数的选择直接决定了模型优化的方向。多分类问题如手写数字识别最常用的是交叉熵损失CrossEntropyLoss。nn.CrossEntropyLoss内部已经集成了Softmax操作所以模型的最后一层不需要再加Softmax激活函数。它期望的输入是未归一化的分数logits形状为(batch_size, num_classes)目标标签是类别索引形状为(batch_size,)。criterion nn.CrossEntropyLoss() # 模型输出 [batch, 10] 例如output model(images) # 标签是LongTensor每个元素是0-9的数字 loss criterion(output, labels)二分类问题可以使用nn.BCEWithLogitsLoss内置Sigmoid或nn.BCELoss需要手动Sigmoid。前者数值稳定性更好。回归问题常用nn.MSELoss均方误差或nn.L1Loss平均绝对误差。自定义损失函数你可以通过继承nn.Module来定义自己的损失函数。关键是确保计算过程使用PyTorch张量操作以便Autograd能够追踪。class MyCustomLoss(nn.Module): def __init__(self, weightNone): super().__init__() self.weight weight def forward(self, predictions, targets): # 确保使用张量运算 diff predictions - targets if self.weight is not None: diff diff * self.weight loss torch.mean(diff ** 2) # 一个加权的MSE return loss6.3 验证阶段关闭梯度与模型模式验证或测试时我们不需要计算梯度也不希望改变模型状态如BatchNorm的统计量。def validate(model, val_loader, criterion): model.eval() # 关键一步 total_loss 0.0 correct 0 total 0 with torch.no_grad(): # 关键一步关闭梯度计算 for data, target in val_loader: data, target data.cuda(), target.cuda() output model(data) loss criterion(output, target) total_loss loss.item() * data.size(0) # 累加批次损失 _, predicted torch.max(output.data, 1) # 获取预测类别 total target.size(0) correct (predicted target).sum().item() avg_loss total_loss / total accuracy 100. * correct / total return avg_loss, accuracymodel.eval()如上文所述关闭Dropout固定BatchNorm。with torch.no_grad():这个上下文管理器会禁用该代码块内所有张量的梯度计算。这带来了两大好处大幅减少内存消耗在前向传播过程中PyTorch不需要为反向传播存储中间变量的梯度信息。轻微提升计算速度避免了为梯度计算所做的额外记录。常见错误在验证循环中忘记使用torch.no_grad()导致GPU内存随着验证进行不断增长因为每个batch的中间激活值都被保留以备梯度计算最终可能引发CUDA out of memory错误。7. 调试与性能分析让训练过程透明化当模型不收敛、精度低或速度慢时系统性的调试方法比盲目调参更有效。7.1 基础检查过拟合一个小数据集这是验证模型学习能力和代码正确性的黄金法则。创建一个极小的数据集例如从训练集中随机抽取100个样本。用这个微型数据集训练你的模型。观察训练损失是否能够迅速下降到接近零例如交叉熵损失可以降到非常小的值。在同一个微型数据集上评估准确率应该接近100%。如果做不到说明你的模型代码、损失函数或优化器配置存在根本性问题。可能的原因包括模型结构错误如最后一层激活函数用错。损失函数输入和目标形状不匹配。优化器的学习率设置得离谱过高或过低。数据预处理出错如图像归一化时均值和方差弄反。标签编码错误如多分类问题中标签不是从0开始的整数。7.2 梯度流动检查可视化与数值检查梯度消失或爆炸是深度网络的常见病。你可以检查各层权重的梯度范数。def check_gradients(model): total_norm 0 parameters [p for p in model.parameters() if p.grad is not None and p.requires_grad] if len(parameters) 0: print(No gradients found. Have you called backward()?) return for p in parameters: param_norm p.grad.detach().data.norm(2) # L2范数 total_norm param_norm.item() ** 2 total_norm total_norm ** 0.5 print(fTotal gradient norm: {total_norm:.6f}) # 也可以打印每层的梯度均值/标准差 for name, param in model.named_parameters(): if param.grad is not None: print(f{name}: grad mean{param.grad.mean():.6f}, std{param.grad.std():.6f})在训练循环中定期调用此函数。如果梯度范数非常小如1e-7可能是梯度消失如果非常大如1e7可能是梯度爆炸。梯度爆炸通常可以通过梯度裁剪Gradient Clipping解决torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) # 在 loss.backward() 之后optimizer.step() 之前调用7.3 使用TensorBoard或Weights Biases进行可视化“黑箱”训练让人不安。可视化工具能让你直观地看到损失下降曲线、参数分布、梯度直方图甚至输入图像。from torch.utils.tensorboard import SummaryWriter # 或 import wandb writer SummaryWriter(runs/experiment_1) for epoch in range(num_epochs): # ... 训练 ... writer.add_scalar(Loss/train, train_loss, epoch) writer.add_scalar(Accuracy/train, train_acc, epoch) # ... 验证 ... writer.add_scalar(Loss/val, val_loss, epoch) writer.add_scalar(Accuracy/val, val_acc, epoch) # 记录参数直方图每10个epoch记录一次 if epoch % 10 0: for name, param in model.named_parameters(): writer.add_histogram(name, param, epoch) if param.grad is not None: writer.add_histogram(f{name}.grad, param.grad, epoch) writer.close()通过观察这些图表你可以判断模型是否过拟合训练损失持续下降但验证损失上升、学习率是否合适损失曲线震荡剧烈可能学习率太大下降缓慢则可能太小、参数分布是否健康是否出现大量死神经元即权重全零。8. 超越基础向实际项目迈进掌握了上述核心要点你已经可以完成大多数标准的监督学习任务。但要应对更复杂的场景还需要了解一些进阶概念。8.1 多GPU训练DataParallel与DistributedDataParallel当单张GPU显存不足或训练速度太慢时需要考虑多GPU训练。nn.DataParallel最简单只需一行代码。它将输入数据在batch维度上拆分分发到多个GPU上前向传播后再将结果收集到主GPU计算损失和梯度。model MyModel() if torch.cuda.device_count() 1: print(fUsing {torch.cuda.device_count()} GPUs!) model nn.DataParallel(model) model.cuda()缺点1) 负载不均衡主GPU承担了额外的梯度汇总和参数更新工作成为瓶颈。2) 只支持单进程多线程在模块定义复杂时可能受Python GIL限制。nn.parallel.DistributedDataParallel(DDP)PyTorch推荐的、更高效的多GPU训练方式。它采用多进程每个进程控制一个GPU模型副本存在于每个GPU上。数据通过进程间通信如NCCL同步梯度。# 启动命令python -m torch.distributed.launch --nproc_per_node4 train_script.py import torch.distributed as dist from torch.nn.parallel import DistributedDataParallel as DDP # 初始化进程组 dist.init_process_group(backendnccl) local_rank int(os.environ[LOCAL_RANK]) torch.cuda.set_device(local_rank) model MyModel().cuda() model DDP(model, device_ids[local_rank]) # 在DataLoader中需要使用DistributedSampler train_sampler DistributedSampler(train_dataset) train_loader DataLoader(..., samplertrain_sampler)DDP性能更好扩展性更强是训练大模型的标配但代码稍复杂。8.2 混合精度训练用更少的内存跑更大的模型混合精度训练Automatic Mixed Precision, AMP使用半精度FP16进行前向和反向传播用单精度FP32存储主权重并更新。这可以显著减少GPU显存占用约50%并利用现代GPU如Volta架构及以后的Tensor Cores加速计算。from torch.cuda.amp import autocast, GradScaler scaler GradScaler() # 梯度缩放防止FP16下梯度下溢 model.cuda() optimizer torch.optim.Adam(model.parameters()) for data, target in train_loader: data, target data.cuda(), target.cuda() optimizer.zero_grad() with autocast(): # 自动混合精度上下文 output model(data) loss criterion(output, target) scaler.scale(loss).backward() # 缩放损失反向传播 scaler.step(optimizer) # 缩放梯度更新参数 scaler.update() # 更新缩放因子原理FP16的数值范围远小于FP32在反向传播中梯度值可能太小而变成0下溢。GradScaler通过在反向传播前放大损失值使梯度保持在FP16可表示的范围内在优化器更新参数前再将缩放后的梯度缩小回去。8.3 学习率调度不是一成不变固定的学习率可能不是最优的。学习率调度器Scheduler可以在训练过程中动态调整学习率帮助模型跳出局部最优更快收敛。optimizer torch.optim.SGD(model.parameters(), lr0.1, momentum0.9) # 常用调度器举例 # 1. 等间隔调整每30个epoch学习率乘以0.1 scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size30, gamma0.1) # 2. 指数衰减每个epoch学习率乘以gamma # scheduler torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma0.95) # 3. 余弦退火学习率按余弦函数从初始值降到最小值再重启SGDR # scheduler torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_010, T_mult2) # 4. ReduceLROnPlateau当验证指标停止改善时降低学习率需要在验证后调用step # scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, factor0.1, patience5) for epoch in range(num_epochs): # 训练一个epoch train(...) # 验证 val_loss validate(...) # 对于StepLR, ExponentialLR, CosineAnnealingLR等在每个epoch后调用step scheduler.step() # 对于ReduceLROnPlateau需要传入监控的指标 # scheduler.step(val_loss) # 打印当前学习率 print(fEpoch [{epoch1}], lr: {scheduler.get_last_lr()})选择哪种调度器取决于任务和数据集。StepLR简单有效CosineAnnealing在图像分类等任务上表现优异ReduceLROnPlateau则是一种自适应策略。学习PyTorch或者说学习任何深度学习框架都是一个“实践-踩坑-反思-再实践”的循环。这篇笔记整理了我从“动手学”到“动手做”过程中那些教程里不常细说却又至关重要的细节和思考。记住能跑通的代码只是开始高效、稳健、可维护的代码才是目标。多写多试多调多思考“为什么”你才能真正掌握这门工具让它为你所用。