【深度学习实践】深入浅出ONNX模型详解

本文将提供一个非常详细的 ONNX 介绍,包括它的基本概念、主要特性、模型转换、生态系统、支持的框架以及优化和量化技术。此外,会提供 Python 和 C++ 代码示例,既包括通用的推理代码,也涵盖特殊模型(如 ResNet、YOLO、BERT 等)的调用方式。

ONNX(Open Neural Network Exchange)详细介绍

1. ONNX 的基本概念和背景

ONNX(Open Neural Network Exchange)是一个开放源代码的深度学习模型交换格式和生态系统。它由 Facebook 和 Microsoft 于 2017 年共同发布,旨在作为不同深度学习框架之间的通用模型表示标准 (Open Neural Network Exchange - Wikipedia)。ONNX 提供了一种开放标准,用于表示机器学习模型和神经网络算法,方便模型在不同的框架和工具之间迁移 (Open Neural Network Exchange - Wikipedia)。简单来说,ONNX 就像是一种通用语言或中间表示(IR),可以描述深度学习模型的计算图和权重,使得各个框架训练得到的模型可以互相转换和共享 (Open Neural Network Exchange (ONNX) Explained | Splunk)。ONNX 项目最初代号为 “Toffee”,由 Facebook 的 PyTorch 团队开发,后来在 2017 年 9 月正式更名为 ONNX,并得到多家公司的支持(如 IBM、华为、英特尔、AMD、Arm、Qualcomm 等) (Open Neural Network Exchange - Wikipedia)。2019 年 ONNX 正式加入 Linux 基金会,成为其 AI 项目的毕业级项目之一。这些背景体现了 ONNX 社区的开放性和行业广泛支持。

2. ONNX 的主要特性

ONNX 作为机器学习模型的开放标准格式,具备以下主要特性:

  • 框架互操作性:ONNX 最核心的特性就是框架无关性和互操作性。模型可以在一个框架(如 PyTorch、TensorFlow)中训练,然后导出为 ONNX 格式,在另一个框架或推理引擎中加载和运行。这允许开发者在不同工具间自由切换,而不被某单一生态锁定 (Open Neural Network Exchange (ONNX) Explained | Splunk) (Announcing ONNX Support for Apache MXNet | AWS Machine Learning Blog)。例如,你可以用 PyTorch 方便地构建和训练模型,再用 ONNX 将模型部署到移动设备的推理引擎中执行。

  • 可移植性与平台无关:ONNX 模型是通过 protobuf 序列化的 .onnx 文件,可以在各种硬件和操作系统上运行。借助 ONNX,模型可以方便地部署到云服务、服务器、边缘设备甚至移动应用中 (Open Neural Network Exchange (ONNX) Explained | Splunk)。这种通用格式减少了因部署环境差异而重写模型的需求。

  • 计算图表示:ONNX 定义了一种可扩展的计算图模型来表示神经网络。一个 ONNX 模型由有向无环图(DAG)的节点组成,每个节点表示一个操作符(Operator),节点之间的有向边表示张量数据的流动 (Open Neural Network Exchange - Wikipedia)。计算图中包含模型的网络结构(层与层的连接关系)、每个节点的输入输出张量名和类型,以及模型的参数权重(以张量常量形式存储)。这种图结构使模型的结构清晰且易于优化。

  • 标准算子和数据类型:ONNX 内置了一套标准的算子(Operators)和数据类型库。各个支持 ONNX 的框架都会实现这些标准算子,以确保一致的计算结果 (Open Neural Network Exchange - Wikipedia)。常见的深度学习算子(如卷积、池化、全连接、激活函数等)都在 ONNX 的算子库中定义,并有版本(opset)管理以支持新特性扩展。这保证了模型在不同框架上执行的一致性 (Open Neural Network Exchange (ONNX) Explained | Splunk)。ONNX 也定义了常用的张量数据类型(如 float32、int64、BOOL 等),使模型交换时数据格式统一。

  • 扩展性:ONNX 设计时考虑了扩展性,新版本可以引入新的算子集(OpSet)而不破坏向后兼容。对于非常新的或自定义的算子,ONNX 支持定义自定义算子域以扩展功能。此外,ONNX 不仅支持深度神经网络,也支持传统机器学习模型(称为 ONNX-ML),包括决策树、随机森林、支持向量机等,使其成为通用的机器学习模型交换格式。

  • 专注推理(Inference):ONNX 格式主要侧重于模型的推理阶段表示。它涵盖了模型前向传播所需的一切(计算图、运算、权重),但不直接包含训练过程中反向传播的信息(目前也有用于训练的扩展,但不常用)。这种专注使 ONNX 模型更简洁高效,非常适合部署和推理。换言之,ONNX 提供的计算图定义和算子主要聚焦于推理/评估用途 (Open Neural Network Exchange - Wikipedia)。

  • 性能优化友好:由于采用统一的计算图和算子标准,各种底层优化技术(如算子融合、内存复用等)可以直接针对 ONNX 模型进行。这为后续的推理引擎优化打下基础。许多硬件厂商也支持直接对 ONNX 模型进行加速优化,这意味着针对 ONNX 的一次优化能同时提升多个框架的性能 (Open Neural Network Exchange - Wikipedia)。在 ONNX 生态中,我们也可以方便地对模型进行图优化和量化(后续介绍),以进一步提高推理速度和减小模型大小。

3. ONNX 如何在深度学习模型之间进行转换

