YOLOV7-tiny模型,从Pytorch移植到华为Atlas 200i DK A2平台的Ascend推理
笔者按:
最近接了一个项目,要把别人训练的YOLOv7-tiny模型,移植到华为Atlas平台,利用Ascend昇腾NPU做推理,同时实现了python和c++的应用。由于笔者是第一次接触华为这个平台,移植过程中还是踩了不少坑的,这里开一篇文章,介绍整个移植的流程、踩过的坑和debug的思路。
整个项目大约花费了我2周时间,这是在我有一定的python、c++和工程经验的基础上。另外当然生成式AI在这个过程中也帮我做了不少事情,不得不感慨科技的进步。
这里不得不吐槽一下,华为Ascend平台的文档和Sample虽然很全面,但是作为一个初上手的人,真的很难知道从哪入手。直接啃API文档肯定不现实,所以肯定还是要找一个和自己的应用比较类似的Sample,从这个sample入手,修改代码,实现自己的目的。但Sample也各有不同,在硬件文档中、cann文档中,都有各自的sample;在官方sample中,有python的、c++的,c++中还有基于acl的和acllite的,这acllite和acl有啥区别?真的很难弄懂……
开这篇文章主要是记录一下我的历程,所以比较偏向记录,不喜勿喷……
Ascend的Sample地址:https://gitee.com/Ascend/samples
本文参考的Sample:
samples/inference/modelInference/sampleYOLOV7
samples/inference/modelInference/sampleResnetQuickStart
samples/inference/modelInference/sampleResnetAIPP
samples/inference/acllite/cplusplus
以及Atlas 200i DK A2文档中的目标检测sample
目录
0 Start Up
拿到项目,我首先确定了Python移植 -> C++移植的路线,这肯定是出于Python的debug易用性考量的。
基础的配环境的部分就不讲了,主要安装了CANN(对应CUDA)和MindStudio(IDE),配了环境变量,还装了如OpenCV等一系列库。注意为了避免错误,这些库我都是下载的源码,然后手动编译的。Atlas平台的cpu架构是ARM指令集的,所以各种包编译的时候也要编译ARM的版本。
最后是选择一个可以快速启动的sample。在Atlas A200i DK平台的官方文档中就有一个目标检测的sample,这里自然是以它为基础,开始我们的移植。
1 Python移植和模型转换
这个sample是以YOLOV5为基础的一个例子。简单阅读代码,发现主要需要操作的部分都在YOLOV5.infer()这个函数里面。
def infer(self, img_bgr):
labels_dict = get_labels_from_txt('./coco_names.txt') # 得到类别信息,返回序号与类别对应的字典
# 数据前处理
img, scale_ratio, pad_size = letterbox(img_bgr, new_shape=[640, 640]) # 对图像进行缩放与填充
img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, HWC to CHW
img = np.ascontiguousarray(img, dtype=np.float32) / 255.0 # 转换为内存连续存储的数组
# 模型推理, 得到模型输出
output = self.execute([img, ])[0]
# 后处理
boxout = nms(torch.tensor(output), conf_thres=0.4, iou_thres=0.5) # 利用非极大值抑制处理模型输出,conf_thres 为置信度阈值,iou_thres 为iou阈值
pred_all = boxout[0].numpy() # 转换为numpy数组
scale_coords([640, 640], pred_all[:, :4], img_bgr.shape, ratio_pad=(scale_ratio, pad_size)) # 将推理结果缩放到原始图片大小
img_dw = draw_bbox(pred_all, img_bgr, (0, 255, 0), 2, labels_dict) # 画出检测框、类别、概率
return img_dw
整个流程就是:读图像,重整,推理,后处理,就这么简单。
那就好说了,在主程序中把模型换成自己的模型,修改输入长宽……等等,输入的模型是.om格式的?没见过啊?
赶紧在文档里翻了翻,看到了.om模型是Atlas平台的acl推理工具支持的输入模型。好在华为提供了官方的工具来将模型转换为.om模型。但蛋疼的是,这个模型支持caffee,tensorflow,onnx等等,却唯独不支持pytorch……
所以我们唯一的方法就是,将pytorch模型转换到onnx,再用Atlas平台的ATC工具,将onnx模型转换到om格式。
好在YOLO模型的仓库中都提供了export.py的脚本文件,可以将YOLO模型转换到ONNX格式输出,于是运行
python3 export.py
当然,模型转换没这么容易成功。要不就是导出onnx不能成功,要不就是onnx转换om不能成功,要么就是转换都成功了推理输出不对。
这个时候,要仔细观察两边的代码。在yolo的推理代码中,可以看到,是先通过模型执行了推理,然后再进行了nms操作:

