下载
中文
注册

图像分类应用样例开发介绍(C++)

在本章节介绍基于AscendCL接口如何开发一个基于ResNet-50模型的图像分类样例。

样例介绍

“图片分类应用”,即标识图片所属的分类。

图1 图片分类应用

本例中使用的是Caffe框架的ResNet-50模型。用户可以直接使用训练好的开源模型,也可以基于开源模型的源码进行修改、重新训练,还可以基于算法、框架构建适合的模型。

ResNet-50模型的基本介绍如下:

  • 输入数据:RGB格式、224*224分辨率的输入图片。
  • 输出数据:图片的类别标签及其对应置信度。
  • 置信度是指图片所属某个类别可能性。
  • 类别标签和类别的对应关系与训练模型时使用的数据集有关,需要查阅对应数据集的标签及类别的对应关系。

获取代码

  1. 获取代码文件。

    单击获取链接或使用wget命令获取代码(使用wget时需确保开发者套件能够连接外网),下载代码文件压缩包,以root用户登录开发者套件。

    wget https://ascend-repo.obs.cn-east-2.myhuaweicloud.com/Atlas%20200I%20DK%20A2/DevKit/models/sdk_cal_samples/resnet50_acl_cplusplus.zip
  2. “resnet50_acl_cplusplus.zip”压缩包上传到开发者套件,解压并进入解压后的目录。
    unzip resnet50_acl_cplusplus.zip
    cd resnet50_acl_cplusplus

    代码目录结构如下所示,按照正常开发流程,需要将框架模型文件转换成昇腾AI处理器支持推理的om格式模型文件,鉴于当前是入门内容,用户可直接获取已转换好的om模型进行推理。

    cd resnet50_acl_cplusplus
    ├── data
    │   ├── dog1_1024_683.jpg            // 测试图片
    ├── model
    │   ├── resnet50.om            // om模型文件                  
    ├── script                           
    │   ├── transferPic.py               // 将测试图片预处理为符合模型要求的图片,包括将*.jpg转换为*.bin,同时将图片从1024*683的分辨率缩放为224*224               
    ├── src
    │   ├── CMakeLists.txt              // cmake编译脚本
    │   ├── main.cpp                    // 主函数,图片分类功能的实现文件
    ├── sample_build.sh                  // 编译代码的脚本
    ├── sample_run.sh                    // 运行应用的脚本

代码解析

