☰
卷积尺度全解析:感受野、特征尺度与采样尺度工程实践
2026/9/30 1:37:05 网站建设 项目流程

把卷积核从 3×3 换成 7×7,参数量涨了 5.4 倍,算力跟着涨,模型却未必看得更"远"。

这是我做遥感影像小目标检测时被打脸的第一次。当时想当然认为大核等于大视野等于能抓大目标,结果换完核之后,大目标召回没涨多少,小目标反而掉了两个点。后来把每一层的感受野手算了一遍,又跑了一轮缩放扫描,才发现问题根本不在核大小上,而在我一直把卷积的尺度特性当成一个东西在谈。

卷积的尺度特性拆开看,至少有三个互不相同、又互相纠缠的维度:感受野尺度说的是"一个输出像素能看见多大范围";特征尺度说的是"同一个物体在网络里越往后越小";采样尺度说的是"stride 和池化把空间分辨率换成了多少倍的下采样"。这三件事里,只有第一件是核尺寸直接决定的,后面两件得靠网络结构、训练策略和推理时的输入分辨率一起凑。

这篇内容写给三类人:手上正在调检测、分割、视频理解模型,发现换个输入分辨率结果就崩的工程师;看论文时被 dilation rate、output stride、scale jittering 这些词绕晕的同学;以及想把 3D 卷积、图卷积、球面卷积串起来理解的人。公式、手算表、踩坑记录和一套能复现的体检流程我都摆出来,可以直接抄作业。

1. 卷积的"尺度"其实有三层意思,混着谈必然吵起来

这个行业里关于卷积尺度的争论,十有八九是两个人说的不是同一个尺度。有人说"加深网络就能扩大感受野",另一个人说"我加了十层也没用",双方都没错,因为后者说的是有效感受野,前者说的是理论感受野。所以先把三个维度拆干净,后面所有讨论才有共同语言。

1.1 感受野尺度:一个输出像素到底能看见多大范围

感受野是输入空间里的概念,指某个输出像素的值,在原始输入图上受哪些像素影响。注意它跟特征图的尺寸不是一回事:一个 7×1 的特征图可以有 100×100 的感受野,一个 100×100 的特征图也可以只有 3×3 的感受野。前者说明信息高度浓缩,后者说明特征图看起来很"大"但每个点看到的上下文很窄。

这个区别在多尺度任务里非常关键。做小目标检测时你会希望浅层特征既保留高分辨率(采样尺度小),又有足够大的感受野(感受野尺度大),这两个诉求天然打架——因为传统的扩大感受野手段就是下采样。这也是空洞卷积能火起来的根本原因:它把这两件事解耦了。

1.2 特征尺度:同一个人脸在浅层是 40 像素,在深层是 3 像素

特征尺度是特征空间里的概念。一张 1024×1024 的图,里面有个 64×64 的人脸,经过 stride 8 的骨干网络之后,这个人在特征图上只有 8×8 大小。如果网络设计时锚框(anchor)最小只覆盖到 32×32 特征尺度,那这张脸就落在检测器的盲区里。

工程上所有的多尺度设计,本质都是在不同特征尺度上各自布一套预测头:FPN 之所以要做 P3 到 P7 五层,就是因为单层的特征尺度覆盖不了现实场景里 10 倍以上的目标尺寸跨度。你以为是在堆结构,其实是在给尺度打补丁。

1.3 采样尺度:stride 和池化决定了尺度的"汇率"

采样尺度就是下采样倍数,也叫 output stride。它决定了特征图上的 1 像素对应原图的多少像素。这个数字是尺度的"汇率",一旦定下来,网络内部所有的空间量(锚框、偏移量、坐标回归的目标值)都要按这个汇率换算。

我给团队新人讲这块时喜欢用地图类比:output stride 就是地图比例尺。你在 1:10000 的图上量出 3 厘米,换成实地就是 300 米。改成 1:50000 之后,同样量 3 厘米就变成 1500 米了。锚框尺度忘了跟着改,等于用旧比例尺的尺子量新地图,结果必然是系统性偏差,而不是随机噪声——这个特征很好辨认,后面第 6 章会详细讲怎么排查。

提示:看到一个多尺度相关的 bug,先问自己"是感受野不够、特征尺度没覆盖、还是采样尺度换算错了"。这三个问题对应完全不同的修法,混着改只会越改越乱。

2. 感受野的账要算清楚:3×3 堆叠为什么能顶掉 7×7

