Press "Enter" to skip to content

实用教程详解:模型部署,用DNN模块部署YOLOv5目标检测(附源代码)

本站内容均来自兴趣收集,如不慎侵害的您的相关权益,请留言告知,我们将尽快删除.谢谢.

计算机视觉研究院专栏

 

作者:Edison_G

 

今天给大家分享一些实践的干货,主要是怎幺将训练好的网络
模型部署落地
。有兴趣的同学,请跟我们一起学习!

 

一、
什幺是模型部署?

 

在典型的机器学习和深度学习项目中,我们通常从定义问题陈述开始,然后是数据收集和准备(数据预处理)和模型构建(模型训练),对吧?
但是,最后,我们希望我们的模型能够提供给最终用户,以便他们能够利用它。模型部署是任何机器学习项目的最后阶段之一,可能有点棘手。如何将机器学习模型传递给客户/利益相关者?模型的部署大致分为以下三个步骤:

 

模型持久化

 

持久化,通俗得讲,就是临时数据(比如内存中的数据,是不能永久保存的)持久化为持久数据(比如持久化至数据库中,能够长久保存)。那我们训练好的模型一般都是存储在内存中,这个时候就需要用到持久化方式,在Python中,常用的模型持久化方式一般都是以文件的方式持久化。

 

选择适合的服务器加载已经持久化的模型

 

提高服务接口,拉通前后端数据交流

 

转自:https://www.zhihu.com/question/329372124/answer/2020888036

 

 

 

P
PLNN

 

二、案例,运行操作:

 

准备ONNX模型

 

我们在tests/testdata下准备了一个分类模型mnasnet0_5.onnx,可用于测试。

 

通过如下手段可以获取更多的ONNX模型:

 

可以从OpenMMLab/PyTorch导出ONNX模型:model-convert-guide.md

 

从ONNX Model Zoo获取模型:https://github.com/onnx/models

 

ONNX Model Zoo的模型opset版本都较低,可以通过tools下的convert_onnx_opset_version.py将opset转换为11:

 

python convert_onnx_opset_version.py --input_model input_model.onnx --output_model output_model.onnx --output_opset 11

 

转换opset具体请参考:
onnx-model-opset-convert-guide.md

 

准备测试图片

 

测试图片使用任何格式均可。我们在tests/testdata下准备了cat0.png和cat1.jpg(ImageNet 的验证集图片):

 

任意大小的图片都可以正常运行,如果想要resize到224 x 224的话,可以修改程序里的如下变量:

 

const bool resize_input = false; // 想要resize的话,修改为true即可

 

测试推理服务

 

运行

 

pplnn-build/samples/cpp/run_model/classification <image_file> <onnx_model_file>

 

推理完成后,会得到如下输出:

 

image preprocess succeed!
[INFO][2021-07-23 17:29:31.341][simple_graph_partitioner.cc:107] total partition(s) of graph[torch-jit-export]: 1.
successfully create runtime builder!
successfully build runtime!
successfully set input data to tensor [input]!
successfully run network!
successfully get outputs!
top 5 results:
1th: 3.416199   284        n02123597 Siamese cat, Siamese
2th: 3.049764   285        n02124075 Egyptian cat
3th: 2.989676   606        n03584829 iron, smoothing iron
4th: 2.812310   283        n02123394 Persian cat
5th: 2.796991   749        n04033901 quill, quill pen

 

不难看出,这个程序正确判断猫是真猫。
至此OpenPPL的安装与图像分类模型推理已完成。
另外,在pplnn-build/tools目录下有可执行文件pplnn,可以进行任意模型推理、dump输出数据、benchmark等操作,具体用法可使用–help选项查看。
大家可以基于该示例进行改动,从而更熟悉OpenPPL的用法。

 

三、DNN模块部署Yolov5

 

用opencv的dnn模块做yolov5目标检测的程序,包含两个步骤:1)、把pytorch的训练模型pth文件转换到onnx文件;2)、opencv的dnn模块读取onnx文件做前向计算。

 

1)、把pytorch的训练模型pth文件转换到onnx文件

 

yolov5
官方代码:
https://github.com/ultralytics/yolov5

 

这套程序里的代码比较乱,在pytorch里,通常是在py文件里定义网络结构的,但是官方代码是在yaml文件定义网络结构,利用pytorch动态图特性,解析yaml文件自动生成网络结构。

 

在yaml文件里有depth_multiple和width_multiple,它是控制网络的深度和宽度的参数。这幺做的好处是能够灵活的配置网络结构,但是不利于理解网络结构,假如你想设断点查看某一层的参数和输出数值,那就没办法了。

 

 

 

