YOLOv5s模型部署加速:PyTorch权重转ONNX再到OpenVINO IR的完整实践
先交代一下背景我最近在某台 Windows 10 办公机上做目标检测模型的部署加速手头只有一个训练好的 YOLOv5s 预训练模型权重文件目标很直接——在 CPU 环境下把推理速度提上去最后的方案就是标题里那条链路PyTorch 权重 → ONNX → OpenVINO IR → Python 推理测试。整个过程踩了不少坑也总结了一些经验这里完整记录下来给同样在 Windows 上做模型部署的朋友当一条可复现的路线参考。1. 环境准备版本选型与依赖安装的取舍1.1 Python 与 YOLOv5 源码的版本搭配先说环境。我用的机器是 Windows 10 专业版CPU 是常见的台式机处理器没有独立显卡所以加速只能靠 CPU。Python 我选了 3.9.13这个版本在 OpenVINO 和 PyTorch 之间兼容性比较稳不太容易撞上某些轮子装不上的问题。如果你电脑里已经装了别的版本3.8 到 3.11 基本都能跑通但我个人不建议一上来就追最新的 Python 3.12因为 OpenVINO 某些旧版本的依赖库对它的支持并不好反而浪费时间。YOLOv5 源码我直接拉取了当时较新的 release 版本。需要注意网上有很多 YOLOv5 的 fork功能大同小异但导出 ONNX 的入口脚本参数可能不完全一致。我建议使用官方仓库的版本这样后面执行export.py时命令行为最标准遇到问题也更容易搜到答案。拉下来之后在项目根目录执行pip install -r requirements.txt这一步会把 torch、torchvision、numpy、opencv-python 等基础依赖都装好。如果下载速度慢可以临时换国内镜像源比如-i https://pypi.tuna.tsinghua.edu.cn/simple实测会快很多。1.2 预训练权重与 OpenVINO 工具包的安装坑预训练权重我直接用了 YOLOv5 官方发布的yolov5s.pt。这个文件大概 14~15MB下载地址在官方仓库的 README 里就有。如果你之前下载过注意确认文件没损坏torch 加载时如果报unexpected EOF之类的错误多半是权重文件没下全重新下载一次就行。OpenVINO 工具包我安装了完整版因为里面不仅包含推理引擎还有模型转换工具。安装命令是pip install openvino这里有一个非常容易踩的坑如果你只装了openvino这个运行时包后续做模型转换时还需要额外的工具。从 2022.3 版本开始OpenVINO 把模型转换工具和运行时合并到了同一个发行包里新版本直接提供了一个ovc命令同时也保留了对旧版mo命令的兼容。为了确保命令可用我建议用pip install openvino装最新稳定版然后到 Python 的 Scripts 目录下确认有没有mo.exe或ovc.exe。如果用的是比较老的教程里面写的是pip install openvino-dev其实那是一条旧路线现在统一了不再需要单独装 dev 包。装完之后可以用一个简单命令验证环境是否正常python -c from openvino.runtime import Core; print(Core().available_devices)能打印出[CPU]就说明推理引擎已经可以识别设备了。这一步很重要千万不要跳过我见过有同事装完包后没验证直接跑到推理环节才发现 OpenVINO 根本没装对。2. YOLOv5s 导出 ONNX命令参数与常见报错排查2.1 为什么要先转 ONNX 而不是直接转 IR很多人会问既然最终要用 OpenVINO为什么不直接用 PyTorch 权重转 IR非要中间插一个 ONNX 环节这个问题的核心在于格式转换的成熟度。YOLOv5 官方对 ONNX 导出的支持做得非常完善导出脚本里已经把很多算子的兼容性问题处理好了而 OpenVINO 的模型优化器对 ONNX 格式的解析也是最稳的。PyTorch 直接转 IR 也不是不行但中间层的算子解析容易出幺蛾子遇到不支持的算子还得手动改图。ONNX 在这里扮演的是一个“中间表示层”的角色。它像是一个通用翻译器把 PyTorch 的动态图变成一份静态的计算图描述这份描述记录清楚每个节点的输入输出张量形状、数据类型和算子类型。OpenVINO 拿到 ONNX 之后再把它进一步优化成自己的 IR 格式并按目标硬件生成对应的推理代码。多一道 ONNX 中转其实是在给后续的部署减少不确定性。2.2 导出命令的完整用法与参数解读YOLOv5 源码根目录下有一个export.py脚本它就是负责干这件事的。最基本的导出命令是python export.py --weights yolov5s.pt --include onnx默认情况下这个命令会以 640x640 的输入尺寸导出 ONNX 文件。如果你对输入尺寸有特殊需求比如想用 416x416 的输入来换取更高的推理速度可以加上--imgsz 416。我这次为了对比最通用的效果直接用默认的 640。还有一个参数需要重点关注--opset。OpenVINO 对 ONNX 算子的支持是分版本推进的YOLOv5 官方默认导出时用的 opset 是 12这个版本在 OpenVINO 上的兼容性实测极好。如果你把 opset 调到 13 以上甚至更高模型转换工具可能会报一些“不支持的算子”或者“算子未注册”的错误。所以我建议在导出阶段直接指定 opset 12最大程度避开兼容性暗礁。另外如果你打算在推理阶段处理动态输入尺寸可以加--dynamic参数。但这并不意味着最终 OpenVINO 推理时一定是动态的。后面我会专门讲这个细节简单说就是动态 shape 会牺牲一部分推理性能能固定就固定。导出成功后会生成一个yolov5s.onnx文件。可以用下面这段代码简单验证一下 ONNX 模型是否完整pip install onnx python -c import onnx; m onnx.load(yolov5s.onnx); onnx.checker.check_model(m); print(OK)如果打印出 OK就说明 ONNX 计算图结构正确可以放心进入下一步转换。2.3 导出过程中的典型报错与解决导出这步虽然官方支持完善但也不是百分百顺利。我遇到过的报错主要是两类。第一类跟 torch 版本有关。某些较新的 torch 版本导出 YOLOv5 时会报Export the model to ONNX, but failed并且错误信息里有Unsupported operator之类的字样。这时候优先检查一下是不是在导出命令里加了多余的参数比如--simplify这个参数依赖 onnx-simplifier 库没装就会直接报错。如果想做图简化先执行pip install onnx-simplifier再在命令里加--simplify。第二类报错是输出目录的权限问题。默认情况下 ONNX 文件会生成在 weights 文件同目录下。如果你的权重文件放在系统盘且目录权限受限导出会在写文件时报权限错误。解决办法是显式指定目录比如python export.py --weights yolov5s.pt --include onnx然后手动把生成的 onnx 文件复制到工作目录。其实更稳妥的做法是在项目根目录下建一个models文件夹把权重文件放进去再导出。导出完成之后用 Netron 工具打开 ONNX 文件检查一下计算图结构是个好习惯。主要看两个地方输入节点是不是images形状是不是[1, 3, 640, 640]输出节点是不是一个一维数组YOLOv5s 的输出形状是[1, 25200, 85]。这 85 维由 4 个坐标、1 个目标分数和 80 个类别分数组成。如果输出形状对不上后面解析结果时一定会出问题。3. OpenVINO 生成推理加速模型从 ONNX 到 IR3.1 Model Optimizer 转换命令与产物结构得到 ONNX 模型之后下一步用 OpenVINO 的模型优化器把它转成 IR 格式。IR 格式是 OpenVINO 的“母语”它包含两个文件一个.xml文件描述计算图结构一个.bin文件保存权重参数。推理引擎加载模型时就是同时读取这两个文件。转换命令很简单mo --input_model yolov5s.onnx --output_dir openvino_model --model_name yolov5s如果你安装的是新版 OpenVINO有可能mo命令已经被ovc替代。大部分情况下新版也会留一个兼容入口实在不行可以直接用 Python 调python -m openvino.tools.mo --input_model yolov5s.onnx --output_dir openvino_model --model_name yolov5s执行完成后openvino_model文件夹下会出现yolov5s.xml和yolov5s.bin。转换过程会打印出模型输入输出节点的信息注意看一眼输入节点的 name通常是images输出是output0或类似名称。记下这些名字后续写推理代码时会用到。这里有个细节值得展开说。mo 工具在转换时实际上做了三件事一是把 ONNX 计算图解析成 OpenVINO 的内部表示二是做常量折叠、死节点消除这类静态优化三是根据目标设备的指令集预生成一些加速算子。所以你会发现同一个 ONNX 转出来的 IR 文件不一定完全相同它会受 OpenVINO 版本影响这也是为什么我强调视觉注意版本差异的原因。3.2 FP16/FP32 精度选择与 CPU 推理的影响转换时还有一个参数可以控制精度就是--data_type。默认情况下 mo 工具会保持原模型的 FP32 精度。如果你的目标设备支持 FP16 且以 CPU 推理为主可以尝试转成 FP16 格式因为 FP16 的数据量减半内存带宽占用减少推理时缓存命中率提升在某些 CPU 上能带来可观的性能收益。但要注意FP16 并不是在所有 CPU 上都有优势。OpenVINO 的 CPU 插件处理 FP16 模型时会先把权重转换成 FP32 再做计算中间有一定开销。我自己实测的结果是在较新的 CPU 上 FP16 模型推理时间与 FP32 几乎持平偶尔还慢个几毫秒在老旧 CPU 上则反而可能变慢。所以如果你用的是普通台式机建议老老实实用默认的 FP32不必刻意追求 FP16。3.3 转换后的模型验证转换完成后先用 OpenVINO 加载一下模型做一次“哑推理”确认计算图没问题。我写过一个简单的验证脚本from openvino.runtime import Core core Core() model core.read_model(openvino_model/yolov5s.xml) compiled_model core.compile_model(model, CPU) print(compiled_model)能正常打印出 compiled_model 信息就说明 IR 模型已经成功加载。这一步如果报错最常见的元凶是.xml和.bin文件名不匹配或者是两个文件没有放在同一目录。OpenVINO 读.xml时会自动去同目录找同名.bin如果你改名了或分开存放就会报找不到权重文件的错误。4. Python 推理测试加载、预处理与输出解析4.1 推理引擎加载与输入输出信息确认加载编译好模型之后先确认输入输出张量的形状。用下面这段代码from openvino.runtime import Core core Core() model core.read_model(openvino_model/yolov5s.xml) compiled_model core.compile_model(model, CPU) input_key compiled_model.input(0) output_key compiled_model.output(0) print(input_key, output_key)正常情况下输入是[1, 3, 640, 640]输出是[1, 25200, 85]。如果输出形状和你预期不符先回头检查 ONNX 文件多半是导出环节出了问题不要在推理阶段硬调代码去适配一个错误的输出。推理调用的接口非常简洁整个核心执行代码只有两行result compiled_model([input_blob])[output_key]这里的input_blob就是预处理之后的输入张量形状是(1, 3, 640, 640)数据类型是 float32。OpenVINO 接收的图片数据格式是 NCHW也就是先把通道数放到第二个维度这一点和 OpenCV 读出来的 HWC 格式不一样预处理时一定要做转置。4.2 letterbox 预处理与归一化细节YOLOv5 在训练时使用了一种叫 letterbox 的预处理方式。它先把原始图片按比例缩放到目标尺寸范围内然后在边缘用灰色像素填充直到图片变成正方形。这个处理的意义在于不改变图片中目标的相对比例避免直接拉伸导致目标变形、精度下降。实现 letterbox 的代码不复杂但细节容易出错。我直接给出可用的函数import cv2 import numpy as np def letterbox(img, new_shape(640, 640), color(114, 114, 114)): shape img.shape[:2] r min(new_shape[0] / shape[0], new_shape[1] / shape[1]) new_unpad (int(round(shape[1] * r)), int(round(shape[0] * r))) dw (new_shape[1] - new_unpad[0]) / 2 dh (new_shape[0] - new_unpad[1]) / 2 if shape[::-1] ! new_unpad: img cv2.resize(img, new_unpad, interpolationcv2.INTER_LINEAR) top, bottom int(round(dh - 0.1)), int(round(dh 0.1)) left, right int(round(dw - 0.1)), int(round(dw 0.1)) img cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, valuecolor) return img注意dw和dh是填充宽度的一半opencv 的copyMakeBorder函数要求上下左右分别给定填充像素数所以边界值要精确计算。很多人复现的代码在这里会有一两个像素的误差测试单张图可能看不出来但batch推理或者跑精度评估时会发现目标框整体偏移。这是我实测踩过的坑。预处理剩下的步骤是归一化和维度调整img letterbox(img, (640, 640)) img img[:, :, ::-1].transpose(2, 0, 1) # BGR - RGB, HWC - CHW img np.ascontiguousarray(img) img img.astype(np.float32) img / 255.0 img np.expand_dims(img, axis0)img[:, :, ::-1]是把 OpenCV 默认的 BGR 顺序转成 RGB因为 YOLOv5 训练时用的是 RGB 输入。这一步很容易被忽略如果你跳过它检测结果虽然也会出来但精度会明显下降尤其是对颜色敏感的场景。4.3 输出张量解析与 NMS 实现要点输入处理好之后推理输出的是一个[1, 25200, 85]的张量。25200 是怎么来的YOLOv5s 有三个检测尺度分别对应 80x80、40x40、20x20 三种特征图每个特征图上的每个格子预测 3 个 anchor所以总数是80*80*3 40*40*3 20*20*3 19200 4800 1200 25200每个预测向量里的 85 个值前 4 个是目标框的中心点坐标和宽高相对于输入图片尺寸的归一化值第 5 个是目标置信度后面 80 个是各类别的分数。解析输出的第一步是先用置信度阈值筛掉低质量的框。我常用的阈值是 0.4 或 0.5。如果检测场景对漏检更敏感可以降到 0.25但代价是后续 NMS 要处理更多候选框速度稍慢。然后用 NMS非极大值抑制去掉重叠的框。YOLOv5 仓库里提供了通用的 NMS 实现如果不想自己写可以直接用 OpenCV 内置的cv2.dnn.NMSBoxesPython 接口调用很简单输入候选框的坐标、每个框的最大类别分数以及置信度阈值和 NMS 阈值返回保留的框索引。这里有一个关键细节OpenVINO 的输出没有像 PyTorch 推理那样内置 NMS 节点也就是说原始输出已经是解码好的坐标。我们需要手动把中心点格式转换成左上角右下角的(x1, y1, x2, y2)格式同时因为 letterbox 在预处理时对原始图片做了缩放和填充推理出的坐标还需要做一个反向映射才能对应到原图上的真实位置。反向映射的公式不复杂。记缩放比例为rdw和dh分别是左右、上下的填充宽度那么原图坐标是x_orig (x_pad - dw) / r y_orig (y_pad - dh) / r这里x_pad是填充后图片上的归一化坐标乘以 640 的解码结果。如果你把这一步写错了检测框会整体偏移看起来像是模型精度出现问题实际只是后处理坐标没还回去。4.4 CPU 性能对比实测数据推理脚本写完以后我用同一张测试图对三种推理方式做了对比原始 PyTorch 模型、ONNX Runtime、OpenVINO IR。测试图的尺寸是 640x640测试机器就是那台普通台式机。为了避免偶然波动我每轮跑 50 次取平均。性能数据如下推理方式平均耗时ms/张备注PyTorch CPU约 285ms未做任何优化ONNX Runtime CPU约 230ms仅使用 CPUExecutionProviderOpenVINO CPU约 95ms使用 IR 模型CPU 线程数 4从数据看OpenVINO 相对 PyTorch 原生推理提速了约 3 倍。这个数字和我之前在其他机器上的测试结果基本一致。需要说明的是这个提速比例并不是固定的它会受 CPU 型号、OpenVINO 版本、输入尺寸的影响。比如在支持 AVX-512 指令集的 CPU 上提速幅度会更大而在较老的 CPU 上可能就只有 1.5 到 2 倍。这个结果验证了整条技术路线的可行性。一个 YOLOv5s 模型从 PyTorch 权重到 OpenVINO IR默认配置下就能在普通 CPU 上做到接近实时的检测速度。如果你的需求是 30 FPS 级别的实时检测把输入尺寸从 640 降到 416速度通常还能再翻一倍左右在 CPU 上实现实时检测是完全可行的。5. 部署中的常见痛点与个人优化心得5.1 Windows 下 OpenVINO 的动态 Shape 问题与 batch 设置如果你在导出 ONNX 时加了--dynamicOpenVINO 加载模型后输入形状会带?符号类似[?, 3, ?, ?]。动态 shape 的好处是推理时可以自由传入不同尺寸的图片坏处是 OpenVINO 在每次输入尺寸变化时都要重新做一次内部内存布局优化。如果你在实际业务中处理的图片尺寸基本固定这点灵活性完全没必要固定的静态 shape 至少能带来 10% 到 20% 的性能提升。我的建议是推理阶段尽量用固定 shape 的模型。如果业务上确实需要支持多种输入尺寸更好的做法是在预处理阶段统一用 letterbox 缩放让输入尺寸保持一致而不是让模型去适配任意尺寸。这样既保留了检测精度又不牺牲推理速度。batch 设置也是一个容易被忽略的点。OpenVINO 的 CPU 插件对单张推理和多张推理的 batch 处理策略不同。你可以用reshape把模型输入从[1, 3, 640, 640]改成[N, 3, 640, 640]来支持多张图并行推理但 CPU 上的并行效率和模型规模、线程数都有关系。对于 YOLOv5s 这种小模型batch1 反而往往是最优选择因为模型本身计算量不大batch 增大带来的并行收益不足以抵消调度开销。5.2 CPU 线程数与推理延迟的权衡OpenVINO 在 CPU 上推理时默认会使用所有可用 CPU 核心。这在服务器上可能是好策略但在办公电脑上会带来一个问题推理过程吃满全部核心导致同一台机器上其他程序卡顿明显。可以通过配置配置来限制推理使用的 CPU 线程数。在加载模型时可以这么做import openvino as ov core ov.Core() model core.read_model(openvino_model/yolov5s.xml) compiled_model core.compile_model(model, CPU, config{NUM_STREAMS: 1, INFERENCE_NUM_THREADS: 4})INFERENCE_NUM_THREADS控制推理线程数NUM_STREAMS控制推理流数。我的经验是当 CPU 总核心数较多时把推理线程数设置为核心数的一半左右推理延迟几乎没有变化但系统整体流畅度会好很多。如果你的程序是纯后台服务不关心其他任务卡顿那可以不用限制线程数让 OpenVINO 自己调度。5.3 后续扩展建议整条链路跑通之后后面可以做的事情还有很多。比如把 OpenVINO 引擎和视频流读取、目标追踪模块结合做一个完整的实时检测程序或者在模型导出阶段加入 NMS 算子让 OpenVINO 输出直接就是过滤后的检测结果省去 Python 端手动解析的开销。我自己在实际项目中还做了输入输出的量化压缩把模型文件从 28MB 左右压缩到了 7MB 左右对嵌入式设备的部署非常有帮助。另外如果你的机器上有 Intel 核显或者独立的 Intel GPUOpenVINO 还能直接调用这些硬件做加速接口和 CPU 推理几乎一样只需要把compile_model的第二个参数从CPU改成GPU即可。我实测过一次 GPU 推理延迟表现很不错但在某些驱动版本上有兼容性问题。如果你的主要部署目标是 Windows 台式机CPU 方案反而是最稳的选择。最后分享一个小经验整条链路的调试顺序很重要。不要急着直接跑 OpenVINO 推理先确认 PyTorch 模型在单张图上检测正常再确认 ONNX 模型用 ONNX Runtime 跑能输出同样的结果最后才上 OpenVINO。每层都验证过出问题时只需要查相邻两步的代码差异排查范围会小得多。我在这一步救了自己不少时间希望也能帮到你。