网上的文章经常给一个结论"两层 3×3 等效一个 5×5",但从不说这个"等效"是在什么条件下成立的。严格说,只有感受野范围等效,表达能力、非线性次数、参数分布都不一样。把账算细一点,选型时心里才有底。

2.1 递推公式与一张手算表

单个维度的感受野递推式非常简洁:

RF_l = RF_(l-1) + (k_l - 1) × S_(l-1) S_l = S_(l-1) × s_l 初始条件:RF_0 = 1, S_0 = 1

其中 k 是核尺寸,s 是这一层的 stride,S 是到这一层为止的累计下采样倍数。关键点在S_(l-1)这一项:感受野的增长被前面所有层的下采样倍数放大了。这就是为什么在 stride 4 的特征图上加一个 3×3,感受野能涨 8 个像素,而在 stride 1 上同样加一个 3×3,只涨 2 个像素。

拿一个 VGG 风格的小骨干手算一遍,比看十遍公式管用:

层核尺寸stride累计下采样 S感受野 RF
conv13113
conv23115
pool12226
conv331210
conv431214
pool222416
conv531424
conv631432

看完这张表就能理解两个常见现象。第一,网络后半段的感受野增长比前半段快得多,同样一个 3×3 在 pool2 之后贡献 8 个像素,在 pool1 之前只贡献 2 个。第二,如果骨干网络有六层卷积两个池化,最终感受野只有 32,而输入图是 224,那意味着网络在最后一层也没有看到全局——这就是为什么分类网络尾巴上要接全局平均池化,它是唯一能把感受野一次性撑满整个图的操作。

再算参数量的账。三层 3×3 的感受野是 7(3→5→7),参数量是3 × 3 × 3 × C² = 27C²(忽略偏置);一个 7×7 的感受野也是 7,参数量是49C²。前者只有后者的 55%,但中间多了两次非线性激活。这两次非线性带来的表达能力提升,往往比大核带来的视野提升更值钱——除非你的任务确实需要提取低频、大范围的纹理模式,比如风格迁移或者某些遥感场景。

2.2 空洞卷积:把核摊开,参数不涨

空洞卷积(也叫扩张卷积)的思路是:核里插空格,让采样点散开。有效核尺寸和感受野的算法:

k_eff = (k - 1) × r + 1 RF_l = RF_(l-1) + (k_l - 1) × r_l × S_(l-1)

3×3 的核,r=2 时有效尺寸是 5,r=3 时是 7,参数量始终是 9 个权重,一点没涨。这就是它在语义分割里几乎是标配的原因:在不损失分辨率的前提下把感受野撑到整张图。

但空洞卷积有个非常隐蔽的坑,叫网格伪影(gridding artifact)。当多个空洞层叠加时,如果 dilation rate 互相之间有公因数、或者排列方式不对,采样点会退化成稀疏的栅格,特征图上出现棋盘状的周期性条纹。有些像素从来没被采样到,有些被反复采样。这个现象在小目标上体现得特别明显:目标恰好落在"没被采样"的位置上,响应就很弱,位置一挪就正常了,你会误以为是数据不均衡。

业界的处理办法是采用混合空洞卷积(HDC)的膨胀率设计,比如[1, 2, 5]这种锯齿状序列,而不是[2, 4, 8]这种等比的。经验规则有两条:一是整组膨胀率里不能有长度超过 1 的公约数([2,4,8]全都能被 2 整除,最糟糕);二是后一层的最大膨胀率不能超过本层的核尺寸覆盖范围,否则层与层之间会出现感受野空洞。这两条规则来自 HDC 那篇论文,我在实际项目里验证过,改完膨胀率之后小目标的召回一般能回升 1 到 3 个点,且不需要重新设计结构。

2.3 理论感受野和有效感受野差着一条街

理论感受野是从公式里算出来的"最大可能范围",但实际训练出来的网络,每个像素对输出的影响力不是均匀分布的。用梯度反传可视化会发现,影响力近似一个高斯分布:中心像素权重极高,边缘像素权重趋近于零。

这意味着三层 3×3 算出来的理论感受野是 7,但真正有显著贡献的可能只有中心那 5×5 甚至 3×3。更反直觉的是,有效感受野的大小大致按层数的平方根增长,而不是线性增长。也就是说,你把层数翻四倍,有效感受野才翻一倍。

