从端到端打通模型端侧部署流程(MNN)

从端到端打通模型端侧部署流程(MNN)

  • MNN框架:
    • MNN的官方介绍:
    • MNN的架构:
    • 问题解决:
    • MNN使用样例:
  • MNN部署:
    • 一般流程:
      • 创建会话:
      • 运行会话:
      • 获取输出:
    • 流程汇总
  • 总结

MNN框架:

从端到端打通模型端侧部署流程(MNN)_第1张图片

MNN的官方介绍:

官方文档
有疑问一定要先查这里!

MNN是一个轻量级的深度神经网络推理引擎,在端侧加载深度神经网络模型进行推理预测。目前,MNN已经在阿里巴巴的手机淘宝、手机天猫、优酷等20多个App中使用,覆盖直播、短视频、搜索推荐、商品图像搜索、互动营销、权益发放、安全风控等场景。此外,IoT等场景下也有若干应用。

MNN的架构:

从端到端打通模型端侧部署流程(MNN)_第2张图片
MNN可以分为Converter和Interpreter两部分。

Converter由Frontends和Graph Optimize构成。前者负责支持不同的训练框架,MNN当前支持Tensorflow(Lite)、Caffe和ONNX(PyTorch/MXNet的模型可先转为ONNX模型再转到MNN);后者通过算子融合、算子替代、布局调整等方式优化图。

Interpreter由Engine和Backends构成。前者负责模型的加载、计算图的调度;后者包含各计算设备下的内存分配、Op实现。在Engine和Backends中,MNN应用了多种优化方案,包括在卷积和反卷积中应用Winograd算法、在矩阵乘法中应用Strassen算法、低精度计算、Neon优化、手写汇编、多线程优化、内存复用、异构计算等。

问题解决:

MNN作为开开源时间并不长的框架,社区的建设相对于NCNN来讲毕竟还是有所欠缺,好在官方已经在努力不断建设社区的内容,建立了很多钉钉群,相关的问题可以先在github里提交issue之后再去钉钉群中解答。具体的链接在MNN的Github的主页里可以找到,请点击这里。一些常见的使用问题可以在这里找到相关的解答。

MNN使用样例:

物体分类样例
车道检测样例
直线检测
人脸识别
中文字OCR
其他案例汇总

MNN部署:

一般流程:

由于有了之前我们部署NCNN的经验,大致上的端侧部署的原理都是相通的,NCNN是以模型推理器(Extractor)的形式来处理数据,加载模型,而到了MNN则是以解释器(Interpreter)的形式来加载数据和模型,处理数据的。

使用MNN推理时,有两个层级的抽象,分别是解释器Interpreter会话Session。Interpreter是模型数据的持有者;Session通过Interpreter创建,是推理数据的持有者。多个推理可以共用同一个模型,即,多个Session可以共用一个Interpreter。

一个标准的运行流程包括以下几部分:
创建会话->输入数据->运行会话->获取输出

创建会话:

  • 创建Interpreter(持有模型数据)
  • 创建Session(持有推理数据)
  • 调度配置ScheduleConfig(一般情况下,不需要额外设置调度配置,函数会根据模型结构自动识别出调度路径、输入输出)
  • 后端配置BackendConfig(内存、功耗和精度偏好)
  • 输入数据:

MNN输入数据是按照tensor的方式输入的,这块包括两大部分,数据的输入和预处理。

Tensor
Interpreter上提供了两个用于获取输入Tensor的方法:getSessionInput用于获取单个输入tensor,
getSessionInputAll用于获取输入tensor映射。

  • 图像处理:

下面是官方给的图像处理的标准处理:

auto input  = net->getSessionInput(session, NULL);
auto output = net->getSessionOutput(session, NULL);

auto dims  = input->shape();
int bpp    = dims[1]; 
int size_h = dims[2];
int size_w = dims[3];

auto inputPatch = argv[2];
FREE_IMAGE_FORMAT f = FreeImage_GetFileType(inputPatch);
FIBITMAP* bitmap = FreeImage_Load(f, inputPatch);
auto newBitmap = FreeImage_ConvertTo32Bits(bitmap);
auto width = FreeImage_GetWidth(newBitmap);
auto height = FreeImage_GetHeight(newBitmap);
FreeImage_Unload(bitmap);

Matrix trans;
//Dst -> [0, 1]
trans.postScale(1.0/size_w, 1.0/size_h);
//Flip Y  (因为 FreeImage 解出来的图像排列是Y方向相反的)
trans.postScale(1.0,-1.0, 0.0, 0.5);
//[0, 1] -> Src
trans.postScale(width, height);

