资讯动态

弱监督视频异常检测:跨片段相似性与多尺度时间MLP实战解析

发布时间:2026/10/22 8:32:23 来源:尧图企业网站定制
监控摄像头记下来的画面每分钟都有可真正能派上用场的异常事件比如打架、闯入、物品遗弃通常只占其中很小一段。想靠人工逐帧看完再标注成本高到不现实想让模型在只有“这段视频是异常”这种粗略提示的前提下学出“具体哪几秒出问题”的定位能力这就是弱监督视频异常检测在做的事。最近读到TCSVT 2025上关于弱监督视频异常检测的工作思路给得挺巧把视频切成一堆片段不直接拿单个片段自己跟自己较劲而是看片段之间互相像不像再靠一种多尺度时间MLP把不同粗细的时间关系都揉进去最终给出逐片段异常分数。我在几个公开的弱监督异常检测基准数据集上复现过类似方案这篇就把整个思路、复现关键点、调参踩坑一起拆开讲清楚正好适合刚入门视频异常检测、或者准备复现这个方向工作的同学参考。1. 先把任务看穿弱监督视频异常检测在解决什么问题1.1 一个抠门的标注约束只有视频级标签却要帧级定位视频异常检测的典型落地场景是监控系统摄像头架好后一天到晚录个不停。镜头里的绝大多数内容是正常的比如行人来来往往、车辆正常行驶但系统真正要报警的是那些占比极小的事件打架斗殴、车辆逆行、人员翻越围栏、物品遗留在通道等。这类任务的训练标注有个现实难题如果要求标注员逐帧把异常标出来一个小时的视频可能要花掉几个小时甚至十几个小时的工作量。而且异常事件边界往往模糊标注员自己也未必能达成一致。于是弱监督设定出现了训练阶段只给你视频级标签这个视频是正常的那个视频包含异常至于异常具体出现在哪几帧不给。测试阶段才用帧级标注去评估模型定位得准不准。模型要在只有“整段视频是否有问题”这一粗粒度监督下学会对每个片段输出异常分数本质上是从非常弱的信号中做细粒度推理。这里自然衍生出一个矛盾正常片段数量远多于异常片段模型很容易被“正常”带偏最后输出一堆高不成低不就的分数。所以弱监督异常检测的核心难点从来不是网络能有多深而是怎么在没有位置标签的情况下让模型自己找到那少数的异常片段。1.2 从多实例排序到时间上下文的思路演进最早被广泛采用的思路是MIL排序多实例学习排序。把每个视频当作一个“包”视频里的片段当作“实例”正常视频包里全是负实例异常视频包里至少存在一部分正实例。训练时取异常视频里分数最高的那几个片段作为正样本代理取正常视频里分数最高的几个片段作为负样本代理拉大两组分数差距。这个思路在当时很新颖但它存在一个明显问题异常视频里top-k选出的片段很可能压根不是真异常。比如视频里有人闯进办公室前几秒推门动作本身线索很弱模型可能把后景抖动或者光照变化当成高分片段梯度方向就被带偏了。后续改进大致沿几条线走用注意力机制筛选关键片段用图结构建模片段间依赖用自监督任务比如预测片段顺序、判断时序是否打乱补充训练信号。再往后大家慢慢意识到异常事件之所以能被识别很大程度上是因为它和前后语境“不搭”。一个人跑步是正常在走廊里突然跑步可能也正常但如果整条视频里所有人都在慢速行走只有某一段跑起来那这段就极可能是异常。于是时间上下文建模成了主战场。跨片段特征相似性就是在这个背景下出现的工具与其只关注片段自身特征不如显式建模片段之间的相似度让每个片段从和它相似或相邻的其他片段那里获得上下文信息。2. 核心设计拆解跨片段特征相似性和多尺度时间MLP的价值2.1 跨片段特征相似性片段和片段的互相印证先说一个直觉例子。一段安静走廊的监控视频正常情况下相邻片段内容几乎一致同一个人匀速走过背景基本不动。某个瞬间有人猛跑过去对应片段的运动特征和前后片段都不像。如果只看这个片段本身模型得硬学“跑这个动作是异常”可跑这个动作在其他场景里完全正常但把这个片段放在整段视频的相似性矩阵里看它跟周围片段相似度急剧下降这种“格格不入”的特征比单个动作本身要稳定得多。跨片段特征相似性模块做的事可以理解成给每个片段做一次加权聚合。对一条特征序列先计算任意两个片段之间的相似度得到一个T乘T的矩阵T是片段数矩阵里每个元素表示当前片段和另一个片段有多像。然后用这个矩阵对片段特征进行加权求和跟当前片段越像的片段贡献越大。这样每个片段的最终特征就不是孤立存在的而是带着整条视频的“观众席意见”。好处非常直接正常片段在聚合后更接近周围正常片段特征更加平滑异常片段因为和上下文差异大聚合后要么仍然保留明显的异质性要么被上下文突出总之比原地单打独斗更有辨识度。实际操作中相似度计算可以用点积、余弦相似度也可以先过一层线性映射再算聚合时通常还要做一次归一化避免极端值把特征冲爆。这个模块还有个附带好处相似性矩阵本身是可可视化的。把矩阵画成热力图正常视频通常表现出明显的对角线和带状结构异常片段的行和列则会出现颜色突变。做实验写分析时这个可视化比单纯堆数字更有说服力。2.2 多尺度时间MLP用轻量结构把时序关系吃透在序列建模上Transformer、RNN、一维卷积各有各的问题。Transformer的自注意力能建模长程依赖但复杂度是片段数的平方监控视频动辄几十上百个片段训练慢、显存高RNN理论上适合时序但长期依赖训练困难还不能并行一维卷积靠堆kernel大小扩大感受野但局部和全局的粒度很难用一个窗口兼顾。于是多尺度时间MLP出场。它的核心想法是时间维度的关系不一定需要复杂机制把同一个片段的特征和不同时间位置的片段特征做线性混合就够了。所谓多尺度就是同时用几个不同大小的“时间窗口”做混合一个分支只看邻近几个片段一个分支看更大范围一个分支甚至可以看整条序列最后把这些分支的结果融合起来。听到这儿可能有人问这不就是一维卷积加多个kernel吗本质上有相似之处。实现时局部窗口的MLP可以用带padding的一维卷积来高效完成全局混合可以用一个横跨时间维度的全连接层实现。区别在于整个模块可以设计成类MLP-Mixer的结构先对时间维度做混合再沿特征通道做混合交替进行。相比自注意力它参数少、训练稳定而且没有位置编码的额外负担只要在输入端加一点位置信息即可。多尺度的意义在于异常事件的“时间粒度”是变化的。打架可能持续三五个片段遗弃物品的特征变化可能要跨越十个片段才看得出全局反差而整个视频的语义背景则提供了更长的上下文。只用一个窗口必然顾此失彼多分支并行再融合等于让模型自己决定在当前样本里信哪个尺度多一些。2.3 串起整条链路从片段特征到异常分数的完整流程把模块串起来一条完整的前向链路大致是这样第一步把输入视频切成片段序列。通常每个片段包含16帧左右片段之间可以重叠也可以不重叠具体根据数据集规模定。对每个片段用一个预训练好的视频特征提取网络提取特征得到特征序列记为T×C的张量T是片段数C是特征通道数。第二步把特征序列送入跨片段特征相似性模块得到上下文增强的特征。第三步送入多尺度时间MLP做时间维度的多粒度建模输出再叠一层轻量预测头得到每个片段的异常分数。第四步训练阶段用视频级标签计算损失推理阶段直接拿分数做后续处理。训练损失通常由三部分组成。第一部分是MIL排序损失异常视频里top-k片段的平均分要高于正常视频里top-k片段的平均分中间留一个边界。第二部分是稀疏正则异常在整段视频里总是少数所以所有片段的分数均值不能太高不然模型会把大片正常片段判成异常。第三部分是时间平滑正则相邻片段的分数不应剧烈跳变否则定位结果在时间轴上会像噪声一样一闪一闪。三个损失各管一件事权重需要在验证集上稍作调整。推理阶段则简单得多。对一段测试视频提取特征过一遍模型拿到每个片段的分数。想要帧级分数直接把片段分数复制回该片段覆盖的每一帧即可。最终视频级分通常取片段分数的最大值或者top-k平均值。评测时用帧级AUC衡量定位性能用视频级AUC衡量分类性能。3. 复现要点数据、代码与训练必须抠的细节3.1 数据切分和特征提取做得稳后面才省心很多人在复现弱监督异常检测时把精力全放在网络结构上结果被数据预处理坑得头破血流。这个方向的训练数据和常规分类任务不一样一个视频要变成一条特征序列中间的每个环节都会影响最终结果。视频切片段方面常见做法是每个片段取16帧相邻片段的起始帧间隔也是16帧也就是不重叠的均匀采样。如果视频长短不一要先统一处理片段数少的视频可以重复采样对齐片段数过多的视频可以等间隔抽掉一部分保证所有视频在训练时都对齐到固定的T。T取多大合适常见取200到400之间太短会丢掉长程上下文太长显存吃紧而且很多视频根本没有那么多内容。特征提取这一步直接决定模型的上限。我用过的方案是用在大型视频分类任务上预训练好的I3D或者3D卷积网络把视频分类任务中学到的通用运动表征搬过来。提取好的特征一定要离线缓存成文件不要每次训练时现场抽帧现场提特征否则训练一个模型大半时间都耗在解码视频上。缓存下来的特征做一次标准化或者L2归一化通常能让训练稳定不少。标签处理也要注意训练阶段只有视频级标签正样本视频标1负样本视频标0测试阶段的帧级标签只用来做评估绝对不能混进训练过程。另外很多公开基准里正常视频数量会比异常视频多一些直接用会导致模型偏向正常训练时要么把正常视频欠采样要么在损失里给异常类别加权。3.2 核心模块的代码骨架这里给一版简化的PyTorch风格代码骨架主要展示跨片段相似性模块和多尺度时间MLP的落法方便对着思路理解。实际工程里可以根据实验需要调整维度、分支数量和融合方式。跨片段相似性模块的要点是先通过一组线性层计算出查询、键、值表示然后用查询和键做点积相似度归一化后对值做加权聚合最后保留原始特征的残差连接。残差连接很关键它保证聚合后的特征没有丢失原始表达堆多层时梯度也更稳。import torch import torch.nn as nn import torch.nn.functional as F class CrossClipSimilarity(nn.Module): def __init__(self, dim): super().__init__() self.query nn.Linear(dim, dim) self.key nn.Linear(dim, dim) self.value nn.Linear(dim, dim) self.scale dim ** 0.5 def forward(self, x): # x: [B, T, C] q self.query(x) k self.key(x) v self.value(x) attn torch.matmul(q, k.transpose(-2, -1)) / self.scale attn F.softmax(attn, dim-1) out torch.matmul(attn, v) return x out多尺度时间MLP模块用一维卷积实现不同尺度的局部时间聚合等价于在不同时间窗口上做MLP式的特征混合。每个分支的kernel大小对应时间感受野kernel为3看临近片段kernel为9看得更远。分支之间共享输入特征输出拼接后过一层线性门控再同样加一个残差。class MultiScaleTemporalMLP(nn.Module): def __init__(self, dim, scales(3, 5, 9)): super().__init__() self.branches nn.ModuleList() for k in scales: # depthwise conv, 只在时间维度做混合, 通道之间不混 self.branches.append( nn.Conv1d(dim, dim, kernel_sizek, paddingk // 2, groupsdim) ) self.fusion nn.Linear(len(scales) * dim, dim) def forward(self, x): # x: [B, T, C] h x.transpose(1, 2) # [B, C, T] outputs [] for branch in self.branches: outputs.append(branch(h).transpose(1, 2)) # back to [B, T, C] fused torch.cat(outputs, dim-1) return x self.fusion(fused)完整模型简单叠加就行特征序列先进跨片段相似性模块再进多尺度时间MLP最后接一个输出异常分数的线性层。如果想再增加容量可以把这两个模块交替叠两组但先跑通单组版本更重要。3.3 训练设置与损失函数权重训练这类模型最重要的经验是先把损失函数调“稳态”再碰网络结构。我的做法是先把排序损失单独跑起来确认异常视频和正常视频的分数差距能从0拉开再逐步加稀疏正则和平滑正则。次序反过来的话正则项会互相牵扯出了问题根本定位不到是哪个环节崩的。优化器选Adam或者AdamW都行学习率我习惯从1e-4起步配合warmup加余弦退火。Batch size在显存允许的情况下尽量偏大一些因为弱监督信号本身就弱小batch会让排序损失里用到的top-k片段波动很剧烈。Epoch不用贪多特征都是用预训练模型提好的真正可学习的参数集中在少量线性层和轻量模块上训练四五十轮基本就收敛了。损失权重上排序损失固定为1稀疏正则通常给0.01到0.1平滑正则给0.1到0.5。具体的绝对值跟着数据集走不用照搬我的数值。判断正则权重是否合适的标准只有一个在验证集上做消融看第一个千分位的定位AUC变化。如果分数曲线一片平坦说明稀疏权重太大如果分数在时间轴上高频抖动说明平滑权重不够。4. 调参排障弱监督训练里的常见坑和对应解法4.1 模型退化预测分数全部趋于正常弱监督训练最常见的失败模式不是loss不降而是loss降了、所有预测分数却挤在一起分不出正常和异常。原因很直白正常片段数量占压倒性多数排序损失里的负样本拉低了整体区分度模型发现输出一个常数也能把损失压到差不多的水平。排查时先看两件事一是训练过程中异常视频top-k平均分和正常视频top-k平均分的差值曲线如果这个差值始终在零附近波动说明排序损失没起效。二是看预测分数分布直方图正常的话应该是双峰或者长尾偏态分布异常的视频会有少数片段分数明显突出要是所有视频的分数都像一条平线基本就是退化。针对性解法有几个。第一把排序损失的距离边界调大一些强制异常分数比正常分数高出一截。第二减小top-k的k值k值越小参与排序的片段越精悍模型被迫把高分集中在最可疑的少数片段上。第三检查正常视频是否过多尝试做数据重采样。第四给异常视频的损失乘以大于1的权重让模型对异常信号更敏感。4.2 相似性模块的效率问题跨片段相似性矩阵是T乘T的如果片段数T取到几百点积运算和矩阵存储量都会涨得很快。在片段数400到500时还算能忍一旦视频特别长或者想搞密集采样训练速度和显存占用会立刻吃紧。我的处理方式是加一个“局部感知”的开关相似性聚合不一定要覆盖全部片段对可以只让每个片段和它在时间上相邻的L个片段、以及全局随机采样的M个片段做相似度计算。这样矩阵从全连接的T乘T退化成T乘(L加M)效果不会差太多但显存占用和计算量都是数量级下降。这里要提一句训练阶段可以用全局相似性获得最强的上下文信息推理阶段再做局部稀疏化部署通常也只有很小的精度损失。4.3 多尺度参数与消融实验怎么做才严谨多尺度时间MLP的窗口大小不是拍脑袋定死的它和你用的片段粒度直接相关。如果每个片段覆盖16帧且不重叠kernel为3只相当于看48帧的上下文这个尺度对短促的异常事件比较有效kernel达到9到15时能覆盖上百帧的上下文对持续时间较长的异常和背景语义变化更有意义。我先按数据集平均视频长度反推假设平均视频被切成200个片段那么窗口序列从3、5、7起步如果分数曲线过于抖动就把小窗口放大到5、7、9如果上下文不够明确就再补一个11或者15的大窗口。做消融实验最忌一次性把所有模块全部拆掉再对比。正确做法是固定其他条件一次只改一个变量先有基线模型然后单独加跨片段相似性模块对比AUC提升再单独加多尺度时间MLP对比提升最后加两者。为了排除随机性影响每个配置至少在三个随机种子上跑一遍取平均和方差报告。种子一换结果就翻盘的话说明实验设置本身不够稳定先解决稳定性再谈模块贡献。有一个容易被忽略的小坑评估时的分数后处理要全程统一。片段分数生成帧级分数时不同人可能用复制、线性插值或者相邻平滑这会让最终AUC差上一两个点。我建议固定一份评估脚本所有配置都用同一份代码出结果否则对比的可信度大打折扣。5. 个人实操体会和可扩展方向做了一段时间弱监督异常检测我最大的体会是这个方向真正难的地方不在模型结构而在怎么和又糙又弱的监督信号周旋。跨片段特征相似性之所以有效本质上是给模型补上了一种“相对异常”的视角让片段在自己所在视频的语境里找位置而不是孤立地背动作模式。多尺度时间MLP又保证了这种语境建模足够轻量不会因为引入复杂时序模型而把训练难度和显存成本拉高。最后分享一个实操技巧训练时记得每个epoch都保存checkpoint和验证集分数曲线。弱监督模型经常会出现某几个epoch突然跳到好的状态、后面又飘回去的情况有了完整历史才有机会回滚到峰值模型。这套设计除了在离线异常检测场景使用还可以继续往在线流式检测扩展让跨片段相似性只回看历史片段多尺度时间MLP改成因果模式就能做到边接收新片段边输出实时分数。另外把片段级的异常分数再做一层边界细化模块也能缓解预测结果在异常起止位置上的模糊问题。顺着这个方向往后做这个思路的余量还相当大。

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

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

免费获取报价 →
↑