☰
PyTorch 便携参考:从模型训练到部署的工程化实践指南
2026/10/11 7:55:55 网站建设 项目流程

1. 为什么我要写这份 PyTorch 便携参考

搞深度学习这几年,我最大的感受就是:模型训练本身其实不是最折磨人的环节,真正让人头大的是从"能跑"到"能复现"再到"能部署"这条链路上的一堆琐碎细节。你可能也遇到过这种情况——实验室里跑通的模型,换台机器就报错;训练脚本写得挺漂亮,但过两周自己都忘了某个参数为什么设成那个值;好不容易训完一个模型,想部署到实际业务里,发现推理速度和精度跟训练时对不上。

这份参考就是我在反复踩坑之后整理出来的。它不是什么官方文档的翻译,也不是教科书式的理论讲解,而是一个一线从业者在实际项目里积累下来的操作手册。核心覆盖三块内容:模型构建的规范化写法、训练过程中的关键配置、以及从训练到部署的完整链路。适合已经了解 PyTorch 基础语法、但想把自己的工程能力提升一个档次的朋友。如果你还在纠结nn.Module和nn.Sequential有什么区别,那这篇可能对你来说稍微超前了一点,但也可以先收藏着,等基础打牢了再回来看。

我写这份东西的初衷很简单:让下一个接手我代码的人(包括三个月后的我自己)能在一小时内把整个项目跑起来。这个目标听起来不高,但真正做到需要很多细节上的功夫。

2. 项目整体设计与思路拆解

2.1 为什么选择"便携"作为核心目标

"便携"这个词在深度学习语境下,我给它定义了三个层面的含义。第一层是环境便携——代码不依赖特定机器的特定路径、特定 CUDA 版本、特定 Python 环境。第二层是结构便携——模型定义、训练逻辑、数据处理、推理接口之间解耦清晰,换一个数据集或者换一个模型 backbone,不需要大改代码。第三层是知识便携——配置和参数的选择理由被完整记录下来,别人看代码就能理解为什么这么设,而不是靠口口相传。

这三个层面里,第三层最容易被忽略,但恰恰是最重要的。我见过太多项目,代码本身写得没问题,但没有任何注释解释超参数的来源,导致后来的人不敢改也不敢动,只能原样复制。一旦遇到新场景,整个项目就废了。

2.2 整体架构的分层思路

我把整个项目分成四个层次,从下到上依次是:数据层、模型层、训练层、部署层。每一层只跟相邻层交互,跨层调用严格禁止。这个约束听起来有点教条,但实际用起来会发现它帮你省掉了大量调试时间。

数据层负责原始数据的读取、清洗、增强和批处理。这一层的输出是标准的DataLoader对象,上层不需要知道数据是从 CSV 来的还是从图像文件夹来的。模型层只负责定义网络结构,不包含任何训练逻辑,也不关心输入数据的具体格式,只要求输入张量的形状符合约定。训练层把数据层和模型层串起来,管理优化器、学习率调度、损失函数、日志记录和 checkpoint 保存。部署层则把训练好的模型导出成适合推理的格式,并提供统一的预测接口。

这种分层的好处在于:当你想换一个模型试试效果时,只需要改模型层,其他三层完全不动。当你想把训练好的模型部署到移动端时,只需要改部署层,训练代码原封不动。这种灵活性在快速迭代的项目里价值极高。

2.3 工具选型的取舍逻辑

在工具选择上,我坚持一个原则:能用标准库解决的,不引入第三方依赖。PyTorch 生态里有很多优秀的辅助库,比如各种训练框架、各种模型库,但每引入一个依赖就多一个版本兼容的隐患。我的做法是,核心训练流程只用 PyTorch 原生 API 加上少量必要的工具库,比如用于指标计算的torchmetrics和用于进度显示的tqdm。

配置文件方面,我试过 YAML、JSON、Python 字典三种方案,最后选了Python 字典 + 命令行参数覆盖的组合。原因很简单:Python 字典可以直接写注释,可以用变量,可以做条件判断,灵活性远超 YAML 和 JSON。而命令行参数覆盖则保证了在不修改配置文件的情况下快速做实验,比如临时调整学习率跑一组对比。

