ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

细粒度动作识别新框架:Cross-Attentive Latent Sparse Experts

细粒度动作识别新框架:Cross-Attentive Latent Sparse Experts 研究生阶段接触动作识别时最初看的都是 Video-level 分类或者时序片段分类比如用 SlowFast、TSM、TimeSformer 判断“这个人是在跑步还是走路”。这类任务相对“粗粒度”类间差异大模型容易学习到通用的运动模式。但真正到了业务落地或者前沿课题研究时问题就变了同样的“倒水”动作倒热水和倒冷水姿态几乎一样“切菜”和“切水果”工具不同但动作模式高度重合。这就是典型的 Fine-Grained Action Recognition即细粒度动作识别。它的难点在于类间差异极其细微而类内变化又很大。近年来的研究方法已经从单纯的 3D 卷积、Video Transformer 演进到“多分支交互、稀疏激活、判别性特征对齐”这一路线上。本文要拆解的这篇工作核心是用 Cross-Attentive Latent Sparse Experts 构建一个面向细粒度动作识别的框架。简单来说它把潜在空间中的稀疏专家机制和跨注意力交互结合起来让模型只关注真正有判别力的局部时空线索避免对背景或共性运动模式的过拟合。我将从背景概念、方法拆解、伪代码实现、实验思考和落地建议几个方面展开。如果你正在看动作识别相关论文或者要做视频理解方向的项目这篇文章可以帮你理清思路。1. 细粒度动作识别到底难在哪里1.1 粗粒度与细粒度的边界先明确业务边界。粗粒度动作识别常用的数据集包括 UCF101、Kinetics-400、HMDB51类别之间差异通常集中在“动作主体与环境的交互模式”上比如 swimming、skiing、playing guitar。这种问题用光流、时序卷积、Video Transformer 已经能解决得不错。细粒度动作识别的数据则通常是 FineGym、Something-Something V2、Moments in Time 的子集甚至是工业场景中自采的几十类相似动作。类别之间的差异表现在手部姿态发生细微变化工具与物体的接触方式不同动作速度、方向、局部形变有差异交互物体的类别极其相似。以 FineGym 中的“体操动作”为例同样是平衡木上的空翻助跑、起跳、转体角度、落地姿态都会影响最终类别。这种识别任务已经超出“整体视频分类”的范畴需要模型在时空上定位到细微且具有判别力的片段。1.2 为什么传统方法在细粒度上失效传统 3D CNN 和 Video Transformer 存在的问题可以归纳为三点全局建模掩盖局部细节。视频编码器会把整段视频压缩成一个全局表征细节区域容易被背景、光照和共性运动稀释。缺少判别性特征选择机制。模型没有能力判断哪些潜在特征真正区分相似类别。计算开销与精度难以平衡。用更大的模型暴力提升精度在细粒度任务上边际收益很低。细粒度动作识别因此更需要一种“可选择的、判别性的、能跨分支交互”的特征提取机制。这就是 Cross-Attentive Latent Sparse Experts 出现的直接动机。1.3 这篇工作的核心贡献概括从标题可以拆出三个关键词Fine-Grained Action Recognition研究方向是细粒度动作识别Cross-Attentive模型在不同特征分支之间做跨注意力交互Latent Sparse Experts在潜在空间设计多个专家但每个输入只激活少量专家从而实现稀疏激活。整体思路可以概括为使用多个专家模型分别捕获不同的潜在动作模式通过跨注意力机制让不同专家相互交换信息同时保证专家在潜在空间中是稀疏调用的。这种结构兼顾了专家多样性与计算效率。2. 方法整体架构拆解2.1 从“单分支全量推理”到“多专家稀疏激活”在正式理解这篇方法之前可以先回顾一下经典的 Mixture of ExpertsMoE思路。MoE 通常包含若干专家网络和一个路由器。输入会经过路由器计算被分配给某个或某几个专家最终输出为专家结果的加权和。MOE 的核心优势在于模型容量变大但单次推理只激活部分专家计算成本可控。但在细粒度动作识别中直接使用 MoE 有两个问题路由决策通常只基于全局特征缺乏局部判别信息专家之间没有交互各专家独立处理输入容易特征冗余。Cross-Attentive Latent Sparse Experts 的改进点就在于把专家放置在潜在空间并使用跨注意力让专家之间交换信息。2.2 模型的整体流程从数据处理角度视频输入经过时间采样后形成 T 帧图像每帧图像经过共享的 2D CNN 或 Video Transformer 编码得到时空令牌序列。整体流程可以分为四个阶段视频令牌化对输入视频进行均匀采样使用 patch embedding 生成时空令牌序列共享编码器使用共享的 video backbone 提取基础特征潜在稀疏专家层将特征映射到潜在空间由路由器选择少量专家参与计算跨注意力聚合不同专家输出通过跨注意力机制做信息交换并输出最终分类特征。这里把“专家混合”和“跨注意力”叠加在 backbone 之后而不是像传统方法那样直接替换 backbone。这使得该方法可以即插即用地适配已有的视频编码器。2.3 潜在空间中的专家定义所谓 Latent Sparse Experts是在模型的隐藏层中定义 N 个专家。每个专家本质上是一个可学习的特征变换函数其维度与 backbone 输出维度保持一致。输入特征 ( z ) 会先经过 router 网络生成一个选择概率分布然后在 N 个专家中选取 Top-K 个参与计算。这里有一个值得注意的细节稀疏选择发生在“潜在特征”上而不是原始像素或者中间 feature map 上。这样选择的粒度更细专家更容易捕获到动作模式中的微小差异。与此同时因为只激活 Top-K 个专家模型参数量可以设计得较大而推理计算量不会成倍增长。3. Cross-Attentive 机制的作用方式3.1 为什么要跨注意力如果你只用独立的多个专家那么模型实际上等价于一个“有条件选择的特征转换器”。这样的问题是每个专家只看到自己的输入无法感知其他专家掌握的互补信息。细粒度动作识别中一个“微小的手部姿态”和“整体身体运动”常常属于不同专家的捕获范围。手部姿态专家如果能参考身体运动专家的中间结果那么它对局部细节的判断会更稳。所以专家之间需要信息交互。Cross-Attentive 的做法是构建一组 Query / Key / Value 交互。假设有 K 个被激活的专家分别输出特征 ( E_1, E_2, ..., E_K )。每个专家既作为 Query 去关注其他专家的输出也作为 Key 和 Value 被其他专家关注。这样每个专家的最终特征都携带全局专家上下文。3.2 单层跨注意力计算过程用公式描述如下[ Q E W_Q, \quad K E W_K, \quad V E W_V ][ A \text{softmax}\left(\frac{Q K^\top}{\sqrt{d_k}}\right) ][ E A V ]其中 ( E \in \mathbb{R}^{K \times d} ) 是 K 个被激活专家的输出拼接矩阵( d_k ) 是缩放因子。经过自注意力后每个专家的特征都会被其他专家信息增强。你可以把它理解成“专家之间的互相校准”。3.3 与时间维度的跨注意力区别细粒度动作识别任务中有另一种跨注意力用法在时间维度和空间维度之间建立跨注意力比如让模型关注“哪几帧的哪个区域对判别最重要”。而本文中 Cross-Attentive 的重点是特征分支之间的交互属于“结构上的跨注意力”并不完全等同于时序注意力。实际工程中两者可以叠加使用。视频编码器负责时序建模Cross-Attentive Latent Sparse Experts 负责在精细表征层做选择与交互。4. 模型完整工程实现拆解4.1 环境准备与依赖OpenCV11.2如果你只是理解方法可以跳过这一节。如果你想跑通一个小型复现实验建议准备以下环境Python 3.8 以上PyTorch 1.10 以上推荐 2.0torchvision一个视频动作识别数据集子集比如 UCF101 的子类或者 Something-Something V2 的部分类别GPU 显存至少 8GB用于视频帧 batch 的编码。对于框架实现我们不需要完整复现大型实验可以构建一个核心模块验证思路project/ ├── config.py # 实验配置 ├── dataset.py # 视频帧采样与预处理 ├── backbone.py # 视频编码器 ├── experts.py # Latent Sparse Experts 实现 ├── cross_attention.py # 跨注意力模块 ├── model.py # 完整模型 └── train.py # 训练脚本4.2 Backbone 特征编码backbone 可以选用 Video Swin Tiny 或者 SlowOnly。为了演示方便可以在小规模数据上使用带 3D 卷积的 ResNet 作为 backbone输出维度设为 512。下面给出一个简单的 backbone 定义代码。注意这里不是完整论文复现而是说明“特征从哪里来”这一层逻辑。# 文件路径backbone.py import torch import torch.nn as nn class VideoBackbone(nn.Module): 将视频帧序列编码为时空令牌特征。 输入形状: (B, C, T, H, W) 输出形状: (B, T, d_model) def __init__(self, d_model512): super().__init__() # 3D 卷积下采样模块 self.conv1 nn.Conv3d(3, 64, kernel_size3, stride(1, 2, 2), padding1) self.bn1 nn.BatchNorm3d(64) self.conv2 nn.Conv3d(64, 128, kernel_size3, stride(1, 2, 2), padding1) self.bn2 nn.BatchNorm3d(128) self.conv3 nn.Conv3d(128, 256, kernel_size3, stride(1, 2, 2), padding1) self.bn3 nn.BatchNorm3d(256) self.pool nn.AdaptiveAvgPool3d((1, 1, 1)) self.fc nn.Linear(256, d_model) def forward(self, x): # x: (B, C, T, H, W) x torch.relu(self.bn1(self.conv1(x))) x torch.relu(self.bn2(self.conv2(x))) x torch.relu(self.bn3(self.conv3(x))) # 全局池化后展平空间与时间 x self.pool(x).flatten(1) # (B, 256) x self.fc(x) # (B, d_model) return x这里对空间和时间维度进行了全局池化实际上会丢失很多细节。完整实现中backbone 输出的是 T 个时间步的令牌序列而不是一个全局向量。为了保留细节建议去掉最后一层全局池化保持时空维度# 保留时空位置的 backbone 输出示例 class VideoBackboneToken(nn.Module): def __init__(self, d_model512): super().__init__() self.conv1 nn.Conv3d(3, 64, kernel_size3, stride(1, 2, 2), padding1) self.bn1 nn.BatchNorm3d(64) self.conv2 nn.Conv3d(64, 128, kernel_size3, stride(1, 2, 2), padding1) self.bn2 nn.BatchNorm3d(128) self.proj nn.Conv3d(128, d_model, kernel_size1) def forward(self, x): x torch.relu(self.bn1(self.conv1(x))) x torch.relu(self.bn2(self.conv2(x))) x self.proj(x) B, D, T, H, W x.shape # 转换为序列形式 (B, T*H*W, D) x x.permute(0, 2, 3, 4, 1).reshape(B, -1, D) return x这种保留时空令牌的特征才能支撑后续细粒度判别。4.3 Latent Sparse Experts 模块实现在特征维度为 d_model 的潜在空间中定义 N 个专家每个专家是一个 MLP。路由器根据输入特征生成 Top-K 采样选择。实现步骤如下输入特征 z 经过 router 线性层得到 N 个 logits取 Top-K 索引构造 mask对选中的专家计算结果进行加权和。# 文件路径experts.py import torch import torch.nn as nn import torch.nn.functional as F class LatentSparseExperts(nn.Module): 潜在稀疏专家层。 输入: (B, L, d_model) 输出: (B, L, d_model) def __init__(self, d_model512, num_experts8, top_k2): super().__init__() self.num_experts num_experts self.top_k top_k self.router nn.Linear(d_model, num_experts) # 每个专家是一个两层的 MLP self.experts nn.ModuleList([ nn.Sequential( nn.Linear(d_model, d_model * 2), nn.GELU(), nn.Linear(d_model * 2, d_model), ) for _ in range(num_experts) ]) def forward(self, z): # z: (B, L, d_model) B, L, D z.shape # 路由打分: (B, L, num_experts) router_logits self.router(z) router_probs F.softmax(router_logits, dim-1) # 取 Top-K 专家 topk_indices torch.topk(router_probs, self.top_k, dim-1).indices # (B, L, K) # 初始化输出 out torch.zeros_like(z) # 构造 expert 权重 mask expert_mask torch.zeros_like(router_probs).scatter_(-1, topk_indices, 1.0) for i in range(self.num_experts): # 当前专家被选中的位置 mask expert_mask[..., i:i1] # (B, L, 1) if mask.sum() 0: continue expert_out self.experts[i](z) # (B, L, D) # 加权累加这里使用路由概率作为权重 weight router_probs[..., i:i1] # (B, L, 1) out out mask * weight * expert_out return out代码里使用了mask * weight * expert_out这种方式保证只有被选中的专家参与输出。需要强调的是在真正训练大规模 MoE 时通常还要引入 load balance loss 辅助约束否则路由器可能收敛到只使用同一个专家。4.4 跨注意力专家交互模块这一模块负责让不同专家之间交互实现上可以使用标准的 Multi-Head Self-AttentionMHSA作用在专家维度上。这里有两种使用方式方式 A先得到 K 个专家的输出把专家拼接后做 Self-Attention方式 B使用 Cross-Attention让每个位置的特征作为 Query所有专家输出作为 Key/Value。在细粒度动作识别中方式 B 更合理。因为视频中的每个时空令牌应该根据自己的局部特征去“挑选”最值得参考的专家信息。代码实现方式 B# 文件路径cross_attention.py import torch import torch.nn as nn import torch.nn.functional as F class CrossAttentiveFusion(nn.Module): 跨注意力融合模块。 将主干特征与专家特征交互。 x: 主干特征 (B, L, D) expert_out: 专家输出特征 (B, L, D) def __init__(self, d_model512, num_heads8): super().__init__() self.num_heads num_heads self.d_k d_model // num_heads self.q_proj nn.Linear(d_model, d_model) self.k_proj nn.Linear(d_model, d_model) self.v_proj nn.Linear(d_model, d_model) self.out_proj nn.Linear(d_model, d_model) def forward(self, x, expert_out): B, L, D x.shape Q self.q_proj(x).view(B, L, self.num_heads, self.d_k).transpose(1, 2) K self.k_proj(expert_out).view(B, L, self.num_heads, self.d_k).transpose(1, 2) V self.v_proj(expert_out).view(B, L, self.num_heads, self.d_k).transpose(1, 2) scores torch.matmul(Q, K.transpose(-2, -1)) / (self.d_k ** 0.5) attn F.softmax(scores, dim-1) context torch.matmul(attn, V) # (B, num_heads, L, d_k) context context.transpose(1, 2).contiguous().view(B, L, D) out self.out_proj(context) return out在这个模块中主干特征负责提供 Query也就是“当前令牌需要什么”专家输出提供 Key 和 Value也就是“哪些专家包含什么信息”。这种设计使得最终特征既包含主干编码又叠加了专家选择的细粒度信息。4.5 完整模型组装把以上模块串联起来就得到一个小型 CLSE 动作识别模型# 文件路径model.py import torch import torch.nn as nn from backbone import VideoBackboneToken from experts import LatentSparseExperts from cross_attention import CrossAttentiveFusion class CLSEActionModel(nn.Module): def __init__(self, num_classes51, d_model512, num_experts8, top_k2, num_heads8): super().__init__() self.backbone VideoBackboneToken(d_modeld_model) self.experts LatentSparseExperts(d_modeld_model, num_expertsnum_experts, top_ktop_k) self.cross_attn CrossAttentiveFusion(d_modeld_model, num_headsnum_heads) self.classifier nn.Linear(d_model, num_classes) def forward(self, x): # x: (B, C, T, H, W) feat self.backbone(x) # (B, L, D) expert_feat self.experts(feat) # (B, L, D) fused self.cross_attn(feat, expert_feat) # (B, L, D) # 全局池化后分类 pooled fused.mean(dim1) # (B, D) logits self.classifier(pooled) # (B, num_classes) return logits这个模型最直接的训练方式就是用交叉熵 loss 做视频分类训练。当然真实论文里通常还要加入辅助损失来保证专家多样性。训练时需要注意的一点是由于引入了 Top-K 稀疏选择网络的反向传播经过 router 时梯度流动是正常的因为scatter和mask操作是可微的或者采用 Straight-Through Estimation 的思想。5. 细粒度动作识别的数据与训练细节5.1 数据采样策略细粒度动作识别模型很容易在时序采样上吃亏。常见做法是 segment-based sampling即从视频中均匀采样多个片段每个片段再随机选取若干帧。推荐采样策略如下视频分为 8 或 16 个 segment每个 segment 随机抽取 1 帧或短片段将采样结果统一为固定帧数例如 8 帧或 16 帧训练时加入随机裁剪、缩放等数据增强。关键点在于“均匀采样”保证了模型能看到完整的动作过程而不只是被动作前几帧带偏。细粒度动作中判别帧往往出现在动作中后段所以采样均匀性比随机连续采样更稳定。5.2 损失函数与辅助约束为了不让专家退化成同一个网络可以在原有交叉熵损失之外增加辅助的负载均衡损失。负载均衡损失希望每个专家被调用的次数尽量均衡。一个简化的实现是计算每个专家被选中的概率分布并加入熵正则或均匀分布约束def load_balance_loss(router_probs, topk_indices, num_experts): router_probs: (B, L, num_experts) topk_indices: (B, L, top_k) # 每个专家被选中的次数 count torch.zeros(num_experts, devicerouter_probs.device) for i in range(num_experts): count[i] (topk_indices i).sum().float() # 归一化 count count / count.sum() # 与均匀分布的 KL 散度 uniform torch.ones_like(count) / num_experts loss F.kl_div(count.log(), uniform, reductionbatchmean) return loss加入这个辅助 loss 后总的损失为[ L L_{ce} \lambda L_{balance} ]其中 (\lambda) 通常设为一个比较小的值比如 0.01不要因为辅助损失干扰主任务。5.3 训练参数建议因为是细粒度识别模型更容易过拟合。建议在你的实验环境中采用以下基础配置配置项建议值优化器AdamW初始学习率1e-4 到 5e-4批次大小16 或 32权重衰减0.05学习率策略Cosine DecayEpoch30 到 50帧数8 到 16输入分辨率224x224需要注意的是以上参数取决于数据集规模和硬件条件。在小规模自采数据集上建议先用小学习率训练 5 个 epoch 观察 loss 是否正常下降再决定是否加大模型容量。6. 为什么“稀疏”和“跨注意力”能互补6.1 稀疏专家带来的好处从表征学习的角度看细粒度动作识别的核心是“让相似的类别在特征空间拉开距离”。如果所有类别共享同一套全量特征变换那么不同动作之间的微小差异容易被平均掉。而稀疏激活机制迫使模型为不同输入选择不同的处理路径。也就是说“用力挥手”与“轻微挥手”需要触发的专家子集不完全一致模型因此更有机会在西西弗斯式的相似运动中发现关键差异。稀疏性还带来了实际工程优势专家总数可以增加模型容量增大但单次计算只使用其中一小部分资源。这和大型推荐系统中的 MoE 思路一致。6.2 跨注意力带来的信息补偿但纯稀疏也有问题路由决策存在误差如果专家选择错误模型仍然无法恢复正确的判别信息。跨注意力机制的作用就是给最终特征提供“再次校准”的能力。交叉注意力通过注意力池化让每个特征都能参考所有被激活专家的输出即使某个局部特征一开始没有被分到最佳专家最终融合阶段仍有机会获取到关键线索。所以Cross-Attentive Latent Sparse Experts 并不是简单把 MoE 和 Attention 拼接而是一种结构上的互补设计稀疏性是“选择”跨注意力是“纠错与融合”。6.3 对细粒度分类的直观解释可以这样理解阶段一Backbone 编码得到每一个局部时空位置的初步语义阶段二Latent Sparse Experts 充当稀疏选择的“放大镜”让不同局部特征进入不同的判别路径阶段三Cross-Attention 充当“信息汇总会议”把所有判别路径的证据汇总到最终特征中。这一设计对细粒度动作识别是自洽的。细粒度动作需要同时具备“局部细节放大”与“全局信息校准”能力。7. 常见问题与调参思路7.1 模型不收敛或 loss 震荡可能原因与解决方案如下表问题现象常见原因解决思路训练 loss 下降慢学习率过大或过小使用 1e-4 初始学习率配合 warmuploss 震荡严重专家负载极不均衡增大负载均衡损失权重验证集 loss 上升过拟合增加数据增强、降低模型容量专家输出接近 0路由器饱和检查 router 初始化或去掉 Top-K 中的硬 mask7.2 专家路由器退化为单一专家这是 MoE 结构最常见的问题。产生原因一般是模型发现某一个专家已经足够完成大部分样本的判别其余专家梯度很少于是越来越不被调用。解决办法包括增加负载均衡损失的权重对 router logits 加入噪声扰动类似 Noisy Top-K Routing定期统计每个专家被调用的频率监控退化趋势。def compute_expert_frequency(topk_indices, num_experts): with torch.no_grad(): count topk_indices.flatten().bincount(minlengthnum_experts) total count.sum() freq count / total return freq7.3 视频帧数太少导致无法区分动作细节细粒度动作往往发生在几帧之内尤其是手部动作。可以结合两个策略提高时间分辨率比如输入 32 帧在空间维度加入局部裁剪增强将画面中的手部区域裁出来作为额外输入。更完整的论文复现中可以设计一个 local region branch专门处理高分辨率局部区域特征。不过这样会增加计算复杂度需要结合业务场景权衡。7.4 显存不足采用以下策略减小显存降低 batch size 并使用梯度累积使用混合精度训练AMP减少输入帧数但增加 segment 数量保证时间覆盖范围backbone 使用更小的网络比如 MobileNetV3 或者 Swin-Tiny。8. 最佳实践与工程落地建议8.1 代码设计与模块化在实现类似结构时不要把专家和跨注意力逻辑全部写进一个巨型类里。建议将模块拆分为独立的 Python 文件并提前定义输入输出形状。上面的示例代码中我们采用了 backbone、experts、cross_attention 的拆分方式这一结构在后续替换 backbone 或调整专家数量时都很方便。8.2 实验管理细粒度动作识别方向需要做大量消融实验。最重要的消融实验包括去掉专家稀疏机制只用普通 MLP去掉跨注意力模块改变 Top-K 值改变专家数量是否启用负载均衡损失。每次实验最好固定随机种子并记录数据集划分方式帧采样策略优化器参数最终 Top-1 / Top-5 准确率。不建议只对比最终精度。要记录专家路由分布观察模型是否真正学到了有区分度的专家分工。8.3 小数据上的训练策略如果业务场景没有大规模预训练数据最稳妥的做法是先在 Kinetics-400 或 Something-Something V2 上做预训练冻结 backbone 前几层训练分类头和专家模块再解冻全部参数做微调。这样能有效避免由于专家机制引入额外参数而导致过拟合。8.4 推理阶段的考虑训练时使用 Top-K 稀疏专家是随机的但推理时最好固定 Top-K 选择结果不要引入随机性。如果你的 router 在训练集上不稳定推理时可以对同一个视频做 2 到 3 次随机采样综合预测结果提升鲁棒性。在工程部署阶段视频解码和帧采样往往会成为性能瓶颈建议单独优化而不是把解码也放进深度学习模型里。8.5 什么时候不用这个方案这套方法不是万能的。如果你的动作类别差异足够大比如“跑步”和“游泳”那么在 Kinetics-400 上预训练的 Video Transformer 已经足够好。只有当你观察到模型在相似类别上频繁混淆且可视化特征中找不到局部判别区域时才值得引入 Latent Sparse Experts 结构。在工业落地中先分析混淆矩阵比直接升级模型更重要。如果确定瓶颈确实是相似动作判别再考虑这种复杂的专家交互架构。9. 总结这篇论文的核心思路——Fine-Grained Action Recognition with Cross-Attentive Latent Sparse Experts——技术上并不难理解。可以把它拆成三件事第一用 Latent Sparse Experts 在潜在空间实现对不同动作模式的选择性激活第二使用 Cross-Attentive 机制让各个专家在决策前相互交换信息第三整个结构作为视频骨干网络之上的增强模块可以独立替换和扩展。如果想要完整复现论文实验还需要补充具体的数据集划分、专家维度设置、backbone 选择以及消融实验的完整细节。但从结构和设计思路上说本文提供的最小实现已经可以帮你跑通一个基础版本并理解每个模块的作用。你可以把它作为 baseline嵌入到自己的动作识别项目中看看在细粒度相似类别上的表现是否提升。下一步可以继续深入了解 Noisy Top-K Routing、Expert Balancing、Video Transformer 的时序建模等内容这些都是当前视频理解领域比较实用的技术方向。
返回列表