1. 从设计理念看懂PyTorch的取舍
1.1 动态计算图:为什么它改变了深度学习的入门体验
很多初学者第一次接触深度学习时,会被各种名词绕晕:前向传播、反向传播、梯度下降、计算图……PyTorch之所以能成为学术界和工业界公认的上手首选,核心就在于它对“计算图”这件事的处理方式足够直观。
传统框架采用的是静态计算图:你先定义好整张图的结构,再往里喂数据。类比一下,就像你先画好一整套电路图,然后把各个元件焊上去,最后通电测试。图一旦建好就不能改了,调试时你只能盯着图找问题,数据本身是“流”过这张图的。
而PyTorch采用动态计算图(Define-by-Run):张量运算在执行过程中实时构建计算图,每次前向传播都会生成一张新图,反向传播结束后图就被释放。这个机制很像你在草稿纸上一步步推公式——写到第几步,图就画到第几步,写错了可以随时涂改重来。
这个设计带来了三个直接好处:
- 调试极其自然:你可以在任意一行代码处打断点,打印中间张量的值和形状,因为计算图就是按Python代码执行的顺序实时构建的。出错时你面对的是熟悉的Python栈信息,而不是一堆看不懂的底层图节点。
- 代码逻辑和数学模型一一对应:你写的循环、分支、条件判断,都可以直接用在模型前向传播里,完全不需要像静态图框架那样把控制流“翻译”成专门的图算子。
- 动态结构很灵活:处理变长序列、树形结构、递归网络这类输入形状不固定的任务时,动态图几乎是零成本适配。
我在实际使用中经常用一句话跟新同事解释:PyTorch让你像写普通NumPy代码一样去写神经网络,唯一的区别是它额外帮你算好了梯度。这句话虽然简单,但点中了它好用的本质。
1.2 Tensor与自动求导:一切围绕数据流动展开
PyTorch最底层的数据结构是Tensor(张量),它本质上就是一个可以跑在GPU上的多维数组。你在NumPy里做的切片、拼接、广播、转置,Tensor基本都支持,而且API风格高度相似。这意味着只要你会NumPy,迁移到PyTorch几乎不需要额外学习成本。
但Tensor比NumPy多了一个杀手级功能:自动求导(Autograd)。
当你创建张量时设置requires_grad=True,PyTorch就会在每次对该张量的运算过程中记录操作历史,形成计算图。调用.backward()后,梯度会按照链式法则从损失函数反向传播到每个叶子节点。开发者在训练循环里只需要写三步:
loss.backward() # 反向传播,计算梯度 optimizer.step() # 更新参数 optimizer.zero_grad() # 清空累计梯度这个设计把深度学习中最大的一部分手写推导工作彻底自动化了。回想一下,在没有自动求导的年代,每写一个新网络结构,都要手动推导反向传播的梯度公式,然后再一层层实现,非常容易出错。PyTorch把这些全部封装好之后,你可以把精力集中在模型结构和业务逻辑上,梯度计算的正确性完全交给框架保证。
另外,nn.Module这个类把网络层、参数管理、设备迁移(CPU/GPU)做了统一封装。你定义一个模型时只需要继承它,在__init__里声明层结构、在forward里写前向计算逻辑,参数初始化、保存加载、移动设备都是现成的API。配合nn.Sequential可以快速堆叠网络,配合函数式接口nn.functional可以灵活控制每一层的细节。
从这里可以看出来,PyTorch的设计哲学很清楚:把复杂度藏起来,把灵活性留给开发者。对比一下其他框架,有的选择“高度自动化”来压缩代码量,但出了问题排查困难;有的选择“完全手动”来最大化控制力,但上手门槛极高。PyTorch站在了一个很好的平衡点上。
2. 环境准备与数据流水线搭建
2.1 版本选择与安装技巧
装PyTorch看起来是个起步动作,但这里面的坑其实不少。首先是版本对齐问题:PyTorch、CUDA、显卡驱动、Python版本四者之间存在兼容关系。我用一套比较保守但稳定的公式来推荐:
- Python选择3.8以上、3.11以下(太新的Python版本对部分旧版PyTorch算子包的支持可能滞后)
- CUDA版本以PyTorch官方安装命令中默认推荐的为准,不要自己单独装一个最新版CUDA再手动配置
- 显卡驱动只需要满足CUDA的最低版本要求即可,没有必要看到新驱动就更新
确定好版本组合后,我强烈建议用官方安装命令生成器。进入PyTorch官网首页,选择你的操作系统、包管理器(pip或conda)、CUDA版本,它会直接给出可复制的安装命令。注意区分stable(稳定版)和nightly(预览版),日常开发一律选stable,只有需要验证新特性时才装nightly。
接下来要区分两个概念:CUDA Toolkit和PyTorch自带的CUDA运行时。很多初装者会困惑“我明明装了CUDA,为什么PyTorch还是只能用CPU”。原因在于,PyTorch通过pip安装的CUDA版本是随包附带的CUDA运行时库,你不需要也没必要单独安装一整套CUDA Toolkit。只要显卡驱动版本够新,PyTorch就能通过自带运行时调用GPU计算。验证是否真正用上了GPU:
python -c "import torch; print(torch.cuda.is_available())" python -c "import torch; print(torch.__version__, torch.version.cuda)"第一行输出True表示CUDA可用,第二行能看到PyTorch版本号以及对应的CUDA版本号。如果第一行为False,先检查安装的是不是CPU版,再看驱动版本是否过低。
还有一个容易被忽略的点是内存分配器。PyTorch默认使用自己的缓存分配器来管理GPU内存,好处是减少了反复cudaMalloc的开销,坏处是显存不一定会立即归还给系统。如果你在程序里删除了大张量,但nvidia-smi看到显存占用没降下来,这是正常现象,不是内存泄漏。可以用torch.cuda.empty_cache()主动清空未使用的缓存块,但没必要每行代码都调用一次,通常只在训练完一个大模型、准备开始下一个实验时调用。
2.2 数据加载:Dataset与DataLoader的正确打开方式
数据处理是深度学习里最容易被轻视、却最影响训练效果的一环。PyTorch的torch.utils.data提供了Dataset和DataLoader两个核心抽象,理解它们的配合方式能帮你省下大量时间。
Dataset负责定义“如何读取一个样本”。你可以继承torch.utils.data.Dataset,实现__len__和__getitem__两个方法。__getitem__的返回值一般是一个元组,包含输入数据x和标签y。这里的关键点在于:不要在__getitem__里做太重的事情。比如把图片从磁盘读出来、做数据增强、转成Tensor,这些都应该放在__getitem__里,但你完全可以利用多进程并行让它们跑得更快。
DataLoader负责“如何批量取样本”。它接收一个Dataset实例,然后按照设定的batch_size、shuffle、num_workers等参数来自动组合批次。num_workers控制的是数据加载的并行进程数,我实测下来,一般设置为4到8就有明显提速(显存不太大的情况下多设几个加载进程不会占显存),再往上提升就不明显了,反而可能因为进程间通信开销导致变慢。
数据增强是另外一门必修课。对图像任务来说,torchvision.transforms提供了丰富的组合方式:随机裁剪、水平翻转、颜色抖动、归一化。有一点需要特别提醒:训练集和验证集/测试集的预处理流程通常不完全一样。训练集一般会加随机增强来提升模型的泛化能力,而验证集合测试集通常只做尺寸调整和归一化,这样评估结果才是确定性的、可复现的。我在实际项目中就见过有人把随机增强也加到了验证集上,结果每次评估指标都有波动,最后误以为自己模型不稳定,排查了很久才发现是数据增强在捣鬼。
还有一个高频需求是处理不均衡数据集。WeightedRandomSampler可以根据样本权重控制采样概率,让少数类被采样到的概率更高。这个方法在分类任务里比我试过的其他策略(比如简单复制少数类样本)要稳得多,因为它是从采样层面调节分布,不会引入过拟合风险。
3. 核心实操:手写一个图像分类任务
3.1 数据准备与模型搭建
为了把前面讲的各个机制串起来,我用一个标准的图像分类任务作为完整的演示案例。这里的选择是某公开的小型图像数据集,包含10个类别,每张图是一张28x28像素的灰度图。选择这个数据集的原因很简单:它足够小,在一台普通笔记本电脑上用CPU也能在几分钟内完成训练,非常适合作为理解整个流程的起点。
首先构造Dataset子类。使用PyTorch自带的torchvision.datasets可以绕过手写下载和解析的步骤,但为了理解原理,这里展示一个简化的自定义实现:
class MyDataset(Dataset): def __init__(self, images, labels, transform=None): self.images = images self.labels = labels self.transform = transform def __len__(self): return len(self.labels) def __getitem__(self, idx): img = self.images[idx] label = self.labels[idx] if self.transform: img = self.transform(img) return img, label这个结构看着简单,但它是几乎所有自定义数据读取代码的骨架。实际项目中,你只需要把self.images替换成从文件路径列表、数据库查询结果或者网络API返回的数据源,然后在该读取时把数据转成tensor即可。
接下来搭建模型。我用一个简单的两层卷积加全连接的网络来说明nn.Module的用法:
class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1) self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) self.pool = nn.MaxPool2d(2, 2) self.fc = nn.Linear(64 * 7 * 7, 10) def forward(self, x): x = torch.relu(self.conv1(x)) x = self.pool(x) x = torch.relu(self.conv2(x)) x = self.pool(x) x = x.view(x.size(0), -1) x = self.fc(x) return xsuper().__init__()这行是必须的,它用于初始化父类内部的参数管理机制,漏掉它会导致模型注册不了参数。nn.Conv2d的参数顺序是输入通道数、输出通道数、卷积核尺寸,初学者最容易搞混的是第一第二个参数的位置,记住“输入在前,输出在后”就不会错。
forward里x.view(x.size(0), -1)这一步是展平操作,它的作用是把形状为[batch_size, 64, 7, 7]的特征图变成[batch_size, 64*7*7],好送到全连接层。你可能想问:为什么不直接在__init__里算好维度?因为batch size是动态的,模型不应该假设固定的batch大小,所以这里用x.size(0)动态获取当前批次大小,这是推荐写法。
3.2 训练循环中的关键细节
训练循环是整个项目的心脏。标准写法如下:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = SimpleCNN().to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) for epoch in range(10): model.train() for batch_x, batch_y in train_loader: batch_x, batch_y = batch_x.to(device), batch_y.to(device) optimizer.zero_grad() outputs = model(batch_x) loss = criterion(outputs, batch_y) loss.backward() optimizer.step()每一行都有讲究,我逐个拆解。
model.train()与model.eval()的模式切换:train()会启用Dropout、BatchNorm等层在训练状态下的行为;eval()则关闭这些行为,让模型进入推理模式。很多人会在验证时忘记切回eval(),导致结果波动,是个常见陷阱。
optimizer.zero_grad()的调用时机:必须在backward()之前调用,否则梯度会累加到上次的值上。PyTorch的梯度默认是累加式的,这个设计本身是为了支持梯度累积(当显存不够时,把一个大批次拆成几个小批次分别算梯度再累加),但在普通训练中它就是每个batch开头都要执行的操作。
数据设备统一:batch_x.to(device)很好理解,但脚本里device = torch.device("cuda" if torch.cuda.is_available() else "cpu")这行值得细说。它的意思是:如果当前环境有可用的GPU就使用GPU,否则退回CPU。这段代码可以让你在GPU服务器和本地CPU之间无缝切换,但要注意把model和每一个batch都放到同一个设备上,混用会直接报错。
损失函数的选择:多分类任务用nn.CrossEntropyLoss(),它内部已经包含了Softmax操作,所以模型最后一层不需要手动加Softmax,直接输出原始logits即可。如果你在最后多加了一层Softmax再传给损失函数,反而会因为数值精度导致训练不稳定。
评估指标方面,除了loss,至少要记录准确率。我习惯在每个epoch结束后在验证集上评估一次:
model.eval() correct = 0 total = 0 with torch.no_grad(): for batch_x, batch_y in val_loader: batch_x, batch_y = batch_x.to(device), batch_y.to(device) outputs = model(batch_x) _, predicted = torch.max(outputs.data, 1) total += batch_y.size(0) correct += (predicted == batch_y).sum().item() print(f"Epoch {epoch+1}, Accuracy: {100 * correct / total:.2f}%")torch.no_grad()这个上下文管理器非常关键:它告诉PyTorch在接下来的计算中不要构建计算图、不要跟踪梯度。验证和推理阶段不需要梯度,开启它之后能显著减少内存占用并加快速度。
3.3 保存加载模型的两个版本
模型训练好之后要保存。这里有一个区分度很高的问题:保存整个模型对象还是只保存状态字典(state_dict)?
我推荐的、也是业界更通用的做法是只保存state_dict:
torch.save(model.state_dict(), "model_weights.pth")加载时先重新实例化模型结构,再加载权重:
model = SimpleCNN().to(device) model.load_state_dict(torch.load("model_weights.pth", map_location=device))map_location=device的作用是把权重从原来的设备映射到当前设备。比如模型在GPU上训练,现在你拿到一台只有CPU的笔记本上做推理,如果不加这个参数,加载时就会报错。
不推荐的写法是torch.save(model, "model.pth")直接保存整个模型对象。这种写法虽然加载时不需要重新定义模型结构,但它会序列化模型的类定义、类路径等元信息,如果之后你改了项目目录结构或者类名,加载就会失败。另外它还保存了optimizer等额外对象,文件体积更大。总而言之,除非是快速验证的临时需求,否则一律用state_dict。
如果要在训练中途保存“检查点”(checkpoint),以便断点续训,建议把更多信息放到一个字典里:
torch.save({ 'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'loss': loss, }, "checkpoint.pth")恢复时用torch.load读出字典,再逐一配对加载。这样可以恢复到中断时的训练进度,包括优化器的动量状态,不至于前功尽弃。
4. 调试实操与性能优化实录
4.1 高频报错速查与排查思路
PyTorch的报错信息整体上算是清晰的,但对新手来说,有些报错第一次看到还是会懵。我把实际使用中最高频的几类问题整理成一个速查表,遇到对应报错时可以按这个思路排查。
| 报错现象 | 常见原因 | 排查步骤 |
|---|---|---|
RuntimeError: Expected tensor to be on the same device | 模型或数据没有统一放在同一设备 | 打印tensor.device和next(model.parameters()).device,确认两边一致 |
RuntimeError: shape '[x, y]' is invalid for input of size z | view操作试图展开的形状和元素总数不匹配 | 先打印原张量的shape,计算总元素数再决定目标形状 |
IndexError: Target X is out of bounds | 标签值超出了类别数量定义范围 | 检查标签是否从0开始编号,确认类别总数设置正确 |
CUDA out of memory | 显存不足 | 减小batch_size;检查是否存在未释放的大张量;确认是否开了torch.no_grad() |
loss is nan | 梯度爆炸、学习率过大或数据含缺失值 | 降低学习率;检查标签是否包含NaN;添加梯度裁剪torch.nn.utils.clip_grad_norm_ |
AttributeError: 'NoneType' object has no attribute 'data' | 断点续训时加载路径不符 | 检查路径是否实际存在、文件名是否有拼写错误 |
CUDA out of memory值得多说两句。这是训练大模型时最常撞到的一面墙。除了调低batch_size,还有几个实操技巧:用torch.cuda.memory_summary(device=None)查看每个环节的显存占用量,找出峰值在哪个操作;检查数据集加载时是否有后台进程保留了多余的张量副本。如果显存还是不够,可以考虑梯度累积策略,把一个大batch拆成小batch分别计算梯度,累计几个小batch后做一次optimizer.step()。
梯度裁剪是一个常被忽视但很实用的技巧。在训练深度网络时,梯度爆炸会让损失函数直接变成NaN。解决方法是限制梯度向量的范数:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)放在backward()之后、step()之前。max_norm=1.0表示如果梯度范数超过1就按比例缩放回去。这个操作在训练RNN或Transformer时几乎是标配,我不止一次靠这一行代码把已经飘走的loss拉回来。
4.2 多GPU训练:DataParallel与DistributedDataParallel的选择
当你有一张以上显卡时,需要考虑怎么用起来。PyTorch提供了两种方案:torch.nn.DataParallel和torch.nn.DistributedDataParallel。
DataParallel在单机多卡场景下用起来非常简单,只需把模型包一层:
model = nn.DataParallel(model)它的原理是把每个batch按卡数切分,分别送进各自的GPU计算前向和反向,最后汇总梯度更新参数。但它在原理上有一个硬伤:每张卡的计算逻辑是串行启动的,主卡的负载明显高于其他卡,而且模型参数需要在所有卡之间复制多份。当模型本身很大、batch较大时,这种方式的扩展效率并不理想。
DistributedDataParallel虽然名字里带“Distributed”,但它在单机多卡场景下同样适用,而且是当前官方推荐的方式。它内部为每个进程分配一块GPU,进程间通过通信库同步梯度,整体效率远高于DataParallel。用法上是启动多个进程:
torchrun --nproc_per_node=2 train.py脚本内需要初始化init_process_group,然后把模型用DistributedDataParallel包装。这套方案的前期配置成本比DataParallel要高一些,但训练速度和多卡扩展性都要好很多。如果只是临时实验、卡数不多,可以直接DataParallel;如果是要跑正式的模型训练,直接上DistributedDataParallel,省得后面迁移时还要改一堆代码。
4.3 提高训练效率的实操技巧
模型训练周期长,每一分效率提升都意味着真金白银。下面这几条是我在多个项目中实践过、效果最直接的建议。
尽量用卷积自带的BN层提高收敛速度。BatchNorm能够加速训练、对学习率的选择也更加鲁棒。它的原理是对每个batch内的特征做归一化,再通过可学习的缩放和偏移参数复原分布。在nn.Conv2d后面加BN层,然后接ReLU,这个“卷积-BN-激活”的组合几乎是当前视觉模型的标配顺序。
控制eval模式下BN和Dropout的行为。这里有一个很多教程没有提到的细节:如果模型含有BatchNorm层,训练时统计量是每个batch动态更新的;而在测试时,BN层使用的是训练阶段累积的全局统计量。所以测试时务必切换到model.eval(),否则同一个输入在不同batch内的输出就不同,结果不可复现。
混合精度训练是另一项大杀器。用torch.autocast自动为合适的算子在FP16精度下执行,利用GPU的Tensor Core硬件加速能力,训练速度通常能提升1.5到2倍,显存占用也显著下降:
from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() for batch_x, batch_y in train_loader: with autocast(): outputs = model(batch_x) loss = criterion(outputs, batch_y) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() optimizer.zero_grad()新手可能会对GradScaler感到陌生。它解决的典型问题是:FP16的数值表示范围比FP32小很多,梯度容易下溢变成0,所以需要一个缩放因子先把梯度放大再做反向传播。训练过程中scaler会根据梯度情况动态调整这个缩放系数,这一步自动完成,不需要手动干预。
关闭不需要梯度计算的操作。前面说了,验证阶段用torch.no_grad()包住。经常会看到有人评估模型时忘了这个上下文管理器,导致显存一度被撑爆。此外,在计算好了所有指标之后,如果你需要进行额外的张量操作,确认是否真的需要梯度链路,不需要就尽量剥离。
5. 从训练到部署的工程化要点
5.1 迁移学习的应用思路
实际项目中很少有人会从头训练一个完整的深度网络,因为那需要海量数据和巨大的算力。迁移学习提供了一条更务实的路径:用在大规模数据集(比如ImageNet)上预训练好的模型,在其基础上针对自己的任务做微调。
PyTorch的torchvision.models里直接提供了大量预训练模型,加一行weights='DEFAULT'参数就能拿到在ImageNet上训练好的权重:
import torchvision.models as models resnet = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)接下来一般有两种做法。第一种是冻结骨干网络,只训练最后新加的全连接层。这样训练很快,适合数据量较小的场景,也能有效避免过拟合。冻结的方式有两种等价写法:设置param.requires_grad = False,或者在优化器里只传入需要更新的参数。
第二种是全量微调,所有参数一起更新,但学习率设低一些,比如1e-4甚至更低。这种方式适合数据量中等以上、且新任务和ImageNet的领域差异较大的场景。实践中还有一个折中策略:先冻结骨干层,只训练分类头,等loss降到一个平台期后,再解冻全部层,用小学习率继续训练几个epoch。这个“先冻结后解冻”的节奏我用过很多次,效果往往比一上来就全部微调更稳。
有一处小坑需要注意:预训练模型通常要求输入尺寸为224x224、且输入做了标准化的均值方差归一化。直接用不同尺寸的输入,不一定报错,但效果会明显下降。所以用预训练模型时,请务必匹配对应的预处理流程。
5.2 模型导出与推理部署
训练完成不等于项目结束,把一个训练好的模型部署到生产环境是更棘手的部分。PyTorch提供了torch.jit.trace和torch.jit.script两种方法把模型转换成一种更轻量、可脱离Python依赖的格式,这个格式常被称为TorchScript。
trace的原理是传入一个示例输入,把实际执行过的计算图记录下来,适合处理没有太多动态控制流的模型:
model.eval() example_input = torch.randn(1, 3, 224, 224) traced_model = torch.jit.trace(model, example_input) traced_model.save("model.pt")对于包含复杂控制流(比如循环依赖数据内容、多层条件分支)的模型,trace容易“录不全”,这时候需要script或混用两种方式。我的经验是:大部分常规模型用trace就够了,但如果你在forward里写了Python原生循环,而且循环次数取决于输入长度,建议使用torch.jit.script,它能完整捕获控制流。
部署到CPU环境时还有一个优化方向:量化。把模型权重从FP32压缩到INT8,可以在几乎不影响精度的情况下减少约四分之三的模型大小和大量计算消耗:
quantized_model = torch.quantization.quantize_dynamic( model, {torch.nn.Linear, torch.nn.Conv2d}, dtype=torch.qint8 )动态量化主要针对推理时的权重,它不需要校准数据,实现起来最省事。如果你的模型里线性层和卷积层占比很大,这个优化效果会非常明显。但量化不是万能的,某些对精度极其敏感的场景(比如医学类预测),压缩后精度下降幅度不能一概而论,必须实测过再决定。
另一种常见部署方式是导出成ONNX格式,然后交给推理引擎(比如ONNX Runtime)执行。它的跨平台兼容性更好,也是很多服务端推理场景的通用做法:
torch.onnx.export(model, example_input, "model.onnx", input_names=["input"], output_names=["output"])不管是TorchScript还是ONNX,核心思路都是一样的:把模型从Python运行时中解放出来,让它在更轻量、更高效、更适合生产环境的运行时中执行。这也是PyTorch走向产品化必须迈过的一步。
6. 踩坑记录与我的实战建议
6.1 几个值得写进备忘录的陷阱
回顾这几个项目,有几类问题反复出现,我列在这里,当你碰到类似症状时可以少走弯路。
数据增强的“泄露”问题。有一次我做图像分类,训练集和测试集的准确率差距很大。排查半天才发现,测试集在预处理里也加入了随机翻转操作。模型在测试时看到的图像是不稳定的,评估结果波动自然很大。数据增强一定要严格限定在训练集,测试集和验证集只做确定性变换。
标签类别与损失函数不匹配。有一回我改了数据集的类别定义,把类别数从10类扩展到12类,但忘记同步更新最后一层全连接层的输出维度,导致报错。这类错误提示通常出现在第一个epoch的loss.backward()附近,信息不太直观,很多人要耗很久才能发现是模型最后几层的维度没对齐。改动类别数量时,要去模型定义里检查输出维度。
学习率设置的影响比想象中大。默认的lr=1e-3对大部分任务都能跑通,但换一个数据集、换一个优化器,这个默认值可能完全不适配。我在一个细粒度识别项目里,换成预训练模型后忘了调整学习率,前三个epoch的loss一直在上升,后来把学习率降到1e-4才恢复正常。训练开始时如果loss总是不降,第一步检查学习率,第二步检查梯度是否正常。
train()和eval()模式带来的幽灵问题。包含Dropout或BatchNorm的模型,如果忘记切模式,轻则结果不稳定,重则表现严重退化。我在验证模型时也遇到过类似情况:验证的准确率在训练过程中突然掉了几个百分点,原因是验证循环里忘了model.eval()。从那之后,我把train()和eval()的调用写成了训练脚本的固定模板,每个epoch必变。
6.2 我目前最推荐的一套学习路径
如果你刚刚接触PyTorch,我不建议直接去啃全套文档。根据我的个人经验,更有效率的学习方式是按“动手—理解—扩展”的顺序做三轮迭代。
第一轮:跑通官方快速入门教程里的图像分类示例,把数据加载、模型定义、训练循环、评估这四个模块看懂并默写一遍。这是整个框架的最小闭环,相当于学骑自行车时先把车蹬走。
第二轮:在最小闭环的基础上,改装成自己的任务:换个数据集、加深网络层数、加入多GPU训练和混合精度。这个阶段你会踩到前面讲的各类问题,每解决一个问题,你对框架的理解就深一步。
第三轮:读模型源码。找一个成熟的开源训练仓库,认真看它的代码组织方式:配置管理、日志记录、模型保存恢复、分布式配置。这一步能帮你建立工程化思维——把模型迭代当做一个软件项目来管理,而不是一段脚本。
我个人的体会是,PyTorch的上手曲线并不陡峭,真正的障碍往往是在数据准备和调试环节,而不是模型结构本身。框架解决的是“怎么写模型”的问题,但最终训练效果还是要靠你对数据的理解、对实验流程的规范来保证。
最后再分享一个细节:写实验记录。每次跑实验之前,把数据集版本、模型结构、超参数组合、随机种子都记下来。我当时有一段时间反复出“这个效果之前怎么跑出来的”的疑问,就是因为在实验中途改了好几个变量但全都没记录。后来养成了用配置文件和实验日志管理每次训练的习惯,再也没出现过这种混乱。这套习惯,可能比多会一个API还要值钱。