日志记录我用的是 Python 标准库的logging模块,配合 TensorBoard 做可视化。没有用更复杂的实验管理工具,因为对于大多数中小规模项目来说,TensorBoard 加上规范的文件命名已经足够了。过度工程化反而会增加维护成本。

3. 核心细节解析与实操要点

3.1 模型定义的规范化写法

模型定义看起来简单,但里面有很多细节决定了后续部署的顺利程度。我总结了几条硬性规则,每条都是踩坑换来的。

规则一:所有网络层在__init__中定义,所有前向逻辑在forward中实现。这条规则看起来是常识,但我见过太多人在forward里动态创建层,导致模型导出时出现各种奇怪的问题。动态创建层在训练时可能没问题,但一旦用torch.jit.script或者torch.jit.trace导出,就会报错。

规则二:输入输出的形状在文档字符串中明确标注。比如一个图像分类模型,我会在forward的 docstring 里写清楚输入是(batch_size, 3, 224, 224),输出是(batch_size, num_classes)。这个习惯在多人协作时特别有用,别人不需要读你的实现就能知道接口约定。

规则三:避免在模型中使用全局变量或外部状态。模型应该是一个纯函数式的存在,给定输入就产生确定的输出。如果模型依赖某个外部变量,部署时这个变量可能不存在,导致推理失败。

下面是一个符合上述规则的模型定义示例:

import torch import torch.nn as nn class ImageClassifier(nn.Module): """图像分类模型,支持任意数量的类别。 输入: (batch_size, 3, 224, 224) 的 RGB 图像张量 输出: (batch_size, num_classes) 的 logits """ def __init__(self, num_classes=10, dropout_rate=0.5): super().__init__() self.num_classes = num_classes self.dropout_rate = dropout_rate # 特征提取部分 self.features = nn.Sequential( nn.Conv2d(3, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(128, 256, kernel_size=3, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True), nn.AdaptiveAvgPool2d((1, 1)), ) # 分类头 self.classifier = nn.Sequential( nn.Flatten(), nn.Dropout(dropout_rate), nn.Linear(256, num_classes), ) def forward(self, x): x = self.features(x) x = self.classifier(x) return x

这个模型定义有几个值得注意的点。AdaptiveAvgPool2d((1, 1))保证了无论输入图像尺寸是多少,输出都是固定大小的特征向量,这让模型可以接受不同尺寸的输入。inplace=True的 ReLU 节省了显存,但在某些需要保留中间激活值的场景下需要关掉。Dropout 放在分类头之前而不是特征提取部分,是因为卷积层的参数量相对较少,过拟合风险主要来自全连接层。

3.2 数据管道的构建要点

数据管道是训练效率和模型效果的基础。我见过很多项目,模型结构很先进,但因为数据管道写得低效,GPU 利用率只有 30% 不到,训练时间白白浪费。

第一个要点是num_workers的设置。这个参数决定了有多少个子进程负责数据加载。设置得太小,GPU 等数据;设置得太大,CPU 上下文切换开销增加。我的经验值是设置为 CPU 核心数的 70% 左右。比如 8 核机器设 6,16 核机器设 12。但要注意,如果数据预处理特别重(比如大图像的解码和增强),可以适当调大;如果数据已经在内存里,设 0 或 2 就够了。

第二个要点是pin_memory的开启。当使用 GPU 训练时,把pin_memory=True打开可以让数据从 CPU 内存到 GPU 显存的传输更快。这个参数几乎没有任何副作用,唯一需要注意的是它会占用一部分锁页内存,如果系统内存紧张需要留意。

第三个要点是数据增强的策略。训练时的增强和验证时的预处理必须区分开。训练时可以用随机裁剪、随机翻转、颜色抖动等增强手段,验证和测试时只能用确定性的中心裁剪和归一化。我见过有人把训练增强用到了验证集上,导致验证指标波动很大,白白浪费了很多调参时间。

