☰
大模型零样本视频异常检测实战:F1从0.09到0.64的调优之路
2026/10/17 3:31:49 网站建设 项目流程

F1从0.09到0.64,做视频异常检测的人看到这组数字,应该都能感受到背后的分量。0.09基本等于系统没法用,漏报一大堆、误报刷屏;0.64放在零样本条件下,意味着一个完全没有经过当前场景数据训练的模型,已经能帮业务方把每天几千条疑似告警压到几百条,人工审核的成本一下就下来了。

我们团队最近在实际安防项目里做了一轮大模型零样本异常检测的实测,用的就是最常见的园区摄像头场景,覆盖周界闯入、人员倒地、烟雾火焰、违规停车这些异常类型。整个过程没有标注新数据,没有微调模型权重,只靠提示词设计、时序聚合和阈值策略调整,就把F1从0.09一步步拉到了0.64。

这篇文章不打算讲什么宏大的概念,纯粹把这轮实测的思路、具体操作、踩过的坑写清楚。如果你也是做安防AI、视频结构化或者任何视频监控场景的算法工程师,尤其是被“异常样本永远不够”折磨过的人,应该能从里面找到几条能直接用的经验。

1. 为什么老方案只有0.09:问题不在算法,在场景定义

1.1 我们当时到底在检测什么

项目背景是某园区的摄像头网络,覆盖区域包括周界围墙、卸货区、消防通道、机房门口。异常事件的种类是开放式的:人员闯入、有人倒地、烟雾、火苗、违规停车、物品遗留。注意“开放式”这三个字,意味着没有任何一个封闭标签集合能覆盖所有情况。

传统思路是每个摄像头标一批数据,训练一个小分类网络,然后跑两阶段:先用前景检测把运动区域框出来,再送进分类器判断是不是异常。这个管线在单一场景、固定角度、光照稳定的前提下勉强能用,但真实安防场景根本不是这样。同一个工位,白天有工人正常搬运,晚上有保安巡逻,清晨有保洁拖地,这些行为的姿态、速度、轨迹有大量重叠。你很难跟一个分类器解释清楚“蹲下系鞋带”和“蹲下呕吐”到底哪里不一样,而这两种情况在像素层面可能高度相似。

异常检测最大的问题不是正样本少,而是负样本无穷无尽。正常行为的长尾比异常行为本身还长,任何“看起来不常见”的动作都会触发误报。这个认知是后续整个方案转型的基础。

1.2 0.09是怎么算出来的

当时按逐帧加短片段评估,一个持续几秒的异常被拉成几十上百帧。旧方案的结果非常惨淡:

指标数值说明
召回率0.33三分之二的真实异常没被捞出来
精确率0.052报出来的东西超过95%是误报
F10.09基本等于不可用

精确率0.052意味着每报20条异常,只有1条是真的。业务方根本没法用,因为告警中心的人只会越来越不信任这套系统,到后面连真告警也不看了。这个数字是一个很重要的经验:安防场景里精确率低比召回率低更致命,误报会直接摧毁整个告警闭环的信任基础。

回头拆原因,有三个主因。第一,前景检测阶段就把目标丢了,很多异常行为运动幅度很小,比如人员缓慢倒地,光流和帧差根本拉不出有效区域。第二,分类器在正负样本极度不平衡下过拟合,训练集里正常样本可能是异常样本的几百倍,模型天然偏向输出“正常”。第三,场景差异大,这个摄像头的“正常”放到另一个摄像头全是“异常”,模型在新点位上的泛化能力约等于零。

1.3 为什么补数据补不动

很多人第一反应是补标注数据。但异常检测的标注成本远超普通分类任务。一个摄像头要覆盖白天黑夜、晴天雨天、春夏秋冬,每类异常至少需要几百个正样本才能训出可用的分类器。一个园区几十个摄像头,这意味着上万小时的视频需要人工逐帧审核,标注周期按年计算。

更麻烦的是异常事件本身是低概率事件。哪怕摄像头挂在那一年,真实发生的倒地、闯入可能就是几十次,你收集都收不齐,更别说标注。就算咬牙标注了一个点位,换一个视角模型基本失效,之前的工作全部归零。