ONNX 的出现大大简化了不同深度学习框架之间模型转换的工作。过去如果想将模型从一个框架转换到另一个,往往需要耗费数周甚至数月编写转换脚本或重新实现模型 (theano - Is there a common format for neural networks - Stack Overflow)。而借助 ONNX 标准,模型转换可以更简单和快速地完成 (theano - Is there a common format for neural networks - Stack Overflow)。

典型的转换流程是:在源框架中将模型导出为 ONNX 格式,然后在目标环境中加载该 ONNX 模型进行推理。许多主流框架都提供了导出或转换工具,例如:

  • PyTorch 中,可以使用内置的 torch.onnx.export 接口将 PyTorch 模型导出为 ONNX 文件。
  • 对于 TensorFlow/Keras,有第三方工具如 tf2onnxkeras2onnx,用于将 TensorFlow 的计算图或 Keras 保存模型转换为 ONNX。
  • MXNet 提供了 mx2onnxonnx2mx 工具,可实现 MXNet 和 ONNX 之间的双向模型转换。
  • 还有像 Microsoft CNTK(Cognitive Toolkit)、Caffe2 等框架,本身对 ONNX 就有内建支持,可以直接导出或导入 ONNX 模型。

转换时,转换器会把源模型的计算图结构和权重映射为等效的 ONNX 计算图和张量。如果源模型中使用的算子属于 ONNX 标准算子集,那么转换通常是无损的。一旦模型被转换为 ONNX 格式,就相当于拥有了一个框架中立的模型表示;这个 ONNX 模型可以在任何支持 ONNX 的引擎或库中载入运行,或者再转换到其他框架中去 (Deploying models converted to ONNX format)。例如,可以在 PyTorch 中训练好模型->导出ONNX->然后在 TensorFlow (通过 ONNX 导入工具) 或直接在 ONNX Runtime 中推理。

需要注意,转换的完整性取决于框架对算子的支持程度。如果模型使用了源框架中特有而 ONNX 尚未支持的算子,转换器可能会失败或需要额外处理(比如自定义算子)。但随着 ONNX 不断扩充算子集,以及社区提供的各种转换工具(包括针对 sklearn、XGBoost、LightGBM 等的转换器),大部分常见模型都能顺利在框架间迁移。

总的来说,ONNX 提供了一个统一桥梁,极大降低了深度学习模型跨框架转换的门槛,实现了一次训练、多处部署的灵活性 (Open Neural Network Exchange (ONNX) Explained | Splunk)。

4. ONNX 的生态系统(如 ONNX Runtime)

