博客專欄

EEPW首頁 > 博客 > 計算機視覺研究院手把手教你深度學(xué)習(xí)的部署

計算機視覺研究院手把手教你深度學(xué)習(xí)的部署

發(fā)布人:CV研究院 時間:2021-09-16 來源:工程師 發(fā)布文章

以下文章來源于DL工程實踐 ,作者DDX

背景

最近采購了一塊新的樹莓派,迫不及待的想要在樹莓派上實現(xiàn)一個實時的手勢識別。從算法的角度講,并不是太難;但是從工程的角度來說,主要有兩個難點,一是手勢數(shù)據(jù)的采集。大家都知道,深度學(xué)習(xí)的高精度離不開大量的訓(xùn)練數(shù)據(jù),網(wǎng)絡(luò)設(shè)計的再好,沒有足夠的數(shù)據(jù)是不行的。

因此要想實現(xiàn)一個好的手勢識別,采集數(shù)據(jù)就成了一個比較重要的難點;另外一個難點是如何在樹莓派上實現(xiàn)實時的識別。樹莓派實際上是一個使用arm作為處理器的linux系統(tǒng),但是由于芯片的性能不是很強,比我們使用的手機要弱很多,并且樹莓派目前對vulkan的支持并不好,無法使用vulkan加速,因此對網(wǎng)絡(luò)的優(yōu)化也是一個難點。要保證網(wǎng)絡(luò)優(yōu)化后的精度不能下降太多,但計算量必須要下降很多。 這次就從這兩個角度出發(fā),實現(xiàn)一套實時的手勢識別。

由于手勢的類型非常多,有識別數(shù)字的,識別字母的,識別動作的,這里為了拋磚引玉,設(shè)計一個相對簡單的識別"剪刀,石頭,布"的手勢識別系統(tǒng),后續(xù)可以用來制作一個剪刀石頭布的對戰(zhàn)機器人。想要實現(xiàn)其他類型的手勢識別,也完全可以按照這個流程來做。


 數(shù)據(jù)采集

對于數(shù)據(jù)采集,首先看看有沒有開源的手勢識別數(shù)據(jù)集。很遺憾,除了收費的手勢識別數(shù)據(jù)集,基本上都是一些不太完整的手勢識別數(shù)據(jù)集。因此我們需要自己采集。工欲善其事必先利其器,自己采集就得有一些比較好的數(shù)據(jù)采集工具。這里我設(shè)計了一款數(shù)據(jù)采集工具(后臺回復(fù)“手勢識別”即獲?。?。大家也可以根據(jù)自己的需要開發(fā)自己的數(shù)據(jù)采集工具。其實本質(zhì)上并不難,使用pyqt+opencv很容易就能開發(fā)一個順手的數(shù)據(jù)采集工具。由于基于python開發(fā),所以移植性非常好,既可以在windows下使用,也可以在linux,樹莓派上使用。我設(shè)計的這個界面非常簡潔,如下圖所示:

1.jpg

opencv會調(diào)用camera開始預(yù)覽,然后設(shè)置一下保存路徑,保存標簽,點擊保存圖片,就可以按照設(shè)置的保存間隔進行采集數(shù)據(jù)。例如默認的保存間隔為30,即30幀保存一張圖片,相當(dāng)于1秒鐘保存一張,如果想要頻率快一些,就將保存間隔設(shè)置的小一點。下面的視頻展示了數(shù)據(jù)采集工具的采集過程,為了展示效果,我把保存間隔設(shè)置為了60幀,大約2秒保存一張圖片。

我把剪刀的標簽設(shè)置為0,石頭的標簽設(shè)置為1,布的標簽設(shè)置為2,最終通過該數(shù)據(jù)收集工具就收集到了三個文件夾:

2.jpg

接下來需要為訓(xùn)練數(shù)據(jù)創(chuàng)建標簽文本。這里我將所有圖片的80%作為訓(xùn)練數(shù)據(jù)數(shù)據(jù)集,剩余的20%作為驗證數(shù)據(jù)集。使用python腳本很容易實現(xiàn)自動創(chuàng)建標簽文件的腳本,代碼如下:

import os
import random
MAX_LABEL=3 #類別的種類數(shù)目
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))        
#對列表進行shuffle操作
random.shuffle(label_list) 
count = len(label_list)
# 80%作為訓(xùn)練數(shù)據(jù)集
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))
# 寫入train.txt標簽文件
with open('train.txt', 'w') as f:
    for line in train_list:
        f.write(line + '')