所以当时我们团队讨论的结果是:传统监督路线在这个场景里没有出路,必须换一种不需要场景标注的思路。大模型零样本方案正好在这个时间点进入候选范围。

2. 零样本异常检测方案:先把骨架搭对

2.1 选型:图文对齐底座比生成式模型更适合实时告警

做大模型零样本异常检测,圈子里主流有两条路线。一条是生成式多模态大模型,输入视频帧和问题文本,直接输出“正常”或“异常”以及原因描述。另一条是图文对齐底座模型,图像和文本各自编码进同一向量空间,用相似度判断匹配程度。

我们两条都做了小规模验证。生成式路线的效果其实挺惊艳,能说出“一个穿红色衣服的人躺在地上”这种自然语言描述,但工程上完全不可用:单帧推理延迟2到3秒,根本无法接实时视频流;每帧都丢给大模型,GPU成本扛不住;输出还不稳定,同一个画面多问几次可能给出不同判断。异常检测本质上是高频过滤问题,先要从几十上百路摄像头里筛出可疑片段,而不是让模型对每一帧做开放式的长篇大论。

图文对齐底座的优点正在于此。它输出一个分数,单帧推理在主流GPU上几十毫秒到一百毫秒,可以支撑多路视频的准实时处理。虽然不能直接告诉你“为什么异常”,但异常检测业务本来也不需要模型写小作文,只需要一个可靠的排序分数。

对比项生成式多模态大模型图文对齐底座模型
单帧延迟2秒以上几十毫秒到一百毫秒
成本每帧都要推理大模型,很贵成本低一个数量级
输出稳定性同一帧多次结果可能不同相似度分数确定性高
可解释性能输出自然语言描述只能提供匹配分数
工程落地难度高,很难做实时低,适合做大规模筛选

最终我们选的是图文对齐底座,但有个核心认知必须先扭转过来:这个底座不是分类器,是一个语义相似度打分器。你不能问它“这帧正常吗”,只能给它一组描述正常行为的文本和一组描述异常行为的文本,让它分别计算相似度,再做比较。

2.2 帧级打分链路的设计

整个系统的处理链路分五步:视频解码抽帧、ROI区域处理、帧级特征提取、文本相似度计算、时间维度聚合。

# 核心流程:视频帧 → 视觉特征 → 异常分数 def frame_score(frame, abnormal_texts, normal_texts): frame_feat = vision_encoder(frame) # 提取图像特征 abnormal_feat = [text_encoder(t) for t in abnormal_texts] normal_feat = [text_encoder(t) for t in normal_texts] score_abn = max(cosine(frame_feat, t) for t in abnormal_feat) score_nor = max(cosine(frame_feat, t) for t in normal_feat) return (score_abn - score_nor) / temperature

为什么异常分要减去正常分?因为图文对齐模型对任何文本都有一定的相似度基数,一个园区画面匹配“人员正常行走”的分数可能也接近0.22,直接拿原始相似度做阈值会在不同摄像头之间飘。减掉正常分相当于做了一个文本层面的差分,能抵消掉一部分与视觉内容无关的系统偏差。

温度系数也很重要,它控制分数分布的锐利程度。温度太小,任何细微差异都会被放大成异常;温度太大,正常和异常混在一起。我们测试下来0.05到0.1这个区间比较合适,具体值跟你用的底座模型有关,建议固定一个值后全流程不要频繁改,否则分数分布一直在变,后面再做阈值就无从谈起。

2.3 提示词模板库:别把描述写成长作文

第一版提示词写得很详细,类似“一个工人躺在地上,可能是晕倒或者身体不适,需要紧急救助”。结果模型几乎完全没有反应。后来才发现,这类底座模型对过长的描述性文本并不敏感,它更擅长理解短语义原子。

这个现象背后的逻辑是:图文对齐模型在预训练阶段学到的文本表示,更偏向名词和动词的核心语义,复杂状语从句和场景描述反而稀释了关键信息。就像你跟同事说“监控里那个躺在地上不动的人”和“监控里那个穿着蓝色工作服、仰面躺在地砖上、看起来像是突然晕倒的人”,后者信息量更大,但在模型的特征空间里,前者的语义词根更清晰。

