海思开发:Nanodet : onnx -> caffe

一、前言

应网友所托,帮忙转换 nanodet 模型(注:原版 nanodet 上采样是线性插值法,海思中不支持,换成反卷积了),花了一天多时间思考与尝试,终于搞定它,过程如下。

二、主要过程

仔细想了下,其实 nanodet 并没有什么生僻的 op,全部是很普通的、常见的那种,但是它们的组合方式以及数据的shape太令人难受了,下面我截图说明一下。

1. split 不支持

海思开发:Nanodet : onnx -> caffe
这里说一下,onnxsplitcaffe 里面是 sliceslice 相关博客。相关转换代码如下:

# onnx2caffe/_operators.py 文件中
def _convert_Split(node, graph, err):
    node_name = node.name
    input_name = str(node.inputs[0])
    #print(node.attrs)
    #print(node.outputs)
    layer = myf("Slice", node_name, [input_name], node.outputs,
                axis=node.attrs['axis'],
                slice_point=node.attrs['split'][0]
                )
    #print(graph.channel_dims)
    graph.channel_dims[node.outputs[0]] = node.attrs['split'][0]
    graph.channel_dims[node.outputs[1]] = node.attrs['split'][1]
    #print(graph.channel_dims)
    return layer

2. transpose 的问题

海思开发:Nanodet : onnx -> caffe
在之前博客里提过,海思 caffepermute 局限很大,即其支持的 order 只能为 (0,2,3, 1)。这就让人很难受了,而且从上图可以看出transpose输入的数据是三维的,难上加难了。
仔细查看输入数据shapetranspose上面那个reshape的输入是四维,满足transpose需要,那我们是否可以调换一下顺序,下面试着写一下。

#                      reshape                transpose        
原始: (1, 1, 52, 52) ---------> (1, 1, 2704) ------------> (1, 2704, 1)
# 现在我们试着这样
#                   transpose (0, 2, 3, 1)       reshape
现在: (1, 1, 52, 52) ---------> (1, 52, 52, 1) ----------> (1, 2704, 1)

这样一来,海思caffepermute 的要求输入shape 满足了,order 也满足了。
海思开发:Nanodet : onnx -> caffe
另一边分支的 transpose 也同样处理。相关转换代码如下:

# onnx2caffe/_operators.py 文件中
def _convert_transpose_reshape(node, input_name_shape):
    node_name = node.name
    input_name = input_name_shape[0]
    output_name = str(node.outputs[0])
    layers = []
    transpose_out_name = node_name + "_tran_out"
    layer = myf("Permute", node_name, [input_name], [transpose_out_name], permute_param=dict(order=[0, 2, 3, 1]))
    layers.append(layer)

    #reshape_out_name = node_name + "reshape_out"
    layer = myf("Reshape", node_name + "_reshape", [transpose_out_name], [output_name],
                reshape_param=dict(shape=dict(dim=[1, -1, input_name_shape[1]])))
    layers.append(layer)

    return tuple(layers)

3. MatMul 的问题

这个还是先看图吧,直接点。
海思开发:Nanodet : onnx -> caffe
输入是三维数据,权值是二维的,虽然在 pc 上写了段代码,试验了下,发现这样的数据做矩阵运算是可以的,但是模型推理时数据还是不对。针对这样的情况,想着干脆加两个 reshape ,将其 shape 换过来后再做打算,流程如下:

#                     matmul
原始:(1, 10816, 11) ---------> (1, 10816, 1) 
#                     reshape                matmul               reshape
现在:(1, 10816, 11) ---------> (10816, 11) ---------> (10816, 1) ---------> (1, 10816, 1)

相关转换代码如下:

# onnx2caffe/_operators.py 文件中
def _convert_matmul_nanodet(node, graph, err):  # 建立网络结构图
    layers = []
    node_name = node.name
    output_name = str(node.outputs[0])  # 输出节点名
    input_name = str(node.inputs[0])  # 上层节点名

    reshape_1_out = node_name + "_reshape1_out"
    layer = myf("Reshape", node_name + "_reshape1", [input_name], [reshape_1_out],
                reshape_param=dict(shape=dict(dim=[-1, 11])))
    layers.append(layer)

    matmul_out = node_name + "_matmul_out"
    weight_name = node.inputs[1]  # 本层参数名
    if weight_name in node.input_tensors:  # 判断参数数组是否真的存在
        W = node.input_tensors[weight_name]  # 获得参数数组
    else:  # 没有的话也就没意义继续了
        err.missing_initializer(node,
                                "MatMul weight tensor: {} not found in the graph initializer".format(weight_name, ))
        return

    b = None
    bias_flag = False
    if len(node.inputs) > 2:  # 如果只有上层节点名和 W 权值,则为 2
        b = node.input_tensors[node.inputs[2]]
    # 权值 shape 不对,也没意义继续了
    if len(W.shape) != 2 or (b is not None and len(b.shape) != 1):
        return err.unsupported_op_configuration(node, "MatMul is supported only for inner_product layer")
    if b is not None:
        bias_flag = True
        if W.shape[1] != b.shape[0]:  # FC 中,二者 shape[0] 是输出通道数, 一定相等,shape[1] 是输入通道数。
            return err.unsupported_op_configuration(node,
                                                    "MatMul is supported only for inner_product layer")
    # 不同于 gemm ,matmul 不做转置操作,w = (A, B), A 是输入通道数, B 是输出通道数
    layer = myf("InnerProduct", node_name, [reshape_1_out], [matmul_out], num_output=W.shape[1], bias_term=bias_flag)
    graph.channel_dims[output_name] = W.shape[1]  # 获得输出通道数
    layers.append(layer)

    #reshape_2_out = node_name + "_reshape2_out"
    layer = myf("Reshape", node_name + "_reshape2", [matmul_out], [output_name],
                reshape_param=dict(shape=dict(dim=[1, -1, 1])))
    layers.append(layer)

    return tuple(layers)

主要是这三个问题吧,其余都是些小问题,就不写出来干扰大家视线了。

三、验证与后语

出于谨慎,对比转换前后模型差异是必不可少的,代码如下:

# 1. onnx 推理 demo
import onnxruntime
import numpy as np

session = onnxruntime.InferenceSession(r"nanodet_weight\m\model_deconv_796_bs16.onnx")
img = np.ones((1, 3, 416, 416)).astype(np.float32)

result = session.run(["236"], {"input.1": img})
print(result)

# 2. caffe 推理 demo
import caffe
import numpy as np

def caffe_test():
    weight_file = r"nanodet_weight\m\nanodet.caffemodel"
    model_file = r"nanodet_weight\m\nanodet.prototxt"
    net = caffe.Net(model_file, weight_file, caffe.TEST)
    img = np.ones((1, 3, 416, 416))

    net.blobs['input.1'].data[...] = img

    out = net.forward()

    print(out["236"].shape)
    print(out["236"])

结果表明,二者除了精度差异外,其余一模一样,所以各位放心使用!
最后,附上完整转换demo链接,仓促之下写成难免有误,还请各位多多包涵,谢谢。本篇博客是今年第一篇,一段时间没写,确实手生不少,难怪说 ”学如逆水行舟,不进则退“,以后要多总结,多分享。

上一篇:模型部署 ONNX ONNX runtim


下一篇:关系型数据库管理系统(RDBMS)与非关系型数据库(NoSQL)之间的区别