【2023 · CANN训练营第一季】应用开发深入讲解③ 快速入门(基于ResNet-50网络模型的图片分类应用)
通过本部分内容,由一个简单的图片分类应用了解使用AscendCL接口开发应用的基本过程以及开发过程中涉及的关键概念。
体系化学习AscendCL应用,目标为对昇腾CANN有初步了解并且可以基于昇腾CANN独立开发一个CV类图片应用。
【2023 · CANN训练营第一季】应用开发深入讲解① AscendCL概述
【2023 · CANN训练营第一季】应用开发深入讲解② 华为弹性云服务器(ECS)搭建介绍
【2023 · CANN训练营第一季】应用开发深入讲解③ 快速入门(基于ResNet-50网络模型的图片分类应用)
【2023 · CANN训练营第一季】应用开发深入讲解④ 模型转换(ATC工具)
通过本部分内容,由一个简单的图片分类应用了解使用AscendCL接口开发应用的基本过程以及开发过程中涉及的关键概念。
目录
1 开发流程
1、准备环境
2、创建代码目录
在开发应用前,需要先创建目录,用以存放代码文件、编译脚本、测试图片数据、模型文件等。
如下是代码目录示例,供参考:
├App名称
├── model // 该目录下存放模型文件
│ ├── xxxxxx
├── data
│ ├── xxx.jpg // 测试数据
├── inc // 该目录下存放声明函数的头文件
│ ├── xxx.h
├── out // 该目录下存放输出结果
├── src
│ ├── xxx.json // 系统初始化的配置文件
│ ├── CMakeLists.txt // 编译脚本
│ ├── xxx.cpp // 实现文件
3、构建模型
模型推理场景下,必须要有适配昇腾AI处理器的离线模型(*.om文件)。
4、开发应用
(1) AscendCL初始化。使用AscendCL接口开发应用时,必须先调用aclInit接口进行AscendCL初始化,否则可能会导致后续系统内部资源初始化出错,进而导致其它业务异常。
(2) 运行管理资源申请。
(3) 数据传输。
(4) 执行模型推理。若需要处理模型推理的结果,还需要进行数据后处理,例如对于图片分类应用,通过数据后处理从推理结果中查找最大置信度的类别标识。模型推理结束后,需及时释放相关资源。
(5) 所有数据处理结束后,需及时释放运行管理资源。
(6) 执行AscendCL去初始化。
5、编译运行应用
包括模型转换、编译代码、运行应用。
2 图片分类应用
通过上一部分我们了解到,应用的整个流程需要测试图片、模型以及调用AscendCL接口来运行模型进行推理,从而得到结果。
本部分以Caffe ResNet-50网络实现图片分类为例,学习在已具有预训练模型的情况下,如何将该模型转换并部署到昇腾AI处理器上,并进行推理得到图片分类结果:
环境准备
参考【2023 · CANN训练营第一季】应用开发深入讲解② 华为弹性云服务器(ECS)搭建介绍
下载样例
登录服务器后,执行以下指令下载样例:
cd ${HOME}
git clone https://gitee.com/ascend/samples.git
进入到样例目录:
cd ${HOME}/samples/cplusplus/level2_simple_inference/1_classification/resnet50_firstapp
该目录结构如下:
resnet50_firstapp
├── data
│ ├── dog1_1024_683.jpg // 测试图片,需按下文的指导获取图片,放到该目录下
├── model
│ ├── resnet50.caffemodel // ResNet-50网络的预训练模型文件(*.caffemodel)
// 需按下文的指导获取图片,放到该目录下
│ ├── resnet50.prototxt // ResNet-50网络的模型文件(*.prototxt)
// 需按下文的指导获取图片,放到该目录下
├── script
│ ├── transferPic.py // 将测试图片预处理为符合模型要求的图片
// 包括将*.jpg转换为*.bin,同时将图片从1024*683的分辨率缩放为224*224
├── src
│ ├── CMakeLists.txt // 编译脚本
│ ├── main.cpp // 主函数,图片分类功能的实现文件
准备模型
● 获取ResNet-50开源模型
包括模型文件(*.prototxt)和权重文件(*.caffemodel),将其置于/model目录下:
cd model
wget https://modelzoo-train-atc.obs.cn-north-4.myhuaweicloud.com/003_Atc_Models/AE/ATC%20Model/resnet50/resnet50.prototxt --no-check-certificate
wget https://modelzoo-train-atc.obs.cn-north-4.myhuaweicloud.com/003_Atc_Models/AE/ATC%20Model/resnet50/resnet50.caffemodel --no-check-certificate
cd ..
此处在获取文件的命令后需加上“--no-check-certificate”,否则会报错。
● 执行模型转换
执行以下命令(以昇腾310 AI处理器为例),将原始模型转换为昇腾AI处理器能识别的*.om模型文件:
atc --model=model/resnet50.prototxt --weight=model/resnet50.caffemodel --framework=0 --output=model/resnet50 --soc_version=Ascend310
以上命令的参数说明:
- model:ResNet-50网络的模型文件(*.prototxt)的路径。
- weight:ResNet-50网络的预训练模型文件(*.caffemodel)的路径。
- framework:原始框架类型。0表示Caffe。
- output:resnet50.om模型文件的路径。请注意,记录保存该om模型文件的路径,后续开发应用时需要使用。
- soc_version:昇腾AI处理器的版本。进入“CANN软件安装目录/compiler/data/platform_config”目录,".ini"文件的文件名即为昇腾AI处理器的版本,请根据实际情况选择。
准备测试图片
获取图片,将其置于/data目录下:
cd data
wget https://c7xcode.obs.cn-north-4.myhuaweicloud.com/models/aclsample/dog1_1024_683.jpg --no-check-certificate
cd ..
此处获取的是一个jpg格式的图片,与模型对图片的要求不符,我们暂且使用/script目录下的transferPic.py脚本,用于将该测试图片转换为模型要求的图片(RGB格式、分辨率为224*224)。
编译及运行应用
● 编译代码
给编译脚本sample_build.sh添加执行权限:
chmod +x sample_build.sh
sample_build.sh文件内容如下:
model_name="MyFirstApp_build"
#找到输入图片目录
cd ${APP_SOURCE_PATH}/data
#运行python脚本,图片预处理,将其转换为模型要求的格式
python3 ../script/transferPic.py
if [ -d ${APP_SOURCE_PATH}/build/intermediates/host ];then
rm -rf ${APP_SOURCE_PATH}/build/intermediates/host
fi
# 新建临时目录,用以存放编译时的临时文件
mkdir -p ${APP_SOURCE_PATH}/build/intermediates/host
cd ${APP_SOURCE_PATH}/build/intermediates/host
#使用cmake命令,用g++编译器编译
cmake ../../../src -DCMAKE_CXX_COMPILER=g++ -DCMAKE_SKIP_RPATH=TRUE
#执行make命令生成应用的可执行文件
make
if [ $? == 0 ];then
echo "make for app ${model_name} Successfully"
exit 0
else
echo "make for app ${model_name} failed"
exit 1
fi
其中,环境变量{APP_SOURCE_PATH}指定当前应用目录,另外,在执行cmake命令时,还需要两个环境变量来指定AscendCL的头文件以及库文件,在执行编译之前需要先设置环境变量。
APP_SOURCE_PATH:指定当前应用的目录。
DDK_PATH:指定AscendCL接口头文件所在路径。
NPU_HOST_LIB:指定AscendCL接口库文件所在路径。
博主所用镜像的环境变量配置如下,在命令行中执行即可:
export APP_SOURCE_PATH=${HOME}/samples/cplusplus/level2_simple_inference/1_classification/resnet50_firstapp
export DDK_PATH=/usr/local/Ascend/ascend-toolkit/latest
export NPU_HOST_LIB=$DDK_PATH/acllib/lib64/stub
执行编译:
./sample_run.sh
此时报以下错误:
① 文件包含出错
/root/samples/cplusplus/level2_simple_inference/1_classification/resnet50_firstapp/src/main.cpp:1:10: fatal error: acl/acl.h: No such file or directory #include "acl/acl.h" ^~~~~~~~~~~ compilation terminated. CMakeFiles/main.dir/build.make:62: recipe for target 'CMakeFiles/main.dir/main.cpp.o' failed make[2]: *** [CMakeFiles/main.dir/main.cpp.o] Error 1 CMakeFiles/Makefile2:67: recipe for target 'CMakeFiles/main.dir/all' failed make[1]: *** [CMakeFiles/main.dir/all] Error 2 Makefile:129: recipe for target 'all' failed make: *** [all] Error 2 make for app MyFirstApp_build failed查找文档发现是没有找到头文件对应的目录,该应用默认指定acl.h文件在{DDK_PATH}/runtime/include/acl下,然而其中的目录/runtime实际为/acllib,因此需要手动修改cmake文件:
vi src/CMakeLists.txt在45行处将runtime改为acllib即可:
② python脚本运行出错
python运行报错:
Traceback (most recent call last): File "../script/transferPic.py", line 3, in <module> from PIL import Image ModuleNotFoundError: No module named 'PIL'表示缺少Pillow库,使用以下命令安装相关库:
apt install libjpeg-dev zlib1g-dev pip3 install Pillow --user -i https://pypi.tuna.tsinghua.edu.cn/simple/安装Pillow库时默认源下载速度很慢,因此在其后加上-i https://pypi.tuna.tsinghua.edu.cn/simple/来指定使用清华源下载。
再次执行编译即可成功,此时在/out目录下会生成名为main的可执行文件。
● 运行应用
给运行脚本sample_run.sh添加执行权限:
chmod +x sample_run.sh
sample_run.sh文件内容如下:
model_name="MyFirstApp_run"
#进入应用的/out目录
cd ${APP_SOURCE_PATH}/out
#执行main文件
./main
运行脚本:
./sample_run.sh
输出结果如下,其中index表示类别标识、value表示该分类的最大置信度:
top 1: index[161] value[0.763672]
top 2: index[162] value[0.157593]
top 3: index[167] value[0.039215]
top 4: index[163] value[0.021835]
top 5: index[166] value[0.011871]
本样例使用的模型是基于imagenet数据集进行训练的,根据该数据集的标签及类别的对应关系,可以得出输入图片分类为巴塞特犬。
3 应用源码
以下给出/src目录下的文件main.cpp的源码及讲解注释:
#include "acl/acl.h"
#include <iostream>
#include <fstream>
#include <cstring>
#include <map>
using namespace std;
int32_t deviceId_ = 0;
uint32_t modelId;
size_t pictureDataSize = 0;
void *pictureHostData;
void *pictureDeviceData;
aclmdlDataset *inputDataSet;
aclDataBuffer *inputDataBuffer;
//模型输入输出数据结构:
aclmdlDataset *outputDataSet; //描述模型输入输出的集合
aclDataBuffer *outputDataBuffer; //描述模型输入输出的内存地址(aclrtMalloc接口申请的地址)和内存大小(测试图片数据大小)
aclmdlDesc *modelDesc; //描述模型基本信息(模型个数、输入输出名称、数据类型fomrat以及维度等信息)
size_t outputDataSize = 0;
void *outputDeviceData;
void *outputHostData;
// 1. AscendCL初始化、运行管理资源申请(指定计算设备)
void InitResource()
{
aclError ret = aclInit(nullptr); //可传入指定精度,对比度等的参数,此处为简单应用,直接传入nullptr
ret = aclrtSetDevice(deviceId_); //device context stream创建接口未调用,此处只有device,因为不涉及复杂的异步等任务
}
// 2. 加载模型
void LoadModel(const char* modelPath)
{
aclError ret = aclmdlLoadFromFile(modelPath, &modelId);
}
// 3. 将图片数据读入内存
void LoadPicture(const char* picturePath)
{
ReadPictureTotHost(picturePath); //将图片数据传入Host中
CopyDataFromHostToDevice(); //由于推理在Device上进行,因此还需从Host传入Device
}
// 申请内存,使用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); //申请Host内存
binFile.read((char*)pictureHostData, pictureDataSize); //将图片数据读入Host中
binFile.close();
}
// 申请Device侧的内存,再以复制内存的方式将内存中的图片数据传输到Device
void CopyDataFromHostToDevice()
{
aclError ret = aclrtMalloc(&pictureDeviceData, pictureDataSize, ACL_MEM_MALLOC_HUGE_FIRST); //申请Device上的内容
ret = aclrtMemcpy(pictureDeviceData, pictureDataSize, pictureHostData, pictureDataSize, ACL_MEMCPY_HOST_TO_DEVICE); //将Host上的内存数据传输到Device上
}
// 4. 执行推理
void Inference()
{
CreateModelInput(); //构造模型输入数据结构
CreateModelOutput(); //构造模型输出数据结构
aclError ret = aclmdlExecute(modelId, inputDataSet, outputDataSet);
//参数-- modelId:之前加载模型成功后返回的模型ID;inputDataSet/outputDataSet:模型输入/输出数据结构
//推理结果数据会保存在申请的输出数据结构的内存中,对于resnet50模式为类别索引及其对应的置信度
}
// 准备模型推理的输入数据结构
void CreateModelInput()
{
// 创建aclmdlDataset类型的数据,描述模型推理的输入
inputDataSet = aclmdlCreateDataset(); //调用接口,创建aclmdlDataset类型数据
inputDataBuffer = aclCreateDataBuffer(pictureDeviceData, pictureDataSize); //调用接口,创建aclDataBuffer类型数据。由于resnet50模型只有一个输入,因此只需创建一个该类型的数据即可
aclError ret = aclmdlAddDatasetBuffer(inputDataSet, inputDataBuffer); //调用接口,将刚刚创建的inputDataBuffer添加到inputDataSet中
}
// 准备模型推理的输出数据结构
void CreateModelOutput()
{
// 创建模型描述信息
modelDesc = aclmdlCreateDesc();
aclError ret = aclmdlGetDesc(modelDesc, modelId); //调用模型描述相关接口,得到输出数据的大小
// 创建aclmdlDataset类型的数据,描述模型推理的输出
outputDataSet = aclmdlCreateDataset();
// 获取模型输出数据需占用的内存大小,单位为Byte
outputDataSize = aclmdlGetOutputSizeByIndex(modelDesc, 0); //resnet50模型只有一个输出,因此传入0
// 申请输出内存
ret = aclrtMalloc(&outputDeviceData, outputDataSize, ACL_MEM_MALLOC_HUGE_FIRST);
outputDataBuffer = aclCreateDataBuffer(outputDeviceData, outputDataSize);
ret = aclmdlAddDatasetBuffer(outputDataSet, outputDataBuffer);
}
// 5. 在终端上屏显测试图片的top5置信度的类别编号
void PrintResult()
{
//推理在Device上进行,因此输出结果存储在Device上,因此需要将其传入Host
aclError ret = aclrtMallocHost(&outputHostData, outputDataSize); //在Host上申请内存
ret = aclrtMemcpy(outputHostData, outputDataSize, outputDeviceData, outputDataSize, ACL_MEMCPY_DEVICE_TO_HOST); //将Device侧的数据传入Host侧
float* outFloatData = reinterpret_cast<float *>(outputHostData); //将结果数据类型转换为float
map<float, unsigned int, greater<float>> resultMap; //创建map类型数据,包括置信度(float)、类别标识(unsigned int)并按置信度进行降序排列(greater)
for (unsigned int j = 0; j < outputDataSize / sizeof(float);++j)
{
resultMap[*outFloatData] = j; //向map中插入数据,按置信度从大到小插入
outFloatData++;
}
int cnt = 0;
for (auto it = resultMap.begin();it != resultMap.end();++it)
{
if(++cnt > 5) //只取置信度最大的前5个
{
break;
}
printf("top %d: index[%d] value[%lf] \n", cnt, it->second, it->first);
}
}
// 6. 卸载模型
void UnloadModel()
{
aclmdlDestroyDesc(modelDesc);
aclmdlUnload(modelId);
}
// 7. 释放内存、销毁推理相关的数据类型,防止内存泄露
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;
}
// 8. AscendCL去初始化、运行管理资源释放(指定计算设备)
void DestroyResource()
{
aclError ret = aclrtResetDevice(deviceId_);
aclFinalize();
}
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();
}
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐


所有评论(0)