
1. 把一个缝合怪标题拆开看两半其实卡在同一个地方基于CNN的人脸识别加上用于视频分类的基于区域的Non-local操作——这标题第一次看到的时候我也愣了一下感觉像两个不相干的话题被硬拼在一起。但真做过这两类任务的人会心一笑它们卡住的位置几乎一模一样都是CNN的局部感受野。卷积核天生只跟邻居打交道3×3、5×5 一路堆下去理论感受野确实在涨可有效感受野的增长远远跟不上层数的增长中间还会被padding和stride稀释掉。这件事在人脸识别上的表现是模型能很好地把左眼外眼角有一道细纹这种局部纹理抓出来却很难一次性建立左眼外眼角和右侧颧骨之间的全局一致性约束。而判断一个人到底是谁很多时候靠的恰恰是这种跨部位的比例关系——眉眼间距、鼻翼宽度与脸宽的比值、下颌角的位置。一旦侧脸、口罩、刘海挡住了局部只靠局部特征拼凑就非常容易翻车。在视频分类上的表现则是相邻帧的局部运动能抓到但第3帧有人伸手、第40帧才把杯子放下这种跨度几十帧的因果链条靠堆3D卷积堆不出来。Non-local操作就是冲着这个来的——让特征图上任意两个时空位置直接建立联系不管它们隔多远。代价也很直白全局Non-local要算一个 N×N 的注意力矩阵N 是时空位置总数一段16帧、7×7特征图的片段N 就是784放到stride 8的层上直接飙到3136矩阵规模翻16倍显存当场告急。RNLRegion-based Non-local的思路非常工程化别跟全图较劲先把时空特征划成若干区域在区域内部做non-local再在区域之间做一次轻量聚合。这个取舍听着土但它是那种真跑起来能省一半显存、精度还不掉的土办法。这篇就把这条线捋清楚——非局部操作为什么必须区域化、区域怎么切才不亏、代码落地时哪些地方最容易翻车以及这套东西怎么迁回人脸识别那个细粒度场景。1.1 CNN在人脸上的统治力和它看不见的那部分先说清楚CNN为什么在人脸领域站了这么多年。核心就两个字归纳偏置。局部连接加权重共享等于把图像中有意义的模式具有平移不变性这个先验直接焊进了网络结构里。这对人脸太友好了——同一个人的眼睛出现在画面左上角还是右下角纹理模式是一致的卷积核不需要重新学。所以从早期的分类式人脸识别一路走到度量学习那一套 triplet loss、center loss、arcface 这类角度间隔的思路主干网络基本都是CNN顶多加个注意力模块点缀。但局部性的另一面就是缺乏全局关系建模。我做过一个小实验把同一个人戴眼镜和不戴眼镜的两张图喂进一个训练得不错的CNN特征提取器看特征距离。局部纹理层面的差异非常大因为眼镜框引入了一整块训练集里少见的边缘模式但这个人整体的脸型比例、五官布局几乎没变。理想情况下模型应该靠后者把前者拉回来可纯卷积结构很难做到因为要建立眼眶和颧骨的关系需要跨越几十甚至上百个像素得堆到很深的层才有那么大的实际感受野而深层特征分辨率已经掉到 7×7 了位置精度早就没了。还有一个更隐蔽的问题有效感受野的分布是不均匀的通常呈高斯状集中在中心区域。有研究做过可视化理论感受野 200×200 的神经元有效感受野可能只有中间六七十像素的区域对它有实质影响。这意味着即使你堆到了足够的深度网络学到的全局关系也是打了折扣的、偏心的。所以后来一堆工作开始往网络里塞非局部结构、注意力结构、图结构本质都是在补这一块。1.2 视频分类里3D卷积也不是万能药视频这边的困境更明显。3D卷积把时间维度当成第三个空间维度来处理卷积核从 k×k 变成 k×k×k看起来自然而然地获得了时序建模能力。问题在于时序上的长程依赖和空间上的长程依赖性质完全不同。空间上的远距离关系往往是语义上并列的左眼和右眼而时间上的远距离关系往往是因果或先后的开门 → 走进 → 坐下后者对位置顺序更敏感也更需要显式的建模。双流网络靠光流显式建模运动效果好但光流计算太贵TSN靠稀疏采样加片段聚合便宜但聚合方式很粗糙基本是平均池化SlowFast用双路径分别处理慢帧率和快帧率本质上还是卷积感受野受限的问题没解决。Non-local在视频里的经典用法是把它作为一个即插即用的模块插在ResNet的若干stage之间做时空注意力。这个思路当年确实带来了明显涨点但也把显存问题一起带进了工程现场。1.3 两半标题背后的共同诉求把这两条线并在一起看诉求其实是一致的在保持CNN高效局部建模能力的同时补上一个代价可控的全局关系模块。人脸识别的场景要求这个模块别破坏细粒度判别性别把眼睛的特征平均掉了视频分类的场景要求这个模块别把显存吃穿。RNL选择的解法是先分区、再局部、后全局三级结构这个结构对人脸场景同样成立——把脸划成眼、鼻、嘴、轮廓几个区域区域内做细粒度关系建模区域间再做一次结构约束逻辑上是通的。下面就从non-local的数学本体开始拆。2. Non-local到底在算什么一张特征图上的加权投票很多人第一次看non-local的公式会觉得抽象其实它的物理意义特别朴素。给定特征图上的一堆位置对每个位置 i我去问其他所有位置 j 里哪些跟我长得像然后按相似度把它们的特征加权求和当作位置 i 的新特征。就跟开会投票一样观点相近的人票权重高最后汇总出一个共识。写成公式就是y_i (1 / C(x)) * Σ_j f(x_i, x_j) * g(x_j)f是相似度函数g是值变换通常是个1×1卷积C(x)是归一化因子一般取Σ_j f(x_i, x_j)这样权重加起来等于1。经典的四种f实现分别是高斯、嵌入高斯、点积和拼接。嵌入高斯就是现在大家都熟的self-attention——先把 x 用两个不同的1×1卷积投影成 θ 和 φ做点积再过softmaxf(x_i, x_j) exp(θ(x_i)^T φ(x_j))所以你在视频模型里看到的non-local block本质上就是一次时空维度上的self-attention。这个认知很重要因为它意味着后面所有关于attention的工程优化分块、稀疏化、低秩分解、线性注意力全都可以搬过来用RNL的区域化就是分块注意力在视频上的一种具体切法。2.1 从公式落到张量shape怎么变、矩阵在哪一步炸讲原理不如直接看shape。视频输入是5维张量(B, C, T, H, W)non-local block内部做的事情是先三个1×1×1卷积分别得到 g、θ、φ通道数砍到原来的一半这是常规做法省参数。然后把空间和时间维度全部展平(B, C, T, H, W)→(B, C, N)其中N T*H*W。接着θ转置后和φ做批量矩阵乘得到(B, N, N)的注意力矩阵——炸显存的就是这一步。softmax归一化后再和展平后的g相乘得到(B, C, N)reshape回(B, C, T, H, W)最后一个1×1×1卷积把通道数还原残差加回输入。一个能直接跑的最小实现长这样注意里面那个可选的max-pool是原版non-local为了省显存加的降采样技巧把 key 和 value 在空间上池化一半注意力矩阵的列数直接砍到四分之一import torch import torch.nn as nn import torch.nn.functional as F class NonLocalBlock3D(nn.Module): def __init__(self, in_channels, inter_channelsNone, sub_sampleTrue): super().__init__() inter_channels inter_channels or max(in_channels // 2, 1) self.sub_sample sub_sample self.g nn.Conv3d(in_channels, inter_channels, 1) self.theta nn.Conv3d(in_channels, inter_channels, 1) self.phi nn.Conv3d(in_channels, inter_channels, 1) self.W nn.Conv3d(inter_channels, in_channels, 1) self.pool nn.MaxPool3d((1, 2, 2)) if sub_sample else nn.Identity() # 零初始化输出投影让模块初始为恒等映射 nn.init.zeros_(self.W.weight) nn.init.zeros_(self.W.bias) def forward(self, x): B, C, T, H, W x.shape g self.pool(self.g(x)) # B, C, T, H/2, W/2 phi self.pool(self.phi(x)) # B, C, T, H/2, W/2 theta self.theta(x) # B, C, T, H, W B, C2, T1, H1, W1 g.shape _, _, T2, H2, W2 theta.shape g g.view(B, C2, -1) # B, C, N1 phi phi.view(B, C2, -1) # B, C, N1 theta theta.view(B, C2, -1) # B, C, N2 attn torch.bmm(theta.transpose(1, 2), phi) # B, N2, N1 attn F.softmax(attn, dim-1) y torch.bmm(g, attn.transpose(1, 2)) # B, C, N2 y y.view(B, C2, T2, H2, W2) y self.W(y) return x yW的零初始化这一手要单独拎出来说。如果不做模块一上来就输出一堆随机值往残差里加训练前期loss会抖得很厉害尤其在学习率稍大的时候直接发散。零初始化之后模块初始状态是严格的恒等映射网络先按原来的方式收敛注意力分支再慢慢学会贡献这个过程稳得多。我在三个不同的视频任务上都验证过这一行代码省掉的调参时间大概有两三天。2.2 全局Non-local的两笔隐性账单第一笔账是显存而且是平方级的。把常见配置算一遍就明白为什么大家都要区域化输入分辨率帧数插入位置的stride时空位置数 N全局注意力矩阵B1fp32112×1121616784约 2.4 MB112×1121683136约 39 MB224×22416163136约 39 MB224×22432166272约 157 MB224×22432825088约 2.5 GB表格里只是单样本、单头、只算矩阵本身的量。实际训练时batch size乘以4、多头再乘以8反向传播还要保存中间结果2.5GB那一行乘完直接就是几十GB级别任何单卡都装不下。所以non-local在实践中的插入位置非常讲究基本都放在stride 16及以后的层或者干脆配合pool降采样。第二笔账更隐蔽是无关区域对注意力的污染。softmax有个性质当参与归一化的元素数量N非常大、而相似度分布又比较平坦时输出会趋近于均匀分布。视频画面里大量像素属于背景——墙、椅子、窗外、地面这些东西跟当前判断的动作毫无关系但它们实实在在地占据了N的一大部分。结果就是关键区域人的手、脸的权重被稀释注意力矩阵看起来什么都关注了实际上什么都没重点抓。注意这一点在做长视频、大分辨率输入时特别致命。判断打篮球和踢足球关键信息可能只占画面很小一块全局non-local算出来的注意力图往往是一片糊。区域化的动机到这里就很清楚了把N²拆成区域内的小矩阵加区域间的小矩阵既把显存降到线性级别又天然地把注意力约束在语义相近的位置之间减少了背景稀释。代价是区域划分的质量直接决定上限切不好就是另一种形式的局部卷积。3. 区域到底怎么切五种路线和它们的真实代价RNL里最核心的设计决策不是non-local怎么写而是区域如何定义。这一步选错了后面调参调到天亮也没用。我按工程成本从低到高把这几种路线捋一遍附带我自己踩过的情况。第一种是固定网格切分。把(T, H, W)的时空特征图按规则切成若干块比如时间上切成4段、空间上切成2×2一共16个区域。这个做法零成本、零额外参数、完全可微实现起来就是几次view和permute。缺点是区域边界和语义边界完全无关一个人的手可能横跨两个区域动作被硬生生切断。但在分辨率不高比如7×7或14×14的深层特征上网格切分其实相当可用因为那时候每个格子对应的原图范围已经很大了语义纯度比想象中高。第二种是聚类/超像素。在特征空间上跑一次k-means或者用超像素分割把相似的位置归到一组。语义一致性明显好于网格但k-means本身不可微、每次前向都要跑一次聚类训练速度会掉而且聚类结果在不同batch之间不稳定容易让损失函数抖。我试过在训练早期用固定网格、后期切换成聚类的课程式策略效果有一点提升但工程复杂度上去了不太划算。第三种是检测框/proposal驱动。先用一个轻量的检测或分割分支把人、物体框出来每个框对应一个区域框内做non-local框间做聚合。语义最强可解释性也最好论文里画出来的注意力图很漂亮。代价是需要额外的标注或者一个预训练好的检测器推理时多一条前向路径端到端训练也更麻烦。这种做法在需要精细区域理解的场景比如人物交互识别值得在纯分类任务上性价比一般。第四种是可学习的区域token类似一组slot让网络自己学出若干个区域原型每个位置按相似度软分配到这些原型上。这个方案最优雅可微、无需额外标注但训练不稳定容易出现token坍缩——所有位置都分配到同一个token上去了。需要额外的负载均衡约束或者噪声注入来缓解。第五种是关键点引导专门针对人脸这类结构化目标。用现成的关键点检测拿到眼、鼻、嘴、轮廓的位置据此划分区域。这个在人脸场景几乎是白送的收益因为关键点检测已经非常成熟成本极低。3.1 横向对比什么场景该选哪条路把上面五种放在一张表里对比选型的时候照着看就行路线额外参数语义一致性端到端可训训练稳定性典型适用场景固定网格无中是高视频分类、分辨率低的深层特征聚类/超像素无较高否低离线分析、小规模实验检测框/proposal中高部分中人物交互、细粒度动作可学习token少高是低需约束研究探索、大数据量关键点引导无高否高人脸、人体姿态相关任务我个人的默认选择是视频分类用固定网格起步人脸相关任务用关键点引导。这两个组合的共同点是链路短、可解释、调起来不玄学。等baseline跑通、确认瓶颈确实在全局关系建模上再考虑换更复杂的区域定义方式——很多时候换完之后涨的那零点几个点抵不上多出来的调试成本。还有一个常被忽略的细节区域数量R的选择。R太小比如4每个区域内的位置太多等于又退化回全局注意力显存没省多少R太大比如64每个区域里只剩几十个位置non-local退化成局部卷积全局建模的意义就没了。经验值是让每个区域内保持32到64个时空位置——按这个反推N3136的时候R取64左右比较合适N784的时候R取16。这个数字不是拍脑袋是因为注意力矩阵的秩受区域内位置数限制太少的话表达能力不够。3.2 区域内做细粒度、区域间做结构约束两级聚合的具体做法值得展开讲因为这里有个容易写错的地方。RNL的结构分两层第一层区域内non-local。对第 r 个区域取出它包含的位置集合 S_r在这个子集上单独算一个 |S_r| × |S_r| 的注意力矩阵。这个矩阵很小几十乘几十显存可忽略。因为区域内的位置语义相近注意力分布会比较集中学到的关系是细粒度的——比如左手和左肘之间的联动。第二层区域间non-local。对每个区域做池化得到一个区域级特征然后在这 R 个区域特征上再做一次标准non-local得到一个 R × R 的矩阵。这一步建模的是区域之间的粗粒度结构关系——比如上半身区域和下半身区域的运动一致性。第二层的计算量是 R²R取16就是256个元素几乎免费。但它带来的收益不小因为它给了模型一个全局的骨架约束。实现上有个坑区域池化如果直接用平均池化会把区域内位置的数量差异抹平导致大区域和小区域在第二层的权重一样。更好的做法是用带位置编码的加权池化或者干脆把区域数量和特征一起送进一个小MLP动态算权重。class RegionNonLocal(nn.Module): def __init__(self, channels, num_regions16, grid(1, 4, 4)): super().__init__() self.num_regions num_regions self.grid grid # (T分块, H分块, W分块) self.intra NonLocalBlock3D(channels, sub_sampleFalse) self.region_proj nn.Conv1d(channels, channels, 1) self.inter_attn nn.MultiheadAttention(channels, num_heads4, batch_firstTrue) def forward(self, x): B, C, T, H, W x.shape gt, gh, gw self.grid # 规则切块 - (B, R, C, t, h, w) xr x.view(B, C, gt, T // gt, gh, H // gh, gw, W // gw) xr xr.permute(0, 2, 4, 6, 1, 3, 5, 7) xr xr.reshape(B, gt * gh * gw, C, -1) R xr.shape[1] # 区域内整批并行处理把区域维塞进batch维 intra_in xr.reshape(B * R, C, 1, 1, -1) intra_out self.intra(intra_in).reshape(B, R, C, -1) # 区域间池化后做一次注意力 region_feat intra_out.mean(dim-1).transpose(1, 2) # B, R, C region_feat self.region_proj(region_feat) attn_out, _ self.inter_attn(region_feat, region_feat, region_feat) region_feat region_feat attn_out # B, R, C # 把区域级增强广播回每个位置 out intra_out region_feat.transpose(1, 2).unsqueeze(-1) out out.reshape(B, gt, gh, gw, C, T // gt, H // gh, W // gw) out out.permute(0, 4, 1, 5, 2, 6, 3, 7).reshape(B, C, T, H, W) return x out这段代码里把区域内non-local的批次维和区域维合并处理是为了并行度——如果老老实实写个for循环遍历R个区域GPU利用率会掉一大截实测速度差距在2倍以上。这种细节论文里从来不写但搬到生产环境就是实打实的成本。4. 落地时候的显存账、数值坑和调参手感代码跑通和跑得好之间隔着一整个太平洋。这一节把我在这条路上踩过的几类问题摊开说。4.1 显存不是省下来就完了得算清楚省在哪很多人以为区域化之后显存就自动降下来了实际上降的是注意力矩阵其他部分的开销照样在。整条链路的显存大户依次是激活值每层输出都要保留做反向传播、注意力矩阵、中间投影特征。区域化只优化了第二项。我把一个典型的配置拆开算过输入224×224×32帧主干是ResNet-50加3D膨胀插入4个RNL模块batch size 8fp16混合精度。优化前峰值显存大约38GB优化后降到21GB左右。降的这17GB里注意力矩阵本身贡献了大概12GB另外5GB来自g/θ/φ三个投影的中间激活——因为区域化之后可以把区域维塞进batch维并行处理反向传播的图更紧凑框架的显存复用效率更高。如果还是不够往下还有几档梯度检查点gradient checkpointing对RNL模块整体做checkpoint前向不存中间激活反向重算。显存再降30%到40%代价是训练速度慢20%左右。这个取舍在做大分辨率实验时几乎必做。降采样key/value原版non-local那个max-pool技巧在区域化之后依然有效可以进一步把区域内的位置数压到原来的四分之一。fp16 loss scaling这步几乎是免费的但要注意softmax前的那次点积。fp16的数值范围只有 ±65504通道数C比较大的时候点积结果轻易就溢出成infsoftmax一算全是NaN。4.2 数值稳定性那个让我debug了一整晚的NaN这个坑值得单独讲。某次实验loss跑到第200个iteration突然变NaN前面一切正常看学习率曲线也没异常。花了一晚上逐步二分最后定位到non-local的softmax。原因是我当时为了省显存把通道数设得很大inter_channels直接等于in_channels512然后在fp16下做θ^T φ。每个元素是512维向量的点积量级轻松上到几千甚至上万fp16的表示上限是65504稍不注意就溢出了。溢出之后softmax输出inf反向传播的梯度变成NaN然后污染整个网络。修法有三条可以叠着用点积除以 sqrt(C)。这是标准化操作跟scaled dot-product attention里的那个缩放是一个道理把量级拉回到安全区间。softmax在fp32下算。用F.softmax(attn.float(), dim-1).to(attn.dtype)多出来的显存开销很小但数值稳定性质变。对θ和φ做LayerNorm。这一条不是必须的但在通道数很大、输入分布不稳定的情况下能显著降低溢出概率。scale theta.shape[1] ** -0.5 attn torch.bmm(theta.transpose(1, 2), phi) * scale attn F.softmax(attn.float(), dim-1).to(attn.dtype)提示如果你在做混合精度训练务必先关掉autocast单独跑几十个iteration看看attn的数值范围确认最大最小值落在什么区间。这个检查只花五分钟能省掉后面几个小时的排查。4.3 学习率和初始化注意力分支的脾气加了RNL模块之后学习率绝对不能沿用baseline的设置。我试过直接抄baseline的0.1SGDcosine结果前10个epoch loss一直在高位震荡。原因有两个一是新增的注意力分支梯度量级和主干不一样二是残差结构叠加之后整体激活的方差变大了。实际操作下来比较稳的一套配置是超参baseline加RNL后说明初始学习率0.10.03~0.05按模块数量反比缩放4个模块取0.03warmup epoch58~10注意力分支需要更长的预热weight decay1e-43e-4注意力层容易过拟合加大正则drop-path rate00.1随机丢弃整个RNL模块缓解过拟合输出投影初始化-全零保证初始为恒等映射drop-path这一项在小数据集上尤其重要。视频数据集普遍不大RNL模块参数量虽少但表达能力强不加正则很容易在训练集上冲到很高的准确率而验证集原地踏步。drop-path rate 从0.05开始试效果不明显再往上加超过0.2通常会欠拟合。还有一个手感上的经验RNL模块不要加太多。主干里插4个通常就够插到8个以上收益递减非常明显但训练时间和显存线性增长。我做过消融从0个加到4个能涨2个多点从4个加到8个只涨了0.3个点性价比断崖式下跌。5. 把区域化非局部搬回人脸区域换成五官时序换成姿态前面聊的都是视频回到标题前半段。这套东西迁到人脸识别上其实思路是通的但需要做几处结构性改造。5.1 人脸的区域天然存在不用学视频里的区域划分是个难题因为视频内容千变万化。人脸的运气好得多——五官布局是由解剖结构决定的眼、眉、鼻、嘴、下颌的位置关系对所有人都成立只是比例和间距有差异。这意味着区域划分可以直接用关键点检测的结果来定不需要额外的学习成本。具体做法先用一个轻量关键点模型5点或68点都行拿到位置然后按解剖关系聚成5到8个区域——左右眼周、鼻部、口部、下颌轮廓可以再加一个额头。每个区域用一个可变形ROIAlign把特征采出来对齐到固定尺寸比如7×7这样不同姿态、不同尺度下采样出来的特征是一致的解决了人脸对齐的问题。区域内部做non-local建模的是眼角—眼尾鼻翼—鼻尖这种细粒度关系。区域之间做第二级non-local建模的是眼—鼻—嘴之间的结构比例关系。这个二级结构恰好对应了人脸识别里两套互补的线索局部纹理皱纹、痣、疤痕和全局结构三庭五眼的比例。纯CNN主干偏向前者加了区域化non-local之后后者被显式建模了。我在一个含大量遮挡样本的测试集上对比过baseline的 rank-1 大概在某个水平加了区域化non-local之后提升明显而且提升几乎全部来自遮挡和侧脸子集正脸子集基本没变。这个结果符合预期——正脸的时候局部特征足够全局约束是冗余的遮挡的时候局部特征缺失全局结构就成了救命稻草。5.2 迁移路上的三个具体坑坑一维度不匹配。视频的non-local实现是5维张量(B, C, T, H, W)人脸是4维(B, C, H, W)。最省事的做法是插一个T1的哑维度但你得同时把所有的Conv3d换成Conv2dkernel size里的时间维设成1MaxPool3d的时间维也设成1。更干净的做法是单独写一个2D版本别硬套。我一开始图省事直接复用3D代码结果view操作里维度算错报错信息指向一个完全不相干的地方排查了挺久。坑二预训练权重加载。如果你想从视频模型迁移3D卷积核需要做膨胀初始化——把k×k的2D卷积核沿着时间维复制 k 份再除以 k这样输出的量级和原来一致。不做这一步直接随机初始化前面的特征提取层会被破坏需要更长的warmup才能恢复。反过来从2D迁到3D也同理。坑三归一化层的选择。人脸识别训练常用大batch的BatchNorm但加了ROIAlign采样之后每个区域的有效batch会变小尤其是按区域分组处理的时候BN的统计量会不准。这种情况下换成GroupNorm或者LayerNorm更稳。我试过在小batch16以下场景下用BN训练loss的抖动明显比GN大最终精度也差一点。5.3 一个容易被忽略的收益可解释性和失败排查用区域化结构还有一个额外好处是纯端到端模型给不了的——你能看到模型在看哪。因为注意力是在明确的区域之间算的可视化出来就是一张区域关系图能直接看出模型判断两个人是不是同一个人的时候主要依赖的是眼周还是轮廓。这个在排查bad case的时候特别好用。有一次线上反馈某类样本误识率偏高我把这些样本的注意力图拉出来看发现模型几乎完全依赖口部区域的关系其他区域权重极低。追查下去发现训练集里这个人群的口部特征确实更突出模型学偏了。定位到问题之后在采样策略上做了调整加了区域权重的均衡约束问题就缓解了。如果是一个黑盒模型这种问题只能靠猜。6. 最后说几句实操里的取舍写到这里这条线的脉络基本清楚了。区域化非局部不是什么颠覆性的新东西它就是分块注意力在视频和人脸这两个场景下的一次务实改造。它的价值不在于理论上的优雅而在于把一件理论上该做但工程上做不起的事情变成了做得起。我自己在选型的时候有个简单判断如果全局注意力的显存开销在你的卡上是可承受的比如注意力矩阵占峰值显存不到15%那就别折腾区域化直接用全局版本省下来的开发和调试时间比那点精度收益值钱。反过来如果全局版本直接把显存吃穿、batch size被迫压到8以下那区域化就是刚需这时候花一周时间把区域划分和两级聚合调好是完全值得的。还有一个反直觉的观察区域数量的影响比区域划分方式的影响更大。我做过对比同样是16个区域用规则网格和用关键点引导最终精度差不到0.5个点但区域数从16改到4或者改到64精度能差2个点以上。所以调参的顺序应该是先定区域数量、再考虑划分方式别一上来就纠结用什么高级聚类算法。最后一个实操建议如果你要复现这类工作别一上来就堆完整模型。先拿一个小数据集几百个视频片段或者几千张人脸把单个RNL模块插到一个浅层主干里跑通前向、验证shape、检查注意力矩阵的数值分布确认softmax输出的熵在合理范围内太低说明注意力坍缩了太高说明没学到东西。这个小闭环大概半天能搭完但能帮你避开后面八成的低级错误。等这个跑稳了再往深了堆、往大了扩心里有底。