围绕 ONNX 标准,已经形成了一个丰富的生态系统,包括推理引擎、模型库和开发工具等:

  • ONNX Runtime:这是由微软主导开发的高性能跨平台推理引擎。ONNX Runtime 可以加载 ONNX 模型并在多种硬件上高效运行 (Deploying models converted to ONNX format)。它针对推理进行了大量优化(比如图优化、算子融合等),并提供了多种执行后端 (Execution Providers),以利用不同硬件的加速能力。例如,ONNX Runtime 支持CPU执行(默认)、GPU执行(CUDA/cuDNN)、OpenVINO(英特尔加速)、TensorRT(英伟达TensorRT加速)、NNAPI(移动端加速)等。通过选择合适的执行提供器,ONNX Runtime 能充分利用硬件特性,实现高效的模型推理 (Deploying models converted to ONNX format)。ONNX Runtime 提供了多语言的API(包括 Python、C++、C#、Java、JavaScript 等),方便在不同开发环境中使用。同一个 ONNX 模型文件,可以在服务器(x86 CPU/GPU)、移动设备(ARM CPU/GPU)、浏览器(WebAssembly/WebGPU)等各种平台上通过 ONNX Runtime 来运行,做到“一次训练,随处部署”。

  • ONNX Model Zoo:ONNX 模型动物园是社区提供的预训练模型集合 (onnx/models: A collection of pre-trained, state-of-the-art ... - GitHub)。里面包含许多常用的深度学习模型(例如 ResNet50、BERT、YOLO 等)已经转换好的 ONNX 文件,开发者可以直接下载使用。模型动物园的存在方便了快速试用和部署 ONNX 模型,并展示了 ONNX 在不同任务上的应用范例。

  • 转换和优化工具:在 ONNX 生态下,有许多工具帮助创建、转换和优化模型。例如:

    • onnx库:官方的 ONNX Python 库,可用于程序matically 构建或操作 ONNX 模型,检查模型格式有效性等。
    • 转换器:如前述的 tf2onnx, keras2onnx, skl2onnx(Scikit-Learn 转 ONNX), onnxmltools 等,将不同框架的模型导出为 ONNX。
    • ONNX Optimizer:有一些库可以对 ONNX 模型进行简化或优化,比如 onnx-simplifier(简化计算图)、polish 工具等,或者直接使用 ONNX Runtime 提供的优化 API。
    • 量化工具:ONNX Runtime 提供了量化工具,可以将浮点模型转换为低比特模型(如 int8),以提高推理速度和减小模型大小(稍后详述)。
  • 社区与支持:ONNX 在 2017 年推出后,迅速得到各大厂商和开源社区的支持,形成了一个开放合作的生态 (Announcing ONNX Support for Apache MXNet | AWS Machine Learning Blog)。Linux 基金会托管了 ONNX 项目,确保其中立性和开放性。众多硬件厂商(Intel、NVIDIA、ARM、Qualcomm、AWS 等)都参与 ONNX,在各自平台上支持 ONNX 模型的加速。比如 NVIDIA 的 TensorRT 可以直接加载 ONNX 模型做高性能推理;Intel 的OpenVINO支持将 ONNX 模型部署在CPU/FPGA/VPU上;安卓平台的NNAPI也逐步支持ONNX模型格式等。这些共同努力让 ONNX 成为了事实上的跨框架、跨平台模型格式标准。

综上,ONNX 不只是一个文件格式,而是围绕模型标准化所构建的一个完整生态。开发者可以利用 ONNX,从模型开发、格式转换到最终部署,一路都有配套的工具和运行时支持,大大简化机器学习项目的流程。

5. ONNX 支持的框架

ONNX 作为通用的模型表示格式,被众多深度学习框架和机器学习库所支持。以下是一些主要支持 ONNX 的框架和工具:

  • PyTorch:PyTorch 对 ONNX 有原生支持,提供了 torch.onnx.export 接口将模型导出为 ONNX 格式。许多 PyTorch 模型可以轻松地通过该接口转换,以便在其他环境中推理。例如,PyTorch 与 Caffe2 早期就通过 ONNX 打通了模型转换的通道(在 PyTorch 训练、Caffe2 部署)。如今 PyTorch 仍是 ONNX 最常用的来源框架之一。

  • TensorFlow / Keras:虽然 Google 的 TensorFlow 并没有官方直接集成 ONNX 导出,但开源社区提供了 tf2onnx 工具,可以将 TensorFlow 1.x 的计算图或 TensorFlow 2.x/Keras 的 SavedModel 转换为 ONNX。另外,keras2onnx 也用于将 Keras 模型导出为 ONNX。目前很多 TensorFlow 训练的模型(尤其是通过 Keras)都能借助这些工具生成 ONNX 文件,以在其他推理引擎(如 ONNX Runtime)中运行。

  • MXNet:Apache MXNet 早期就加入了 ONNX 支持 (Announcing ONNX Support for Apache MXNet | AWS Machine Learning Blog)。AWS 于 2017 年发布了 onnx-mxnet 工具包,可将 ONNX 模型导入 MXNet,利用 MXNet 引擎推理 (Announcing ONNX Support for Apache MXNet | AWS Machine Learning Blog)。MXNet 也支持将模型导出为 ONNX(mx2onnx)。因此,可实现 PyTorch/Caffe2/CNTK 等训练 -> ONNX -> MXNet 推理的流程 (Announcing ONNX Support for Apache MXNet | AWS Machine Learning Blog)。

  • Caffe2:Caffe2(Facebook 开源的推理框架)与 PyTorch 一起推动了 ONNX 的诞生。Caffe2 可以直接加载 ONNX 模型,这也是当初 Facebook 实现训练部署分离(PyTorch训练,Caffe2部署)的关键。后来 Caffe2 已并入 PyTorch 项目,但 ONNX 对于 PyTorch/Caffe2生态的作用依然显著。

  • Microsoft CNTK:微软认知工具包(CNTK)在 ONNX 发布时就宣称支持 ONNX,能够导出模型为 ONNX,或加载 ONNX 模型。尽管 CNTK 已停止更新,但作为历史框架,它对 ONNX 的支持推动了标准早期的发展 (What every ML/AI developer should know about ONNX | DigitalOcean)。

  • 其他深度学习框架:诸如 ChainerMicrosoft ML.NETApache SingaMindSporePaddlePaddle 等框架或多或少都提供了 ONNX 支持或转换工具,使模型能够在 ONNX 生态中流通。

  • 传统机器学习库:ONNX-ML 扩展使得像 scikit-learnXGBoostLightGBMCatBoost 等非深度学习模型也可以转换为 ONNX 格式进行部署 (Deploying models converted to ONNX format)。例如,Microsoft 提供的 skl2onnx 可将 scikit-learn 的模型(如Pipeline、SVM、RandomForest等)序列化为 ONNX。这意味着 ONNX 不仅联通了深度学习框架,也涵盖许多经典机器学习工作流。

  • 跨平台转换:除了框架本身支持,很多第三方项目专注于模型转换,将各种格式互相转为 ONNX。例如,Apple 的 CoreML 提供了与 ONNX 之间的双向转换工具(CoreML ⇄ ONNX),Open Neural Network Exchange Format (Khronos NNEF) 也可以和 ONNX 转换等等。可以说,目前业界大多数训练框架都能以某种方式导出 ONNX 模型 (What every ML/AI developer should know about ONNX | DigitalOcean)。

综上,ONNX 已获得广泛的框架支持。当前原生支持 ONNX 导出的包括 PyTorch、MXNet、Caffe2、CNTK 等,通过转换器支持的有 TensorFlow、Keras、CoreML 等 (What every ML/AI developer should know about ONNX | DigitalOcean)。无论是 Python 生态还是其他语言环境(如 R 的 H2O、Julia 的 Flux 等),都能找到相应的途径将模型融入 ONNX 格式。这种广泛支持使 ONNX 成为事实上的“模型通用格式”,极大地方便了跨框架的模型部署和交流。

6. ONNX 的优化和量化技术

ONNX 优化量化是ONNX生态中用于提升模型推理性能、减小模型体积的重要技术手段:

  • 计算图优化(Graph Optimizations):ONNX 模型在推理前可以通过一系列图优化来简化和加速计算。ONNX Runtime 就提供了多级别的图优化机制 (Graph optimizations | onnxruntime):

    • 基本优化:移除冗余节点、常量折叠等。这类优化在不改变模型功能的前提下,删除计算图中多余的计算。例如,常量折叠会静态计算图中与常量相关的子计算,将结果直接写入模型,从而减少运行时的计算 (Graph optimizations | onnxruntime)。又如去除恒等运算、合并连续的reshape等。
    • 扩展优化:更复杂的算子融合和内存布局调整等。这些通常发生在针对特定硬件的子图上,例如将一组操作融合为一个高效内核。比如卷积层和后面的批归一化、ReLU可以融合成一个算子执行,以减少内存读写和计算开销 (Graph optimizations | onnxruntime)。再如,将多个小矩阵乘法加和融合成大的GEMM等。
    • 图优化可以在模型加载时在线进行(加载模型后即时优化),也可以离线进行(事先将优化后的模型保存以加快每次启动)。经过优化的 ONNX 模型通常计算图更紧凑、执行效率更高 (Graph optimizations | onnxruntime)。这些优化由 ONNX Runtime 自动完成,开发者也可以使用 onnxoptimizer 等工具手动优化模型。
  • 算子级别优化:除了结构性的图优化,不同硬件的执行提供器也会对某些ONNX算子进行特定优化实现。例如,GPU上用高度并行的CUDA核函数实现Conv算子,CPU上利用向量化指令优化MatMul等。这些优化属于底层实现,但因为ONNX算子标准化,硬件厂商只需针对ONNX算子优化即可适配所有使用该算子的模型(这也是 ONNX “一次优化,多处适用”的理念体现 (Open Neural Network Exchange - Wikipedia))。

  • 模型量化(Quantization):量化是将模型从高精度(如FP32)转换为低精度(如INT8、UINT8)的过程,以换取更快的推理速度和更小的模型尺寸。ONNX Runtime 提供了成熟的量化工具和API,可对 ONNX 模型进行动态或静态量化 (Quantize ONNX models | onnxruntime):

    • 动态量化:在动态量化中,模型的权重通常量化为 INT8,而激活值(中间计算结果)在推理时动态计算量化参数(scale和zero-point)并量化 (Quantize ONNX models | onnxruntime)。也就是说,不需要预先用校准数据确定激活的量化范围,而是在每次推理时根据当前批次数据动态调整。这会带来一些运行时开销(需要实时计算量化参数),但通常能取得比静态量化更高的精度,因为保留了对每批输入动态调整的灵活性 (Quantize ONNX models | onnxruntime)。动态量化使用方便,不需要校准过程。典型应用是对 NLP 模型(如 Transformer、BERT)进行动态量化,它们的全连接权重很多,量化后大幅减少矩阵乘法开销,同时保持良好的精度。
    • 静态量化:静态量化需要在量化前进行校准(calibration)。开发者需要提供一组代表性输入数据跑过模型,收集各层激活值的分布范围 (Quantize ONNX models | onnxruntime)。然后选择合适的算法(如MinMax、KL散度等)确定每个张量的量化缩放因子和零点,并将这些量化参数嵌入到模型中 (Quantize ONNX models | onnxruntime)。之后模型的推理就使用固定的量化参数,将权重和激活统一用INT8(或者部分用INT8)计算。静态量化通常能带来最大的性能提升(因为整个推理过程中都是低精度计算,无需动态计算量化系数),但相对可能引入更多精度损失,尤其如果校准数据不足以代表实际数据分布。静态量化常用于卷积神经网络(CNN)等在图像领域的模型,在CPU上能显著加速推理。
    • 量化感知训练 (QAT):除上述事后量化外,有些框架支持在训练过程中加入量化仿真(即量化感知训练),直接训练出对低精度鲁棒的模型,再转换为量化后的 ONNX 模型。这种方法往往精度损失最小,但实现和训练成本较高。ONNX 模型可以表示通过 QAT 得到的量化算子(ONNX 有 QuantizeLinear/DequantizeLinear 等节点以及 QDQ 格式支持)。

    ONNX Runtime 的量化工具支持上述动态和静态量化流程,并提供API如 quantize_dynamic()quantize_static() 一键量化模型 (Quantize ONNX models | onnxruntime) (Quantize ONNX models | onnxruntime)。经过量化的模型会在计算图中插入量化/反量化节点或者直接将算子替换为量化算子,使模型以 8 位整数进行计算。量化模型通常大小缩减(约为原1/4,如果从FP32到INT8)、CPU上推理延迟降低(利用向量化的INT8指令),非常适合对延迟敏感或资源受限的部署场景。

  • 混合精度:除了整型量化,ONNX 也支持使用 FP16(半精度浮点)或 BF16 等混合精度来权衡性能和精度。在部分算子上降为 FP16,可以减小显存占用并提高吞吐,ONNX Runtime 也有相应的模型转 FP16 工具。混合精度通常用于 GPU 上加速,TensorRT 等执行提供器对 FP16 支持很好。

总之,ONNX 不仅提供模型交换格式,本身也在模型高效推理方面提供了诸多支持。通过图优化,我们可以精简模型计算图、消除冗余;通过量化和混合精度,我们可以压缩模型和加速推理,同时尽量保证精度可接受。这些优化技术通常可以叠加使用,例如先进行图优化,再执行量化,以获得更好的综合效果。ONNX 及其运行时工具链让开发者能够方便地应用这些优化手段,将模型部署推理的性能发挥到更优。


接下来,通过 Python 和 C++ 代码示例,展示如何使用 ONNX 进行模型推理,包括通用的加载运行方法,以及针对特定模型(如 ResNet、YOLO、BERT)的推理过程。代码中包含详细的注释和解析,便于理解。

ONNX 模型推理的 Python 和 C++ 代码示例

以下将分别提供 Python 和 C++ 环境下使用 ONNX 模型进行推理的示例代码。

Python 推理示例

Python 拥有丰富的库支持来加载和运行 ONNX 模型,最常用的是 ONNX Runtime 的 Python API。假设我们已经安装了 onnxruntime 库 (pip install onnxruntime),下面分别给出通用的推理代码,以及 ResNet(图像分类)、YOLO(目标检测)、BERT(NLP)的具体示例。

通用 ONNX 模型推理代码 (Python)

下面的代码演示如何在 Python 中加载一个 ONNX 模型并进行一次前向推理。此代码不针对特定模型,而是展示通用步骤。

import onnxruntime as ort
import numpy as np

# 1. 创建 ONNX Runtime 推理会话,加载模型
#   使用 InferenceSession 类加载 .onnx 模型文件。
#   这里假设模型文件名为 'model.onnx',需要在同目录下已有该文件。
session = ort.InferenceSession("model.onnx")

# 2. 准备输入数据
#   获取模型的第一项输入的名称和形状,以便构造模拟输入。
#   通常我们需要知道模型期望的输入张量形状和数据类型。
input_name = session.get_inputs()[0].name        # 输入节点名称
input_shape = session.get_inputs()[0].shape      # 输入张量形状,例如 [1, 3, 224, 224]
input_dtype = session.get_inputs()[0].type       # 输入数据类型,例如 'tensor(float)' 表示 float32

# 构造一个与模型输入形状匹配的假输入数据,这里用随机数填充。
# 注意:实际应用中应使用真实的数据,并进行必要的预处理。
dummy_input = np.random.random(size=tuple(input_shape)).astype(np.float32)

# 3. 运行模型推理
#   使用 session.run() 方法执行模型推理。
#   第一个参数设置输出层名列表,这里传入 None 表示获取模型的所有输出。
#   第二个参数是一个字典,将输入名映射到具体的数据。
outputs = session.run(None, {input_name: dummy_input})

# 4. 获取输出结果
#   outputs 将是一个列表,包含模型的各个输出张量数据(numpy 数组)。
#   根据模型类型,可能有一个或多个输出。这里简单打印输出的形状。
for i, output in enumerate(outputs):
    print(f"Output {i} shape: {output.shape}")

代码解析:以上代码首先使用 ort.InferenceSession 加载 ONNX 模型文件,然后随机生成了一个与模型输入匹配的 numpy 数组作为输入。调用 session.run 执行推理后,即可得到模型的输出结果列表。在实际使用中,我们会用真实数据替换 dummy_input,并根据需要处理 outputs(例如取概率最大的位置作为预测类别等)。但无论模型种类如何,加载模型、准备输入、运行推理、获取输出这几个步骤是通用的。

ResNet 模型推理示例 (Python)

下面以经典的 ResNet-50 图像分类模型为例,演示如何进行推理。ResNet-50 的 ONNX 模型通常接受形状为 [1, 3, 224, 224] 的输入图像张量(批大小1,3通道RGB,224x224分辨率)。输入像素一般需要标准化处理。模型输出是一个 shape 为 [1, 1000] 的向量,对应于对1000个类别(ImageNet)的预测概率分布。

import onnxruntime as ort
from PIL import Image
import numpy as np

# 加载 ResNet ONNX 模型(文件名假设为 resnet50.onnx)
session = ort.InferenceSession("resnet50.onnx")

# 打开一张测试图片并预处理
image = Image.open("test.jpg")  # 假设当前目录有一张名为 test.jpg 的图片
# ResNet 模型要求输入224x224,这里进行缩放和中心裁剪(简单缩放示例)
image = image.resize((224, 224))
# 将图像转换为 numpy 数组并调整维度
img_data = np.array(image).astype(np.float32)     # (224, 224, 3) HWC格式
# 如果图像是RGB通道,我们需要将其转置为 CHW 格式,即 (3, 224, 224)
img_data = np.transpose(img_data, (2, 0, 1))      # 现在 img_data.shape == (3, 224, 224)
# 增加batch维度至 (1, 3, 224, 224)
input_tensor = np.expand_dims(img_data, axis=0)   # shape: (1, 3, 224, 224)
# 图像像素通常需要归一化到模型训练时的分布,典型操作是除以255并减均值除以标准差等。
# 这里简单将像素值缩放到0~1范围:
input_tensor = input_tensor / 255.0

# 获取模型输入的名称
input_name = session.get_inputs()[0].name
# 运行模型推理
outputs = session.run(None, {input_name: input_tensor})
# ResNet50只有一个输出,即对1000个类别的预测值
probabilities = outputs[0]  # shape (1, 1000)
# 找出概率最大的类别索引
pred_class_idx = np.argmax(probabilities, axis=1)[0]  # 取 batch 中第一个样本的结果
print(f"Predicted class index: {pred_class_idx}")

代码解析:此示例中,我们使用 PIL 库加载并处理图像:调整大小到224x224,转换为numpy数组并变换维度以符合模型输入的 [batch, channel, height, width] 格式。然后将像素值归一化到 [0,1](实际使用ResNet通常进一步减去均值/除以方差,但此处为简单起见未详细处理)。接着,将处理后的图像作为输入运行 session.run。得到的输出是一个 shape 为 [1,1000] 的数组,我们使用 np.argmax 找到概率最高的类别索引。这个索引对应ImageNet 1000类中的一种,我们可以据此推断模型认为图片属于哪一类(如果有类别标签列表,可以将索引转换为人类可读的类名)。通过这些步骤,我们成功对一张图片进行了分类推理。

YOLO 模型推理示例 (Python)

接下来展示目标检测模型的推理,以 YOLO 系列模型为例(比如 YOLOv5)。YOLO 模型的 ONNX 通常输出检测框和类别的列表,需要后处理将之转化为人类可理解的边界框坐标及类别名称。这里简化示例:读取图像,获得模型输出后,对输出张量进行阈值过滤,打印出检测到的目标类别和框。

import onnxruntime as ort
from PIL import Image
import numpy as np

# 加载 YOLO ONNX 模型(假设为 yolov5s.onnx)
session = ort.InferenceSession("yolov5s.onnx")

# 读取并预处理输入图像
image = Image.open("street.jpg")  # 一张街景测试图像
# YOLOv5 模型要求 640x640 的输入,并通常要做 letterbox 填充。
# 简化处理:直接缩放为 640x640(实际应用中应保持宽高比并补边)。
image = image.resize((640, 640))
img_data = np.array(image).astype(np.float32)  # (640, 640, 3)
img_data = np.transpose(img_data, (2, 0, 1))   # (3, 640, 640)
img_data = np.expand_dims(img_data, axis=0)    # (1, 3, 640, 640)
# 像素归一化到0~1(YOLOv5 通常还会减均值,但此处简单处理)
input_tensor = img_data / 255.0

# 推理
input_name = session.get_inputs()[0].name
outputs = session.run(None, {input_name: input_tensor})
# 假设 YOLO 模型有一个输出 (也可能有多个输出,看具体模型)
output = outputs[0]  # 取得输出张量,shape 可能为 (1, N, 85),其中N为检测数,85包含bbox和类别等
# YOLOv5的输出格式: 每个检测向量有 [x, y, w, h, conf, class1_conf, class2_conf, ..., classN_conf]
# 其中 x,y 为框中心坐标,w,h 为宽高,conf 为目标置信度,各 class_conf 为该类别的置信度
# 下面我们根据输出进行简单的后处理:筛选高置信度的检测框
detections = output[0]  # 去掉 batch 维度,得到 (N, 85)
conf_threshold = 0.5    # 置信度阈值
for det in detections:
    obj_conf = det[4]
    if obj_conf < conf_threshold:
        continue  # 忽略低置信度目标
    class_scores = det[5:]
    class_id = np.argmax(class_scores)
    class_score = class_scores[class_id]
    if class_score * obj_conf < 0.5:
        # 如果期望结合obj_conf筛选,这里用乘积做进一步阈值判断
        continue
    # 提取边界框参数(YOLO输出的 x,y,w,h 通常是相对于模型输入尺寸的归一化或绝对值)
    x_center, y_center, width, height = det[0], det[1], det[2], det[3]
    # 将中心坐标和宽高转换为左上角坐标 (xmin, ymin) 和右下角坐标 (xmax, ymax)
    xmin = x_center - width/2
    ymin = y_center - height/2
    xmax = x_center + width/2
    ymax = y_center + height/2
    print(f"Detected object class {int(class_id)} with confidence {float(class_score*obj_conf):.2f} at [{xmin:.1f}, {ymin:.1f}, {xmax:.1f}, {ymax:.1f}]")

代码解析:此示例对 YOLO 模型的输出进行了解析。在推理部分,我们将输入图像缩放为模型所需的分辨率,并进行归一化。YOLO 模型输出通常是一个二维张量,每一行对应一个候选检测结果,包括边界框位置和各类别分数。代码中我们遍历每个检测行,先根据目标置信度(objectness confidence)过滤掉不可靠的检测,然后找出最大类别分数对应的类别ID,并结合该类别的分数进一步筛选。接着将给出的中心坐标和宽高转换成矩形框的边界坐标,最后打印检测结果(类别ID及置信度,还有框的坐标)。实际应用中,我们可以将类别ID映射到具体的标签名称(例如 0 映射为 "person" 等),并在原图上绘制边界框。需要注意的是,YOLO 的输出解释和后处理因版本而异,如 YOLOv5以上包含嵌入的 NMS,这里演示的是通用的后处理思路。

BERT 模型推理示例 (Python)

最后,以 BERT 为代表的Transformer模型演示 ONNX 在自然语言处理(NLP)任务中的推理。以一个句子分类的 BERT 模型为例(比如用于情感分析的BERT),该模型通常有多个输入(input_ids, attention_mask, 有时还有 token_type_ids),输出可能是分类 logits。下面代码展示如何准备文本输入并运行 BERT 模型的 ONNX 推理。

import onnxruntime as ort
import numpy as np

# 加载 BERT ONNX 模型(假设为 bert_classifier.onnx)
session = ort.InferenceSession("bert_classifier.onnx")

# 模拟一个输入句子。例如: "I love this movie."
# 通常需要经过分词器(tokenizer)将文本转换为 input_ids 等张量,这里直接给出示例ID序列:
input_ids = np.array([[101,  1045, 2293, 2023, 3185, 1012,  102]])  # [CLS] I love this movie . [SEP]
# 上述数字是假设的 WordPiece token ID,如101='[CLS]', 102='[SEP]', 1045='I', 2293='love', 2023='this', 3185='movie', 1012='.'
# 注意实际ID需根据所用的分词器词表。这里主要演示格式。
# 构造 attention_mask,对于实际序列长的token标记为1,padding部分为0。此例中无padding:
attention_mask = np.array([[1, 1, 1, 1, 1, 1, 1]])
# 若模型需要 token_type_ids(区分句对的0/1标记),此处假设只有单句,提供全0
token_type_ids = np.array([[0, 0, 0, 0, 0, 0, 0]])

# 将准备的输入字典提供给会话运行。假设模型有三个输入分别对应以下名字:
inputs = {
    "input_ids": input_ids.astype(np.int64),
    "attention_mask": attention_mask.astype(np.int64),
    "token_type_ids": token_type_ids.astype(np.int64)
}
# 如果模型只需要input_ids和attention_mask两个输入,可只传两个。

# 执行推理
outputs = session.run(None, inputs)
# BERT 分类模型通常输出 logits,shape 为 [batch_size, num_classes]
logits = outputs[0]  # 取得第一个输出
# 根据 logits 计算预测类别
predicted_class_id = int(np.argmax(logits, axis=1)[0])
print(f"Predicted class: {predicted_class_id}")

代码解析:BERT 等Transformer模型的 ONNX 推理需要准备好正确格式的张量输入。一般通过 Hugging Face Transformers 等库的 tokenizer 将文本转为 token序列(input_ids)、attention mask 等。本示例直接使用了一串预先假定的 token ID 来说明格式。我们创建了 input_ids(包含 [CLS] 和 [SEP] 标记的ID序列)、对应的 attention_mask(同长度,全1表示这些位置有内容)以及 token_type_ids(这里全0表示句子一的标记,因只有单句)。然后将这些作为字典传入 session.run。模型输出 logits,通过对每行取 argmax 得到预测的类别索引。在情感分析例子中,模型可能有两个输出类别(积极/消极),则 predicted_class_id 为0或1。总的来说,使用 ONNX Runtime 进行BERT推理的流程与其他模型类似,不同在于需要提供多个输入张量;我们通过 Python 字典传入多个命名输入即可。推理得到的输出同样以 numpy 数组形式给出。

C++ 推理示例

在 C++ 环境中,可以使用 ONNX Runtime C++ API 来加载和运行 ONNX 模型。首先需要在项目中集成 ONNX Runtime(例如链接 onnxruntime 静态库并包含头文件)。ONNX Runtime 提供了一套 C++ 封装的 API(位于头文件 onnxruntime_cxx_api.h 中),使用 RAII 风格管理会话和张量。下面用 C++ 实现一个通用的推理代码示例,并附注释说明关键步骤。

#include <onnxruntime_cxx_api.h>
#include <vector>
#include <iostream>

int main() {
    // 1. 创建 ONNX Runtime 环境和会话
    Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "ONNXRun");  // 初始化ORT环境,带有日志等级和日志名
    Ort::SessionOptions session_options;
    session_options.SetIntraOpNumThreads(1);  // 设置并行线程数,视需要配置
    // 如果需要使用GPU, 可调用 session_options.AppendExecutionProvider_CUDA(0) 等
    
    // 加载 ONNX 模型文件,创建会话 (假设模型文件名为 "model.onnx")
    const char* model_path = "model.onnx";
    Ort::Session session(env, model_path, session_options);
    std::cout << "Model loaded successfully!\n";

    // 2. 读取模型输入输出信息
    Ort::AllocatorWithDefaultOptions allocator;
    // 获取第一个输入节点的名字、类型和维度
    char* input_name = session.GetInputName(0, allocator);
    Ort::TypeInfo input_type_info = session.GetInputTypeInfo(0);
    auto tensor_info = input_type_info.GetTensorTypeAndShapeInfo();
    ONNXTensorElementDataType type = tensor_info.GetElementType();
    std::vector<int64_t> input_shape = tensor_info.GetShape();
    std::cout << "Input name: " << input_name << "\n";
    std::cout << "Input type: " << type << "\n";  // ONNXTensorElementDataType 枚举,例如浮点为 ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT
    std::cout << "Input shape: [";
    for (size_t i = 0; i < input_shape.size(); ++i) {
        std::cout << input_shape[i] << (i < input_shape.size()-1 ? ", " : "");
    }
    std::cout << "]\n";

    // 准备一个与模型输入形状相同的输入数据,这里使用全零初始化
    // 假设输入shape为 [1,3,224,224],即batch=1、3通道、224x224
    if (input_shape.size() == 4) {
        size_t total_tensor_size = 1;
        for (int64_t dim : input_shape) {
            // 对于动态维度(-1),这里简化处理为用1替代
            // 实际应用中若出现动态维度,应根据具体数据调整
            if (dim <= 0) dim = 1;
            total_tensor_size *= dim;
        }
        std::vector<float> input_data(total_tensor_size);
        // 这里未特别赋值,默认初始化为0.0,可根据需要填充或读取真实数据
        std::fill(input_data.begin(), input_data.end(), 0.0f);

        // 3. 创建 ONNX Runtime 张量
        // ONNX Runtime 使用 Ort::Value 表示张量。需提供内存信息、数据指针、形状等构造。
        Ort::MemoryInfo memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
        Ort::Value input_tensor = Ort::Value::CreateTensor<float>(memory_info, input_data.data(),
                                                                  input_data.size(), input_shape.data(), input_shape.size());
        // 如果模型有多个输入,可按类似方式创建多个 Ort::Value,并准备对应的名字数组。

        // 4. 执行推理
        // 定义输出节点名称数组。如果我们不确定输出名,也可以使用 session.GetOutputName 获取。
        char* output_name = session.GetOutputName(0, allocator);
        std::array<const char*, 1> output_names = { output_name };
        std::array<const char*, 1> input_names = { input_name };
        // 运行会话,获取输出
        auto output_tensors = session.Run(Ort::RunOptions{nullptr}, 
                                          input_names.data(), &input_tensor, 1, 
                                          output_names.data(), 1);
        // session.Run 返回 Ort::Value 的 vector,这里只有一个输出
        Ort::Value& output_tensor = output_tensors.front();
        // 将输出 Ort::Value 转为易于操作的形式,例如获取指向数据的指针
        float* output_data = output_tensor.GetTensorMutableData<float>();  // 假设输出也是 float 类型
        // 获取输出维度信息
        auto output_type_info = session.GetOutputTypeInfo(0).GetTensorTypeAndShapeInfo();
        std::vector<int64_t> output_shape = output_type_info.GetShape();
        std::cout << "Output shape: [";
        for (size_t i = 0; i < output_shape.size(); ++i) {
            std::cout << output_shape[i] << (i < output_shape.size()-1 ? ", " : "");
        }
        std::cout << "]\n";
        // 简单地打印输出第一个元素的值作为示例
        if (total_tensor_size > 0) {
            std::cout << "Output[0] = " << output_data[0] << std::endl;
        }

        // 释放资源
        allocator.Free(input_name);
        allocator.Free(output_name);
    } else {
        std::cerr << "Unexpected input shape dimension!" << std::endl;
    }

    return 0;
}