因此,在编写的转换到onnx文件的程序里,网络结构是在py文件里定义的。其次,在官方代码里,还有一个奇葩的地方,那就是pth文件。起初,下载官方代码到本地运行时,torch.load读取pth文件总是出错,后来把pytorch升级到1.7,就读取成功了。可以看到
版本兼容性不好
,这是它的一个不足之处。设断点查看读取的pth文件里的内容,可以看到ultralytics的pt文件里既存储有模型参数,也存储有网络结构,还储存了一些超参数,包括anchors,stride等等。

 

self.register_buffer('anchors', a)  #shape(nl,na,2)
self.register_buffer('anchor_grid', a.clone().view(self.nl, 1, -1, 1, 1, 2)) 

 

尝试过把这两行代码改成:

 

self.anchors = a
self.anchor_grid = a.clone().view(self.nl, 1, -1, 1, 1, 2)

 

程序依然能正常运行,但是torch.save保存模型文件后,可以看到pth文件里没有存储anchors和anchor_grid了,在百度搜索register_buffer,解释是:
pytorch中register_buffer模型保存和加载的时候可以写入和读出。
在这两行代码的下一行:

 

self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch)   # output conv

 

它的作用是做特征图的输出通道对齐,通过1×1卷积把三种尺度特征图的输出通道都调整到num_anchors*(num_classes+5)。

读Detect类的forward函数代码,可以看出它的作用是根据偏移公式计算出预测框的中心坐标和高宽,这里需要注意的是,计算高和宽的代码:

 

pwh = (ps[:, 2:4].sigmoid() * 2) ** 2 * anchors[i]

 

没有采用exp操作,而是直接乘上anchors[i],这是
yo
lov5与yolov3v4的一个最大区别
(还有一个区别就是在训练阶段的loss函数里,yolov5采用邻域的正样本anchor匹配策略,增加了正样本。其它的是一些小区别,比如yolov5的第一个模块采用FOCUS把输入数据2倍下采样切分成4份,在channel维度进行拼接,然后进行卷积操作,yolov5的激活函数没有使用Mish)。

 

现在可以明白Detect类的作用是计算预测框的中心坐标和高宽,简单来说就是生成proposal,作为后续NMS的输入,进而输出最终的检测框。我觉得在Detect类里定义的1×1卷积是不恰当的,应该把它定义在Detect类的外面,紧邻着Detect类之前定义1×1卷积。

 

在官方代码里,有转换到onnx文件的程序:

 

python models/export.py --weights yolov5s.pt --img 640 --batch 1

 


pytorch1.7版本里,程序是能正常运行生成onnx文件的。
观察export.
py里的代码,在执行torch.onnx.export之前,有这幺一段代码:

 

# Input
img = torch.zeros(opt.batch_size, 3, *opt.img_size)   # image size (1, 3, 320, 192) iDetection


# Update model
for k, m in model.named_modules():
    m._non_persistent_buffers_set = set()    # pytorch 1.6.0 compatibility
    if isinstance(m, models.common.Conv):    # assign export-friendly activations
        if isinstance(m.act, nn.Hardswish):
            m.act = Hardswish()
        elif isinstance(m.act, nn.SiLU):
            m.act = SiLU()
    # elif isinstance(m, models.yolo.Detect):
    #     m.forward = m.forward_export     #assign  forward (optional)
model.model[-1].export = True   # set Detect() Layer export = True
y = model(img)   # dry run

 

注意其中的for循环,我试验过注释掉它,重新运行就会出错,打印出的错误如下:

 

由此可见,这段for循环代码是必需的。SiLU其实就是swish激活函数,而在onnx模型里是不直接支持swish算子的,因此在转换生成onnx文件时,SiLU激活函数不能直接使用nn.Module里提供的接口,而需要自定义实现它。

 

2)、opencv的dnn模块读取.onnx文件做前向计算

 

在生成onnx文件后,就可以用opencv的dnn模块里的cv2.dnn.readNet读取它。然而,在读取时,出现了如下错误:

 

 

 

其实是:

 

 

 

于是查看yolov5的代码,在common.py文件的Focus类,torch.cat的输入里有4次切片操作,代码如下:

 

 

 

那幺现在需要更换索引式的切片操作,观察到注释的Contract类,它就是用view和permute函数完成切片操作的,于是修改代码如下:

 

 

 

其次,在models\yolo.py里的Detect类里,也有切片操作,代码如下:

 

y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i].to(x[i].device)) * self.stride[i]
y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i]   # wh

 

前面说过,Detect类的作用是计算预测框的中心坐标和高宽,生成proposal,这个是属于后处理的,因此不需要把它写入到onnx文件里。

 

总结一下,按照上面的截图代码,修改Focus类,把Detect类里面的1×1卷积定义在紧邻着Detect类之前的外面,然后去掉Detect类,组成新的model,作为torch.onnx.export的输入:

 

