基于CenterFace的模型優(yōu)化記錄
【GiantPandaCV導(dǎo)語(yǔ)】CenterFace移動(dòng)端模型優(yōu)化實(shí)驗(yàn)記錄
一、序
CenterFace是基于CenterNet的一種AnchorFree人臉檢測(cè)模型。在widerface上性能雖然沒有超過SOTA(Retinaface),但是勝于推理速度較快(不需要NMS),模型結(jié)構(gòu)簡(jiǎn)單,便于移植部署。
二、背景
模型主要是使用在移動(dòng)端App中,需要滿足:
上傳圖片后返回帶有人臉檢測(cè)框的結(jié)果圖片。 打開攝像頭實(shí)時(shí)進(jìn)行檢測(cè)并拍照,返回檢測(cè)后的圖片。 需要在室內(nèi)、室外、白天,晚上場(chǎng)景下均可使用,受眾群體密集度比較高,需要支持戴口罩檢測(cè),Recall和precision均要求比較高(大于90%)。 需要支持大小人臉檢測(cè)。樣例如下,難點(diǎn)在于支持小人臉檢測(cè)以及小模型優(yōu)化。
? ?? ?
圖1-場(chǎng)景例子
三、CenterFace
本節(jié)先簡(jiǎn)單介紹一下CenterFace模型
模型結(jié)構(gòu)
CenterFace模型構(gòu)造比較簡(jiǎn)單,基礎(chǔ)backbone+FPN+head即完成網(wǎng)絡(luò)構(gòu)建。1. backbone模型上采用了mobilenetv2作為backbone,mobilenetv3因?yàn)槎喾种б约癝E模塊,手機(jī)端移植不是很友好,實(shí)機(jī)測(cè)試,iphonex上mbv3相比mbv2要慢1ms左右,如果是低端機(jī),這個(gè)差距會(huì)更大。2. FPN采用的是傳統(tǒng)的top-down結(jié)構(gòu),沒有使用PAN。3. Head采用4個(gè)conv+bn結(jié)構(gòu)分別輸出locationmap,scalemap,offsetmap和pointsmap。整體結(jié)構(gòu)圖如下:

