4. 使用NCNN部署量化后的MobileNetV2

任务目标

将上一案例中量化后的MobileNetV2模型(INT8或FP16)转换为NCNN格式,在PC端使用NCNN推理引擎加载模型,对单张测试图片进行分类推理,并验证输出结果与量化前模型的预测类别一致。

环境依赖

  • NCNN(编译安装或直接下载预编译包):ncnn 库及 ncnn2mem 工具
  • ONNX模型:mobilenetv2_quant.onnx(量化后模型,可由PyTorch导出或由前序案例生成)
  • OpenCV(用于图片读取与预处理)
  • 测试图片:任选一张ImageNet类别图片(如 cat.jpg)
  • 类别标签文件:synset.txt(ImageNet 1000类标签)

实现步骤

整体流程分为两步:模型转换(ONNX→NCNN)与C++推理。转换使用NCNN自带的 onnx2ncnn 工具完成,推理代码使用NCNN C++ API实现。

1. ONNX模型转换为NCNN格式

使用NCNN提供的转换工具,将ONNX模型转为NCNN的 .param 和 .bin 文件:

onnx2ncnn mobilenetv2_quant.onnx mobilenetv2.param mobilenetv2.bin

若模型包含量化信息(如QDQ节点),转换后需检查 .param 文件中是否包含 Quantize/Dequantize 层。若需进一步压缩为FP16存储,可执行:

ncnn2mem mobilenetv2.param mobilenetv2.bin mobilenetv2.id.h mobilenetv2.mem.h

本案例使用普通 .param/.bin 即可完成推理闭环。

2. C++推理代码

// mobilenetv2_ncnn_infer.cpp
#include <opencv2/opencv.hpp>
#include <ncnn/net.h>
#include <fstream>
#include <iostream>
#include <vector>
#include <string>

int main(int argc, char** argv) {
    if (argc < 3) {
        std::cerr << "Usage: " << argv[0] << " <image_path> <label_file>" << std::endl;
        return -1;
    }

    // 加载标签
    std::vector<std::string> labels;
    std::ifstream label_file(argv[2]);
    std::string line;
    while (std::getline(label_file, line)) {
        labels.push_back(line);
    }

    // 加载NCNN模型
    ncnn::Net net;
    int ret = net.load_param("mobilenetv2.param");
    if (ret != 0) {
        std::cerr << "Failed to load param" << std::endl;
        return -1;
    }
    ret = net.load_model("mobilenetv2.bin");
    if (ret != 0) {
        std::cerr << "Failed to load model" << std::endl;
        return -1;
    }

    // 读取图片并预处理
    cv::Mat img = cv::imread(argv[1]);
    if (img.empty()) {
        std::cerr << "Failed to read image" << std::endl;
        return -1;
    }
    cv::resize(img, img, cv::Size(224, 224));
    img.convertTo(img, CV_32FC3, 1.0 / 255.0);

    // 转换为NCNN Mat(RGB顺序,减均值除方差)
    ncnn::Mat in = ncnn::Mat::from_pixels_resize(img.data, ncnn::Mat::PIXEL_BGR2RGB, 224, 224, 224, 224);
    const float mean_vals[3] = {0.485f, 0.456f, 0.406f};
    const float norm_vals[3] = {1/0.229f, 1/0.224f, 1/0.225f};
    in.substract_mean_normalize(mean_vals, norm_vals);

    // 推理
    ncnn::Extractor ex = net.create_extractor();
    ex.input("input", in);
    ncnn::Mat out;
    ex.extract("output", out);

    // 获取top-1结果
    float* ptr = (float*)out.data;
    int max_idx = 0;
    float max_score = ptr[0];
    for (int i = 1; i < out.w; i++) {
        if (ptr[i] > max_score) {
            max_score = ptr[i];
            max_idx = i;
        }
    }

    std::cout << "Predicted class: " << labels[max_idx] << " (index " << max_idx << ")" << std::endl;
    std::cout << "Confidence: " << max_score << std::endl;

    return 0;
}

3. 编译与运行

g++ -O2 -std=c++11 mobilenetv2_ncnn_infer.cpp -o infer \
    -I/path/to/ncnn/include -I/path/to/opencv/include \
    -L/path/to/ncnn/lib -lncnn -L/path/to/opencv/lib -lopencv_core -lopencv_imgproc -lopencv_imgcodecs

./infer cat.jpg synset.txt

验证方法

运行程序后,观察输出类别是否为图片的真实内容(如 tabby cat)。同时,使用同一张图片在量化前模型(原始ONNX)上推理,对比两者的预测类别是否一致。若类别一致,说明NCNN部署正确;若不一致,检查模型转换时是否丢失了量化信息或预处理参数是否匹配。

通过条件:程序正常输出top-1类别与置信度,且类别与量化前模型预测结果一致。

常见问题

  • 输入输出节点名不匹配:需在转换前确认ONNX模型的输入输出名称,并在代码中对应修改 ex.input 和 ex.extract 的字符串。
  • 量化精度损失导致类别变化:若INT8量化后精度下降明显,可改用FP16量化模型,或检查量化校准数据集是否覆盖了测试图片类别。
  • 编译链接错误:确认NCNN和OpenCV的库路径正确,且NCNN编译时启用了 NCNN_VULKAN 或 NCNN_OPENMP 等选项时,需额外链接对应依赖库。