本节摘要:VGG 用 1.38 亿参数换深度,同年夺冠的 GoogLeNet 只用了 680 万——秘密是一个叫 Inception 的并行分拣模块:同一层里 1×1、3×3、5×5 的核与池化各开一条支线,各自看清自己擅长的尺度,再把结果拼回一起;大核支线入口先用 1×1 降维压成本。本节算清降维支线的省料账,并复刻一个能跑的 Inception 模块。
工程圈聊网络时常会冒出一句"这里开个 Inception",意思是:别把所有通道塞进同一种核,开几条并行支线,让不同尺度的模式各走各的路。这个说法的出处就是 GoogLeNet 的 Inception 模块——名字玩的是电影梗,机制却极其严肃:物体的尺寸差异是图像识别的固有难题(同一辆车近看占满画面、远看只有巴掌大),单一大小的感受野总有力不从心的尺度,那就一层里同时摆上多种视野。
阅读完本节,你应当能够:
模块内部的分工是:1×1 支线管逐像素的通道组合;3×3 支线管中小局部;5×5 支线管更大的上下文;第四条支线先 3×3 最大池化再做 1×1 投影,负责"忽略纹理、只看整体"。四条支线的输出在通道维拼接,一张图同时被四种尺度审视。 naive 版本有个致命账目:5×5 的核在几百通道上直接开卷,参数与算力都会爆炸。解法是给 3×3 与 5×5 支线的入口各装一个 1×1 降维——先把通道压小,再做贵的大核卷积。
import torch import torch.nn as nn # 同样是 256 通道进、256 通道出的 3×3 工序,两种开法 naive = nn.Conv2d(256, 256, kernel_size=3, padding=1) bottleneck = nn.Sequential( nn.Conv2d(256, 64, kernel_size=1), nn.ReLU(), # 先降维到 64 nn.Conv2d(64, 256, kernel_size=3, padding=1), # 再做 3×3 ) n_naive = sum(p.numel() for p in naive.parameters()) n_bottle = sum(p.numel() for p in bottleneck.parameters()) print("朴素 3×3:", f"{n_naive:,}") # 589,824 print("降维 1×1 加 3×3:", f"{n_bottle:,}") # 163,840 print("省料倍数:", round(n_naive / n_bottle, 1)) # 3.6
同样一道工序,58.9 万参数压到 16.4 万,省料 3.6 倍,而表达能力几乎不损——压掉的通道里大部分是冗余相关。GoogLeNet 整座网络到处是这类"入口先减员"的支线,22 层深度只背了 680 万参数,约为 VGG-16 的二十分之一,精度却旗鼓相当。
class Inception(nn.Module): def __init__(self, cin, c1, c3red, c3, c5red, c5, pool): super().__init__() self.b1 = nn.Sequential(nn.Conv2d(cin, c1, 1), nn.ReLU()) self.b2 = nn.Sequential(nn.Conv2d(cin, c3red, 1), nn.ReLU(), nn.Conv2d(c3red, c3, 3, padding=1), nn.ReLU()) self.b3 = nn.Sequential(nn.Conv2d(cin, c5red, 1), nn.ReLU(), nn.Conv2d(c5red, c5, 5, padding=2), nn.ReLU()) self.b4 = nn.Sequential(nn.MaxPool2d(3, stride=1, padding=1), nn.Conv2d(cin, pool, 1)) def forward(self, x): return torch.cat([self.b1(x), self.b2(x), self.b3(x), self.b4(x)], dim=1) inc = Inception(192, 64, 96, 128, 16, 32, 32) # 论文里第一个模块的通道配置 x = torch.randn(1, 192, 28, 28) out = inc(x) print("输出通道:", out.shape[1]) # 64+128+32+32 = 256 print("输出尺寸:", tuple(out.shape[2:])) # (28, 28),same 卷积保尺寸
两条验证要点:输出通道是四条支线之和,配置模块时各支线通道就是你的"预算分配表";四条支线全用填充卷积,空间尺寸不变,模块才能像积木一样任意堆叠。GoogLeNet 的另外两处精修也值得一记:分类头用全局平均池化(2.4 节的伏笔在此兑现,头部参数几乎归零);训练时在中途挂两个辅助分类头,给深层注入额外梯度——这个手法在残差连接普及后退役,但"给深网络搭梯度梯子"的思路贯穿至今。
⚠️ 常见坑:自己配 Inception 时把某条支线通道开得过大,模块参数立刻失控。先看输入通道:降维目标的合理量级通常是输入的四分之一上下——预算先压入口,再谈出口。
同样面对"参数与深度"的矛盾,VGG 押注堆深、GoogLeNet 押注并行,而真正把深度上限推过百层的,是下一节的残差跳线。