而在我们的sample中,也是这么个流程
那我们只要保证模型的输出out和output的shape一致,基本上就对了。
通过对原模型的推理脚本设置断点,我们可以看到,out.shape=(batch, 25200, cls+5)
那么,我们要在export.py的脚本中,逐步去debug,要到在这个脚本找到,哪个步骤输出的tensor,是和上述形状一致的。
export.py脚本的输入参数非常多:

部分参数的说明也不太清楚,这里呢就是一个不断尝试的过程了。具体的方法就是在test.py脚本和export.py脚本中同时设置断点,要将test.py中的模型输出和export.py中的模型输出对齐。
上面说过,由于我们的NMS部分是独立于推理过程的,所以有关于nms的部分都不要设置,这包括"max_wh","topk_all", "iou_thresh", "conf_thresh"和"include-nms"。至于这个“end2end”参数,我最终也没太清楚它是干嘛的。另外,dynamic相关的参数,用于控制模型输入的batch和尺寸是否不变,这里先都不要设置。
总之,我这里最终输出合适的模型的命令是:
python export.py --weights xxxx.pt --grid
导出onnx模型后,将其传到Atlas平台上,通过atc命令转换为om模型:
atc --model=xxxx.onnx --framework=5 --output=xxxx --soc_version=<soc_version>
这里的<soc_version>需要用命令
npu-smi info
来查看,会给出一个类似