代码解析:上述 C++ 示例首先创建了一个 Ort::Env 环境和 Ort::Session 会话并加载模型文件。在获取模型信息部分,我们使用 session.GetInputNamesession.GetInputTypeInfo 来查询模型的输入名、类型和形状,并将形状存入 input_shape 向量中。然后,我们根据输入形状大小,分配了一个对应大小的 std::vector<float> 作为输入数据缓冲,并用零填充(实际使用中,这里应该填入真实的预处理后数据)。

接下来,使用 Ort::MemoryInfo::CreateCpu 创建内存描述(这里表示使用CPU分配器的默认内存),并调用 Ort::Value::CreateTensor<float> 将 C++的数据缓冲封装成 ONNX Runtime 可接受的张量对象 input_tensor。需要提供的数据类型模板、数据指针、元素数量、形状数组和维度数。

在执行推理时,使用 session.Run 方法:传入输入名数组、输入 Ort::Value 数组,以及想获取的输出名数组。session.Run 会返回输出 Ort::Value 的向量(对应每个输出)。我们获取第一个输出,利用 GetTensorMutableData<float>() 得到指向输出数据的 C++ 指针,方便后续处理。此外,通过 session.GetOutputTypeInfo 获取输出张量的形状并打印。

这个例子最后简单打印了输出张量第一个元素的值。在真实场景中,我们会根据模型类型对输出进行进一步解释:比如如果是分类模型,则寻找最大值的索引;如果是检测模型,解析边界框数据;如果是序列模型,则解析序列输出等。

