将模型转为NNIE框架支持的wk模型——以tensorflow框架为例(二)

上一篇介绍了如何将tensorflow转为caffe模型,这一篇将介绍从caffe模型转为nnie框架的wk模型,以及一些模型转换精度下降后的定位方法。

开始之前,首先需要准备RuyiStudio软件,上一篇中有RuyiStudio的链接,参考《HiSVP 开发指南》中的第五章节(RuyiStudio 工具使用指南)进行安装,主要有很多的依赖项,配置依赖项的过程中可以查看ruyi_env_setup中的readme.txt与error.log,按照开发指南的操作很顺畅的安装完毕。

caffe转wk

在进行这一步之前一定要确保转出来的caffe模型与pb模型的推理结果完全一致,否则后续的结果误差定位无法开展。

1. 模型转换的前期准备

参考开发指南的3.5.2节,根据配置文件准备需要的文件。
1.caffe模型
2.模型训练时使用的测试数据集中的20~50张用于数据量化。
3.mean.txt,其中每一行浮点数表示对应的通道均值。
image.png

准备好上面三个文件就可以开始模型转换啦。

2.创建新NNIE项目

打开RuyiStudio.exe,创建一个新的nnie_project。
image.png

写入工程名称,所用芯片,注意选择红框中的参数,由于不同芯片对应的转换工具不同,这里一定要选择自己需要的芯片类型!
image.png

创建完成后,左侧工程目录会出现创建好的项目。
image.png

将第一步准备好的文件拷贝到该工程中,具体的目录结构如下。
image.png

3.填写转换配置文件

双击打开test.cfg,参考开发指南的3.5.2节来配置文件。
参数设置:
marked_prototxt:自动生成,不要对其进行修改。
log_level:选择Function level打印出每一层的量化输出,方便定位精度问题。
batch_num:一般情况下填写 1。
image_type: 一般情况下选择 U8。
RGB_order: 模型推理时图片输入为RGB或者BGR,根据自己的模型训练时的输入进行选择。
image_list:可以通过选择create,选择好需要的图片之后RuyiStudio会自动生成 image_list.txt,不需要自己创建txt。
norm_type: 查看开发指南里的定义,根据实际图像预处理选择减均值与乘方差的操作。该模型每个通道对应一个均值,mean.txt 分别写三个通道的均值,注意按照 RGB_ORDER设定的通道数填写的对应通道的mean,模型转换时会将图像预处理的相关操作加在wk模型里,先减均值后乘以scale。
其他的参数参考开发指南填写,填写完成后进行保存。
image.png

4.模型转换

转换配置文件填写好后,运行转换操作。
image.png

转换成功的日志如下:
image.png

成功转换后会生成:

  • test_inst.wk
  • 由于选择的log_level为Function level,所以会生成mapper_quant文件夹,里面会有针对image_list.txt中的最后一张图片的每一层的量化输出结果,这个对之后定位精度问题大有用处。

到此为止模型转换的所有步骤就结束了,上板验证模型转换结果是否正确。

5.wk模型校验

直接在板子上校验生成的wk模型(为什么不使用RuyiStudio自带的仿真程序,因为之前尝试过,仿真跑出来的结果与板子上跑出来的结果不一致,仿真结果相似度99%,而板子只有 70%)。
针对Hi3516CV500的SDK,通过简单的修改sample样例代码去验证。
重新拷贝一份svp文件夹

cp -r /smp/a7_linux/mpp/sample/svp /smp/a7_linux/mpp/sample/test_svp

之后需要修改样例代码 test_svp/nnie/sample/sample_nnie.c中的 void SAMPLE_SVP_NNIE_Cnn(void) 函数的头和尾,img_3_112_112.bgr是需要提前准备好的测试数据并且为BGR planar类型:

void SAMPLE_SVP_NNIE_Cnn(void)
{
    HI_CHAR *pcSrcFile = "./data/img_3_112_112.bgr";  //测试数据 BGR PLANAR image
    HI_CHAR *pcModelName = "./data/test_inst.wk";    //转换好的模型
    
    HI_U32 u32PicNum = 1;
    ···
    ···
    /*NNIE process(process the 0-th segment)*/
    stProcSegIdx.u32SegIdx = 0;
    s32Ret = SAMPLE_SVP_NNIE_Forward(&s_stCnnNnieParam,&stInputDataIdx,&stProcSegIdx,HI_TRUE);
    SAMPLE_SVP_CHECK_EXPR_GOTO(HI_SUCCESS != s32Ret,CNN_FAIL_1,SAMPLE_SVP_ERR_LEVEL_ERROR,
        "Error,SAMPLE_SVP_NNIE_Forward failed!\n");

    /* 删除之后的 Software process 与 Print result
     s_stCnnNnieParam.astSegData[0].astDst 为模型的output,最多支持16个output
     根据模型output个数,将所有的output都打印出来, 假如模型有5个输出 */
     for(int i = 0; i < 5; i++) {
       int output_width = s_stCnnNnieParam.astSegData[0].astDst[i].unShape.stWhc.u32Width;
       int output_height = s_stCnnNnieParam.astSegData[0].astDst[i].unShape.stWhc.u32Height;
       int output_channel = s_stCnnNnieParam.astSegData[0].astDst[i].unShape.stWhc.u32Chn;
       int output_stride = s_stCnnNnieParam.astSegData[0].astDst[i].unShape.stWhc.u32Chn;
       int output_size = output_width * output_height * output_channel;

       printf("output_width %d, output_height %d, output_channel %d, output_size %d\n", output_width, output_height, output_channel, output_size);
       HI_S32* output = (HI_S32* )((HI_U8* )s_stCnnNnieParam.astSegData[0].astDst[i].u64VirAddr);
       HI_FLOAT outputScore = 0.0f;
       printf("output %d: \n", i);
       for (int j = 0; j < output_size; ++j) {
          outputScore = *(output + j) / 4096.f;
          printf("%f ", outputScore);
       }
       printf("\n");
     }
     
     // SAMPLE_SVP_NNIE_PrintReportResult 函数可以输出结果报表文件,在定位精度误差的时候可能会用到
     // s32Ret = SAMPLE_SVP_NNIE_PrintReportResult(&s_stCnnNnieParam);
    //     SAMPLE_SVP_CHECK_EXPR_GOTO(HI_SUCCESS != s32Ret, CNN_FAIL_1, SAMPLE_SVP_ERR_LEVEL_ERROR,"Error,SAMPLE_SVP_NNIE_PrintReportResult failed!");
     
     CNN_FAIL_1:
        ...
        ...
    

对比上述的输出与caffe模型输出误差是否相差不大(多尝试几张测试图片),如果相差不大,则模型转换成功,可以进行后续的工程落地,若误差较大,则需要定位误差来源!

wk模型转换定位输出误差

定位输出误差基本从几个方面考虑,输入是否一致,nnie算子支持形式,量化造成的结果。下面从这几方面入手,来分析误差造成的原因。
这里只提供分析误差的方法与可以通过努力而规避的误差问题,但实际中有一些问题是本身模型转换的问题(量化或算子支持的问题),没有办法解决,只能更换模型了。

1.使用vector compare

RuyiStudio支持caffe输出每一层结果,点击工具栏上的caffe output标识。
image.png

caffe inference 是根据RuyiStudio\Resources\pythonScript\CNN_convert_bin_and_print_featuremap.py脚本运行,脚本默认使用BGR图片,
如果模型输入为RGB,需要在脚本中进行修改:
image.png

填写配置参数需要注意Image File 一定要选择 image_list.txt中的最后一张图,因为mapper quant中输出的是最后一张图的量化结果。
image.png

最终输出结果log:
image.png

查看自己设置的output dir中出现每一层的输出结果:
image.png

将caffe output 与mapper quant对比,点击对比按钮。
image.png
填写相应的参数信息,Left Folder和Right Folder分别选择caffe output文件夹和mapper_quant文件夹,等待加载完成后点击compare即可开始对比。
image.png

双击CosinSimilarity这一列的数字可以查看每一层的具体输出,勾选Convert To Float 将量化结果转换为浮点数。
image.png

这一步主要关注输入data是否一致,如果层数太多,可以直接点击文件进行文件的更换,更快的对比自己怀疑拥有误差的层数。如果输入一致,data这一层的余弦相似度应为99%以上,若data相似度很低,一般情况下是RGB,BGR的问题。
image.png

若data输入一致,则可以直接对比output层是否一致,如果output层相似度也比较高,则模型转换就没有问题。若output层相似度低,可以从第一层开始观察在哪一层相似度急剧下降,查看相似度低的这一层是否设置的参数与NNIE支持算子的参数设置相冲突。

2.量化造成的误差?

定位是否是由于量化造成的误差,可以在模型转换参数配置时将 compile_mode 的模式修改为 High-precision,生成int16的模型,重新上板跑结果,对比是否与caffe的output一致,一般回归模型在低精度模型下的结果会很飘,在高精度下的结果与caffe基本一致。
image.png

3.裁剪caffe模型,定位误差

有时通过上面的操作,也不能找到误差原因。可以采用裁剪caffe模型的方法(直接在prototxt中删除操作,caffemodel不用修改),然后逐渐增加层数去逐步转模型,对比caffe与mapper quant结果是否一致。
将转出来的小模型上板子跑,调用 SAMPLE_SVP_NNIE_PrintReportResult 函数输出结果报表,将输出结果与caffe结果使用vector compare工具进行对比,根据prototxt中算子的前后判断对应的输出参数层名称。
image.png

自己在使用这个方法定位出模型中Reshape算子问题,算子的参数如下,加上Reshape操作之后,caffe与量化的data相似度只有58%(很疑惑为什么模型很后面的Reshape层会影响第一层的输入data的量化结果),但是删掉Reshape层后,量化的data以及上一层的output结果与caffe的相似度为99%。

layer {
  name: "reshape_6/Reshape"
  type: "Reshape"
  bottom: "dense_9/Relu:0"
  top: "reshape_6/Reshape:0"
  reshape_param {
    shape {
      dim: 1
      dim: 3
      dim: 3
    }
  }
}

定位出Reshape算子的问题,尝试将Reshape参数改为4维参数,最后结果就很正常了,可能nnie只能支持4维运算。

layer {
  name: "reshape_6/Reshape"
  type: "Reshape"
  bottom: "dense_9/Relu:0"
  top: "reshape_6/Reshape:0"
  reshape_param {
    shape {
      dim: 1
      dim: 3
      dim: 3
      dim: 1
    }
  }
}

可能遇到的问题

这一小节总结一下最近转模型遇到的问题,供大家参考~

1. caffe与mapper output中的输入data与output的相似度都很高,为什么最后上板之后结果很差!!

在转模型时,vector compare的输入data与output相似度都很高,但是上板结果很差,通过查询开发指南与资料,发现是由于输入问题导致最后误差较大,这里说明一下RGB_ORDER与输入图像通道之间的关系。

RGB_ORDER 给进去的图片通道 最终NNIE喂给模型的图片通道
BGR BGR BGR
BGR RGB RGB
RGB BGR RGB
RGB RGB BGR

从上面的表格可知,

  • 若RGB_ORDER设置为BGR,最终NNIE喂给模型的图片通道排序与你给进去的通道排序一致。
  • 若RGB_ORDER设置为RGB,则最终NNIE喂给模型的图片通道排序与你给进去的通道排序相反。
  • 若RGB_ORDER设置为RGB,在转wk模型时,已经将通道转换加在模型里了。

输入很重要!!输入很重要!!输入很重要!!有问题先排查输入问题!!

2. 为什么caffe与mapper quant对比时在某一层相似度为99%,后面几层相似度很低,之后又出现一层相似度为99%?为什么有时caffe中出现的层mapper quant中没有出现?

还记得在设置模型转换配置文件的时候,有一个 marked_prototxt 中的参数是自动生成的吗?该文件中进行了一些算子融合的操作,RuyiStudio最终使用marked_prototxt进行模型转换,而不是我们提供的caffe prototxt, 所以最终的mapper quant层对应的输出并不是caffe中对应层的输出,举个例子。

caffe与mapper quant 对比的第一层conv层相似度只有31%。
image.png

查看marked_prototxt与caffe prototxt,发现marked_prototxt中将 CONV+BN+RELU 进行了融合,故mapper quant 的 conv1 输出对应的应该是caffe模型的 relu1 层的输出。
image.png

将caffe中的conv1 层文件更改为 relu1 层的输出文件,对比结果如下,可看出该层相似度达到 99%以上,模型转换在该层没有问题。
image.png

在mapper quant中有一些层没有出现也是因为算子融合的问题,在使用vector compare时关注层的对齐即可。

3. 开发指南上面有一些NNIE支持的算子,caffe 1.0并不支持,使用RuyiStudio软件的caffe output 会提示算子不支持,这种情况下怎么使用vector compare来定位问题呢?

应该有同学已经知道怎么做了哈,就是在你自己本地的caffe环境中添加这些层后重新编译,将CNN_convert_bin_and_print_featuremap.py脚本拷贝到本地,运行后会生成每一层的feature map文件xxx_caffe.linear.float,将文件拷贝到项目目录中与mapper quant文件夹对比。运行命令如下,每一个参数的意义在脚本中都可以看到,这里不再赘述。

python CNN_convert_bin_and_print_featuremap.py -m ./facelandmark_mbv2/mbv2_new.prototxt -w ./facelandmark_mbv2/mbv2_new.caffemodel -i ./000030.jpg -n 2 -s 1.0 -o ./caffe_output/ -c 0

需要提醒的是:

  • 处理的图片需要是image_list.txt中的最后一张图片。
  • 脚本不能处理后缀为txt的mean文件,所以将通道均值直接写在脚本中,使用 transformer.set_mean(‘data’,np.array([103.0, 116.0, 123.5]))替换脚本中的均值读取部分。
  • 用于量化的测试图片先resize成模型输入的大小,然后再进行模型转换与本地caffe推理,这样可以避免输入data不一致但是找不到原因的情况。

4. 模型转换配置文件中data scale只能填写一个数,但是实际模型推理的时候是针对每个通道都进行data scale,也就是需要填写3个数值,这种情况怎么办?

可以从两个方面来解决:

  • 尝试在转换模型过程中,使用三个通道的 scale 均值作为 模型转换的 data scale,看最终转换的模型误差是否可以接受。
  • 在模型开始加一个mul算子,将三个通道的 scale 参数写在mul算子当中,添加算子不同模型有不同的方式,这里不详细展开讲解,关于onnx模型添加算子可以参考此系列的上一篇博客。

5. 模型共有3个输出,caffe与mapper quant相似度很高,但是上板后最终结果很奇怪?

sample样例代码中的 s_stCnnNnieParam.astSegData[0].astDst 为模型的output,最多支持16个output,根据转模型的经验,有时由于修改了caffe模型,导致输出index与最终输出不匹配。
解决方法是尝试打印16个output的width,height,channel,记录与实际输出shape一致的output index,打印对应index的output,看是否与caffe结果相似。
注意输出的每个channel数据是16位对齐的,故当输出shape类似:output_width 1, output_height 1, output_channel 36, output_size 36, 读取数据需要16位对齐:


for (int i = 0; i < output_size; ++i) {
    outputScore = *(output + i * 4) / 4096.f;
    printf("%f ", outputScore);
}
printf("\n");

6. 一般情况下模型转为量化模型后不是应该变小吗?为什么我转了之后模型反而变大了!!

我转的模型也出现了这种情况,查询资料后发现是因为 conv 中有group参数,在mapper编译的时候会进行拆分,拆分出来的层和指令比较占空间,convolution参数如下:

layer {
  name: "conv3"
  type: "Convolution"
  bottom: "relu2:0"
  top: "conv3:0"
  convolution_param {
    num_output: 24
    bias_term: true
    group: 24
    pad_h: 1
    pad_w: 1
    kernel_h: 3
    kernel_w: 3
    stride_h: 1
    stride_w: 1
    dilation: 1
  }
}

当出现这种情况之后,若该层 Convolution 的 input channel,output channel 和 参数中的group相等,这时使用 DepthwiseConv 替换卷积操作,如上面的卷积操作可以变为:

layer {
  name: "conv3"
  type: "DepthwiseConv"
  bottom: "relu2:0"
  top: "conv3:0"
  convolution_param {
    num_output: 24
    bias_term: true
    pad_h: 1
    pad_w: 1
    kernel_h: 3
    kernel_w: 3
    stride_h: 1
    stride_w: 1
    dilation: 1
  }
}

使用DepthwiseConv替换Convolution完成后,发现转模型后确实体积变小了,而且耗时也变短了。根据查询资料发现,若想提高硬件利用率,对于每层的CHW,最好channel是4对齐,width是16对齐。

总结

根据经验,一般情况下,如果不是算子不支持,70%的问题都出现在输入问题上,无论如何,一定要确保data的相似度99%以上,但是有时相似度为99%,但是数据的绝对误差很大,这种情况下要从图像的大小思考一下。上板验证要确保输入的通道情况,保证输入之后再考虑是否是剩下层的问题。另外,裁剪caffe模型定位误差这个方法也很好用。

这篇文章主要介绍了如何使用RuyiStudio将caffe转为wk模型,一些误差定位的方法,以及模型转换过程中可能有疑问的情况。希望这篇文章可以在模型转换过程帮助到大家。

(完)