基于昇腾CANN的推理应用开发——图片分类应用(C&C++)
前情提要
通过一个简单的图片分类应用了解使用 AscendCL 接口开发应用的基本过程以及开发过程中涉及的关键概念。
什么是图片分类应用?
“图片分类应用”,从名称上,我们也能直观地看出它的作用:按图片所属的类别来区分图片。
但“图片分类应用”是怎么做到这一点的呢?当然得先有一个能做到图片分类的模型,我们可以直接使用一些训练好的开源模型,也可以基于开源模型的源码进行修改、重新训练,还可以自己基于算法、框架构建适合自己的模型。
考虑到不同受众,这里做了简化,我们直接获取已训练好的开源模型,毕竟这种最简单、最快。此处我们选择的是Caffe框架的ResNet-50模型。
ResNet-50模型的基本介绍如下:
-
输入数据:RGB格式、224*224分辨率的输入图片
-
输出数据:图片的类别标签及其对应置信度
说明:
- 置信度是指图片所属某个类别可能性。
- 类别标签和类别的对应关系与训练模型时使用的数据集有关,需要查阅对应数据集的标签及类别的对应关系。
了解基本概念
-
Host
Host指与Device相连接的X86服务器、ARM服务器,会利用Device提供的NN(Neural-Network )计算能力,完成业务。
-
Device
Device指安装了昇腾AI处理器SoC的硬件设备,利用PCIe接口与Host侧连接,提供NN计算能力。
了解开发过程
AscendCL(Ascend Computing Language)是一套用于在昇腾平台上开发深度神经网络推理应用的C语言API库,提供模型加载与执行、媒体数据处理、算子加载与执行等API,能够实现在昇腾CANN平台上进行深度学习推理计算、图形图像预处理、单算子加速计算等能力。
了解了这些大步骤后,下面我们再展开来说明开发应用具体涉及哪些关键功能?各功能又使用哪些AscendCL接口,这些AscendCL接口怎么串联?
虽然此时您可能不理解所有细节,但这也不影响,通过快速入门旨先了解整体的代码逻辑,后续再深入学习,了解其它细节。
代码目录介绍
代码目录中包含测试图片、ResNet-50原始模型、编译脚本、代码文件等,如下所示。
MyFirstApp
├── data
├── dog1_1024_683.bin // 测试图片
├── model
├── resnet50.om // 适配昇腾AI处理器的ResNet-50模型文件
├── src
├── CMakeLists.txt // 编译脚本
├── main.cpp // 主函数,图片分类功能的实现文件
├── out
├── main // 应用的可执行文件
开发应用
- 定义一个main函数,按照官方参考文档中的开发应用的过程详解,串联整个应用的代码逻辑。
int main()
{
// 1.定义一个资源初始化的函数,用于AscendCL初始化、运行管理资源申请(指定计算设备)
InitResource();
// 2.定义一个模型加载的函数,加载图片分类的模型,用于后续推理使用
const char *mdoelPath = "../model/resnet50.om";
LoadModel(mdoelPath);
// 3.定义一个读图片数据的函数,将测试图片数据读入内存,并传输到Device侧,用于后续推理使用
const char *picturePath = "../data/dog1_1024_683.bin";
LoadPicture(picturePath);
// 4.定义一个推理的函数,用于执行推理
Inference();
// 5.定义一个推理结果数据处理的函数,用于在终端上屏显测试图片的top5置信度的类别编号
PrintResult();
// 6.定义一个模型卸载的函数,卸载图片分类的模型
UnloadModel();
// 7.定义一个函数,用于释放内存、销毁推理相关的数据类型,防止内存泄露
UnloadPicture();
// 8.定义一个资源去初始化的函数,用于AscendCL去初始化、运行管理资源释放(释放计算设备)
DestroyResource();
}
了解总体的代码逻辑后,接下来开始写各自定义函数的实现,以下函数的实现请按顺序添加在main函数之前。
- include依赖的头文件,包括AscendCL的、C或C++标准库的头文件。
#include "acl/acl.h"
#include <iostream>
#include <fstream>
#include <cstring>
#include <map>
using namespace std;
- 资源初始化。
使用AscendCL接口开发应用时,必须先初始化AscendCL ,否则可能会导致后续系统内部资源初始化出错,进而导致其它业务异常。
在初始化时,还支持跟推理相关的可配置项(例如,性能相关的采集信息配置),以json格式的配置文件传入AscendCL初始化接口。如果当前的默认配置已满足需求(例如,默认不开启性能相关的采集信息配置),无需修改,可向AscendCL初始化接口中传入nullptr。
int32_t deviceId = 0;
void InitResource()
{
aclError ret = aclInit(nullptr);
ret = aclrtSetDevice(deviceId);
}
有初始化就有去初始化,在确定完成了AscendCL的所有调用之后,或者进程退出之前,需调用接口实现AscendCL去初始化,具体代码请参考相应版本的官方文档。
-
模型加载。
此处加载的是ResNet-50模型文件(*.om文件)。
uint32_t modelId;
void LoadModel(const char* modelPath)
{
aclError ret = aclmdlLoadFromFile(modelPath, &modelId);
}
有加载就有卸载,模型推理结束后,需要卸载模型。
- 将测试图片读入内存,在传输到Device侧,供推理使用。
size_t pictureDataSize = 0;
void *pictureHostData;
void *pictureDeviceData;
//申请内存,使用C/C++标准库的函数将测试图片读入内存
void ReadPictureTotHost(const char *picturePath)
{
string fileName = picturePath;
ifstream binFile(fileName, ifstream::binary);
binFile.seekg(0, binFile.end);
pictureDataSize = binFile.tellg();
binFile.seekg(0, binFile.beg);
aclError ret = aclrtMallocHost(&pictureHostData, pictureDataSize);
binFile.read((char*)pictureHostData, pictureDataSize);
binFile.close();
}
//申请Device侧的内存,再以内存复制的方式将内存中的图片数据传输到Device
void CopyDataFromHostToDevice()
{
aclError ret = aclrtMalloc(&pictureDeviceData, pictureDataSize, ACL_MEM_MALLOC_HUGE_FIRST);
ret = aclrtMemcpy(pictureDeviceData, pictureDataSize, pictureHostData, pictureDataSize, ACL_MEMCPY_HOST_TO_DEVICE);
}
void LoadPicture(const char* picturePath)
{
ReadPictureTotHost(picturePath);
CopyDataFromHostToDevice();
}
-
执行推理。
在调用AscendCL接口进行模型推理时,模型推理有输入、输出数据,输入、输出数据需要按照AscendCL规定的数据类型存放。相关数据类型如下:
-
使用aclmdlDesc类型的数据描述模型基本信息(例如输入/输出的个数、名称、数据类型、Format、维度信息等)。
模型加载成功后,用户可根据模型的ID,调用该数据类型下的操作接口获取该模型的描述信息,进而从模型的描述信息中获取模型输入/输出的个数、内存大小、维度信息、Format、数据类型等信息。
-
使用aclDataBuffer类型的数据来描述每个输入/输出的内存地址、内存大小。
调用aclDataBuffer类型下的操作接口获取内存地址、内存大小等,便于向内存中存放输入数据、获取输出数据。
-
使用aclmdlDataset类型的数据描述模型的输入/输出数据。
模型可能存在多个输入、多个输出,调用aclmdlDataset类型的操作接口添加多个aclDataBuffer类型的数据。
-
aclmdlDataset *inputDataSet;
aclDataBuffer *inputDataBuffer;
aclmdlDataset *outputDataSet;
aclDataBuffer *outputDataBuffer;
aclmdlDesc *modelDesc;
size_t outputDataSize = 0;
void *outputDeviceData;
// 准备模型推理的输入数据结构
void CreateModelInput()
{
// 创建aclmdlDataset类型的数据,描述模型推理的输入
inputDataSet = aclmdlCreateDataset();
inputDataBuffer = aclCreateDataBuffer(pictureDeviceData, pictureDataSize);
aclError ret = aclmdlAddDatasetBuffer(inputDataSet, inputDataBuffer);
}
// 准备模型推理的输出数据结构
void CreateModelOutput()
{
// 创建模型描述信息
modelDesc = aclmdlCreateDesc();
aclError ret = aclmdlGetDesc(modelDesc, modelId);
// 创建aclmdlDataset类型的数据,描述模型推理的输出
outputDataSet = aclmdlCreateDataset();
// 获取模型输出数据需占用的内存大小,单位为Byte
outputDataSize = aclmdlGetOutputSizeByIndex(modelDesc, 0);
// 申请输出内存
ret = aclrtMalloc(&outputDeviceData, outputDataSize, ACL_MEM_MALLOC_HUGE_FIRST);
outputDataBuffer = aclCreateDataBuffer(outputDeviceData, outputDataSize);
ret = aclmdlAddDatasetBuffer(outputDataSet, outputDataBuffer);
}
// 执行模型
void Inference()
{
CreateModelInput();
CreateModelOutput();
aclError ret = aclmdlExecute(modelId, inputDataSet, outputDataSet);
}
- 处理模型推理的结果数据,打屏显示图片的top5置信度的类别编号。
void *outputHostData;
void PrintResult()
{
// 获取推理结果数据
aclError ret = aclrtMallocHost(&outputHostData, outputDataSize);
ret = aclrtMemcpy(outputHostData, outputDataSize, outputDeviceData, outputDataSize, ACL_MEMCPY_DEVICE_TO_HOST);
// 将内存中的数据转换为float类型
float* outFloatData = reinterpret_cast<float *>(outputHostData);
// 屏显测试图片的top5置信度的类别编号
map<float, unsigned int, greater<float>> resultMap;
for (unsigned int j = 0; j < outputDataSize / sizeof(float);++j)
{
resultMap[*outFloatData] = j;
outFloatData++;
}
int cnt = 0;
for (auto it = resultMap.begin();it != resultMap.end();++it)
{
if(++cnt > 5)
{
break;
}
printf("top %d: index[%d] value[%lf] \n", cnt, it->second, it->first);
}
}
-
卸载模型,并释放模型描述信息。
推理结束,需及时释放模型描述信息、卸载模型。
void UnloadModel()
{
// 释放模型描述信息
aclmdlDestroyDesc(modelDesc);
// 卸载模型
aclmdlUnload(modelId);
}
- 释放内存、销毁推理相关的数据类型。
void UnloadPicture()
{
aclError ret = aclrtFreeHost(pictureHostData);
pictureHostData = nullptr;
ret = aclrtFree(pictureDeviceData);
pictureDeviceData = nullptr;
aclDestroyDataBuffer(inputDataBuffer);
inputDataBuffer = nullptr;
aclmdlDestroyDataset(inputDataSet);
inputDataSet = nullptr;
ret = aclrtFreeHost(outputHostData);
outputHostData = nullptr;
ret = aclrtFree(outputDeviceData);
outputDeviceData = nullptr;
aclDestroyDataBuffer(outputDataBuffer);
outputDataBuffer = nullptr;
aclmdlDestroyDataset(outputDataSet);
outputDataSet = nullptr;
}
10.资源释放。
在确定完成了AscendCL的所有调用之后,或者进程退出之前,需调用如下接口实现计算设备释放,AscendCL去初始化。
void DestroyResource()
{
aclError ret = aclrtResetDevice(deviceId);
aclFinalize();
}
运行结果为:
在提示信息中,index表示类别标识、value表示该分类的最大置信度。
说明:
类别标签和类别的对应关系与训练模型时使用的数据集有关,本样例使用的模型是基于imagenet数据集进行训练的,您可以在互联网上查阅对应数据集的标签及类别的对应关系。
当前屏显信息中的类别标识与类别的对应关系如下:
“161”: [“basset”, “basset hound”]
“162”: [“beagle”]
“163”: [“bloodhound”, “sleuthhound”]
“166”: [“Walker hound”, “Walker foxhound”]
“167”: [“English foxhound”]
好了,到此基本结束,总的来说,CANN的具体接口与相应硬件和对应版本有关,但大体流程和思路是一样的,注意细节和接口就好,还是多看看文档吧。
- 点赞
- 收藏
- 关注作者
评论(0)