开源项目|基于darknet实现量化感知训练,已实现yolov3-tiny所有算子
◎本文為極市開發(fā)者「ArtyZe」原創(chuàng)投稿,轉(zhuǎn)載請注明來源。
◎極市「項(xiàng)目推薦」專欄,幫助開發(fā)者們推廣分享自己的最新工作,歡迎大家投稿。聯(lián)系極市小編(fengcall19)即可投稿~
量化簡介
在實(shí)際神經(jīng)網(wǎng)絡(luò)在例如端側(cè)的部署時(shí),由于內(nèi)存,帶寬或者最重要計(jì)算資源的限制,通常會(huì)采用量化等手段來加速神經(jīng)網(wǎng)絡(luò)的表現(xiàn)。量化的意思即是將原來浮點(diǎn)運(yùn)算轉(zhuǎn)化為定點(diǎn)運(yùn)算,例如最常見的8bit量化,無論是int8還是uint8,都是將浮點(diǎn)的區(qū)間參數(shù)映射到256個(gè)離散區(qū)間上。這樣原來32位的運(yùn)算就變成了8位的運(yùn)算
r=S(q?Z)r=S(q-Z)r=S(q?Z)
這里我們以非對(duì)稱量化到uint8舉例,其中S代表量化因子(scale factor), Z代表zero point.
量化的優(yōu)點(diǎn)非常明顯,即使除去后處理,反量化或者非對(duì)稱量化帶來額外運(yùn)算,單張圖片的推理速度通常都能獲得2-3倍的提升(這里不討論針對(duì)硬件進(jìn)行特殊優(yōu)化帶來的加速),但是隨之而來的就是量化造成的精度下降問題。
簡單來說,量化造成精度損失主要來自兩個(gè)方面:
-
取整損失,例如r = [6.8, 7.2, -0.6], scale = (7.2+0.6)/127 = 0.061417, q1 = 7.2/scale = 117.23,那么他的量化值就是117,有了0.23的損失
-
截?cái)鄵p失?,因?yàn)閟cale是取最優(yōu)區(qū)間,那么邊界的點(diǎn)勢必會(huì)有超過最大量化值的情況,這些離群點(diǎn)就會(huì)被忽略掉,量化的最大最小值區(qū)間相比于原數(shù)據(jù)分布就有了截?cái)鄵p失
為了能夠減少量化過程中的精度損失,我們參考google的論文
《Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference》,這種方法屬于aware training quantization,與之對(duì)應(yīng)的是post training quantization,后面一種方法是tensorRT使用的量化方法,后面有機(jī)會(huì)可以把實(shí)現(xiàn)的代碼上傳到github上。
事實(shí)上,學(xué)術(shù)界認(rèn)為8bit的量化已經(jīng)飽和了,已經(jīng)開始做4bit的量化研究了,但是在實(shí)際的工作過程中,發(fā)現(xiàn)對(duì)于較小的識(shí)別網(wǎng)絡(luò),8bit的量化效果依然不是令人非常滿意。
量化實(shí)現(xiàn)
為了方便的部署到嵌入式端,我最初選擇實(shí)現(xiàn)框架定在實(shí)現(xiàn)語言為C或者C++,最終選定的框架為darknet,一方面darknet在工業(yè)界有著不錯(cuò)的應(yīng)用群體,二來框架簡單直接,實(shí)現(xiàn)起來非常方便,同時(shí)還可以驗(yàn)證反向過程是否正確。在復(fù)現(xiàn)過程中,為了能夠?qū)⑺惴ǔ晒Φ募蛇M(jìn)去,對(duì)darknet做了許多小的修改,正好這里也記錄一下。
代碼鏈接:
https://github.com/ArtyZe/yolo_quantization
偽量化
相信對(duì)量化了解的同學(xué)都讀過這篇文章,tf-lite都是用的這種量化方式。區(qū)別于訓(xùn)練后量化的方式,google采用的是在訓(xùn)練過程中加入偽量化來模擬量化過程中由于取整造成的精度損失。
那么偽量化是個(gè)什么操作呢?
q=?x?as?x+aq=\left\lfloor\frac{x-a}{s}\right\rfloor x+aq=?sx?a??x+a
其中,類似中括號(hào)那里就是取整的意思。可以看到,如果說沒有取整這個(gè)操作,完全就是減一個(gè)數(shù),除一個(gè)數(shù),再乘回來,再加回來,完全就沒有任何變化。但是因?yàn)橛辛诉@個(gè)取整,所以這中間就有了變化。
想象一下,如果在訓(xùn)練過程中,采取了這么一個(gè)操作,那不就相當(dāng)于提前就把量化的損失考慮進(jìn)去了嗎?這樣等到inference的時(shí)候,精度下降就少的多了呀。
那么要把這個(gè)偽量化放在哪里呢?
那當(dāng)然是放在inference的時(shí)候需要進(jìn)行量化的位置,以論文中給出的圖來解析,
卷積的操作用公式來描述無非就是:
y=f(wx)y=f(w x)y=f(wx)
所以要量化的就是weights以及feature x。
這時(shí)候就有人提出疑問了,可是你看啊,人家給出的圖中是weights和激活值的偽量化啊,你怎么說是input的feature呢,可是如果你這樣想呢,除了第一層真正的輸入之外,剩下的層,上一層的activ輸出值不就是下一層的input值嗎,而且使用activ值有一個(gè)什么最大的好處呢?在最后一層將定點(diǎn)值反量化回到浮點(diǎn)值需要用到激活值的scale和zero_point(如果是非對(duì)稱量化的話)。
在訓(xùn)練中融合BN到CONV
我們平時(shí)見到的最多的融合BN+CONV就是在inference的時(shí)候?yàn)榱思铀僮龅?#xff0c;但是你細(xì)想一下,你BN的參數(shù)在inference的時(shí)候怎么辦呢?如果inference的時(shí)候不融合,那么BN的參數(shù)你要怎么量化,如果融合了,那么weights的量化參數(shù)是根據(jù)融合前生成的啊,那你怎么能用呢?
所以解決方案就是,把BN融合在訓(xùn)練階段就加進(jìn)去,如下圖:
具體怎么做呢?
- 首先就y=wxy=w xy=wx的前向跑一遍,計(jì)算得到均值,方差等一系列BN的參數(shù)
- 然后,利用這些BN的參數(shù),通過融合公式加到input和weights中去,將卷積公式變成真正的
y′=w′x+b′y^{\prime}=w^{\prime} x+b^{\prime}y′=w′x+b′
其中
w′=pwσ2+εb′=b?γμσ2+εw^{\prime}=\frac{p w}{\sqrt{\sigma^{2}+\varepsilon}} \quad b^{\prime}=b-\frac{\gamma \mu}{\sqrt{\sigma^{2}+\varepsilon}}w′=σ2+ε?pw?b′=b?σ2+ε?γμ?
為了后續(xù)能夠更新原生www 和 b,b,b, 該過程中不僅需要保存 w′w^{\prime}w′ 和 b′,b^{\prime},b′, 還需要保存 www 和 bbb,至于反向更新過程中,需要使用Straight Through Estimator(STE)來跳過偽量化過程中的round使得梯度可以正常回傳
- 之后根據(jù)不同層的type添加input, weights和activation量化即可。目前我采用的方式是第一層卷積input, weights和activation量化都要有,其他層如route后面的卷積層同樣需要input量化,因?yàn)閞oute的activation量化參數(shù)直接使用他的輸入層的activation量化參數(shù)即可;maxpool或者upsample都是添加activation量化即可
需要注意的
Uint8推理實(shí)現(xiàn)
下面開始介紹定點(diǎn)推理,公式如下
y=wx+by=w x+by=wx+b
由前面可知
r=S(q?Z)r=S(q-Z)r=S(q?Z)
S3(q3?Z3)=S2(q2?Z2)S1(q1?Z1)+Sb(qb?Zb)S_{3}\left(q_{3}-Z_{3}\right)=S_{2}\left(q_{2}-Z_{2}\right) S_{1}\left(q_{1}-Z_{1}\right)+S_{b}\left(q_{b}-Z_{b}\right)S3?(q3??Z3?)=S2?(q2??Z2?)S1?(q1??Z1?)+Sb?(qb??Zb?)
為了保持量綱一致,令,Sb=S1S2,Zb=0S_{b}=S_{1} S_{2}, \quad Z_{b}=0Sb?=S1?S2?,Zb?=0
對(duì)上式進(jìn)行簡單的變換
q3=Z3+M(NZ1Z2?Z1∑q2?Z2∑q1+∑q1q2+qb)q_{3}=Z_{3}+M\left(N Z_{1} Z_{2}-Z_{1} \sum q_{2}-Z_{2} \sum q_{1}+\sum q_{1} q_{2}+q_{b}\right)q3?=Z3?+M(NZ1?Z2??Z1?∑q2??Z2?∑q1?+∑q1?q2?+qb?)
其中, M=S1S2/S3M=S_{1} S_{2} / S_{3}M=S1?S2?/S3? 是唯一的浮點(diǎn)數(shù), 因此采用 M=M0×2?shiftM=M_{0} \times 2^{-s h i f t}M=M0?×2?shift 來代表, M0M_{0}M0? 和 shift 都是定點(diǎn)值,具體多大需要看精度需要,一般采用32位的值來表示。
-
在進(jìn)入到正式的推理之前,首先看上式哪些值是常量可以提前計(jì)算出來,例如Z3,Z1Z2,Z1∑q2,qbZ_{3}, Z_{1} Z_{2}, Z_{1} \sum q_{2}, q_{b}Z3?,Z1?Z2?,Z1?∑q2?,qb?都是常量,其中1代表ft,2代表weights
-
進(jìn)入到正式推理后,需要注意的問題就是溢出的問題,一般情況下為了防止這種情 況有兩種方式,一種就是使用一個(gè)shift來統(tǒng)計(jì)溢出的情況,另一種就是直接把輸出范圍擴(kuò)大,例如8bit的乘加輸出到32bit。下面我們開始計(jì)算Z2∑q1Z_{2} \sum q_{1}Z2?∑q1? 及 ∑q1q2\sum q_{1} q_{2}∑q1?q2?,為了能夠盡可能的探索優(yōu)化速度的極限,gemm函數(shù)我們使用的是mkl中的cblas庫函數(shù)。
-
得到q3q_{3}q3?之后的最后一步操作就是激活,這部分在實(shí)際使用過程中也是關(guān)乎到量化精度的一個(gè)關(guān)鍵點(diǎn)。如果激活函數(shù)是類似softmax,tanh,swish等非線性函數(shù)的話,都要通過lookup table查表的方式,為了能夠盡快的實(shí)現(xiàn),我這里選用的是tiny-yolov3,里面的激活函數(shù)都是leaky relu的線性激活函數(shù)
-
其他層例如maxpool,route由于并不涉及到計(jì)算操作,因此直接將代碼轉(zhuǎn)成uint8的即可
-
在最后一層yolo層的前面需要將uint8反量化回到float類型,方式如下
后續(xù)改進(jìn)
目前已經(jīng)實(shí)現(xiàn)了yolov3-tiny的所有算子的實(shí)現(xiàn),為了方便,目前使用relu6替代了原來的
leakyrelu,包括conv, pooling, route, upsample,這些除了conv全部都是線性的算子,后續(xù)會(huì)
繼續(xù)支持leaky relu, softmax, shortcut, elementwise add, concat等非線性算子。
量化performance
為了盡可能的不影響精度,我選擇在yolo層的上面一層conv層不進(jìn)行量化。測試結(jié)果如下,可以看到前向時(shí)間相比于原來的darknet壓縮明顯,同時(shí)精度下降非常低。
傳送門
Github鏈接:
https://github.com/ArtyZe/yolo_quantization
總結(jié)
以上是生活随笔為你收集整理的开源项目|基于darknet实现量化感知训练,已实现yolov3-tiny所有算子的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Batchsize不够大,如何发挥BN性
- 下一篇: WACV 2021 论文大盘点 目标检测