简介YOLOv5是目前广泛使用的高效实时目标检测模型在图像识别、自动驾驶、安防监控等场景中均有重要应用。PyTorch动态计算图虽便于训练与调试但实际部署时常需要转换成ONNX这种开放模型交换格式以便跨框架复用并利用ONNX Runtime等工具提升推理效率。面向有PyTorch基础并希望解决模型跨平台部署问题的开发者这份资料完整梳理了YOLOv5从PyTorch到ONNX的转换过程包括模型准备、torch.onnx.export导出、使用ONNX验证工具检查正确性、可选优化步骤以及进一步转成CoreML/TFLite的方法并针对框架间算子映射差异和精度损失给出了实用排错建议。压缩包大小约1.02MB内容紧凑可直接用于实操参考能显著缩短部署调研与排查时间。无论最终目标是服务端加速还是移动端集成都能从中获得明确指引。目前已有1342人学习下载可作为快速完成YOLOv5跨框架迁移的实用手册。1. 模型转换这关人人要过YOLOv5 从 PyTorch 换到 ONNX 到底图什么训练时 PyTorch 的灵活性和动态图机制让人离不开手但真到了部署环节PyTorch 模型反而是个烫手山芋服务端要用 C 推理移动端要走 CoreML、TFLite边缘盒子可能只吃 NCNN、RKNN你总不能让人家先把 PyTorch 环境装一遍。把模型转成 ONNX 就是这关的标准解法YOLOv5 从 PyTorch 转 ONNX 是目标检测落地最常碰到的流程也是后面所有部署动作的前提。这个资源解决的就是一件事让训练好的 YOLOv5 权重变成一份框架无关、后端可优化的静态计算图。适合正在做部署的算法工程师、边缘设备开发者和第一次碰模型转换的初学者——你不需要理解 ONNX 的全部规范照着参数走一遍能导出能验证就算过了这关。反直觉的一点是导出本身只占三成工作量七成都在导出后的校验和踩坑上。输出结构变没变、算子支不支持、动态维度能不能用这些问题不提前搞明白后面转 TensorRT 或移动端时会连环翻车。下面把这些细节全部拆开说。2. 导出前的准备工作版本、权重和输入尺寸先对齐2.1 环境三件套torch、onnx、onnxruntime 的版本怎么配先说最容易翻车的版本问题。PyTorch 的torch.onnx.export本质上是把 torch 的算子逐条映射成 ONNX 算子这个映射表跟着版本走。torch 1.13 和 torch 2.x 的算子支持范围差得很多onnx 1.12 和 onnx 1.16 能识别的算子也不是一回事。我的习惯是 torch 1.13 以上配 onnx 1.13 以上torch 2.x 就配 onnx 1.14 以上onnxruntime 版本尽量跟 onnx 大版本保持一致别把 onnx 升到 1.16 结果 onnxruntime 还是 1.10那样跑推理时经常报算子不支持。git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt pip install onnx onnxruntime第一行命令把官方仓库拉下来YOLOv5 的导出工具和模型定义都在里面。requirements.txt会装好 torch、torchvision 等训练依赖后面两行补上 ONNX 相关的东西。如果只是做导出和推理验证不需要装 CUDA 版本的 torchCPU 版足够导出过程不涉及 GPU 计算。2.2 加载权重eval 模式和 export 标记缺一不可权重加载看起来简单但在这里埋着三个细节。第一个是model.eval()PyTorch 模型默认是训练模式BatchNorm 和 Dropout 的行为跟推理时完全不同直接导出会把 BN 层的统计量带进图里推理结果可能不对。第二个是.float()从yolov5s.pt里取出来的权重如果是半精度或混合精度训练留下的要先统一转成 float32。第三个是官方仓库里models/yolo.py的Detect类有一个export属性导出前必须把它设成 True。import torch from models.experimental import attempt_load model attempt_load(yolov5s.pt, map_locationcpu) model.eval() model.model[-1].export Trueattempt_load是 YOLOv5 仓库里的加载函数它能自动处理权重文件里嵌套的 dict 结构比torch.load更省事。model.model[-1]取到的是最后一个 Detect 模块把它的export置 True 之后forward 会走专门为导出售后处理计算图的分支anchor grid 的生成、坐标解码这些操作会被展开成普通张量运算而不是训练时那种灵活的 Python 循环。没有这一行导出来的 ONNX 输出还是未解码的原始特征图部署后处理会完全对不上。2.3 输入尺寸640 不是随便定的模型里的 stride 说了算YOLOv5 的下采样倍数是 32也就是说输入图片的长宽必须能被 32 整除否则主干网络最后几层特征图的尺寸对不上。官方预训练权重默认的输入尺寸是 640×640即 640 32 × 20。导出时 dummy input 的 shape 必须和训练时一致(1, 3, 640, 640)顺序是 batch、通道、高、宽。不同输入尺寸对应的输出网格数量如下表这对后面验证输出 shape 很有用输入尺寸stride 32 网格stride 16 网格stride 8 网格总预测数COCO 80 类320×32010×1020×2040×4010500416×41613×1326×2652×5217700512×51216×1632×3264×6426880640×64020×2040×4080×8025200如果训练时自己改了 imgsz比如用的是 416那导出时 dummy 也要对应改成(1, 3, 416, 416)。另外还要提醒一句YOLOv5 推理前有 letterbox 预处理图片会先等比例缩放到接近目标尺寸再补边这一步在部署端也要同样实现否则 ONNX 输出的坐标和你预期的检测框位置会偏移。3. 用 torch.onnx.export 做导出四个参数吃透一次导出成功3.1 导出脚本从加载模型到生成 ONNX 文件的完整写法把上一章准备的东西串起来核心导出代码就是下面这一段。我用的是官方仓库里的attempt_load加载权重这样兼容性最好不建议自己用torch.load再慢慢拆权重 dict。import torch from models.experimental import attempt_load weights yolov5s.pt model attempt_load(weights, map_locationcpu) model.eval() model.model[-1].export True dummy torch.zeros(1, 3, 640, 640) torch.onnx.export( model, dummy, yolov5s.onnx, input_names[images], output_names[output], opset_version12, do_constant_foldingTrue, dynamic_axes{images: {0: batch}, output: {0: batch}}, ) print(export ok)先说执行流程attempt_load加载权重后model.eval()切到推理模式model.model[-1].export True让 Detect 层走导出专用分支。dummy是一个全零张量只用来给 torch 提供输入 shape 参考不参与实际计算。torch.onnx.export会走一遍 forward 图把遇到的每个算子映射成 ONNX 算子最后写到yolov5s.onnx。input_names和output_names是给计算图的输入输出张量起名字后面用 ONNX Runtime 推理时要靠名字传输入、按索引取输出。opset_version12表示用 ONNX 算子集第 12 版这是兼容性和算子支持度比较平衡的起点。do_constant_foldingTrue会把能提前算好的常量折叠进权重减小模型体积但个别 torch 版本下会触发导出崩溃遇到再调成 False。如果不喜欢手写这段官方仓库里也带了现成的导出脚本等价于以上逻辑python export.py --weights yolov5s.pt --include onnx --opset 12--images 640可以指定导出时的输入尺寸不带就是默认 640。--dynamic会顺带把动态 batch 配上。这个脚本还会自动做一次简化比手写更省事但后面讲到的几个坑它同样会遇到。3.2 关键参数逐项拆解opset、输入输出名、do_constant_folding很多人第一次导出失败问题不在模型而在这三个参数没调对。opset 版本的坑最常见YOLOv5 的 Detect 导出路径用到了meshgrid、index_put这类算子opset 11 及以下对它们的支持不完整opset 12 开始才比较稳。torch 2.x 环境下我把 opset 提到 16 或 17 也能正常导出但下游转 CoreML 时新版算子反而更容易触发兼容问题所以默认还是推荐 12。input_names一旦定了就不要改因为后面所有部署脚本、TensorRT profile、NCNN 转换都要用同一个输入名。do_constant_folding建议保持 True它能帮你把 Detect 里那堆固定的 anchor 偏移量直接折叠成常量让计算图更干净。理解这几个参数的核心区别opset 决定 ONNX 图的算子粒度名字决定后端接口常量折叠决定计算图静态程度。参数作用踩坑提示opset_version指定 ONNX 算子集版本11 以下不支持部分算子12 是保险起点input_names给输入张量命名改名后所有下游配置要同步do_constant_folding折叠常量到权重中个别版本设 True 会崩溃可临时关掉dynamic_axes标记哪些维度是动态的宽高动态要注意算子兼容性3.3 动态轴配置batch 可变很容易高宽可变要慎重动态 batch 是部署里最常见的需求服务端批量推理时 batch 从 1 到 16 随意切ONNX 图不能定死成 1。写法是把dynamic_axes配到输入输出张量上dynamic_axes { images: {0: batch}, output: {0: batch}, }这个配置只把第 0 维 batch 设成动态高宽依然是固定 640。这么写最稳妥因为 YOLOv5 的 Detect 导出路径里有 anchor grid 的生成逻辑如果同时把高宽设成动态# 不推荐部分后端不支持 dynamic_axes { images: {0: batch, 2: height, 3: width}, output: {0: batch, 2: height, 3: width}, }一旦让高和宽可变导出图上会出现动态 shape 算子转 NCNN、RKNN、TensorRT 时很容易报不支持。而且很多后端的显存优化是按固定分辨率做的动态宽高会直接导致推理速度下降。我的建议是服务端只要动态 batch边缘端干脆 batch、宽高全部固定把能折的常量都折掉。注意导出时用了动态轴不代表部署端可以随便喂任意尺寸。部分后端只支持在预设的 min、max 范围内动态超出范围会直接推理失败。4. 常见问题与踩坑排查导出三分做排错七分查4.1 第一道校验onnx.checker 报错先看算子再查版本导出完不要急着拿去部署先用 ONNX 自带的检查器过一遍结构合法性。这一步能发现算子映射残缺、图的拓扑断裂这类低级问题。import onnx m onnx.load(yolov5s.onnx) onnx.checker.check_model(m) print(structure ok)check_model只做静态结构校验不跑数值所以它通过不代表模型真的能用。如果报错信息里出现Unsupported operator先看算子名再查版本——大概率是 opset 太老或者 onnx 库版本太低把 onnx 升到 1.13 以上opset 提到 12 再导一次大部分就过了。如果报Duplicate attribute这类解析错误多半是导出时 torch 和 onnx 的版本映射错位重装一套匹配的组合比在代码里绕路划算得多。4.2 第二道校验用 ONNX Runtime 跑一次推理做数值对比结构合法只是及格线真正要确认的是转换前后数值一致。做法很简单同一张输入图分别喂给 PyTorch 模型和 ONNX Runtime比较输出。注意 PyTorch 模型这边也要带上export True否则两边输出含义不同比对没有意义。import numpy as np import onnxruntime as ort import torch from models.experimental import attempt_load model attempt_load(yolov5s.pt, map_locationcpu).float() model.eval() model.model[-1].export True x np.random.rand(1, 3, 640, 640).astype(np.float32) with torch.no_grad(): y_torch model(torch.from_numpy(x))[0].numpy() sess ort.InferenceSession(yolov5s.onnx, providers[CPUExecutionProvider]) y_onnx sess.run(None, {images: x})[0] diff np.max(np.abs(y_torch - y_onnx)) print(shape:, y_torch.shape, y_onnx.shape) print(max diff:, diff)这段是排查工具的核心。shape 对不上说明导出时export标记没生效或者 YOLOv5 版本输出结构不一样max diff 在 1e-4 到 1e-3 量级是正常浮点误差超过 0.1 就要回头查预处理或者算子兼容性。注意sess.run(None, ...)的第二个参数是输入名字到 numpy 数组的映射名字必须和导出时input_names一致。4.3 五个高频翻车现场现象、原因、解决第一个导出时报Unsupported operator: aten::meshgrid。原因基本是 onnx 版本太旧或 opset 太低。解决pip install -U onnxopset 提至 12torch 升到 1.12 以上再导出。第二个ONNX Runtime 推理出来结果数量完全对不上COCO 模型应该输出 25200 个预测实际得到 3 张大特征图。原因是导出时漏了model.model[-1].export True导出的还是训练期的原始输出。解决重新导出并确认这行赋值在torch.onnx.export之前执行。第三个转成 TensorRT 或移动端格式后小目标完全检测不到。原因多数是 SiLU也就是 swish激活函数在目标后端的算子映射有精度差异或者被某些工具错误优化掉了。解决先测 FP32 的 ONNX Runtime 结果如果也没小目标说明是转出后预处理不一致如果 FP32 正常、FP16 消失就把 SiLU 换成 ReLU 再转一次代价是 mAP 略降。第四个导出成功、推理结果也对但速度比 PyTorch 还慢。原因是 ONNX Runtime 没开启图优化或者图里有大量冗余算子没做折叠。解决把graph_optimization_level设成ORT_ENABLE_ALL再用onnx-simplifier过一遍图最后确认后端用的是 CPU 还是 GPU 的 provider。第五个转 NCNN 或 RKNN 时报Input shape not support dynamic。原因是有动态维度或者动态 shape 算子留在图里。解决导出时去掉高宽动态只保留固定 batch或者干脆全部固定。5. 优化与进一步转换从 ONNX 到 Runtime、TensorRT、NCNN 和移动端5.1 ONNX Runtime先把推理跑到 CPU 上验证整个链路拿到 ONNX 文件第一件事是先用 ONNX Runtime 在 CPU 上跑通完整链路。这一步能确认转换结果可被真实推理引擎消费也能顺手排查出前面没暴露的算子兼容问题。import onnxruntime as ort so ort.SessionOptions() so.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL so.intra_op_num_threads 4 sess ort.InferenceSession(yolov5s.onnx, so, providers[CPUExecutionProvider]) outputs sess.run(None, {images: image_np}) detections outputs[0]graph_optimization_level开满后ONNX Runtime 会做算子融合和常量折叠这是 CPU 上提速最直接的手段。intra_op_num_threads控制单算子内部的线程数树莓派这类小设备建议给 2 到 4给多了反而因为线程切换损失性能。providers里写CPUExecutionProvider是强制用 CPU如果机器有 NVIDIA GPU把CUDAExecutionProvider放前面会自动优先走 GPU。需要明确一点ONNX 模型里不带 NMS。YOLOv5 导出的 ONNX 输出的是解码后的预测框和类别置信度但非极大值抑制仍然是后处理的一部分必须自己在应用层实现。很多人把 ONNX Runtime 接上以后发现输出了大量重叠框就以为是模型的问题其实只是 NMS 没写。5.2 TensorRT 转换FP16 精度、batch 和 dynamic shape 的处理做服务器或 Jetson 部署时TensorRT 是绕不开的高性能后端。它不吃 ONNX 原始文件需要先用trtexec转成 engine。trtexec --onnxyolov5s.onnx --saveEngineyolov5s.engine --fp16--fp16开启半精度推理网络带宽和计算量几乎减半在 Jetson 上收益非常明显。代价是输出数值会出现更大浮点误差个别小目标框可能会消失精度敏感性测试必须在目标场景里做。如果输入尺寸要动态加一组 profile 参数trtexec --onnxyolov5s.onnx \ --minShapesimages:1x3x640x640 \ --optShapesimages:4x3x640x640 \ --maxShapesimages:8x3x640x640 \ --saveEngineyolov5s_dynamic.engineminShapes、optShapes、maxShapes里的images必须和导出时的input_names对应。TensorRT 会按这三个档位优化内存布局实际推理的 batch 超出 max 范围会直接拒绝。新手最容易在这里漏掉的是动态宽高的 ONNX 在转 TensorRT 时会报不支持所以前面导出时别急着开高宽动态。5.3 移动端转换coremltools、TFLite 和 NCNN 的边界iOS 端一般用coremltools把 ONNX 转成 CoreML 模型代码非常短import coremltools as ct mlmodel ct.convert(yolov5s.onnx, minimum_deployment_targetct.target.iOS15)minimum_deployment_target建议至少 iOS 15因为 CoreML 对动态 shape 和 Resize 算子的支持在 iOS 15 以后才完善。注意转换结果同样不带 NMS水果家的后处理要自己写或者用VNCoreMLRequest在外面包一层。Android 端和边缘盒子则常见两条线TFLite 是 Google 系设备的首选NCNN 是腾讯开源、对国产芯片适配较广的推理库。TFLite 没有直接从 ONNX 转换的官方通道常见做法是用onnx2tf先生成 SavedModel再用tflite_converter转 TFLiteNCNN 有独立的转换工具onnx2ncnn yolov5s.onnx yolov5s.param yolov5s.binNCNN 转换有几个硬性要求输入尺寸固定、动态轴必须删掉、部分算子要预先替换。yolov5s.param是网络结构文本yolov5s.bin是权重文件。转换完还要检查 param 文件里是不是所有层都被支持转 RKNN 的流程类似用rknn-toolkit2加载 ONNX 后再 build 成rknn格式RKNN 对动态 shape 基本零容忍所以 RKNN 场景下固定尺寸导出是铁律。5.4 量化Int8 到底要不要做怎么做ONNX 转出来后模型体积和推理速度还能再压一轮这就是 Int8 量化。ONNX Runtime 自带量化接口静态量化效果明显比动态量化好因为它需要一批校准数据来统计每层激活值的范围。from onnxruntime.quantization import quantize_static, CalibrationDataReader, QuantType class MyDataReader(CalibrationDataReader): def get_next(self): # 每次返回一小批输入数据格式为 {images: numpy_array} ... quantize_static( yolov5s.onnx, yolov5s_int8.onnx, MyDataReader(), weight_typeQuantType.QInt8, )校准数据用 50 到 100 张覆盖真实场景的图片就够直接用训练集图片容易过拟合到熟悉场景。weight_type默认是 QUInt8对 CPU 优化更友好部分硬件对 QInt8 加速更激进要按实际后端决定。Int8 量化后 YOLOv5 的 mAP 通常会掉 2 到 4 个点小目标损失更快所以量化前先跑一遍 FP32 的基线量化后再跑同一套测试集差距可接受再上板。6. 把对比验证做成固定动作一段脚本守住转换质量6.1 一段可复用的导出验证脚本到了这一步导出和部署链路基本通畅剩下的就是把它变成可重复执行的固定流程。我每次拿到新的训练权重不会直接改完导出参数就跑而是用一个脚本把导出和数值对比打包在一起import numpy as np import onnxruntime as ort import torch from models.experimental import attempt_load def export_and_check(pt_path, onnx_path): model attempt_load(pt_path, map_locationcpu).float() model.eval() model.model[-1].export True torch.onnx.export( model, torch.zeros(1, 3, 640, 640), onnx_path, input_names[images], output_names[output], opset_version12, do_constant_foldingTrue, ) x np.random.rand(1, 3, 640, 640).astype(np.float32) with torch.no_grad(): y_pt model(torch.from_numpy(x))[0].numpy() sess ort.InferenceSession(onnx_path, providers[CPUExecutionProvider]) y_onnx sess.run(None, {images: x})[0] return np.max(np.abs(y_pt - y_onnx))把这段存成verify_onnx.py每次换权重只改两个路径参数输出 max diff 小于 1e-3 就可以进入下一步部署流程。这里面的逻辑和第四章的验证是同一套但变成函数后不会每一次都临场手写也就不会漏掉export True这种关键赋值。6.2 允许误差参考为什么不能要求完全一致验证时不少人问为什么 PyTorch 和 ONNX 的输出不能完全相等对比场景正常误差范围说明FP32 CPU 上 PT vs ONNX Runtime1e-4 ~ 1e-3浮点舍入是主因FP16 与 FP32 对比1e-2 ~ 1e-1半精度必然损失低位小数TensorRT engine vs ONNX Runtime1e-2 量级算子融合会改变计算顺序要求两个框架输出逐字节一致属于玄学范围部署上也没必要。判断标准是检测结果是否在 NMS 后仍然一致框位置偏移不超过几个像素置信度差在 0.01 以内就可以认为转换合格。6.3 从命令行到一键导出把流程固化下来最终我习惯在仓库里放一个deploy.sh把导出和验证串成一行python export.py --weights yolov5s.pt --include onnx --opset 12 python verify_onnx.pyexport.py用官方脚本负责常规导出verify_onnx.py就是我们前面写的验证函数入口。从那以后我每次导出都强制走一遍“导出到对比验证”的完整动作确认 max diff 合格才允许自己往 TensorRT、NCNN、RKNN 方向继续转再没出现过拿着一个错误输出直接上板子折腾半天的情况。希望帮到你。本文还有配套的精品资源点击获取