第2节:模型转换与基本推理

好,模型选好或者训好之后,咱们得把它弄到MNN里跑起来。

这一步说白了就是把其他框架的模型“翻译”成MNN能懂的格式。我刚开始接触MNN时,觉得这一步就是个转换命令,后来踩了几个坑才发现,这里面的门道不少。

2.1 模型转换:从ONNX/PyTorch到MNN

MNN提供了一个命令行工具——MNNConvert,负责“翻译”工作。

它支持的源格式挺全的:TensorFlow、PyTorch(通过ONNX)、Caffe、TFLite等等。我个人最常用的是ONNX,因为它生态好,基本主流框架都能导出。

基础用法长这样:

./MNNConvert -f ONNX --modelFile your_model.onnx --MNNModel your_model.mnn --bizCode industry

这里每个参数都有讲究:

  • -f:源框架类型,ONNX或TF等。
  • --modelFile:输入的源模型文件路径。
  • --MNNModel:输出的MNN模型文件名。
  • --bizCode:业务标识,随便写就行,用于区分模型。

但有些模型结构复杂,或者包含一些自定义算子,直接转可能会报错。

我遇到过的最常见问题是op不兼容。比如ONNX里的某个算子MNN没有原生支持,这时候MNN会给出警告,并尝试用组合op替代。你需要回去看一下这个op是不是必须的,或者能不能用MNN支持的等价结构替换。

我曾经在转一个EfficientDet模型时,死活过不去,报错说某个Resize模式不支持。后来查文档发现,MNN对双线性插值的对齐模式有特定要求,我手动修改了ONNX中的参数,才顺利转换。这里你也要注意,ONNX导出时的opset版本最好和MNN匹配,一般opset 11比较稳妥。

转换成功后,命令行会输出一个摘要:

Convert success! Output: your_model.mnn
Input shape: [1, 3, 224, 224]
Output name: 843
Memory: 12.5 MB

看到这个,说明第一步走通了。

2.2 MNN推理接口:Interpreter、Session与Tensor

转换完模型,接下来就是在代码里让它干活。MNN的C++接口主要由三个核心类组成:

  • Interpreter:模型解释器,负责加载模型、管理配置。
  • Session:推理会话,存放实际运行时数据(输入、输出、中间结果)。
  • Tensor:张量,数据载体。

你想想看,这就像一个播放器。Interpreter是播放器本体,Session是播放进度,Tensor是正在播放的视频流。

最基本的推理流程如下:

#include <MNN/Interpreter.hpp>
#include <MNN/Tensor.hpp>

// 1. 创建解释器
auto interpreter = std::shared_ptr<MNN::Interpreter>(MNN::Interpreter::createFromFile("your_model.mnn"));

// 2. 创建会话(配置后端)
MNN::ScheduleConfig config;
config.type = MNN_FORWARD_CPU; // 使用CPU推理
config.numThread = 4;          // 线程数

auto session = interpreter->createSession(config);

// 3. 获取输入张量
auto inputTensor = interpreter->getSessionInput(session, "input_node_name");

// 4. 填充输入数据(这一步后面详说)
// ...

// 5. 执行推理
interpreter->runSession(session);

// 6. 获取输出张量
auto outputTensor = interpreter->getSessionOutput(session, "output_node_name");

ScheduleConfig里的type参数可以指定后端,除了CPU,还有MNN_FORWARD_OPENCL(GPU)、MNN_FORWARD_METAL(苹果GPU)、MNN_FORWARD_NNAPI(Android NNAPI)等。

我个人习惯在初始化时把后端和线程数都做成可配置的,这样在基准测试时可以快速切换对比。比如同一个模型,CPU跑多少帧,GPU跑多少帧,一目了然。

2.3 数据准备:图片预处理与张量填充

模型能跑了,但输入不能乱填。你得按照训练时的预处理方式,把图片数据转换成模型能吃的格式。

一般流程是:读取图片 -> 解码 -> resize到目标尺寸 -> 归一化 -> HWC转CHW -> 送入Tensor。

MNN提供了的接口来直接操作数据指针,不用像我早期那样手动malloc再memcpy。

假设模型输入是1x3x224x224,float32:

// 假设cv::Mat mat 已经读取并resize到224x224
int inputSize = 1 * 3 * 224 * 224;
std::vector<float> inputData(inputSize);

// 处理像素:HWC转CHW 并 归一化到[0,1]
for (int h = 0; h < 224; ++h) {
    for (int w = 0; w < 224; ++w) {
        for (int c = 0; c < 3; ++c) {
            int srcIdx = h * 224 * 3 + w * 3 + c;
            int dstIdx = c * 224 * 224 + h * 224 + w;
            inputData[dstIdx] = mat.data[srcIdx] / 255.0f;
        }
    }
}

// 填充到输入张量
auto nchwTensor = MNN::Tensor::create<float>(inputTensor->getShape(), nullptr, MNN::Tensor::CAFFE);
::memcpy(nchwTensor->host<float>(), inputData.data(), inputData.size() * sizeof(float));
inputTensor->copyFromHostTensor(nchwTensor);

这段代码有几点要说明:

  1. 维度顺序:MNN默认是NCHW(CAFFE格式),但可以设置NHWC(TENSORFLOW格式)。具体看你的模型是用什么框架训的。对比一下:
格式 数据排列顺序 常见框架
NCHW Batch × Channel × Height × Width PyTorch, Caffe
NHWC Batch × Height × Width × Channel TensorFlow

弄错这个,推理结果会完全不对。嗯,这里要注意,转换模型时MNNConvert会保留原模型的维度顺序,你推理时得保持一致。

2.4 运行推理与结果解读

数据准备好了,调用runSession就完事了。但拿到结果之后怎么解读?

比如分类模型,输出通常是一个1xN的向量,每个值代表该类别得分。你需要找到最大值的位置,对应到类别标签。

// 获取输出数据
auto outputTensor = interpreter->getSessionOutput(session, "output_node_name");
auto outputHost = MNN::Tensor::create<float>(outputTensor->getShape(), nullptr, MNN::Tensor::CAFFE);
outputTensor->copyToHostTensor(outputHost);

float* data = outputHost->host<float>();
int classCount = outputHost->elementSize();

int maxIdx = 0;
float maxScore = data[0];
for (int i = 1; i < classCount; ++i) {
    if (data[i] > maxScore) {
        maxScore = data[i];
        maxIdx = i;
    }
}
printf("Predicted class: %d, score: %f\n", maxIdx, maxScore);

如果你是第一次跑,我建议把整个输出向量打印出来看看。我曾经遇到过模型转换后输出顺序和原始模型不一致的情况(虽然概率很低),排查时打印全量输出帮了大忙。

完整推理基本流程总结:

加载模型 → 创建Session → 准备输入Tensor → 填充数据 → runSession → 读取输出Tensor → 后处理。

记住这个路线,后续所有复杂功能都是在这个基础上叠加的。

下面这张图帮你理清从模型文件到推理结果的完整链路:

MNN模型加载与推理流程 your_model.mnn Interpreter ScheduleConfig createSession(config) Session getSessionInput ("input_node_name") 填充数据 → Tensor copyFromHostTensor runSession(session) 前向推理 getSessionOutput ("output_node_name") copyToHostTensor 获取float*数据 结果

整个流程很清晰:模型文件由Interpreter加载,带着ScheduleConfig配置创建出Session,然后通过getSessionInput拿到输入Tensor并填充数据,调用runSession完成推理,最后从getSessionOutput读出结果。

你可以把这个流程封装成一个简单的推理函数,后续做批次校正和基准测试时直接调用。