ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RNL区域级注意力:从视频分类长程建模到人脸识别迁移

RNL区域级注意力:从视频分类长程建模到人脸识别迁移

基于 CNN 的人脸识别这条路走了很多年,从检测对齐到特征提取再到度量学习,整条工程链早就定型了,想再挤出几个点提升都很费劲。前阵子看到某高校研究团队放出的 RNL,全称 Region-based Non-local,专门用来做视频分类里的长程时空建模,我第一反应不是“又一个注意力模块”,而是这东西能不能反过来用到人脸识别上。原因很直接:基于 CNN 的人脸识别在单帧静态图上的天花板已经很明显了,跨姿态、跨年龄、遮挡这些问题,单帧信息量根本不够,真正的增量在视频序列里。而视频分类恰恰是处理“多帧、长时序、大空间”的老手,RNL 又是其中性价比很高的一种做法。这篇就围绕这两个看似不搭界的方向,把 RNL 的结构拆透,讲清楚它为什么能把 Non-local 的计算量砍下来,再把手写代码、训练配方、踩过的坑一并交代清楚,无论你是做视频理解的还是做人脸识别的,都能直接拿去改。

1. 两条技术线为什么会被放在一个标题里

1.1 CNN人脸识别的工程链路已经固化到什么程度

先把人脸识别这边讲清楚,不然没法解释为什么需要视频分类的技术。一条标准的基于 CNN 的人脸识别流水线,本质是四个环节:人脸检测、关键点对齐、特征提取、度量学习。检测阶段把图里的人脸框出来,对齐阶段用五个点或一百多个点做相似变换,把眼睛、鼻尖、嘴角摆到固定位置,这一步看着不起眼,实际上决定了后面特征的上限。你拿一张侧脸 45 度、没有对齐的图直接塞进网络,跟对齐之后的图相比,特征相似度能差十几个点,我在实际项目里反复验证过。

特征提取环节主流还是 ResNet 系列,50 层、100 层都有人用,输出一个 512 维的 embedding。真正拉开差距的是损失函数,从最早的 Softmax 到后来的角度间隔系列,核心思路就一句话:让同类特征在超球面上靠得更紧,异类之间留出角度余量。间隔参数 m 取 0.5 是个经验值,取大了训练前期收敛极慢,取小了区分度上不去。这一段有个容易被忽略的细节,特征必须做 L2 归一化之后再算角度,否则模长会干扰间隔的作用,很多人复现不出来就是漏了这步。

但整条链路有个共同前提:输入是一张静态图。静态图能提供的信息就那么多,同一个人戴墨镜、戴口罩、低头、逆光,单帧特征退化非常严重。业界的解法是拿多张图做特征融合,可多张图怎么融合、按什么权重融合,这就变成了一个时序建模问题,而时序建模恰恰是视频分类的主场。这就是标题把两条线放一起的底层逻辑,不是硬凑,是需求真的对接上了。

1.2 视频分类卡在长程依赖上很多年了

视频分类的老大难是时空建模。早期的双流法把 RGB 和光流分开训再融合,光流那一支算起来极慢,工程上很难落地。后来 3D 卷积把时间维度直接并进卷积核,一个 3x3x3 的核在时间上只能看 3 帧,想覆盖几十帧的长动作,比如“起身走到门口再回头”,就得靠堆层数,堆到感受野够大时参数量和显存都爆了。

Non-local 操作就是在这个背景下被提出来的。它的思路借自图像领域的自注意力:每一个位置的特征,都和其他所有位置做一次相似度计算,然后按相似度加权求和,这样任意两个位置之间的依赖路径长度都是 1,不管隔多远。数学上写成:

y_i = (1/C(x)) · Σ_j f(x_i, x_j) · g(x_j)

其中 f 是相似度函数,g 是特征变换,C 是归一化系数。把 f 取成嵌入式高斯,就是最常见的实现。这个操作确实有效,插入到 3D 骨干网络中间,动作识别的准确率能涨两三个点,而且不改变特征图尺寸,接起来很省事。

问题出在复杂度上。视频特征图的元素总数 N = T × H × W,一个 16 帧、56×56 分辨率的特征图,N 就是 50176。Non-local 要算一个 N×N 的相似度矩阵,那是 25 亿个元素,一个都存不下。工程上的妥协是只在低分辨率层用,或者把时间空间分开算,但这些都是绕着走,没解决根本矛盾。

