之前的文章跟大家介绍过如何使用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算法实现专栏。
更多嵌入式AI相关的技术文章请关注极术嵌入式AI专栏。