快速入门
在本节中,您可以通过一个简单的图片分类应用了解使用AscendCL接口(C语言接口)开发应用的基本过程以及开发过程中涉及的关键概念。
了解图片分类应用
“图片分类应用”,从名称上,我们也能直观地看出它的作用:标识图片所属的分类。
“图片分类应用”是怎么做到这一点的呢?当然得先有一个能做到图片分类的模型,我们可以直接使用一些训练好的开源模型,也可以基于开源模型的源码进行修改、重新训练,还可以自己基于算法、框架构建适合自己的模型。
鉴于当前我们是入门内容,此处我们直接获取已训练好的开源模型,这种方式相对简单。此处我们选择的是ONNX框架的ResNet-50模型。
ResNet-50模型的基本介绍如下:
- 输入数据:BGR格式、224*224分辨率的输入图片
- 输出数据:图片的类别标签及其对应置信度
- 置信度是指图片所属某个类别可能性。
- 类别标签和类别的对应关系与训练模型时使用的数据集有关,需要查阅对应数据集的标签及类别的对应关系。
前提条件
已完成环境部署,详细步骤请参见准备开发和运行环境。
了解基本概念
- Host
Host指与Device相连接的X86服务器、ARM服务器,会利用Device提供的NN(Neural-Network)计算能力,完成业务。
- Device
- 开发环境、运行环境
开发环境指编译开发代码的环境,运行环境指运行算子、推理或训练等程序的环境,运行环境上必须带昇腾AI处理器。
开发环境和运行环境可以合设在同一台服务器上,也可以分设:
- 合设场景下,登录到合设的服务器上,不用切换开发环境、运行环境。
- 分设场景下,若开发环境和运行环境上的操作系统架构不同,在开发环境中需使用交叉编译,这样编译出来的可执行文件,才可以在运行环境中执行。
您可以登录对应的环境,执行“uname -a”命令查询其操作系统的架构。
- 运行用户:
了解开发过程
AscendCL(Ascend Computing Language)是一套用于在CANN(Compute Architecture for Neural Networks)上开发深度神经网络推理应用的C语言API库,提供模型加载与执行、媒体数据处理、算子加载与执行等API,能够实现在昇腾CANN平台上进行深度学习推理计算、图形图像预处理、单算子加速计算等能力。
了解了这些大步骤后,下面我们再展开来说明开发应用具体涉及哪些关键功能?各功能又使用哪些AscendCL接口,这些AscendCL接口怎么串联?
虽然此时您可能不理解所有细节,但这也不影响,通过快速入门旨在先了解整体的代码逻辑,后续再深入学习,了解其它细节。
创建代码目录
您可以单击Link,获取基础的样例包MyFirstApp_ONNX.zip,以运行用户将该样例包上传至开发环境(例如,存放在/root/demo目录下),先执行“chmod +x MyFirstApp_ONNX.zip”命令增加执行权限,再执行“unzip MyFirstApp_ONNX.zip”命令解压样例包。
该样例包中包含测试图片、测试图片预处理脚本、编译脚本等,如下所示。
MyFirstApp_ONNX ├── data │ ├── dog1_1024_683.jpg // 测试图片 ├── model // 用于存放ResNet-50模型文件 ├── script │ ├── transferPic.py // 将测试图片预处理为符合模型要求的图片 // 包括将*.jpg转换为*.bin,同时将图片从1024*683的分辨率缩放为224*224 ├── src │ ├── CMakeLists.txt // cmake编译脚本 │ ├── main.cpp // 主函数,图片分类功能的实现文件 ├── sample_build.sh // 编译代码的脚本 ├── sample_run.sh // 运行应用的脚本
准备模型
对于开源框架的网络模型,不能直接在昇腾AI处理器上做推理,需要先使用ATC(Ascend Tensor Compiler)工具将开源框架的网络模型转换为适配昇腾AI处理器的离线模型(*.om文件)。
- 以运行用户登录开发环境。
- 执行模型转换。
执行以下命令,将原始模型转换为昇腾AI处理器能识别的*.om模型文件。请注意,执行命令的用户需具有命令中相关路径的可读、可写权限。以下命令中的“<SAMPLE_DIR>”请根据实际样例包的存放目录替换、“<soc_version>”请根据实际昇腾AI处理器版本替换。
cd <SAMPLE_DIR>/MyFirstApp_ONNX/model wget https://obs-9be7.obs.cn-east-2.myhuaweicloud.com/003_Atc_Models/resnet50/resnet50.onnx atc --model=resnet50.onnx --framework=5 --output=resnet50 --input_shape="actual_input_1:1,3,224,224" --soc_version=<soc_version>
各参数的解释如下,详细约束说明请参见《ATC工具使用指南》。
- --model:ResNet-50网络的模型文件的路径。
- --framework:原始框架类型。5表示ONNX。
- --output:resnet50.om模型文件的路径。请注意,记录保存该om模型文件的路径,后续开发应用时需要使用。
- --input_shape:模型输入数据的shape。
- --soc_version:昇腾AI处理器的版本。
开发应用
有了模型,接下来可以使用AscendCL接口加载、执行模型,开发AI应用了。
在开发环境中,打开“MyFirstApp_ONNX/src/main.cpp”文件,在该cpp中添加如下步骤中灰色底纹的代码。
- 定义一个main函数,按照了解开发过程中的开发应用的过程详解,串联整个应用的代码逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
int main() { // 1.定义一个资源初始化的函数,用于AscendCL初始化、运行管理资源申请(指定计算设备) InitResource(); // 2.定义一个模型加载的函数,加载图片分类的模型,后续推理使用,若om文件不在model目录下,请根据实际存放目录修改此处的代码 const char *modelPath = "../model/resnet50.om"; LoadModel(modelPath); // 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++标准库的头文件。在“MyFirstApp_ONNX/src/main.cpp”文件的开头增加如下代码。
1 2 3 4 5 6 7 8
#include "acl/acl.h" #include <iostream> #include <fstream> #include <cstring> #include <map> #include <math.h> using namespace std;
- 资源初始化。
资源初始化包括2部分:
- 调用aclInit接口初始化AscendCL:
使用AscendCL接口开发应用时,必须先初始化AscendCL ,否则可能会导致后续系统内部资源初始化出错,进而导致其它业务异常。
在初始化时,还支持跟推理相关的可配置项(例如,性能相关的采集信息配置),以json格式的配置文件传入AscendCL初始化接口。如果当前的默认配置已满足需求(例如,默认不开启性能相关的采集信息配置),无需修改,可向AscendCL初始化接口中传入nullptr。
- 调用aclrtSetDevice接口指定计算设备。
1 2 3 4 5 6
int32_t deviceId = 0; void InitResource() { aclError ret = aclInit(nullptr); ret = aclrtSetDevice(deviceId); }
有初始化就有去初始化,在确定完成了AscendCL的所有调用之后,或者进程退出之前,需执行资源去初始化,请参见10。
- 调用aclInit接口初始化AscendCL:
- 模型加载。此处通过加载om模型文件来实现模型加载,如何获取om模型文件请参见准备模型。
1 2 3 4 5
uint32_t modelId; void LoadModel(const char* modelPath) { aclError ret = aclmdlLoadFromFile(modelPath, &modelId); }
有加载就有卸载,模型推理结束后,需要卸载模型,请参见8。
- 将测试图片读入内存,再传输到Device侧,供推理使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
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类型的数据。
图3 aclmdlDataset类型与aclDataBuffer类型的关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
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); }
- 使用aclmdlDesc类型的数据描述模型基本信息(例如输入/输出的个数、名称、数据类型、Format、维度信息等)。
- 处理模型推理的结果数据,在屏幕上显示图片的top5置信度的类别编号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
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++; } // do data processing with softmax and print top 5 classes double totalValue=0.0; for (auto it = resultMap.begin(); it != resultMap.end(); ++it) { totalValue += exp(it->first); } 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, exp(it->first) /totalValue); } }
- 卸载模型,并释放模型描述信息。推理结束,需及时释放模型描述信息、卸载模型。
1 2 3 4 5 6 7
void UnloadModel() { // 释放模型描述信息 aclmdlDestroyDesc(modelDesc); // 卸载模型 aclmdlUnload(modelId); }
- 释放内存、销毁推理相关的数据类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
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; }
- 资源释放。在确定完成了AscendCL的所有调用之后,或者进程退出之前,需调用如下接口实现计算设备释放,AscendCL去初始化。
1 2 3 4 5
void DestroyResource() { aclError ret = aclrtResetDevice(deviceId); aclFinalize(); }
编译及运行应用
- 编译代码。
以运行用户登录开发环境,切换到MyFirstApp_ONNX目录下,执行以下命令。
如下为设置环境变量的示例,<SAMPLE_DIR>表示样例所在的目录,$HOME/Ascend/ascend-toolkit表示CANN软件包的安装路径,<arch-os>中arch表示操作系统架构(需根据运行环境的架构选择),os表示操作系统(需根据运行环境的操作系统选择)。
export APP_SOURCE_PATH=<SAMPLE_DIR>/MyFirstApp_ONNX export DDK_PATH=$HOME/Ascend/ascend-toolkit/latest export NPU_HOST_LIB=$HOME/Ascend/ascend-toolkit/latest/<arch-os>/devlib chmod +x sample_build.sh ./sample_build.sh
- 如果执行脚本报错“ModuleNotFoundError: No module named 'PIL'”,是由于开发环境中缺少Pillow库,需使用pip3 install Pillow --user命令安装Pillow库后,再执行脚本。
- 如果执行脚本报错“bash: ./sample_build.sh: /bin/bash^M: bad interpreter: No such file or directory”,是由于开发环境中没有安装dos2unix包,需使用sudo apt-get install dos2unix命令安装dos2unix包后,执行dos2unix sample_build.sh,然后再执行脚本。
- 运行应用。
以运行用户将MyFirstApp_ONNX目录上传至运行环境,以运行用户登录运行环境,切换到MyFirstApp_ONNX目录下,执行以下命令。
chmod +x sample_run.sh ./sample_run.sh
终端上屏显的结果如下,index表示类别标识、value表示该分类的最大置信度:
1 2 3 4 5
top 1: index[162] value[0.954676] top 2: index[161] value[0.033442] top 3: index[166] value[0.006534] top 4: index[167] value[0.004561] top 5: index[163] value[0.000315]
类别标签和类别的对应关系与训练模型时使用的数据集有关,本样例使用的模型是基于imagenet数据集进行训练的,您可以在互联网上查阅对应数据集的标签及类别的对应关系。
当前屏显信息中的类别标识与类别的对应关系如下:
"162": ["beagle"]
"161": ["basset", "basset hound"]
"166": ["Walker hound", "Walker foxhound"]
"167": ["English foxhound"]
"163": ["bloodhound", "sleuthhound"]