1.3 RNL 的真正切入点:语义是成片的,不必逐像素算

RNL 的洞察其实很朴素:自然图像和视频里,语义相近的像素从来不是零散分布的,而是一块一块连成片的。一个人的手臂、一件衣服、一片天空,相邻像素的语义高度一致。既然这样,为什么还要对每一个像素都做一次全局交互?把特征图划成若干区域,每个区域出一个代表向量,在区域级别做注意力,再把结果广播回像素,效果损失很小,算力却能砍掉一个数量级。

这个思路的价值不只在省算力上,还在于它让 Non-local 可以放到更高分辨率的层去用。原来只能在 res4、res5 之后的 14×14 特征图上用,现在放到 28×28 甚至 56×56 也不是不能承受,感受野的收益是实打实的。我自己的实测里,把区域级注意力放在较高分辨率的层,对小目标和细粒度动作的帮助比对大目标更明显。

再往深一层想,区域这个概念本身也提供了额外的归纳偏置。像素级注意力容易过拟合到纹理细节,区域级注意力强制模型关注“块与块之间的关系”,泛化性反而更好一些。这一点在后面做人脸识别迁移时体现得尤其明显,人脸的五官天然就是分区域的,眼睛、鼻子、嘴巴各成一块,区域级建模跟人脸的解剖结构是对得上的。

2. RNL的结构拆解与参数账本

2.1 三步流程:划分、聚合、广播

RNL 的完整流程拆成三步,每一步都有讲究。

第一步是区域划分与聚合。输入特征 x 的形状是 (B, C, T, H, W),先按网格划成 (T, r, r) 个区域,每个区域内做平均池化得到一个 C 维的代表向量。这里用平均而不是最大池化,原因是最大池化只保留最显著的那个响应,会丢掉区域内部的整体统计特性,而后续要算的是区域间的相似度,需要的是区域的整体表征,平均池化更合适。池化完得到形状 (B, C, T, r, r) 的区域特征,拉平成 (B, C, R),R = T × r × r 就是区域总数。

第二步是区域级注意力。对区域特征分别做三个 1×1×1 卷积,得到 θ、φ、g 三路变换,把通道数压到 C/2 减少计算。然后用 θ 和 φ 算相似度矩阵 A,形状是 (R, R),softmax 归一化。这一步的复杂度是 O(R²·C),因为 R 通常只有几十到几百,完全可控。再用 A 去加权 g,得到区域级的输出 (B, C, R)。

第三步是广播回像素。把区域输出上采样回原来的 (T, H, W) 分辨率,这一步用三线性插值就行,因为时间维度的采样点数和原来一样,实际上就是每帧做一次双线性插值。最后接一个 1×1×1 卷积做通道融合,加上残差连接和可学习的缩放系数 γ,γ 初始化为 0,保证训练初期这个分支是恒等映射,不会破坏预训练权重。

这个“γ 初始化为零”的细节非常关键。Non-local 类模块如果一开始就带着随机初始化参与前向,预训练模型的输出分布会被打乱,前几个 epoch loss 会剧烈震荡。我在第一次实现时没注意,loss 曲线前 2000 步像心电图一样,加了零初始化之后立刻平滑。这个技巧现在已经是标配,但自己从零写的时候很容易忘。

2.2 划分策略:固定网格还是可学习聚类

区域怎么划,直接影响效果和工程复杂度,主流有两条路。

固定网格划分是把特征图均匀切成 r×r 个格子,实现上就是一个 adaptive_avg_pool 加 interpolate,没有任何自定义算子,导出、量化、部署都很友好。缺点是网格边界和真实语义边界对不上,一个人的身体可能被切到四个格子里,区域表征就被稀释了。实际用下来,r 取 7 是比较好的平衡点,取 3 太粗,区域内部混杂严重,取 14 又太细,省不下多少算力。

可学习聚类是让网络自己学一组聚类中心,每个像素按软分配权重归到各个中心。理论上更贴合语义,但训练稳定性差,聚类中心容易坍缩到少数几个位置,需要额外的正则项来约束分配熵。我试过一版,前 10 个 epoch 还算正常,后面分配矩阵就退化成近似均匀分布,等于没聚。想走这条路,得配上分配熵约束和中心分离约束,工程量翻倍,收益不一定对得起。

