ONNX 结构分析

onnx 将每一个网络的每一层或者说是每一个算子当作节点Node,再由这些Node去构建一个Graph,相当于是一个网络。最后将Graph和这个 onnx 模型的其他信息结合在一起,生成一个model,也就是最终的. onnx 的模型。

onnx.helper——node、graph、model

在构建 onnx 模型这个过程中,这个文件至关重要。其中make_nodemake_graphmake_model是不可或缺的。make_tensor_value_infomake_tensor是构建 graph 中所需要用到的。

make_node [类型: NodeProto]

make_node(op_type,inputs,outputs,name=None,doc_string=None,**kwargs)

  • op_type: 节点的算子类型 [类型: 字符串]
    比如 Conv、Relu、Add 这类,详细可以参考onnx 给出的算子列表,这个可以自己赋值,但最好与官网对应上,否则其他框架在跑 onnx 的时候会不知道这是什么。
  • inputs: 存放节点输入的名字 [类型: 字符串列表]
    每个节点输入的数量根据情况会有不同,比如 inputs(2-3),即输入为 2 个或 3 个,可选的输入都会标注 (optional)。以 Conv 为例,必有输入 X 和权重 W,偏置 B 作为可选。
  • outputs: 存放节点输出的名字 [类型: 字符串列表]
    inputs类似,同样需要根据官网给出的输出个数来设置,大多数情况是一个输出,我暂且还没碰到多输出情况。
  • name: 节点名,可有可无,不要和 op_type 搞混了
  • doc_string: 描述文档的字符串,这个默认为 None [类型: 字符串]
  • **kwargs: 存放节点的属性 attributes [类型: 任意]
    这个 **kwargs可以是字典形式输入,也可以拆开分别赋值 (类型任意),反正不管是什么最后这个 node 都给你转换成 NodeProto 的形式。在用 IDE 的时候,你可以进到一个 onnx_ml_pb2.py 的文件中,你可以看到诸如 AttributeType、DataType、AttributeProto、ValueInfoProto、NodeProto 这些描述符号。onnx_ml_pb2.py 是由 protoc buffer 编译器通过 onnx-ml.proto 生成的。
    Attributes 在官网也被明确的给出了,一般被标注 (default:xxxxx) 的可以根据自己的需求不设置,没有标注 default 的属性则一定需要设置。
    以 Conv 举例:
    auto_pad:VALID,dilations:[1,1,1],group:1,kernel_shape:(7,7),pads:[3,3,3,3],strides:(2,2)
    可以写成:
  1. dict = {"kernel_shape": (7, 7),
  2. "group": 1,#default1,所以可以不写
  3. "strides": (2, 2),
  4. "auto_pad": "VALID",
  5. "dilations": [1, 1, 1],
  6. "pads": [3, 3, 3, 3]}#顺序无所谓
  7. node_def = helper.make_node(
  8. NodeType, # 节点名
  9. X_name, # 输入
  10. Y_name, # 输出
  11. **dict
  12. )

也可以写成:

  1. node_def = helper.make_node(
  2. NodeType, # 节点名
  3. X_name, # 输入
  4. Y_name, # 输出
  5. kernel_shape = (7,7),
  6. strides = (2,2),
  7. auto_pad = "VALID",
  8. dilations = [1,1,1],
  9. pads = [3,3,3,3],
  10. )

当然你也可以自己魔改,想放什么进去都可以,不过尽量还是统一符合官网要求比较好~

make_graph [类型: GraphProto]

make_graph(nodes,name,inputs,outputs,initializer=None,doc_string=None,value_info=[])

  • nodes: 用 make_node 生成的节点列表 [类型: NodeProto 列表]
    比如[node1,node2,node3,…]这种的
  • name:graph 的名字 [类型: 字符串]
  • inputs: 存放 graph 的输入数据信息 [类型: ValueInfoProto 列表]
    输入数据的信息以 ValueInfoProto 的形式存储,会用到make_tensor_value_info,来将输入数据的名字、数据类型、形状 (维度) 给记录下来。
  • outputs: 存放 graph 的输出数据信息 [类型: ValueInfoProto 列表]
    inputs相同。
  • initializer: 存放超参数 [类型: TensorProto 列表]
    比如 Conv 的权重 W、偏置 B,BatchNormalization 的 scale、B、mean、var。这些参数数据都是通过make_tensor来转换成 TensorProto 形式。
  • doc_string: 描述文档的字符串,这个默认为 None [类型: 字符串]
  • value_info: 存放中间层产生的输出数据的信息 [类型: ValueInfoProto 列表]

