
做深度學習做到今天我見過太多人把 1×1 卷積當“降維工具”把 3×3 卷積當“特征提取工具”各自用得很溜卻從沒想過它們之間其實藏著一套組合哲學。我最初也是這樣——直到有一次要把一個基準模型壓縮到能跑在端側設備上才把“1×1 3×3”的搭配從“碰巧好用”變成了“刻意設計”。這篇文章我想把這兩步的關系徹底講透它們各自在干什么、為什么組合起來這么強、以及最常見的三種“兩步走”套路分別省在哪、又該怎么在代碼里落地。不管你是剛接觸卷積神經網絡的新手還是已經在調模型但始終對“瓶頸結構”“深度可分離卷積”半懂不懂的從業(yè)者這套思路都值得花十分鐘重新捋一遍??炊竽銜l(fā)現(xiàn)很多號稱精巧的網絡結構本質上都是在反復使用同一個哲學把空間維度的處理和通道維度的處理解耦開分兩步走。這個認知會直接影響你設計網絡時的判斷力。1. 兩種卷積各自的底層邏輯先別混著用1.1 1×1卷積不是“小卷積”它是通道維上的全連接很多初學者第一眼看到 1×1 卷積會覺得它“什么都沒做”——畢竟卷積核在空間上只有一個像素對單個位置來說它沒有聚合任何鄰域信息。但問題恰恰出在這里1×1 卷積根本不需要空間信息它專管通道之間的線性組合。拿具體計算來說輸入形狀是 H×W×C_in經過一個 1×1 卷積輸出 H×W×C_out。在空間上每個點 (i, j)它做的事情是把一個長度為 C_in 的向量通過一個 C_in×C_out 的權重矩陣映射成另一個長度為 C_out 的向量。這不就是作用在每個空間位置上的全連接嗎所以 1×1 卷積有三個核心作用通道信息混合把不同通道在同一空間位置上的特征重新線性組合讓信息在通道維度上“流動”起來。改變通道數(shù)C_in 和 C_out 可以自由設置想壓縮就壓縮想擴張就擴張。這是控制模型計算量的最關鍵開關。廉價的計算代價它完全不涉及空間鄰域FLOPs 只有標準卷積的平方反比級別。后面我會寫具體算式。如果你把網絡想象成一條流水線1×1 卷積就像流水線上給每件產品“貼標簽、重新裝箱”的工位——不改變物品本身的空間位置但徹底重排了它的屬性組合。1.2 3×3卷積才是真正的空間特征提取器3×3 卷積就完全不同了。它的每個輸出位置會看輸入特征圖上周圍 3×3 鄰域共 9 個像素在空間上產生局部感受野。圖像中的邊緣、角點、紋理等最基本的結構都是靠這種局部鄰域聚合才能被捕捉到。那為什么偏偏是 3×3 這么流行而不是 5×5 或者 7×7最經典的解釋來自 VGG 系列的經驗兩個串聯(lián)的 3×3 卷積感受野等效于一個 5×5 卷積三個串聯(lián)的 3×3 卷積感受野等效于一個 7×7 卷積。但參數(shù)量和計算量卻少得多。推一下單層 7×7 卷積的參數(shù)量是 49×C×C而三個 3×3 卷積是 3×9×C×C27×C×C直接省了接近一半。更重要的是兩層 3×3 之間夾著非線性激活比單層 7×7 多引入兩次非線性變換特征表達能力反而更強。關于“為什么主流網絡都在用 3×3 堆疊而不是大卷積核”這是核心答案。1.3 它們不在同一個維度上干活這才是能組合的前提這句話聽起來像廢話但它是“兩步走”哲學的全部基礎1×1 卷積只動通道維3×3 卷積主要動空間維同時也在混合通道。一個管“有哪些信息”一個管“周圍發(fā)生了什么”。如果你只用 1×1 卷積堆網絡永遠無法在空間上感知鄰域結構如果你只用 3×3 卷積堆網絡通道之間的信息融合效率會很低而且計算量爆炸。兩種卷積各自都“缺一半”。把它們按順序組合就是在兩個維度上分別完成自己的職責——先整理通道再提取空間或者先提取空間再融合通道。這兩步的順序、次數(shù)、通道比例演化出了過去十年里大量經典結構設計。2. “兩步走”的三種經典走法本質是同一種哲學2.1 先降、再提、后升瓶頸結構BottleneckResNet 系列里最經典的瓶頸模塊走法是這樣的先用一個 1×1 卷積把通道從 256 壓到 64再用一個 3×3 卷積在低通道數(shù)下做空間特征提取最后用一個 1×1 卷積把通道恢復到 256。整個過程是典型的“降維-卷積-升維”。這一步每一步都有明確動機。直接拿 256 通道做 3×3 卷積計算量非常大先降維到 64讓 3×3 卷積作用在窄通道上計算量一下子降到一個零頭空間特征的提取并沒有損失太多——因為信息在降維后被壓縮到了更緊湊的表示里。最后再升維是為了讓輸出通道數(shù)和輸入對齊便于做殘差連接。這一個模塊內部的“兩步走”還不止一次1×1 降維是第一步調整3×3 提取是第二步空間操作最后的 1×1 升維算是第三步收尾。在 ResNet-50 及更深版本里這種瓶頸結構基本取代了原來的兩層 3×3 結構原因只有一個同樣表達能力下FLOPs 和參數(shù)量都大幅下降。2.2 先空間、后通道深度可分離卷積Depthwise Separable ConvolutionMobileNet 系列大量使用的深度可分離卷積是另一種兩步走。它把標準卷積拆成兩個獨立操作第一步是深度卷積每個輸入通道單獨用一個 3×3 卷積核去卷通道之間互不干擾第二步是逐點卷積用一個 1×1 卷積把上一步各通道的結果按位置重新混合。對比一下標準卷積標準 3×3 卷積同時完成“空間聚合”和“通道混合”兩步攪在一起計算量是毫不留情的 C_in×C_out×9。深度可分離卷積把兩者拆開先做空間聚合每個通道獨立再做通道混合1×1 卷積計算量變成 C_in×9 C_in×C_out。這一步拆解能省多少我下一節(jié)給具體數(shù)字。這里先說哲學層面標準卷積逼著網絡在同一個操作里同時考慮“周圍是什么”和“各通道怎么組合”這兩個任務其實是可以被解耦的。深度可分離卷積的假設就是——先用 3×3 只提取空間結構再用 1×1 自由融合通道信息效果不會比同步做差多少但計算量小了一個數(shù)量級。2.3 兩步走也能只走通道SE 模塊的啟發(fā)SENet 提出的 SESqueeze-and-Excitation模塊表面上看和卷積無關實際上也在用兩步走哲學第一步是全局平均池化把每個通道的空間信息壓縮成一個標量第二步是兩個 1×1 卷積實現(xiàn)上通常用全連接先降維再升維對每個通道算出一個權重。最后把權重乘回原特征圖。這一步“壓縮-激勵”的過程本質上就是對通道維度做一次“注意力重標定”——先全局看一眼每個通道整體有多重要再用 1×1 做非線性變換重新分配通道權重。它和瓶頸結構有個共同點中間都故意插入了一個維度瓶頸降維再升維目的是讓網絡在低維空間學會更高效的通道關系順帶減少計算量。SE 模塊的流行告訴我一件事1×1 卷積不只是配合 3×3 用的“計算量調節(jié)器”它本身就是一種通道維度的建模工具。只要你能把問題拆成“空間上怎么辦”和“通道上怎么辦”兩步這兩步走的思想就可以在不同層級上反復套用。3. 算一筆賬“兩步走”到底省在哪數(shù)字說了算3.1 誰在消耗算力公式先擺出來標準卷積的 FLOPs乘加次數(shù)通常約等于實際計算量的一半可以寫成輸出尺寸 H×W×C_out 再接上遍歷整個特征圖的所有位置FLOPs H × W × C_out × C_in × k × k這個公式里最值得注意的變量是 C_in × k×k輸出通道越多、卷積核越大計算量呈平方級上漲。這也是為什么 7×7 卷積極度昂貴、深網絡里普遍用 3×3 的原因。為了對比我取一個非常常見的中間層配置輸入特征圖 28×28×256輸出也是 28×28×256。在這個配置上分別算四種做法的 FLOPs單層 7×7 卷積784 × 65536 × 49 ≈ 25.2 億三個串聯(lián) 3×3 卷積每層通道不變3 × 784 × 65536 × 9 ≈ 13.9 億瓶頸結構1×1 降到 640.13 億 3×3 64 通道0.29 億 1×1 升回 2560.13 億≈ 0.55 億深度可分離卷積3×3 深度卷積0.018 億 1×1 逐點卷積0.51 億≈ 0.53 億3.2 對比表格感受一下數(shù)量級差異結構方案FLOPs28×28×256→28×28×256參數(shù)量核心特點單層 7×7約 25.2 億3,211,264感受野大但計算量驚人三個 3×3約 13.9 億1,767,168等效感受野 7×7省 45%瓶頸結構降到64約 0.55 億69,632計算量降到單層 7×7 的約 2%深度可分離約 0.53 億67,840空間與通道解耦極輕量看到瓶頸結構和深度可分離卷積的數(shù)字你可能和我第一次算出來時一樣震驚同樣做一次 28×28×256 的特征變換兩步走的方案消耗的計算量只有直接大卷積核的百分之二左右。3.3 省的是什么不省的是什么這里有一個容易混淆的點。FLOPs 降下去了參數(shù)量也降下去了但“省”的方式不同。瓶頸結構省的大頭是 3×3 卷積作用在高通道數(shù)上的開銷深度可分離卷積省的大頭是 3×3 卷積同時對通道做混合的冗余計算。但要注意FLOPs 低不代表實際推理一定快。在 GPU 上小卷積核的并行度可能受內存帶寬限制在 CPU 或移動端則友好得多。所以動手選方案時一定要結合自己的硬件環(huán)境做實測不能只看 FLOPs 紙面數(shù)字。這一點我在第五節(jié)再展開。4. 用代碼把“兩步走”寫出來直接能落地的模塊4.1 標準瓶頸模塊的實現(xiàn)先說最常用的瓶頸結構。以下 PyTorch 代碼我做過多次微調后固定下來的一版帶殘差連接和可選下采樣import torch import torch.nn as nn class Bottleneck(nn.Module): def __init__(self, in_channels, out_channels, stride1, reduction4): super().__init__() hidden out_channels // reduction self.conv1 nn.Conv2d(in_channels, hidden, 1, biasFalse) self.bn1 nn.BatchNorm2d(hidden) self.conv2 nn.Conv2d(hidden, hidden, 3, stridestride, padding1, biasFalse) self.bn2 nn.BatchNorm2d(hidden) self.conv3 nn.Conv2d(hidden, out_channels, 1, biasFalse) self.bn3 nn.BatchNorm2d(out_channels) self.relu nn.ReLU(inplaceTrue) self.downsample None if stride ! 1 or in_channels ! out_channels: self.downsample nn.Sequential( nn.Conv2d(in_channels, out_channels, 1, stridestride, biasFalse), nn.BatchNorm2d(out_channels), ) def forward(self, x): identity x out self.relu(self.bn1(self.conv1(x))) out self.relu(self.bn2(self.conv2(out))) out self.bn3(self.conv3(out)) if self.downsample is not None: identity self.downsample(identity) out identity return self.relu(out)幾個值得注意的細節(jié)biasFalse卷積后面緊跟 BatchNormbias 會被 BN 抵消掉留著反而浪費參數(shù)。reduction4隱藏通道數(shù) 輸出通道數(shù) / 4這是最常用的壓縮比例。后面我會講什么時候該調它。downsample輸入輸出通道不一致或 stride2 時需要對殘差路徑做投影這里用 1×1 卷積完成其實就是一次“1×1 步走”來對齊維度。4.2 深度可分離卷積模塊的實現(xiàn)下面這段代碼實現(xiàn)的是深度卷積 逐點卷積的完整結構class DepthwiseSeparableConv(nn.Module): def __init__(self, in_channels, out_channels, kernel_size3, stride1): super().__init__() self.depthwise nn.Conv2d( in_channels, in_channels, kernel_size, stride, paddingkernel_size // 2, groupsin_channels, biasFalse ) self.bn1 nn.BatchNorm2d(in_channels) self.pointwise nn.Conv2d(in_channels, out_channels, 1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) self.relu nn.ReLU(inplaceTrue) def forward(self, x): x self.relu(self.bn1(self.depthwise(x))) x self.relu(self.bn2(self.pointwise(x))) return x這里最關鍵的是 depthwise 卷積里的 groupsin_channels。這個參數(shù)讓每個輸入通道只被自己的卷積核卷積完全不會跨通道混合。很多新手在這里習慣性不寫 groups以為自己在做深度卷積實際上 channels 之間悄悄混在一起了和標準卷積區(qū)別只在參數(shù)少了點計算量的節(jié)省就完全沒有了。我習慣把深度可分離卷積層寫成“一個深度卷積 BN ReLU 一個逐點卷積 BN ReLU”激活函數(shù)放在每個步驟之后而不是只在最后放一個。實測這樣訓練更容易收斂精度也略高。4.3 選擇瓶頸還是深度可分離我的經驗如果是用于 CPU/移動端推理深度可分離卷積通常是更穩(wěn)的選擇因為它每個操作都很簡單對低并行度的硬件更友好。如果在 GPU 上做大規(guī)模訓練瓶頸結構往往效果更穩(wěn)定精度更好調。很多輕量級網絡比如 MobileNet 系列的變體在 GPU 上實測并沒有想象中那么快核心原因就是深度卷積在 GPU 上很難把算力吃滿。如果你剛起步我建議先在同一個數(shù)據(jù)集上同時實現(xiàn)兩種模塊對比參數(shù)量和精度。直接在現(xiàn)有網絡里把標準卷積替換成其中任意一種觀察變化這比看十篇論文都有用。4.4 順帶附一個 SE 模塊實現(xiàn)SE 模塊的實現(xiàn)也很短幾乎必背class SqueezeExcitation(nn.Module): def __init__(self, channels, reduction16): super().__init__() self.squeeze nn.AdaptiveAvgPool2d(1) self.fc nn.Sequential( nn.Linear(channels, channels // reduction, biasFalse), nn.ReLU(inplaceTrue), nn.Linear(channels // reduction, channels, biasFalse), nn.Sigmoid(), ) def forward(self, x): b, c, _, _ x.size() y self.squeeze(x).view(b, c) y self.fc(y).view(b, c, 1, 1) return x * y它和卷積模塊配套使用時一般放在殘差分支的最后、相加之前對通道做加權。如果你把 SE 想象成一個只走“通道維”的兩步走模塊就很容易理解它和卷積模塊為什么能無縫配合。5. 常見誤區(qū)與調參心得踩過的坑都在這5.1 1×1卷積不只是“降維工具”這是我見過最多的誤解。1×1 卷積確實常被用來降維但它的本質是通道混合。如果你只把它當前往計算量削減器你會忽略它在特征融合中的巨大作用。比如在深淺層特征融合的很多結構里1×1 卷積就是用來把不同來源的通道對齊并混合而未必降維。帶著這個認知去看網絡結構你會少很多困惑。5.2 深度可分離卷積不是萬能靈藥FLOPs 降了 80% 不意味著推理時間降 80%。至少有兩個隱藏成本深度卷積內部必須逐通道串行/并行分通道做GPU 利用率偏低逐點卷積雖然計算量小但要讀取整張中間特征圖內存訪問壓力大。所以如果你是 GPU 服務器部署深度可分離卷積的優(yōu)勢會被削弱很多。我自己踩過的坑是把一個標準卷積網改成深度可分離后GPU 訓練速度反而慢了 15%而參數(shù)量確實降了很多。最終方案是部分層保持標準卷積只在計算熱點層使用深度可分離。5.3 BN 和 ReLU 的位置不能亂放瓶頸模塊里我強調過一個順序conv1 → BN → ReLU → conv2 → BN → ReLU → conv3 → BN → 與恒等分支相加 → ReLU。最后一個 ReLU 放在相加之后是因為殘差分支的恒等映射需要“干凈”地傳遞到下一層不能在相加前就被激活截斷。很多人圖省事把 ReLU 全堆在卷積后面結果殘差梯度流被嚴重阻礙訓練變慢甚至不收斂。如果你在訓練時發(fā)現(xiàn) loss 下不去優(yōu)先查一下激活函數(shù)相對殘差連接的位置。5.4 降維比例reduction不是固定 1/4瓶頸結構里 reduction4 是最常見選擇但絕不是唯一選擇。我曾經在某個分類任務上把 reduction 從 4 調到 8參數(shù)量繼續(xù)下降精度掉了 0.3 個百分點調到 2精度漲了一點參數(shù)量卻翻了一倍。這個比例本質上是“信息瓶頸”的寬度調得越小中間層能表達的信息越少但計算量也越小。我的經驗是如果任務比較簡單或者精度壓力大用 2 或 4如果就是要極輕量模型可以直接上 8 甚至 16然后配合更寬的網絡整體調參。5.5 瓶頸結構的瓶頸到底在哪一層設計瓶頸模塊時最容易忽略的是中間 3×3 卷積的輸入通道已經被壓縮到 hidden因此它的感受野操作只能建立在壓縮后的特征上。如果壓縮得太狠空間信息的豐富度會被嚴重削弱。這也是為什么 MobileNet 那類結構寧可逐步降低通道也不做一次性極窄瓶頸的原因。你在設計網絡時每一層的壓縮比例要跟“這個位置到底需要多豐富的空間特征”匹配而不是統(tǒng)一套一個參數(shù)。5.6 初始化對兩步走結構的影響不可忽略BN 后接 1×1 卷積時輸出方差很大程度上由后續(xù)權重尺度決定。我遇到過深度可分離網絡在訓練初期 loss 掉不下去的情況排查后發(fā)現(xiàn)是深度卷積那層初始化方差過大導致經過 BN 后輸出震蕩。后來在所有深度卷積層把 weight 初始化標準差調小到原來的 0.5 倍問題就消失了。如果你復現(xiàn)輕量網絡發(fā)現(xiàn)收斂異常先檢查初始化分布再檢查學習率不要一上來就怪數(shù)據(jù)集。小結與個人體會把 1×1 和 3×3 放在一起看最深的體會是現(xiàn)代卷積網絡在設計上一直在做“拆解”和“解耦”——把空間特征提取和通道信息融合拆成兩步在各自維度上做更高效的操作。瓶頸結構是先降再提后升深度可分離卷積是先空間后通道SE 模塊則是純通道維的兩步走。套路不同哲學相通。我自己在一次輕量模型改造里用瓶頸替換了三個標準卷積層參數(shù)量從 9.6M 掉到 2.1M精度只損失了 0.4 個百分點推理速度提升了接近一倍。踩過的坑也不少ReLU 位置、groups 設置、初始化尺度都是靠打印中間張量和逐層對比才定位出來的。如果你也想動手改一版自己的輕量網絡我建議從一個小模型開始把標準卷積替換成瓶頸或深度可分離用同樣的訓練配置跑一個對比實驗。不用追求一步到位先跑通一個最小的“兩步走”再逐步鋪開這比啃任何結構分析的論文都更實在。