我的建议很明确:先用固定网格把整条流水线跑通,确认精度和速度都有收益,再考虑要不要上聚类。绝大多数场景下固定网格够用了,而且它的计算模式规整,在推理框架里的算子融合效果好得多。真要提升语义贴合度,不如把 r 做成多尺度,同时在 r=5 和 r=7 两个粒度上做区域注意力,然后把两路结果加起来,代价增加不多,效果比单粒度稳。

再说时间维度怎么处理。有两种做法:一是每个时间步独立做空间区域注意力,时间维度完全交给 3D 卷积;二是把 (T, r, r) 整体当作区域集合,让不同时刻的区域之间也能交互。第一种实现简单,但时间上的长程依赖还是没解决;第二种才是 RNL 真正的价值所在,因为一个动作的起承转合往往跨越几十帧,必须让远处的时间片段直接对话。代价是 R 会随 T 线性增长,T=16、r=7 时 R 就到了 784,R² 接近 61 万,还在可接受范围内,但 T 再大就得在时间上做降采样了。

2.3 算力与显存的实账核算

光说不练没意义,把数字摊开算一遍。假设输入特征图 C=512,T=8,空间分辨率 28×28,那么 N = 8×28×28 = 6272。标准 Non-local 的相似度矩阵是 6272×6272,约 3934 万个元素,用 float16 存也要 78MB 一个矩阵,而且这只是中间结果,反向传播还要存,实际显存占用得乘个三四倍。

换成 RNL,r=7,R = 8×7×7 = 392。相似度矩阵 392×392 只有 15.4 万个元素,显存不到 0.3MB,中间省略了 250 倍。就算把像素到区域的上采样和广播算进去,总计算量也降了两个数量级。

配置特征尺寸 (T×H×W)元素数 N区域数 R注意力矩阵规模相对算力
Non-local8×28×286272不适用6272²1.00
RNL r=58×28×286272200200²0.006
RNL r=78×28×286272392392²0.012
RNL r=148×28×28627215681568²0.063
Non-local16×56×5650176不适用50176²64.0

最后一行是关键。表里把 Non-local 在 16×56×56 上的配置作为对比,相对算力到了 64 倍,也就是说同一个位置,RNL 在 8×28×28 上做一次的开销,够你在 16×56×56 上做六十多次。这就是为什么 Non-local 只能用在高层的低分辨率特征图上,而 RNL 可以往下铺。我实际测下来,在 8×28×28 上加一个 RNL 块,单次前向的耗时增加不到 3%,显存增加约 40MB,放到 16 帧 112×112 的输入下完全扛得住。

注意:上面的“相对算力”只算了注意力矩阵本身,实际还要加上池化、插值和三路 1×1×1 卷积的开销。通道数被压到 C/2 之后,这三路卷积的占比会上升,在极端小的 R 下(比如 r=3),卷积反而不是瓶颈,池化和插值才是,做性能优化时优先看插值。

3. 把RNL模块写进视频分类主干

3.1 模块代码与关键实现细节

下面是我实际在用的实现,PyTorch 版本,去掉了不必要的分支,只保留固定网格这一条路线。输入是 5D 张量,通道在前。

