Bootstrap

【深度学习实践】移动端和嵌入式设备上ncnn模型部署

以下内容将从 ncnn 简介模型生成(从 PyTorch/ONNX 转换到 ncnn)应用案例(C++ 推理示例) 三个部分进行介绍,并附带示例代码详细说明,帮助你理解并上手使用 ncnn。


一、ncnn 简介

ncnn 是由腾讯优图团队开源的高性能神经网络推理框架,专为移动端和嵌入式设备上高效运行推理而设计,具有以下特点:

  1. 跨平台:支持多种操作系统(Android、iOS、Linux、Windows、Mac 等),并可轻松进行移植。
  2. 高效率:针对 ARM 设备做了大量优化,能够在移动端以较小的二进制体积、高速运行深度学习模型。
  3. 轻量级:使用 C++ 实现,不依赖第三方框架,在移动端部署时占用空间小。
  4. 易集成:提供了 C++ API 和 Android NDK 相关的支持,便于快速接入已有项目。

ncnn 框架主要解决了在移动端对训练好的深度学习模型进行推理的问题。它不包含训练部分(通常在服务器/PC 端完成训练后再转换模型到 ncnn),它的主要流程如下:

  1. 在 PC 端使用 PyTorch、TensorFlow 等框架对模型进行训练和导出(一般导出为 ONNX 或者 caffe 模式)。
  2. 使用 ncnn 提供的 模型转换工具onnx2ncnncaffe2ncnn 等),将 ONNX 或 Caffe 模型转换为 ncnn 专有的 .param(网络结构)和 .bin(权重)文件。
  3. 在移动端或嵌入式设备上,使用 ncnn 的 C++ 或 Java 接口加载 .param.bin 文件,通过 ncnn 的推理接口进行前向推理,从而得到推理结果。

二、ncnn 模型生成案例

这里以 PyTorch 训练一个简单分类模型转换为 ONNX,再转换到 ncnn 为例,给出一个完整的流程示例。

2.1 在 PyTorch 中训练(或使用已有预训练模型)

下面的示例代码展示了如何用 PyTorch 定义一个简单的分类网络,并保存为 ONNX。你可以使用任意预训练模型,这里以一个示例网络为演示:

import torch
import torch.nn as nn
import torch.nn.functional as F

class SimpleNet(nn.Module):
    def __init__(self, num_classes=10):
        super(SimpleNet, self).__init__()
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)
        self.fc1 = nn.Linear(32 * 8 * 8, 128)
        self.fc2 = nn.Linear(128, num_classes)

    def forward(self, x):
        x = F.relu(self.conv1(x))    # [N, 16, 32, 32]
        x = F.max_pool2d(x, 2)      # [N, 16, 16, 16]
        x = F.relu(self.conv2(x))   # [N, 32, 16, 16]
        x = F.max_pool2d(x, 2)      # [N, 32, 8, 8]
        x = x.view(x.size(0), -1)   # flatten => [N, 32*8*8]
        x = F.relu(self.fc1(x))     # [N, 128]
        x = self.fc2(x)             # [N, num_classes]
        return x

if __name__ == "__main__":
    # 假设已经训练完成的模型,这里只演示前向导出
    model = SimpleNet(num_classes=10)
    
    # 这里你通常会加载训练好的权重
    # model.load_state_dict(torch.load("simple_net.pth"))
    
    model.eval()
    # 随机输入,模拟1张 RGB 32x32 的图像
    dummy_input = torch.randn(1, 3, 32, 32)

    # 导出 ONNX
    torch.onnx.export(
        model, 
        dummy_input, 
        "simple_net.onnx", 
        input_names=["input"], 
        output_names=["output"], 
        opset_version=11
    )
    print("ONNX model exported: simple_net.onnx")
  • 说明:
    • 在实际场景中,你需要使用真实数据进行训练,得到训练好的模型权重,再用 model.load_state_dict(torch.load(...)) 来加载。
    • 这里为了演示如何导出 ONNX,使用了随机输入 dummy_input
    • 生成的文件 simple_net.onnx 就是后续 ncnn 转换所需要的输入文件。

2.2 使用 ncnn 的转换工具将 ONNX 转换为 ncnn

  1. 需要从 ncnn 的仓库中获取 onnx2ncnn 工具,可以:
    • 直接下载已编译好的 Release 版(Windows / Linux / macOS 均有预编译可执行文件)。
    • 或者自己编译 ncnn(cmake)并在 build/tools/onnx/ 目录下获得可执行文件 onnx2ncnn
  2. 假设你拿到 onnx2ncnn 可执行文件,并将其放到和 simple_net.onnx 同一目录下,执行以下命令(终端 / 命令行):
    ./onnx2ncnn simple_net.onnx simple_net.param simple_net.bin
    
  3. 执行完成后,你会得到两个文件:
    • simple_net.param:网络结构描述
    • simple_net.bin:网络权重数据

这两个文件就是 ncnn 推理所需的最终模型文件。


三、ncnn 推理应用案例

下面展示 C++ 使用 ncnn 进行推理的示例(在 Linux 或者 Android NDK 下均可类似操作)。这里以输入一张图片并进行分类推理为例。流程如下:

  1. 加载 ncnn 模型:读取 .param + .bin
  2. 准备输入:根据模型需要,预处理图片到对应的维度(3 x 32 x 32),并归一化等。
  3. 推理得到输出,并解析结果(比如输出是10维度的分类结果,就取最大值对应的类别)。

3.1 示例工程结构

假设工程目录如下:

ncnn-example/
  |-- simple_net.param
  |-- simple_net.bin
  |-- main.cpp
  |-- CMakeLists.txt  (示例)
  `-- ...

3.2 main.cpp (示例)

下面给出一个简化的示例代码(适用于 ncnn 2022+ 版本),使用 OpenCV 进行图像读取,使用 ncnn 进行推理:

#include <stdio.h>
#include <vector>
#include <algorithm>
#include <opencv2/opencv.hpp>
#include "net.h"  // ncnn 主头文件

// 假设分类输出10个类别
#define NUM_CLASSES 10

int main(int argc, char** argv)
{
    if (argc < 2)
    {
        fprintf(stderr, "Usage: %s [imagepath]\n", argv[0]);
        return -1;
    }

    const char* imagepath = argv[1];

    // 1. 读取图像
    cv::Mat bgr = cv::imread(imagepath, 1);
    if (bgr.empty())
    {
        fprintf(stderr, "cv::imread %s failed\n", imagepath);
        return -1;
    }
    
    // 2. 初始化 ncnn::Net
    ncnn::Net net;
    
    // 可选的设置,比如不使用 Vulkan
    net.opt.use_vulkan_compute = false;
    
    // 3. 加载 param 和 bin
    net.load_param("simple_net.param");
    net.load_model("simple_net.bin");

    // 4. 创建输入 (注意网络期望的输入尺寸及归一化)
    // 这里 SimpleNet 期望输入 3x32x32,但实际可以先对 bgr 做 resize,然后再进行 ncnn Mat 构造。
    cv::Mat resized;
    cv::resize(bgr, resized, cv::Size(32, 32));

    // ncnn 的 Mat 通常是 NCHW,但在传入时可以用 from_pixels() 等接口转换
    ncnn::Mat in = ncnn::Mat::from_pixels(resized.data, ncnn::Mat::PIXEL_BGR, 32, 32);

    // 如果有归一化需求,这里可以对 in 进行 normalize
    const float mean_vals[3] = { 127.5f, 127.5f, 127.5f };
    const float norm_vals[3] = { 1/127.5f, 1/127.5f, 1/127.5f };
    in.substract_mean_normalize(mean_vals, norm_vals);

    // 5. 创建 ncnn 预测 Extractor
    ncnn::Extractor ex = net.create_extractor();
    ex.set_num_threads(4);  // 根据硬件情况设定线程数

    ex.input("input", in);  // 对应 PyTorch 导出时的 input name

    // 6. 输出
    ncnn::Mat out;
    ex.extract("output", out);  // 对应 PyTorch 导出时的 output name
    
    // out 的 shape 一般是 1 x num_classes
    // 这里假设是 [1, 10]
    std::vector<float> scores(NUM_CLASSES);
    for (int i = 0; i < NUM_CLASSES; i++)
    {
        scores[i] = out[i];
    }
    // 7. 找到分数最大值
    int predicted_class = std::max_element(scores.begin(), scores.end()) - scores.begin();
    float max_score = scores[predicted_class];

    printf("Predicted class: %d, score = %f\n", predicted_class, max_score);

    return 0;
}
  • 要点说明:
    1. ncnn::Net 是 ncnn 中用于加载模型和进行推理的主要对象。
    2. load_param()load_model() 分别加载 .param.bin 文件,需保证路径正确。
    3. 预处理阶段要注意和训练时一致的图像尺寸归一化处理(均值、方差等)。
    4. 在 PyTorch 导出时指定的 inputoutput 名称在推理时要对应一致。

3.3 简易 CMakeLists.txt 示例

在 Linux 平台下(假设 ncnn 已经安装在系统或有编译好的 static / shared library),一个简单的 CMakeLists.txt 可能是:

cmake_minimum_required(VERSION 3.10)
project(ncnn_example)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_BUILD_TYPE Release)

# 指定 ncnn 头文件和库文件的路径(根据实际情况修改)
# 假设 /usr/local/include/ncnn /usr/local/lib
include_directories(/usr/local/include/ncnn)
link_directories(/usr/local/lib)

# 找到 OpenCV
find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})

# 生成可执行文件
add_executable(ncnn_example main.cpp)

# 链接 ncnn 和 OpenCV
target_link_libraries(ncnn_example ncnn ${OpenCV_LIBS})

然后在 ncnn-example/ 目录下执行:

mkdir build
cd build
cmake ..
make
./ncnn_example ../test.jpg  # 预测

四、其他应用方向

  1. 目标检测:ncnn 也广泛用于 Mobile端常见的目标检测模型(YOLO 系列、SSD、RetinaNet 等)部署,流程与上面类似,先转 ONNX,再使用 onnx2ncnn
  2. 人脸识别/人脸检测:如 scrfd、MobileFaceNet 等模型,也有相应的 ncnn 例程
  3. 语音处理:ncnn 也可以用于一些语音推理模型(但要注意运算层支持,如 Transformer 相关算子支持)。
  4. 量化模型:ncnn 支持低比特量化,例如 int8 量化,可以进一步压缩模型、加速推理。

五、小结

  1. ncnn 的主要优势是在移动端和嵌入式端轻量且高效,容易移植,同时支持多线程、Vulkan 加速等优化手段。
  2. 使用流程一般为PC 端训练 -> 导出 ONNX -> onnx2ncnn 转换 -> ncnn 推理
  3. 推理过程在 C++ 中可以简要分为:
    • 创建 ncnn::Net
    • load_param + load_model
    • 预处理输入数据为 ncnn::Mat
    • extractor.input() / extractor.extract()
    • 获得输出,进行结果解析

希望通过上述模型生成与应用案例,你能清晰了解 ncnn 的模型转换推理流程。结合你实际项目的需求,通常只需在 PC 端完成训练和转换,然后在移动/嵌入式端利用 ncnn 的 API 进行加载和推理即可快速部署。祝你在 ncnn 的使用中一切顺利!

;