嘮一嘮對AI煉丹師的模型部署的探索
點擊下方卡片,關注「集智書童」公眾號
作者丨matrix明仔@知乎 來源丨h(huán)ttps://zhuanlan.zhihu.com/p/557709588 編輯丨小書童
1、內容介紹
這期內容是@走走大佬關于目標檢測模型End to End推理方案的探索和嘗試。其實說到推理和部署,其實怎么也繞不開ONNX,ONNX在成立的初衷就是希望解決神經網絡在不同的訓練框架、推理框架上的轉換問題。 所以本期的內容會從如何玩轉ONNX出發(fā),嘮一嘮,我們在目標檢測部署遇到的那些事情。因為篇幅以及有部分內容我不太了解不敢亂說的關系,我會在這里對開放麥的內容做一點順序和內容上進行一點的調整,我也會加入自己的一些經歷和看法,讓大家看得更加輕松有趣一點。
2、ONNX是什么,如何生成ONNX(ONNX簡要的介紹)?
預告:下面用三種方法向大家介紹如何生成Relu的ONNX模型,那么哪種方法才是最強的ONNX的生成方法呢?大家可以思考一下,我們繼續(xù)往下看~
2.1、ONNX的組成
ONNX的靜態(tài)圖主要由「Node」(節(jié)點),「Input」(輸入)和「initializer」(初始化器)但部分所組成的。
- 節(jié)點就代表了神經網絡模型一層的layer
- 輸入代表了輸入矩陣的維度信息
- 初始化器通常是存儲權重/權值的。
- 每個組件元素都是hierarchical的結構,都是有著相互依賴關系的;
- 這是一個雙向的鏈表。(Node、Graph彼此關聯(lián)有相互關系的);
大家覺得難改,其實很大一部分也是因為ONNX的結構,邊與邊是一個穩(wěn)定的結構關系,彼此很大程度上是相互依賴的。所以我們具體要怎么轉化模型,怎么修改模型呢?我們接著看下去~
2.2、Pytorch導出ONNX模型
Pytorch是可以直接導出ONNX的模型,然后我們可以把導出的模型使用Netron的工具進行可視化工具。
Pytorch -> ONNX2.3、Numpy出發(fā),揉一個數據結構是可行嗎?
ONNX可以在Pytorch,通過轉換得到。那么我們假如我們不用Pytorch上的轉換,從零開始直接用Numpy人手揉一個ONNX模型是可行的嗎?答案是可行的。
ONNX是用protobuf數據格式進行保存的。而protobuf本身也是跨語言的可以支持C, C++, C#, Java, Javascript, Objective-C, PHP, Python, Ruby,使用ONN下的helper fuction就可以幫助我們順利的完成這些跨語言的轉變,所以Numpy也自然可以使用helper函數揉出輸入、節(jié)點以及初始化權值。

