基于昇腾CANN的推理应用开发——图片分类应用(C&C++)

Tianyi_Li 发表于 2022/06/06 09:57:16 2022/06/06
【摘要】 以图片分类应用(C&C++语言)为例,带您了解使用AscendCL接口开发应用的基本步骤,并了解开发过程涉及的关键概念。

前情提要

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

什么是图片分类应用?

“图片分类应用”,从名称上,我们也能直观地看出它的作用:按图片所属的类别来区分图片。

image.png

但“图片分类应用”是怎么做到这一点的呢?当然得先有一个能做到图片分类的模型,我们可以直接使用一些训练好的开源模型,也可以基于开源模型的源码进行修改、重新训练,还可以自己基于算法、框架构建适合自己的模型。

考虑到不同受众,这里做了简化,我们直接获取已训练好的开源模型,毕竟这种最简单、最快。此处我们选择的是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平台上进行深度学习推理计算、图形图像预处理、单算子加速计算等能力。

image.png

了解了这些大步骤后,下面我们再展开来说明开发应用具体涉及哪些关键功能?各功能又使用哪些AscendCL接口,这些AscendCL接口怎么串联?

虽然此时您可能不理解所有细节,但这也不影响,通过快速入门旨先了解整体的代码逻辑,后续再深入学习,了解其它细节。

代码目录介绍

代码目录中包含测试图片、ResNet-50原始模型、编译脚本、代码文件等,如下所示。

MyFirstApp
   ├── data
       ├── dog1_1024_683.bin  // 测试图片
   ├── model
       ├── resnet50.om  // 适配昇腾AI处理器的ResNet-50模型文件
   ├── src
       ├── CMakeLists.txt  // 编译脚本
       ├── main.cpp // 主函数,图片分类功能的实现文件
   ├── out
       ├── main   // 应用的可执行文件

开发应用

  1. 定义一个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函数之前

  1. include依赖的头文件,包括AscendCL的、C或C++标准库的头文件。
    #include "acl/acl.h"
    #include <iostream>
    #include <fstream>
    #include <cstring>
    #include <map>

    using namespace std;
  1. 资源初始化。

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

在初始化时,还支持跟推理相关的可配置项(例如,性能相关的采集信息配置),以json格式的配置文件传入AscendCL初始化接口。如果当前的默认配置已满足需求(例如,默认不开启性能相关的采集信息配置),无需修改,可向AscendCL初始化接口中传入nullptr。

    int32_t deviceId = 0;
    void InitResource()
    {
        aclError ret = aclInit(nullptr);
        ret = aclrtSetDevice(deviceId);
    }

有初始化就有去初始化,在确定完成了AscendCL的所有调用之后,或者进程退出之前,需调用接口实现AscendCL去初始化,具体代码请参考相应版本的官方文档。

  1. 模型加载。

    此处加载的是ResNet-50模型文件(*.om文件)。

    uint32_t modelId;
    void LoadModel(const char* modelPath)
    {
        aclError ret = aclmdlLoadFromFile(modelPath, &modelId);
    }

有加载就有卸载,模型推理结束后,需要卸载模型。

  1. 将测试图片读入内存,在传输到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();
    }
  1. 执行推理。

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

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

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

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

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

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

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

image.png

 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);
    }
  1. 处理模型推理的结果数据,打屏显示图片的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);
        }
    }
  1. 卸载模型,并释放模型描述信息。

    推理结束,需及时释放模型描述信息、卸载模型。

    void UnloadModel()
    {
        // 释放模型描述信息
        aclmdlDestroyDesc(modelDesc);
        // 卸载模型
        aclmdlUnload(modelId);
    }
  1. 释放内存、销毁推理相关的数据类型。
  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();
    }

运行结果为:

image.png

在提示信息中,index表示类别标识、value表示该分类的最大置信度。

说明:

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

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

“161”: [“basset”, “basset hound”]

“162”: [“beagle”]

“163”: [“bloodhound”, “sleuthhound”]

“166”: [“Walker hound”, “Walker foxhound”]

“167”: [“English foxhound”]

好了,到此基本结束,总的来说,CANN的具体接口与相应硬件和对应版本有关,但大体流程和思路是一样的,注意细节和接口就好,还是多看看文档吧。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区),文章链接,文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:cloudbbs@huaweicloud.com进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。