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