22

张新栋 · 2020年03月20日

PFLD-lite:基于MNN和mxnet的嵌入式部署

之前的文章跟大家介绍过如何使用mxnet和MNN做产品化部署,选取的案例为MNIST手写数字识别的项目,该项目是一个比较简单的分类问题。今天我们将选取一个稍微复杂一点的例子进行讲解,所选取的案例可参考之前专栏的文章:PFLD:一个实用的人脸关键点检测器
首发:https://zhuanlan.zhihu.com/p/80051906
作者:张新栋

基于MNN的inference代码和训练代码可参考链接

对算法模型设计感兴趣的可以参考上述的链接,文中有对算法的结构和设计思路进行简单的分析和论述。本文主要关注的是如何使用MNN和mxnet进行开发和部署,尽量贴近项目开发的场景。

一般端上的AI的部署流程一般分如下几个步骤,之前的文章中也简单跟大家介绍过,这里再重复一下:1、模型设计和训练 2、针对推断框架的模型转换 3、模型部署。虽然把整个流程分成三步,但三者之间是相互联系、相互影响的。首先第一步的模型设计需要考虑推断框架中对Op的支持程度,相应的对网络结构进行调整,进行修改或者裁剪都是经常的事情;模型转换也需要确认推断框架是否能直接解析,或者选取的解析媒介是否支持网络结构中的所有Op,如果发现有不支持的地方,再权衡进行调整。以PFLD的复现为例,下面我们先看看第一步:

模型的设计和训练

模型的网络结构大家可以参考PFLD:一个实用的人脸关键点检测器,本文中我们实现了一个精简的版本。我们在设计的过程中移除了进行角度预测的子网络(其实我复现了原文的结构,发现有和没有这个子网络对结果影响其实很有限);由于在项目移植的过程中发现MNN的onnx二元运算符“+”号,即elementwise-add的支持精度上可能存在问题,所以在网络结构上,我们移除了Mobilenet-v2中的shortcut connection及多尺度的特征融合;同时存在精度问题的还有onnx的dense layer,于是我们将最后进行回归的全连接层替换成了Conv2D。这里需要注意的是,如果是基于Dense layers进行回归,则输出为(1,2*98)的向量(假设关键点个数为98);如果是基于Conv2D进行回归,输出结果维度则为(1,2*98,1,1)。最终的网络结构如下图,完整的代码请参考PFLD网络结构