# 寫入test.txt標簽文件
with open('test.txt', 'w') as f:
    for line in test_list:
        f.write(line + '')


 網(wǎng)絡(luò)設(shè)計

完成了數(shù)據(jù)收集,那么就可以開始為手勢識別系統(tǒng)設(shè)計一個網(wǎng)絡(luò)了。由于需要在樹莓派這樣的低性能硬件上面運行CNN,那么可以考慮從輕量級網(wǎng)絡(luò)中選擇一個來進行優(yōu)化。例如google的mobilenet系列,efficient lite系列,曠世的shufflenet系列,華為的ghostnet等。那這些模型如何選擇呢?我之前有一篇關(guān)于這些輕量級的模型的評測,有興趣的可以去看看,《輕量網(wǎng)絡(luò)親測 | 專家從7個維度全面評測輕量級網(wǎng)絡(luò)》,通過之前的評測,我發(fā)現(xiàn)shufflenetv2在精度和推理延時上面有一個很好的平衡,因此我選擇了shufflenetv2作為手勢識別系統(tǒng)的基礎(chǔ)網(wǎng)絡(luò)。直接使用shufflenetv2雖然能夠在樹莓派上較為流暢的運行,但是還達不到實時的效果,因此需要對shufflentv2進行一些優(yōu)化,主要是為了降低計算量,并且能夠盡量保持精度。降低計算量可以從如下幾個方面考慮:

降低shufflenet的通道系數(shù)

shufflenetv1/v2在設(shè)計之初,本身就考慮了應(yīng)用在不同的資源設(shè)備上,因此設(shè)置了一個通道系數(shù),直接調(diào)整該通道系數(shù),就可以獲得更小計算量的模型。然而通過實際測試,直接將通道系數(shù)從1.0x降低為0.5x,在降低計算量的同時,也會對精度損失較大。因此不采用該方案。

降低輸入分辨率

shufflenet的原始輸入分辨率為224*224,如果將分辨率降低x,那么計算量將降低x^2,因此收益很大。但是通過測試發(fā)現(xiàn),直接將分辨率降低,對精度的影響也會很大。所以也不采用降低分辨率的方案。

裁剪shufflenetv2不重要的1*1卷積

通過觀察shufflenet的block,可以分為兩種結(jié)構(gòu),一種是每個stage的第一個block,該block由于需要降采樣,升維度,所以對輸入直接復(fù)制成兩份,經(jīng)過branch1,和branch2之后再concat到一起,通道翻倍,如下圖中的降采樣block所示。另外一種普通的block將輸入split成兩部分,一部分經(jīng)過branch2的卷積提取特征后直接與branch1的部分進行concat。如下圖中的普通block所示:

3.png

一般在DW卷積(depthwise卷積)的前或后使用1*1的卷積處于兩種目的,一種是融合通道間的信息,彌補dw卷積對通道間信息融合功能的缺失。另一種是為了降維升維,例如mobilenet v2中的inverted reddual模塊。而shufflenet中的block,在branch2中用了2個1*1卷積,實際上有一些多余,因為此處不需要進行升維降維的需求,那么只是為了融合dw卷積的通道間信息。實際上有一個1*1卷積就夠了。因此將上述紅色虛線框中的1*1卷積核刪除。經(jīng)過測試,精度幾乎不降低,計算量卻下降了30%。因此裁剪1*1的卷積核將是一個不錯的方法。

加入CSP模塊

csp在大型網(wǎng)絡(luò)上取得了很大的成功。它在每個stage,將輸入split成兩部分,一部分經(jīng)過原來的路徑,另一部分直接shortcut到stage的尾部,然后concat到一起。這既降低了計算量,又豐富了梯度信息,減少了梯度的重用,是一個非常不錯的trip。在yolov4,yolov5的目標檢測中,也引入了csp機制,使用了csp_darknet。此處將csp引入到shufflenet中。并且對csp做了一定的精簡,最終使用csp stage精簡版本作為最終的網(wǎng)絡(luò)結(jié)構(gòu)。

4.png

經(jīng)過測試,網(wǎng)絡(luò)雖然能大幅降低計算量,但是精度降低的也很明顯。分析原因,主要有兩個,一是shufflenetv2本身已經(jīng)使用了在輸入通道split,然后concat的blcok流程,與csp其實是一樣的,只是csp是基于一個stage,shufflenetv2是基于一個block,另外csp本來就是在densenet這種密集連接的網(wǎng)絡(luò)上使用有比較好的效果,在輕量級網(wǎng)絡(luò)上不見得效果會好。

