一、ONNX的结构
ONNX作为一种文件存储格式,使用的是Protobuf这个序列化数据结构去存储神经网络的权重信息。Protobuf是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
加载了一个深度学习系列的onnx 模型文件之后,我们获得的就是一个ModelProto,它包含了一些版本信息,生产者信息和一个GraphProto。在GraphProto里面又包含了四个repeated数组,它们分别是node,input,output,initializer。其中node中存放了模型中所有的计算节点,input存放了模型的输入节点,output存放了模型中所有的输出节点,initializer存放了模型的所有权重参数。每个计算节点(NodeProto)除了必须的参数(input, output, op_type)外,可能还有一些和op_type相关的参数,这里会用AttributeProto存储。比如conv的属性:group, pad, strides。 Transpose 属性:perm。
其中模型中每一层算子的信息使用NodeProto表示,保存在node的列表中,每个node节点都会有input和output两个数组,这两个数组是string类型,通过input和output的指向关系,就可以利用上述信息快速构建出一个深度学习模型的拓扑图。
-
ModelProto
-
GraphProto
-
node (NodeProto)
-
(AttributeProto)
-
-
input (ValueInfoProto)
-
output (ValueInfoProto)
-
initializer (TensorProto)
-
-
二、读取或者导出ONNX
定义好一个onnx格式的模型后,通过onnx.load, onnx.save即可读取和保存模型文件。
import onnx
model = onnx.load(load_onnx_path) #读取模型onnx.save(model, save_onnx_path) #保存模型
三、手动构建模型拓扑结构
通过onnx.help库的函数封装,构建上述的GraphProto, NodeProto, TensorProto, ValueinfoProto类型数据 ,之后保存文件。主要涉及onnx.helper.make_node, onnx.helper.make_tensor_value_info, onnx.helper.make_tensor, onnx.helper.make_graph, 这些函数具体的参数接口可以看onnx.helper文档。
其中,make_node时候,op_type很关键,要和onnx的opset对应上,具体支持的类型和构建示例见官方文档:https://github.com/onnx/onnx/blob/main/docs/Operators.md
下面是构建一个只有一层matmul算子的model的示例:
import onnx
import numpy as npM=10
N=20
K=16
A_data=np.random.random([M,K])
B_data=np.random.random([K,N])
output_C=np.matmul(A_data,B_data)nodes = [onnx.helper.make_node(op_type="MatMul",inputs=['input_A', 'input_B'],outputs=['output_C'],name=name)]init_tensor = [onnx.helper.make_tensor('input_B', onnx.TensorProto.FLOAT, B_data.shape, B_data.flatten().tolist())]inputs= [onnx.helper.make_tensor_value_info(name=net_info[lname]['name'], elem_type=onnx.TensorProto.FLOAT, shape=A_data.shape]outputs = [onnx.helper.make_tensor_value_info(name='output_C', elem_type=onnx.TensorProto.FLOAT, shape=output_C.shape)]graph = onnx.helper.make_graph(nodes=nodes, name="onnx_test", inputs=inputs, outputs=outputs,initializer=init_tensor)onnx_model = onnx.helper.make_model(graph, opset_imports=[make_opsetid(domain="", version=18)])onnx.save(onnx_model, onnx_fpath)
四、导出模型检查
如下代码可以对手动构建或者修改后的onnx模型进行拓扑结构检查。检查模型格式是否正确,图结构是否完整,节点是否正确等。
该步骤主要帮助发现一些不合理的连接关系,比如按nodes列表顺序构建到节点b时候,b节点依赖a节点,但是b节点前面没有出现过a节点,这就是一个错误的graph拓扑关系。
onnx.checker.check_model(onnx_model)
如果上面的检查通过了,那么可以试着对保存的onnx执行一次推理,如果正常执行结束,则说明我们的构建保存的onnx模型是正确的。
这一步可以检查出一些不合理的数据类型。比如对于reshape节点来说有两个输入,一个是要reshape的数据数组(假设是A),另一个是reshape的形状数组(假设是B),B的数据类型必须是整数类型,这在导出onnx模型的check_model是检查不出来的。
import onnx
import onnxruntimedef get_in_out_name(op, onnx_session):node_name = []if op =='inputs':for node in onnx_session.get_inputs():node_name.append(node.name)else:for node in onnx_session.get_outputs():node_name.append(node.name)return node_nameonnx_path = '../model.onnx'
xdata = np.random.random((1, 1, 1, 384, 1024))onnx_session = onnxruntime.InferenceSession(onnx_path)
input_name = get_in_out_name('inputs', onnx_session) #得到所有的输入节点名字dummy_input = {input_name[0]: np.float32(xdata), #假设这里只有一个输入节点
}
result = onnx_session.run(None, input_feed=dummy_input)
print('output shape is:', result[0].shape)#其中一个输出的形状
五、从ini算子到onnx模型的处理逻辑
1, 从generate_model_file.py获取算子的信息
包括:
name, 层名字
op_type, 算子类型
input_shape, 输入尺寸
output_shape, 输出尺寸
in_layer_name,该层的输入来自哪些层
out_layer_name,该层的输出会去向哪些层
data: [ifmap, ifmap2, weights, bias, output_compile], 具体的map数据和权重数据
head,attention模块的head
scale,attention模块score的除数数据
a_transpose, 当matmul算子相关时候参数
b_transpose, 当matmul算子相关时候参数
2, 构建onnx模型的node节点。
根据上一步的算子信息,逐层构建节点。需要输入,输出,初始化超参数的节点都构建好。这个过程也对应着处理好模型的拓扑结构。
目前benchmark模型可以支持的onnx node算子如下,后面可以根据https://github.com/onnx/onnx/blob/main/docs/Operators.md增加更多需要的算子。
“MatMul”, “Add”, “Softmax”, “LayerNormalization”, “Reshape”, “Transpose”,“Constant”,“Div”
3,保存onnx模型
使用了onnx.checker.check_model(onnx_model)来检查构建的模型拓扑结构是否正确,如果不对会程序报错退出,但是前一步也会保存出一个onnx模型文件,可以用netron可视化看到哪里的连接关系不对。