我们把提示词结构化为三元组:

  • 主体:人、车辆、烟雾、火焰、包裹
  • 动作/状态:倒地、奔跑、横穿、冒烟、燃烧、静止放置
  • 场景位置(可选):消防通道、卸货区、周界围墙

异常提示词示例:

1. 一个人倒地 2. 一个人躺在地上一动不动 3. 不明人员翻越围墙 4. 大量烟雾 5. 火焰燃烧 6. 车辆停在消防通道 7. 不明包裹遗留在角落

正常提示词同样重要,这是很多人忽略的地方。异常分减掉正常分之后,正常提示词相当于在给场景画“安全边界”,模型会先把画面内容和已知正常语义做匹配。没写正常提示词时,很多正常动作被当成异常。加了之后,误报立刻降下来。

2.4 阈值的初始设定和评估口径

阈值一开始用的是固定方式:在少量验证帧上算出分数分布,取P95或P99作为初始阈值。这个方案很快暴露出问题,不同摄像头的分数分布差异很大,同一个摄像头白天和晚上也差异很大。固定阈值要么白天误报爆炸,要么晚上漏报严重。

评估口径我们同步做了定义。一个持续8秒的倒地事件,逐帧算的话就是几百帧,逐帧F1会被这类长事件严重拉偏。我们改成事件级评估:一个持续数秒的异常事件算一个样本,只要检测系统能在事件发生后连续触发3秒以上,就算一次正确检测。这个口径对后文0.64这个数字影响非常大,建议做同类项目的团队先把这个口径定下来,不然后面每次对比都在拿不同尺子量。

3. 四轮实测调优:F1怎么从0.09爬到0.64

3.1 第一轮:直接拿大模型当普通分类器,结果只有0.21

第一轮实测我们做得比较粗暴:既然大模型懂语义,那把视频帧送进模型,让它直接判定当前画面是正常还是异常。试了两条路线,生成式模型输出文本标签,图文对齐底座输出一个异常分数加固定阈值。结果都算不上好,F1只有0.21左右。

这个结果其实已经比0.09强了一倍多,说明大模型的语义理解确实能捕捉到一部分异常信息。但离可用差很远。看具体坏在哪:

  • 生成式模型结果不稳定,同一帧换个提问方式,判断就翻转。
  • 缺少负向语义约束,模型对“正常但少见”的行为过度敏感。夜班保安正常巡逻,因为它“不常见”,模型就判成异常。
  • 长尾场景语义重叠严重,电动车经过、树叶影子晃动都被归属为“不明移动物”。

提示:这一轮最大的教训是,大模型不是分类器,是打分器。你需要做的是给它两堆积木——正常描述和异常描述——让它比较画面更贴哪边,而不是让它自己从“正常/异常”两个词里做选择。后者是在浪费它学到的世界知识。

3.2 第二轮:提示词重构加负向语义,F1到0.38

第一轮彻底失败之后,我们把重心放到提示词工程上。做了三件事。

第一,把模板统一为“主体+动作/状态+场景位置”的短句结构,长句全部拆掉。第二,给每个异常模板配一个对应的正常负模板,例如“行人快速奔跑”对应的负模板是“行人正常行走”,通过分数差来抑制模型对正常行为的冲动。第三,多模板投票机制,不依赖单条提示词,取多个异常模板相似度的最大值作为异常分,正常模板同理取最大值,然后做差。

这里有个值得说的经验:聚合方式用最大而不是平均。理论上平均更稳健,但在安防场景里,一个异常事件只要有一两条模板精准命中,分数就会很高,用最大能保持这个信号。平均会把高分稀释掉,导致“有点异常但不够异常”的中间态遍地都是。当然模板数量不能无限加,我们控制在8到12个以内,模板太多反而会引入噪声。

这一轮的提升非常明显:

版本精确率召回率F1
直接把大模型当分类器0.150.390.21
提示词结构重写+负向语义0.330.450.38

我当时挺震惊的,没有改任何模型权重,只是把文本描述从长作文改成短语义,再补了几个负模板,F1直接涨了0.17。原因是模型的输入空间被约束住了,它只需要做“语义匹配”而不是“自由联想”。给模型的选择越清晰,它的输出就越稳定。

3.3 第三轮:多帧时序聚合,F1到0.52