import torch import torch.nn as nn import torch.nn.functional as F class RegionNonLocal(nn.Module): def __init__(self, channels, region=7, reduction=2, use_temporal=True): super().__init__() inner = max(channels // reduction, 16) self.region = region self.use_temporal = use_temporal self.theta = nn.Conv3d(channels, inner, 1, bias=False) self.phi = nn.Conv3d(channels, inner, 1, bias=False) self.g = nn.Conv3d(channels, inner, 1, bias=False) self.proj = nn.Conv3d(inner, channels, 1, bias=False) self.bn = nn.BatchNorm3d(channels) self.gamma = nn.Parameter(torch.zeros(1)) def forward(self, x): B, C, T, H, W = x.shape r = self.region # 1. 区域聚合:均匀网格平均池化 if self.use_temporal: pooled = F.adaptive_avg_pool3d(x, (T, r, r)) else: pooled = F.adaptive_avg_pool3d(x, (T, 1, 1)).expand(-1, -1, -1, r, r) theta = self.theta(pooled).flatten(2) # (B, C', R) phi = self.phi(pooled).flatten(2) # (B, C', R) g = self.g(pooled).flatten(2) # (B, C', R) # 2. 区域级注意力 attn = torch.bmm(theta.transpose(1, 2), phi) # (B, R, R) attn = F.softmax(attn * (theta.shape[1] ** -0.5), dim=-1) z = torch.bmm(g, attn.transpose(1, 2)) # (B, C', R) z = z.view(B, -1, T, r, r) # 3. 广播回像素 z = z.permute(0, 2, 1, 3, 4).contiguous().view(B * T, -1, r, r) z = F.interpolate(z, size=(H, W), mode='bilinear', align_corners=False) z = z.view(B, T, -1, H, W).permute(0, 2, 1, 3, 4).contiguous() z = self.bn(self.proj(z)) return x + self.gamma * z

几个实现要点值得单独说。

第一,相似度缩放。加了个theta.shape[1] ** -0.5,也就是除以 sqrt(通道数)。这不是原版 Non-local 的做法,但在实际训练里非常有用,因为区域特征经过 1×1×1 卷积之后数值范围差异很大,不缩放的话 softmax 容易饱和,注意力直接退化成独热,等于只取了最大的那个区域。加上缩放之后,注意力的熵明显更高,说明模型在真正做“加权融合”而不是“硬选择”。

第二,上采样的内存布局。先把 (B, C', T, r, r) 重排成 (B×T, C', r, r) 再做双线性插值,这样 interpolate 就只需要处理 4D 张量,少一次三线性插值的核调度。实测在 T=16 的时候,这一步能省 15% 左右的耗时。插值完再重排回去,虽然多了两次 permute 和 contiguous,但整体是值的。

第三,BatchNorm 放在投影卷积之后。不加 BN 的话,广播回来的特征和主干特征的分布差异很大,直接相加会让后面的层很难适应。加了 BN 之后训练稳定很多,代价是推理时需要额外维护一组统计量,影响不大。

第四,use_temporal=False这个分支保留了,用来做消融。关掉之后退化成一个全局空间池化,区域内只有一个点,本质上就是不带时间交互的空间注意力。我建议先用 False 跑一版作为基线,再开 True 看增量,这样能清楚知道收益来自哪里。

3.2 插入位置、层数和时空维度处理

模块写完了,插在哪是个经验活。

常见做法是把 Non-local 块插在残差阶段之后,比如骨干网络的 res3 和 res4 各插几个。RNL 因为便宜,可以往前挪一挪,插到 res2 的末尾(通常是 28×28 的分辨率)也不过分。我自己跑的一组配置是:res2 末尾插 1 个,res3 末尾插 2 个,res4 末尾插 2 个,总共 5 个。太少没感觉,太多会拖慢收敛,而且后面几个块的注意力图高度相似,边际收益递减。有篇工作做过类似实验,插到 5 个之后再往上加,准确率的提升基本在噪声范围内。

时间维度的处理上,我把use_temporal打开之后,区域数 R 会变成 T×r×r,这时候要注意显存随 T 线性增长。T=32、r=7 时 R 就是 1568,R² 是 246 万,虽然比 N² 小得多,但 batch size 开大之后还是能吃满显存。如果显卡不够,有两个妥协方案:一是在时间维度上也做池化,把 T 压到 T/2 再算注意力;二是把相似度矩阵改成稀疏计算,只保留每个区域 top-k 个相似区域,k 取 16 或 32,softmax 在稀疏矩阵上做。第二种方案我在一个长视频任务上试过,精度损失不到 0.3 个点,显存省了 60%。

还有个小坑,adaptive_avg_pool3d在 T 不能被整除的时候行为不是特别直观,如果你的时间维度做过下采样导致 T 是奇数,最好先补一帧到偶数再池化,或者干脆把 r 在时间上单独设成 T 本身(也就是不做时间下采样)。我在一个变长视频的任务里就因为这个出过问题,同一段视频在不同采样长度下,区域划分对不齐,导致 batch 内的统计量不稳定。

3.3 训练配方与调参记录

训练配置这块,把我在视频分类任务上跑通的一套给出来,仅供参考,具体要按你的数据量调整。

优化器用 SGD,momentum 0.9,weight decay 1e-4,不用 Adam,因为视频任务的 batch 通常不大,Adam 的泛化性在小 batch 下反而不如 SGD。学习率做 cosine 退火,初始 0.01,配上 5 个 epoch 的线性 warmup。batch size 是 8 段 × 8 帧,也就是一次喂 64 帧。混合精度训练打开,梯度裁剪设 1.0,防止注意力分数爆炸导致的梯度尖峰。

输入尺寸我用的 112×112,不是 224。原因很实际:224 分辨率下,同等的显存只能跑一半的 batch,而视频任务里 batch 内的负样本多样性比单帧分辨率更重要。如果显存充裕,可以试 160×160,再往上收益不明显。

超参数取值调整理由
优化器SGD小 batch 下泛化优于 Adam
初始学习率0.01配合 cosine 与 warmup
warmup epoch5稳定注意力模块初期的梯度
权重衰减1e-4抑制区域投影卷积过拟合
梯度裁剪1.0防止 softmax 饱和引起尖峰
输入分辨率112×112分辨率与 batch 的折中
采样帧数8 或 1616 帧时显存约翻倍
精度混合精度注意力矩阵用 fp16 存储

一个反直觉的观察:加了 RNL 之后,训练 epoch 总数要适当减少,不能照搬基线。因为注意力模块带来的收敛加速是实打实的,基线要 100 个 epoch 才收敛,加了 RNL 之后 80 个 epoch 就到平台了,再多训会开始过拟合。我一开始没注意,按 100 个 epoch 跑,最后 20 个 epoch 验证集准确率反而掉了 0.8 个点。

还有一个调参点是 γ 的初始值。虽然标准做法是零初始化,但如果你的骨干网络是从零开始训练而不是加载预训练权重,γ 初始化为 0 会让这个分支在前期完全不起作用,白白浪费训练时间。这种情况下可以初始化为 0.1 或者用一个小的高斯初始化,让它在早期就参与梯度更新。

3.4 消融与结果解读

跑了四组对照,固定随机种子,其他配置完全一致,只改 RNL 的配置,结果如下。

配置是否含时间交互区域粒度插入层数准确率相对基线
基线(无注意力)无无0基准 - 0.00.0
空间区域注意力否7×75+1.6+1.6
时空区域注意力是7×75+2.7+2.7
时空区域注意力是5×55+2.4+2.4
时空区域注意力是14×145+2.8+2.8

表格里最后一行看起来很香,r=14 比 r=7 还高 0.1 个点,但代价是算力涨了五倍多,性价比完全不划算。实用推荐还是 r=7。空间注意力和时空注意力之间的 1.1 个点差距,说明时间维度的交互确实是主要收益来源,这也是我在前面强调use_temporal要单独做消融的原因。

再补一个观察:把插入层数从 5 减到 3(只保留 res3 和 res4),准确率只掉了 0.4 个点,速度却快了近 20%。如果你的推理延迟有硬约束,这个裁剪是值得做的。反过来,如果只保留 1 个块在 res4,掉 1.5 个点,说明单一位置的注意力无法覆盖多尺度的依赖关系。

还有一点,注意力图可视化之后能看出一些有意思的模式。在动作识别的例子里,区域注意力高亮的区域确实集中在动作主体和动作相关物体上,比如“切菜”这个动作,注意力同时落在手、刀和砧板上,这三个区域之间形成高相似度连接。这也从侧面说明,区域级建模并没有丢太多信息,语义对应的区域确实能自动聚到一起。

4. 从视频分类回灌人脸识别

4.1 视频人脸识别的聚合层到底该怎么设计

回到标题的另一半。视频人脸识别的核心问题不是特征提取,而是多帧特征怎么聚合成一个。最简单的是平均池化,把所有帧的特征加起来除以帧数。这个方法在视频质量好的时候勉强能用,但一段视频里总有模糊帧、侧脸帧、遮挡帧,平均之后会把噪声也平均进去,最终特征反而比单张最好的帧还差。

改进方向是加权聚合,权重来自质量评估。质量分可以由几个因素构成:人脸检测的置信度、关键点对齐的残差、清晰度(拉普拉斯方差)、姿态角偏离正脸的程度。每个因素归一化之后加权求和,得到一个 0 到 1 的质量分,再把它当成聚合权重。这套方法在工程上非常好用,实现简单,不需要额外训练,我做过对比,在同等的特征提取器下,质量加权比平均池化在验证集上的准确率能高出两到三个点。

但加权聚合有个前提:质量分是准的。如果质量评估本身有偏,比如过度惩罚侧脸,那你就会把一段本来有信息量的侧脸序列全部压掉。更稳的做法是引入注意力聚合,让网络自己学该关注哪一帧。实现上就是一个小的注意力网络,输入是每帧特征,输出是每帧的标量权重,softmax 归一化后加权求和。这个思路和 RNL 的区域注意力在形式上是同构的,只不过把“区域”换成了“帧”,把“空间相似度”换成了“帧与帧的时序依赖”。这就是我在开头说的技术回灌,两边的数学结构完全一致,代码可以直接改。

4.2 人脸识别工程链路的落地细节

真正落地时,注意力聚合只是链条上的一环,前面几个环节的细节更能决定成败。

对齐环节,五个点的相似变换是最低配置,如果条件允许,用 68 点或 98 点做更精细的仿射变换,把眼睛、鼻子、嘴巴的位置统一到标准坐标。对齐模板的选择有讲究,不同模板下训练出来的模型不通用,换模板必须重训。我在一个项目里换过模板,同一个验证集上准确率差了两个多点,原因是新模板下人脸被裁得更紧,边缘信息丢失了。

数据清洗环节,公开数据集里的噪声比想象中严重。标注错误的、同一个人多个身份标签的、图像本身是重复的,这些都会污染训练。我的做法是先训一个小模型,跑一遍全集,把类内距离特别大的样本和类间距离特别小的样本挑出来人工过一遍,通常能清掉 3% 到 5% 的脏数据,效果相当于多刷了一轮训练。

特征归一化环节,前面提过要 L2 归一化,但还有个细节是特征维度。512 维是主流,维度再高收益很小,反而增加存储和检索成本。做 1:N 检索时,如果底库规模到了百万级,用 512 维的 float32 存就是 2GB,配上量化能压到 500MB 以内,检索延迟在毫秒级。这个量级下,特征维度的取舍要跟检索方案一起考虑,不能只盯着准确率。

注意:做多帧聚合时,一定要确认所有帧的特征来自同一个模型版本。中途换模型会导致不同帧的特征落在不同的嵌入空间里,聚合出来的结果毫无意义。这类错误在多人协作的项目里出过不止一次。

4.3 一个可复现的训练配方

把上面的东西串起来,给出一个完整的人脸识别训练配方,包含度量学习部分。

损失用带角度间隔的形式,实现如下:

import math import torch import torch.nn as nn import torch.nn.functional as F class AngleMarginHead(nn.Module): def __init__(self, in_features, num_classes, s=64.0, m=0.5): super().__init__() self.weight = nn.Parameter(torch.empty(num_classes, in_features)) nn.init.xavier_uniform_(self.weight) self.s = s self.m = m def forward(self, x, label): # x 必须已做 L2 归一化 cos = F.linear(F.normalize(x), F.normalize(self.weight)) cos = cos.clamp(-1 + 1e-7, 1 - 1e-7) theta = torch.acos(cos) target = theta.gather(1, label.view(-1, 1)) + self.m one_hot = F.one_hot(label, cos.size(1)).float() final = torch.where(one_hot.bool(), torch.cos(target), cos) return F.cross_entropy(final * self.s, label)

参数选择上,s 取 64、m 取 0.5 是通用起点。如果你的类别数很少(比如几百个),m 可以取 0.3 到 0.4,避免训练初期过于困难;类别数上万的时候,m 取 0.5 甚至 0.6 才有足够的判别力。学习率用 0.1 起步,cosine 退火到 0,权重衰减 5e-4,warmup 用 2 个 epoch。batch size 越大越好,因为角度间隔损失本质上是在做对比学习,负样本数量直接决定梯度质量,能上 512 就别用 256。

视频人脸识别的微调策略是分两阶段。第一阶段用静态图训练主干和度量头,把单帧能力打扎实;第二阶段冻结主干的前几层,只训练后几层和聚合模块,用视频序列做微调。冻结的层数取决于你的视频数据量,数据少就多冻几层,数据多可以全放开。我做过对比,直接拿视频数据从头训,收敛慢且容易过拟合;两阶段训法在相同数据量下验证集准确率高出接近 4 个点。

5. 常见问题与排查速查

5.1 显存与速度类问题

第一个高频问题是训练刚开始就 OOM。分两种情况:如果加上 RNL 之后才爆,先用torch.cuda.memory_allocated()打印各阶段的显存,重点看相似度矩阵那一步。R 太大的话,把 r 从 7 降到 5,或者把use_temporal关掉先跑通。如果是上采样那一步爆,检查是不是 T 太大,把 T 从 32 降到 16 试试。

第二个问题是训练速度比预期慢很多。先 profile 一下,看时间花在什么上。很多人以为是注意力矩阵,实际上小 R 的情况下,时间主要花在interpolate和contiguous上。优化手段是减少不必要的 permute,把上采样前的重排做成 view 而不是 reshape,确保张量是连续的。另外,adaptive_avg_pool3d在 T 和 r 都不是 2 的幂时,kernel 调度效率会下降,能凑成 2 的幂就凑一下。

第三个问题是推理时的显存占用远小于训练但延迟没降下来。这是因为混精训练时注意力矩阵是 fp16,但推理框架可能把它自动升到了 fp32。检查一下导出后的计算图,确认所有节点都在预期精度上。

5.2 精度不涨与掉点类问题

现象可能原因排查手段处理方式
加 RNL 后精度不涨γ 未初始化或学习率过低打印 γ 的绝对值γ 用零初始化,学习率恢复 1.0
前 10 epoch loss 剧烈震荡注意力分支破坏预训练分布观察 loss 曲线前 2000 步加 BN,γ 零初始化
精度先涨后掉过拟合对比训练和验证曲线减少 epoch,加 dropout
注意力图几乎全黑softmax 饱和打印注意力熵加 sqrt(C) 缩放
换成大 r 后反而变差区域过细,注意力稀疏对比不同 r 的结果r 取 5 到 7
多卡训练后精度下降BN 统计量跨卡不同步检查 BN 的 sync 设置改用 SyncBN

前面几行基本覆盖了我遇到的主要情况。特别说一下“注意力图全黑”这个,打印注意力矩阵的熵是最快的诊断手段,正常情况下熵应该在 log(R) 的 60% 到 80% 之间,如果低于 30%,基本可以确定是饱和了,缩放系数和初始化都要检查。

还有一个容易被忽略的点:如果你的骨干网络用了预训练权重,而 RNL 是随机初始化的,两者在初期的特征尺度可能差一个数量级。这时候残差相加会把主干的输出带偏,表现为前几个 epoch 精度断崖式下降。除了 γ 零初始化,还可以在投影卷积之后加一个 LayerNorm,把输出的尺度拉回来。

5.3 跨任务迁移时最容易踩的坑

从视频分类往人脸识别迁移,最容易犯的错是直接照搬超参数。视频分类的输入是 8 到 32 帧的连续片段,人脸识别的视频序列可能只有 3 到 5 帧,而且帧间间隔不均匀,可能是关键帧采样,不是均匀采样。这种情况下,时间维度上的区域划分就失去了均匀网格的前提,需要改成按实际帧数动态划分,或者干脆不做时间池化,直接用注意力聚合。

第二个坑是空间分辨率不匹配。视频分类常见的特征图是 28×28,人脸识别在对齐之后裁切出来的图通常是 112×112,特征图在最后阶段可能到 7×7。这个尺寸下再划 7×7 的区域,等于每个区域就一个像素,区域注意力退化成了像素注意力,不仅没省算力,还多了插值开销。人脸场景下区域数要相应减少,5×5 对应 7×7 的特征图就够粗了,或者把区域注意力放到更浅的层去做。

第三个坑是评估协议的差异。视频分类看 top-1 准确率,人脸识别看的是 1:1 验证的准确率和 1:N 检索的召回率,评估时用的阈值也不同。把视频分类的模型直接搬到人脸识别上,即使特征本身有判别力,阈值不对也会让指标看起来很差。我建议先在小的验证集上扫一遍阈值,找到等错误率最低的点,再拿到大集上测。

我自己在实际操作中的体会是,RNL 这种区域级注意力最大的价值不在于某个具体任务的涨点,而在于它把自注意力的成本压到了可以在多处、多尺度使用的程度。一旦成本不再是障碍,你就可以在网络的浅层、中层、深层都铺上,让不同尺度上的长程依赖都能被捕捉到。这个思路用到人脸识别上,把“区域”理解成“人脸部件”甚至“视频里的关键帧”,数学结构完全不用改,改的只是划分规则和聚合对象。真正花时间的从来不是写这个模块,而是搞清楚在你的数据上,哪些东西该被当成一个区域,以及这些区域之间的关系值不值得建模。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进