注意! inputs、outputs、value_info 都是 ValueInfoProto 列表形式,那么它们各自存放什么东西呢?
对于一个多层网络而言,其中间层的输入有来自上一层的输出,也有来自外界的超参数和数据,为了区分,onnx 中将来自外界的超参数信息和输入数据信息统一放在 inputs 里,而 value_info 里存放的是来自经过前向计算得到的中间层输出数据的信息 (2019.04.09 更新:现在 onnx 官方提供了前向推理计算 value_info 并生成 onnx 模型的api)。注意,是信息,不是具体数据值。outputs 只存放整个网络的输出信息。

第二个需要注意的是: initializer 作为存放超参数具体数值的 TensorProto 列表,其中每个 TensorProto 总会有与其对应的 ValueInfoProto 存在,对应关系通过 name 来联系。比如 inputs 里放了一个 Conv1 的权重参数信息,名字为 “Conv1_W” 那么对应的 initializer 里会有个名字与其相同的 TensorProto 来存储这个权重参数的具体数值。

第三个需要注意的是: 对于一个网络而言如何能体现其网络结构呢?即节点与节点之间的关联。
在构建每一个 node 时就需要注意,当前 node 的输入来自于哪一个 node 的输出,名字要匹配上,才能将 node 间联系体现出来。

make_model

make_model(graph, **kwargs)

  • graph: 用 make_graph 生成的 GraphProto
  • **kwargs: 构建 ModelProto 中的 opset_import,这个还没弄太清楚,不过不影响生成模型

这个函数中会先实例化一个 ModelProto——model,其中会对它的 ir_version(现在默认是 3)、graph(就是把传入的 graph 复制进 model.graph)、opset_import 做处理。具体可以看 helper 里的 make_model 这个函数。我们只要知道这是个最后把 graph 和模型其他信息组合在一起构建出一个完整的 onnx model 的函数就可以了。

onnx.helper——tensor、tensor value info、attribute

make_tensor [类型: TensorProto]

make_tensor(name,data_type,dims,vals,raw=False)

  • name: 数据名字,要与该数据的信息 tensor value info 中名字对应 [类型: 字符串]
  • data_type: 数据类型 [类型: TensorProto.DataType] 如 TensorProto.FLOAT、TensorProto.UINT8、TensorProto.FLOAT16 等
  • dims: 数据维度 [类型: int 列表 / 元组]
  • vals: 数据值,好像要可迭代的 [类型: 任意]
  • raw: 选择是否用二进制编码 [类型: bool]
    raw 为 False 的时候,就会用相应的 TensorProto 来存储基于 data_type 的值,若 raw 为 True,则是用二进制编码来存储数据。
    注:我发现 cntk 官方转 onnx 用的是 raw 为 False 的方式,而 pytorch 官方转 onnx 用的是 raw 为 True 的方式。

make_tensor_value_info [类型: ValueInfoProto]

make_tensor_value_info(name,elem_type,shape,doc_string=””,shape_denotation=None)

  • name: 数据信息名字 [类型: 字符串]
  • elem_type: 数据类型 [类型: TensorProto.DataType]
  • shape: 数据维度 (形状) [类型: int 列表 / 元组]
  • doc_string: 描述文档的字符串,这个默认为 None [类型: 字符串]
  • shape_denotation: 这个没太看懂,可能是对 shape 的描述 [类型: 字符串列表]
    根据数据类型和形状创建一个 ValueInfoProto。

make_attribute [类型: AttributeProto]

make_attribute(key,value,doc_string=None)

  • key: 键值 [类型: 字符串]
  • value: 数值 [类型: 任意]
  • doc_string: 描述文档的字符串,这个默认为 None [类型: 字符串]
    根据数值类型来创建一个 AttributeProto,这个函数用在了 make_node 里,用于将 make_node 传入的 **kwargs 转为 AttributeProto 形式。

构建一个简单的 onnx 模型,实质上,只要构建好每一个 node,然后将它们和输入输出超参数一起塞到 graph,最后转成 model 就可以了。

写了一个 base,在构建 onnx 的时候可以直接调用 createOnnxNode、createOnnxModel 来构建一个 onnx 模型,可以选择把 onnx 保存为 txt 格式,很大就是了。具体流程后续补上。
代码
https://blog.csdn.net/u013597931/article/details/84401047