news 2026/10/1 19:09:30

YOLOv5模型转换实战:PyTorch转ONNX导出、验证与优化部署全指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
YOLOv5模型转换实战:PyTorch转ONNX导出、验证与优化部署全指南

简介: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 版本的 torch,CPU 版足够,导出过程不涉及 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_location='cpu') model.eval() model.model[-1].export = True

attempt_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×4010500
416×41613×1326×2652×5217700
512×51216×1632×3264×6426880
640×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_location='cpu') 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_version=12, do_constant_folding=True, 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_version=12表示用 ONNX 算子集第 12 版,这是兼容性和算子支持度比较平衡的起点。do_constant_folding=True会把能提前算好的常量折叠进权重,减小模型体积,但个别 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_location='cpu').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 onnx,opset 提至 12,torch 升到 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 --onnx=yolov5s.onnx --saveEngine=yolov5s.engine --fp16

--fp16开启半精度推理,网络带宽和计算量几乎减半,在 Jetson 上收益非常明显。代价是输出数值会出现更大浮点误差,个别小目标框可能会消失,精度敏感性测试必须在目标场景里做。如果输入尺寸要动态,加一组 profile 参数:

trtexec --onnx=yolov5s.onnx \ --minShapes=images:1x3x640x640 \ --optShapes=images:4x3x640x640 \ --maxShapes=images:8x3x640x640 \ --saveEngine=yolov5s_dynamic.engine

minShapes、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_target=ct.target.iOS15)

minimum_deployment_target建议至少 iOS 15,因为 CoreML 对动态 shape 和 Resize 算子的支持在 iOS 15 以后才完善。注意转换结果同样不带 NMS,水果家的后处理要自己写,或者用VNCoreMLRequest在外面包一层。

Android 端和边缘盒子则常见两条线:TFLite 是 Google 系设备的首选,NCNN 是腾讯开源、对国产芯片适配较广的推理库。TFLite 没有直接从 ONNX 转换的官方通道,常见做法是用onnx2tf先生成 SavedModel,再用tflite_converter转 TFLite;NCNN 有独立的转换工具:

onnx2ncnn yolov5s.onnx yolov5s.param yolov5s.bin

NCNN 转换有几个硬性要求:输入尺寸固定、动态轴必须删掉、部分算子要预先替换。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_type=QuantType.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_location='cpu').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_version=12, do_constant_folding=True, ) 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.py

export.py用官方脚本负责常规导出,verify_onnx.py就是我们前面写的验证函数入口。从那以后,我每次导出都强制走一遍“导出到对比验证”的完整动作,确认 max diff 合格才允许自己往 TensorRT、NCNN、RKNN 方向继续转,再没出现过拿着一个错误输出直接上板子折腾半天的情况。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 19:09:11

COZE低代码AI平台:5大核心能力+2类交付物实战解析

1. 项目概述:为什么“5.2平台一:COZE”突然成为高频搜索词? 最近两周,我在三个不同行业的客户群里都看到同一个词被反复提起——“5.2平台一:COZE”。不是“coze怎么用”,也不是“coze和dify哪个强”&#…

作者头像 李华
网站建设 2026/10/1 19:08:38

AI辩论系统:用多智能体对抗检验投资观点,拆穿逻辑陷阱

1. 整体设计思路:为什么是“辩论”而不是“预测”圈子里一直有个争论:AI到底能不能拿来炒股?我的答案很土——别让机器替你下单,但可以让机器替你“挨打”。这套系统做完之后,我最大的感受是:它压根不是在跟…

作者头像 李华
网站建设 2026/10/1 19:08:36

散列函数六种构造方法详解:从原理到工程选型实战

散列函数看起来是个有点“学院派”的概念,但只要你写过缓存、设计过数据库表,或者哪怕只是用过HashMap,你就已经在跟它打交道了。散列函数的核心任务,就是把一个任意长度的关键字,通过某种规则映射到一个固定范围的地址…

作者头像 李华
网站建设 2026/10/1 19:07:20

Python二手房房价预测全流程:爬虫、清洗、建模到可视化

简介:基于Python的深圳二手房房价预测与分析可视化项目,数据来自链家,面向计算机相关专业的毕业设计、课程设计及项目实战学习者,也适合有Python基础、希望了解数据采集、房价回归预测和数据可视化流程的中级开发者。资源共13个文…

作者头像 李华
网站建设 2026/10/1 19:07:10

AI Agent 架构分层实战:MCP、A2A 与 Agent Skills 协作指南

1. 从一堆协议名词说起:Agent 生态到底在分层什么过去一年里,只要你在做 AI Agent 相关的东西,几乎不可能绕开三个词:MCP、A2A、Agent Skills。我第一次同时看到这三个概念摆在一起的时候,脑子里第一反应是——这不就是…

作者头像 李华