我在做缺陷检测时吃过这个亏:按理论感受野算出 400 像素足够覆盖一个缺陷,实际标注框是 380 像素,结果边缘区域死活检不出来。后来把上采样后的特征图叠了一层 3×3,把有效感受野的中心区域往边缘推了一点,召回就上来了。所以我的建议是:理论感受野算出来之后,打个七折当有效值用,特别敏感的任务打六折。

3. 平移等变是天赋,尺度等变得自己挣

卷积之所以统治了视觉任务,核心原因是一条漂亮的数学性质:平移等变性。但同一套推导里,尺度等变性是不成立的——这是很多人直觉上过不去的一道坎,也是多尺度问题的理论根源。

3.1 卷积为什么天生平移等变

严格定义一下。设T_δ是把图像在空间上平移 δ 个像素的操作,* 表示卷积,那么对任意输入 x 和卷积核 w:

(T_δ x) * w = T_δ (x * w)

翻译成人话:先把图片往右挪 5 像素再做卷积,和先做卷积再把结果往右挪 5 像素,结果完全一样。这个性质来自卷积的权重共享——同一个核在图像每个位置用同一套权重,位置本身不携带任何特殊含义。

从频域看更直观。卷积定理告诉我们,空间域的卷积等于频域的乘法:F(x * w) = F(x) · F(w)。滤波器在频域上就是一个固定的通带和阻带形状。这里埋着下一节的关键:滤波器对"频率"的响应是固定的,不随输入变化的。

3.2 把图放大两倍,卷积响应为什么塌了

现在把尺度变换引进来。设S_s是把图像放大 s 倍的操作。注意这里:

S_s(x) * w ≠ S_s(x * w)

为什么会不等?因为放大在空间域对应的是压缩在频域。图片放大 2 倍,它原本占据的频段整体向低频方向平移了一半。而卷积核 w 的通带位置是固定的,没有跟着移动。原来正好落在通带里的那些纹理模式,放大之后就掉到通带外面去了,响应强度自然崩掉。

我做过一个特别直观的小实验:拿一个在 ImageNet 上预训练的 ResNet,输入一张 224 的猫,取最后卷积层的特征图,记录激活值最大的位置和强度。然后把这张猫裁出来单独放大到 224 再喂进去,同一个卷积核的响应值掉了将近一半,而且响应最强的位置偏移了几十个像素。同一个物体,同一个网络,只是尺度变了,行为完全不同。

这就是 CNN 缺乏尺度不变性的本质。它并不是"没学到尺度变化",而是每次尺度变化都要靠数据重新教一遍。训练集里没出现过的尺度组合,网络就是不会。

3.3 尺度空间理论:高斯金字塔和 DoG 的经典解法

在深度学习之前,尺度空间理论已经把这件事想得很透了。核心思想是:不要把尺度当成要"克服"的干扰,而是当成一个显式的维度引入进来,构建一个"图像的尺度族"。

具体做法是把图像和一串逐步增大的高斯核做卷积:

L(x, y, σ) = G(x, y, σ) * I(x, y)

σ 就是尺度参数。σ 越大,图像越模糊,细节越少,剩下的是低频结构。这一串不同 σ 的结果叠起来,就是一个三维的尺度空间。这个构造不是随便选的,它满足几个很硬的公理:线性、平移不变、旋转不变、尺度不变,而高斯核是唯一同时满足这些条件的核。这不是工程偏好,是数学结论。

真正漂亮的是归一化高斯拉普拉斯:真正的尺度不变检测要用σ²∇²G,那个 σ² 因子是为了抵消微分带来的幅值衰减。实际操作里没人真的去算 LoG,因为差分近似就够了:

G(kσ) - G(σ) ≈ (k - 1) σ² ∇²G

这就是 DoG,高斯拉普拉斯的近似,比值 (k-1) 是常数,所以 DoG 的极值位置和归一化 LoG 几乎一致。SIFT 就是靠这个建立金字塔,每个 octave 里放三层,k 取 2 的三分之一次方,然后找尺度空间和空间位置上的三维极值点。一个特征点如果同时在多个尺度上都很显著,它就会在不同尺度上被重复检测出来——这就是尺度不变性的实现方式。

这套东西对今天的深度学习依然有启发:与其指望网络自己学会尺度不变,不如把尺度显式地喂进去。第 4 章讲的四种工程打法,本质上都是这个思路的深度学习版本。

4. 工程上补尺度的四种打法,各自适合什么场景

