Tengine推理框架之初见
[导读] 前段时间从电子芯吧客白嫖了一块开发板EAIDK310,该平台的一个主要应用就是Tengine是OPEN AI LAB针对于嵌入式终端平台以及终端AI应用场景特点,采用模块化设计为终端人工智能量身打造的高效、简洁、高性能的前端推理计算框架。对于人工智能,本人还是小白,本文仅记录个人学习Tengine的笔记,文章中一定有大量错误,分享笔记也是希望能起到交流求证的作用,让自己能逐步深入了解人工智能的技术知识。
Tengine的特点
AI之于嵌入式应用而言,个人理解存在下面一些痛点:
- 应用问题场景多样性,工业/医疗/智慧城市/智能家居等等
- 嵌入式平台多样性,单片机/DSP/处理器/FPGA等等
- 对于应用开发而言,AI曲高和寡、高深莫测,无法落地
- 很多框架都侧重在算法训练,对于嵌入式部署比较好的方案则相对较少
- ……
Tengine的特点:
- 定位于嵌入式平台推理计算框架
- 支持TensorFlow、Caffe、PyTorch、MXNet、ONNX、PaddlePaddle等流行训练框架。
- 跨芯片平台适配
- 超轻量无依赖,甚至可以在MCU上部署
- 全栈部署移植支持
- …..
Tengine的框架初了解
图片及介绍自Tengine用户手册
Tengine 由六大模块组成: core/operator/serializer/executor/driver/wrapper。
- core : 提供系统的基本组件和功能。
- operator: 在此处定义了基本的 operators 框架, 比如 convolution(卷积), relu,pooling池化 等等。
- serializer: 用来导入已保存的模型。该 serializer 框架可扩展以支持不同的框架模型,包括自定义模型格式。Tengine 可以直接导入 Caffe/ONNX/Tensorflow/MXNet 模型是或者 Tengine 自己的模型。
- executor: 实现运行 graph 和 operators 的代码。 主要实现了计算优化调度,内置了一个计算框架,主要定义了
- driver: 实际硬件的适配器,并通过 HAL API 向设备执行器提供服务。单个 driver 可以创建多个设备。
- wrapper: 为不同的框架提供 API 封装。caffe API wrapper 和 tensorflow API wrapper 现在都可以工作了。
对其中的operator/executor稍加分析:
executor
大致分析了几个源文件:
主要实现了计算框架,实现了调度器、设备、驱动、平台计算优化相应的框架。
operator
对于operator个人理解是基本算子的实现层,比如卷积、GRU(GRU是LSTM网络)、RELU(**函数)等,比如**函数,有哪些呢?sigmoid、tanh、ReLU 、Leaky Relu、RReLU、softsign 、softplus,对于这些概念想深入学习,可以去参考机器学习、深度学习的理论书籍。从实现的角度,都封装为了一些基本的数学处理类,实现了相应的数学数值计算。
Tengine应用代码分析
参考./Tengine/examples/classification.cpp
int main(int argc, char* argv[])
{
gExcName = std::string(argv[0]);
int repeat_count = DEFAULT_REPEAT_CNT;
std::string model_name;
std::string tm_file;
std::string label_file;
std::string image_file;
std::vector<int> hw;
std::vector<float> ms;
int img_h = 0;
int img_w = 0;
float scale = 0.0;
float mean[3] = {-1.0, -1.0, -1.0};
int res;
while((res = getopt(argc, argv, "m:n:t:l:i:g:s:w:r:h")) != -1)
{
switch(res)
{
case 'm':
tm_file = optarg;
break;
case 'l':
label_file = optarg;
break;
case 'i':
image_file = optarg;
break;
case 'g':
hw = ParseString<int>(optarg);
if(hw.size() != 2)
{
std::cerr << "Error -g parameter.\n";
show_usage();
return -1;
}
img_h = hw[0];
img_w = hw[1];
break;
case 's':
scale = strtof(optarg, NULL);
break;
case 'w':
ms = ParseString<float>(optarg);
if(ms.size() != 3)
{
std::cerr << "Error -w parameter.\n";
show_usage();
return -1;
}
mean[0] = ms[0];
mean[1] = ms[1];
mean[2] = ms[2];
break;
case 'r':
repeat_count = std::strtoul(optarg, NULL, 10);
break;
case 'h':
show_usage();
return 0;
default:
break;
}
}
if (tm_file.empty())
{
std::cerr << "Error: Tengine model file not specified!" << std::endl;
show_usage();
return -1;
}
if(image_file.empty())
{
std::cerr << "Error: Image file not specified!" << std::endl;
show_usage();
return -1;
}
if(label_file.empty())
{
label_file = DEFAULT_LABEL_FILE;
std::cout << "Label file not specified, use default [" << label_file << "]." << std::endl;
}
// check input files
if(!check_file_exist(tm_file) || !check_file_exist(label_file) || !check_file_exist(image_file))
return -1;
if(img_h == 0)
{
img_h = DEFAULT_IMG_H;
std::cout << "Image height not specified, use default [" << DEFAULT_IMG_H << "]" << std::endl;
}
if(img_w == 0)
{
img_w = DEFAULT_IMG_W;
std::cout << "Image width not specified, use default [" << DEFAULT_IMG_W << "]" << std::endl;
}
if(scale == 0.0)
{
scale = DEFAULT_SCALE;
std::cout << "Scale value not specified, use default [" << scale << "]" << std::endl;
}
if(mean[0] == -1.0 || mean[1] == -1.0 || mean[2] == -1.0)
{
mean[0] = DEFAULT_MEAN1;
mean[1] = DEFAULT_MEAN2;
mean[2] = DEFAULT_MEAN3;
std::cout << "Mean value not specified, use default [" << mean[0] << ", " << mean[1] << ", " << mean[2] << "]" << std::endl;
}
if(model_name.empty())
model_name = tm_file;
const char* _model_file = model_name.c_str();
const char* _image_file = image_file.c_str();
const char* _label_file = label_file.c_str();
const float* _channel_mean = mean;
tengine::Net somenet;
tengine::Tensor input_tensor;
tengine::Tensor output_tensor;
std::cout << "tengine library version: " << get_tengine_version() << "\n";
if(request_tengine_version("1.0") < 0)
return -1;
std::cout << "\nModel name : " << model_name << "\n"
<< "tengine model file : " << tm_file << "\n"
<< "label file : " << label_file << "\n"
<< "image file : " << image_file << "\n"
<< "img_h, imag_w, scale, mean[3] : " << img_h << " " << img_w << " " << scale << " " << mean[0] << " "
<< mean[1] << " " << mean[2] << "\n";
/@@* load model */
somenet.load_model(NULL, "tengine", _model_file);
/@@* prepare input data */
input_tensor.create(img_w, img_h, 3);
get_input_data(_image_file, (float* )input_tensor.data, img_h, img_w, _channel_mean, scale);
/@@* forward */
somenet.input_tensor(0, 0, input_tensor);
double min_time, max_time, total_time;
min_time = __DBL_MAX__;
max_time = -__DBL_MAX__;
total_time = 0;
for(int i = 0; i < repeat_count; i++)
{
double start_time = get_current_time();
somenet.run();
double end_time = get_current_time();
double cur_time = end_time - start_time;
total_time += cur_time;
if (cur_time > max_time)
max_time = cur_time;
if (cur_time < min_time)
min_time = cur_time;
printf("Cost %.3f ms\n", cur_time);
}
printf("Repeat [%d] min %.3f ms, max %.3f ms, avg %.3f ms\n", repeat_count, min_time, max_time, total_time / repeat_count);
/@@* get result */
somenet.extract_tensor(0, 0, output_tensor);
/@@* after process */
PrintTopLabels(_label_file, (float*)output_tensor.data, 1000);
std::cout << "--------------------------------------\n";
std::cout << "ALL TEST DONE\n";
return 0;
}
对于上述代码,梳理一下有哪些关键点运行Tengine推理引擎:
总结下来:
- 导入模型
- 输入待预测的数据(本例为图像)
- 运行推理引擎,调用run方法
- 获取结果
应用
EAIDK-310板子,具有下面的硬件资源:
厂家已提供了Linux系统源代码以及相应Tengine编译部署使用例子,如果想要基于该硬件平台做一个实际的项目,这里描述下我的一些想法,比如实现一个人脸识别门禁:
有哪些开发工作需要去做呢?
- USB摄像头图像导入,官方有相应的例程。当然也可以自己去做图像数据采集部分程序。比如可以使用libusb很容易将数据采集进控制板
- 门禁控制,比如可以绘制一个继电器控制板,从而控制门禁。对于软件开发而言,需要编写一个GPIO控制的字符设备驱动程序。
- 模型训练。对于模型训练,可以通过学习Tensorflow等有名的框架入手,并结合深度学习理论知识,学习如何训练模型,导出模型
- 系统集成控制软件,按照上述推理例程。
总结一下
本文旨在学习一下Tengine的总体概念,形成对嵌入式AI部署落地的基本概念,梳理一下要实现一个嵌入式AI端侧项目的思路概念,应该从何入手。本人对于人工智能属于小白一枚,文章纯属学习笔记,所以认知错误一定难免。