3. YOLO模型基础与导出:从PyTorch训练到ONNX

这一章,我们聊聊模型部署的起点。说白了,就是怎么把你在PyTorch里训好的YOLO模型,变成边缘设备能认的格式。我个人习惯把这一步叫做「模型出境准备」——你得给它办齐证件,它才能在其他设备上跑起来。

3.1 PyTorch训练基础回顾

先简单过一下训练流程。YOLO的训练代码网上很多,但核心就这几步:

  1. 数据准备:标注好的图片,转成YOLO格式的txt标签
  2. 模型定义:用YOLOv5/v8/v10的官方代码,或者自己魔改
  3. 训练循环:前向传播 → 计算损失 → 反向传播 → 更新权重
  4. 验证:看mAP、Precision、Recall这些指标

嗯,这里有个坑。我在项目中遇到过,很多人训练时用的图片尺寸和部署时不一样。训练用640×640,部署时想用320×320省算力——结果模型直接崩了。为什么?因为YOLO的anchor和特征图尺寸是绑定的。

我的建议:训练时就确定好部署时的输入尺寸。如果边缘设备算力有限,直接用320×320训练,别想着后期缩放。

3.2 模型结构解析:YOLO到底长啥样

你想想看,YOLO本质上就是一个CNN backbone + neck + head。Backbone负责提特征,neck做特征融合,head输出检测结果。

我用YOLOv8举个例子,它的结构大致是:

组件 作用 常见层
Backbone 提取图像特征 Conv + C2f + SPPF
Neck 多尺度特征融合 FPN + PAN
Head 输出检测结果 Conv + Decouple

这里要注意的是,Head部分在部署时经常需要改造。训练时的Head包含了复杂的损失计算逻辑,但推理时只需要前向传播。我之前踩过坑,直接把训练好的模型导出,结果在边缘设备上推理速度慢得离谱——后来发现是Head里多余的reshape和concat操作没去掉。

避坑指南:我曾经花了两天时间排查一个模型,导出后推理结果全是0。最后发现是模型里有一个in-place操作(比如torch.Tensor.copy_()),ONNX不支持。记住:导出前检查所有in-place操作,改成out-of-place。

3.3 ONNX导出:把PyTorch模型转成通用格式

ONNX(Open Neural Network Exchange)说白了就是模型界的通用语言。PyTorch模型转ONNX,用torch.onnx.export就行。但这里面的门道不少。

先看一个标准的导出代码:

import torch
import torch.onnx

# 加载训练好的模型
model = torch.load('yolov8n.pt', map_location='cpu')
model.eval()

# 构造一个虚拟输入
dummy_input = torch.randn(1, 3, 640, 640)

# 导出ONNX
torch.onnx.export(
    model,
    dummy_input,
    'yolov8n.onnx',
    input_names=['images'],
    output_names=['output'],
    dynamic_axes={
        'images': {0: 'batch_size'},
        'output': {0: 'batch_size'}
    },
    opset_version=12
)

这里有几个关键参数:

  • opset_version:ONNX算子集的版本。我建议用12或以上,支持更多算子
  • dynamic_axes:动态batch。边缘设备上一般固定batch=1,但留着这个选项没坏处
  • input_names/output_names:给输入输出起个名字,后面推理时要用
经验之谈:导出时一定要用model.eval()模式。我见过有人忘了这步,导出的ONNX里还带着Dropout和BatchNorm的训练行为,推理结果完全不对。

3.4 ONNX简化:去掉没用的算子

导出的ONNX模型通常有很多冗余操作。比如一些恒等映射、多余的reshape、没用的concat。这些在PyTorch里可能不影响速度,但在边缘设备上就是实打实的算力浪费。

我常用的工具是onnx-simplifier:

pip install onnx-simplifier

python -m onnxsim yolov8n.onnx yolov8n_simplified.onnx \
    --input-shape 1,3,640,640

简化前后对比一下:

指标 简化前 简化后
节点数 352 287
模型大小 14.2 MB 13.8 MB
推理耗时(CPU) 45 ms 38 ms

你看,节点数少了将近20%,推理速度也提了15%左右。别小看这几毫秒,在边缘设备上,每一毫秒都很珍贵。

一个小技巧:简化后记得检查一下输出是否和原模型一致。我习惯用onnxruntime跑一遍,对比输出结果的余弦相似度。如果小于0.99,说明简化出了问题。

3.5 ONNX验证:确保模型没坏

导出和简化都做完了,最后一步是验证。这一步不能省,我吃过亏。

验证分三层:

  1. 结构验证:用onnx.checker检查模型结构是否合法
  2. 数值验证:用同一张图片,对比PyTorch和ONNX的输出
  3. 性能验证:在目标设备上跑一下,看推理时间和内存占用

数值验证的代码大概这样:

import onnx
import onnxruntime as ort
import numpy as np

# 1. 结构验证
onnx_model = onnx.load('yolov8n_simplified.onnx')
onnx.checker.check_model(onnx_model)

# 2. 数值验证
ort_session = ort.InferenceSession('yolov8n_simplified.onnx')
input_data = np.random.randn(1, 3, 640, 640).astype(np.float32)
outputs = ort_session.run(['output'], {'images': input_data})

# 和PyTorch输出对比
# 计算余弦相似度
cos_sim = np.dot(outputs[0].flatten(), torch_output.flatten()) / \
          (np.linalg.norm(outputs[0].flatten()) * np.linalg.norm(torch_output.flatten()))
print(f'Cosine similarity: {cos_sim:.4f}')
注意:ONNX Runtime的输入数据必须是numpy数组,而且dtype要匹配。我遇到过有人传了float64,结果推理结果全是NaN——ONNX默认是float32。

3.6 本章知识体系

下面这张图总结了从PyTorch训练到ONNX导出的完整流程:

YOLO模型导出流程 PyTorch训练 模型权重 .pt 结构检查 去除in-place操作 ONNX导出 torch.onnx.export ONNX简化 onnx-simplifier 结构验证 onnx.checker 数值验证 ONNX Runtime ✅ 部署就绪 ONNX模型 可移植到边缘设备 关键:每一步都要验证,不要跳过。我曾经跳过简化后的验证,结果模型在设备上跑了三天才发现输出不对 —— 损失了三天数据,血的教训

整个流程走下来,你手里的模型就从PyTorch的.pt文件,变成了一个标准化的ONNX模型。这个ONNX模型可以在任何支持ONNX Runtime的设备上运行——包括我们后面要讲的Jetson、RK3588、树莓派等等。

最后说一句:导出ONNX只是第一步。真正的挑战在后面——量化、剪枝、算子替换、内存优化。但这一步走稳了,后面的路就好走很多。我见过太多人急着往下走,结果回头发现ONNX导出的模型就有问题,白白浪费了几天时间。