模型部署 ONNX模型转换说明

ONNX

更多代码可以参考我的github:ONNX

ONNX底层实现

ONNX 的存储格式
ONNX 在底层是用 Protobuf 定义的。Protobuf,全称 Protocol Buffer,是 Google 提出的一套表示和序列化数据的机制。使用 Protobuf 时,用户需要先写一份数据定义文件,再根据这份定义文件把数据存储进一份二进制文件。可以说,数据定义文件就是数据类,二进制文件就是数据类的实例。
这里给出一个 Protobuf 数据定义文件的例子:

message Person { 
  required string name = 1; 
  required int32 id = 2; 
  optional string email = 3; 
} 

这段定义表示在Person这种数据类型中,必须包含 name、id 这两个字段,选择性包含 email字段。根据这份定义文件,用户就可以选择一种编程语言,定义一个含有成员变量nameidemailPerson 类,把这个类的某个实例用 Protobuf 存储成二进制文件;反之,用户也可以用二进制文件和对应的数据定义文件,读取出一个 Person类的实例。

而对于 ONNX ,Protobuf 的数据定义文件在其开源(https://github.com/onnx/onnx/tree/main/onnx),这些文件定义了神经网络中模型、节点、张量的数据类型规范;而二进制文件就是我们熟悉的“.onnx"文件,每一个 onnx 文件按照数据定义规范,存储了一个神经网络的所有相关数据。直接用 Protobuf 生成 ONNX 模型还是比较麻烦的。幸运的是,ONNX 提供了很多实用 API,我们可以在完全不了解 Protobuf 的前提下,构造和读取 ONNX 模型。

ONNX的结构定义

神经网络本质上是一个计算图,计算图的节点是算子,边是参与运算的张量,通过netron可视化ONNX模型,可以直观看到ONNX记录的所有节点的属性信息,并把参与运算的张量信息存储在算子节点的输入输出信息中。ONNX 模型的结构可以用类图大致表示如下:

在这里插入图片描述

构造ONNX模型

从上图可以ONNX模型是由以下几个部分构造的:

  • ModelProto
  • NodeProto
  • ValueInfoProto
  • GraphProto

尝试完全用 ONNX 的 Python API 构造一个描述线性函数output=a*x+b的ONNX模型,代码如下所示:

import onnx 
from onnx import helper 
from onnx import TensorProto 
 
# input and output 
a = helper.make_tensor_value_info('a', TensorProto.FLOAT, [10, 10]) 
x = helper.make_tensor_value_info('x', TensorProto.FLOAT, [10, 10]) 
b = helper.make_tensor_value_info('b', TensorProto.FLOAT, [10, 10]) 
output = helper.make_tensor_value_info('output', TensorProto.FLOAT, [10, 10]) 
 
# Mul 
mul = helper.make_node('Mul', ['a', 'x'], ['c']) 
 
# Add 
add = helper.make_node('Add', ['c', 'b'], ['output']) 
 
# graph and model 
graph = helper.make_graph([mul, add], 'linear_func', [a, x, b], [output]) 
model = helper.make_model(graph) 
 
# save model 
onnx.checker.check_model(model) 
print(model) 
onnx.save(model, 'linear_func.onnx') 

运行验证ONNX模型:

import onnxruntime 
import numpy as np 
 
sess = onnxruntime.InferenceSession('linear_func.onnx') 
a = np.random.rand(10, 10).astype(np.float32) 
b = np.random.rand(10, 10).astype(np.float32) 
x = np.random.rand(10, 10).astype(np.float32) 
 
output = sess.run(['output'], {'a': a, 'b': b, 'x': x})[0] 
 
assert np.allclose(output, a * x + b) 

相关案例

pytorch

pytorch转ONNX的API主要讲一下dynamic_axes这个参数。由于Pytorch的Input的flexible特性(见https://stackoverflow.com/questions/66488807/pytorch-model-input-shape),不像Tensorflow那样需要指定input的shape,因此在onnx转换的时候可能需要指定一些动态的输入。如cityscapes的分割模型PIDNet,在尝试转换成ONNX模型的时候,它输入图像的长宽可以是动态变长的,因此可以进行如下设定,表示图像长宽为可变输入。

onnx_file_name = './model/PIDNet_S_Cityscapes_val_13.onnx'
x = torch.randn((1, 3, 12, 12), requires_grad=True)
torch.onnx.export(model,x,onnx_file_name,
                          export_params=True,
                          verbose=True,
                          opset_version=11,
                          do_constant_folding=True,
                          dynamic_axes={'input.1':[2,3]})

在netron上查看模型,可以看到图像的长宽为动态输入:
在这里插入图片描述

关于pytorch flexibility说明如下:

在这里插入图片描述

torch.onnx.export(model, args, f, export_params, verbose=False, training= False)

#Function to Convert to ONNX 
def Convert_ONNX(): 

    # set the model to inference mode 
    model.eval() 

    # Let's create a dummy input tensor  
    dummy_input = torch.randn(1, 3, 32, 32, requires_grad=True)  

    # Export the model   
    torch.onnx.export(model,         # model being run 
         dummy_input,       # model input (or a tuple for multiple inputs) 
         "ImageClassifier.onnx",       # where to save the model  
         export_params=True,  # store the trained parameter weights inside the model file 
         opset_version=10,    # the ONNX version to export the model to 
         do_constant_folding=True,  # whether to execute constant folding for optimization 
         input_names = ['modelInput'],   # the model's input names  可选参数
         output_names = ['modelOutput'], # the model's output names  可选参数
         dynamic_axes={'modelInput' : {0 : 'batch_size'},    # variable length axes 
                                'modelOutput' : {0 : 'batch_size'}}) 
    print(" ") 
    print('Model has been converted to ONNX') 

以YOLOV4为例。

首先看一下pytorch Yolov4的网络结构,如下代码所示。它的输入是[batchsize, channel, img_height, img_width],输出是[box, confs]

def transform_to_onnx(weight_file, batch_size, n_classes, IN_IMAGE_H, IN_IMAGE_W):
    
    model = Yolov4(n_classes=n_classes, inference=True)

    pretrained_dict = torch.load(weight_file, map_location=torch.device('cpu'))
    model.load_state_dict(pretrained_dict)

    input_names = ["input"]
    output_names = ['boxes', 'confs']

    dynamic = False
    if batch_size <= 0:
        dynamic = True

    if dynamic:
        x = torch.randn((1, 3, IN_IMAGE_H, IN_IMAGE_W), requires_grad=True)
        onnx_file_name = "./models/yolov4_-1_3_{}_{}_dynamic.onnx".format(IN_IMAGE_H, IN_IMAGE_W)
        dynamic_axes = {"input": {0: "batch_size"}, "boxes": {0: "batch_size"}, "confs": {0: "batch_size"}}
        # Export the model
        print('Export the onnx model ...')
        torch.onnx.export(model,
                          x,
                          onnx_file_name,
                          export_params=True,
                          opset_version=11,
                          do_constant_folding=True,
                          input_names=input_names, output_names=output_names,
                          dynamic_axes=dynamic_axes)

        print('Onnx model exporting done')
        return onnx_file_name

    else:
        x = torch.randn((batch_size, 3, IN_IMAGE_H, IN_IMAGE_W), requires_grad=True)
        onnx_file_name = "./models/yolov4_{}_3_{}_{}_static.onnx".format(batch_size, IN_IMAGE_H, IN_IMAGE_W)
        # Export the model
        print('Export the onnx model ...')
        torch.onnx.export(model, #pytorch模型
                          x, #input,目的是onnx用该数据运行一次模型,了解计算过程,进行编译
                          onnx_file_name, #保存文件名
                          export_params=True, # 如果为True则导出模型和模型权重,否则导出为训练的模型
                          opset_version=11, #默认情况下,将模型导出到ONNX子模板的opset版本。默认为9。
                          do_constant_folding=True, #如果为 True,则在导出期间将恒定折叠优化应用于模型。 常量折叠优化将用预先计算的常量节点替换一些具有所有常量输入的操作。
                          input_names=input_names, #(字符串列表 , 默认空列表)–依次分配给图形输入节点的名称 可选参数
                          output_names=output_names,# (字符串列表 , 默认空列表)–依次分配给图形输出节点的名称 可选参数
                          dynamic_axes=None)

        print('Onnx model exporting done')
        return onnx_file_name

keras

只需要通过pip install keras2onnx就可以安装keras2onnx转换工具,该代码不完整,完整代码请参考:unet

import colorsys
import copy
import time

import numpy as np
from PIL import Image

from nets.unet import Unet as unet
import onnx
import keras2onnx
import onnxruntime
import cv2

class Unet(object):
    def __init__(self, **kwargs):
        _defaults = {
            "model_path"        : kwargs['model'],
            "model_image_size"  : kwargs['model_image_size'],
            "num_classes"       : kwargs['num_classes']
        }
        self.onnx_model = kwargs['onnxmodel']
        self.__dict__.update(_defaults)
        self.generate()

    def generate(self):
        self.model = unet(self.model_image_size, self.num_classes)

        self.model.load_weights(self.model_path)
        print('{} model loaded.'.format(self.model_path))

        # 导出为onnx模型
        onnx_model = keras2onnx.convert_keras(self.model, self.model.name)
        onnx.save_model(onnx_model, self.onnx_model)

    def detect_image(self, image):
        image = image.convert('RGB')
        old_img = copy.deepcopy(image)
        orininal_h = np.array(image).shape[0]
        orininal_w = np.array(image).shape[1]
        img, nw, nh = self.letterbox_image(image,(self.model_image_size[1],self.model_image_size[0]))
        img = np.asarray([np.array(img).astype(np.float32) /255])

        # 使用onnx进行推理预测
        sess = onnxruntime.InferenceSession(self.onnx_model)
        x = img if isinstance(img, list) else [img]
        feed = dict([(input.name, x[n]) for n, input in enumerate(sess.get_inputs())])
        pred_onnx = sess.run(None, feed)[0]
        pred_onnx = np.squeeze(pred_onnx)

        # 使用模型进行推理预测
        pr = self.model.predict(img)[0]
        pr = pr.argmax(axis=-1).reshape([self.model_image_size[0],self.model_image_size[1]])
        pr = pr[int((self.model_image_size[0]-nh)//2):int((self.model_image_size[0]-nh)//2+nh), int((self.model_image_size[1]-nw)//2):int((self.model_image_size[1]-nw)//2+nw)]

        seg_img = np.zeros((np.shape(pr)[0],np.shape(pr)[1],3))
        for c in range(self.num_classes):
            seg_img[:,:,0] += ((pr[:,: ] == c )*( self.colors[c][0] )).astype('uint8')
            seg_img[:,:,1] += ((pr[:,: ] == c )*( self.colors[c][1] )).astype('uint8')
            seg_img[:,:,2] += ((pr[:,: ] == c )*( self.colors[c][2] )).astype('uint8')

        image = Image.fromarray(np.uint8(seg_img)).resize((orininal_w,orininal_h), Image.NEAREST)
        blend_image = Image.blend(old_img,image,0.3)

        return image, blend_image

 

推理过程:

# 使用onnx进行推理预测
sess = onnxruntime.InferenceSession(self.onnx_model)
x = img if isinstance(img, list) else [img]
feed = dict([(input.name, x[n]) for n, input in enumerate(sess.get_inputs())])
pred_onnx = sess.run(None, feed)[0]
pred_onnx = np.squeeze(pred_onnx)

PaddleOCR

PaddleOCR 以及PaddleONNX的repo地址:https://github.com/PaddlePaddle/PaddleOCR和https://github.com/PaddlePaddle/Paddle2ONNX。在这里主要是介绍如何将ch_ppocr_server_v2.0_xx的rec模型转换成ONNX模型。介绍这个原因是paddleocr的repo上ch_ppocr_server_v2.0_rec inference模型不支持动态输入,因此需要重新将pretrain model加载导出成inference模型,然后通过paddle2onnx转换成onnx模型。

在这里插入图片描述

  1. 首先需要配置好paddle,paddleocr,paddle2onnx环境。然后将paddleocr的repo git clone到本地。我的目录结构如下图所示。ch_ppocr_server_v2.0_rec 的pretrain 保存在paddleocrmodel下,导出的inference保存在tmp下。
    在这里插入图片描述

  2. 执行以下命令:

python tools/export_model.py -c ./configs/rec/ch_ppocr_v2.0/rec_chinese_common_train_v2.0.yml -o \
Global.pretrained_model=./paddleocrmodel/ch_ppocr_server_v2.0_rec_pre/best_accuracy Global.save_inference_dir=./tmp/
  1. 使用paddle2onnx转换
paddle2onnx --model_dir ./tmp  --model_filename inference.pdmodel --params_filename inference.pdiparams --save_file model.onnx --opset_version 10 --enable_onnx_checker True
  1. 最终使用的推理结果:

在这里插入图片描述

参考

  1. pytorch-YOLOv4
  2. torch Onnx
  3. 模型部署入门教程:ONNX 模型的修改与调试
  4. Onnx tutorials
  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值