当前位置:   article > 正文

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

torch-jit-export

关注并星标

从此不迷路

计算机视觉研究院

06da105953a213aaa391e22866c44c68.gif

a4548d68fa121edb6257ecad662e6835.gif

公众号ID|ComputerVisionGzq

学习群|扫码在主页获取加入方式

计算机视觉研究院专栏

作者:Edison_G

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

一、什么是模型部署?

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

  • 模型持久化

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

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

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

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

bdeb102da4e55133c5d0789404aca9b7.png

PPLNN

二、案例,运行操作:

  • 准备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 的验证集图片):

a68f94612d699e0f4881844814eb115d.png

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

const bool resize_input = false; // 想要resize的话,修改为true即可
  • 测试推理服务

运行

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

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

  1. image preprocess succeed!
  2. [INFO][2021-07-23 17:29:31.341][simple_graph_partitioner.cc:107] total partition(s) of graph[torch-jit-export]: 1.
  3. successfully create runtime builder!
  4. successfully build runtime!
  5. successfully set input data to tensor [input]!
  6. successfully run network!
  7. successfully get outputs!
  8. top 5 results:
  9. 1th: 3.416199 284 n02123597 Siamese cat, Siamese
  10. 2th: 3.049764 285 n02124075 Egyptian cat
  11. 3th: 2.989676 606 n03584829 iron, smoothing iron
  12. 4th: 2.812310 283 n02123394 Persian cat
  13. 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,它是控制网络的深度和宽度的参数。这么做的好处是能够灵活的配置网络结构,但是不利于理解网络结构,假如你想设断点查看某一层的参数和输出数值,那就没办法了。

5f93449a964c8e258ed3d0049bd21779.png

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

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

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

  1. self.anchors = a
  2. 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

它的作用是做特征图的输出通道对齐,通过1x1卷积把三种尺度特征图的输出通道都调整到num_anchors*(num_classes+5)。阅读Detect类的forward函数代码,可以看出它的作用是根据偏移公式计算出预测框的中心坐标和高宽,这里需要注意的是,计算高和宽的代码:

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

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

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

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

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

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

  1. # Input
  2. img = torch.zeros(opt.batch_size, 3, *opt.img_size)   # image size (1, 3, 320, 192) iDetection
  3. # Update model
  4. for k, m in model.named_modules():
  5.     m._non_persistent_buffers_set = set() # pytorch 1.6.0 compatibility
  6.     if isinstance(m, models.common.Conv): # assign export-friendly activations
  7.      if isinstance(m.act, nn.Hardswish):
  8.             m.act = Hardswish()
  9.         elif isinstance(m.act, nn.SiLU):
  10.          m.act = SiLU()
  11.     # elif isinstance(m, models.yolo.Detect):
  12.     # m.forward = m.forward_export #assign forward (optional)
  13. model.model[-1].export = True   # set Detect() Layer export = True
  14. y = model(img) # dry run

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

496e93e234737e3a36d7508a1a4217f9.png

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

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

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

a014628f3f82d4e734e208df9ee48d5f.png

其实是:

e01d91644fc33fd279e6270da11166aa.png

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

cf0c2d9e6aab7b77b8b933960be578cc.png

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

9d8b596b10dc31af5e04784de4ab56bd.png

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

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

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

总结一下,按照上面的截图代码,修改Focus类,把Detect类里面的1x1卷积定义在紧邻着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文件里。

  1. import torch
  2. from collections import OrderedDict
  3. import pickle
  4. import os
  5. device = 'cuda' if torch.cuda.is_available() else 'cpu'
  6. if __name__=='__main__':
  7. choices = ['yolov5s', 'yolov5l', 'yolov5m', 'yolov5x']
  8. modelfile = choices[0]+'.pt'
  9. utl_model = torch.load(modelfile, map_location=device)
  10. utl_param = utl_model['model'].model
  11. torch.save(utl_param.state_dict(), os.path.splitext(modelfile)[0]+'_param.pth')
  12. own_state = utl_param.state_dict()
  13. print(len(own_state))
  14. numpy_param = OrderedDict()
  15. for name in own_state:
  16. numpy_param[name] = own_state[name].data.cpu().numpy()
  17. print(len(numpy_param))
  18. with open(os.path.splitext(modelfile)[0]+'_numpy_param.pkl', 'wb') as fw:
  19. 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里的内容,截图如下:

24c5d35d7e98766dfbd879d0a17625b6.png

可以看到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必须为常量。可以用以下代码解决:

  1. if isinstance(size, torch.Size):
  2. 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检测人脸+关键点的程序里实验成功了。

© 未完待续

转载请联系本公众号获得授权

33f3bdecbe81d5e824cfe8fe63a599a7.gif

计算机视觉研究院学习群等你加入!

计算机视觉研究院主要涉及深度学习领域,主要致力于人脸检测、人脸识别,多目标检测、目标跟踪、图像分割等研究方向。研究院接下来会不断分享最新的论文算法新框架,我们这次改革不同点就是,我们要着重”研究“。之后我们会针对相应领域分享实践过程,让大家真正体会摆脱理论的真实场景,培养爱动手编程爱动脑思考的习惯!

51caad66f505e89116944d9f60a3295b.png

扫码关注

计算机视觉研究院

公众号ID|ComputerVisionGzq

学习群|扫码在主页获取加入方式

 往期推荐 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/190897
推荐阅读
相关标签