单帧分数再稳也有噪声。码流抖动、行人突然加速、树枝摇晃,都会让某几帧的异常分突然飙高。如果按单帧告警,一个正常工作的人可能一天被误报几十次。

我们加了时序聚合状态机,思路很简单:滑窗5帧,其中至少3帧的异常分超过阈值才触发事件;连续触发才进入“已确认”状态,输出一条告警;告警结束后,如果连续若干帧恢复正常分,则关闭事件。

选择等待3帧意味着会延迟0.3到0.5秒,对倒地、闯入这类持续数秒的事件来说,完全能接受。但对突然冒烟起火这类需要秒级响应的事件来说,这个延迟也可以通过调小滑窗来压缩,比如3帧中2帧触发就告警。参数没有绝对标准,要看业务接受度。

时序聚合带来的收益很大,假阳率降了差不多四成,F1从0.38跳到0.52。这个阶段我们还处理了一个特殊情况:摄像头在夜间会自动切换红外模式,画面整体像素值突变,模型会把整帧当成“异常光照变化”。后来加了摄像头状态检测,模式切换后的一两秒内不参与打分,问题直接消失。

3.4 第四轮:自适应阈值加分数校准,F1到0.64

固定阈值在这个阶段成了最大瓶颈。白天的正常帧平均异常分比晚上低一截,同一个模型在不同摄像头上的分数分布也不一样。一个在白天调好的阈值,到了晚上就会产生大量误报;一个在强光照摄像头下调好的阈值,放到昏暗消防通道上就会全部漏报。

我们的解法是给每个摄像头维护一个正常帧分数缓冲队列,只保存最近30分钟的非告警帧分数,用P95作为动态阈值。画面分数超过这个动态基线才判定为异常,相当于每个摄像头有一套根据自己的光照和场景持续更新的标准。如果摄像头画面持续正常,队列会慢慢适应当前环境;如果进入异常事件,那些高分帧不会推入队列,避免污染基线。

分数校准也加上了一层,把相似度差值通过logistic函数映射到0到1区间,再用映射后的分数和动态阈值比较。这样做的好处是分数分布更接近概率语义,阈值调整更直观,也方便不同摄像头之间做横向对比。

def adaptive_threshold(frame_scores, window_seconds=1800, quantile=0.95): # frame_scores: 当前摄像头最近一帧的分数 # 维护一个存储历史正常帧分数的队列,长度按帧率换算 normal_queue = deque(maxlen=window_seconds * fps) dynamic_threshold = max( minimum_threshold, percentile(normal_queue, quantile) ) return dynamic_threshold

到这轮结束,事件级评估结果稳定在:精确率约0.71,召回率约0.58,F1 0.64。精确率比召回率高不少,业务上这是个偏保守配置。实际上可以往回收一点阈值,把F1推到0.66左右,但误报会变多。我们最终没有刻意追求最高F1,而是给业务方调到一个“每天误报能数得过来”的状态。

阶段关键调整F1
直接当分类器单帧+固定阈值0.21
提示词优化模板重构+负向语义0.38
时序聚合滑窗投票+状态机0.52
自适应阈值滚动分位数+logistic校准0.64

4. 高频问题排查与避坑速查

4.1 模型“看不见”小目标怎么办

零样本方案最大的痛点是目标太小。一个距离摄像头几十米的倒地人员,在1080P画面里可能只占5%的像素。模型对整帧提取特征时,注意力基本被大面积的地面、墙面、天空背景分走,小目标被完全淹没。

想确认模型到底在看哪里,可以可视化注意力热力图。我们实测下来,模型在小目标场景下确实没有聚焦到目标区域。解决办法不需要换模型,只需要改输入视角。

一是ROI裁剪。对整帧跑一个轻量的前景检测,把运动区域放大后再送入打分模型。这样做异常目标在画面中的占比变大,模型更容易捕捉到语义。二是分块滑动窗口。把画面切成2×2或3×3的网格,每个网格单独送模型打分,取最高分作为该帧的异常分数。网格方式计算量会乘网格数,但异常语义的捕获率明显提升。

小目标优化是我们最后阶段F1还能继续往上拉的主要来源,后续还可以结合跟踪框做限制,只在有目标的区域打分。

4.2 误报集中在哪,怎么快速找共性

