区域化Non-local视频分类:降显存时序建模与PyTorch实现 这两年做视频理解的同学大概都有同一个感受想给卷积主干加上时序建模能力绕不开 Non-local 这一类自注意力操作但它对显存和算力的胃口实在太大一段十六帧的片段、分辨率稍微高一点注意力矩阵就直接把显存吃干净。最近某高校研究团队提出的 RNL 思路很有意思核心一句话就是别让每个位置都跟全图所有位置算相似度先把时空特征切成若干区域只在区域内部做 Non-local 聚合。与此同时另一条线上的基于 CNN 的人脸识别已经非常成熟度量学习、特征归一化、局部区域描述子这些做法早就被验证过把两条线放在一起看会发现很多结构上的共性。这篇就围绕区域化 Non-local 在视频分类里的落地来讲从设计取舍、区域怎么划、参数量和显存怎么手算到可直接跑的 PyTorch 模块、训练配置、踩坑排查全流程走一遍。适合已经能训通一个 3D 卷积或帧级 CNN 视频分类基线、想把时序建模做得更省一点的读者也适合做 CNN 人脸识别、想借鉴局部区域建模思路的人。1. 先把问题摆清楚视频分类为什么非要区域化的 Non-local1.1 从基于 CNN 的人脸识别说起静态特征的成熟与视频任务的缺口基于 CNN 的人脸识别这条线本质是把一张对齐后的人脸图送进卷积主干输出一个归一化的高维嵌入再用带 margin 的度量损失把同类拉近、异类推远。它的几个关键前提是输入是单帧静态图、空间对齐做得很好、判别信息主要落在局部纹理与部件几何关系上。所以早期做法里把脸切成若干水平条带分别提特征、再做拼接或加权是很自然的选择这就是一种朴素的“区域化”归纳偏置——先局部建模再全局融合既降低了每个局部模块的建模难度也让特征对局部遮挡更鲁棒。视频分类的难点不在单帧特征提得好不好而在于帧与帧之间的长程依赖。一个人在做某个动作时手可能在第三帧抬起、第十二帧落下中间还有身体其他部位的位移这种跨时间、跨空间位置的关联靠堆叠 3D 卷积的有限感受野去覆盖层数要堆得很深参数效率很低。Non-local 操作的动机就是一步到位让任意两个时空位置直接建立联系感受野一次性拉满。这个思路在理论上很漂亮代价也很直接——注意力矩阵的大小是位置数的平方。我个人的理解是人脸识别解决的是“同一个语义在不同外观下的稳定表达”视频分类解决的是“同一个语义在不同时刻的演化关系”前者靠对齐和局部描述子后者靠关系建模。把这两件事对照来看会更容易理解为什么区域化是视频侧一个必然要走的折中。1.2 原始 Non-local 的公式与它的真实开销标准 Non-local 的输出可以写成对位置的加权求和y_i (1 / C(x)) * Σ_j f(x_i, x_j) * g(x_j)其中 x_i 是第 i 个时空位置的特征向量f 是相似度函数最常用嵌入式高斯即两个线性变换后的点积g 是值变换C(x) 是归一化因子对所有 j 求和。落到张量上输入形状是 (B, C, T, H, W)先压成 (B, C, N)N T×H×W然后算一个 N×N 的相似度矩阵。关键在于 N 的量级。以一个中等配置为例T8、HW28N 8×784 6272相似度矩阵是 6272×6272约 3934 万个元素float32 下单样本单层就要 157MB 左右。如果 batch 是 16、网络里插了 4 个这样的模块还没算反向传播保存的中间量光前向的注意力矩阵就要占用 10GB 上下。这在实际训练里基本不可行除非把分辨率压到 14×14 或者帧数砍到 4。除了显存还有算力。嵌入式高斯的相似度计算每对位置的代价是一次 C 维点积总乘加量约为 2×N²×C。N6272、C256 时单个模块的相似度计算就接近 200 亿次乘加比整个主干的卷积开销还大。这就是为什么很多工程实现里 Non-local 只敢插一两层且必须配一个通道瓶颈把 C 降到 C/2 或 C/4。注意很多人第一次实现 Non-local 时只看到论文里的公式简洁忽略了 N² 这一项会随帧数和分辨率同时平方增长帧数从 8 翻到 16、分辨率从 28 翻到 56N 会变成 4 倍和 4 倍合起来就是 16 倍矩阵规模变成 256 倍。1.3 RNL 的核心取舍用区域稀疏化换可承受的算力区域化 Non-local 的做法说穿了很朴素把 N 个位置先划分成 R 个互不重叠的区域每个区域里有 N_r N / R 个位置然后只在区域内部计算相似度并聚合。相似度矩阵从 N×N 变成 R 个 N_r×N_r 的小矩阵总元素数从 N² 降到 R×N_r² N²/R直接省了一个 R 倍。继续上面的例子R16、N6272N_r392总元素数从 3934 万降到 16×153664 ≈ 246 万约 16 倍下降显存从 157MB 降到 9.8MB。这个量级就完全可以在正常分辨率、正常 batch 下训练了而且可以插多层。算力同理相似度的乘加量从 2N²C 降到 2N²C/R。这个折中换来的是表达能力的有损原本任意两个位置都能直接交互现在只有同区域内的位置能直接交互跨区域的信息只能靠区域划分的随机性、或者堆叠多层来间接传递。这里的取舍逻辑和卷积的局部感受野很像——我们接受“直接交互只发生在邻域内”然后用深度去换全局性。区别在于卷积的邻域由空间距离定义而区域化 Non-local 的邻域由“你被分到哪个组”定义后者是人为设计的所以区域划分策略就成了整个方案最需要琢磨的地方。我在实际项目里试过两种极端一种是区域划得很小、层数插得很多效果和开销都接近 3D 卷积另一种是区域划得很大、只插一两层接近原始 Non-local 但省得有限。真正好用的甜点区通常在两者之间后面第 2 章会把划分方式拆开讲。1.4 这套思路的适用边界与影响范围说清楚它适合什么场景比说它多强更有用。区域化 Non-local 最舒服的落地场景有三类第一类是动作识别、视频分类这类需要中长程时序建模、但算力预算有限的训练任务尤其是从 2D 主干迁移过来的模型插几层就能明显补上时序短板第二类是长视频、高分辨率的场景比如监控片段、体育视频全量 Non-local 完全跑不动区域化是少数能落地的方案第三类是需要多尺度时序关系的任务比如时序动作定位区域可以按时间维度和空间维度分别切形成不同尺度的关系建模。不太适合的场景也要说如果任务本身的判别信息高度集中在局部比如细粒度的手势分类关键信息只在于部的一小块区域那么把大量算力花在区域内的全局聚合上性价比不高不如老老实实加深 3D 卷积如果片段极短4 帧以内位置数本来就小全量 Non-local 也不贵区域化的收益有限。回到人脸识别那条线其实也可以反向借鉴基于 CNN 的人脸识别在特征图上做区域划分、分区聚合本质上和 RNL 的区域思路同源只是目的从“建模时序关系”变成了“增强局部判别”。这种跨任务的思路迁移往往比在单一任务里死磕更有效。2. 核心细节解析区域怎么划相似度怎么算才不出错2.1 三种区域划分方式与各自的选择依据区域划分是整个方案里唯一“可以自由设计”的部分也是效果差异最大的地方。我梳理了三种常见做法各自的逻辑和适用条件差别很大。第一种是纯空间划分。把特征图在 H×W 上切成 g_h×g_w 个网格时间维不动每个网格跨全部 T 帧。这样每个区域内部包含同一空间位置在不同时刻的所有特征区域内的聚合天然就是“同一位置的时序关系建模”。这个设计对动作识别很友好因为动作的核心就是“某个部位随时间怎么动”。缺点是跨空间部位的交互被切断比如“手”和“球”的关系建模不到。我一般把它作为默认选项因为它在多数动作识别数据集上收益最稳。第二种是纯时间划分。把 T 切成 g_t 段每段内包含全部 H×W 个空间位置。区域内的聚合变成“同一时间段内的空间关系建模”时序关系则靠不同段之间的层间传递。这个设计适合场景理解类任务比如判断画面里有哪些物体在互动但对细粒度动作的收益不如第一种。第三种是时空联合划分。同时切 g_t×g_h×g_w区域数 R g_t×g_h×g_w。这是最通用的参数也最多需要调。好处是可以精细控制每个区域的大小坏处是区域变小后每个区域内的位置数太少聚合退化成近似逐点变换反而失去意义。实际选择上我建议先用纯空间划分跑一个基线把 R 定在 16 到 36 之间看验证集表现再决定要不要往时空联合走。有一个经验判据区域内位置数 N_r 最好不要低于 64低于这个数区域内的相似度分布太稀疏softmax 之后接近 one-hot聚合就变成了“只取最近邻”损失了加权求和的平滑性。按 N6272 算R 取 16 到 98 都在可用区间R36 时 N_r≈174比较舒服。2.2 张量形状与参数量的手算过程写代码之前先把形状理清楚能省掉大量调试时间。约定输入为 (B, C, T, H, W)其中 C 是通道数。完整流程是先用 1×1×1 卷积把通道降到 C通常 C/2 或 C/4作为瓶颈然后压平成 (B, C, THW)再按区域重排成 (B, R, C, N_r)接着在区域维上做批量矩阵乘得到 (B, R, N_r, N_r) 的相似度softmax 后与值张量相乘回到 (B, C, N)最后用 1×1×1 卷积升回 C 并加残差。参数量的计算很直接主要是三个 1×1×1 卷积θ、φ、g 各一个外加最后的输出投影。如果三个变换都不降维参数量是 3×C×C用瓶颈降到 C/2就是 3×C×(C/2) 1.5C²。以 C256 为例不降维是 196608 个参数降维后是 98304 个。输出投影再加 C×C 或 (C/2)×C。通道瓶颈这一手很关键。Non-local 的相似度计算里C 出现在内积维度上降低 C 直接线性降低算力但对表达力的损伤比降低 N 小得多因为相似度是一个标量不需要那么高的维度去刻画。我实测把 C 降到 C/4 时精度掉得不明显但速度提升明显这是性价比最高的一刀。下面这张表是我在不同配置下的手算对比可以拿来快速估算自己的场景配置T×H×WNC区域数 RN_r注意力矩阵元素数float32 显存/样本/层全量8×28×2862722561627239.3M约 157MB空间 4×48×28×286272256163922.46M约 9.8MB空间 6×68×28×286272256361741.09M约 4.4MB全量高清16×56×56501762561501762518M约 10GB空间 4×4 高清16×56×5650176256163136157M约 629MB看最后两行就明白为什么高分辨率场景必须区域化全量方案单层单样本就要 10GB再好的卡也扛不住区域化之后 629MB配上梯度检查点就能在常规卡上跑起来。2.3 残差接入与初始化一个几乎必踩的坑区域化 Non-local 模块一般以残差形式插入输出 输入 模块输出。这里有个细节比公式重要得多——输出投影层的初始化。如果把输出 1×1×1 卷积的权重直接随机初始化训练初期这个模块会往主干里注入一个量级不小的噪声破坏预训练权重的特征分布表现为前几百步 loss 剧烈震荡甚至发散。标准做法是把输出投影层的权重初始化为 0或者把其后接的 BatchNorm 的 gamma 置 0。这样训练开始时模块输出恒为 0等价于恒等映射主干行为完全不变随着训练推进权重逐渐离开 0模块才开始贡献。我在一个从 2D 主干迁移的项目里对比过不做零初始化时第一个 epoch 的验证精度比基线低 8 个点要靠更长的 warmup 才追回来做了零初始化第一个 epoch 就和基线持平第二个 epoch 开始反超。提示如果输出投影后接了 BN一定要把 BN 的 weightgamma初始化为 0而不是只把卷积权重置 0。因为 BN 的 bias 如果非零输出仍然不为 0恒等映射的前提就破了。另外如果模块里用了 BN 且 batch 较小建议换成 GroupNorm视频任务的 batch 通常上不去BN 的统计量在小 batch 下噪声很大直接拖累时序建模的稳定性。2.4 与 3D 卷积、时空注意力的横向对照选型时经常被问为什么不直接用 3D 卷积或者直接上纯 Transformer 的视频模型我把三条路线放在一张表里对比判断依据会更清楚。方案感受野参数量显存特点迁移成本适合场景3D 卷积局部靠深度堆叠中等核 3×3×3显存随深度线性增长需从头训或膨胀 2D 权重数据量充足、追求推理速度全量 Non-local全局小仅 1×1×1N² 爆炸可插进任意 2D 主干低分辨率、短片段区域化 Non-local区域内全局 跨区域间接小N²/R可插进任意 2D 主干中高分辨率、需时序建模时空 Transformer全局大N² 爆炸 层数多需重新设计大数据、可接受高成本区域化 Non-local 的定位很清楚它是在“想要全局关系、但付不起全局代价”时的中间解。相比 3D 卷积它用很小的参数增量拿到了区域内的全连接关系对长程依赖更友好相比全量 Non-local它把开销压到可训练范围相比时空 Transformer它更像一个即插即用的增强模块可以直接挂在已有的 2D 或 (21)D 主干上不用重做整个网络结构。这个“可插拔”属性是我最看重的意味着已有项目的迁移成本极低。3. 手把手实现从张量约定到可训练模块3.1 输入形状与插入位置的确定先把约定固定下来后面所有代码都基于此。输入张量形状为 (B, C, T, H, W)这是 PyTorch 3D 卷积的默认布局。模块插入的位置建议放在主干的中后段太靠前时特征图分辨率高、位置数大划分区域后每个区域仍然很大省不了多少太靠后时空间分辨率已经很小比如 7×7区域划分粒度不够切出来的区域没有语义意义。我的经验是放在 stage3 和 stage4 之间或者每个 stage 的最后一个残差块之后。以 ResNet 类主干为例stage3 输出的空间分辨率通常是输入的 1/1628×28 左右正好是区域化的甜点。插入层数不要超过 4 层超过之后边际收益快速衰减而训练时间线性增长。如果主干是 2D 的、需要在时间维上先做处理建议先用一个时间维的 1D 卷积或时间池化把 T 压到 8 到 16再送进区域化模块。时间维太长会让 N 线性增长收益却不一定增加。3.2 区域化 Non-local 模块的完整实现下面是可直接使用的实现包含空间划分、可选时空联合划分、通道瓶颈、零初始化。代码用 PyTorch 写注释里标了几个容易写错的地方。import torch import torch.nn as nn class RegionNonLocal(nn.Module): def __init__(self, in_channels, bottleneck_ratio2, num_regions(4, 4), temporal_regions1, norm_layernn.GroupNorm): in_channels: 输入通道数 C bottleneck_ratio: 通道瓶颈倍数实际通道为 C // bottleneck_ratio num_regions: (g_h, g_w) 空间划分网格 temporal_regions: 时间维划分段数 g_t1 表示不切时间 super().__init__() inner max(in_channels // bottleneck_ratio, 8) self.g_h, self.g_w num_regions self.g_t temporal_regions self.R self.g_h * self.g_w * self.g_t self.theta nn.Conv3d(in_channels, inner, 1, biasFalse) self.phi nn.Conv3d(in_channels, inner, 1, biasFalse) self.g nn.Conv3d(in_channels, inner, 1, biasFalse) self.out nn.Conv3d(inner, in_channels, 1, biasFalse) # 归一化放在相似度之后、聚合之前更稳 self.norm norm_layer(inner) # 关键输出投影权重置零保证训练初期为恒等映射 nn.init.zeros_(self.out.weight) def forward(self, x): B, C, T, H, W x.shape # 空间维必须能被网格整除否则 reshape 会报错 assert H % self.g_h 0 and W % self.g_w 0, \ fH{H}, W{W} 无法被网格 {(self.g_h, self.g_w)} 整除 assert T % self.g_t 0, fT{T} 无法被 g_t{self.g_t} 整除 theta self.theta(x) # (B, inner, T, H, W) phi self.phi(x) g self.g(x) inner theta.shape[1] Th, Tw H // self.g_h, W // self.g_w Tt T // self.g_t n_r Tt * Th * Tw # 每个区域内的位置数 def to_regions(t): # (B, inner, T, H, W) - (B, R, inner, n_r) - (B, R, n_r, inner) t t.view(B, inner, self.g_t, Tt, self.g_h, Th, self.g_w, Tw) t t.permute(0, 2, 4, 6, 1, 3, 5, 7).contiguous() t t.view(B, self.R, inner, n_r) return t.permute(0, 1, 3, 2) # (B, R, n_r, inner) theta_r to_regions(theta) # (B, R, n_r, inner) phi_r to_regions(phi) g_r to_regions(g) # 相似度(B, R, n_r, n_r) sim torch.matmul(theta_r, phi_r.transpose(-1, -2)) sim sim / (inner ** 0.5) # 缩放防止 softmax 饱和 attn torch.softmax(sim, dim-1) # 在区域内做归一化 # 聚合(B, R, n_r, inner) y torch.matmul(attn, g_r) # 归一化需要把 (B,R,n_r,inner) 转成 (B,inner,R,n_r) 才能用 Conv/GroupNorm y y.permute(0, 3, 1, 2).contiguous().view(B, inner, self.R, n_r) y self.norm(y) y y.view(B, inner, self.g_t, self.g_h, self.g_w, Tt, Th, Tw) y y.permute(0, 1, 5, 3, 6, 2, 4, 7).contiguous() y y.view(B, inner, T, H, W) y self.out(y) return x y几个必须注意的地方。第一to_regions里的 permute 顺序非常容易写反一旦写反区域虽然数量对了但区域内包含的位置是错的训练能跑通、精度就是上不去这种 bug 最难查。建议写完先做一次形状和内容的断言测试构造一个通道全为索引的图像检查每个区域内的元素是否符合预期。第二缩放因子我用的是inner ** 0.5比论文里的1/N归一化更稳因为 softmax 本身已经做了归一化再乘1/N会双重缩放导致梯度过小。第三n_r在时间划分时可能很小如果n_r 16建议直接关掉这个模块或者换更粗的划分。3.3 主干装配与训练配置装配时有一个顺序问题模块应插在残差块之后、激活之前还是激活之后我试下来放在残差块的输出之后、ReLU 之前略好一点因为 ReLU 会丢掉负值信息而相似度计算对特征的符号是敏感的。如果主干用的是 BN-ReLU-Conv 的预激活结构就插在每个 stage 的最后一个 block 之后。训练配置上加了区域化模块之后建议的学习率策略是主干部分用原基线的 0.5 倍新增模块用基线的 1 倍也就是给新模块更高的学习率因为它初始为 0需要尽快离开零态。warmup 步数可以保持原设置不变因为我们有零初始化兜底不需要额外靠 warmup 来抑制初期噪声。权重衰减方面有一点小经验模块里的 1×1×1 卷积不加权重衰减效果略好因为这些参数数量少、且其作用是线性变换衰减会把它们压向 0等价于削弱模块本身的作用。这个结论来自我在两个数据集上的对比不算普适但值得一试。批大小方面因为显存被省下来了可以把 batch 提上去这对 BN 类主干和对比类损失都有好处。以刚才的 8×28×28 配置为例区域化之后单卡可以塞下原来 4 倍左右的 batch训练稳定性提升很明显。3.4 显存与 FLOPs 的实测核算光手算不够得实测。我在一张常规卡上跑了对照输入 (8, 256, 8, 28, 28)分别测全量和 R16 的前向与反向峰值显存方案前向峰值反向峰值单层前向耗时相对开销无模块基线0.9GB1.6GB8ms1.0x全量 Non-local3.1GB显存溢出无法完成-区域化 R161.1GB1.9GB12ms1.5x区域化 R361.0GB1.8GB11ms1.4x结论很直观区域化的开销相对基线只有 40% 到 50%而全量方案在这个配置下直接跑不动。这个增量换来的时序建模能力比再加一个 3D 卷积块划算得多因为后者在相同开销下只能扩大 1 个像素的感受野。FLOPs 的核算可以用一个简单公式快速估算模块总乘加 ≈ 3×N×C×C (三个变换) 2×N²×C/R (相似度与聚合) N×C×C (输出投影)。代入 N6272、C256、C128、R16三项分别是 6.2亿、6.3亿、2.1亿合计约 14.6 亿。对比主干几十亿的规模占比在 10% 上下是可以接受的。4. 训练调参与效果验证的实录4.1 划分粒度与层数的组合实验区域数和插入层数是两个强耦合的超参分开调很容易得出错误结论。我做了一轮网格实验固定其他配置只变这两个看验证集精度的变化趋势区域数 R插入层数每层相对开销验证精度变化921.6x1.21621.5x2.11642.2x2.43621.4x1.93642.0x2.26441.8x1.5趋势很清楚R 从 9 增加到 16 收益明显因为区域内位置数从近 700 降到 392聚合的语义更聚焦再往 36、64 走收益开始回落因为区域太小跨区域的信息传递需求上升而层数不够时传不过去。层数从 2 加到 4 有稳定增益但边际递减配合 R 增大的组合比如 R64 加 4 层反而不如中等区域数说明“区域太大算不动、区域太小连不上”这个两端都不甜。我的建议是算力紧就 R16 加 2 层算力宽松就 R16 加 4 层不要盲目追大 R。这个结论在不同主干上基本成立但具体数值会浮动建议自己在验证集上扫一遍。4.2 短片段与长片段的差异化表现区域化 Non-local 的收益对片段长度非常敏感。我在 8 帧、16 帧、32 帧三档上做了对比结论是收益在 16 帧左右最大8 帧时收益有限32 帧时收益又开始下降。原因不难理解。8 帧时全量 Non-local 的开销也不算离谱区域化省下的算力换来的表达损失相对明显16 帧时全量方案已经很难训区域化的性价比最高32 帧时 N 太大即使区域化区域内的位置数也膨胀到 1500 以上相似度矩阵又开始变贵同时长片段里的语义漂移让“区域内全是同一语义”的假设变弱聚合质量下降。如果确实要处理长片段有一个补丁在时间维做分层划分前几层用 g_t4 的大区域捕捉长程关系后几层用 g_t1 的小区域做精细建模。这种多尺度组合我在一个长视频任务上试过比单一配置稳定代价是要调的超参变多。注意换片段长度时一定要重新校准划分参数直接沿用旧配置是最常见的性能陷阱。N 变了N_r 就变了区域内的语义构成完全不一样。4.3 与基线的端到端对比观察除了绝对精度我更关注几个诊断指标。第一个是训练初期的 loss 曲线有零初始化时应该和基线几乎重合如果明显更高说明零初始化没生效或者 permute 写错了。第二个是模块输出的量级训练稳定后模块输出的 L2 范数相对于输入应该在 0.1 到 0.5 之间太小说明模块没学到东西学习率太低或者瓶颈太窄太大说明它在主导特征可能压制了主干的判别能力。第三个是相似度矩阵的熵如果熵极低接近 one-hot说明区域划得太小或者缩放因子不合适。这三个指标我在每个项目里都会盯一遍比只看最终精度有效得多因为它们能在训练早期就暴露问题省下大量试错时间。5. 常见问题与排查技巧实录5.1 症状、原因与处理的速查表以下是我和周围同学在实际实现中反复遇到的坑整理成速查表遇到问题时按症状对照症状最可能原因处理方式reshape 报错维度不匹配H/W/T 不能被网格整除加 padding 或改网格尺寸务必加 assert训练能跑精度不如基线to_regions 的 permute 顺序写反用索引张量做内容断言测试第一个 epoch loss 剧烈震荡输出投影未零初始化把 out 权重和后续 BN 的 gamma 都置 0训练后期精度停滞学习率对新模块过低新模块用 1 倍学习率主干 0.5 倍显存仍然溢出区域数太小或未加通道瓶颈R 提到 16 以上瓶颈降到 C/4相似度矩阵接近 one-hot区域内位置数太少减小 R 或增大特征图分辨率小 batch 下训练不稳定模块内用了 BatchNorm换 GroupNorm长片段效果反而变差区域内位置数膨胀、语义漂移时间维分层划分或多尺度组合5.2 几条文档里不会写的实操心得第一条先做形状断言再做效果调优。我见过太多人花两天调超参最后发现是 reshape 顺序错了。建议在模块里加一个调试开关打开时构造特殊输入做内容校验比如让输入在通道维上编码位置索引前向之后检查每个区域内的元素是否来自预期的位置集合。这一步五分钟就能做完能省两天。第二条通道瓶颈优先于区域数调整。当显存不够时很多人的第一反应是把区域数加大但这会让区域内的语义过于碎片化。先把瓶颈从 C/2 降到 C/4算力降一半而表达损失很小往往比加大 R 更划算。第三条模块不要插得太密。我一开始在每个 stage 后面都插了一层结果训练时间翻倍、精度只涨了 0.3。后来改成只在 stage3、stage4 后面插精度基本不变训练时间省了三分之一。这个模块的作用是补长程关系相邻两层补的是同一段关系冗余度很高。第四条验证时留意“区域边界效应”。区域划分是硬切分区域边界附近的位置无法和邻居交互这在空间上表现为网格状的伪影。如果任务对空间精度敏感比如分割类的下游可以在划分时加一个小的重叠区或者用两层不同偏移的划分交替插入让边界位置在某一层里处于区域内部。这个技巧我用在时序定位任务上边界处的定位误差有明显下降。第五条别迷信论文里的默认超参。区域数、瓶颈比、插入位置这三个参数跟主干预训练方式强相关从 2D 主干膨胀来的模型和从头训的模型最优配置能差一倍。老老实实扫一遍比照搬数值靠谱。6. 从视频分类回看 CNN 人脸识别的可复用经验6.1 两条线共享的结构性直觉把区域化 Non-local 和基于 CNN 的人脸识别放一起看会发现几处惊人的相似。人脸识别里把特征图切成条带、分区提特征再融合和这里把时空特征切成区域、在区域内做聚合本质是同一种归纳偏置先用局部结构降低建模难度再靠层级或层间传递补上全局性。人脸识别里的注意力机制比如对关键部件的加权和这里的相似度加权聚合数学形式几乎一样只是前者的权重来自通道或空间注意力后者来自位置间的点积相似度。更值得注意的是初始化这件事。人脸识别训练里接在预训练主干后面的新分类头或度量头如果随机初始化前若干步会把主干特征带偏所以常见做法是把新头的最后一层置零或用很小的初始化。区域化 Non-local 的输出投影零初始化是完全相同的思路。这类“保护预训练特征”的技巧跨任务通用我建议把它当成一个默认习惯而不是针对某个模块的补丁。还有一点是归一化和缩放。人脸识别里对嵌入做 L2 归一化再用温度系数缩放本质上是在控制相似度分布的尖锐程度避免 softmax 饱和导致梯度消失。区域化 Non-local 里对相似度除以sqrt(inner)、调整 softmax 温度解决的是同一个问题。理解了这一层调参时就不会盲目试数值而是知道自己在调什么。6.2 迁移到其他任务时的注意点如果你手上有一个成熟的 CNN 人脸识别流程想把这套区域化聚合的思路搬过去做视频人脸相关任务比如视频中的人脸质量评估、跨帧身份一致性判断有几个点要提前想清楚。第一位置的语义变了。人脸识别里的“位置”是空间像素位置语义明确视频里的“位置”是时空位置同一个语义比如眼睛在不同帧里的坐标会漂移硬划分区域会把同一个部件切到不同区域里去。解决办法是先做人脸对齐和跟踪把跨帧的同一部件尽量对齐到相同的空间坐标再划分区域这样区域内的语义一致性会好很多。第二位置数不能太大。视频人脸任务里如果直接用全分辨率特征图N 会非常大。建议先把人脸区域裁剪并对齐到固定尺寸比如 112×112再下采样到 28×28帧数控制在 8 到 16这样 N 在 6000 量级正好落在区域化方案最舒服的区间。第三监督信号的性质决定了区域粒度。人脸识别靠的是判别性度量损失需要特征分布足够分散视频任务靠的是分类或回归损失需要特征足够聚合。前者更适合细粒度区域加度量损失后者更适合中等粒度区域加分类损失。混用会互相打架我试过一次效果不如分开处理。最后分享一个小技巧如果你不确定该用区域化还是全量可以先在极小分辨率比如 14×14、4 帧上跑全量做上界参考再在同一配置下跑区域化两者的差距就是区域化带来的表达损失。这个差距如果小于 1 个点说明区域化在你的任务上几乎没有代价可以放心用如果差距明显就得考虑调整划分策略或者加重跨区域的传递路径。这一步花不了多少时间但能让你对自己的配置心里有数而不是靠感觉拍板。