from torch.utils.data import DataLoader from torchvision import transforms # 训练时的预处理:包含随机增强 train_transform = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), transforms.RandomHorizontalFlip(p=0.5), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) # 验证/测试时的预处理:确定性变换 val_transform = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) train_loader = DataLoader( train_dataset, batch_size=32, shuffle=True, num_workers=6, pin_memory=True, drop_last=True, # 丢弃最后一个不完整的 batch ) val_loader = DataLoader( val_dataset, batch_size=64, shuffle=False, num_workers=6, pin_memory=True, )

注意drop_last=True这个设置。当数据集大小不能被 batch size 整除时,最后一个 batch 会比其他 batch 小。如果模型里有 BatchNorm 层,这个小 batch 会导致统计量估计不准,影响训练稳定性。所以训练时通常开启drop_last,验证时则不需要,因为验证不更新 BatchNorm 的统计量。

3.3 训练循环的关键配置

训练循环是整个项目的核心,里面有几个参数直接决定了训练能否收敛以及收敛得多快。

学习率的选择是最关键的。我的经验是,对于 Adam 优化器,初始学习率设在 1e-3 到 1e-4 之间;对于 SGD,设在 1e-2 到 1e-1 之间。但这不是绝对的,具体要看模型深度、batch size 和数据集大小。一个实用的技巧是先用一个较小的学习率跑几百步,观察 loss 下降的速度,然后逐步调大,直到 loss 开始震荡,再往回退一半。

学习率调度策略同样重要。我用得最多的是余弦退火(Cosine Annealing)和阶梯下降(Step LR)。余弦退火适合训练轮数确定的情况,它让学习率从初始值平滑下降到接近零,通常能获得更好的最终精度。阶梯下降适合训练轮数不确定的情况,每过固定的 epoch 数把学习率乘以一个衰减系数。

梯度裁剪在训练 RNN 或 Transformer 时几乎是必须的。即使训练 CNN,如果发现 loss 偶尔出现 NaN,也值得加上梯度裁剪。torch.nn.utils.clip_grad_norm_是最常用的方法,把梯度的 L2 范数限制在一个阈值内。

import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR # 优化器配置 optimizer = optim.AdamW( model.parameters(), lr=1e-3, weight_decay=1e-4, # 权重衰减,防止过拟合 ) # 学习率调度器 scheduler = CosineAnnealingLR( optimizer, T_max=num_epochs, # 总训练轮数 eta_min=1e-6, # 最小学习率 ) # 训练循环 for epoch in range(num_epochs): model.train() for batch_idx, (data, target) in enumerate(train_loader): data, target = data.cuda(), target.cuda() optimizer.zero_grad() output = model(data) loss = criterion(output, target) loss.backward() # 梯度裁剪 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() scheduler.step() # 验证 model.eval() with torch.no_grad(): for data, target in val_loader: data, target = data.cuda(), target.cuda() output = model(data) # 计算验证指标

这里用AdamW而不是Adam,是因为AdamW把权重衰减和梯度更新解耦了,在带权重衰减的场景下效果更好。这是近几年的一个共识,如果你还在用Adam加weight_decay参数,建议换成AdamW。

注意:optimizer.zero_grad()的位置很关键。放在loss.backward()之前是标准做法,但如果你用了梯度累积(gradient accumulation),就需要调整位置。梯度累积是在显存不足时模拟大 batch 的常用技巧,每累积 N 个 batch 才更新一次参数。

4. 实操过程与核心环节实现

4.1 从零搭建一个可复现的训练项目

我以一个图像分类任务为例,完整走一遍从项目初始化到训练完成的流程。这个流程可以直接套用到其他任务上,只需要替换数据层和模型层的具体实现。

第一步是项目目录结构的规划。我习惯用下面这种结构:

project/ ├── configs/ # 配置文件 │ └── default.py ├── data/ # 数据相关 │ ├── dataset.py │ └── transforms.py ├── models/ # 模型定义 │ └── classifier.py ├── engine/ # 训练和验证逻辑 │ ├── trainer.py │ └── evaluator.py ├── utils/ # 工具函数 │ ├── logger.py │ └── checkpoint.py ├── deploy/ # 部署相关 │ └── export.py ├── train.py # 训练入口 └── requirements.txt # 依赖清单