根據上圖展示的情況,可想而知要想實現一個能用的模型整體的代碼量是非??植赖摹我粋€Relu結構的代碼量就要比Pytorch的轉化(最上面的圖)實現要多將近三倍左右。(其實最大的代碼量還是出現在輸入的數據類型轉化上)如果要實現一個完整的深度模型轉化,工作量可想而知,所以我們有沒有其他的更科學高效一點的做法呢?
2.4、ONNX GraphSurgeon Basics
如果我們從ONNX出發(fā)要修改ONNX,其實是一個比較復雜的過程。那自然我們就會思考,那如果直接ONNX轉ONNX困難的化,能不能借助點工具,也就是有沒有更好的IR(中間表達)來幫助修改ONNX模型呢?
沒錯「TensorRT」就已經做出來一套有效幫助Python用戶修改ONNX的工具GraphSurgeon(圖手術刀)
這款IR主要有三部分組成
- Tensor——分為兩個子類:變量和常量。
- 節(jié)點——在圖中定義一個操作。可以放任何的Python 原始類型(list、dict),也可以放Graph或者Tensor。
- 圖表——包含零個或多個節(jié)點和輸入/輸出張量。
目的就是為了更好的編輯ONNX
ONNX GraphSurgeon轉化Relu結構有了輸入,再使用圖手術刀對模型的結構進行組合,最后完成了Relu在ONNX上的轉化。
3、如何在 ONNX 上進行圖手術
因為ONNX本身是一種hierarchical的設計,這種其實就是一種經典的計算機思路。當我們打算動其中一層的時候,因為上下游的關聯(lián),下游的框架也會跟著被修改。
3.1、ONNX的IR,我們需要一個友好的中間表達!
比如在目標檢測的場景中,我們有兩種數據標注表達「txt」和「mscoco」。如果是一個區(qū)分train集和val集的工作的話,txt直接把標簽隨機分開兩組就行。但是在coco數據集上,如果需要劃分val集的時候,就要對json格式進行劃分,還需要遵循一個圖片和標注信息一一對應的關系,就會更加復雜。那ONNX的轉化也是同理,所以理論上為了簡便這個整個修改的工程,我們需要一個IR工具的一個中間表達來幫助我們進行修改。
3.2、GraphSurgeon IR
提供了豐富的API來幫我們進行表達。
沒有邊的信息,邊的信息存在了輸入輸出中,所以ONNX需要對模型的信息進行拓撲排序。平時的手,我們在使用Pytorch導出ONNX模型的時候會發(fā)現有孤立的節(jié)點。如果對這個模型進行拓撲排序的話,會發(fā)現這個孤立算子是沒有意義的,應該是需要處理掉這個冗余的算子。
3.3、TorchScript (具體的使用)
首先Pytorch是一個動態(tài)圖,我們需要把Pytorch轉變?yōu)镺NNX的靜態(tài)圖!
Torchscript模式主要分為Tracing和Script,區(qū)分是用「Tracing」還是「Script」,主要是看是否是動態(tài)流。
Tracing會從頭到腳執(zhí)行一遍,記錄下來所有的函數
如果遇到動態(tài)控制流「(if-else)Script」 會走其中的一條,執(zhí)行哪條就會記錄哪條,另一條就會忽略。并且Script對不同大小的輸入有效。
「一般都建議Trace,因為除了ONNX_runtime,別的都不會人if這個動態(tài)算子。」
3.4、Symbolic
某些情況下,幫助模型在端側落地,在轉換后也能夠達到很好的效果。Python語言和c++本質上不一致,找到所以還是希望找到一種方式可以直接轉換,不用自己再手動寫C++算子,這算是一件很棒的事情。

NMS_F是一個很平常的一個算子,在上圖的實現。如果我們用Symbolic的函數會直接轉出,整個后處理都可以用ONNX轉出來,但是這種方法
3.5、ONNX GraphSurgeon

如何把上面的結構轉換為下面的結構,大概需要做到是吧x0的輸入分支給去掉,然后再加入LeakyReLU和Identity的節(jié)點,最后完成輸出。
-
先找到add的算子,把add的名字改成LeakyReLU
-
補充attribute屬性,加入alpha
-
指定一個輸出屬性identuty_out
-
然后再把identity節(jié)點加入到結構當中
-
先clean一些,再進行拓撲排序。
操作的代碼展示這個任務其實也可以用ONNX原生的IR來做,但是原生IR沒有太多的幫助函數,很多工具鏈還是不太完善的,所以還是建議使用ONNX GraphSurgeon,因為用原生的IR的代碼量一頁肯定是寫不完的了。
3.6、torch.fx
因為這塊沒有聽太懂,所以就直接簡單的吹一些沒啥用的,大家可以放松一下看看!
從以前的量化進化到現在也能涉及一部分的圖手術。其實為了能夠在python上也能進行圖手術上,在symbolic tracer的底層上也做了很多不一樣的工作,但是在轉化過程中也是有很多坑的,但是因為Python-to-Python會更加有利于模型訓練師的開發(fā),Pytorch的工程師也正在繼續(xù)發(fā)展,「正在逐步舍棄TorchScript」。
利用torch.fx+ONNX做量化可以極大的節(jié)省代碼量,是一個很棒的工作。

4、Focus模塊替換(部署的技巧)
Focus在yolov5提出來的!
Focus包括了兩部分組成「Space2Depth + Conv3」。有一點值得注意的是,Focus在實現過程focus_transform中會用到Slice的動態(tài)的操作,而這種動態(tài)的操作在部署的時候往往是會出大問題的。但是有意思的一點是,Space2Depth和Pytorch中的nn.PixelUnShuffle物理意義是一致的,但是實現的過程卻有點不太相同,這也是這一版本YoloV5比較坑的點。
但是實現上一般有 CRD/DCR mode 方式, 由排列順序決定, focus的實現跟這兩種常用的mode 均不一致
「nn.PixelUnShuffle」是我們分割任務的老朋友了,在這里的出現,其更多的是在說明,其實下游任務正常逐步的進行一個統(tǒng)一和兼容。FPN到現在也轉變?yōu)镻AN,這樣的轉變也說明了很多。


