做深度学习这几年,PyTorch 是我用得最顺手的框架。最早接触它是在一个时间紧、数据集还很乱的图像分类项目里,当时模型结构天天改,参数换来换去,它“定义即运行”的风格让我第一次觉得写模型可以这么顺手。后来这个框架逐渐成了我做算法的主力工具,不管是快速验证想法、训练大模型,还是把模型推到线上服务,我基本都是拿它起手。这篇文章就围绕 PyTorch 的实际使用展开,聊聊我踩过的坑、总结出来的套路,还有那些文档里很少写清楚、但实战中很关键的经验。不管是刚开始学深度学习的新手,还是已经被项目折磨过几轮的工程师,应该都能从中找到能直接落地的东西。
1. 为什么 PyTorch 能“又好写又灵活”:动态图与自动求导的底气
很多人第一次听说 PyTorch,都会听到“动态图”和“自动求导”这两个词。说实话,对新手来说这些概念挺抽象的,但它们正是这个框架好用的根源。
1.1 动态图带来的调试自由
传统的静态图框架,一般得先搭好整张计算图,再拿去执行。这个过程更像“写剧本再演出”:你先把所有演员和走位定死,然后整场戏一起跑。一旦中途想改个情节,就得重新写剧本、重新编译。PyTorch 不一样,它是一句一句“即兴演出”,代码执行到哪,计算图就建到哪。这意味着你可以在循环里随意加if条件,可以在中途打印中间变量的值,甚至用普通的 Python 调试器打断点,一点一点看数据怎么变化。
这个特性在日常开发里价值极大。假设我的模型在某个 batch 上 loss 变成了 NaN,用 PyTorch 我可以直接在反向传播前打印每一层输出的分布,定位是哪个操作把数值搞炸的。几行print就能解决的事,换成静态图框架得专门写调试逻辑,体验完全不同。前端工程师写页面可以随时看 DOM 结构,Python 脚本可以随时print,我在 PyTorch 里调试模型也有类似的爽感。这种自由度对研究型工作尤其重要,因为你经常要尝试一些非常规的操作,比如在 forward 中根据输入动态决定是否跳过某个模块,或者写一个带随机采样的自定义层。
1.2 Python 生态的天然亲和
PyTorch 的张量操作和 NumPy 非常接近。NumPy 里怎么切片、怎么广播、怎么计算均值方差,在它这里几乎可以直接照搬。这意味着大部分做数据处理、做传统算法出身的人,迁移成本很低。很多老代码可以先把 NumPy 数组变成torch.Tensor,算完再变回去,整个过程毫无割裂感。
这年头做 AI 项目,没几个预训练模型加持,起步会慢很多。PyTorch 社区里有人维护了大量现成的模型库和训练工具,从图像分类、目标检测到文本生成都有开箱即用的实现。我接到新任务时,第一件事往往不是从头搭模型,而是去找一个结构最接近的现成实现,在此基础上改头换面。这种“站在别人肩膀上”的开发模式,已经成了行业常态,而 PyTorch 生态让这件事变得异常顺手。很多论文的官方代码也是拿它写的,复现一个最新算法时,大部分时间花在理解论文,而不是纠结框架写法。
1.3 哪些场景其实不该选 PyTorch
不过,我也得泼一点冷水。PyTorch 虽然全能和灵活,它并不是所有场景的最优解。如果你的模型要部署到特别老旧的嵌入式设备上,内存只有几百 MB,算力也有限,那可能需要考虑更轻量级的推理引擎。如果你的业务是纯前端浏览器里跑小模型,那更适合的是 Web 端推理方案,PyTorch 模型通常需要转换。还有一种情况是团队里全是传统 Java 工程师,没有 Python 技术栈,硬上一个 Python 服务可能维护成本很高。框架选型永远受制于环境,这个道理越早想通越好。但在模型训练、算法探索、快速原型验证这个领域,PyTorch 目前依然是最主流的选项。我这么说不是在拉踩谁,就是把它放在合适的场景里使用而已。
2. 把项目环境搭稳:版本匹配与工程骨架
很多新手在 PyTorch 上遇到的第一道坎,不是模型写不出来,而是环境装不明白。Python 版本、PyTorch 版本、GPU 驱动和加速库版本之间有一定的匹配关系,装错了轻则报错,重则直接检测不到显卡。
2.1 版本匹配是第一道门槛
我之前帮同事排查过一个“离奇”的问题:代码在服务器上跑得好好的,换到自己电脑上却提示 CUDA 初始化失败。最后发现他的 PyTorch 版本对应的加速库版本,和他显卡驱动支持的版本对不上。这类问题在论坛里天天有人问,说白了就是版本匹配没做好。
建议的做法是:先确认自己的显卡驱动支持到哪个版本的 CUDA,再回头安装对应版本的 PyTorch。这里有个小技巧,不要装“最新版”,而是装社区里验证最多、文档最全的稳定版本。以一般的 NVIDIA 显卡为例,装好驱动后可以用系统命令查看支持的版本号,然后到 PyTorch 官网的安装页选对应组合。下面这张表是我在实际项目里常用的组合方式:
| Python 版本 | PyTorch 版本 | 加速库版本 | 适用场景 |
|---|---|---|---|
| 3.9 | 2.0 系列 | 11.7 | 大多数老项目兼容性好 |
| 3.10 | 2.1 系列 | 11.8 | 中等规模训练任务 |
| 3.11 | 2.2 及以上 | 12.1 | 新功能、大模型训练更友好 |
不同项目的依赖可能冲突,所以我强烈建议给每个项目建独立的虚拟环境,不要图省事直接装在全局 Python 里。哪怕是同一个 PyTorch 版本,和不同版本的依赖库组合,行为都可能不一样。我在本地机器上通常会同时维护三四个环境,每个环境对应不同项目的依赖组合,切换项目就是切换环境,互不干扰。这一步做好了,后续能省下大量“装完 A 坏了 B”的折腾时间。
注意:装 PyTorch 时千万别图快直接
pip install torch拉最新版,先确认自己的显卡驱动和系统环境。装完马上跑一段简单的矩阵运算,确认 GPU 真的可用再继续。检查命令大致是torch.cuda.is_available()返回 True。这个检查只要十秒钟,但能帮你排除一大半硬件相关的坑。
2.2 设计一个可复现的项目目录
我以前也经历过,实验跑出个不错的结果,想回头复现却发现自己都看不懂之前写的代码结构。后来我养成一个习惯,每个项目从第一天起就按一套统一的目录结构来组织。不一定适合所有人,但可以参考:
project_root/ ├── configs/ # 所有配置文件,超参数放这里 ├── data/ # 数据集和预处理脚本,原始数据别动 ├── models/ # 模型定义 ├── utils/ # 工具函数,比如日志、指标计算 ├── scripts/ # 训练、评估、推理的入口脚本 ├── checkpoints/ # 模型权重保存 └── logs/ # 训练日志、tensorboard 事件这套结构的好处是:代码、配置、数据、产物是分开的。换数据集、换超参数都不需要改代码,只改配置文件就行。我后来做任何项目都默认先搭这样的架子,看起来多花了十分钟,实际上后面省下来的时间远远不止。
2.3 随机种子:实验不玄学
深度学习里有大量随机因素:模型参数初始化、数据加载顺序、Dropout 的随机失活、甚至卷积在 GPU 上的计算顺序。所以同一份代码跑两次,结果可能有一点点差别。问题是,有些任务对这点差别非常敏感,导致实验结果不稳定,你甚至分不清某个改动到底有没有效果。
我的做法是在训练脚本最前面设置固定种子,包括 Python 的random、NumPy 的random和 PyTorch 的random。同时还要给 DataLoader 设置好 worker 的随机种子,否则多进程加载数据时,数据增强的随机性还是无法固定。另外,卷积算子可能因为加速库的 benchmark 模式导致结果不完全确定,如果实验要求严格可复现,可以把torch.backends.cudnn.deterministic打开。不过打开之后计算速度会稍微下降,要不要开,取决于你的场景——学术实验建议开,工业训练追求速度时可以不开。
3. 从数据到模型:构建一个能跑通的训练流程
环境和工程骨架准备好了,接下来就是把数据和模型搭起来。这一部分是 PyTorch 最常见的日常操作,但里面的细节比表面看起来多得多。
3.1 Dataset 与 DataLoader:数据管线的正确打开方式
自定义 Dataset 大概是每个 PyTorch 用户都干过的事。你要做的事情其实很简单:继承Dataset这个基类,实现__len__和__getitem__两个方法。前者告诉框架你的数据量多大,后者告诉框架给定一个索引,怎么把数据取出来。
__getitem__里通常不只是读数据,还会做预处理、数据增强、把标签转成模型需要的格式。我见过不少新手把预处理的逻辑放在训练循环里,写出来也能跑,但效率低、代码乱。正确做法是把这些逻辑都放进 Dataset 里面,训练循环只负责拿 batch、喂给模型。这里还有一个关键点:如果__getitem__开销很大,比如要读大图、做重度的数据增强,那这个环节会成为训练瓶颈。判断方法很简单,训练时打开 CPU 占用率看看,如果 GPU 一直空闲、CPU 跑满了,多半就是数据加载跟不上。
DataLoader 里有个参数num_workers,很多人喜欢盲目调大,结果发现数据还没加载完,机器先卡死了。它并不是越大越好,通常建议设为 CPU 核心数的一半左右,或者用我常用的土办法:从 4 开始试,观察训练速度不再上升就停下来。还有一个参数pin_memory,在 GPU 训练时建议设为 True,它能让数据从内存拷贝到显存的过程更快。另外要注意,collate_fn这个参数承担“怎么把一堆样本拼成一个 batch”的工作。比如你的样本里有不同长度的文本,默认的拼接逻辑可能会失败,这时候就需要自定义collate_fn来统一处理变长数据。
提示:如果你的模型输入是图片,日常最佳实践是在 Dataset 里用轻量的 PIL 或者 OpenCV 做基础读取和增强,不要把头重脚轻的增强算法放进 GPU 计算里,除非你的 GPU 非常闲而 CPU 已经很紧张了。总体原则是让 CPU 和 GPU 尽量并行忙碌,而不是互相等待。
3.2 nn.Module 的构造习惯:子模块管理与参数初始化
PyTorch 里面,模型都是通过继承nn.Module来组织的。它的核心优势是自动帮你管理所有子模块和参数。你在__init__里声明一个nn.Conv2d,它就能自动把这个层的权重注册到模型的参数列表里,反向传播时梯度也能自动回传。这是 PyTorch 设计得极其优雅的一点,整个模块层级关系清晰,优化器也能直接拿到所有需要更新的参数。
但要注意一个常见错误:如果你在__init__里用普通的 Python list 来存放子模块,PyTorch 是不会自动识别里面的参数的。正确姿势是用nn.ModuleList或nn.ModuleDict。我自己早期吃过这个亏,模型跑起来看似正常,但参数更新缓慢,最后排查发现是参数压根没注册进去。这种情况不会直接报错,特别隐蔽。
参数初始化常常被忽略,但它对收敛影响很大。PyTorch 里每个nn.Linear和nn.Conv2d在创建时都有默认的初始化方案,大部分情况下是够用的。但如果你的模型结构非常深,或者用了一些特殊模块,比如自定义注意力层,最好显式写一个init_weights方法,把权重初始化的主动权掌握在自己手里。比如对Linear层,我会用均值为 0、方差比较小的正态分布初始化;对BatchNorm的 bias 和 scale,会有专门的初始化策略。另外,当你的模型包含BatchNorm时,训练和评估阶段它的行为不一样,PyTorch 通过model.train()和model.eval()做切换。很多人忘了调 eval 模式,导致预测结果不稳定,这是新手非常容易踩的坑。
3.3 损失函数与优化器选择的取舍
PyTorch 自带大量损失函数,但日常用得最多的就是那几种。分类任务用交叉熵损失,回归任务用均方误差或平均绝对误差,目标检测还会用到一些组合损失。新手最大的误区是觉得损失函数只是“一个函数”,随便选一个就行。实际上损失函数的设计决定了模型优化的方向和难度,比如分类任务用 BCE 还是 CE,取决于标签是不是 one-hot;多标签任务和多分类任务容易混淆,选错损失函数模型根本训不收敛。
优化器的选择更是一个典型问题。早年大家用SGD比较多,配合动量项,泛化效果往往不错。现在Adam族是默认选项,尤其是AdamW,在做 Transformer 类模型时几乎是标配。它和Adam的核心区别在于权重衰减的实现方式不同,AdamW把权重衰减和梯度更新解耦,训练大模型时效果更好。我下面的经验表可以作为参考:
| 优化器 | 推荐使用场景 | 核心注意事项 |
|---|---|---|
| SGD + Momentum | 稳定数据集、CNN 类模型 | 学习率需要精细调节 |
| Adam | 通用任务、快速验证 | 对学习率相对不敏感 |
| AdamW | 预训练微调、Transformer 结构 | 配合 warmup 效果更佳 |
关于学习率,我听过一个非常实用的经验:如果你不确定该用多大学习率,可以从一个比较小的值开始,比如 3e-4,然后观察 loss 的下降趋势。如果 loss 震荡得很厉害,就降低一点;如果下降太慢,就适当提高。还有一些像学习率冻结、周期性调整的策略,后面会展开讲。
4. 训练循环手写实录:从模型到收敛
很多人喜欢用高层封装好的训练器,几行代码就自动开始训练了。但我觉得,至少自己手写一遍训练循环是很有必要的。手写一遍之后,你会知道 loss 是怎么反传的,梯度是怎么更新的,学习率在哪个时机生效,这在排查问题时帮助极大。
4.1 一个标准训练函数的骨架
最经典的训练循环大概长这样,核心逻辑就六个步骤:取数据、前向传播、计算 loss、梯度清零、反向传播、参数更新。
for epoch in range(num_epochs): model.train() for batch_data, batch_labels in train_loader: batch_data = batch_data.to(device) batch_labels = batch_labels.to(device) outputs = model(batch_data) loss = criterion(outputs, batch_labels) optimizer.zero_grad() loss.backward() optimizer.step()这段代码看起来简单,但每一步都有值得深挖的细节。model.train()切换模型为训练模式;to(device)把数据放到 GPU 上,如果忘了这一步,很多时候会报设备不匹配的错误,但也有些时候不会立刻报错,而是 CPU 和 GPU 之间反复拷贝数据,训练慢到你怀疑人生。optimizer.zero_grad()这一步千万别漏,PyTorch 的梯度是会累积的,不清零的话,上一个 batch 的梯度会叠加到当前 batch 上。还有一点容易被忽略:loss.backward()之后调度器更新学习率的时机,一般习惯是optimizer.step()之后再调用scheduler.step(),这样才符合大多数调度器“每个 epoch 更新一次”的预期。
4.2 梯度累积:小显存也能拉大批次
有些模型特别吃显存,比如大尺寸图片、大 batch 的 Transformer。卡不够大的时候,一个 batch 塞不下太多样本,但不增大 batch 又影响 BatchNorm 统计量和训练稳定性。这时候可以用一个民间技巧:梯度累积。
梯度累积的原理很简单,把几个小 batch 的梯度都累加起来,攒够了再统一更新一次参数。代码改动也不大:
accumulation_steps = 4 # 相当于把 batch size 放大 4 倍 loss = loss / accumulation_steps # 保证累计梯度均值不变 loss.backward() if (step + 1) % accumulation_steps == 0: optimizer.step() optimizer.zero_grad()这里把 loss 除以累积步数很关键。如果不除,相当于把梯度放大了 N 倍,很容易导致 loss 炸掉。这个技巧在比赛和工业项目里非常常用,可以理解成“钱不够,分期付款,但总价不变”。需要注意的是,如果模型里有 BatchNorm,累积梯度并不能完全等价于大的真实 batch,因为 BN 的统计量还是基于每个小 batch 计算的。
4.3 学习率策略:warmup 与余弦退火
学习率可能是深度学习训练里最重要的超参数。它在整个训练过程中不是一成不变的,而是需要根据阶段调整。训练初期,模型参数还在“摸索期”,如果一下子用很大的学习率,很容易走过头,loss 直接飞起来。所以很多人会在前几百个 step 用一个小学习率做“热身”,让模型循序渐进进入状态,这个过程叫 warmup。等模型大致稳定后,再用一个相对较大的学习率快速收敛。到了训练后期,模型已经接近最优点,再用大步长就会在最优解附近反复横跳,这时候需要让学习率逐步降下来,走到哪里算哪里。
我常用的策略是线性 warmup 加余弦退火。初始学习率设为 1e-4,warmup 步数设为训练总步数的 5% 左右,然后按照余弦曲线慢慢衰减到接近 0。这个策略在大量任务上表现稳定,不需要太多人工干预。PyTorch 里可以直接用torch.optim.lr_scheduler.OneCycleLR或者LinearLR+CosineAnnealingLR组合实现。如果你不想调得太细,至少也要用个StepLR,让学习率每隔若干个 epoch 降一降。很多人训练效果不好,其实不是模型结构问题,而是学习率从头到尾一直没变过。
4.4 评估与模型保存
训练到每个 epoch 结束,一定要做验证集上的评估。验证的作用是看模型见过新数据时表现如何。评估时记得调用model.eval(),同时把不需要梯度的代码块用torch.no_grad()或torch.inference_mode()包起来。这两个上下文管理器虽然效果相似,但inference_mode会更快一些,因为它进一步关闭了一些运行时的追踪逻辑。验证集上的 loss 和训练集 loss 如果差距越来越大,那说明模型过拟合了,早停法就可以派上用场。
模型保存也是很多人的困惑点。PyTorch 官方推荐的保存方式是保存状态字典,而不是用序列化库把整个模型对象打包。因为保存整个对象往往包含代码结构依赖,换个环境就加载不了。保存时我通常会存一个字典,里面同时包含模型权重、优化器状态、当前 epoch 数和最佳指标,这样断点续训非常方便:
torch.save({ 'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'best_acc': best_acc, }, 'checkpoint.pth')加载的时候,用load_state_dict把权重恢复到模型里,再用torch.load的map_location参数处理设备迁移问题。记住一个原则:模型权重尽量只保存参数,不保存计算图。
5. 提速与省显存:训练加速的几条实用路线
模型能跑通和能快跑是两回事。训练速度直接决定了你一天能试多少个想法,迭代速度快的人,在项目里优势是压倒性的。这一部分聊聊我自己实测比较有效的提速路线。
5.1 先看显存去哪了
如果你损失函数、优化器都选对了,但一张卡跑不动大模型,第一个要解决的问题是显存占用。PyTorch 的显存主要消耗在四块:模型参数本身、梯度、优化器状态、前向传播保存的中间激活值。前三个相对固定,真正的大头往往是中间激活值。
减少激活值最直接的方法是减小 batch size,这招最见效,但可能影响训练稳定性。另一个思路是使用torch.utils.checkpoint,它会把前向传播的中间结果丢弃,反向传播时再重算一遍,用计算换显存。适合层数特别深的模型。还有一个很多人忽略的习惯:用完一个大的中间变量之后,可以考虑del掉,再加一步torch.cuda.empty_cache()清理显存缓存。如果你用的是 PyTorch 2.x 的torch.compile,也可以试试开启内存优化选项,效果因模型而异,但值得一试。
5.2 混合精度训练
混合精度训练是目前最通用的加速手段之一。它的思路是:在计算时用半精度(FP16),在更新参数时用单精度(FP32),两者结合。为什么能加速?因为现代 GPU 对半精度数据的计算速度几乎是双倍的速度,而且显存占用直接减半,这对大模型训练的好处不言而喻。
PyTorch 自带的torch.cuda.amp可以自动管理精度切换,关键步骤是使用torch.amp.autocast和torch.amp.GradScaler。GradScaler 做的事情是:在反向传播时对梯度进行缩放,避免小梯度在 FP16 下变成 0,更新参数前再缩放回来。如果不用它,loss 很容易变成 NaN。我刚开始上手的时候偷懒没用,结果小 batch 下 loss 经常莫名炸掉,后来老老实实加上 scaler 就正常了。混合精度训练在使用时基本不需要改别的代码,收益却非常明显,强烈建议列入默认配置。
5.3 DataLoader 的并行与性能优化
训练任务里,数据加载这一环是 CPU 密集型工作。数据增强、图片解码、格式转换都在这里发生。我见过不止一个项目,GPU 利用率只有 30%,原因就是 DataLoader 加载数据的速度跟不上 GPU 计算速度。优化思路是把num_workers调高,让多个 CPU 进程并行准备数据。同时加上pin_memory=True,让数据从内存到显存的拷贝走更快的路径。
如果你用数据增强比较重,那num_workers不能设太低。但设太高也有问题,每个 worker 都会复制一份数据集引用和随机数生成器,内存开销不小。另外,如果你的数据读取有明显瓶颈,比如每次要从几千张小图里读取,更快的办法是把图打包成类似 LMDB 或者 HDF5 的格式,减少磁盘 IO。这是我在处理大规模图片数据集时实测最有效的一招。
还有一个小技巧是使用prefetch_factor,控制每个 worker 预取多少批数据。调高一些可以在数据流水线中提前准备好后续 batch,避免等待。配合 GPU 和 CPU 的并行机制,实测训练吞吐量能再提一成左右。如果你在 PyTorch 2.x 上用DataLoader,可以留意persistent_workers参数,它让 worker 跨 epoch 保持存活,省去重建进程的开销,但吃完数据后要手动释放,注意别把内存占得太满。
6. 踩坑记录与排查速查表
不管代码写得多么小心,实际项目里总会遇到各种幺蛾子。这里把我这几年遇到的典型问题整理成一份速查表,每个都是真金白银换来的经验。
6.1 显存溢出与“神秘”重启
显存溢出的报错通常很直白,就是 CUDA out of memory。但有些时候报错莫名其妙,比如程序运行着运行着,显卡驱动直接重置了,或者进程被杀掉。这种情况往往是显存占用超过了物理限制,但又没有正确触发 PyTorch 的报错机制。
排查思路我一般分三步:先用nvidia-smi看当前显存占用,确认是不是有别的进程占着卡;再在自己的代码里用torch.cuda.memory_summary()查看显存分配细节;最后检查有没有显存泄漏。所谓显存泄漏,是指在不需要保留数据的地方不小心保留了计算图引用。比如把loss.item()写成了loss,这个张量就会持有整张计算图,每个 epoch 累积下来显存很快爆掉。还有一个常见场景是在循环内部重复创建优化器或模型,并且旧的没有被回收。用gpustat这类工具监控一段时间,如果显存随时间缓慢上涨而不是平缓波动,大概率就是泄漏了。
6.2 梯度不更新和 loss 为 NaN
梯度不更新是最让人头疼的问题之一,因为不报错,只是 loss 不下降或者迟迟不变。先检查优化器传给模型的参数列表是不是空的。我之前遇到一次,就是因为模型参数全被requires_grad=False或.no_grad()包住了。再检查是不是忘了调用zero_grad(),导致梯度一直累加,优化方向越来越怪。还有一个容易忽视的:自定义损失函数里有detach()操作,把梯度直接切断了。如果你想用一些特殊操作,但又不希望它影响反向传播,可以考虑用stop_gradient思路包装。
loss 为 NaN 的问题,最可能的原因是学习率太大,模型一步迈过头,数值炸了。先降低学习率试试,大概率能解决。如果降学习率没用,再检查数据里是不是有 NaN 或者 Inf。文本任务里这种问题很常见,某些样本长度异常或包含脏数据,就会污染整个 batch。另外,混合精度训练的 GradScaler 如果设置不对,也会导致梯度溢出成 NaN。这里有一个通用排查法:把torch.autograd.set_detect_anomaly(True)打开,它会帮你定位具体是哪一行计算图产生了 NaN,但代价是训练速度变慢,所以只在调试时用。
6.3 结果复现不了
“我明明设了随机种子,为什么两次训练结果还是不一样?”这个问题我被问过很多次。前面讲过的设置固定种子只是第一步。模型训练还受数据加载顺序影响,而 DataLoader 的多进程 worker 即使设了 Python 种子,也可能因为系统调度导致采样顺序不同。所以,要让数据顺序完全一样,可以考虑把shuffle的随机种子也用固定的方式控制。
GPU 算术本身也不是完全确定的,同一份数据在 GPU 上跑两次,结果可能有细微差别,这是硬件层面的浮点运算问题。特别是当你用了torch.backends.cudnn.benchmark = True时,卷积算法可能会被动态选择,导致结果不稳定。在做消融实验的时候,如果要比较不同方法之间的差异,一定要固定好这些因素,否则实验结论可能是噪音驱动的。
6.4 跨环境部署的坑
训练完的模型要部署到生产环境,又是另一番折腾。最通用稳妥的方式是先把模型导出成 ONNX 格式,然后再做后续的推理优化。ONNX 是通用的模型中间格式,很多推理引擎都支持。导出时记得把torch.onnx.export的输入尺寸固定好,因为如果你的模型里有动态维度操作,比如不同长度的序列,导出时就比较麻烦。
另外,部署时模型的输入预处理要和训练时完全一致。很多模型上线之后效果变差,不是模型本身不行,而是预处理逻辑变了。举个例子,训练时图片是 normalize 到 0-1 的,部署时如果忘了这一步,模型输入分布完全不一样,输出自然很怪。我习惯把预处理逻辑和模型权重放在一起发布,作为一个整体服务,尽量消除环境差异。模型导出成功后,先用同样的输入分别在 PyTorch 和推理引擎里跑一次,对比输出是否接近。误差在一定范围内是可以接受的,如果差太多,要回头排查算子兼容问题。
我在实际使用中还有一个小习惯:所有和随机性相关的操作都留一个开关,包括数据增强、Dropout、采样逻辑,这样出了问题时方便快速复现和排查,不需要大规模重写代码。调试框架问题时,我会先写一个最小复现脚本,把模型降到最简单的版本,一点点加复杂度,直到复现为止。这种做法比在完整项目里大海捞针高效得多。
说到底,PyTorch 不过是一个工具,但好工具能让人少走很多弯路。它的设计足够贴近思考方式,让我可以把精力集中在模型本身,而不是框架的坑上。这两年我的工作重心逐渐从“怎么把模型训出来”转向“怎么把它稳定可靠地跑起来”,PyTorch 在这两件事上都给了我足够的支持。如果你还在观望,或者已经踩了几个坑,希望这篇文章能帮你把路铺平一点。