这个结构的核心思想是关注点分离。每个目录只负责一件事,目录内部的模块之间可以互相引用,但跨目录的引用要尽量少。train.py是唯一的入口文件,它读取配置、构建数据管道、初始化模型、启动训练循环。

第二步是配置文件的编写。我把所有可调参数集中在一个 Python 文件里,用嵌套字典组织:

# configs/default.py config = { 'data': { 'root': './data/images', 'num_classes': 10, 'batch_size': 32, 'num_workers': 6, 'image_size': 224, }, 'model': { 'name': 'ImageClassifier', 'dropout_rate': 0.5, }, 'train': { 'epochs': 100, 'lr': 1e-3, 'weight_decay': 1e-4, 'clip_grad_norm': 1.0, 'save_dir': './checkpoints', 'log_interval': 50, }, 'deploy': { 'export_format': 'torchscript', 'input_shape': [1, 3, 224, 224], }, }

这种写法的好处是可以用 Python 的语法做条件配置。比如根据是否使用 GPU 来决定num_workers的值,或者根据模型名称自动选择对应的预处理参数。YAML 和 JSON 做不到这一点。

第三步是训练入口的编写。train.py的逻辑很直接:解析命令行参数、加载配置、设置随机种子、构建数据管道、初始化模型和优化器、启动训练循环、保存最终模型。

import argparse import random import numpy as np import torch from configs.default import config def set_seed(seed=42): """设置所有随机种子,保证实验可复现。""" random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) # 下面两行会让 cuDNN 的卷积算法选择变得确定 # 代价是可能损失一点性能 torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False def main(): parser = argparse.ArgumentParser() parser.add_argument('--config', type=str, default='default') parser.add_argument('--lr', type=float, help='覆盖配置文件中的学习率') parser.add_argument('--batch_size', type=int, help='覆盖 batch size') args = parser.parse_args() # 命令行参数覆盖配置文件 if args.lr is not None: config['train']['lr'] = args.lr if args.batch_size is not None: config['data']['batch_size'] = args.batch_size set_seed(42) # 后续构建和训练逻辑 ... if __name__ == '__main__': main()

set_seed这个函数看起来简单,但它是实验可复现的基石。我建议在项目一开始就加上,而不是等到发现结果无法复现时才补。注意cudnn.deterministic = True会牺牲一点性能,如果对复现要求没那么严格,可以只设前四个种子。

4.2 模型导出与推理接口的实现

训练完成之后,下一步是把模型导出成适合部署的格式。PyTorch 提供了两种主要的导出方式:TorchScript和ONNX。TorchScript 是 PyTorch 原生的格式,导出后可以在没有 Python 环境的 C++ 程序里加载。ONNX 是跨框架的格式,可以在 TensorFlow、ONNX Runtime 等环境中使用。

我通常优先选 TorchScript,因为它的导出过程更简单,对 PyTorch 特性的支持也更完整。导出方式有两种:torch.jit.trace和torch.jit.script。trace通过实际运行一次模型来记录计算图,适合没有控制流的模型。script通过分析源码来编译,支持控制流,但对代码写法有更多限制。

import torch def export_to_torchscript(model, save_path, input_shape): """把模型导出为 TorchScript 格式。""" model.eval() example_input = torch.randn(*input_shape) # 使用 trace 方式导出 traced_model = torch.jit.trace(model, example_input) # 保存 traced_model.save(save_path) print(f'模型已导出到 {save_path}') # 验证导出结果 loaded_model = torch.jit.load(save_path) with torch.no_grad(): original_output = model(example_input) loaded_output = loaded_model(example_input) # 检查输出是否一致 max_diff = (original_output - loaded_output).abs().max().item() print(f'导出前后最大差异: {max_diff:.6f}') assert max_diff < 1e-5, '导出前后输出差异过大'

