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.GetInputName 和 session.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 作为统一的格式,使得我们在不同环境下都能以类似的方式运行相同的模型,大大提高了开发与部署的灵活性和效率。