class CPFLD(mx.gluon.HybridBlock):
    def __init__(self, num_of_pts=98, alpha=1.0, **kwargs):
        super(CPFLD, self).__init__(**kwargs)
        self.pts_num = num_of_pts
        self.feature_shared = mx.gluon.nn.HybridSequential()
        self.lmks_net = mx.gluon.nn.HybridSequential()
        self.angs_net  = mx.gluon.nn.HybridSequential()
        
        ##------shared feature-----##
        ## shadow feature extraction
        _add_conv(self.feature_shared, channels=64, kernel=3, stride=2, pad=1, num_group=1)
        _add_conv_dw(self.feature_shared, dw_channels=64, channels=64, stride=1)
        ## mobilenet-v2, t=2, c=64, n=5, s=2
        self.feature_shared.add(
            LinearBottleneck(in_channels=64, channels=64, t=2, alpha=alpha, stride=2),
            LinearBottleneck(in_channels=64, channels=64, t=2, alpha=alpha, stride=1),
            LinearBottleneck(in_channels=64, channels=64, t=2, alpha=alpha, stride=1),
            LinearBottleneck(in_channels=64, channels=64, t=2, alpha=alpha, stride=1),
            LinearBottleneck(in_channels=64, channels=64, t=2, alpha=alpha, stride=1)
        )

        ##------landmark regression-----##
        ## mobilenet-v2, t=2, c=128, n=1, s=2
        self.lmks_net.add(
            LinearBottleneck(in_channels=64, channels=128, t=2, alpha=alpha, stride=2)
        )
        ## mobilenet-v2, t=4, c=128, n=6, s=1
        self.lmks_net.add(
            LinearBottleneck(in_channels=128, channels=128, t=4, alpha=alpha, stride=1),
            LinearBottleneck(in_channels=128, channels=128, t=4, alpha=alpha, stride=1),
            LinearBottleneck(in_channels=128, channels=128, t=4, alpha=alpha, stride=1),
            LinearBottleneck(in_channels=128, channels=128, t=4, alpha=alpha, stride=1),
            LinearBottleneck(in_channels=128, channels=128, t=4, alpha=alpha, stride=1),
            LinearBottleneck(in_channels=128, channels=128, t=4, alpha=alpha, stride=1)
        )
        ## mobilenet-v2, t=2, c=16, n=1, s=1
        self.lmks_net.add(
            LinearBottleneck(in_channels=128, channels=16, t=2, alpha=alpha, stride=1),
        )
        ## landmarks regression: base line
        self.s2_conv = nn.Conv2D(channels=32, kernel_size=(3,3), strides=(2,2), padding=(1,1))
        self.s2_bn   = nn.BatchNorm(scale=True)
        self.s2_act  = nn.Activation('relu')
        self.s3_conv = nn.Conv2D(channels=128, kernel_size=(3,3), strides=(2,2), padding=(1,1)
        self.s3_bn   = nn.BatchNorm(scale=True)
        self.s3_act  = nn.Activation('relu')
        

        self.lmks_out = nn.HybridSequential()
        self.lmks_out.add(
            nn.Conv2D(channels=num_of_pts*2, kernel_size=(3,3), strides=(1,1), padding=(0,0)),
        )

    def hybrid_forward(self, F, x):
        x  = self.feature_shared(x)
        
        ## regress facial landmark: base-line
        s1  = self.lmks_net(x)

        s2  = self.s2_conv(s1) 
        s2  = self.s2_bn(s2)
        s2  = self.s2_act(s2)

        s3  = self.s3_conv(s2)
        s3  = self.s3_bn(s3)
        s3  = self.s3_act(s3)
     
        lmk = self.lmks_out(s3)
        return lmk

针对推断框架的模型转换

MNN针对多种深度学习训练框架进行了适配,提供了tensorflow pb、tensorflow tflite、caffe及onnx等模型转换工具。这里我们选取onnx作为模型转换的媒介,我们可以采用如下代码进行onnx的模型转换:

import mxnet as mx
import numpy as np
from mxnet.contrib import onnx as onnx_mxnet
import logging
logging.basicConfig(level=logging.INFO)
from onnx import checker
import onnx

syms   = './outputs/pfld_7.5w/lmks_detector-symbol.json'
params = './outputs/pfld_7.5w/lmks_detector-0400.params'

input_shape = (1,3,96,96)

onnx_file = './outputs/pfld_7.5w/pfld-lite.onnx'

# Invoke export model API. It returns path of the converted onnx model
converted_model_path = onnx_mxnet.export_model(syms, params, [input_shape], np.float32, onnx_file)

# Load onnx model
model_proto = onnx.load_model(converted_model_path)

# Check if converted ONNX protobuf is valid
checker.check_graph(model_proto.graph)

然后使用MNN提供的onnx模型转换工具就可以转换成MNN可以解析的模型文件,

./MNNConvert --framework ONNX --modelFile pfld-lite.onnx --MNNModel pfld-lite.mnn --bizCode MNN

模型部署

模型部署流程就比较简单了,使用MNN提供的API先进行模型加载,然后依次进行数据预处理、网络inference、结构后处理,最后就可以取得最终结果。下面是主要的业务代码,最后还需要针对不同的部署平台进行编译(本文的项目提供了针对android平台的编译脚本)。

#include "Backend.hpp"
#include "Interpreter.hpp"
#include "MNNDefine.h"
#include "Tensor.hpp"
#include "revertMNNModel.hpp"

#include <math.h>
#include <opencv2/opencv.hpp>
#include <iostream>
#include <stdio.h>

int main(void)
{
    std::string image_name = "./image-02.jpg";
    std::string model_name = "./pfld-lite.mnn";
    int forward = MNN_FORWARD_CPU;

    int threads    = 1;
    int INPUT_SIZE = 96;

    cv::Mat raw_image    = cv::imread(image_name.c_str());
    cv::resize(raw_image, raw_image, cv::Size(INPUT_SIZE, INPUT_SIZE)); 
    int raw_image_height = raw_image.rows;
    int raw_image_width  = raw_image.cols; 
    cv::Mat image;
    cv::resize(raw_image, image, cv::Size(INPUT_SIZE, INPUT_SIZE));

    // load and config mnn model
    auto revertor = std::unique_ptr<Revert>(new Revert(model_name.c_str()));
    revertor->initialize();
    auto modelBuffer      = revertor->getBuffer();
    const auto bufferSize = revertor->getBufferSize();
    auto net = std::shared_ptr<MNN::Interpreter>(MNN::Interpreter::createFromBuffer(modelBuffer, bufferSize));
    revertor.reset();
    MNN::ScheduleConfig config;
    config.numThread = threads;
    config.type      = static_cast<MNNForwardType>(forward);
    MNN::BackendConfig backendConfig;
    config.backendConfig = &backendConfig;
    
    auto session = net->createSession(config);
    net->releaseModel();
    
 

    clock_t start = clock();
    // preprocessing
    image.convertTo(image, CV_32FC3);
    image = (image - 123.0) / 58.0;

    // wrapping input tensor, convert nhwc to nchw    
    std::vector<int> dims{1, INPUT_SIZE, INPUT_SIZE, 3};
    auto nhwc_Tensor = MNN::Tensor::create<float>(dims, NULL, MNN::Tensor::TENSORFLOW);
    auto nhwc_data   = nhwc_Tensor->host<float>();
    auto nhwc_size   = nhwc_Tensor->size();
    ::memcpy(nhwc_data, image.data, nhwc_size);

    std::string input_tensor = "data";
    auto inputTensor  = net->getSessionInput(session, nullptr);
    inputTensor->copyFromHostTensor(nhwc_Tensor);

    // run network
    net->runSession(session);

    // get output data
    std::string output_tensor_name0 = "conv5_fwd";

    MNN::Tensor *tensor_lmks  = net->getSessionOutput(session, output_tensor_name0.c_str());

    MNN::Tensor tensor_lmks_host(tensor_lmks, tensor_lmks->getDimensionType());
    
    tensor_lmks->copyToHostTensor(&tensor_lmks_host);

    int batch = tensor_lmks->batch();
    int channel = tensor_lmks->channel();
    int height = tensor_lmks->height();
    int width  = tensor_lmks->width();
    int type   = tensor_lmks->getDimensionType();
    printf("%d, %d, %d, %d, %d\n", batch, channel, height, width, type);

    // post processing steps
    auto lmks_dataPtr  = tensor_lmks_host.host<float>();

    int num_of_pts = 98;
    for (int i = 0; i < num_of_pts; ++i)
    {
        int x = (int) (lmks_dataPtr[i*2 + 0]);
        int y = (int) (lmks_dataPtr[i*2 + 1]);
        cv::circle(raw_image, cv::Point(x, y), 2, cv::Scalar(0,0,255), -1);
    }

    cv::imwrite("./output.jpg", raw_image);
    return 0;
}

最后

本文我们结合PFLD人脸关键点算法模型,采用mxnet和MNN分别进行了算法复现及模型部署。在这一过程中,我们对MNN中不支持或可能存在问题的Op进行了裁剪,保证精度的同时进行了Op的替换(Dense换成Conv2D)。最后,欢迎大家留言讨论,关注本专栏及公众号,谢谢大家!

参考

1、https://zhuanlan.zhihu.com/p/73546427

2、https://zhuanlan.zhihu.com/p/79701540

3、https://zhuanlan.zhihu.com/p/75742333


推荐阅读

专注嵌入式端的AI算法实现,欢迎关注作者微信公众号和知乎嵌入式AI算法实现专栏

WX20200305-192544.png

更多嵌入式AI相关的技术文章请关注极术嵌入式AI专栏

推荐阅读
关注数
18838
内容数
1372
嵌入式端AI,包括AI算法在推理框架Tengine,MNN,NCNN,PaddlePaddle及相关芯片上的实现。欢迎加入微信交流群,微信号:aijishu20(备注:嵌入式)
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息