計算機視覺(jué)研究院手把手教你深度學(xué)習的部署
以下文章來(lái)源于DL工程實(shí)踐 ,作者DDX
背景
最近采購了一塊新的樹(shù)莓派,迫不及待的想要在樹(shù)莓派上實(shí)現一個(gè)實(shí)時(shí)的手勢識別。從算法的角度講,并不是太難;但是從工程的角度來(lái)說(shuō),主要有兩個(gè)難點(diǎn),一是手勢數據的采集。大家都知道,深度學(xué)習的高精度離不開(kāi)大量的訓練數據,網(wǎng)絡(luò )設計的再好,沒(méi)有足夠的數據是不行的。
因此要想實(shí)現一個(gè)好的手勢識別,采集數據就成了一個(gè)比較重要的難點(diǎn);另外一個(gè)難點(diǎn)是如何在樹(shù)莓派上實(shí)現實(shí)時(shí)的識別。樹(shù)莓派實(shí)際上是一個(gè)使用arm作為處理器的linux系統,但是由于芯片的性能不是很強,比我們使用的手機要弱很多,并且樹(shù)莓派目前對vulkan的支持并不好,無(wú)法使用vulkan加速,因此對網(wǎng)絡(luò )的優(yōu)化也是一個(gè)難點(diǎn)。要保證網(wǎng)絡(luò )優(yōu)化后的精度不能下降太多,但計算量必須要下降很多。 這次就從這兩個(gè)角度出發(fā),實(shí)現一套實(shí)時(shí)的手勢識別。
由于手勢的類(lèi)型非常多,有識別數字的,識別字母的,識別動(dòng)作的,這里為了拋磚引玉,設計一個(gè)相對簡(jiǎn)單的識別"剪刀,石頭,布"的手勢識別系統,后續可以用來(lái)制作一個(gè)剪刀石頭布的對戰機器人。想要實(shí)現其他類(lèi)型的手勢識別,也完全可以按照這個(gè)流程來(lái)做。
數據采集
對于數據采集,首先看看有沒(méi)有開(kāi)源的手勢識別數據集。很遺憾,除了收費的手勢識別數據集,基本上都是一些不太完整的手勢識別數據集。因此我們需要自己采集。工欲善其事必先利其器,自己采集就得有一些比較好的數據采集工具。這里我設計了一款數據采集工具(后臺回復“手勢識別”即獲?。?。大家也可以根據自己的需要開(kāi)發(fā)自己的數據采集工具。其實(shí)本質(zhì)上并不難,使用pyqt+opencv很容易就能開(kāi)發(fā)一個(gè)順手的數據采集工具。由于基于python開(kāi)發(fā),所以移植性非常好,既可以在windows下使用,也可以在linux,樹(shù)莓派上使用。我設計的這個(gè)界面非常簡(jiǎn)潔,如下圖所示:
opencv會(huì )調用camera開(kāi)始預覽,然后設置一下保存路徑,保存標簽,點(diǎn)擊保存圖片,就可以按照設置的保存間隔進(jìn)行采集數據。例如默認的保存間隔為30,即30幀保存一張圖片,相當于1秒鐘保存一張,如果想要頻率快一些,就將保存間隔設置的小一點(diǎn)。下面的視頻展示了數據采集工具的采集過(guò)程,為了展示效果,我把保存間隔設置為了60幀,大約2秒保存一張圖片。
我把剪刀的標簽設置為0,石頭的標簽設置為1,布的標簽設置為2,最終通過(guò)該數據收集工具就收集到了三個(gè)文件夾:
接下來(lái)需要為訓練數據創(chuàng )建標簽文本。這里我將所有圖片的80%作為訓練數據數據集,剩余的20%作為驗證數據集。使用python腳本很容易實(shí)現自動(dòng)創(chuàng )建標簽文件的腳本,代碼如下:
import os import random MAX_LABEL=3 #類(lèi)別的種類(lèi)數目 label_list=[] for label in range(0,MAX_LABEL+1): for file in os.listdir(str(label)): label_list.append(str(label)+'/' + str(file) + ' ' + str(label)) #對列表進(jìn)行shuffle操作 random.shuffle(label_list) count = len(label_list) # 80%作為訓練數據集 train_count = int(count * 0.8) train_list = label_list[0:train_count] test_list = label_list[train_count:] print('total count=%d train_count=%d test_count=%d'%(count, train_count, count-train_count)) # 寫(xiě)入train.txt標簽文件 with open('train.txt', 'w') as f: for line in train_list: f.write(line + '') # 寫(xiě)入test.txt標簽文件 with open('test.txt', 'w') as f: for line in test_list: f.write(line + '')
網(wǎng)絡(luò )設計
完成了數據收集,那么就可以開(kāi)始為手勢識別系統設計一個(gè)網(wǎng)絡(luò )了。由于需要在樹(shù)莓派這樣的低性能硬件上面運行CNN,那么可以考慮從輕量級網(wǎng)絡(luò )中選擇一個(gè)來(lái)進(jìn)行優(yōu)化。例如google的mobilenet系列,efficient lite系列,曠世的shufflenet系列,華為的ghostnet等。那這些模型如何選擇呢?我之前有一篇關(guān)于這些輕量級的模型的評測,有興趣的可以去看看,《輕量網(wǎng)絡(luò )親測 | 專(zhuān)家從7個(gè)維度全面評測輕量級網(wǎng)絡(luò )》,通過(guò)之前的評測,我發(fā)現shufflenetv2在精度和推理延時(shí)上面有一個(gè)很好的平衡,因此我選擇了shufflenetv2作為手勢識別系統的基礎網(wǎng)絡(luò )。直接使用shufflenetv2雖然能夠在樹(shù)莓派上較為流暢的運行,但是還達不到實(shí)時(shí)的效果,因此需要對shufflentv2進(jìn)行一些優(yōu)化,主要是為了降低計算量,并且能夠盡量保持精度。降低計算量可以從如下幾個(gè)方面考慮:
降低shufflenet的通道系數
shufflenetv1/v2在設計之初,本身就考慮了應用在不同的資源設備上,因此設置了一個(gè)通道系數,直接調整該通道系數,就可以獲得更小計算量的模型。然而通過(guò)實(shí)際測試,直接將通道系數從1.0x降低為0.5x,在降低計算量的同時(shí),也會(huì )對精度損失較大。因此不采用該方案。
降低輸入分辨率
shufflenet的原始輸入分辨率為224*224,如果將分辨率降低x,那么計算量將降低x^2,因此收益很大。但是通過(guò)測試發(fā)現,直接將分辨率降低,對精度的影響也會(huì )很大。所以也不采用降低分辨率的方案。
裁剪shufflenetv2不重要的1*1卷積
通過(guò)觀(guān)察shufflenet的block,可以分為兩種結構,一種是每個(gè)stage的第一個(gè)block,該block由于需要降采樣,升維度,所以對輸入直接復制成兩份,經(jīng)過(guò)branch1,和branch2之后再concat到一起,通道翻倍,如下圖中的降采樣block所示。另外一種普通的block將輸入split成兩部分,一部分經(jīng)過(guò)branch2的卷積提取特征后直接與branch1的部分進(jìn)行concat。如下圖中的普通block所示:
一般在DW卷積(depthwise卷積)的前或后使用1*1的卷積處于兩種目的,一種是融合通道間的信息,彌補dw卷積對通道間信息融合功能的缺失。另一種是為了降維升維,例如mobilenet v2中的inverted reddual模塊。而shufflenet中的block,在branch2中用了2個(gè)1*1卷積,實(shí)際上有一些多余,因為此處不需要進(jìn)行升維降維的需求,那么只是為了融合dw卷積的通道間信息。實(shí)際上有一個(gè)1*1卷積就夠了。因此將上述紅色虛線(xiàn)框中的1*1卷積核刪除。經(jīng)過(guò)測試,精度幾乎不降低,計算量卻下降了30%。因此裁剪1*1的卷積核將是一個(gè)不錯的方法。
加入CSP模塊
csp在大型網(wǎng)絡(luò )上取得了很大的成功。它在每個(gè)stage,將輸入split成兩部分,一部分經(jīng)過(guò)原來(lái)的路徑,另一部分直接shortcut到stage的尾部,然后concat到一起。這既降低了計算量,又豐富了梯度信息,減少了梯度的重用,是一個(gè)非常不錯的trip。在yolov4,yolov5的目標檢測中,也引入了csp機制,使用了csp_darknet。此處將csp引入到shufflenet中。并且對csp做了一定的精簡(jiǎn),最終使用csp stage精簡(jiǎn)版本作為最終的網(wǎng)絡(luò )結構。
經(jīng)過(guò)測試,網(wǎng)絡(luò )雖然能大幅降低計算量,但是精度降低的也很明顯。分析原因,主要有兩個(gè),一是shufflenetv2本身已經(jīng)使用了在輸入通道split,然后concat的blcok流程,與csp其實(shí)是一樣的,只是csp是基于一個(gè)stage,shufflenetv2是基于一個(gè)block,另外csp本來(lái)就是在densenet這種密集連接的網(wǎng)絡(luò )上使用有比較好的效果,在輕量級網(wǎng)絡(luò )上不見(jiàn)得效果會(huì )好。
因此最終將網(wǎng)絡(luò )設計為基于shufflenetv2 1.0x,并精簡(jiǎn)了多余的1*1卷積的版本,命名為:shufflenetv2_liteconv版本。
網(wǎng)絡(luò )訓練
收集好了數據,并且也設計好了網(wǎng)絡(luò ),那么接下來(lái)就是訓練了?;趐ytroch,大家可以很方便的編寫(xiě)出一個(gè)簡(jiǎn)單的訓練流程。這里我選擇從0開(kāi)始訓練,沒(méi)有使用shufflenet v2 1.0x的預訓練模型,因為我們對shufflenet做了優(yōu)化,刪除了很多1*1的conv,直接使用預訓練模型會(huì )不匹配,因此從0開(kāi)始訓練。學(xué)習率可以適當的放大一些,epoch數目可以適當大一些。我把我的訓練超參貼出來(lái),大家可以參考使用:
訓練epoch:60
初始學(xué)習率:0.01
學(xué)習率策略:multistep(35,40)
優(yōu)化器:moment sgd
weight decay:0.0001
最終在訓練完50個(gè)epoch之后,loss大約為0.1,測試集上面的精度為0.98。
網(wǎng)絡(luò )部署
網(wǎng)絡(luò )部署可以采用很多開(kāi)源的推理庫。例如mnn,ncnn,tnn等。這里我選擇使用ncnn,因為ncnn開(kāi)源的早,使用的人多,網(wǎng)絡(luò )支持,硬件支持都還不錯,關(guān)鍵是很多問(wèn)題都能搜索到別人的經(jīng)驗,可以少走很多彎路。但是遺憾的是ncnn并不支持直接將pytorch模型導入,需要先轉換成onnx格式,然后再將onnx格式導入到ncnn中。另外注意一點(diǎn),將pytroch的模型到onnx之后有許多膠水op,這在ncnn中是不支持的,需要使用另外一個(gè)開(kāi)源工具:onnx-simplifier對onnx模型進(jìn)行剪裁,然后再導入到ncnn中。因此整個(gè)過(guò)程還有些許繁瑣,為了簡(jiǎn)單,我編寫(xiě)了從"pytorch模型->onnx模型->onnx模型精簡(jiǎn)->ncnn模型"的轉換腳本,方便大家一鍵轉換,減少中間過(guò)程出錯。我把主要流程的代碼貼出來(lái)(詳細的代碼請關(guān)注公眾號"DL工程實(shí)踐",后臺回復“手勢識別”四個(gè)字,可獲?。?/p>
# 1、pytroch模型導出到onnx模型 torch.onnx.export(net,input,onnx_file,verbose=DETAIL_LOG) # 2、調用onnx-simplifier工具對onnx模型進(jìn)行精簡(jiǎn) cmd = 'python -m onnxsim ' + str(onnx_file) + ' ' + str(onnx_sim_file) ret = os.system(str(cmd)) # 3、調用ncnn的onnx2ncnn工具,將onnx模型準換為ncnn模型 cmd = onnx2ncnn_path + ' ' + str(new_onnx_file) + ' ' + str(ncnn_param_file) + ' ' + str(ncnn_bin_file) ret = os.system(str(cmd)) # 4、對ncnn模型加密(可選步驟)cmd = ncnn2mem_path + ' ' + str(ncnn_param_file) + ' ' + str(ncnn_bin_file) + ' ' + str(ncnn_id_file) + ' ' + str(ncnn_mem_file) ret = os.system(str(cmd))
導出到ncnn模型之后,就可以在ncnn模型上運行訓練好的手勢識別庫。ncnn是基于C++開(kāi)發(fā)的,因此編寫(xiě)上層應用的時(shí)候使用C++是效率最高的。我為了簡(jiǎn)單,使用python來(lái)調用ncnn的C++庫也是可以的,不過(guò)會(huì )損失一丟丟的性能,但這是值得的,人生苦短,我用python。下面這個(gè)視頻是最終部署好的手勢識別程序。
總結
本次實(shí)踐完成了基于樹(shù)莓派的實(shí)時(shí)手勢識別,算法上并不復雜,主要是工程實(shí)踐上的一些問(wèn)題,例如數據的采集,網(wǎng)絡(luò )的優(yōu)化,以及后期的推理轉換等。實(shí)際上還有一些工作可以?xún)?yōu)化,例如對模型的量化,對數據的增強。通過(guò)模型量化,可以進(jìn)一步提升運算效率,通過(guò)數據增強可以彌補我們自己采集的數據分布單一,過(guò)擬合的風(fēng)險,這些問(wèn)題就留給讀者朋友們自己去思考了。
*博客內容為網(wǎng)友個(gè)人發(fā)布,僅代表博主個(gè)人觀(guān)點(diǎn),如有侵權請聯(lián)系工作人員刪除。