补尺度这件事没有银弹,四种主流打法各有明确的适用边界和代价。选错方向的代价不只是精度,还有推理延迟和工程复杂度。

4.1 图像金字塔与测试时多尺度

最朴素也最可靠的做法:把输入图缩放到若干尺度,每个尺度独立跑一遍网络,最后把结果融合。检测任务里常用{0.75, 1.0, 1.25, 1.5}这组,再叠加水平翻转,也就是常说的多尺度测试增强(TTA)。

它的优点是不需要重新训练,对任何已训练好的模型都直接生效,而且对小目标的提升非常明显:小目标在放大后的图里变大,落到了检测器熟悉的尺度区间。缺点是推理成本按尺度数量线性增长,五个尺度就是五倍延迟,再叠翻转就是十倍。而且融合逻辑有讲究:分类分支一般取平均,回归框要对齐到原图坐标再做 NMS。如果各个尺度之间置信度没有做归一化,直接取最大值融合,容易出现同一个物体在多个尺度上都被检出、NMS 又因为框差异太大而没能合并的情况。

适用场景:离线批处理、精度优先的竞赛或者标注补全任务。实时线上服务基本用不起。

4.2 特征金字塔与多分支空洞卷积

这是把多尺度做进网络结构里的主流方案,代表是 FPN 和 ASPP。FPN 的思路是自顶向下加横向连接:深层特征语义强但分辨率低,浅层特征分辨率高但语义弱,把深层上采样后和浅层相加,每一层都能同时拿到两种信息,然后每层各自出预测头。

ASPP 走的是另一条路:在同一个特征图上并联几组不同膨胀率的空洞卷积,加上一个全局平均池化分支,把它们拼接起来。这样单层特征就能同时覆盖多个感受野尺度,不需要靠结构层级来分散。

这里有个很多人忽略的细节:ASPP 的膨胀率必须跟着 output stride 一起改。经典配置是 output stride 16 时用[6, 12, 18],如果把 output stride 降到 8 以获得更精细的分割边界,膨胀率要相应翻倍成[12, 24, 36],否则感受野相对缩小了一半,全局上下文就丢了。这个坑我在第一次做高分辨率分割时踩得很结实,改完 output stride 之后模型收敛完全正常,但大面积的天空、路面区域开始出现碎裂,查了两天才意识到是膨胀率没同步。

方案多尺度来源参数量代价训练复杂度典型场景
图像金字塔 + TTA输入侧0无需训练离线推理、竞赛
FPN结构层级中中通用检测
ASPP单层多分支低低语义分割
可变形卷积采样位置学习低高(需专门初始化)形变目标、密集预测

4.3 可变形卷积:把采样位置交给网络学

前面的方案里,采样位置都是人工设计的固定规则。可变形卷积的思路是给每个采样点学一个偏移量:原来在 3×3 网格上采 9 个点,现在这 9 个点各自可以在邻域内自由游走。

第二版还加了一个调制标量,每个采样点乘一个 0 到 1 的权重,让网络同时决定"在哪里采"和"这个点重要不重要"。这套机制对非刚性形变特别有效,比如动物姿态变化、衣服褶皱。

用可变形卷积有几个实操经验。第一,学习率不能直接用骨干网络的,偏移量分支一般要调低,我用的是骨干的 0.1 倍,否则训练初期偏移会乱飘,特征图完全对不上。第二,偏移量的初始值必须显式置零,不置零的话初始状态就是随机扰动。第三,推理时偏移量是逐图计算的,所以它对 batch size 敏感度较高,训练时如果 batch 太小,偏移量的梯度噪声会很大。

4.4 多尺度训练与尺度抖动

这是最省事、收益最稳定的一招:训练的时候就把输入尺度随机化,让网络见过各种尺度组合。检测里叫尺度抖动(scale jittering),常用的短边范围是 640 到 800,每个 batch 随机采一个。分类任务里更激进,有的会从 0.35 倍到 1.0 倍之间随机裁切再缩放到固定尺寸。

原理跟第 3 章讲的频率响应一致:网络通过数据见到了"同一个物体落在不同频段"的样本,各个卷积核就学会了在更宽的频段上响应,本质上是在训练过程中隐式地做了一次多核融合。