圖2-模型結(jié)構(gòu)
損失函數(shù)
CenterFace的損失函數(shù)和CenterNet一樣,由FocalLoss形式Location分類損失和L1回歸損失構(gòu)成
Location分類損失:
L1回歸損失:
offset points scale 最終損失:
對(duì)于location損失,只有每個(gè)bbox的中心點(diǎn)為正樣本,其余點(diǎn)均為負(fù)樣本,公式。對(duì)于offset損失,由于featuremap進(jìn)行下采樣的時(shí)候,計(jì)算中心點(diǎn)會(huì)由于取整產(chǎn)生偏移,需要用l1損失計(jì)算這個(gè)偏差。對(duì)于scale損失,這個(gè)是對(duì)bbox的w和h進(jìn)行回歸,取log便于計(jì)算。對(duì)于points損失,計(jì)算的是人臉5個(gè)關(guān)鍵點(diǎn)到中心點(diǎn)之間的距離的損失,做了normalize處理。最后的損失,就是各個(gè)損失的加權(quán)之和。
標(biāo)簽生成
locationmap, centerface中最重要的target就是bbox中心點(diǎn)gaussianmap生成,代碼,效果圖如下:
def?_gaussian_radiusv1(self,?height,?width,?min_overlap=0.7):
????"""from?cornernet"""
????a1?=?1
????b1?=?(height?+?width)
????c1?=?width?*?height?*?(1?-?min_overlap)?/?(1?+?min_overlap)
????sq1?=?torch.sqrt(b1?**?2?-?4?*?a1?*?c1)
????r1?=?(b1?+?sq1)?/?2
????a2?=?4
????b2?=?2?*?(height?+?width)
????c2?=?(1?-?min_overlap)?*?width?*?height
????sq2?=?torch.sqrt(b2?**?2?-?4?*?a2?*?c2)
????r2?=?(b2?+?sq2)?/?2
????a3?=?4?*?min_overlap
????b3?=?-2?*?min_overlap?*?(height?+?width)
????c3?=?(min_overlap?-?1)?*?width?*?height
????sq3?=?torch.sqrt(b3?**?2?-?4?*?a3?*?c3)
????r3?=?(b3?+?sq3)?/?2
????return?min(r1,?r2,?r3)
def?_gaussian2D(self,?shape,?sigma=1):
????m,?n?=?[(ss?-?1.)?/?2.?for?ss?in?shape]
????#?y,?x?=?np.ogrid[-m:m+1,-n:n+1]
????y?=?torch.arange(-m,?m+1,?dtype=torch.int).view(-1,?1)
????x?=?torch.arange(-n,?n+1,?dtype=torch.int).view(1,?-1)
????h?=?torch.exp(-(x?*?x?+?y?*?y)?/?(2?*?sigma?*?sigma))
????h[h?1e-5?*?h.max()]?=?0
????return?h
def?_draw_umich_gaussian(self,?heatmap,?center,?radius,?k=1):
????diameter?=?2?*?radius?+?1
????#?get?the?gaussian?heatmap
????gaussian?=?self._gaussian2D((diameter,?diameter),?sigma=diameter?/?6)
????x,?y?=?int(center[0]),?int(center[1])
????height,?width?=?heatmap.shape[0:2]
????left,?right?=?min(x,?radius),?min(width?-?x,?radius?+?1)
????top,?bottom?=?min(y,?radius),?min(height?-?y,?radius?+?1)
????masked_heatmap?=?heatmap[y?-?top:y?+?bottom,?x?-?left:x?+?right]
????masked_gaussian?=?gaussian[radius?-?top:radius?+
????????????????????????????bottom,?radius?-?left:radius?+?right]
????if?min(masked_gaussian.shape)?>?0?and?min(masked_heatmap.shape)?>?0:??#?TODO?debug
????????torch.max(masked_heatmap,?masked_gaussian?*?k,?out=masked_heatmap)
????return?heatmap
圖3-heatmap
offsetmap, 每個(gè)bbox中心點(diǎn)上x方向和y方向的偏移結(jié)果,輸出是一個(gè)(BX2XHXW)的map。
scalemap, 每個(gè)bbox中心點(diǎn)上w和h的log結(jié)果,輸出是一個(gè)(BX2XHXW)的map。
pointsmap, 每個(gè)bbox的中心點(diǎn)到5個(gè)關(guān)鍵點(diǎn)x和y方向的距離,輸出是一個(gè)(BX10XHXW)的map。
模型推理
CenterFace沒有使用NMS作為后處理,而是采用的maxpooling作為代替,代碼如下:
loc?=?loc.unsqueeze(0)????#?heatmap
hm_max?=?nn.MaxPool2d(kernel_size=3,?stride=1,?padding=1)(loc)
loc[loc?!=?hm_max]?=?0
四、優(yōu)化路線
數(shù)據(jù):由于場(chǎng)景任務(wù)中存在部分帶有口罩的情況,所以采集了2000張百度的帶有口罩的數(shù)據(jù),混合widerface的train和val的數(shù)據(jù)來進(jìn)行訓(xùn)練,測(cè)試數(shù)據(jù)使用業(yè)務(wù)提供的數(shù)據(jù),保持一致。
優(yōu)化:流程圖如下:

圖4-模型優(yōu)化流程
baseline模型為mobilenetv2+fpn模型,測(cè)試數(shù)據(jù)上ap為95%,初始圖片訓(xùn)練大小為800x800,測(cè)試大小為416x416,F(xiàn)LOPs為1G。
通常來講,train的size大于測(cè)試的時(shí)候,效果表現(xiàn)不好。所以調(diào)整圖片的trainsize,分別為800,640,512,416,最終在416x416測(cè)試的情況下,訓(xùn)練size為416的時(shí)候效果最好。模型固定為416訓(xùn)練,416測(cè)試。
centerface的FPN的最后一層直接進(jìn)行輸出,這里把多層layer進(jìn)行了concat,ap提升了一個(gè)點(diǎn),但是FLOPs增加,收益不大。
由于centerface沒有anchorbbox,bbox回歸和中心點(diǎn)損失沒有實(shí)質(zhì)的關(guān)聯(lián),所以bbox的表現(xiàn)不是很好,添加了一個(gè)scale_dis分支,輸出(BX4XHXW)的一個(gè)featuremap,分別表示的是中心點(diǎn)到上下左右的距離,計(jì)算IOU損失,效果提升不明顯,還帶來了4個(gè)通道的featuremap的冗余計(jì)算,直接棄用了(有興趣的同學(xué)可以參考FCOS的說明)。
由于FPN最后的輸出,經(jīng)過一個(gè)卷積后,要輸出多個(gè)head,會(huì)帶來多個(gè)卷積計(jì)算,所以考慮優(yōu)化為一個(gè)卷積輸出,輸出的層為(BX(1+2+2+10)xHXW),計(jì)算的時(shí)候分別對(duì)應(yīng)channel計(jì)算損失,減少了10M的FLOPs,推理速度有所提升,同時(shí)提升了將近1個(gè)點(diǎn)的ap。
?
?圖5-修改head結(jié)構(gòu)
由于業(yè)務(wù)只要求輸出框,對(duì)于關(guān)鍵點(diǎn)沒有需求,所以構(gòu)建結(jié)構(gòu)圖的時(shí)候,把關(guān)鍵點(diǎn)的channel砍掉,可以減少20M的FLOPs,精度保持不變。
?
?圖6-推理減少channel
使用mobilenetv2x0.25+fpn作為backbone,使用上面的操作,F(xiàn)LOPs降低為0.2G,ap基本保持不變。
修改訓(xùn)練size為400,推理size也為400,修改FPN的channel從24降低到16,F(xiàn)LOPs降低到了0.131G,精度保持95.2%相比baseline有輕微的提升。
最后對(duì)模型進(jìn)行剪枝,采用SlimmingBN方法,對(duì)mobilenetv2中的
InvertResdiual模塊中的升維層進(jìn)行剪枝。流程如下:訓(xùn)練的時(shí)候,對(duì)bn的gamma進(jìn)行l(wèi)1正則,設(shè)置為1e-4代碼如下:
def?updataBN(self):
????"""add?a?l1?panety?for?bn?channel"""
????for?m?in?self.model.modules():
????????if?isinstance(m,?nn.BatchNorm2d):
????????????m.weight.grad.data.add_(self.s?*?torch.sign(m.weight.data))剪枝的時(shí)候,根據(jù)bn總數(shù)設(shè)置閾值,對(duì)weights從小到大進(jìn)行排序,按比例定位閾值,根據(jù)閾值對(duì)bn的weights置0,重新計(jì)算測(cè)試集ap,測(cè)試發(fā)現(xiàn)卡0.3的時(shí)候,精度保持不變,0.4的時(shí)候精度下降1個(gè)點(diǎn)的ap,需要進(jìn)行finetune。
保存模型,根據(jù)剪枝出來的Config重構(gòu)模型結(jié)構(gòu),復(fù)制保留下來的權(quán)重到對(duì)應(yīng)修改的層上,再次進(jìn)行推理,ap保持不變即可,需要finetune的話,load權(quán)重后進(jìn)行finetune,這里finetune個(gè)人建議是pretrain,finetune的話,可能會(huì)由于剪枝導(dǎo)致過檢和漏檢的情況在訓(xùn)練結(jié)束后還存在。
for?[m0,?m1]?in?zip(model.fpn.backbone.modules(),?newmodel.fpn.backbone.modules()):
????#?only?prune?the?invertedresidual?conv?and?bn
????if?isinstance(m0,?InvertedResidual):
????????if?len(m0.conv)?>?5:
????????????for?i?in?range(len(m0.conv)):
????????????????if?i?==?1:
?????????????????if?isinstance(m0.conv[i],?nn.BatchNorm2d):
????????????????????????idx1?=?np.squeeze(np.argwhere(np.asarray(mask.cpu().numpy())))
????????????????????????#?first?batchnormalize?(hidden)
????????????????????????m1.conv[i].weight.data?=?m0.conv[i].weight.data[idx1].clone()
????????????????????????m1.conv[i].bias.data?=?m0.conv[i].bias.data[idx1].clone()
????????????????????????m1.conv[i].running_mean?=?m0.conv[i].running_mean[idx1].clone()
????????????????????????m1.conv[i].running_var?=?m0.conv[i].running_var[idx1].clone()
????????????????????????#?first?conv?(hidden,?inp,?1,?1)
????????????????????????if?isinstance(m0.conv[i-1],?nn.Conv2d):
????????????????????????????w?=?m0.conv[i-1].weight.data[idx1,?:,?:,?:].clone()
?????????????????????????m1.conv[i-1].weight.data?=?w.clone()
????????????????????????#?second?conv??(hidden,?1,?1,?1)
????????????????????????if?isinstance(m0.conv[i+2],?nn.Conv2d):
????????????????????????????w?=?m0.conv[i+2].weight.data[idx1,?:,?:,?:].clone()
?????????????????????????m1.conv[i+2].weight.data?=?w.clone()
????????????????????????#?second?bn?(hidden)
????????????????????????if?isinstance(m0.conv[i+3],?nn.BatchNorm2d):
????????????????????????????m1.conv[i+3].weight.data?=?m0.conv[i+3].weight.data[idx1].clone()
????????????????????????????m1.conv[i+3].bias.data?=?m0.conv[i+3].bias.data[idx1].clone()
????????????????????????????m1.conv[i+3].running_mean?=?m0.conv[i+3].running_mean[idx1].clone()
?????????????????????????m1.conv[i+3].running_var?=?m0.conv[i+3].running_var[idx1].clone()
????????????????????????#?third?conv?(oup,?hidden,?1,?1)
????????????????????????if?isinstance(m0.conv[i+5],?nn.Conv2d):
????????????????????????????w?=?m0.conv[i+5].weight.data[:,?idx1,?:,?:].clone()
?????????????????????????m1.conv[i+5].weight.data?=?w.clone()
????????????????????????#?third?bn?(oup)
????????????????????????if?isinstance(m0.conv[i+6],?nn.BatchNorm2d):
????????????????????????????m1.conv[i+6].weight.data?=?m0.conv[i+6].weight.data.clone()
????????????????????????????m1.conv[i+6].bias.data?=?m0.conv[i+6].bias.data.clone()
????????????????????????????m1.conv[i+6].running_mean?=?m0.conv[i+6].running_mean.clone()
?????????????????????????m1.conv[i+6].running_var?=?m0.conv[i+6].running_var.clone()
????????????????????????layer_id_in_cfg?+=?1
????????????????????????if?layer_id_in_cfg??????????????????????????mask?=?cfg_mask[layer_id_in_cfg]最后輸出的模型為0.116G的FLOPs,相比baseline降低了10倍FLOPs,參數(shù)量400k左右,測(cè)試集上ap95.2%相比baseline高了0.2%個(gè)點(diǎn)。iphonex上測(cè)試10ms左右,低端機(jī)可以滿足10FPS以上的輸出,精度,速度均滿足要求。
五、簡(jiǎn)單的思考
為什么gaussian map是一個(gè)3X3的map,而不是全局的map或者說是一個(gè)點(diǎn)? 可能是因?yàn)閏ornernet是計(jì)算角點(diǎn)的,因?yàn)榇嬖谄钏孕枰粋€(gè)小點(diǎn)的map來做約束,centernet直接繼承了這個(gè)idea,論文中也是直接引用沒有過多的思考。 從損失構(gòu)建上來看,如果增大gaussianmap,對(duì)應(yīng)的增加負(fù)樣本,會(huì)影響中心點(diǎn)的約束。相當(dāng)于中心點(diǎn)是points anchor,其他的都是非anchor。 從gaussianmap生成來看,對(duì)應(yīng)的3x3可以cover出現(xiàn)offset過大的情況,可以約束范圍,畢竟沒有bboxanchor。 如果把bbox全部填滿? bbox填滿,那么就是每個(gè)點(diǎn)都是正樣本,框中不存在負(fù)樣本,這樣中心點(diǎn)的價(jià)值就不存在了,論文的延伸也就是FCOS。 loss之間的聯(lián)系? 由于anchorbase的方法是存在海量的positive和negative anchors,回歸的好壞,一是會(huì)受到anchor的影響,二是會(huì)受到classification的影響,所以生成的bbox比較穩(wěn)定。centernet的各個(gè)loss實(shí)際上是獨(dú)立的,只是建立在中心點(diǎn)的基礎(chǔ)上,所以導(dǎo)致框會(huì)不穩(wěn)定,尤其是在實(shí)時(shí)中抖動(dòng)比較大,OneNet就這個(gè)問題給出了解決方案。
預(yù)測(cè)結(jié)果
400x400 移動(dòng)端上進(jìn)行測(cè)試的結(jié)果如下