因此最終將網(wǎng)絡(luò)設(shè)計為基于shufflenetv2 1.0x,并精簡了多余的1*1卷積的版本,命名為:shufflenetv2_liteconv版本。


 網(wǎng)絡(luò)訓(xùn)練

收集好了數(shù)據(jù),并且也設(shè)計好了網(wǎng)絡(luò),那么接下來就是訓(xùn)練了?;趐ytroch,大家可以很方便的編寫出一個簡單的訓(xùn)練流程。這里我選擇從0開始訓(xùn)練,沒有使用shufflenet v2 1.0x的預(yù)訓(xùn)練模型,因為我們對shufflenet做了優(yōu)化,刪除了很多1*1的conv,直接使用預(yù)訓(xùn)練模型會不匹配,因此從0開始訓(xùn)練。學(xué)習(xí)率可以適當(dāng)?shù)姆糯笠恍?,epoch數(shù)目可以適當(dāng)大一些。我把我的訓(xùn)練超參貼出來,大家可以參考使用:

訓(xùn)練epoch:60

初始學(xué)習(xí)率:0.01

學(xué)習(xí)率策略:multistep(35,40)

優(yōu)化器:moment sgd

weight decay:0.0001

最終在訓(xùn)練完50個epoch之后,loss大約為0.1,測試集上面的精度為0.98。


 網(wǎng)絡(luò)部署

網(wǎng)絡(luò)部署可以采用很多開源的推理庫。例如mnn,ncnn,tnn等。這里我選擇使用ncnn,因為ncnn開源的早,使用的人多,網(wǎng)絡(luò)支持,硬件支持都還不錯,關(guān)鍵是很多問題都能搜索到別人的經(jīng)驗,可以少走很多彎路。但是遺憾的是ncnn并不支持直接將pytorch模型導(dǎo)入,需要先轉(zhuǎn)換成onnx格式,然后再將onnx格式導(dǎo)入到ncnn中。另外注意一點,將pytroch的模型到onnx之后有許多膠水op,這在ncnn中是不支持的,需要使用另外一個開源工具:onnx-simplifier對onnx模型進行剪裁,然后再導(dǎo)入到ncnn中。因此整個過程還有些許繁瑣,為了簡單,我編寫了從"pytorch模型->onnx模型->onnx模型精簡->ncnn模型"的轉(zhuǎn)換腳本,方便大家一鍵轉(zhuǎn)換,減少中間過程出錯。我把主要流程的代碼貼出來(詳細的代碼請關(guān)注公眾號"DL工程實踐",后臺回復(fù)“手勢識別”四個字,可獲?。?/p>

# 1、pytroch模型導(dǎo)出到onnx模型
torch.onnx.export(net,input,onnx_file,verbose=DETAIL_LOG)
# 2、調(diào)用onnx-simplifier工具對onnx模型進行精簡
cmd = 'python -m onnxsim ' + str(onnx_file) + ' ' + str(onnx_sim_file)
ret = os.system(str(cmd))
# 3、調(diào)用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))

導(dǎo)出到ncnn模型之后,就可以在ncnn模型上運行訓(xùn)練好的手勢識別庫。ncnn是基于C++開發(fā)的,因此編寫上層應(yīng)用的時候使用C++是效率最高的。我為了簡單,使用python來調(diào)用ncnn的C++庫也是可以的,不過會損失一丟丟的性能,但這是值得的,人生苦短,我用python。下面這個視頻是最終部署好的手勢識別程序。


 總結(jié)

本次實踐完成了基于樹莓派的實時手勢識別,算法上并不復(fù)雜,主要是工程實踐上的一些問題,例如數(shù)據(jù)的采集,網(wǎng)絡(luò)的優(yōu)化,以及后期的推理轉(zhuǎn)換等。實際上還有一些工作可以優(yōu)化,例如對模型的量化,對數(shù)據(jù)的增強。通過模型量化,可以進一步提升運算效率,通過數(shù)據(jù)增強可以彌補我們自己采集的數(shù)據(jù)分布單一,過擬合的風(fēng)險,這些問題就留給讀者朋友們自己去思考了。

*博客內(nèi)容為網(wǎng)友個人發(fā)布,僅代表博主個人觀點,如有侵權(quán)請聯(lián)系工作人員刪除。



關(guān)鍵詞: AI

相關(guān)推薦

技術(shù)專區(qū)

關(guān)閉