第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提供了
假设模型输入是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);
这段代码有几点要说明:
- 维度顺序: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 → 后处理。
记住这个路线,后续所有复杂功能都是在这个基础上叠加的。
下面这张图帮你理清从模型文件到推理结果的完整链路:
整个流程很清晰:模型文件由Interpreter加载,带着ScheduleConfig配置创建出Session,然后通过getSessionInput拿到输入Tensor并填充数据,调用runSession完成推理,最后从getSessionOutput读出结果。
你可以把这个流程封装成一个简单的推理函数,后续做批次校正和基准测试时直接调用。