导出后的验证步骤非常重要。我见过不少情况,导出过程没有报错,但推理结果跟原始模型对不上。原因可能是模型里有不支持的操作被静默替换了,或者某些层的状态在导出时没有正确保存。加上这个验证步骤,可以及早发现问题。

推理接口的设计要遵循简单、稳定、无状态三个原则。简单是指调用方式越少越好,最好一个函数搞定。稳定是指输入输出的格式固定,不随内部实现变化。无状态是指每次调用之间互不影响,不依赖任何外部变量。

class Predictor: """统一的推理接口。""" def __init__(self, model_path, device='cuda'): self.device = torch.device(device) self.model = torch.jit.load(model_path, map_location=self.device) self.model.eval() # 预处理参数,与训练时保持一致 self.mean = torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1) self.std = torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1) @torch.no_grad() def predict(self, images): """输入一批图像张量,返回预测类别和置信度。""" images = images.to(self.device) images = (images - self.mean.to(self.device)) / self.std.to(self.device) logits = self.model(images) probs = torch.softmax(logits, dim=1) confidences, predictions = probs.max(dim=1) return predictions.cpu().numpy(), confidences.cpu().numpy()

这个Predictor类把预处理、推理、后处理封装在一起,调用方只需要传入原始图像张量,就能拿到预测结果。预处理参数硬编码在类里,保证了推理时的预处理跟训练时完全一致。这是很多部署事故的根源——训练时用了某种归一化,推理时忘了或者用错了参数。

4.3 性能优化的几个实操技巧

模型能跑通之后,下一步就是让它跑得快。我总结了几个投入产出比最高的优化手段。

混合精度训练是第一个要上的。用torch.cuda.amp把部分计算转成 FP16,可以在几乎不损失精度的情况下把训练速度提升 30% 到 50%,同时显存占用减少将近一半。用法很简单,用autocast上下文管理器包住前向计算,用GradScaler处理梯度缩放。

from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() 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.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) scaler.step(optimizer) scaler.update()

注意scaler.unscale_这一步。因为GradScaler会把 loss 放大,梯度也跟着放大,所以在做梯度裁剪之前需要先还原。如果忘了这一步,裁剪阈值就失去意义了。

数据加载的优化是第二个重点。除了前面提到的num_workers和pin_memory,还有一个容易被忽略的点是避免在__getitem__里做重复计算。比如每次读取图像都要重新计算归一化参数,这就是浪费。正确的做法是把能预计算的都预计算好,__getitem__里只做必要的操作。

推理时的优化主要是torch.no_grad()和model.eval()的配合使用。no_grad关闭梯度计算,节省显存和计算量。eval把 BatchNorm 和 Dropout 切换到推理模式。这两个操作缺一不可,我见过只写了no_grad忘了eval的情况,导致推理结果跟验证结果对不上。

5. 常见问题与排查技巧实录

5.1 训练不收敛的排查思路

训练不收敛是最高频的问题,表现是 loss 不下降或者震荡剧烈。我的排查顺序是这样的:

先检查数据和标签是否对应。这是最容易被忽略但也最常见的原因。把几个 batch 的数据可视化出来,人工确认标签是否正确。我遇到过一次,数据加载器的文件排序和标签文件的排序不一致,导致模型一直在学错误的对应用关系,loss 自然降不下来。

再检查学习率是否合适。学习率太大会导致 loss 震荡,太小会导致下降缓慢。一个快速的判断方法是看 loss 曲线的形状。如果 loss 在初期快速下降然后停滞,可能是学习率太小;如果 loss 上下跳动没有明显趋势,可能是学习率太大。

然后检查模型初始化。默认的初始化在大多数情况下够用,但如果模型很深或者用了特殊的激活函数,可能需要手动初始化。比如用kaiming_normal_初始化 ReLU 之前的卷积层,用xavier_normal_初始化 tanh 之前的层。

最后检查 BatchNorm 的状态。如果 batch size 太小(比如小于 8),BatchNorm 的统计量估计会很不准确,导致训练不稳定。这种情况下可以考虑用 GroupNorm 或者 LayerNorm 替代。

