網(wǎng)絡(luò)CNN:從原理到工程部署的實(shí)戰(zhàn)總結(jié))
1. 從一條卷積計(jì)算開(kāi)始CNN到底在算什么先說(shuō)個(gè)我自己的體驗(yàn)。當(dāng)年第一次接觸卷積神經(jīng)網(wǎng)絡(luò)時(shí)找了一堆資料來(lái)看結(jié)果越看越糊涂。網(wǎng)上教程大多從“圖像分類(lèi)任務(wù)”講起上來(lái)就是“把圖片輸入網(wǎng)絡(luò)輸出類(lèi)別概率”中間那一大段黑箱過(guò)程基本靠想象。后來(lái)我把一張圖的完整處理流程在紙上一步步畫(huà)出來(lái)才真正明白CNN不是玄學(xué)它就是在做一件很樸素的事用可學(xué)習(xí)的濾波器去像素矩陣?yán)锓磸?fù)提取有結(jié)構(gòu)的特征。那段時(shí)間我還在某項(xiàng)目里做工業(yè)質(zhì)檢拍出來(lái)的零件圖有劃痕、有臟污傳統(tǒng)圖像處理方法寫(xiě)了幾百行規(guī)則換個(gè)光照就崩。后來(lái)?yè)Q成CNN檢測(cè)效果一下子穩(wěn)定了很多。但真正讓我頭疼的不是網(wǎng)絡(luò)本身的準(zhǔn)確率而是怎么理解它、怎么調(diào)它、怎么讓它跑得快。這篇文章就是把我這幾年從理論到實(shí)際部署過(guò)程中關(guān)于CNN的全部心得整理出來(lái)不寫(xiě)教科書(shū)式的內(nèi)容只講我驗(yàn)證過(guò)、踩過(guò)坑的東西。先從最核心的計(jì)算開(kāi)始。你有一張灰度圖就是一個(gè)二維矩陣比如 256×256。卷積操作就是拿一個(gè)小矩陣——通常叫卷積核或?yàn)V波器——在這個(gè)大矩陣上按步長(zhǎng)滑動(dòng)每停一個(gè)位置就把對(duì)應(yīng)位置的元素逐項(xiàng)相乘再求和得到一個(gè)數(shù)值。所有位置滑完就生成一張新的、尺寸更小的矩陣叫特征圖。舉個(gè)例子假設(shè)輸入是一個(gè) 5×5 的矩陣卷積核是 3×3步長(zhǎng)stride為 1沒(méi)有填充padding那么輸出尺寸是 3×3。計(jì)算公式很簡(jiǎn)單[ \text{輸出邊長(zhǎng)} \frac{\text{輸入邊長(zhǎng)} - \text{核大小}}{\text{步長(zhǎng)}} 1 (5 - 3) / 1 1 3 ]如果加padding把輸入矩陣周?chē)a(bǔ)一圈0那么5×5的輸入在padding1后變成7×7輸出就能保持5×5。這一點(diǎn)在搭建深層網(wǎng)絡(luò)時(shí)至關(guān)重要——一旦層數(shù)多了不加padding特征圖尺寸會(huì)迅速縮水網(wǎng)絡(luò)沒(méi)法建深。而一張彩色圖通常是三通道RGB也就是三層矩陣疊在一起。這時(shí)卷積核就不再是二維的而是和輸入通道數(shù)一致的三維核比如 3×3×3。每一層和對(duì)應(yīng)的輸入通道做卷積三個(gè)通道的結(jié)果相加再加上偏置得到輸出特征圖的一個(gè)通道。到這里你需要記住第一個(gè)要點(diǎn)卷積層的參數(shù)數(shù)量只和卷積核尺寸、輸入通道數(shù)、輸出通道數(shù)有關(guān)和輸入圖片的寬高完全無(wú)關(guān)。比如輸入是任意分辨率的RGB圖一個(gè) 3×3 卷積核從3通道變到64通道參數(shù)數(shù)就是 3×3×3×64 64 1792。不管你喂進(jìn)去的是 224×224 還是 512×512參數(shù)都一樣。這一點(diǎn)直接決定了卷積網(wǎng)絡(luò)可以處理任意尺寸輸入也為后面講部署優(yōu)化埋下伏筆。1.1 卷積核它到底在提取什么很多人問(wèn)卷積核里面的數(shù)值是怎么來(lái)的是人為設(shè)計(jì)的嗎早期確實(shí)有人工設(shè)計(jì)特征提取器比如邊緣檢測(cè)里的Sobel算子[ \begin{bmatrix} -1 0 1 \ -2 0 2 \ -1 0 1 \end{bmatrix} ]這個(gè)矩陣是固定的圖片經(jīng)過(guò)它之后橫向邊緣信息就凸顯出來(lái)了。但CNN里的卷積核初始值是隨機(jī)初始化然后通過(guò)反向傳播在訓(xùn)練過(guò)程中不斷調(diào)整最終網(wǎng)絡(luò)自己學(xué)會(huì)了類(lèi)似邊緣、紋理、形狀等特征。換句話說(shuō)我們連“讓網(wǎng)絡(luò)提取什么特征”都不用指定只要告訴它最終任務(wù)是什么它自己組織出一整套由低層到高層的特征提取流水線。低層卷積核學(xué)到的是邊緣、顏色塊這類(lèi)基礎(chǔ)紋理中層學(xué)到的是更有結(jié)構(gòu)的局部模式比如某個(gè)弧線組合、某個(gè)紋理單元高層則對(duì)應(yīng)整個(gè)物體部件甚至整體語(yǔ)義。這個(gè)特點(diǎn)我在實(shí)際項(xiàng)目里驗(yàn)證過(guò)很多次當(dāng)訓(xùn)練樣本不足時(shí)網(wǎng)絡(luò)往往會(huì)停在低層特征上輸出的特征圖看不出什么語(yǔ)義信息訓(xùn)練充分后高層特征圖對(duì)目標(biāo)位置的響應(yīng)會(huì)非常明確。1.2 步長(zhǎng)與填充特征的“采樣粒度”步長(zhǎng)設(shè)置也很關(guān)鍵。很多初學(xué)者習(xí)慣把步長(zhǎng)固定為1認(rèn)為卷積就該逐像素滑動(dòng)。但實(shí)際工程中步長(zhǎng)為2的卷積經(jīng)常被用來(lái)替代池化做下采樣好處是可以在降維的同時(shí)學(xué)習(xí)采樣方式而不是用固定規(guī)則硬性丟掉信息。每個(gè)卷積層輸入尺寸、輸出尺寸、核大小、填充、步長(zhǎng)共五個(gè)參數(shù)配置它們要滿足兩個(gè)約束一是輸出尺寸不能變成非整數(shù)即上式必須整除二是感受野覆蓋范圍要合理別讓邊緣信息在第一層就丟太多。填充的選擇也要分情況。如果你做的是圖像分類(lèi)通常paddingsame保持空間尺寸不變讓深層的特征圖不至于縮得太快如果你做的是像素級(jí)任務(wù)比如語(yǔ)義分割更要注意padding對(duì)邊界的影響因?yàn)樘畛?會(huì)在邊緣引入無(wú)效信息可能讓模型對(duì)邊緣區(qū)域的預(yù)測(cè)噪聲更大。我曾經(jīng)在分割模型里反復(fù)調(diào)整padding策略最終在訓(xùn)練時(shí)給輸入圖做小幅隨機(jī)裁剪讓邊緣目標(biāo)不那么頻繁地出現(xiàn)在邊緣位置效果比單純換padding好很多。2. 激活函數(shù)與池化讓網(wǎng)絡(luò)具備非線性與尺度魯棒性卷積本身是線性操作——它不過(guò)是一組乘加運(yùn)算。如果沒(méi)有非線性變換你再堆多少層卷積整個(gè)網(wǎng)絡(luò)等效于一個(gè)線性變換表達(dá)能力非常有限。所以每個(gè)卷積層后面幾乎都會(huì)接一個(gè)激活函數(shù)逐元素地對(duì)特征圖做非線性映射。早期網(wǎng)絡(luò)多用sigmoid但它在深層網(wǎng)絡(luò)中會(huì)帶來(lái)一個(gè)很致命的問(wèn)題梯度消失。我們公司曾經(jīng)做過(guò)一個(gè)實(shí)驗(yàn)把一個(gè)小型網(wǎng)絡(luò)用sigmoid激活函數(shù)反復(fù)加深到二十幾層結(jié)果是訓(xùn)練loss幾乎不下降。后來(lái)?yè)Q成ReLU同樣結(jié)構(gòu)、同樣超參數(shù)幾十個(gè)epoch后收斂明顯。ReLU的定義太簡(jiǎn)單了[ f(x) \max(0, x) ]正區(qū)間導(dǎo)數(shù)為1負(fù)區(qū)間直接置0。這讓梯度在正向傳播路徑上能順暢回傳緩解了梯度消失問(wèn)題。但它的問(wèn)題在于“死神經(jīng)元”——大量負(fù)值輸入被置0后對(duì)應(yīng)神經(jīng)元可能再也得不到有效梯度更新永遠(yuǎn)輸出0。我在實(shí)際訓(xùn)練中觀察過(guò)統(tǒng)計(jì)模型剛初始化時(shí)ReLU死神經(jīng)元比例通常在10%以下隨著訓(xùn)練推進(jìn)某些通道的響應(yīng)逐漸變成全0占比可以達(dá)到20%甚至更高使用Leaky ReLU后這個(gè)比例顯著下降但訓(xùn)練速度略有下降。所以現(xiàn)在更推薦在中等規(guī)模網(wǎng)絡(luò)里優(yōu)先試Leaky ReLU或GELU而不是一把梭用ReLU。2.1 池化解決“位置微變但類(lèi)別不變”的問(wèn)題池化層做的事情概括起來(lái)就是兩個(gè)字降采樣。最常用的是最大池化——取一個(gè)小窗口內(nèi)的最大值作為輸出。這么做有兩個(gè)直觀的動(dòng)機(jī)一是降低特征圖分辨率減少后續(xù)計(jì)算量二是引入一定平移不變性圖像里的目標(biāo)稍微移動(dòng)幾個(gè)像素池化結(jié)果仍然穩(wěn)定。但池化有個(gè)容易被人忽略的問(wèn)題它會(huì)讓位置信息變得粗糙。如果你做的任務(wù)是圖像分類(lèi)目標(biāo)位置偏移幾個(gè)像素本來(lái)就不影響語(yǔ)義粗一點(diǎn)沒(méi)關(guān)系但如果是目標(biāo)檢測(cè)或關(guān)鍵點(diǎn)定位隨隨便便下采樣四倍定位精度就會(huì)大打折扣。所以現(xiàn)代檢測(cè)模型里主干網(wǎng)絡(luò)的下采樣比例往往會(huì)被刻意限制比如輸出特征圖的步長(zhǎng)一般控制在32或16個(gè)別對(duì)小目標(biāo)敏感的任務(wù)甚至?xí)档?。池化的具體操作不復(fù)雜但選擇哪種池化是有講究的池化類(lèi)型計(jì)算方式適用場(chǎng)景最大池化取窗口最大值分類(lèi)任務(wù)、紋理顯著目標(biāo)保留強(qiáng)響應(yīng)平均池化取窗口均值全局信息整合避免極端值干擾全局平均池化對(duì)整個(gè)特征圖取均值分類(lèi)頭前最后一層直接替代全連接層自適應(yīng)池化由PyTorch按輸出尺寸反推窗口輸入尺寸不固定時(shí)統(tǒng)一特征維度在實(shí)際項(xiàng)目中我常常把分類(lèi)網(wǎng)絡(luò)末尾的全連接層刪掉換成全局平均池化加一個(gè)1×1卷積輸出類(lèi)別數(shù)。這樣做的理由是全連接層參數(shù)量巨大而且一旦輸入尺寸變了維度就對(duì)不上全局平均池化則對(duì)輸入尺寸完全免疫并且天然匯總了每張?zhí)卣鲌D的全局語(yǔ)義。這個(gè)調(diào)整對(duì)控制過(guò)擬合也有幫助——參數(shù)量小了需要擬合的空間就小了。順便說(shuō)一嘴現(xiàn)在很多新網(wǎng)絡(luò)已經(jīng)不用傳統(tǒng)池化。比如用步長(zhǎng)為2的卷積做下采樣或者用空洞卷積在保持分辨率的前提下擴(kuò)大感受野。但池化這個(gè)概念并沒(méi)有過(guò)時(shí)理解池化的本質(zhì)有助于你理解為什么特征圖尺寸會(huì)一層層縮小以及怎么控制網(wǎng)絡(luò)的空間信息流。3. 特征圖的形狀變化書(shū)寫(xiě)維度勝過(guò)看十遍論文我見(jiàn)過(guò)很多新手從GitHub上拉了一個(gè)模型下來(lái)代碼運(yùn)行通了但一旦要改輸入尺寸或者改網(wǎng)絡(luò)深度就卡住了。問(wèn)題的根子在于沒(méi)有建立對(duì)特征圖形狀變化的直覺(jué)。下面我用一個(gè)具體例子完整走一遍。假設(shè)輸入圖像是 224×224×3網(wǎng)絡(luò)結(jié)構(gòu)如下卷積層164個(gè)3×3卷積核padding1步長(zhǎng)1。輸入224×224×3輸出224×224×64。參數(shù)量3×3×3×64641792。池化層1最大池化窗口2×2步長(zhǎng)2。輸出112×112×64。卷積層2128個(gè)3×3卷積核padding1步長(zhǎng)1。輸入112×112×64輸出112×112×128。參數(shù)量3×3×64×12812873856。池化層2窗口2×2步長(zhǎng)2。輸出56×56×128。卷積層3256個(gè)3×3卷積核padding1步長(zhǎng)1。輸出56×56×256。參數(shù)量3×3×128×256256295168。池化層3輸出28×28×256。全局平均池化輸出1×1×256。全連接層或者1×1卷積輸出類(lèi)別數(shù)。這個(gè)流程中可以清晰看到空間尺寸從224→112→56→28通道數(shù)從3→64→128→256??臻g尺寸下降意味著網(wǎng)絡(luò)在更大范圍內(nèi)匯總信息通道數(shù)上升意味著在更多維度上描述特征。這兩者總是交織在一起這也是CNN比較優(yōu)雅的地方。從計(jì)算量角度看主流框架PyTorch、TensorFlow的FLOPs計(jì)算工具都能幫你自動(dòng)統(tǒng)計(jì)但你自己要能快速估算每層的計(jì)算量。單個(gè)卷積層的計(jì)算量大概是[ \text{FLOPs} \approx \text{輸出元素?cái)?shù)} \times \text{核元素?cái)?shù)} \times \text{輸入通道數(shù)} ]以剛才第一層為例輸出是224×224×64核元素?cái)?shù)是9輸入通道是3所以 FLOPs ≈ 224×224×64×9×3 8.67 億次乘加接近9億??雌饋?lái)很大但和后面層比起來(lái)這只是毛毛雨。在實(shí)際部署時(shí)模型推理時(shí)間往往不是按參數(shù)數(shù)量算的而是按計(jì)算量FLOPs和訪存量算的。這兩個(gè)指標(biāo)在CPU和GPU上的權(quán)重完全不同后面我會(huì)專(zhuān)門(mén)展開(kāi)講。3.1 感受野每個(gè)點(diǎn)看著多大范圍的輸入感受野Receptive Field是CNN里最容易被忽視的概念它決定了最后一層特征圖上的一個(gè)點(diǎn)在原圖上對(duì)應(yīng)多大范圍。簡(jiǎn)單記公式初始感受野為1每經(jīng)過(guò)一個(gè)卷積核尺寸為k、步長(zhǎng)為s的層感受野按如下方式增大[ RF_{\text{out}} RF_{\text{in}} (k - 1) \times \text{stride累積} ]我舉個(gè)例子你就能秒懂。一個(gè)兩層3×3卷積堆疊中間沒(méi)有池化每層步長(zhǎng)1。第一層輸出上每個(gè)點(diǎn)看到的是原圖3×3區(qū)域第二層輸出的每個(gè)點(diǎn)看到的是第一層特征圖上3×3區(qū)域而第一層上3×3區(qū)域?qū)?yīng)原圖5×5范圍。所以?xún)蓪?×3卷積的感受野是5×5。三層3×3卷積的感受野是7×7。這一點(diǎn)在實(shí)際工程里的意義在于感受野如果太小模型看不到足夠的上下文識(shí)別大目標(biāo)時(shí)容易漏檢太大又可能引入過(guò)多無(wú)關(guān)區(qū)域?qū)е戮植考?xì)節(jié)被淹沒(méi)。比如檢測(cè)小目標(biāo)高分辨率特征圖上的感受野本身就不大反而沒(méi)太大問(wèn)題而檢測(cè)大目標(biāo)通常依賴(lài)低分辨率、大感受野的高層特征。所以有些任務(wù)會(huì)刻意用空洞卷積來(lái)快速擴(kuò)大感受野??斩绰蕿?的3×3卷積實(shí)際覆蓋范圍相當(dāng)于5×5但參數(shù)仍然是9個(gè)。我在一個(gè)語(yǔ)義分割項(xiàng)目里就靠空洞卷積把感受野擴(kuò)大了一倍多又不增加什么參數(shù)量?jī)?nèi)存占用和速度幾乎沒(méi)有損失。4. 經(jīng)典網(wǎng)絡(luò)架構(gòu)的演進(jìn)邏輯每個(gè)改動(dòng)背后都有原因CNN發(fā)展到現(xiàn)在主干結(jié)構(gòu)已經(jīng)相當(dāng)成熟。但不了解來(lái)龍去脈你去讀那些新模型的代碼會(huì)非常吃力。我梳理了一下我認(rèn)為最關(guān)鍵的幾次架構(gòu)演進(jìn)它們每一個(gè)都回答了某個(gè)具體痛點(diǎn)。AlexNet2012用ReLU解決了深層網(wǎng)絡(luò)訓(xùn)練慢的問(wèn)題用Dropout緩解了過(guò)擬合用重疊池化小幅提升精度。這個(gè)網(wǎng)絡(luò)第一次讓大家認(rèn)識(shí)到GPU并行訓(xùn)練深度網(wǎng)絡(luò)的威力。VGG2014核心思想就是“小核堆疊”。用三個(gè)3×3卷積堆疊替代一個(gè)7×7卷積感受野相同但參數(shù)量從49C2變成27C2還引入了更多非線性。VGG結(jié)構(gòu)極其規(guī)整是很多工程項(xiàng)目的默認(rèn)backbone選擇之一。GoogLeNet2014提出了Inception結(jié)構(gòu)每個(gè)block里并行多個(gè)尺寸的卷積核最后拼接通道。好處是在同一層內(nèi)捕捉多尺度特征但結(jié)構(gòu)復(fù)雜工程落地不太友好。ResNet2015核心是殘差連接把輸入直接加到輸出上。為什么有效因?yàn)榫W(wǎng)絡(luò)只需要學(xué)習(xí)殘差部分梯度可以從后層直接跳回前層訓(xùn)練幾十上百層不再是問(wèn)題。我試過(guò)把ResNet的殘差連接去掉在CIFAR-10上同樣的深度準(zhǔn)確率掉了接近10個(gè)百分點(diǎn)。這不是玄學(xué)是優(yōu)化難度實(shí)實(shí)在在降低了。DenseNet2016每一層都跟前面所有層連接特征復(fù)用能力極強(qiáng)參數(shù)效率也很高。但其顯存占用通常較大因?yàn)橐4娲罅恐虚g特征。EfficientNet2019用復(fù)合縮放方法同時(shí)調(diào)整深度、寬度、分辨率。這個(gè)思路很實(shí)用——你不需要手動(dòng)試很多組合按固定比例一起放大經(jīng)常能得到明顯更優(yōu)的效果。不同任務(wù)對(duì)主干架構(gòu)的需求差異很大。我做工業(yè)質(zhì)檢的時(shí)候小樣本場(chǎng)景居多輕量網(wǎng)絡(luò)反而更穩(wěn)在另一個(gè)需要高精度的識(shí)別項(xiàng)目里用了預(yù)訓(xùn)練的ResNet系列做遷移學(xué)習(xí)效果比從零訓(xùn)練好非常多。4.1 從零訓(xùn)練還是微調(diào)這是一個(gè)工程決策很多初學(xué)者以為模型總要從頭訓(xùn)練但實(shí)際上在絕大多數(shù)實(shí)際項(xiàng)目里遷移學(xué)習(xí)才是正確起點(diǎn)。預(yù)訓(xùn)練模型在ImageNet等大規(guī)模數(shù)據(jù)集上訓(xùn)練過(guò)的已經(jīng)在低中層的特征提取上非常成熟了你只需要在其基礎(chǔ)上針對(duì)自己的數(shù)據(jù)微調(diào)高層。我記得有一個(gè)直觀對(duì)比同樣只有5000張工業(yè)圖片的數(shù)據(jù)集從零訓(xùn)練的ResNet50驗(yàn)證集準(zhǔn)確率大約在87%而使用ImageNet預(yù)訓(xùn)練權(quán)重微調(diào)的版本能達(dá)到95%以上。這個(gè)差距在數(shù)據(jù)量越少時(shí)越明顯。不過(guò)要注意一個(gè)坑預(yù)訓(xùn)練權(quán)重本身是用自然圖像訓(xùn)出來(lái)的如果你的數(shù)據(jù)和自然圖像差異極大——比如是X光片、光譜圖、顯微圖像——那預(yù)訓(xùn)練對(duì)低層特征的增益就沒(méi)那么明顯了這時(shí)候可以考慮凍結(jié)前面幾層甚至全部從頭訓(xùn)練。微調(diào)時(shí)學(xué)習(xí)率要設(shè)得比從頭訓(xùn)練小很多通常建議設(shè)為主訓(xùn)練學(xué)習(xí)率的10%甚至更小。否則第一輪迭代就可能劇烈破壞原有權(quán)重。我自己常用的策略是先用1e-4的學(xué)習(xí)率微調(diào)全部層訓(xùn)練幾個(gè)epoch后再解凍所有層用小學(xué)習(xí)率繼續(xù)訓(xùn)練。數(shù)據(jù)增強(qiáng)在微調(diào)階段也不能省哪怕你的任務(wù)是工業(yè)圖我也會(huì)做隨機(jī)翻轉(zhuǎn)、平移、亮度擾動(dòng)把有限數(shù)據(jù)的利用率提上去。5. 訓(xùn)練CNN你真正需要盯住的幾個(gè)信號(hào)很多人以為訓(xùn)練CNN就是把數(shù)據(jù)丟進(jìn)去、等loss下降就完事。實(shí)際上訓(xùn)練過(guò)程中的很多信號(hào)非常重要我在多個(gè)項(xiàng)目里靠這些信號(hào)救回了必失敗的訓(xùn)練。第一個(gè)信號(hào)是初始loss。分類(lèi)任務(wù)如果是C類(lèi)初始loss應(yīng)該在ln(C)附近。如果你的初始loss偏差很大大概率是權(quán)重初始化不對(duì)、輸入沒(méi)有歸一化或者標(biāo)簽有錯(cuò)。我在一個(gè)多標(biāo)簽分類(lèi)任務(wù)里初始loss一直比理論值高很多排查了一天發(fā)現(xiàn)是損失函數(shù)里標(biāo)簽平滑參數(shù)寫(xiě)錯(cuò)了把不該平滑的維度也消掉了。養(yǎng)成在跑訓(xùn)練前先算一遍期望初始loss的習(xí)慣能幫你省下大量排查時(shí)間。第二個(gè)信號(hào)是梯度范數(shù)。每個(gè)epoch記錄梯度的L2范數(shù)如果訓(xùn)練初期梯度范數(shù)突然爆炸到1e3以上說(shuō)明要么是學(xué)習(xí)率太大要么是數(shù)據(jù)里有異常值。反之如果梯度范數(shù)一直在1e-7量級(jí)幾乎不動(dòng)那大概率梯度已經(jīng)消失需要檢查激活函數(shù)和網(wǎng)絡(luò)深度。第三個(gè)信號(hào)是訓(xùn)練集和驗(yàn)證集的gap。gap小說(shuō)明模型沒(méi)在過(guò)擬合還有提升空間gap大說(shuō)明泛化能力不夠優(yōu)先考慮增加數(shù)據(jù)增強(qiáng)、正則化或者降低模型容量。但很多人忽略的是驗(yàn)證集loss有時(shí)會(huì)因?yàn)閿?shù)據(jù)增強(qiáng)強(qiáng)度太大而長(zhǎng)期不降這時(shí)候不代表模型沒(méi)學(xué)好只是驗(yàn)證集和你喂給模型的數(shù)據(jù)分布不一致了。這種時(shí)候最好單獨(dú)跑一組無(wú)增強(qiáng)的驗(yàn)證數(shù)據(jù)做對(duì)照我靠這招排掉過(guò)好幾個(gè)假象。5.1 數(shù)據(jù)增強(qiáng)是治過(guò)擬合的特效藥但不是萬(wàn)能藥數(shù)據(jù)增強(qiáng)在CNN訓(xùn)練里的地位非常高尤其當(dāng)數(shù)據(jù)量不夠時(shí)。常用的增強(qiáng)手段有隨機(jī)水平翻轉(zhuǎn)、小角度旋轉(zhuǎn)、隨機(jī)裁剪顏色擾動(dòng)亮度、對(duì)比度、飽和度Cutout / Random Erasing隨機(jī)遮掉一個(gè)小區(qū)域Mixup / CutMix按比例混合兩張圖及標(biāo)簽這些方法各有適用場(chǎng)景。水平翻轉(zhuǎn)對(duì)多數(shù)目標(biāo)識(shí)別任務(wù)都安全但對(duì)文字識(shí)別、車(chē)牌識(shí)別這類(lèi)有方向性的任務(wù)反而不安全。Cutout類(lèi)增強(qiáng)能提升模型對(duì)遮擋的魯棒性但對(duì)小目標(biāo)檢測(cè)隨機(jī)遮掉一個(gè)區(qū)域可能恰好把目標(biāo)蓋住了造成標(biāo)簽與內(nèi)容不匹配訓(xùn)出來(lái)的模型反而變差。我在一個(gè)產(chǎn)品表面缺陷檢測(cè)項(xiàng)目里的經(jīng)驗(yàn)是增強(qiáng)強(qiáng)度需要根據(jù)數(shù)據(jù)量和任務(wù)復(fù)雜度調(diào)到一個(gè)合適的甜點(diǎn)區(qū)。增強(qiáng)太弱模型記憶數(shù)據(jù)增強(qiáng)太強(qiáng)模型學(xué)不到真實(shí)的判別細(xì)節(jié)。這里沒(méi)有萬(wàn)能公式只能靠實(shí)驗(yàn)。你可以固定一個(gè)基準(zhǔn)增強(qiáng)配置然后每次只改一個(gè)維度看驗(yàn)證集變化。改動(dòng)一次、驗(yàn)證一次這聽(tīng)起來(lái)慢但比同時(shí)改五個(gè)變量最后不知道是哪個(gè)起作用要快得多。5.2 Batch Size、學(xué)習(xí)率與訓(xùn)練策略Batch Size 的選擇對(duì)訓(xùn)練結(jié)果影響很大。太小的 batch比如2、4梯度噪聲大訓(xùn)練不穩(wěn)定太大的 batch比如512、1024雖然梯度穩(wěn)定但容易收斂到尖銳極小值泛化能力可能變差。學(xué)術(shù)界在“大batch與小batch”上的討論很多我的工程經(jīng)驗(yàn)是中等batch32到128通常普適性最好尤其在顯存不夠時(shí)先用小batch確認(rèn)代碼邏輯能跑通再逐步往上加。學(xué)習(xí)率策略上比較推薦的做法是先用學(xué)習(xí)率熱身warmup從很小的值線性上升到目標(biāo)值避免初期劇烈震蕩訓(xùn)練中段用余弦退火或階梯下降把學(xué)習(xí)率逐步減小最后1-2個(gè)epoch可以降到極小值讓loss在底部多磨一會(huì)兒。我自己常用的是OneCycle策略學(xué)習(xí)率先升后降配合適當(dāng)?shù)淖畲髮W(xué)習(xí)率常常能在更短的epoch里達(dá)到更好的精度。但有前提——你對(duì)數(shù)據(jù)量、batch和網(wǎng)絡(luò)結(jié)構(gòu)已經(jīng)有比較充分的把握否則直接硬套OneCycle反而容易跑飛。6. 從訓(xùn)練到部署讓CNN真正跑起來(lái)的工程考量模型在GPU上驗(yàn)證準(zhǔn)確率不錯(cuò)不等于就能順利上線。真正落地部署時(shí)你會(huì)遇到另一套問(wèn)題推理延遲、顯存占用、精度損失、平臺(tái)兼容性。先明確一個(gè)基本事實(shí)訓(xùn)練時(shí)看重吞吐量throughput推理時(shí)看重延遲latency和資源占用memory/FLOPs。訓(xùn)練時(shí)可以隨便用大的batch、混合精度、多卡并行但推理時(shí)通常要求在單樣本或小batch下盡可能快。更關(guān)鍵的是硬件差異。GPU擅長(zhǎng)并行稠密矩陣運(yùn)算CPU對(duì)這類(lèi)運(yùn)算的支持遠(yuǎn)不如GPU但如果網(wǎng)絡(luò)里有大量小卷積、拼接、切分操作反而可能受限于訪存和調(diào)度在CPU上的表現(xiàn)差異很大。所以在實(shí)際項(xiàng)目里必須先搞清楚最終跑在什么設(shè)備上再?zèng)Q定優(yōu)化方向——如果跑在服務(wù)器GPU上浮點(diǎn)精度和顯存占用優(yōu)先如果跑在邊緣設(shè)備或手機(jī)上優(yōu)先考慮量化、剪枝、算子融合。6.1 通道剪枝、量化與知識(shí)蒸餾落地時(shí)最常用的三個(gè)手段是剪枝、量化、蒸餾但它們的收益和代價(jià)大相徑庭。通道剪枝是去掉網(wǎng)絡(luò)中對(duì)最終輸出貢獻(xiàn)不大的通道。常見(jiàn)做法是觀察每個(gè)通道的權(quán)重范數(shù)或者激活統(tǒng)計(jì)把范數(shù)小或激活接近零的通道刪掉再微調(diào)恢復(fù)精度。剪枝比例很關(guān)鍵剪太狠精度崩得快剪太少收益又低。我在一個(gè)輕量分類(lèi)網(wǎng)絡(luò)里把通道數(shù)從128剪到96精度只掉0.3%FLOPs卻少了近30%這比例相當(dāng)劃算。量化是把你訓(xùn)練的32位浮點(diǎn)模型壓縮成8位整數(shù)甚至更低精度。一般有三種方式訓(xùn)練后動(dòng)態(tài)量化、訓(xùn)練后靜態(tài)量化、量化感知訓(xùn)練。動(dòng)態(tài)量化實(shí)現(xiàn)簡(jiǎn)單但只對(duì)部分算子有效靜態(tài)量化需要校準(zhǔn)數(shù)據(jù)效果更好但操作更繁瑣。量化感知訓(xùn)練是在訓(xùn)練過(guò)程中模擬量化噪聲讓模型提前適應(yīng)低精度通常精度損失最小。對(duì)于CPU部署8位量化是性?xún)r(jià)比最高的優(yōu)化沒(méi)有之一。知識(shí)蒸餾是訓(xùn)練一個(gè)輕量學(xué)生模型去學(xué)大模型的行為。做法通常是讓學(xué)生模型同時(shí)去擬合真實(shí)標(biāo)簽和教師模型的軟輸出。軟輸出里包含了類(lèi)別間相似度信息比如“貓”和“狗”的概率分布其實(shí)蘊(yùn)含了大量結(jié)構(gòu)知識(shí)這比單純的one-hot標(biāo)簽信息量豐富得多。我的一個(gè)落地項(xiàng)目里教師模型是ResNet50學(xué)生模型是只有原來(lái)1/4計(jì)算量的輕量網(wǎng)絡(luò)蒸餾之后學(xué)生模型準(zhǔn)確率比從零訓(xùn)練高了約4個(gè)百分點(diǎn)。實(shí)踐中這三招經(jīng)常配合使用先剪枝、再量化、必要時(shí)蒸餾輔助每一步都要在驗(yàn)證集上確認(rèn)精度變化不能盲目相信某個(gè)預(yù)設(shè)壓縮率。6.2 框架選擇與推理引擎在訓(xùn)練側(cè)PyTorch和TensorFlow是目前的主流選擇。如果項(xiàng)目以研究驗(yàn)證為主PyTorch生態(tài)更靈活在過(guò)去的幾年內(nèi)新論文模型基本都優(yōu)先提供PyTorch版本如果團(tuán)隊(duì)已有成熟的TensorFlow生產(chǎn)鏈路沿用TensorFlow也完全合理畢竟工程穩(wěn)定性和圖優(yōu)化集成更成熟。模型訓(xùn)練完成后的推理環(huán)節(jié)則可以考慮專(zhuān)門(mén)推理引擎比如TensorRTNVIDIA GPU、OpenVINOIntel CPU或ONNX Runtime跨平臺(tái)通用它們會(huì)把計(jì)算圖做算子融合、層間緩沖復(fù)用等優(yōu)化加速效果通常非常顯著。一個(gè)容易被忽略的點(diǎn)是推理引擎支持的算子集合和訓(xùn)練框架不完全一致。有些新算子或者自定義算子在ONNX轉(zhuǎn)換時(shí)可能直接失敗或者轉(zhuǎn)換后跑得非常慢。所以從一開(kāi)始訓(xùn)練時(shí)就要考慮部署端兼容性。如果目標(biāo)平臺(tái)不支持某個(gè)算子網(wǎng)絡(luò)設(shè)計(jì)階段就換掉別等到訓(xùn)練完再折騰。我在一個(gè)項(xiàng)目里就因?yàn)樽远x池化算子無(wú)法導(dǎo)出到ONNX最后繞了一個(gè)大彎改網(wǎng)絡(luò)結(jié)構(gòu)損失了三天時(shí)間。另外輸入尺寸在部署時(shí)最好固定下來(lái)。動(dòng)態(tài)輸入尺寸雖然靈活但很多推理引擎對(duì)動(dòng)態(tài)shape支持不好會(huì)退化成低效的通用路徑。預(yù)先設(shè)定好幾個(gè)常用分辨率讓推理引擎把圖的shape和內(nèi)存布局都靜態(tài)化性能和穩(wěn)定性都會(huì)好很多。7. CNN的邊界與變體什么時(shí)候它是最好的選擇CNN很強(qiáng)但它不是所有問(wèn)題的唯一答案。在圖像類(lèi)任務(wù)里CNN至今依然是默認(rèn)選擇但在處理序列、點(diǎn)云、圖結(jié)構(gòu)數(shù)據(jù)時(shí)像Transformer、GNN這些架構(gòu)可能更合適。甚至在某些圖像任務(wù)上Vision TransformerViT已經(jīng)在大型數(shù)據(jù)集上超越CNN但ViT往往需要海量數(shù)據(jù)和更強(qiáng)的正則化才能訓(xùn)練好在小樣本場(chǎng)景下未必比得上有先驗(yàn)偏置的CNN。所謂“先驗(yàn)偏置”就是CNN天然具備的特性局部連接、權(quán)值共享、平移等變。你不需要告訴它“相鄰像素通常相關(guān)”網(wǎng)絡(luò)結(jié)構(gòu)本身就把這個(gè)歸納偏置內(nèi)置進(jìn)去了。在數(shù)據(jù)量中等或偏少的場(chǎng)景中這個(gè)偏置是巨大優(yōu)勢(shì)——模型不需要重新摸索像素間的空間關(guān)系。但這幾年有一種趨勢(shì)是用卷積注意力的混合結(jié)構(gòu)來(lái)取長(zhǎng)補(bǔ)短。比如在淺層用卷積抓局部紋理在深層用注意力機(jī)制捕捉全局依賴(lài)。我在一個(gè)遙感影像分類(lèi)項(xiàng)目里試過(guò)這種混合結(jié)構(gòu)全局檢測(cè)效果比純卷積高不少尤其在需要跨區(qū)域上下文時(shí)優(yōu)勢(shì)明顯。如果你手頭任務(wù)的特征同時(shí)有強(qiáng)局部性和強(qiáng)全局性比如大尺寸醫(yī)學(xué)影像或遙感圖值得優(yōu)先考慮混合架構(gòu)。不過(guò)需要提醒的是不要盲目追求最新架構(gòu)。對(duì)一個(gè)特定業(yè)務(wù)問(wèn)題穩(wěn)定性、可復(fù)現(xiàn)性、部署易用性往往比刷那零點(diǎn)幾個(gè)百分點(diǎn)更重要。我在和不少同行交流時(shí)發(fā)現(xiàn)最穩(wěn)妥的路徑往往是先用一個(gè)成熟且更簡(jiǎn)單的CNN基線跑通全鏈路再通過(guò)消融實(shí)驗(yàn)逐步替換模塊每一步都驗(yàn)證收益和成本。8. 我踩過(guò)的最有價(jià)值的幾個(gè)坑這部分挑三個(gè)印象最深的坑來(lái)寫(xiě)可能比前面所有章節(jié)都更有實(shí)際價(jià)值。第一個(gè)坑是把輸入歸一化忘在訓(xùn)練前處理里但推理時(shí)沒(méi)有對(duì)應(yīng)處理。一個(gè)分類(lèi)模型的輸入歸一化方式在訓(xùn)練和推理時(shí)必須完全一致。有一陣我部署一個(gè)模型時(shí)訓(xùn)練代碼里對(duì)圖像先做減均值除以方差但導(dǎo)出的模型沒(méi)有把這些歸一化操作包進(jìn)去推理端直接把原始像素喂進(jìn)去結(jié)果準(zhǔn)確率降了十多個(gè)百分點(diǎn)。后來(lái)我在導(dǎo)出時(shí)把歸一化作為一個(gè)預(yù)處理節(jié)點(diǎn)嵌入計(jì)算圖或者確保外部預(yù)處理和訓(xùn)練時(shí)完全相同問(wèn)題才徹底解決。第二個(gè)坑是用數(shù)據(jù)增強(qiáng)后的分布去評(píng)估模型。最初做一項(xiàng)檢測(cè)任務(wù)時(shí)我沿用分類(lèi)任務(wù)的模式對(duì)驗(yàn)證集也加了較強(qiáng)的增強(qiáng)結(jié)果驗(yàn)證集指標(biāo)一直在95%左右沒(méi)什么變化我就以為是訓(xùn)練飽和了。后來(lái)發(fā)現(xiàn)增強(qiáng)的隨機(jī)性掩蓋了真實(shí)困難樣本的表現(xiàn)。改成固定翻轉(zhuǎn)、固定裁剪的評(píng)估方式后模型的薄弱點(diǎn)立刻暴露出來(lái)針對(duì)性補(bǔ)了數(shù)據(jù)之后最終泛化性能大幅提升。第三個(gè)坑是推理引擎默認(rèn)選擇難以預(yù)料很慢的路徑。某個(gè)算子在某些推理引擎上需要跨步內(nèi)存拷貝性能一下子掉好幾倍。有一次我測(cè)量一個(gè)全卷積檢測(cè)網(wǎng)絡(luò)的推理延遲TensorRT下只有8毫秒改用CPU端的ONNX Runtime卻跑了近200毫秒。后來(lái)定位到是某個(gè)步長(zhǎng)為2的轉(zhuǎn)置卷積算子沒(méi)有好的實(shí)現(xiàn)路徑。這種情況下最簡(jiǎn)單的辦法就是改網(wǎng)絡(luò)結(jié)構(gòu)用最近鄰上采樣加普通卷積代替轉(zhuǎn)置卷積代價(jià)極小但延遲下降了非常多。這些坑說(shuō)出來(lái)都特別簡(jiǎn)單但每一個(gè)都是實(shí)際項(xiàng)目中耗費(fèi)了大量時(shí)間才換來(lái)的教訓(xùn)。如果你以后遇到類(lèi)似“怎么數(shù)據(jù)越訓(xùn)越差”“怎么部署后精度和訓(xùn)練時(shí)對(duì)不上”的問(wèn)題可以先按上面的清單排查一遍。對(duì)我個(gè)人來(lái)說(shuō)CNN早已不是模型結(jié)構(gòu)那么簡(jiǎn)單。它是理解深度學(xué)習(xí)中“表示學(xué)習(xí)”的最佳教材——從像素到特征、從特征到語(yǔ)義、從語(yǔ)義到?jīng)Q策每一層的變換都有清晰的幾何與統(tǒng)計(jì)含義。無(wú)論未來(lái)Transformer類(lèi)架構(gòu)怎么流行這份對(duì)特征提取與空間建模的理解放到任何架構(gòu)上都不會(huì)過(guò)時(shí)。如果你正在學(xué)習(xí)和使用CNN我建議你不要只跑通現(xiàn)成代碼而是親手把每層的輸入輸出尺寸算一遍把中間特征圖可視化出來(lái)把loss曲線的每一個(gè)異常都追究到底。這個(gè)過(guò)程熬過(guò)去了你對(duì)深度學(xué)習(xí)的整體認(rèn)知會(huì)上一個(gè)臺(tái)階。