体系化学习AscendCL应用,目标为对昇腾CANN有初步了解并且可以基于昇腾CANN独立开发一个CV类图片应用。

【2023 · CANN训练营第一季】应用开发深入讲解① AscendCL概述

【2023 · CANN训练营第一季】应用开发深入讲解② 华为弹性云服务器(ECS)搭建介绍

【2023 · CANN训练营第一季】应用开发深入讲解③ 快速入门(基于ResNet-50网络模型的图片分类应用)

【2023 · CANN训练营第一季】应用开发深入讲解④ 模型转换(ATC工具)


        通过本部分内容,由一个简单的图片分类应用了解使用AscendCL接口开发应用的基本过程以及开发过程中涉及的关键概念。

目录

1 开发流程

2 图片分类应用

环境准备

下载样例

准备模型

● 获取ResNet-50开源模型

● 执行模型转换

准备测试图片

编译及运行应用

● 编译代码

● 运行应用

3 应用源码


1 开发流程

应用开发流程图

1、准备环境

        安装CANN开发或运行环境

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();
}

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