误报不能拍脑袋猜,要让模型自己“招供”。我们把一周的误报帧全部捞出来,对每一帧计算它与各个异常模板的相似度,取相似度最高的模板作为这条误报的归因类别。汇总后误报的规律非常清晰,集中在几类:

  • 夜间灯光闪烁、手电筒扫过,被归因到“异常光源”
  • 树木影子、飞鸟掠过,被归因到“不明移动物”
  • 落叶和塑料袋飘过,被归因到“漂浮物”

针对每一类共性问题,操作很简单:把这些语义加入正常提示词,作为免训练白名单。某个摄像头的落叶特别多,就给这个摄像头单独加一条“落叶飘落”正常模板;夜间灯光乱闪,就加“灯光闪烁”正常模板。这本质上相当于不训练模型,只训练文本空间,成本很低。

如果某类误报在多个摄像头都出现,说明提示词本身太宽泛,需要收缩语义。比如“不明移动物”这个模板就太宽了,它能匹配各种乱七八糟的东西,应该改成“人员快速奔跑”这种更具体的语义,缩小误报范围。

4.3 评估口径的坑:事件级和逐帧级,数字差一倍

零点几个F1的差异,很可能不是模型好坏,而是评估口径不同。我们的事件级F1是0.64,但如果按逐帧计算,只有0.39左右。

逐帧指标有个隐藏问题:它会奖励那些“把一个异常长视频持续判为异常”的保守模型,哪怕它在视频的前几秒没发现异常,只要后面一直触发,帧级指标也会不错。但它会惩罚“精准发现异常但只触发了几秒”的灵敏模型。这两种模型在业务上恰恰是后者更有价值。

所以统一口径很重要。我们内部迭代和对外汇报都用事件级,具体标准是:检测连续触发超过3秒记为一次有效事件,事件开始时间落在真实异常事件时间范围内,算True Positive。这样评估出来的结果才真正对应业务方关心的“你们一天告警几次、其中有几次是真的”。

4.4 零样本方案目前的边界

这套方案不是万能的,我尽量把适用边界说清楚。

目前能用的场景包括:场景级别的异常,人员倒地、周界闯入、烟雾火焰、车速异常、物品遗留,这些都能取得不错的效果。作为预筛选模块,把每天海量视频先粗过滤一遍,把真正可能有问题的事件压缩到原来的十分之一,完全可行。

不建议强上的场景包括:需要精确到个体级别的异常,比如“人群中第三个穿黑衣服的人在打架”;严重遮挡的密集人流中识别个体行为;需要理解长视频上下文逻辑的事件,比如“先逗留观察再实施盗窃最后离开”这种多步骤行为。零样本模型对单帧或短时间窗口的语义理解很强,但对跨分钟级的事件因果推理很弱。

提示:零样本方案真正的定位是冷启动和预筛选,不是替代所有传统模型。用大模型把数据量降下来之后,再对剩余候选片段做精细判断,性价比最高。

5. 一些还没写进正题的经验

最后聊点个人体会。最开始听到“零样本异常检测”这个概念,我心里是打鼓的,一个连当前场景画面都没见过的模型,怎么可能比我在这个点位标了三个月数据训出来的小模型强?但实测数据说明,它不一定更强,但确实更适合冷启动。

做这轮优化最大的收获不是某个模型的参数,而是“把问题定义清楚”这件事。F1每一步的上涨,都不是靠把模型换得更大,而是靠提示词结构、时序窗口、阈值策略这些环绕模型的外围工程。模型只是发动机,提示词是方向盘,阈值是油门,光踩油门不修方向,车只会越跑越偏。

再分享一个小技巧:如果你的系统已经运行了一段时间,正常帧的积累非常多,可以挑当前场景的几十帧正常图像作为参考锚点,把这些图像和正常提示词做绑定,让原本的零样本变成“轻少样本”。我们在某些摄像头下试过,F1还能再涨三到五个点。这个方案比标注训练数据便宜太多,效果却接近一个弱监督模型。

目前这套检测器已经接进了某个园区的实时告警流,每天产出的疑似事件从几千条降到几百条,业务方终于愿意排人去处理告警闭环了。异常检测这条路往后还有很长一段要走,至少我们现在知道,起点不应该放在标注数据上,而是应该放在语义和场景理解的边界上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询