的输出,其中红框框出来的部分就是soc_version参数了。注意这里不能简单写310B4,而应该写Ascend310B4。
在拿到输出模型后,把sample中的模型路径换成输出的模型,debug,发现模型的推理输出变成了一个长度为4的list,其中这个list[0].shape=(1, 25200, cls+5),那么这个就是我们需要拿来nms的输出了。
sample中给出的nms函数和我们原本的测试脚本肯定不太一致,这里直接把原脚本中的nms函数复制过来,替换本身的sample中的nms函数,对输出进行处理。
简单找了两张图像进行验证,再写一个画target的函数。OK,输出没问题,感觉良好!
2 尝试CPP移植
为了实现更好的速度和内存控制,项目方还要求通过纯c++进行部署。我们这里还是一样,先找一个sample,然后在这个sample的基础上进行修改。
上面给出的sample中,modelInference中有很多,我们可以从其中的sampleResnetQuickStart开始,看一下在C++中整个推理流程是怎么走的。
代码放在src/sampleResnetQuickStart.cpp中,整个流程是初始化->读取数据->前处理->推理->拿结果。
其中初始化的部分我们通常不需要关心,只要从读取数据的部分开始看就好。
读取数据
读取数据的部分比较简单。
Result SampleResnetQuickStart::ProcessInput(const string testImgPath)
{
// read image from file by cv
imagePath = testImgPath;
srcImage = imread(testImgPath);
Mat resizedImage;
// zoom image to modelWidth_ * modelHeight_
resize(srcImage, resizedImage, Size(modelWidth_, modelHeight_));
// get properties of image
int32_t channel = resizedImage.channels();
int32_t resizeHeight = resizedImage.rows;
int32_t resizeWeight = resizedImage.cols;
// data standardization
float meanRgb[3] = {min_chn_2, min_chn_1, min_chn_0};
float stdRgb[3] = {var_reci_chn_2, var_reci_chn_1, var_reci_chn_0};
// create malloc of image, which is shape with NCHW
imageBytes = (float*)malloc(channel * resizeHeight * resizeWeight * sizeof(float));
memset(imageBytes, 0, channel * resizeHeight * resizeWeight * sizeof(float));
uint8_t bgrToRgb=2;
// image to bytes with shape HWC to CHW, and switch channel BGR to RGB
for (int c = 0; c < channel; ++c)
{
for (int h = 0; h < resizeHeight; ++h)
{
for (int w = 0; w < resizeWeight; ++w)
{
int dstIdx = (bgrToRgb - c) * resizeHeight * resizeWeight + h * resizeWeight + w;
imageBytes[dstIdx] = static_cast<float>((resizedImage.at<cv::Vec3b>(h, w)[c] -
1.0f*meanRgb[c]) * 1.0f*stdRgb[c] );
}
}
}
return SUCCESS;
}
可以看到,代码首先通过opencv的方法读入了图像,并且resize到模型要求的输入长宽。随后申请了一块channel * resizeHeight * resizeWeight * sizeof(float)类型的内存,用于存放图像数据。
随后,将数据逐个放到内存的指定位置。这里需要注意,代码中用了三个技巧:
- 代码将BGR的通道排列顺序倒转成了RGB。因为在用OpenCV读取的图像,其通道顺序默认是BGR排列的。而用Pytorch推理时,则通常用到的是RGB顺序。
- 代码对图像值进行了normalization处理。这部分其实要根据自己的模型来看,如果你的模型训练的时候就是输入的标准化数据,那这样做也无妨。如果训练时输入的是0~1的归一化数据,那么这里用。从性能上来说,用标准化的数据输入,应该对性能有一定帮助?
- 代码将data的HWC的维度顺序调整到了CHW的顺序,这更符合PyTorch中的输入格式。在OpenCV中,读入的数据在data指针中是按照通道 -> 列 -> 行的顺序来存储的,即,首先是第一行第一列的R、G、B数据,然后是第一行第二列的R、G、B数据,以此类推。转换成CHW顺序即,首先是R通道第一行从左到右的顺序,然后是第二行从左到右的顺序……遍历完R通道后,再到G通道,以此类推。
imageBytes[dstIdx] = static_cast<float>((resizedImage.at<cv::Vec3b>(h, w)[c] / 255.f);
就行。
推理
这部分代码也没有特别需要我们操作的。过。
后处理
目前我们需要的是先让推理输出对齐。所以可以先把后处理部分给注释掉,只看输出部分。
验证
简单过了一遍代码,并把必要的地方做了修改(模型路径、模型height、width、上述读入的float数据归一化处理)后,尝试编译运行代码,果不其然,代码返回了一个很奇怪的数字,139,并且并没有详细的说明。于是这里就卡住了。
3 ACLLITE,我的救命稻草
经过了一系列的debug后,始终没办法找到确切的错误原因,于是我开始去看其它的sample,看看能不能帮上我的忙。
在sample中,除了上面的QuickStart,我还看到了一个sampleAIPP。它和前面的QuickStart从代码上看是差不多的,所以我简单看了下就没再尝试了。这主要还是因为我在sample中看到了一个sampleYOLOV7。我选择这个sample的原因很简单:
- 我要部署的模型是YOLOV7-tiny,两者很相似。
- 这个sample是基于一个叫做acllite的库,这似乎是对acl本身的功能做了一个二次封装,并且代码是开源的,放在了samples/inference/acllite库中。简单看了下acllite,它其中添加了比较多的LOG,这对debug是非常大的帮助。
我现在的首要过程是,让模型跑起来,然后根据错误信息来进行逐步的DEBUG,于是这次我只对模型路径做了修改(我的模型的输入size和sample是一致的),就尝试编译运行了。
第一次运行的时候,提示Only baseline Jpeg supported,我查了一下,这是由于我的数据集图片是渐进式jpeg。于是我写了个脚本,把所有的渐进式jpeg图像改成了baseline jpeg(非常简单,用ffmpeg批量处理或者用opencv读取再写入就可以)。
输入整理
解决了输入问题后,再次运行代码,给出了非常清晰的LOG(大意):
model requires input size of 4,915,200, but 614,400 provided.
这个数字看起来还是非常有零有整的,结合自己的模型要求的输入算一下
height * width * channel * sizeof(float) = 640 * 640 * 3 * 4 = 4,915,200。这里面每个float类型是四个byte,所以后面*4。
但输入的这个614,400是怎么来的呢?我们观察sample代码中的前处理部分:
Result SampleYOLOV7::ProcessInput(string testImgPath)
{
// read image from file
ImageData image;
AclLiteError ret = ReadJpeg(image, testImgPath);
if (ret == FAILED) {
ACLLITE_LOG_ERROR("ReadJpeg failed, errorCode is %d", ret);
return FAILED;
}
// copy image from host to dvpp
ImageData imageDevice;
ret = CopyImageToDevice(imageDevice, image, runMode_, MEMORY_DVPP);
if (ret == FAILED) {
ACLLITE_LOG_ERROR("CopyImageToDevice failed, errorCode is %d", ret);
return FAILED;
}
// image decoded from JPEG format to YUV
ImageData yuvImage;
ret = imageProcess_.JpegD(yuvImage, imageDevice);
if (ret == FAILED) {
ACLLITE_LOG_ERROR("Convert jpeg to yuv failed, errorCode is %d", ret);
return FAILED;
}
// zoom image to modelWidth_ * modelHeight_
ret = imageProcess_.Resize(resizedImage_, yuvImage, modelWidth_, modelHeight_);
if (ret == FAILED) {
ACLLITE_LOG_ERROR("Resize image failed, errorCode is %d", ret);
return FAILED;
}
return SUCCESS;
}
其中有一部分的注释写到: // image decoded from JPEG format to YUV
小学二年级的时候学过,YUV420格式占据的内存空间是:height * width * 1.5 (因为U和V通道都被resize到了原来的0.5倍长宽,那么面积也就是0.25倍。所以加起来是1 + 0.25 + 0.25 = 0.5。取出我们的计算器算一下:
640 * 640 * 1.5 = 614,400
刚好这就对上了。这可以说明,前处理部分的代码,将我的图像变成了uint8类型的YUV420(uint8格式,每一个占1byte的内存),给到了模型。
找到了问题,那我们就逐个去找这个前处理函数做了哪些事,首先是ReadJpeg(),这个函数定义在AclliteUtils.hpp/cpp中:
AclLiteError ReadJpeg(ImageData& image, const std::string& fileName)
{
uint32_t size = 0;
void* buf = nullptr;
ReadBinFile(fileName, buf, size);
int32_t ch = 0;
acldvppJpegGetImageInfo(buf, size, &(image.width), &(image.height), &ch);
if (image.width == 0 || image.height == 0) {
ACLLITE_LOG_ERROR("unsupported format, only Baseline JPEG");
return ACLLITE_ERROR;
}
image.data.reset((uint8_t *)buf, [](uint8_t* p) { delete[](p); });
image.size = size;
return ACLLITE_OK;
}
其中又引入了ReadBinFile(),在同一个文件中:
AclLiteError ReadBinFile(const string& fileName, void*& data, uint32_t& size)
{
struct stat sBuf;
int fileStatus = stat(fileName.data(), &sBuf);
if (fileStatus == -1) {
ACLLITE_LOG_ERROR("failed to get file");
return ACLLITE_ERROR_ACCESS_FILE;
}
if (S_ISREG(sBuf.st_mode) == 0) {
ACLLITE_LOG_ERROR("%s is not a file, please enter a file",
fileName.c_str());
return ACLLITE_ERROR_INVALID_FILE;
}
std::ifstream binFile(fileName, std::ifstream::binary);
if (binFile.is_open() == false) {
ACLLITE_LOG_ERROR("open file %s failed", fileName.c_str());
return ACLLITE_ERROR_OPEN_FILE;
}
binFile.seekg(0, binFile.end);
uint32_t binFileBufferLen = binFile.tellg();
if (binFileBufferLen == 0) {
ACLLITE_LOG_ERROR("binfile is empty, filename is %s", fileName.c_str());
binFile.close();
return ACLLITE_ERROR_INVALID_FILE;
}
binFile.seekg(0, binFile.beg);
uint8_t* binFileBufferData = new(std::nothrow) uint8_t[binFileBufferLen];
if (binFileBufferData == nullptr) {
ACLLITE_LOG_ERROR("malloc binFileBufferData failed");
binFile.close();
return ACLLITE_ERROR_MALLOC;
}
binFile.read((char *)binFileBufferData, binFileBufferLen);
binFile.close();
data = binFileBufferData;
size = binFileBufferLen;
return ACLLITE_OK;
这部分的核心操作就是,读取char格式的文件数据,作为一个uint8类型的智能指针,赋值给image.data。需要注意的是,image是一个ImageData的实例,其data指针定义为一个
std::shared_ptr<uint8_t> data = nullptr;
这部分的操作有些繁琐,如果只是为了读取数据、指针拷贝,我们完全可以写一个类似的函数。并且最关键的,把其中的uint8类型的数据,变为float类型的数据。为此,我们可以写一个ImageDataFloat,再写一个ReadJpegFloat:
struct ImageDataFloat {
acldvppPixelFormat format;
uint32_t width = 0;
uint32_t height = 0;
uint32_t alignWidth = 0;
uint32_t alignHeight = 0;
uint32_t size = 0;
std::shared_ptr<float> data = nullptr;
};
AclLiteError ReadJpegFloat(ImageDataFloat& image, const std::string& fileName, cv::Size modelSize){
cout << " Reading:" << fileName << endl;
cv::Mat imgMat = cv::imread(fileName);
cv::resize(imgMat, imgMat, cv::Size(640, 640));
imgMat.convertTo(imgMat, CV_32FC3, 1.f/255.f);
cv::cvtColor(imgMat, imgMat, cv::COLOR_BGR2RGB);
// 这里把图像的宽、高、通道数读取出来,存到image中
image.width = imgMat.cols;
image.height = imgMat.rows;
int32_t ch = imgMat.channels();
image.size = imgMat.total() * imgMat.elemSize(); // 这里size是以uint_8为单位的(Byte)
if (image.width == 0 || image.height == 0) {
ACLLITE_LOG_ERROR("unsupported format, only Baseline JPEG");
return ACLLITE_ERROR;
}
float* floatBuf = (float*)imgMat.data;
float* buf = new float[image.width * image.height * ch];
uint32_t hw = image.width * image.height;
// 将图像数据从HWC格式转换为CHW格式,并将通道从BGR转换为RGB
for (int c = 0; c < ch; ++c) {
for (int h = 0; h < image.height; ++h) {
for (int w = 0; w < image.width; ++w) {
// cout << "c: " << c << ", h: " << h << ", w: " << w << endl;
buf[c * hw + h * image.width + w] = floatBuf[(h * image.width + w) * 3 + c];
}
}
}
image.data.reset(buf, [](float* p) { delete[](p); });
// image.data = buf;
image.size = image.width * image.height * ch * sizeof(float);
return ACLLITE_OK;
}
这里,我们借用opencv完成了图像的读取并resize到模型的长宽,并且把图像的分布从BGR转换到RGB,从HWC转换到CHW,并且把数据进行了归一化。最后把size从原来的w*h*c变成了w*h*c*4
数据迁移到device
在processInput()函数中,除了ReadJpeg(),第二个调用的就是CopyImageToDevice()了。由于RGB2YUV的部分我们不需要、resize的部分我们也已经在读取的部分完成,所以我们可以注释掉后面的JpegD函数和Resize函数,只关注CopyImageToDevice()的部分。
这个函数顾名思义,应该就像Pytorch中的to(device),把数据拷贝到推理的设备中。其定义如下:
AclLiteError CopyImageToDevice(ImageData& destImage, ImageData& srcImage,
aclrtRunMode curRunMode, MemoryType memType)
{
void* data = CopyDataToDevice(srcImage.data.get(), srcImage.size,
curRunMode, memType);
if (data == nullptr) {
return ACLLITE_ERROR_COPY_DATA;
}
destImage.format = srcImage.format;
destImage.width = srcImage.width;
destImage.height = srcImage.height;
destImage.size = srcImage.size;
destImage.alignWidth = srcImage.alignWidth;
destImage.alignHeight = srcImage.alignHeight;
if (memType == MEMORY_DEVICE) {
destImage.data = SHARED_PTR_DEV_BUF(data);
} else {
destImage.data = SHARED_PTR_DVPP_BUF(data);
}
return ACLLITE_OK;
}
void* CopyDataToDevice(const void* data, uint32_t size,
aclrtRunMode curRunMode, MemoryType memType)
{
if ((data == nullptr) || (size == 0) ||
((curRunMode != ACL_HOST) && (curRunMode != ACL_DEVICE)) ||
(memType >= MEMORY_INVALID_TYPE) || (memType == MEMORY_HOST)) {
ACLLITE_LOG_ERROR("Copy data args invalid, data %p, "
"size %d, src dev %d, memory type %d",
data, size, curRunMode, memType);
return nullptr;
}
aclrtMemcpyKind policy = GetCopyPolicy(curRunMode, TO_DEVICE, memType);
return CopyData(data, size, policy, memType);
}
void* CopyData(const void* data, uint32_t size,
aclrtMemcpyKind policy, MemoryType memType)
{
void* buffer = MallocMemory(size, memType);
if (buffer == nullptr) {
return nullptr;
}
aclError aclRet = aclrtMemcpy(buffer, size, data, size, policy);
if (aclRet != ACL_SUCCESS) {
ACLLITE_LOG_ERROR("Copy data to device failed, aclRet is %d", aclRet);
FreeMemory(buffer, memType);
return nullptr;
}
return buffer;
}
这里面,其实两个二级和三级函数并没涉及到类型的东西,我们不需要关注,只需要看CopyImageToDevice中,出现了SHARED_PTR_DEV_BUF和SHARED_PTR_DVPP_BUF这两个东西。在ActliteUtils.hpp中,可以看到他们的定义
/**
* @brief generate shared pointer of dvpp memory
* @param [in]: buf: memory pointer, malloc by acldvppMalloc
* @return shared pointer of input buffer
*/
#define SHARED_PTR_DVPP_BUF(buf) (shared_ptr<uint8_t>((uint8_t *)(buf), [](uint8_t* p) { acldvppFree(p); }))
/**
* @brief generate shared pointer of device memory
* @param [in]: buf: memory pointer, malloc by acldvppMalloc
* @return shared pointer of input buffer
*/
#define SHARED_PTR_DEV_BUF(buf) (shared_ptr<uint8_t>((uint8_t *)(buf), [](uint8_t* p) { aclrtFree(p); }))
那么这里比较简单,我们只需要重载这个CopyImageToDevice的函数(用我们前面的ImageDataFloat),并且定义两个相似的宏SHARED_PTR_DVPP_BUF_FLOAT和SHARED_PTR_DEV_BUF_FLOAT,用float代替uint8_t即可:
#define SHARED_PTR_DVPP_BUF_FLOAT(buf) (shared_ptr<float>((float *)(buf), [](float* p) { acldvppFree(p); }))
#define SHARED_PTR_DEV_BUF_FLOAT(buf) (shared_ptr<float>((float *)(buf), [](float* p) { aclrtFree(p); }))
AclLiteError CopyImageToDevice(ImageDataFloat& destImage, ImageDataFloat& srcImage,
aclrtRunMode curRunMode, MemoryType memType)
{
void* data = CopyDataToDevice(srcImage.data.get(), srcImage.size,
curRunMode, memType);
if (data == nullptr) {
return ACLLITE_ERROR_COPY_DATA;
}
destImage.format = srcImage.format;
destImage.width = srcImage.width;
destImage.height = srcImage.height;
destImage.size = srcImage.size;
destImage.alignWidth = srcImage.alignWidth;
destImage.alignHeight = srcImage.alignHeight;
if (memType == MEMORY_DEVICE) {
destImage.data = SHARED_PTR_DEV_BUF_FLOAT(data);
} else {
destImage.data = SHARED_PTR_DVPP_BUF_FLOAT(data);
}
return ACLLITE_OK;
}
这样一来,前处理部分就完成了。
推理
推理部分也是一样,我们要检查一下数据的get等等部分,有没有错误。
在model.Execute()函数中,引用了一个GetOutputItem()方法,这个方法中又引用了一些类似上面的宏定义的指针和一些Copy的方法。我们要用同样的思路将uint8_t的部分修改到float的类型。这里不再赘述。
后处理和NMS
在GetResult()函数中,sample实现了自己的后处理和NMS方法。我们首先看后处理部分。在推理结束后,我们拿到了一个std::vector<InferenceOutput>& inferOutputs。这个inferOutputs其实就类似于我们在Pytorch中得到的推理输出。像我的模型,在Pytorch中的输出是一个len=4的list,那么这里的这个vector的长度就是4。而每个输出的内容,要通过inferOutputs[idx].data.get()来获取指针。
在我的模型输出中,我只用到了list[0]的输出来做后续的nms,所以我这里使用以下方式将这个输出变成了一个cv::Mat的实例:
uint32_t outputDataBufId = 0;
float *classBuff = static_cast<float *>(inferOutputs[outputDataBufId].data.get());
// resize the 201600 float* into 25200 * 8
// 25200 rows and 8 cols match the data formation(1 * 25200 * 8)
cv::Mat inferOutputMat(25200, 8, CV_32FC1, classBuff);
由于我已经清楚我的输出的形状,所以这里就直接写死了这个Mat的形状,而data指针引用了前面从推理输出中获得的指针。
后续的confidence threshold和NMS部分就是比较简单的了,不管是用AI工具或者是用其它方法都可以比较简单的完成,在这里不再赘述。
4 精度对齐
在拿到最终的result后,我将输出卸载了一个txt文件里面,并且用cocoeval等工具尝试对模型的性能进行分析。但是我发现模型的AP和AR等指标都有一定的降低(比起移植前降低了约5~10个点),无论怎样尝试confidence threshold和iou threshold,都没法提高指标。
我一度准备放弃,觉得这就是迁移或者模型转换中引入的量化误差。但这是我想起了一个在前面一直被我忽略的细节:
在最开始的Pytorch测试源码中,如果逐步对代码debug,可以看到输入模型的img的size并不是640,而是672。并非是模型设定的输入是672,而是在dataloader的创建过程中,手动通过cv2.copyMakeBorder()为图像的上下都添加了一个灰色边界。也许就是这个边界影响了精度呢?
抱着试一试的态度,我重新按照上述过程转换了一个输入模型要求为672的.om模型,并且把相关的size都进行了改动。在这次输出后,我的精度已经能够和在pytorch下的cuda运行的模型相媲美。
下图为在cuda环境测试的精度:

下图为640的模型的输出精度。可以看到,640模型虽然AP很高,但是mAP很低,部分class的recall甚至降低到了0.8以下。

下图为672的模型输出精度,可以看到,整体精度对比cuda结果,在1%差别上浮动,这才是一个成功的移植结果。

最后,我也跑了一下时间测试,作为参考。我这个模型参数量是10.5G,推理时间是13ms。不得不说,NPU作为专门为推理优化的芯片,其推理算力还是非常强的。
5 模型迁移 & 部署
占坑,晚点更。
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐


所有评论(0)