尺度抖动有一个坑必须提前说:BatchNorm 的统计量会跟着尺度分布一起漂。训练时尺度在 640 到 800 之间跳,BN 学到的是这一段的混合统计量;推理时如果你固定用 800,统计量其实有轻微不匹配;如果你用 1200 去推理,分布差异更大。缓解办法一是把 BN 换成 GroupNorm(代价是精度通常略降),二是在模型表达能力足够时直接冻结骨干的 BN,三是尺度抖动的范围别开太大,抖动幅度超过一倍通常会造成训练不稳定。

5. 卷积变体里"尺度"的变形记

上面讲的都是二维图像卷积。当你换到视频、点云、球面数据、图结构上,尺度的定义会跟着变,很多在图像上成立的直觉会失效。

5.1 3D 卷积:时间维是一把新尺子

把 2D 卷积核在时间维度上扩展,就得到 3D 卷积,核形状变成k_t × k_h × k_w。感受野公式一字不改,只是每个维度各算一份:

RF_t = RF_t + (k_t - 1) × S_t RF_h = RF_h + (k_h - 1) × S_h × r_h

关键的设计问题是:时间核该开多大?直接用 3×3×3 的问题在于时间维和空间维的尺度语义完全不同。空间上相邻的 3 个像素是强相关的,时间上相邻的 3 帧几乎一模一样,信息冗余极高,参数花在这上面很不划算。

I3D 那篇工作给的方案很优雅,叫"膨胀初始化"(inflation):把预训练好的 2D 卷积核在时间维度上复制k_t份,再整体除以k_t。除这一步是为了保持输出激活的量级不变——如果直接复制不除,输出响应会放大k_t倍,接在后面的 BN 层统计量直接失衡,训练一开始就会炸。这个技巧让我第一次做视频任务时省掉了从零训练的成本,直接用图像预训练权重初始化就收敛了。

另一个时间维特有的问题是时间尺度的采样:如果视频抽帧太快,动作在两个相邻帧之间几乎没有位移,时间感受野在大动作上就不够用;抽帧太慢,快速动作又会产生混叠。这个折中跟图像里降采样之前的抗混叠滤波是同一类问题,我的经验是动作识别抽帧步长控制在 4 到 8 之间比较稳,检测和跟踪则要更密。

5.2 深度可分离与转置卷积:分辨率和通道的重新分配

深度可分离卷积把标准卷积拆成两步:先对每个通道单独做空间卷积(depthwise),再用 1×1 卷积做通道混合(pointwise)。参数量的比值是:

(k²·C_in + C_in·C_out) / (k²·C_in·C_out) = 1/C_out + 1/k²

3×3 的核在 C_out 很大时,参数量大约压到原来的 1/9。它本身不改变感受野尺度,但因为它把空间卷积和通道混合解耦了,你可以放心地把 k 开大一点来扩大感受野,参数压力不大。这在移动端模型里很实用。

顺带说一句,1×1 卷积在尺度视角下是一个非常特殊的东西:它的感受野是 1×1,也就是完全没有空间上下文,只在通道维度做线性组合。理解这一点之后,很多结构设计就说得通了:ResNet 的瓶颈结构先用 1×1 降维,是为了在通道上省钱,然后把省下来的算力交给 3×3 做空间建模。全局平均池化则相反,它是把感受野一次性撑到整张图,代价是所有空间位置信息全部丢失。

转置卷积负责把特征图放大回来,输出尺寸公式:

out = (in - 1) × s - 2p + k + output_padding

它有先天缺陷:当 stride 大于 1 且核尺寸不能被 stride 整除时,不同的输出位置被覆盖的次数不一样,多余的次数就表现为棋盘格状的高频纹理。这个问题的本质还是尺度错配——转置卷积想用一套固定权重同时完成"放大"和"加权求和"两件事,而这两件事在尺度上是矛盾的。稳妥的做法是先用双线性插值把图放大到目标尺寸,再接一个 3×3 卷积做细化,把放大和滤波拆开。

5.3 图卷积、球面卷积:尺度不再是欧氏距离

到了非欧几里得数据上,"尺度"这个概念得重新定义,这是我觉得最值得琢磨的一块。

图卷积里的尺度有两个维度。一是邻域跳数:一阶邻居、二阶邻居、k 跳邻居,跳数越多覆盖的节点越多,相当于感受野越大。二是谱域频率:图傅里叶变换把信号投到拉普拉斯矩阵的特征向量上,特征值 λ 小对应低频(相邻节点取值相近的平滑模式),λ 大对应高频。λ=0 对应的特征向量是常向量,那就是图信号里最大尺度的成分。图卷积网络本质上就是一个在谱域上做低通滤波的操作,层数堆多了会过平滑(over-smoothing)——所有节点的表示趋同,这跟图像里堆太多层导致特征图糊成一片是同一个道理。