仔細看看右邊的Focus2的內容,其實只是一個reshape+conv+reshape的操作,這在雖然物理含義上是Space2Depth,但是與ONNX和Pytorch對應的實現都是不一致的。
mmdepoly有提供一系列的轉換,可以幫助我們更好的解釋這一點,大家可以看看。
在yolov5-6.0的時候已經沒有人再使用Focus,這個操作也被替換成了一個6x6的卷積操作了。
再到現在6x6的卷積會轉換為3x3的卷積拼接出來,這樣會讓整體的推理速度會更快。
4.1、Torch.FX對Focus進行替換

主要是用自己的卷積的實現替換focus_transform和Focus的實現。把中間層展開來看的,就可以發(fā)現還需要自己手寫去補充buffer的算子。
一個比較新的圖手術的方法,大家可以看一看,代碼量也不是很大。
5、給目標檢測網絡插入EfficientNMS結構(如何做后處理會更加的高效?。?/span>
其實EfficientNMS是trt8.0的一個插件,以前是BatchedNMS,這個操作還沒有手寫的插件快。
下面就展開的講講EfficientNMS。
EfficientNMS原本是來自谷歌的EfficientDet 來的,能提速,而且與BatchedNMS一致,這么好用,我們?yōu)樯恫挥媚兀?/p>
Yolo系列中,卷積后會加一個Box的解碼,最后再加上NMS的操作。Box解碼需要我們去重點的優(yōu)化,如果想要在C++實現,就要自己手寫一個實現。但是在我們的實現中,我們會更多的加速方法,這里就不展開說了。但是總的來說現在的新版本的Yolo都基本都固定了前處理跟NMS部分,卷積部分和編解碼一直在變,不過現在也基本被RepVGG統(tǒng)治了。

大家也可以看看上面實現的yolov7后處理的方法。這是一個日本的一個項目實現出的一個整體的結構,我們其實也還有另一種方式可以用pytorch+onnx直接拼出來這樣一個graph出來的。主要還是為了解決這種動態(tài)性,轉化為靜態(tài)去部署。
6、小小的概括一下
「EfficientNMS」該插件主要用于在 「TensorRT」 上與 「EfficientDet」 一起使用,因為這個網絡對引入的延遲特別敏感較慢的 NMS 實現。 但是,該插件對于其他檢測架構也足夠的適用,例如 SSD 或Faster RCNN。
- 標準 NMS 模式:僅給出兩個輸入張量,
(1)邊界框坐標和
(2)和每個Box的對應的分數。
- 融合盒解碼器模式:給出三個輸入張量,
(1)原始每個盒子的定位預測直接來自網絡的定位頭,
(2)相應的分類來自網絡分類頭的分數
(3)認錨框坐標通常硬編碼為網絡中的常數張量
7、項目的實驗結果
這里的實驗結果大家可以看一看,這里值得注意的是居然時間要比以前沒有加操作的結構要快。其實主要原因是沒有把后處理的時間給加上,顯然這樣的快就沒有啥意義。大家要記得對比后處理的時間,如果不對比這樣的比較是不公平的,也不具有啥意義!

8、總結
感覺這個筆記還是沒有記錄得很到位,因為有些信息確實也是我的盲區(qū),但是整個講解過程,其實巨棒,我這里小小一篇雜談,其實還不足以記錄這么多內容,建議大家去看看開放麥的錄播,以及去了解YOLORT和MMDeploy的代碼哦~!
https://link.zhihu.com/?target=https%3A//github.com/zhiqwang/yolov5-rt-stack
https://link.zhihu.com/?target=https%3A//github.com/open-mmlab/mmdeploy
小目標檢測技巧 | 全局上下文自適應稀疏卷積CEASA | 助力微小目標檢測漲點
目標檢測落地必備Trick | 結構化知識蒸餾讓RetinaNet再漲4個點
掃碼加入??「集智書童」交流群
(備注: 方向+學校/公司+昵稱 )



想要了解更多:
前沿AI視覺感知全棧知識??「分類、檢測、分割、關鍵點、車道線檢測、3D視覺(分割、檢測)、多模態(tài)、目標跟蹤、NerF」
行業(yè)技術方案??「AI安防、AI醫(yī)療、AI自動駕駛」 AI模型部署落地實戰(zhàn)??「CUDA、TensorRT、NCNN、OpenVINO、MNN、ONNXRuntime以及地平線框架」歡迎掃描上方二維碼,加入「集智書童-知識星球」,日常分享論文、學習筆記、問題解決方案、部署方案以及全棧式答疑,期待交流!
免責聲明 凡本公眾號注明“來源:XXX(非集智書童)”的作品,均轉載自其它媒體,版權歸原作者所有,如有侵權請聯(lián)系我們刪除,謝謝。點擊下方“閱讀原文”, 了解更多AI學習路上的 「武功秘籍」