值得注意的是,在 C++ 调用中需要手动管理一些资源,例如通过 allocator.Free 释放用 Allocator 获取的字符串(输入名、输出名)。Ort::Session 和 Ort::Value 等则利用 RAII自动释放。

多输入模型处理:如果 ONNX 模型有多个输入(如前面的 BERT 示例有3个输入),在 C++ 中需要准备多个 Ort::Value。在调用 session.Run 时,传入所有输入名的数组和对应 Ort::Value的数组,以及相应的计数。例如:

// 假设有 input_names = { "input_ids", "attention_mask", "token_type_ids" }
// 和已经构造好的 Ort::Value 数组 inputs = { input_ids_tensor, mask_tensor, typeid_tensor };
auto output_tensors = session.Run(Ort::RunOptions{nullptr}, 
                                  input_names.data(), inputs.data(), inputs.size(), 
                                  output_names.data(), output_names.size());

这样即可将多输入一起传入并运行。输出的获取方法与单输入情况类似,只是会有对应多个输出时处理多个 Ort::Value。

总的来说,使用 C++ API 进行 ONNX 模型推理需要先将数据准备为连续的内存块,并正确设置维度和类型,然后调用 ONNX Runtime 执行。在性能敏感的场景下,C++ 推理通常搭配编译优化以及可能的批处理等策略,以获得比 Python 更高的运行速度。上述代码提供了一个基本的模板,可以根据具体模型的输入输出进行扩展和修改。


通过以上 Python 和 C++ 的示例,我们可以看到:不论使用何种语言,ONNX 模型推理的大致流程都是加载模型 -> 提供输入 -> 执行推理 -> 处理输出。ONNX 作为统一的格式,使得我们在不同环境下都能以类似的方式运行相同的模型,大大提高了开发与部署的灵活性和效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值