torch.onnx.export(model, inputs, output_onnx, verbose=False, opset_version=12, input_names=['images'], output_names=['out0', 'out1', 'out2'])

 

最后生成的onnx文件,opencv的dnn模块就能成功读取了,接下来对照Detect类里的forward函数,用python或者C++编写计算预测框的中心坐标和高宽的功能。

 

在github上,地址是
https://github.com/hpc203/yolov5-dnn-cpp-python

 

四、后处理模块

 

后处理模块,python版本用numpy array实现的,C++版本的用vector和数组实现的,整套程序只依赖opencv库(opencv4版本以上的)就能正常运行,彻底摆脱对深度学习框架pytorch,tensorflow,caffe,mxnet等等的依赖。用openvino作目标检测,需要把onnx文件转换到.bin和.xml文件,相比于用dnn模块加载onnx文件做目标检测是多了一个步骤的。因此,我就想编写一套用opencv的dnn模块做yolov5目标检测的程序,用opencv的dnn模块做深度学习目标检测,在win10和ubuntu,在cpu和gpu上都能运行,可见dnn模块的通用性更好,很接地气。

 

生成yolov5s_param.pth 的步骤:

 

首先下载
https://github.com/ultralytics/yolov5
的源码到本地,在yolov5-master主目录(注意不是我发布的github代码目录)里新建一个.py文件,把下面的代码复制到.py文件里。

 

import torch
from collections import OrderedDict
import pickle
import os
 
device = 'cuda' if torch.cuda.is_available() else 'cpu'
 
if __name__=='__main__':
    choices = ['yolov5s', 'yolov5l', 'yolov5m', 'yolov5x']
    modelfile = choices[0]+'.pt'
    utl_model = torch.load(modelfile, map_location=device)
    utl_param = utl_model['model'].model
    torch.save(utl_param.state_dict(), os.path.splitext(modelfile)[0]+'_param.pth')
    own_state = utl_param.state_dict()
    print(len(own_state))
 
    numpy_param = OrderedDict()
    for name in own_state:
        numpy_param[name] = own_state[name].data.cpu().numpy()
    print(len(numpy_param))
    with open(os.path.splitext(modelfile)[0]+'_numpy_param.pkl', 'wb') as fw:
        pickle.dump(numpy_param, fw)

 

运行这个.py文件,这时候就可以生成yolov5s_param.pth文件。之所以要进行这一步,我在上面讲到过:ultralytics的.pt文件里既存储有模型参数,也存储有网络结构,还储存了一些超参数,包括anchors,stride等等的。torch.load加载ultralytics的官方.pt文件,也就是utl_model = torch.load(modelfile, map_location=device)这行代码,在这行代码后设断点查看utl_model里的内容,截图如下:

 

 

 

可以看到utl_model里含有既存储有模型参数,也存储有网络结构,还储存了一些超参数等等的,这会严重影响转onnx文件。此外,我还发现,如果pytorch的版本低于1.7,那幺在torch.load加载.pt文件时就会出错的。

 

因此在程序里,我把模型参数转换到cpu.numpy形式的,最后保存在.pkl文件里。这时候在win10系统cpu环境里,即使你的电脑没有安装pytorch,也能通过python程序访问到模型参数。

 

五、pytorch转onnx常见坑:

 

onnx只能输出静态图,因此不支持if-else分支。一次只能走一个分支。如果代码中有if-else语句,需要改写。

 

onnx不支持步长为2的切片。例如a[::2,::2]

 

onnx不支持对切片对象赋值。例如a[0,:,:,:]=b, 可以用torch.cat改写

 

onnx里面的resize要求output shape必须为常量。可以用以下代码解决:

 

if isinstance(size, torch.Size):
    size = tuple(int(x) for x in size)

 

此外,在torch.onnx.export(model, inputs, output_onnx)的输入参数model里,应该只包含网络结构,也就是说model里只含有nn.Conv2d, nn.MaxPool2d, nn.BatchNorm2d, F.relu等等的这些算子组件,而不应该含有后处理模块的。图像预处理和后处理模块需要自己使用C++或者Python编程实现。

 

在明白了这些之后,在转换生成onnx文件,你需要执行两个步骤,第一步把原始训练模型.pt文件里的参数保存到新的.pth文件里,第二步编写yolov5.py文件,把yolov5的往来结构定义在.py文件里,此时需要注意网络结构里不能包含切片对象赋值操作,F.interpolate里的size参数需要加int强制转换。在执行完这两步之后才能生成一个opencv能成功读取并且做前向推理的onnx文件。

 

不过,最近我发现在yolov5-pytorch程序里,其实可以直接把原始训练模型.pt文件转换生成onnx文件的,而且我在一个yolov5检测人脸+关键点的程序里实验成功了。

 

Be First to Comment

发表评论

您的电子邮箱地址不会被公开。