5.2 显存不足的应对策略

显存不足的报错信息通常是CUDA out of memory。解决思路按优先级排列:

策略显存节省对训练的影响实施难度
减小 batch size高可能影响收敛低
混合精度训练中几乎无影响低
梯度累积高训练变慢中
梯度检查点高训练变慢中
模型并行高实现复杂高

我的建议是先用混合精度训练,如果还不够就减小 batch size 配合梯度累积。梯度检查点(gradient checkpointing)适合特别深的模型,它用计算时间换显存空间,把中间激活值丢掉,反向传播时重新计算。

注意:减小 batch size 之后,学习率通常也需要相应调小。一个经验公式是学习率与 batch size 的平方根成正比。如果 batch size 减半,学习率乘以 0.7 左右。

5.3 部署时的精度对齐问题

训练时精度很高,部署后精度下降,这个问题我遇到过好几次。原因通常有三个:

预处理不一致。训练时用的归一化参数、图像尺寸、通道顺序,推理时必须完全一致。我建议把这些参数写进模型文件或者配置文件里,而不是靠记忆。

BatchNorm 的 running stats 问题。如果导出模型时忘了调用model.eval(),BatchNorm 会继续用当前 batch 的统计量,导致结果不稳定。导出前一定要确认模型处于 eval 模式。

数值精度差异。GPU 和 CPU 的浮点运算结果可能有微小差异,如果模型对数值特别敏感,这个差异可能被放大。解决办法是在导出后做一次完整的精度验证,对比原始模型和导出模型在验证集上的指标。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
loss 为 NaN学习率过大、梯度爆炸打印每层梯度范数降低学习率、加梯度裁剪
验证指标远低于训练过拟合对比训练和验证 loss 曲线加正则化、数据增强、早停
训练速度慢数据加载瓶颈查看 GPU 利用率增大 num_workers、预取数据
显存溢出batch size 过大打印显存占用混合精度、梯度累积
导出模型报错有不支持的操作查看报错信息改写模型、用 script 替代 trace
推理结果不一致预处理或模式问题对比导出前后输出检查 eval 模式和预处理参数

6. 我在这套流程里踩过的坑

第一个坑是过度依赖默认参数。PyTorch 的默认参数在大多数情况下是合理的,但在特定场景下需要调整。比如DataLoader的num_workers默认是 0,意味着数据加载在主进程里做,GPU 大部分时间在等数据。这个默认值在调试时方便,但正式训练时一定要改。

第二个坑是忽略随机种子的影响。深度学习里随机性来源很多:参数初始化、数据打乱、Dropout、数据增强。如果不固定种子,两次训练的结果可能差好几个百分点。我现在的习惯是在项目模板里就把set_seed写好,每次实验都从固定的种子开始。

第三个坑是checkpoint 保存策略不当。我一开始只保存最后一个 epoch 的模型,结果有一次训练到一半机器断电,所有进度都没了。后来改成每个 epoch 都保存,并且保留验证指标最好的那个。再后来发现磁盘空间不够,又改成了只保留最近三个和最好的一个。这个策略在大多数项目里都够用。

第四个坑是配置文件没有版本管理。我试过用命令行参数直接改配置,结果过了一周完全不记得当时改了什么。现在我的做法是每次实验都生成一个独立的配置文件副本,跟 checkpoint 存在一起。这样任何时候都能复现当时的实验条件。

第五个坑是部署环境跟训练环境不一致。训练时用的 PyTorch 版本、CUDA 版本、依赖库版本,部署时最好保持一致。如果做不到,至少要做完整的精度验证。我遇到过一次,训练环境是 PyTorch 1.12,部署环境是 1.10,某些算子的实现有细微差异,导致推理结果对不上。

这套流程我用了大概两年,中间不断调整和补充。现在的版本不敢说完美,但至少让我在换机器、换数据集、换模型的时候不用从头再来。如果你也在做类似的事情,希望这些经验能帮你少走一些弯路。后面我还会继续整理模型压缩和量化相关的内容,那又是另一个大坑了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询