球面卷积面对的是另一个问题:球面上不存在一个全局的平移定义,你没法把北极附近的卷积核直接搬到赤道。解决思路是限制卷积核为各向同性,只依赖两点之间的测地距离。这样尺度就由一个参数控制——球谐函数的带宽(也就是保留多少阶的球谐系数)。带宽越大,空间分辨率越高,越能表达高频细节。用等面积像素化方案(比如常见的层次化球面分块方法)时,层级每细分一级,分辨率翻倍,尺度呈现完美的二次幂阶梯。

我在做球形全景图像处理时最大的感受是:图像领域里那些靠"堆层+池化"扩大感受野的习惯做法,在球面上要格外小心,因为池化的下采样在球面上会造成不均衡,不同纬度区域的失真程度不一样,靠近极点的区域会被严重挤压。业界的处理方式是改用各向同性的核加上带宽控制,让尺度变化在球面上保持均匀。

5.4 门控卷积、三角卷积:把尺度选择交给数据

这类变体的共同点是把"固定的核"换成"由数据决定权重的核"。

门控卷积的典型形式是:

y = (W_f x) ⊙ σ(W_g x)

其中 ⊙ 是逐元素乘,σ 是 sigmoid。它最初是为了解决图像修复里不规则孔洞的问题:普通卷积会把孔洞区域当成有效像素一起加权求和,导致周边的颜色和纹理"渗"进孔洞边缘,修复结果看上去像糊了一层。门控分支让网络自己学出"这个位置该不该算",权重接近 0 的区域就自动被屏蔽了。

这跟尺度有什么关系?关系在于孔洞的尺寸是会变的。小孔洞靠 3×3 的核就能处理好,大孔洞必须靠堆层来扩大有效感受野,门控只解决了"哪些像素有效",解决不了"范围够不够"。所以实际做修复模型时,往往要把门控卷积和空洞卷积配合起来用,前者管有效性,后者管覆盖范围。我在复现这类模型时的经验是,门控分支的初始化不能太激进,如果开局的 sigmoid 输出接近 0.5,前面几层的梯度会被压得比较厉害,收敛明显变慢。

至于"三角卷积"这类命名,不同社区给出的实现并不完全一致,有的指把卷积核的支撑区域裁成三角形,有的指在三角网格上做插值卷积。共通的思想是一致的:放弃矩形规则采样,改用符合数据几何特征的支撑形状。这种做法的收益在几何形状有明显方向性的场景里比较明显,代价是核的索引逻辑变得复杂,并行效率下降。如果你手上的实现跟我描述的不一样,以源码和原论文的定义为准——这类名词的歧义度比想象中大得多,别照着二手博客手撸实现。

6. 多尺度任务里我踩过的四个坑

理论讲完,讲讲真实的翻车现场。这四个坑有一个共同特征:报错信息完全正常,loss 曲线也很漂亮,但精度就是不对,排查起来极其耗时。

6.1 BN 统计量和输入尺度绑死了

第一个项目,分类模型在 224 上训到 78% 的准确率,换到 448 上推理直接掉到 62%。我当时以为是模型没有尺度泛化能力,准备上多尺度训练。后来做了个对照实验,把输入缩放到 448 之后,网络的中间层激活值分布和训练时差了将近一个标准差。

原因就在 BatchNorm。BN 在训练时统计的是"当前尺度下,经过每一层卷积和池化之后的激活均值方差"。你改输入尺度,特征图的尺寸跟着变,而池化和卷积的组合对空间频率的响应不是尺度不变的,所以每一层的激活分布都会偏移。BN 用的是训练时冻结下来的统计量,跟推理时的真实分布对不上,输出就偏了。

解决办法我试过三种:一是在推理时用更大的 batch 重新估计 BN 统计量(也就是让 BN 跑训练模式但不动权重),简单有效,代价是需要一批无标注样本过一遍网络;二是训练时就做尺度抖动,让 BN 见到更大的尺度范围;三是换掉 BN。实测第一种最省事,在无法重新训练的场景下几乎是唯一选择。

6.2 改了输入分辨率,anchor 和 stride 忘了改

第二个坑更隐蔽。我们把检测模型的输入从 800×1333 提到 1024×1024 以提升小目标召回,mAP 反而降了。查了一整天,最后发现是输入尺度变了但锚框尺度没变。

