如何将MindSpore模型转ONNX格式并使用OnnxRuntime推理---开发测试篇
Tips
1 此文档建立在“onnx前期准备”文档的基础上,默认已完成算子统计,models和mindspore仓库的clone工作,若未全部完成请回看该文档,或查看网页版教程:https://bbs.huaweicloud.com/blogs/360051 。
2 文档中的基本概念等知识如果已经掌握可以跳过。
3 不需要开发的同学:如果所有算子都已经实现,请直接从第二章开始。
4 需要开发的同学:文档中用Pad算子来介绍开发流程的部分写的很详细,可能有同学很会觉得很啰嗦,但是还是希望能耐心看完,这样就可以清楚整个开发流程。
5 此文档也有网页版:,大家愿意的话可以支持一下,刷一刷阅读量。
6 如有错误,欢迎修正和反馈
常用网址
1 如何将 MindSpore 模型转 ONNX 格式并使用 OnnxRuntime 推理 --- 前期准备篇
2 如何将 MindSpore 模型转 ONNX 格式并使用 OnnxRuntime 推理 --- 开发测试篇
3 CNN+CTC ONNX 导出 onnx_exporter.cc 文件 PR
4 CNN+CTC ONNX 导出脚本 + 推理脚本 PR
6 MindSpore 官方模型 checkpoint 下载网址
目录
(1) 一对一(完全对应):MS Conv2D -> Onnx Conv 11
(2) 一对一(不完全对应):MS ExpandDims -> Onxx Reshape 11
(3) 一对多:MS BatchMatMul -> ONNX Transpose + MatMul 13
2. MindSpore包重安装或Python运行路径设置(推荐) 16
1、
2、 算子映射
1. 基本概念
(1) 图
华为官网对于图的定义如下:
实际上开发onnx并不需要完全熟悉和清晰图的概念,相反将图抽象的理解为一个计算流程图会更好理解他的作用和调用流程,假设现在我们需要计算如下表达式:F(x, y, z)=(x + y)* z - b,则其对应的计算流程图如下:
图1 F(x, y, z)计算流程图
图1中的每一个圆圈或者椭圆都是一个节点,而所有这些节点合起来就构成了我们F(x, y, z)这个函数的计算流程图,而“图”就是用计算机代码来表示和记录这种计算流程。而在这张图中,我们可以看到有三种不同颜色的节点,对于我们本次onnx映射来说他们就是最常用到的三种节点:输入数值节点(橙色x, y, z)、常量数值节点(灰色b)和算子节点(蓝色+, *, -)。大家也注意到了我把图中的蓝色节点(+, *, -)称之为算子而不是运算符,这就是因为其实这些运算符就是最基础最基本的算子,而我们所用到的算子实际上就是这样基础的算子不断叠加起来的,也就是我们调用的库方法和库函数。因此可以将图1 转换为算子计算图2:
图2 算子计算图
至此我们就对“图”有了一个简单但是我个人觉得完全够用了的理解,下面将节点合并为两类:数值节点和算子节点,并介绍节点的相关概念。
(1) 节点
1 节点的name
可以注意到图2中我除了把运算符(+, *, -)换成算子的名字外,还在每一个节点边上标了一个序号,这个序号可以理解为每个节点的唯一标识符,也就是计算机用来存储和记录运算流程的一个标记。这种标记在本次onnx算子映射中被设为这个节点的“name”,同时对于这张图来说存在一个变量用于存储其中所有节点的“name”,也就是官方文档中的:“node_map_ptr” <节点,节点序号>。
2 数值节点
前面的输入数值节点和常量数值节点都是用来存储数据数值的节点,其主要包括以下信息:
Name(序号):该节点的唯一标识符(如果不好理解就全部理解为序号)。
Value(数值):输入或者自己设定的常量的数值,如1,10,1000等。
Type(类型):该节点所存储数值的数据类型。
Output(输出):数值节点需要传输给其他节点进行计算,因此需要设定此节点的输出(即传什么给下游节点),实际上大多数情况都是add_output(name),也就是把自己的序号传下去,到时候下游节点就可以通过name访问数值了。
3 算子节点
算子节点中定义了数据的计算逻辑,其计算部分是“算子开发”所需要做的事情,本次onnx映射仅仅只需要调整和映射以下信息即可:
Name(序号):该节点的唯一标识符(如果不好理解就全部理解为序号)。
OpType(算子类型):这个可以简单理解为此算子的名字,比如“+”他的OpType设为“Add”。
Input(输入):定义此算子的输入是什么,即将什么数值节点或者其他算子节点的输出传给此算子,大部分情况下都是add_input(name_xxx),即告诉计算机我要序号为name_xxx的节点作为我的输入。
Attr(属性):有些算子除了输入外还有属性值,属性值是在算子初始化的时候赋值的,而在此次项目中可以将其与输入看为同一个东西,只不过获取的方法不同罢了。
Output(输出):定义此算子的输出是什么,即将什么数值传给下游算子,实际上大多数情况也是add_output(name1),即此算子的输出就是其唯一标识符,下游算子需要调用的话可以add_input(name1)来设置这个算子为输入。
通过设置好每一个数值节点的value,type,output和算子节点的input和output信息后,整张图就连接起来了。
1. 开发流程
此次开发流程将以我本次onnx推理的CNN+CTC模型中缺少的Pad算子为例,以功能描述+代码+映射对应图的形式讲解开发中需要用到的方法,学会了这些方法后就可以通过拼接和组合完成其他算子的映射。
(1) 算子映射分析
1 MindSpore Pad算子接口分析
MindSpore官方算子查询网址:
https://www.mindspore.cn/docs/zh-CN/r1.7/index.html
这个算子其实非常简单,输入一个任意维度的Tensor,根据初始化时赋值的paddings属性在对应的维度前和后增加0值,以2维Tensor为例:
输入input_x如下:
属性paddings如下:
input_x的shape是(2,4),这个paddings属性取值的含义就是,在input_x的第0个维度增加3(前面1,后面2),对于这个例子就是增加三行(上面1行,下面2行),然后在input_x的第1个维度(列)增加3列(左边2列,右边一列),所有增加的行和列全部填充数值0,结果如下:
而mode属性值是设定填充的方式,分为三种,具体大家可以看官网案例,本次网络使用默认值故此处不分析。Onnx Pad算子接口分析
Onnx算子查询网址:
https://github.com/onnx/onnx/blob/main/docs/Operators.md
可以看到Onnx中也是有这个Pad算子,并且简单查阅后发现功能是相同的,那么就进入下一步。
4 差异分析
通过对比分析我们可以看出MindSpore中的Pad算子和Onnx中的Pad算子功能是相同的,但是在输入和属性上存在差异,具体为:
MindSpore中用来设置增加维度的paddings是一个属性,而Onnx中起到相同功能的pads是一个输入,因此我们需要写paddings到pads的映射。
同时paddings要求是一个shape为[N, 2](N为input_x的维度)的tuple,其第D行的第0个元素代表在input_x的第D维前面增加的数量,而第1个元素代表在后面增加的元素。而pads则必须为一个1D的tensor其shape为[2 * N],其数值形式为[dim1_begin, dim2_begin,…, dim1_end, dim2_end]其中dim1_begin和dim1_end就代表在input_x的第1个维度前和后增加的数量。
文字看的可能比较晕,那我们看下面的图:
这张图就可以很清晰的看出paddings和pads之间的对应关系,同时可以得到paddings到pads映射所需要做的事情:
• MindSpore属性paddings映射为Onnx输入pads
• 将paddings从2D tuple类型映射为1D tensor类型
• 将paddings的数值正确映射为pads的数值
而由于CNN+CTC网络中使用的是默认mode属性值CONSTANT,往新增行列填充0,故此处不分析,同样也不需要映射,如果mode属性值设定了某个特定的数值则需要增加对mode属性的映射。
至此我们完成了对需要映射算子的分析,下面就可以进入开发阶段了,如果对于应该在那一行或者哪里写相应的代码可以查看CNN+CTC ONNX导出的PR链接,其中有清晰的增添删改代码和代码行号:
https://gitee.com/mindspore/mindspore/pulls/35915/files
(如果你发现需要映射的算子能够完美映射,即MindSpore XXX算子与Onnx XXX算子的输入输出完全一致,那么可以跳过此章,直接进入第 3 节 官方案例分析 )。
(2) 声明映射方法
在OnnxExporter类下声明类私有方法ExportPrimXXX:
其中四个参数的含义分别为:
• func_graph是MindSpore的图。
• node是图中当前的节点,该函数中就是Pad算子节点。
• node_map_ptr存储图中所有节点<节点,节点序号>。
• graph_proto是ONNX的图。
(3) 注册算子映射表
在OnnxExporter::ExportCNode方法中注册算子映射表:
(4) 实现映射方法
实现第(2)步声明的映射方法ExportPrimXXX:
(5) 获取节点输入与属性
获取MindSpore Pad算子第1个输入input_x的name:
获取MindSpore Pad算子的属性paddings值:
(6) 数值映射
将paddings(2D tuple)转化为pads_sequence(1D vector),并完成数值映射
(7) 添加数值节点
将数值pads_sequence注册为数值节点pads:
在node_map_ptr中登记pads常量数值节点并获取其name:
在ONNX图graph_proto中新建一个常量数值节点pads_node,并指定其输出为pads常量数值节点的name:
为pads_node节点添加一个value数值属性,并将数值节点pads中的数值转换为onnx::tensor后赋值给value:
(8) 添加算子节点
本次Pad算子映射不涉及算子节点的添加,第 3 节 的 BatchMatMul 算子 映射中会介绍如何添加算子节点。
(9) Onnx图添加算子节点
在node_map_ptr中登记当前MindSpore Pad算子节点并获取其name:
在ONNX图graph_proto中新建ONNX Pad算子节点:
指定ONNX Pad算子节点的输出为MindSpore Pad算子节点的输出(name)即ms_pad_node_name:
指定ONNX Pad算子节点的第1个输入为MindSpore Pad算子的第1个输入input_x的name即x_name:
指定ONNX Pad算子节点的第2个输入为(7)中创建的pads数值节点的name即pads_name:
映射完成后:
2. 官方案例分析
官方案例就不画图了,以功能描述加代码的方法分析,同时映射方法的声明和算子注册也不赘述,直接分析核心映射代码。
(1) 一对一(完全对应):MS Conv2D -> Onnx Conv
1 映射分析
官方案例,就不分析了,结果就是MindSpore Conv2D 和Onnx Conv 的输入和属性能够完全对应上,那么就只需要写一段代码,把相同作用的参数名映射一下就好了:
(2) 一对一(不完全对应):MS ExpandDims -> Onxx Reshape
1 映射分析
MindSpore ExpandDims 算子 -> ONNX Reshape 算子 , 输入输出可以对应,属性含义不同,无法直接对应, ExpanDims输入axis(int)是要扩展维度的轴,Reshape输入shape是扩展后的维度,需要在转换时作特殊处理。因此我们明确映射所需要做的事:
• 获取MindSpore ExpandDims算子的输入axis并结合输入input_x的shape计算出扩维之后的new_shape,而这个new_shape就是Onnx Reshape算子所需要的输入shape。
5 获取节点输入与属性
获取MindSpore ExpandDims算子的第1个输入input_x和第2个输入axis:
6 MindSpore输入axis映射为Onnx输入shape
获取MindSpore第1个输入input_x的shape值x_shape:
通过MindSpore的输入axis和x_shape推导出正确的Onnx输入new_shape,具体算法不分析,如果感兴趣可以自己查看:
7 添加数值节点
将数值new_shape注册为数值节点shape:
在node_map_ptr中登记shape常量数值节点并获取其name(name_shape):
在ONNX图graph_proto中新建一个常量数值节点node_proto,并指定其输出为常量数值节点的name(name_shape):
为node_proto节点添加一个value数值属性,并将数值节点shape中的数值转换为onnx::tensor后赋值给value:
8 Onnx图添加算子节点
在node_map_ptr中登记当前MindSpore ExpandDims算子节点并获取其name:
在ONNX图graph_proto中新建ONNX Reshape算子节点:
指定ONNX Reshape算子节点的输出为MindSpore ExpandDims算子节点的输出(name)即node_name:
指定ONNX Pad算子节点的第1个输入为MindSpore ExpandDims算子的第1个输入input_x的name即input_x:
指定ONNX Pad算子节点的第2个输入为④中创建的shape数值节点的name即name_shape:
至此完成映射
(3) 一对多:MS BatchMatMul -> ONNX Transpose + MatMul
1 映射分析
MindSpore的 BatchMatMul 算子 有transpose_a,transpose_b属性,控制是否将输入转置,ONNX的 MatMul 算子 无转置属性,因此,需要判断BatchMatMul的transpose_a,transpose_b属性是否为true,如果为true,则需要在MatMul对应输入前添加Transpose算子作转换。因此我们可以明确需要做的事:
• 获取MindSpore BatchMatMul算子的属性transpose_a和transpose_b
• 获取MindSpore BatchMatMul算子的输入input_x和input_y
• 若transpose_a为true,则需要增加一个Onnx Transpose节点将输入input_x进行转置
• 若transpose_b为true,则需要增加一个Onnx Transpose节点将输入input_y进行转置
9 获取节点输入与属性
获取MindSpore BatchMatMul算子的第1个输入input_x和第2个输入input_y:
获取MindSpore BatchMatMul算子的第1个属性transpose_a和第2个属性transpose_b:
10 若transpose_a为True则在input_x后增加Transpose算子节点
获取输入input_x的shape:
在node_map_ptr中登记一个transpose_input_x_name常量数值节点来存储转置后的input_x数值:
在ONNX图graph_proto中新建ONNX Transpose算子节点transpose_inputx_node_proto,并且指定其输入为MindSpore BatchMatMul算子节点的第1个输入input_x,输出为刚才创建的常量节点transpose_input_x_name:
为transpose_inputx_node_proto节点添加一个perm数值属性,并依据input_x的shape为其赋值:
至此对input_x进行转置的Onnx Transpose算子节点添加完成。
11 若transpose_b为True则在input_y后增加Transpose算子节点
获取输入input_y的shape:
在node_map_ptr中登记一个transpose_input_y_name常量数值节点来存储转置后的input_y数值:
在ONNX图graph_proto中新建ONNX Transpose算子节点transpose_inputy_node_proto,并且指定其输入为MindSpore BatchMatMul算子节点的第1个输入input_y,输出为刚才创建的常量节点transpose_input_y_name:
为transpose_inputy_node_proto节点添加一个perm数值属性,并依据input_y的shape为其赋值:
至此对input_y进行转置的Onnx Transpose算子节点添加完成。
12 Onnx图添加算子节点
在node_map_ptr中登记当前MindSpore BatchMatMul算子节点并获取其name(node_name):
在ONNX图graph_proto中新建ONNX MatMul算子节点:
指定ONNX Reshape算子节点的输出为MindSpore BatchMatMul算子节点的输出(name)即node_name:
下面这行代码我也不知道啥意思,如果有同学知道了请联系我修改:
若transpose_a为True,则说明input_x需要转置,那么指定ONNX MatMul算子节点的第1个输入为转置后的常量数值节点transpose_input_x_name;若为False,则说明input_x不需要转置,那么指定ONNX MatMul算子节点的第1个输入为原始的input_x:
若transpose_b为True,则说明input_y需要转置,那么指定ONNX MatMul算子节点的第1个输入为转置后的常量数值节点transpose_input_y_name:若为False,则说明input_y不需要转置,那么指定ONNX MatMul算子节点的第1个输入为原始的input_y:
至此完成映射。
1、 编译导出
1. MindSpore编译
在MindSpore路径下输入以下命令开始编译:
sh build.sh -e gpu -j32
初次编译需要下载很多第三方包所以很慢,之后再次编译就很快了。
编译报错则根据报错修改,编译成功则显示如下界面:
3. MindSpore包重安装或Python运行路径设置(推荐)
(1) MindSpore包重安装
编译完的MindSpore包会生成在mindspore/build/package/路径下:
可以在mindspore路径下输入以下命令重新安装mindspore包:
pip uninstall build/package/xxx.whl
pip install build/package/xxx.whl
(2) Python运行路径设置
输入以下命令设置Python运行路径:
export PYTHONPATH=/disk1/user14/wzb/test/mindspore/build/package:$PYTHONPATH
把其中的路径换成自己编译的mindspore存放的绝对路径,通过这种方式可以避免重复卸载再安装mindspore包的麻烦,只需要设置一次后就可以直接调用每次编译出来的新mindspore包了。
4. Onnx模型导出
更新完MindSpore包后,在Models文件夹中自己模型路径下调用README文件中的export命令,设置导出格式--file_format为ONNX,若成功导出则在模型路径下生成xxx.onnx文件:
若报错则根据报错信息修改算子映射文件onnx_exporter.cc并重复(1)(2)(3)步骤,直到能够导出ONNX文件为止。
2、 推理测试
1. 数据集下载
(1) 依据Models README下载
查看Models中README文件中对于数据集的描述,通过其给出的超链接下载数据集并放在对应的路径下:
(2) 依据模型原论文源码中的README下载
若发现Models的README文件给出的超链接中数据集太大、下载很慢或者保存在谷歌云盘上需要翻墙,那么可以查阅一下模型原论文中给出的源码链接,查看源码中README提供的数据下载地址。
比如我的CNN+CTC模型Models中给出的超链接是用谷歌云盘存储的,并且把训练集、验证集和测试集打包在一起,导致数据集高达18G,又需要翻墙,直接下载经常中断:
屡次尝试未果后,我查阅了原论文的代码并且找到了其readme文件中给出的数据集下载链接:
点开后我发现了原作者的数据集中既有Models中给出的集合包,还有单独的训练集、验证集和测试集压缩包,由于我们只涉及推理所以只需要下载测试集压缩包即可,其大小只有160M,下载非常顺利:
2. MindSpore模型推理
(1) MindSpore推理
这一步骤只需要参照自己Models模型中的README文件中关于MindSpore模型eval的说明即可,只是需要注意有些模型的脚本可能由于MindSpore的更新导致一些算子的用法发生了改变,如果报错可以自己看着修改一下:
3. Onnx模型推理
(1) Onnx推理文件
Onnx推理文件就是使用onnxruntime接口导入第二章中导出的onnx文件,然后传入符合网络要求的输入,最终输出Onnx离线推理的模型精度。
这一步骤实际上很简单,主要包含两步,完整推理文件可以参考此PR中的eval.py和infer_cnnctc_onnx.py:https://gitee.com/mindspore/models/pulls/2913/files
1 数据获取dataloader
使用Models模型eval脚本中的数据集dataloader获取数据,然后将其从MindSpore::Tesnor转化为Onnx一般的输入numpy格式:
13 网络替换:MindSpore Net -> Onnx
将Models模型eval脚本中关于网络的定义替换成Onnx中onnxruntime导入的网络:
MindSpore:
Onnx:
(2) Onnx推理脚本
推理脚本就是仿照eval脚本写一个可以用bash调用的.sh文件,使得用户可以通过bash命令一键推理,详细可参考此PR中的run_infer_onnx.sh:
https://gitee.com/mindspore/models/pulls/2913/files
5. PR提交
(1) Leader创建分支
联系小组Leader fork官方MindSpore和Models仓库并给每个模型创建分支。
(2) 下载新MindSpore和Models仓库
下载小组用来提交PR的新MindSpore和Models仓库:
git clone https://gitee.com/ xxxx /mindspore.git
git clone https://gitee.com/xxxx/models.git
(3) 更新替换文件
进入MindSpore和Models路径后切换到自己的分支:
git checkput cnnctc
将自己需要修改的文件传入新下载的MindSpore和Models文件夹对应路径下进行替换。
(4) 上传代码
使用如下命令提交pr:
1 查看已修改文件:
git status
2 提交到本地仓库:
依据①中输出的已修改文件,核对是否均是自己修改的,如果是则直接git add .若不全是则一个个的git add xxxx文件。
MindSPore仓库:
git add .
git commit -am " ONNX converter: CNN+CTC support"
保存并退出:点击ESC,然后按住shift和 ”:” ,输入wq,回车
Models仓库:
git add .
git commit -am " ONNX infer: CNN+CTC support"
保存并退出:点击ESC,然后按住shift和 ”:” ,输入wq,回车
3 提交到远程仓库分支:
git push origin cnnctc
6. 创建PR
联系Leader创建PR,或自己前往提交代码的仓库创建PR:
7. 门禁测试
1 开启测试
在自己PR的评论区输入/retest即可开启测试:
点击系统自动评论中的Link超链接即可打开门禁界面:
2 测试结果
报错后会中断门禁测试,在评论区找报错信息,点击报错评论中的超链接即可查看门禁报错信息:
一段代码错误会以两段相同代码的不同格式输出,下面一段为自己当前写的代码,而上面一段为门禁系统的修改意见,只需要按照上面一段改就行了(字有点小放大点看)。
如果通过则系统自动回复如下:
- 点赞
- 收藏
- 关注作者
评论(0)