资讯动态

卷积网络感受野增强:SPP、ASPP、RFB、PPM 对比与选型

发布时间:2026/9/18 3:07:38 来源:尧图企业网站定制
搜 PPM 这个词的时候搜索引擎大概会先给你推一屏动力电池的行业新闻——动力电池 PPM 级缺陷控制那是每百万件里面的不良品数制造业的质量指标跟视觉模型里那个金字塔池化模块Pyramid Pooling Module完全是两码事。我第一次给同事解释 PPM 的时候就被这个同名梗坑过对方以为我在聊电池良率。所以在正式聊感受野之前先把话说清楚这篇内容是计算机视觉里卷积网络感受野增强的那些模块SPP、ASPP、RFB、PPM 四个东西。它们解决的是同一类问题——卷积核看得太近而目标有大有小、有远有近。如果你在做分割、检测或者只是想让自己的 backbone 在小目标上别漏那么多这四个模块大概是你绕不过去的一站。我从最早手搓 SPP 到现在把四个模块在不同任务里都跑过一轮中间踩的坑不算少下面把这些东西摊开来聊。1. 感受野到底是个什么东西为什么四个模块都围着它转1.1 一次小目标漏检引出的问题刚入行那会儿我做过一个道路场景的检测任务图片里的交通标志牌尺寸跨度非常大近处的占画面三分之一远处的只有几十个像素。模型训完之后指标看着还行但一放到实际视频里远处的小标志基本全军覆没分类置信度低得可怜。我当时的第一个反应是数据不够加了一批小目标样本重新训结果提升有限。后来才反应过来问题不在数据而在网络的视野上——浅层特征的感受野只有几个像素深层特征虽然感受野大但分辨率已经被下采样压得很低小目标在深层特征图上早就变成一个点或者直接消失了。感受野这个词说白了就是特征图上一个像素点能看到原始输入图像上多大的一块区域。3x3 卷积堆两层感受野是 5x5堆三层是 7x7。听着挺线性但真正的问题在于感受野的扩张速度和下采样带来的信息损失是两条互相拉扯的曲线。你要更大的感受野就得堆更深或者下采样更狠而这两条路都会让细节信息变少。四个模块的出发点其实都在这里能不能在不下采样、不显著增加参数的前提下让特征图上的每个点看到更大的范围。1.2 理论感受野和有效感受野差在哪这里有个很多人知道但没细想过的点。理论感受野是按卷积核尺寸和步长一层层算出来的公式很简单但实际训练出来的网络特征点对感受野内不同位置的响应强度完全不是均匀的。靠近中心区域的像素权重高边缘区域的像素贡献很小甚至接近噪声。有研究专门给这个现象起了个名字叫有效感受野实测下来通常只有理论感受野的三分之一到五分之一而且这个比例跟网络初始化方式、激活函数都有关系。这个差别直接解释了为什么堆卷积核这条路性价比不高——你把理论感受野从 100 堆到 300有效感受野可能只从 30 涨到 50剩下的算力基本浪费在边缘那些响应微弱的区域上。所以 SPP 那一类用池化、ASPP 那一类用空洞卷积、RFB 那一类用多分支不同尺寸的核本质上都是在想办法让感受野的形状更贴合目标而不是单纯比谁的数字大。理解了这一层后面看每个模块的设计就不会觉得是在堆砌技巧。1.3 四个模块的定位差异一眼看清四个模块名字里都有金字塔或者多尺度的味道但手法差别挺大。SPP 走的是池化路线用不同大小的池化核在同一个特征图上采样把多尺度信息拼在一起它不太改感受野的理论大小更多是把不同粒度的上下文聚起来。ASPP 走的是空洞卷积路线用不同膨胀率的空洞卷积并行实打实地把感受野撑大同时保持分辨率不变。RFB 借鉴的是人眼视网膜感受野的分布规律用多个分支配合不同尺寸和膨胀率的卷积模拟出中心密集、外围稀疏的响应模式。PPM 则更直接把特征图切成不同数量的网格块做池化专门抓全局上下文。我一般这么跟人解释SPP 是同一个视野看不同粗细ASPP 是同一张图看不同远近RFB 是模拟眼睛怎么看PPM 是先看整体再看局部。这几个定位在选型的时候很有用因为很多任务其实只需要其中一两个的特性四个全堆上去除了让工程变复杂收益未必叠加。注意这四个模块都不是越堆越好感受野增强和特征分辨率之间是此消彼长的关系堆多了会导致小目标特征被大范围的池化或大膨胀率的卷积抹平。2. SPP从输入尺寸必须固定的坑里爬出来2.1 SPP 最开始解决的不是感受野问题聊 SPP 得先说清楚它的出身。它最早出现在分类网络里当年分类网络的最后几层是全连接层全连接层要求输入特征图的尺寸是固定的所以训练和推理时输入图片必须缩放到同一个尺寸强行裁剪或者拉伸图片变形严重。SPP 的做法是在最后的卷积特征图上做多个尺度的池化把不同尺度的池化结果拼成一个固定长度的向量这样不管输入图片多大输出向量的长度都一致全连接层就自由了。后来大家发现这个结构顺手还带来了感受野的好处。因为池化核足够大比如 13x13 的池化核作用在特征图上每个输出点覆盖的范围比任何卷积核都大等于用一个非常廉价的方式获取了大范围上下文。于是 SPP 被搬进了检测和分割网络成了标准配置。我第一次在检测网络里加 SPP 的时候最直观的感受是模型对大目标的框回归稳定了不少因为大目标的整体轮廓被池化层看全了。2.2 池化核尺寸怎么定才不出错SPP 的实现有个反直觉的细节池化核必须是奇数而且要配合 stride1 和 paddingk//2这样才能保证输出特征图尺寸和输入完全一致不然拼接的时候通道数对了、宽高对不上直接报维度错误。核尺寸的选择也有讲究。常见的配置是 5、9、13这三个数字不是随便挑的。5 对应中等范围9 对应大范围13 在一张 20x20 到 40x40 的特征图上基本接近全局。如果特征图只有 10x10你上 13x13 的核padding 之后几乎全是边界填充输出响应会被稀释反而拖累性能。我见过有人在小分辨率特征图上照抄 5、9、13结果 SPP 分支的输出几乎全是同一个值白白增加了一倍计算量。一个经验判断是核尺寸不要超过特征图短边长度的三分之一。特征图是 30x30最大的核放到 9 或者 11 就够特征图 60x60可以上 13 甚至更大。你要是不确定先在几个候选值上做一次前向把每个分支输出的均值和方差打出来看看如果某个分支的方差接近零说明这个核太大基本在做全局平均了可以砍掉。2.3 代码实现与三个容易写错的细节下面是一个标准的 SPP 实现为了方便对比我把关键点都标出来了。import torch import torch.nn as nn class SPP(nn.Module): def __init__(self, c1, c2, k(5, 9, 13)): super().__init__() c_ c1 // 2 # 先压缩通道控制参数量 self.cv1 nn.Conv2d(c1, c_, 1, 1) self.cv2 nn.Conv2d(c_ * (len(k) 1), c2, 1, 1) # stride 必须为 1padding 取 kernel//2保证尺寸不变 self.m nn.ModuleList([ nn.MaxPool2d(kernel_sizex, stride1, paddingx // 2) for x in k ]) def forward(self, x): x self.cv1(x) # 原始特征 多个池化分支一起拼接 return self.cv2(torch.cat([x] [m(x) for m in self.m], 1))第一个容易错的点是c_ * (len(k) 1)这个通道数。拼接的时候原始特征本身也占一份所以是分支数加一漏掉那个加一就会在cv2那里报通道不匹配。第二个是 padding 的取整如果用paddingx//2而 x 是偶数输出尺寸会差一个像素所以核尺寸坚持用奇数。第三个是MaxPool2d默认没有 padding必须显式写。实际部署里还有个隐性的坑SPP 的多个大核池化在移动端或者推理引擎上不一定都被良好支持13x13 的 maxpool 在某些图优化流程里会被拆成一堆小操作速度掉得厉害。上线前务必在目标推理框架上跑一遍 profiler别只在 PyTorch 里测。2.4 SPPF把串联池化做便宜SPPF 是 SPP 的一个变体思路很聪明与其用三个不同大小的池化核并行不如用三个相同大小的 5x5 池化核串起来。串联的效果等效于感受野逐级扩大第一个池化覆盖 5x5第二个在第一个的输出上再覆盖 5x5等效 9x9第三个等效 13x13。效果和并行三核接近但计算量小很多因为每级只需要算一次 5x5。class SPPF(nn.Module): def __init__(self, c1, c2, k5): super().__init__() c_ c1 // 2 self.cv1 nn.Conv2d(c1, c_, 1, 1) self.cv2 nn.Conv2d(c_ * 4, c2, 1, 1) self.m nn.MaxPool2d(kernel_sizek, stride1, paddingk // 2) def forward(self, x): x self.cv1(x) y1 self.m(x) y2 self.m(y1) y3 self.m(y2) return self.cv2(torch.cat((x, y1, y2, y3), 1))我在几个检测项目里对比过 SPP 和 SPPF精度差异基本在千分位级别但 SPPF 在 TensorRT 上的推理延迟能低 15% 到 25%越是边缘设备差距越明显。所以现在的默认选择基本都是 SPPF除非你的任务对大范围上下文特别敏感需要 13x13 那种真正的宽核响应。3. ASPP用空洞卷积不动分辨率地撑开视野3.1 空洞卷积的等效核尺寸怎么算空洞卷积也叫膨胀卷积的核心操作是在卷积核的元素之间插入空隙。一个 3x3 的卷积核膨胀率为 2 的时候实际覆盖的区域是 5x5膨胀率为 6 的时候是 13x13。公式很直接k_eff k (k - 1) * (d - 1)其中 k 是原始核尺寸d 是膨胀率。3x3 配 d6就是 3 2*5 13一个 3x3 的卷积拿到了 13x13 的感受野参数量还是 9 个权重加一个偏置没有增加。这就是空洞卷积迷人的地方——用极小的参数代价换感受野。但代价也很明显。空洞卷积的采样点是稀疏的核元素之间有间隙如果膨胀率选得不合适会出现网格效应特征图上的一些位置被反复采样另一些位置从来没被覆盖到形成棋盘状的伪影。这个现象在膨胀率是质数、且各层膨胀率有公约数的时候特别明显。我早期做语义分割时用过 2、4、8 这组膨胀率结果分割结果里出现了明显的网格纹理一度怀疑是数据标注问题排查了半天才定位到膨胀率的公约数上。3.2 网格效应和膨胀率组合的选型规避网格效应的思路是让膨胀率之间互质或者不要有太强的公约数关系。DeepLab 系列后来用的典型组合是 6、12、18这三个数有公约数 6听起来违反原则但它们对应的等效核尺寸是 13、25、37跨度足够大叠加之后覆盖区域能互补实际效果是好的。另一类常见组合是 1、2、3 或者 1、2、4用在较浅的层上。选择膨胀率时有个比较实用的判断方法让你的最大膨胀率对应的等效核尺寸接近或者略小于当前特征图的短边长度。如果特征图是 32x32最大等效核尺寸放在 25 到 31 之间比较合适如果放到 37 就偏大了边缘填充会占主导。另外膨胀率超过一定值之后比如 d 大于 103x3 空洞卷积退化成几个孤立的采样点卷积本身的空间连续性就丢失了这时候考虑用全局池化分支来补而不是继续加大膨胀率。3.3 DeepLab 里的 ASPP 演进ASPP 的结构从 DeepLabv2 到 v3 变化不小值得说一下。v2 版本是并联四个不同膨胀率的空洞卷积简单粗暴但每个分支的输出直接相加。问题在于不同膨胀率的特征分布差异大直接相加会有冲突而且没有全局信息。v3 版本做了两个重要改进一是加了全局平均池化分支专门抓图像级上下文二是把相加改成拼接然后接一个 1x1 卷积做融合把不同分支的特征投影到统一空间。拼接改相加这一步看起来小实际影响挺大。相加要求各分支的输出在同一语义空间里这在训练早期很难满足拼接更宽容让网络自己学怎么组合。我在自己搭的分割网络里两种都试过拼接版本的 mIoU 稳定高 1 到 2 个百分点而且收敛更快大概是因为拼接给了融合层更大的自由度。还有一点容易被忽略ASPP 里的每个空洞卷积分支后面都需要接 BN 和 ReLU但最后的 project 层里BN 之后接 ReLU 再接 Dropout。Dropout 放在 BN 之后是 DeepLab 的原始做法它能让推理时的统计更稳定不过如果你在训练时 batch size 很小Dropout 加上小 batch 的 BN 统计会不太稳这时候可以考虑把 Dropout 换掉或者调低比例。3.4 代码实现与几个配置坑import torch import torch.nn as nn import torch.nn.functional as F class ASPPConv(nn.Sequential): def __init__(self, in_channels, out_channels, dilation): super().__init__( nn.Conv2d(in_channels, out_channels, 3, paddingdilation, dilationdilation, biasFalse), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue) ) class ASPPPooling(nn.Sequential): def __init__(self, in_channels, out_channels): super().__init__( nn.AdaptiveAvgPool2d(1), nn.Conv2d(in_channels, out_channels, 1, biasFalse), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue) ) def forward(self, x): size x.shape[-2:] for mod in self: x mod(x) return F.interpolate(x, sizesize, modebilinear, align_cornersFalse) class ASPP(nn.Module): def __init__(self, in_channels, atrous_rates(6, 12, 18), out_channels256): super().__init__() modules [] modules.append(nn.Sequential( nn.Conv2d(in_channels, out_channels, 1, biasFalse), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue))) for rate in atrous_rates: modules.append(ASPPConv(in_channels, out_channels, rate)) modules.append(ASPPPooling(in_channels, out_channels)) self.convs nn.ModuleList(modules) self.project nn.Sequential( nn.Conv2d(len(self.convs) * out_channels, out_channels, 1, biasFalse), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue), nn.Dropout(0.5), ) def forward(self, x): res [conv(x) for conv in self.convs] return self.project(torch.cat(res, dim1))几个实测踩过的坑。第一ASPPPooling里池化到 1x1 之后F.interpolate必须用align_cornersFalse用 True 的话上采样结果会整体偏移半个像素在大图上表现为边界区域轻微错位分割边缘会糊。第二如果输入特征图尺寸小于最大膨胀率的等效核比如特征图 8x8 而膨胀率是 18等效核 37那么卷积几乎全在做零填充输出基本退化成常数这个分支等于废掉。判断方式很简单打印这个分支输出的标准差就行。第三所有空洞卷积分支的输出通道数必须一致否则拼接会失败DeepLab 里统一是 256。注意ASPP 的全局池化分支在小 batch 训练时如果图像尺寸变化大BN 的滑动统计会被剧烈扰动。稳妥做法是给这个分支的 BN 设一个更小的 momentum或者干脆换成 GroupNorm。4. RFB让感受野长得像人眼4.1 从生态学里的感受野分布说起RFB 这个模块的灵感来源比较有意思它借鉴的是生态学和神经科学里关于生物视网膜感受野分布的观察。人眼看东西的时候中央凹区域的感受野小、密度高负责细节辨识外围区域的感受野大、密度低负责整体感知。这种中心密集、外围稀疏的分布让眼睛既有分辨率又有大视野。RFB 想复现的就是这种分布特性。它的设计目标是让特征图上不同位置、不同通道对感受野的利用更符合多尺度且不平坦的规律。跟 ASPP 那种每个分支权重一样的并行结构比RFB 更强调分支之间的尺度差异和融合方式。在我做过的工业缺陷检测任务里RFB 对小缺陷的召回确实比单纯堆卷积好因为它能在多个尺度上同时保持对细小纹理的敏感。4.2 RFB 的结构拆解RFB 的一个基本单元可以拆成这几部分先用 1x1 卷积做通道压缩然后分出多个分支每个分支用不同尺寸的普通卷积比如 1x1、3x3、5x5部分分支后面再接膨胀率不同的空洞卷积最后把所有分支的 3x3 输出拼接起来再过一个 1x1 做融合。结构上有一点很关键RFB 的短接设计。它不像残差那样直接相加而是把各分支的输出拼接后再降维。这样的好处是不同分支的信息不会互相覆盖融合层可以通过 1x1 卷积的权重决定每个尺度的重要程度。我实测过把拼接改成相加效果确实掉了原因也是前面提到的语义空间问题。实现上最常见的写法是定义一个BasicConv作为基础卷积单元然后组装 RFB。class BasicConv(nn.Module): def __init__(self, in_planes, out_planes, kernel_size, stride1, padding0, dilation1, groups1, reluTrue, bnTrue): super().__init__() self.conv nn.Conv2d(in_planes, out_planes, kernel_sizekernel_size, stridestride, paddingpadding, dilationdilation, groupsgroups, biasFalse) self.bn nn.BatchNorm2d(out_planes, eps1e-5, momentum0.01) if bn else None self.relu nn.ReLU(inplaceTrue) if relu else None def forward(self, x): x self.conv(x) if self.bn is not None: x self.bn(x) if self.relu is not None: x self.relu(x) return x注意momentum设成了 0.01比默认的 0.1 小。这是 RFB 原始实现里的选择目的是让 BN 的滑动平均更新更平滑尤其在目标检测那种输入尺寸多变的场景下更稳。这个细节在很多复现版本里被丢了但小 batch 训练时确实有影响。4.3 RFB-s 与轻量化取舍RFB 有两个版本RFB 和 RFB-s区别在分支数量和通道数。RFB-s 是轻量版分支少、通道窄适合放在浅层或者移动端。我在一个嵌入式缺陷检测项目里用过 RFB-s当时的约束是模型要跑在算力有限的板子上单帧延迟控制在 30 毫秒以内。用完整版 RFB 延迟超标换成 RFB-s 之后达标精度只掉了不到 1 个点。但 RFB 也有它的问题主要是分支多导致的显存占用和算子碎片化。一个 RFB 模块里可能有六七个不同的卷积分支在推理引擎里就是一堆小算子GPU 利用率不高实际吞吐量不如参数量相同的单路卷积。我做过一次对比参数量差不多的 RFB 模块和 ASPP 模块ASPP 在 GPU 上的吞吐高约 20%因为它的算子是规则的、通道宽、大核少。所以如果你在意吞吐而不是参数效率ASPP 或者直接用大核深度卷积会更省事。提示RFB 的价值主要在多尺度响应模式上如果你的数据集里目标尺度分布本身很集中RFB 相对 ASPP 的优势不明显不必强行替换。5. PPM分块池化抓住全局上下文5.1 PPM 和 SPP 的血缘关系PPM 和 SPP 有直接的血缘关系都是金字塔式池化但作用点不一样。SPP 通常放在 backbone 末端池化核作用于整个特征图输出拼回去继续做检测或者后续处理。PPM 在 PSPNet 里被放在分割网络的最后阶段把特征图按不同粒度切分成若干个子区域每个子区域单独做池化然后上采样回原尺寸再拼接。这个分块是 PPM 最核心的设计。SPP 是全局池化加几个不同大小的核PPM 是先把特征图切成 1x1、2x2、3x3、6x6 的格子每格单独池化。区别在于1x1 的池化抓的是全图的平均语义2x2 抓的是四个象限的语义6x6 抓的是更局部的区域语义。这样拼接之后特征图上的每个位置都同时携带了从全局到局部的多级上下文对场景理解类的任务特别有用。我在做城市场景分割的时候最明显的变化是天空、道路、建筑这些大块区域的一致性好了很多。没有 PPM 的时候同一片天空里经常出现零星的错误类别因为局部感受野看不到整体加了 PPM 之后全图上下文被注入到每个位置这类错误基本消失。5.2 分块金字塔的尺寸选择PPM 的分块方案常见的是 1、2、3、6 这四档输出四组池化特征加上原始特征一共五路。选这个组合是有道理的1x1 抓全局2x2 分隔上下左右3x3 提供更细的划分6x6 用于捕捉局部纹理级别的上下文。再往上加 8x8 或 12x12 收益就很小了因为分块太细的时候每个格子里的特征已经接近局部卷积的感受野跟原始特征重复。分块数和特征图尺寸的关系也要看。如果你的特征图只有 16x16分到 6x6 意味着每个格子平均 2.7 个像素池化结果几乎就是单点采样意义不大。一般来说最大分块数不要超过特征图短边的四分之一。16x16 的特征图用 1、2、3 三档就够了32x32 以上再用 1、2、3、6。还有一点PPM 的各路输出都要上采样回原始尺寸再拼接这里的上采样方式影响细节。双线性插值最常用速度快、伪影少用最近邻插值会出现明显的块状边界分割结果里表现为矩形网格用转置卷积虽然理论上更精细但容易产生棋盘伪影而且参数多。我基本都选双线性只在特别在意细节的时候才考虑带学习参数的上采样。5.3 代码实现与拼接顺序import torch import torch.nn as nn import torch.nn.functional as F class PPM(nn.Module): def __init__(self, in_dim, reduction_dim, bins): super().__init__() self.features nn.ModuleList() for bin_size in bins: self.features.append(nn.Sequential( nn.AdaptiveAvgPool2d(bin_size), nn.Conv2d(in_dim, reduction_dim, kernel_size1, biasFalse), nn.BatchNorm2d(reduction_dim), nn.ReLU(inplaceTrue) )) def forward(self, x): x_size x.shape[-2:] out [x] for f in self.features: pooled f(x) out.append(F.interpolate(pooled, sizex_size, modebilinear, align_cornersTrue)) return torch.cat(out, dim1)这里最后一个torch.cat的输出通道是in_dim reduction_dim * len(bins)后面的融合层必须按这个数来配。我见过有人按reduction_dim * len(bins)写结果通道数对不上。AdaptiveAvgPool2d的好处是你不用关心输入尺寸是不是能被整除任意尺寸都能分块比固定 kernel 的池化省心得多。另外reduction_dim一般设成in_dim // 4这是一个比较经典的压缩比例既降低了 PPM 模块的参数量又保留了足够的表达能力。如果你的显存不紧张设成in_dim // 2也能跑但收益不大。6. 四个模块怎么选参数、场景与组合策略6.1 一张表看清四个模块的差别模块核心操作是否改变分辨率感受野扩张方式参数量典型场景SPP多核最大池化否池化核覆盖低检测头、backbone 末端ASPP多分支空洞卷积否膨胀率扩大等效核中语义分割、高分辨率特征RFB多分支多尺寸卷积否分支尺度叠加中高小目标检测、缺陷检测PPM分块自适应池化否内部降采样后上采样分块粒度覆盖低场景解析、大区域分割从表里能看出来四者都不改变输入输出的分辨率这是它们的共同底线。差别主要在感受野的扩张机制和参数开销上。SPP 和 PPM 走池化路线参数量小但感受野的形状比较粗ASPP 和 RFB 走卷积路线参数量大但响应更精细。6.2 按任务场景选型如果你在做目标检测尤其是单阶段检测器SPPF 基本是标配成本低、效果稳。如果你的检测任务里小目标占比高可以考虑在 neck 部分插一两个 RFB但要注意显存。ASPP 在检测里用得相对少一些因为检测头通常需要较高的特征分辨率ASPP 的膨胀率不好配。分割任务就完全是另一个局面。语义分割的标准配置基本是 ASPP 或 PPM或者两个都用。DeepLab 系列用 ASPPPSPNet 用 PPM后来的很多工作把两者组合在 backbone 末端同时接 ASPP 和 PPM再把输出融合。我自己做的分割项目里PPM 加 ASPP 的组合比单用任何一个mIoU 能高 2 到 3 个点代价是推理延迟增加大概 30%。如果延迟不敏感这个组合值得试。分类任务里这几个模块用得不多因为分类本身只需要全局判断全局平均池化就够了。不过在细粒度分类那种需要局部细节的任务里加个 SPP 偶尔有奇效。6.3 组合与堆叠的实测经验堆叠的顺序有讲究。我的经验是先 ASPP 后 PPM 比反过来好。原因是 ASPP 的输出保持了空间结构PPM 接在后面做分块池化时分块的语义更清晰如果反过来PPM 的池化输出已经被全局语义稀释再进 ASPP 的空洞卷积膨胀率的效果会被削弱。还有一个容易被忽略的点这几个模块放在哪个位置。放在 backbone 末端的高层特征上时特征图小、通道多池化和空洞卷都不贵放在浅层的高分辨率特征上计算量会成倍增加而且浅层特征的感受野本来就小用大膨胀率反而会引入噪声。我一般只在 stride 16 及以上的特征层上用这些模块浅层交给普通卷积。7. 实测踩坑与排查清单7.1 显存、速度和精度之间的取舍这几个模块对显存的消耗比看起来大尤其 ASPP。原因是空洞卷积虽然参数量不大但它在高分辨率特征图上计算激活值占的显存很可观。64 通道、128x128 的特征图上跑一个膨胀率 18 的 3x3 空洞卷积激活值就是 64 * 128 * 128 * 4 字节单个分支接近 4MB多个分支叠加起来很快。我在一张 8GB 的卡上吃过亏backbone 能跑加了 ASPP 之后 batch size 从 8 降到 2。速度方面前面提过 ASPP 的算子规整吞吐优于 RFB。SPP 和 PPM 里的大核池化在某些推理引擎上会被拆解速度不一定比卷积快。这是个反直觉的点很多人以为池化一定便宜实际上 13x13 的 maxpool 在某些实现里比 3x3 卷积还慢。上线前一定要测。精度上我的整体感受是大目标为主的任务SPP 和 PPM 的收益最明显小目标多的任务RFB 和 ASPP 更好目标尺度分布均匀的任务随便选一个差异在 1 个点以内。7.2 常见现象排查表现象可能原因排查方法某个池化分支输出接近常数池化核过大接近全局平均打印各分支输出的标准差分割结果出现网格纹空洞卷积膨胀率有公约数网格效应改膨胀率组合或加全局池化分支拼接时通道数报错漏算原始特征那一份检查融合层输入通道公式上采样后边界错位align_corners设置与训练不一致训练和推理统一为 False加了模块后精度反降膨胀率超过特征图尺寸分支退化计算等效核尺寸与特征图短边的关系显存暴涨高分辨率特征图上做空洞卷积把模块后移到 stride 16 以上这张表里的每一条我基本都真遇到过尤其是网格纹和align_corners这两条排查起来最耗时因为现象不明显、容易往数据方向怀疑。7.3 训练侧的几个小技巧第一个技巧是 warmup 配合这些模块。ASPP 和 RFB 的分支多初始化差异大训练一开始各分支输出分布不匹配损失会震荡。开几百步的 warmup让 BN 的统计先稳下来收敛会顺很多。我一般用 500 到 1000 步的线性 warmup。第二个技巧是给这些模块单独设学习率。如果用分层学习率backbone 用基础学习率这些新增的融合模块可以乘 1.5 到 2 倍因为它们是从头训的需要更快的学习速度。这个做法在微调预训练模型的时候特别有用。第三个是别急着把模块塞进预训练权重里。如果你拿的是在别的数据集上训好的 backbone加载权重的时候这些新模块是没有预训练参数的加载会报 missing keys这是正常的。但要留个心眼有些实现会用strictFalse静默加载结果某些原本应该加载的层也没加载上精度莫名其妙地低排查半天。加载完权重之后打印一下缺失的 key 列表确认只有新模块缺失。提示在分割任务里如果 PPM 和 ASPP 同时使用建议先把 PPM 的reduction_dim调小一点再训几轮看指标不要一上来就用大通道容易过拟合小数据集。最后分享一个我自己常用的小手段想快速判断某个感受野模块有没有用先不加模块训一个 baseline记下指标然后只在一个尺度上加模块泡在一个小验证集上跑十几个 epoch。如果十来个 epoch 内验证指标没有明显优势大概率这个模块跟你的任务尺度分布不匹配硬塞进去只会增加工程复杂度。我一开始也迷信模块越多越好踩过几次之后发现选对位置比选对模块更重要。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价