开发代码过程中,在“resnet50_acl_cplusplus/src/main.cpp”文件中已包含读入数据、前处理、推理、后处理等功能,串联整个应用代码逻辑,此处仅对代码进行解析。

  1. “main.cpp”文件的开头有如下代码,用于导入第三方库与调用AscendCL接口推理所需文件。
    #include "acl/acl.h"
    #include <iostream>
    #include <fstream>
    #include <cstring>
    #include <map>
    
    using namespace std;
  2. 资源初始化。

    使用AscendCL接口开发应用时,必须先初始化AscendCL ,否则可能会导致后续系统内部资源初始化出错,进而导致其它业务异常。

    在初始化时,还支持跟推理相关的可配置项(例如,性能相关的采集信息配置),以json格式的配置文件传入AscendCL初始化接口。如果当前的默认配置已满足需求(例如,默认不开启性能相关的采集信息配置),无需修改,可向AscendCL初始化接口中传入nullptr。
    int32_t deviceId = 0;//指定运行模型的昇腾AI处理器ID,0表示指定0号处理器
    void InitResource()
    {
    	aclError ret = aclInit(nullptr);//调用AscendCL初始化接口
    	ret = aclrtSetDevice(deviceId); //调用指定运行模型的昇腾AI处理器ID的接口
    }
  3. 模型加载。
    uint32_t modelId;
    void LoadModel(const char* modelPath)
    {
    	aclError ret = aclmdlLoadFromFile(modelPath, &modelId);//调用加载模型文件接口,modelId为模型ID的指针,通过接口加载模型后,会为模型ID赋值
    }
  4. 将测试图片读入内存,供推理使用。
    size_t pictureDataSize = 0;
    void *pictureData;
    
    //申请内存,使用C/C++标准库的函数将测试图片读入内存
    void LoadPicture(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(&pictureData, pictureDataSize);
    	binFile.read((char*)pictureData, pictureDataSize);
    	binFile.close();
    }
  5. 执行推理。

    在调用AscendCL接口进行模型推理时,模型推理有输入、输出数据,输入、输出数据需要按照AscendCL规定的数据类型存放。相关数据类型如下:

    • 使用aclmdlDesc类型的数据描述模型基本信息(例如输入/输出的个数、名称、数据类型、Format、维度信息等)。

      模型加载成功后,用户可根据模型的ID,调用该数据类型下的操作接口获取该模型的描述信息,进而从模型的描述信息中获取模型输入/输出的个数、内存大小、维度信息、Format、数据类型等信息。

    • 使用aclDataBuffer类型的数据来描述每个输入/输出的内存地址、内存大小。

      调用aclDataBuffer类型下的操作接口获取内存地址、内存大小等,便于向内存中存放输入数据、获取输出数据。

    • 使用aclmdlDataset类型的数据描述模型的输入/输出数据。

      模型可能存在多个输入、多个输出,调用aclmdlDataset类型的操作接口添加多个aclDataBuffer类型的数据。

      图2 aclmdlDataset与aclDataBuffer的关系
    aclmdlDataset *inputDataSet;
    aclDataBuffer *inputDataBuffer;
    aclmdlDataset *outputDataSet;
    aclDataBuffer *outputDataBuffer;
    aclmdlDesc *modelDesc;
    size_t outputDataSize = 0;
    void *outputData;
    
    // 准备模型推理的输入数据结构
    void CreateModelInput()
    {
        // 创建aclmdlDataset类型的数据,描述模型推理的输入
    	inputDataSet = aclmdlCreateDataset();
    	inputDataBuffer = aclCreateDataBuffer(pictureData, 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(&outputData, outputDataSize, ACL_MEM_MALLOC_HUGE_FIRST);
    	outputDataBuffer = aclCreateDataBuffer(outputData, outputDataSize);
    	ret = aclmdlAddDatasetBuffer(outputDataSet, outputDataBuffer);
    }
    
    // 执行模型
    void Inference()
    {
        CreateModelInput();
    	CreateModelOutput();
    
    	float start;
    	float finish;
    	aclError ret = aclmdlExecute(modelId, inputDataSet, outputDataSet);
    }
  6. 处理模型推理的结果数据,在屏幕上显示图片的top5置信度的类别编号。
    void PrintResult()
    {
        // 将内存中的数据转换为float类型
    	float* outFloatData = reinterpret_cast<float *>(outputData);
    	
        // 屏显测试图片的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);
    	}
    }
  7. 卸载模型,并释放模型描述信息。
    推理结束,需及时释放模型描述信息、卸载模型。
    void UnloadModel()
    {
            // 释放模型描述信息
    	aclmdlDestroyDesc(modelDesc);
            // 卸载模型
    	aclmdlUnload(modelId);
    }
  8. 释放内存、销毁推理相关的数据类型。
    void UnloadPicture()
    {
    	aclError ret = aclrtFreeHost(pictureData);
    	pictureData = nullptr;
    	aclDestroyDataBuffer(inputDataBuffer);
    	inputDataBuffer = nullptr;
    	aclmdlDestroyDataset(inputDataSet);
    	inputDataSet = nullptr;
    	
    	ret = aclrtFree(outputData);
    	outputData = nullptr;
    	aclDestroyDataBuffer(outputDataBuffer);
    	outputDataBuffer = nullptr;
    	aclmdlDestroyDataset(outputDataSet);
    	outputDataSet = nullptr;
    }
  9. 释放资源。
    在确定完成AscendCL所有调用之后,或者进程退出之前,需调用如下接口实现计算设备释放,AscendCL去初始化
    void DestroyResource()
    {
    	aclError ret = aclrtResetDevice(deviceId);
    	aclFinalize();
    }
  10. 定义一个运行main函数。
    int main()
    {	
            // 1.定义一个资源初始化的函数,用于AscendCL初始化、运行管理资源申请(指定计算设备)
    	InitResource();
    	
            // 2.定义一个模型加载的函数,加载图片分类的模型,用于后续推理使用
    	const char *modelPath = "../model/resnet50.om"; //定义模型文件路径,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();
    }

运行推理

  1. 编译代码。

    进入“resnet50_acl_cplusplus”目录下,执行以下命令。

    下为设置环境变量的示例,<SAMPLE_DIR>表示样例所在的目录。

    export APP_SOURCE_PATH=<SAMPLE_DIR>/resnet50_acl_cplusplus
    export DDK_PATH=/usr/local/Ascend/ascend-toolkit/latest
    export NPU_HOST_LIB=/usr/local/Ascend/ascend-toolkit/latest/aarch64-linux/devlib
    chmod +x sample_build.sh
    ./sample_build.sh
    • 如果复制多行命令到MobaXterm执行,可能会出现是否确认执行的提示,单击"OK"即可;
    • 如果执行脚本报错“ModuleNotFoundError: No module named 'PIL'”,则表示缺少Pillow库,请使用pip3 install Pillow --user命令安装Pillow库。
  2. 执行以下命令。
    chmod +x sample_run.sh
    ./sample_run.sh

    终端上屏显的结果如下,index表示类别标识、value表示该分类的最大置信度。

    top 1: index[161] value[0.764648]
    top 2: index[162] value[0.156616]
    top 3: index[167] value[0.038971]
    top 4: index[163] value[0.021698]
    top 5: index[166] value[0.011887]

    类别标签和类别的对应关系与训练模型时使用的数据集有关,本样例使用的模型是基于imagenet数据集进行训练的,用户可以在互联网上查阅对应数据集的标签及类别的对应关系。

    当前屏显信息中的类别标识与类别的对应关系如下:

    "161": ["basset", "basset hound"]

    "162": ["beagle"]

    "163": ["bloodhound", "sleuthhound"]

    "166": ["Walker hound", "Walker foxhound"]

    "167": ["English foxhound"]