本文讲述了用caffe训练的模型使用Arm NN SDK放到Arm自家的处理器上运行的方法。虽然Arm官方给出了例子Deploying a Caffe MNIST model using the Arm NN SDK和ML-examples,但是工程化的文档是没有的,放到Arm环境的过程中是有很多隐晦的问题存在的。
想将下面的代码在target上跑起来需要配置好armnn SDK的环境,可参考文章Configuring the Arm NN SDK build environment for Caffe,这篇文章可以帮助快速配置好环境,避免很多很多的弯路,特别对于Linux下上网不方便的可怜的娃们。
这里给出了对单幅图像进行inference的例子。
头文件armnn_loader.hpp
:
/* 加载单张图片并解码得到其data */
#pragma once
#include<vector>
#include <opencv2/opencv.hpp>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
// Helper struct for loading a image
struct Image
{
unsigned int m_label;
std::vector<float> m_image;
};
// Load a single image
std::unique_ptr<Image> loadImage(std::string image_path, const int label)
{
/* 此处使用OpenCV读取图片,也可以使用其它解码方法 */
cv::Mat img = cv::imread(image_path);
int totalpixel = 0;
const unsigned int image_byte_size = img.channels() * img.rows * img.cols;
std::vector<float> inputImageData;
inputImageData.resize(image_byte_size);
unsigned int countR_o = 0;
unsigned int countG_o = img.rows * img.cols;
unsigned int countB_o = 2 * img.rows * img.cols;
unsigned int step = 1;
/* 逐像素提取data,注意OpenCV中颜色通道的排列顺序是BGR */
for (int i = 0; i != img.rows; ++i)
{
for (int j = 0; j != img.cols; ++j)
{
inputImageData[countR_o] = static_cast<float>(img.data[totalpixel + 2]);
inputImageData[countG_o] = static_cast<float>(img.data[totalpixel + 1]);
inputImageData[countB_o] = static_cast<float>(img.data[totalpixel]);
countR_o += step;
countG_o += step;
countB_o += step;
totalpixel += 3;
}
}
// store image and label in Image
std::unique_ptr<Image> ret(new Image);
ret->m_label = label;
ret->m_image = std::move(inputImageData);
return ret;
}
具体实现armnn_caffe.cpp
:
/* 对单张图片做inference */
/* 用法: ./armnn_caffe image_path image_label
例如: ./armnn_caffe elephant.jpg 2 */
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <memory>
#include <array>
#include <algorithm>
#include "armnn/ArmNN.hpp"
#include "armnn/Exceptions.hpp"
#include "armnn/Tensor.hpp"
#include "armnn/INetwork.hpp"
#include "armnnCaffeParser/ICaffeParser.hpp"
#include "armnn_loader.hpp"
// Helper function to make input tensors
armnn::InputTensors MakeInputTensors(const std::pair<armnn::LayerBindingId,
armnn::TensorInfo>& input,
const void* inputTensorData)
{
return { { input.first, armnn::ConstTensor(input.second, inputTensorData) } };
}
// Helper function to make output tensors
armnn::OutputTensors MakeOutputTensors(const std::pair<armnn::LayerBindingId,
armnn::TensorInfo>& output,
void* outputTensorData)
{
return { { output.first, armnn::Tensor(output.second, outputTensorData) } };
}
int main(int argc, char** argv)
{
// Load a test image and its ground truth label
std::string img_path = argv[1];
const int gtlabel = std::stoi(argv[2]);
std::unique_ptr<Image> input = loadImage(img_path, gtlabel);
if (input == nullptr){return 1;}
// Import the Caffe model. Note: use CreateNetworkFromTextFile for text files.
std::string input_node_name = "data";
std::string output_node_top = "prob";
armnnCaffeParser::ICaffeParserPtr parser = armnnCaffeParser::ICaffeParser::Create();
armnn::INetworkPtr network = parser->CreateNetworkFromBinaryFile("model/ResNet-50_inference.caffemodel",
{ }, // input taken from file if empty
{ output_node_top }); // output node
// Find the binding points for the input and output nodes
armnnCaffeParser::BindingPointInfo inputBindingInfo = parser->GetNetworkInputBindingInfo(input_node_name );
armnnCaffeParser::BindingPointInfo outputBindingInfo = parser->GetNetworkOutputBindingInfo(output_node_top);
// Optimize the network for a specific runtime compute device, e.g. CpuAcc, GpuAcc
armnn::IRuntimePtr runtime = armnn::IRuntime::Create(armnn::Compute::CpuAcc);
armnn::IOptimizedNetworkPtr optNet = armnn::Optimize(*network, runtime->GetDeviceSpec());
// Load the optimized network onto the runtime device
armnn::NetworkId networkIdentifier;
runtime->LoadNetwork(networkIdentifier, std::move(optNet));
// Run a single inference on the test image
const int category = 10; /* 分类类别数目 */
std::array<float, category> output;
armnn::Status ret = runtime->EnqueueWorkload(networkIdentifier,
MakeInputTensors(inputBindingInfo, input->m_image.data()),
MakeOutputTensors(outputBindingInfo, &output[0]));
// Convert 1-hot output to an integer label and print
int label = std::distance(output.begin(), std::max_element(output.begin(), output.end()));
std::cout << "Predicted: " << label << std::endl;
std::cout << " Actual: " << input->m_label << std::endl;
return 0;
}
这里给出了读取图片并得到像素值的送入inference的方法,实际应用中可能会有target上更为合适的解码方法,这里使用OpenCV更为方便,毕竟主题不是解码。
解码部分以及将解码后的数据送入inference都很容易理解,出问题也好定位。工程化的时候容易出问题的地方是:
armnn::INetworkPtr network = parser->CreateNetworkFromBinaryFile("model/ResNet-50_inference.caffemodel",
{ }, // input taken from file if empty
{ output_node_top }); // output node
armnnCaffeParser::BindingPointInfo inputBindingInfo = parser->GetNetworkInputBindingInfo(input_node_name);
armnnCaffeParser::BindingPointInfo outputBindingInfo = parser->GetNetworkOutputBindingInfo(output_node_top);
可能出问题的地方有三个:
第一个是加载的caffemodel,第二个是输入节点,第三个是输出节点,对应这上面三句代码。
训练出来的caffemodel是没有办法直接使用的,送到代码里解析的时候会报错,至少会报错找不到prob(即output_node_top)
,需要对训练出来的caffemodel进行转换,得到inference时候用的caffemodel,转换的方法如下:
import caffe
net = caffe.Net('/home/lg/ResNet-50/ResNet-50_deploy.prototxt', \
'/home/lg/ResNet-50/snapshot/ResNet-50_iter_25000.caffemodel', caffe.TEST)
net.save('/home/lg/ResNet-50/ResNet-50_inference.caffemodel')
此处的caffe是python caffe,编译caffe的时候启用python即可。
有时候官方下载的deploy.prototxt需要进行修改后才能使用,拿ResNet-50-deploy.prototxt举例子,官方ResNet-50-deploy.prototxt开头是:
name: "ResNet-50"
input: "data"
input_dim: 1
input_dim: 3
input_dim: 224
input_dim: 224
应该改为
name: "ResNet-50"
layer {
name: "data"
type: "Input"
top: "data"
input_param {
shape: { dim: 1 dim: 3 dim: 224 dim: 224 }
}
}
也就是说必须明确指定输入层的name和type。name可以随便写,name对应的是输入节点,对应上面第二句代码,也就是说此处的name
必须与代码中的input_node_name
对应。但是type必须写成Input
,因为armnn 只支持Input
。armnn支持的层以及官方测试支持的网络请参考官方说明。
官方ResNet-50-deploy.prototxt结尾是:
layer {
bottom: "fc1000"
top: "prob"
name: "prob"
type: "Softmax"
}
这里的top(不是name)
是输出节点的名字,对应这上面第三句代码,也就是说此处的top
必须与代码中的output_node_top
对应。
也就是说对deploy.prototxt进行修改的时候输入节点的name
与代码中的input_node_name
对应,毕竟name
在Input Layer中就是输入;输出节点的top
与代码中的output_node_top
对应,毕竟top
在Layer中就是输出,只要代码里和deploy.prototxt对应即可。但是其它层的name不要修改,否则做inference的时候没法根据name去找对应层的参数了,如果去做inference结果肯定是错误的。
###编译
由于使用的是交叉编译,所以编译器需要修改,将原makefile中的g++改为target上的g++。
ARMNN_LIB = ../armnn
ARMNN_INC = ../armnn/include
armnn_caffe: armnn_caffe.cpp armnn_loader.hpp
arm-linux-gnueabihf-g++ -O3 -std=c++14 -I$(ARMNN_INC) mnist_caffe.cpp \
-o mnist_caffe -L/usr/local/lib -lprotobuf -L$(ARMNN_LIB) -larmnn -larmnnCaffeParser \
-L$(ARMNN_LIB) -larmnn -larmnnCaffeParser
clean:
-rm -f armnn_caffe
test: armnn_caffe
LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:$(ARMNN_LIB) ./armnn_caffe
由于进行的是交叉编译,因此使用arm-linux-gnueabihf-g++
的时候是没有办法链接到linux下/usr/local/lib
中的库的,因此虽然我们对protobuf成功的进行了make install,仍然是没有办法链接到protobuf的库,诸如libprotobuf.so.15
等,解决的办法是编译的时候加入-L/usr/local/lib -lprotobuf
明确指定链接路径。
arm-linux-gnueabihf-g++ -O3 -std=c++14 -I$(ARMNN_INC) mnist_caffe.cpp -o mnist_caffe -L/usr/local/lib -lprotobuf -L$(ARMNN_LIB) -larmnn -larmnnCaffeParser
需要注意的是使用如下常用的添加库路径的方法是行不通的,将protobuf库路径添加到/etc/ld.so.conf
或是直接export LD_LIBRARY_PATH=protobuf库路径:$LD_LIBRARY_PATH
都无法再交叉编译的时候将protobuf库链接进去,编译的时候会报找不到protobuf库的错误,因为交叉编译器根本不会自动的进入linux系统的/usr/local/lib
目录下去找,所以必须自己在编译语句中指定。
即使对于protobuf编译好之后没有make install成功,也可以正确使用armnn,指定libprotobuf.so.15
的路径即可。
对于armnn SDK调用TensorFlow的方法和调用Caffe的方法如出一辙。
###可能出现的错误
- 出现下面的错误可能是输入图片尺寸和网络要求的输入图片尺寸不一致。例如MobileNetV1要求输入尺寸是224224,如果输入的测试图片尺寸是299299就会报如下的错误。
# ./armnn_caffe elephant.jpg
terminate called after throwing an instance of 'std::out_of_range'
what(): _Map_base::at
Aborted (core dumped)
本文的代码已放到GitHub。