圖7-移動(dòng)端檢測(cè)結(jié)果
結(jié)束語(yǔ)
本人才疏學(xué)淺,以上都是自己在做項(xiàng)目中的一些方法和實(shí)驗(yàn),以及一些粗淺的思考,并不一定完全正確,只是個(gè)人的理解,歡迎大家指正,留言評(píng)論。
參考文獻(xiàn)
CenterFace(https://arxiv.org/pdf/1911.03599.pdf) CenteNet(https://arxiv.org/abs/1904.07850) CornerNet(https://arxiv.org/abs/1808.01244) SlimmingBN(https://openaccess.thecvf.com/content_ICCV_2017/papers/Liu_Learning_Efficient_Convolutional_ICCV_2017_paper.pdf) FCOS(https://arxiv.org/abs/1904.01355) OneNet(https://arxiv.org/abs/2012.05780)
歡迎關(guān)注GiantPandaCV, 在這里你將看到獨(dú)家的深度學(xué)習(xí)分享,堅(jiān)持原創(chuàng),每天分享我們學(xué)習(xí)到的新鮮知識(shí)。( ? ?ω?? )?
有對(duì)文章相關(guān)的問題,或者想要加入交流群,歡迎添加BBuf微信:
為了方便讀者獲取資料以及我們公眾號(hào)的作者發(fā)布一些Github工程的更新,我們成立了一個(gè)QQ群,二維碼如下,感興趣可以加入。
