【他山之石】深度学习模型转换与部署那些事(含ONNX格式详细分析)
深度学习模型在训练完成之后,部署并应用在生产环境的这一步至关重要,毕竟训练出来的模型不能只接受一些公开数据集和榜单的检验,还需要在真正的业务场景下创造价值,不能只是为了PR而躺在实验机器上。
在现有条件下,一般涉及到模型的部署就要涉及到模型的转换,而转换的过程也是随着对应平台的不同而不同,一般工程师接触到的平台分为GPU云平台、手机和其他嵌入式设备。
对于GPU云平台来说,在上面部署本应该是最轻松的事情,但是实际情况往往比较复杂。有历史遗留问题,比如说3年前的古董级的模型因为效率和推理速度问题需要进行优化,也有算法团队使用了一些比较小众或者自定义的OP的问题。其实解决这类问题有非常直接的方式,例如直接用最新的框架重新训练,或者算法团队对模型做一些妥协,替换掉一些骚操作,那么对部署工程师来说问题就简单了很多。但是有些情况下(算法团队很忙或者必须效果优先),我们只能自己从框架的层面来解决这个问题,包括但不限于:实现新OP、修改不兼容的属性、修改不兼容的权重形状。
当然这里无论是模型转换还是模型部署,我个人比较推荐的做法是都是使用ONNX作为中间媒介。所以我们有必要对ONNX做一个比较透彻的了解。
地址:http://bindog.github.io/
01
第一部分:ONNX结构分析与修改工具
ONNX结构分析
ModelProto GraphProto NodeProto AttributeProto ValueInfoProto TensorProto
ONNX的兼容性问题
https://github.com/onnx/models/issues/156 https://github.com/microsoft/onnxruntime/issues/2175
修改ONNX模型
02
第二部分:各大深度学习框架如何转换到ONNX?
MXNet转换ONNX
import mxnet as mx
import numpy as np
from mxnet.contrib import onnx as onnx_mxnet
import logging
logging.basicConfig(level=logging.INFO)
# Download pre-trained resnet model - json and params by running following code.
path='http://data.mxnet.io/models/imagenet/'
[mx.test_utils.download(path+'resnet/18-layers/resnet-18-0000.params'),
mx.test_utils.download(path+'resnet/18-layers/resnet-18-symbol.json'),
mx.test_utils.download(path+'synset.txt')]
# Downloaded input symbol and params files
sym = './resnet-18-symbol.json'
params = './resnet-18-0000.params'
# Standard Imagenet input - 3 channels, 224*224
input_shape = (1,3,224,224)
# Path of the output file
onnx_file = './mxnet_exported_resnet50.onnx'
# Invoke export model API. It returns path of the converted onnx model
converted_model_path = onnx_mxnet.export_model(sym, params, [input_shape], np.float32, onnx_file)
这个重点提一下MXNet转换ONNX模型可能会遇到的一些问题,不排除在未来版本MXNet修复了相关问题,也不排除未来ONNX版本更新又出现新的不兼容问题。
count_include_pad = 1 if attrs.get("count_include_pad", "True") in ["True", "1"] else 0
# ...
# ...
# ...
elif pool_type == "avg":
node = onnx.helper.make_node(
pool_types[pool_type],
input_nodes, # input
[name],
count_include_pad=count_include_pad,
kernel_shape=kernel,
pads=pad_dims,
strides=stride,
name=name
)
TensorFlow模型转ONNX
# your network def
import network
input_size = (224, 224)
ckpt_model_path = "./model.ckpt"
pb_model_path = "./model.pb"
output_node_name = "your model output name"
graph = tf.Graph()
with graph.as_default():
placeholder = tf.placeholder(
dtype=tf.float32, shape=[None, input_size[0], input_size[1], 3], name="pb_input"
)
output = network(placeholder)
# your can get all the tensor names if you do not know your input and output name in your ckpt with this code
# nl = [n.name for n in tf.get_default_graph().as_graph_def().node]
# for n in nl:
# print(n)
saver = tf.train.Saver()
sess = tf.Session(
config=tf.ConfigProto(
gpu_options=tf.GPUOptions(
allow_growth=True, per_process_gpu_memory_fraction=1.0),
allow_soft_placement=True
)
)
saver.restore(sess, ckpt_model_path)
output_graph_def = graph_util.convert_variables_to_constants(
sess, sess.graph_def, [output_node_name]
)
with tf.gfile.FastGFile(pb_model_path, mode="wb") as f:
f.write(output_graph_def.SerializeToString())
# you can get the input and output name of your model.pb file
# maybe a "import/" is needed to append before the name if you
# get some error
# gf = tf.GraphDef()
# gf.ParseFromString(open('./model.pb', 'rb').read())
# nl2 = [n.name + '=>' + n.op for n in gf.node if n.op in ('Softmax', 'Placeholder')]
# for n in nl2:
# print(n)
python3 -m tf2onnx.convert --input xxxx.pb --inputs pb_input:0 --inputs-as-nchw pb_input:0 --outputs
resnet_v2_101/predictions/Softmax:0 --output xxxx.onnx
注意,由于tensorflow的模型输入一般会比较灵活,输入的batch_size可以留空,可以在运行时传入不同大小的batch_size数据。但是一般在ONNX和TensorRT这些框架中,我们习惯于指定一个固定的batch_size,那如何修改呢,可以参考上一篇文章中我写的那个小工具,有一个例子展示如何修改ONNX模型的batch_size
PyTorch模型转ONNX
import torch
import torchvision
dummy_input = torch.randn(10, 3, 224, 224, device='cuda')
model = torchvision.models.alexnet(pretrained=True).cuda()
# Providing input and output names sets the display names for values
# within the model's graph. Setting these does not change the semantics
# of the graph; it is only for readability.
#
# The inputs to the network consist of the flat list of inputs (i.e.
# the values you would pass to the forward() method) followed by the
# flat list of parameters. You can partially specify names, i.e. provide
# a list here shorter than the number of inputs to the model, and we will
# only set that subset of names, starting from the beginning.
input_names = [ "actual_input_1" ] + [ "learned_%d" % i for i in range(16) ]
output_names = [ "output1" ]
torch.onnx.export(model, dummy_input, "alexnet.onnx", verbose=True, input_names=input_names, output_names=output_names
注意上面的input_names和output_names不是必需的参数,省略也是可以的
03
第三部分:ONNX到目标平台
历史文章推荐
太牛逼了!一位中国博士把整个CNN都给可视化了,每个细节看的清清楚楚!
Nature发表牛津博士建议:我希望在读博士之初时就能知道的20件事
沈向洋、华刚:读科研论文的三个层次、四个阶段与十个问题
如何看待2021年秋招算法岗灰飞烟灭?
独家解读 | ExprGAN:基于强度可控的表情编辑
独家解读 | 矩阵视角下的BP算法
独家解读 | Capsule Network深度解读
独家解读 | Fisher信息度量下的对抗攻击
论文解读 | 知识图谱最新研究综述
你的毕业论文过了吗?《如何撰写毕业论文?》
卡尔曼滤波系列——经典卡尔曼滤波推导
一代传奇 SIFT 算法 专利到期!
人体姿态估计的过去,现在,未来
给研究新生的建议,光看论文是学不好的,一定要看书,看书,看书!
分享、点赞、在看,给个三连击呗!