ImageProcess::Config config;
config.filterType = NEAREST;
float mean[3] = {103.94f, 116.78f, 123.68f};
float normals[3] = {0.017f,0.017f,0.017f};
::memcpy(config.mean, mean, sizeof(mean));
::memcpy(config.normal, normals, sizeof(normals));
config.sourceFormat = RGBA;
config.destFormat = BGR;

std::shared_ptr<ImageProcess> pretreat(ImageProcess::create(config));
pretreat->setMatrix(trans);
pretreat->convert((uint8_t*)FreeImage_GetScanLine(newBitmap, 0), width, height, 0, input);
net->runSession(session);

从代码可以看到,用到了Freeimage的库,鉴于我们大部分使用的还是opencv,这里也给一个替换的教程阿里MNN移动端部署框架,将FreeImage更换为opencv的实现。

运行会话:

这一块比较简单,一般来讲直接调用即可。
runSession(Session* session) const;

获取输出:

输出这块也是用的tensor

auto outputTensor = interpreter->getSessionOutput(session, NULL);
auto nchwTensor = new Tensor(outputTensor, Tensor::CAFFE);
outputTensor->copyToHostTensor(nchwTensor);
auto score = nchwTensor->host<float>()[0];
auto index = nchwTensor->host<float>()[1];
// ...
delete nchwTensor;

然后把相关的指针赋值给输出的数组或者mat即可。

流程汇总

接下来为流程的汇总调用。

#include 
#include 
#include 
#include 
#include 
#include 

#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "MNNExample", __VA_ARGS__)

static MNN::Interpreter *interpreter = nullptr;
static MNN::Session *session = nullptr;
static MNN::Tensor *inputTensor = nullptr;
static MNN::Tensor *outputTensor = nullptr;

extern "C" {
JNIEXPORT jint JNICALL
Java_com_example_mnnexample_MNNRunner_init(JNIEnv *env, jobject instance, jstring modelPath_) {
    // 获取模型路径
    const char *modelPath = env->GetStringUTFChars(modelPath_, 0);

    // 创建解释器
    interpreter = MNN::Interpreter::createFromFile(modelPath);
    MNN::ScheduleConfig config;
    config.numThread = 1; // 设置线程数
    session = interpreter->createSession(config);

    // 获取输入输出张量
    std::vector<MNN::Tensor *> inputs = interpreter->getSessionInputAll(session);
    std::vector<MNN::Tensor *> outputs = interpreter->getSessionOutputAll(session);
    inputTensor = inputs[0];
    outputTensor = outputs[0];

    // 释放字符串
    env->ReleaseStringUTFChars(modelPath_, modelPath);

    return 0;
}

JNIEXPORT jfloatArray JNICALL
Java_com_example_mnnexample_MNNRunner_predict(JNIEnv *env, jobject instance,
                                              jfloatArray data_, jint width, jint height,
                                              jint channel) {
    // 获取传入的数据
    jfloat *data = env->GetFloatArrayElements(data_, NULL);
    if (data == NULL) {
        return NULL;
    }

    // 输入张量的形状为 NHWC
    inputTensor->resize({1, height, width, channel});
    inputTensor->copyFromHostFloat(data);

    // 运行模型
    interpreter->runSession(session);

    // 获取输出张量数据
    std::vector<int> dims = outputTensor->shape();
    jfloatArray output = env->NewFloatArray(dims[1]);
    outputTensor->copyToHostFloat(output);

    // 释放内存
    env->ReleaseFloatArrayElements(data_, data, 0);

    return output;
}

JNIEXPORT void JNICALL
Java_com_example_mnnexample_MNNRunner_release(JNIEnv *env, jobject instance) {
    // 释放资源
    inputTensor->release();
    outputTensor->release();
    interpreter->releaseSession(session);
    delete interpreter;
}
}

其中,Java_com_example_mnnexample_MNNRunner_init函数用于初始化模型解释器,Java_com_example_mnnexample_MNNRunner_predict函数用于运行模型,并返回输出结果,Java_com_example_mnnexample_MNNRunner_release函数用于释放资源。

需要注意的是,安卓JNI中访问Java类成员变量或者调用Java类的函数,需要使用env->GetObjectClass(instance)获取Java类对象。

总结

相对来讲,目前我们过了两个端侧部署框架NCNN和MNN,

NCNN突出的特点在于:

  1. 开源较早,社区成熟,案例较多,问题容易解决。
  2. 易用性较强,相对于MNN会更容易上手,很多操作都直接封装到NCNN内部,方便操作。
  3. 相对来讲速度会稍慢,模型优化的部分会少一些。

MNN的突出特点在于:

  1. 网上的案例相对较少,问题解决较困难一点。
  2. 易用性稍差,需要较高的学习成本,使用时需要搭配CV的库。
  3. 支持训练蒸馏量化,速度较快

你可能感兴趣的:(端侧框架,mnn,人工智能,深度学习)