原来的锚框是按 800 的输入标定的,最小组 32×32 对应 stride 8 的特征层。输入提到 1024 之后,同一个物理目标在特征图上变大了 1.28 倍,原来匹配 32×32 锚框的目标,现在应该匹配 41×41 的锚框。锚框没跟着调,正样本匹配率就下降了,尤其是中等尺寸目标——它们从"正好匹配"变成了"刚好错开",被分到了负样本或者被忽略。

这类问题的诊断方法很简单:统计一下每个锚框组分配到的正样本数量。正常情况下,各个尺度的锚框分配应该是相对平衡的。如果某一组的正样本数接近零,而相邻组暴涨,那基本可以确定是尺度匹配出了问题。改法有两种:按比例缩放所有锚框尺寸,或者保持锚框不变、改输入时同步缩放标定基准。

6.3 转置卷积的棋盘伪影,本质是尺度错配

分割任务的输出图上出现规律的网格状纹理,一开始我以为是训练不足,加了 epoch 和更强的数据增强都没用。后来把转置卷积的输出单独可视化,才发现是棋盘伪影。

原因就是第 5.2 节讲的那个:stride 2 的转置卷积,核尺寸 4 时,输出位置上被覆盖的次数呈 1-2-1-2 的周期性分布;核尺寸 3 时更糟,会出现明显的 1-1-2-1-1-2 模式。覆盖次数多的位置输出值偏大,覆盖次数少的位置偏小,这就是棋盘。

修复方案按效果排序:用双线性上采样加 3×3 卷积(最稳,我现在的默认选择);或者把核尺寸调到能被 stride 整除,比如 stride 2 配核尺寸 4,覆盖次数变成均匀的 2-2-2;最后还可以在转置卷积后面接一层平滑卷积,但这是治标。

6.4 上采样方式选错,小目标直接消失

这个坑是我在做人脸检测时踩的。FPN 的上采样默认用了最近邻插值,结果小目标召回一直上不去。换成双线性插值之后涨了两个多点。

原因是最近邻插值在放大时直接复制像素,不产生新的中间值,也就不能在频域上做任何平滑。深层特征图里本来就只剩低频信息了,最近邻放大之后,高频位置是空白的,叠加上来的横向连接里的浅层高频信息会被这种"块状"结构干扰。双线性插值的加权平均相当于一次低通滤波,能把块状边界抹平,融合效果更自然。

反过来的场景也有:如果你做的是超分辨率或者需要锐利边缘的任务,双线性反而会糊掉细节,这时候可以考虑亚像素卷积(像素重排),它是把通道维度重排成空间维度,没有任何插值运算,理论上无信息损失。代价是对上采样倍数的灵活性差一些,只能做整数倍。

7. 一套可以照抄的尺度体检流程

前面讲的都是零散的知识点,最后给一套我每次接手新模型都会跑的检查流程。三步,半天之内能做完,能提前发现八成的尺度相关问题。

7.1 第一步:量化感受野,别靠脑补

写一个几十行的感受野计算器,把骨干网络每一层的核尺寸和 stride 喂进去,输出完整的感受野表和下采样倍率。这一步必须在动任何代码之前做完。

def rf_table(layers): """layers: [(name, kernel, stride), ...]""" rf, s = 1, 1 rows = [] for name, k, st in layers: rf = rf + (k - 1) * s s = s * st rows.append((name, k, st, s, rf)) return rows backbone = [ ("stem_conv", 3, 2), ("stem_pool", 2, 2), ("stage1_conv1", 3, 1), ("stage1_conv2", 3, 1), ("stage2_conv1", 3, 2), ("stage2_conv2", 3, 1), ("stage3_conv1", 3, 2), ("stage3_conv2", 3, 1), ] for r in rf_table(backbone): print(f"{r[0]:16s} k={r[1]} s={r[2]} -> out_stride={r[3]:2d} rf={r[4]}")

跑完拿到输出,对照你的任务问三个问题:训练集里目标的典型像素尺寸是多少?最小的目标尺寸是多少?最后一层特征图的感受野能不能覆盖最大目标?我见过太多模型最后一层感受野只有 100 像素,训练数据里却有大到 300 像素的目标,那网络根本就没法把这些目标当成一个整体看待。

感受野的有效值再用梯度法验证一次,比公式更接近真实:

import torch def effective_rf(model, target_cls, in_size=224): model.eval() x = torch.zeros(1, 3, in_size, in_size, requires_grad=True) out = model(x) if out.dim() == 4: h, w = out.shape[-2:] score = out[0, target_cls, h // 2, w // 2] else: score = out[0, target_cls] score.backward() grad = x.grad.abs().sum(dim=1)[0] # 用 95% 能量占比的范围作为有效感受野 flat = grad.flatten().sort(descending=True).values cumsum = flat.cumsum(0) / flat.sum() n = int((cumsum < 0.95).sum().item()) + 1 side = int(n ** 0.5) return side, grad

这个函数返回的是"覆盖 95% 梯度能量所需的边长",比理论感受野小一大截,但它才是网络真实用到的范围。我一般会把理论值和有效值都记下来,两个数字的比值长期稳定在 0.6 到 0.75 之间,如果某次算出比值只有 0.3,说明网络深处有大量"用了但没用上"的参数,值得回头看看是不是膨胀率设计有问题。

7.2 第二步:尺度扫描曲线,一把尺子量到底

固定权重、固定数据,只改推理时的输入尺度,从 0.5 倍扫到 2.0 倍,记录每个尺度下的精度指标。

import torch import torchvision.transforms.functional as TF @torch.no_grad() def scale_sweep(model, images, targets, evaluator, scales=(0.5, 0.75, 1.0, 1.25, 1.5, 2.0)): results = {} for s in scales: preds, gts = [], [] for img, tgt in zip(images, targets): h, w = img.shape[-2:] nh, nw = int(h * s), int(w * s) big = TF.resize(img, [nh, nw]) out = model(big.unsqueeze(0))[0] # 把预测框坐标映射回原图尺度 out_boxes = out["boxes"] / s preds.append({"boxes": out_boxes, "scores": out["scores"]}) gts.append(tgt) results[s] = evaluator(preds, gts) return results

拿到曲线之后重点看三件事。一是峰值位置:如果你的模型在 1.0 倍时最好,说明训练时的尺度分布和测试集一致,没有额外余量;如果峰值出现在 1.5 倍,说明模型系统性地偏向大目标,小目标在 1.0 倍时根本没被充分激活。二是曲线两侧的下降速度:往小尺度方向下降太快,说明浅层特征的高频建模不足;往大尺度方向下降快,说明感受野不够。三是同一类目标在不同尺度下的分数方差,方差大的类别就是尺度敏感的类别,值得针对性加数据。

这套扫描做完,多尺度 TTA 该用哪几个尺度就一目了然了——只选那些单尺度精度还不错的尺度做融合,如果某个尺度的精度已经掉到峰值的一半以下,把它加进 TTA 只会拖后腿。这个判断标准比盲目堆尺度实用得多。

7.3 第三步:把尺度参数整理成一张配置表

排查完、调完,最后一定要把跟尺度相关的所有参数集中到一处,写成一张表放在仓库文档里。这不是形式主义,我在接手别人的项目时最快建立直觉的办法就是看这张表。

参数作用维度当前值调整依据
input_size采样尺度1024×1024小目标最短边不低于 16 像素
output_stride采样尺度16与分割边界精度要求折中
dilation_rates感受野尺度[6, 12, 18]output_stride=16 的标准配置
anchor_scales特征尺度[32, 64, 128, 256, 512]与 FPN 各层 stride 对齐
scale_jitter训练尺度[640, 800]抖动幅度不超过 1.25 倍
tta_scales推理尺度[0.75, 1.0, 1.25]由尺度扫描曲线确定
upsampling尺度还原bilinear + 3×3 conv避免棋盘伪影

这张表的价值在于,任何时候模型的输入尺度变了,你都能顺着"作用维度"这一列快速找出所有需要同步修改的参数,而不是靠记忆。我们团队后来把这张表的每一项都写成了配置项加断言检查:如果input_size改了而anchor_scales没改,训练脚本直接报错退出。这个断言拦下过至少三次线上事故,成本几乎为零。

最后分享一个我觉得最实用的判断习惯:任何关于卷积尺度的问题,先问"是哪个尺度"。感受野不够就加膨胀率或者堆层,特征尺度没覆盖就加 FPN 层级,采样尺度换算错了就统一改锚框和坐标回归的基准。三个问题对应三套完全不同的修法,分清楚了,大部分所谓的"玄学调参"其实都是可以推导的工程问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询