1. 先把问题拆开:Video Grounding 到底在算一道什么题
Video Grounding 这个词第一次出现在我的任务清单上时,我下意识觉得它是个“带时间轴的搜索”——输入一段视频加一句话,输出一个起止时间。真做完一轮才发现,检索只是表层,真正吃掉大部分工期的是边界:模型能大致知道“东西在视频后半段”,但它说不准是第 47 秒开始还是第 51 秒开始,而这 4 秒的差距在业务侧往往就是“能不能用”的分水岭。
先把这个任务的骨架说清楚,后面所有选型都建立在这三要素上:输入是一段长度不定的视频 V 和一条自然语言查询 Q,输出是一个时间区间 [t_s, t_e],要求这个区间在语义上与 Q 描述的事件、动作、片段对应,同时尽量贴合人工标注的边界。它解决的问题很朴素——视频是非结构化的、以秒为单位的时序信号,而人的需求是“把提到那件事的那一小段找出来”。适合参考这篇内容的人大致分三类:正在做视频理解相关功能、需要把“一句话”映射到“一段时间”的工程同学;手上有一批带时间戳标注的视频数据、想验证能不能训出可用模型的研究同学;以及产品侧想知道这个能力边界在哪、什么场景别硬上的同学。
它和几个相邻任务特别容易被混为一谈,我在需求评审上就见过把三者当一件事的情况。时序动作检测只关心“动作类别 + 起止”,没有语言输入,类别是闭集;视频问答输出的是答案文本,不要求定位;视频摘要/高光检测输出的是“值得看的片段”,没有查询条件约束。Video Grounding 的特殊性在于它是开放语义 + 精确边界的组合:查询是自由文本,所以你要处理语言的长尾表达;输出是连续区间,所以你要处理回归精度。这两件事叠在一起,难度不是相加而是相乘。
还有一个常被低估的点:图像里的目标检测,框是离散的、边界是“物体轮廓”,人有共识;而视频里的“这件事”边界是模糊的。“他打开冰箱拿了一瓶水”,拿水的动作从手伸进去算还是从握住瓶子算?标注员之间就能差出两秒。所以这个任务从数据那一端就带着噪声,模型不是在学习一个确定函数,而是在学习一个人群标注的分布中心。理解这一点,后面看指标、调阈值、做验收的时候心态会完全不一样——你追的不是 100% 准确,而是“和标注习惯对齐”。
2. 方案选型:两阶段、单阶段、集合预测,我分别在什么情况下用
2.1 两阶段方案:候选生成加跨模态排序,最稳但天花板明显
两阶段的思路非常工程化:先不管文本,只从视频里生成一批候选时间段(滑窗、基于动作性分数峰值、基于片段边界的聚类),再用跨模态模型给每个候选打一个“和这句话匹配”的分数,排序取 top-k。它的好处是每一段都能被解释:候选是怎么来的、分数为什么高,你都能拆开看。线上出问题时,你可以单独检查候选生成有没有漏——如果漏了,那是召回问题,跟文本模型无关;如果没漏但排错了,那是排序问题。这种可归因性在项目早期极其宝贵。
我一般用滑窗做冷启动的基线:窗口长度取视频平均事件时长的 1.5 到 2 倍,步长取窗口的一半。举个例子,如果业务里的事件平均 40 秒,那窗口就设 64 秒、步长 32 秒。一段 10 分钟(600 秒)的视频,窗口数 = (600 - 64) / 32 + 1 ≈ 17 个,每个窗口再往后扩几个尺度(0.5x、0.75x、1x、1.5x),候选规模在 60 到 70 个,完全可控。这套东西的检索指标通常能到一个“能交代”的水平,但它的天花板也很清楚:候选的粒度决定了精度的上限。如果真实片段是 7 秒而你的最小候选是 16 秒,那你无论排序多准,IoU 最高也就 0.4 出头,永远过不了 0.5 的线。
2.2 单阶段稠密预测:把定位拆成“逐帧打分 + 边界回归”
单阶段的思路是把每个时间步(或者每个短片段)当成一个二分类问题:这一步是否落在目标区间内,同时回归它到左右边界的距离。训练时对每一帧都算损失,监督信号非常稠密,小片段也能拿到足够的梯度。这类方案在推理时很轻:一次前向,拿到帧级分数曲线,取峰值附近的连续段作为输出。
它最香的地方是能吃到细粒度监督,短片段召回明显好于纯滑窗方案。但它有个天然毛病:帧级分类学的是“在不在里面”,而边界附近的帧本身语义就模糊,模型倾向于把分数曲线学得很平,导致输出区间要么偏长(把相邻内容吞进来)要么被后处理的阈值切得偏短。所以这类方案必须配合一个像样的后处理:先对分数曲线做平滑(滑动平均或高斯核),再以峰值为中心向两侧扩展到分数跌破峰值某个比例的位置,最后用回归头预测的边界做一次修正。只靠分类分数取阈值,我试过,边界抖动非常厉害。
2.3 集合预测式结构:端到端很漂亮,但训练期是真的磨人
集合预测的思路是直接让模型输出固定数量的 (起, 止, 置信度),训练时用二分图匹配把预测和真值一一配对,只对配对上的算回归损失和分类损失。它的好处是不依赖手工候选、天然去重、可以端到端。我第一次跑通的时候确实觉得优雅,但后面两周基本都在跟训练稳定性较劲:匹配结果在早期几个 epoch 里剧烈跳变,同一个真值这轮配给 query 3、下轮配给 query 17,模型一直在重新学;学习率稍大就发散,稍小就卡在一个“所有预测都挤在视频中间”的退化解上。
后来我的处理方式是三条:一是匹配成本里加一项时序位置先验,让靠得近的预测更容易被匹配上,减少早期抖动;二是预热阶段先只训分类头,让每个 query 先分化出各自负责的时间区域,再打开回归损失;三是给不同尺度的 query 初始化不同的参考区间,不要全部从零开始学。这三条加上之后,收敛曲线才变得正常。如果你团队里没有专门的人愿意花时间调训练稳定性,我建议先别碰这条路线。
2.4 三条路线怎么选:一张对照表说清楚
| 维度 | 两阶段(候选+排序) | 单阶段(稠密预测) | 集合预测式 |
|---|---|---|---|
| 冷启动速度 | 快,一周内能出基线 | 中,需要调后处理 | 慢,训练稳定性成本高 |
| 边界精度 | 受候选粒度限制 | 中上,依赖回归头 | 高 |
| 短片段召回 | 差(小于最小候选就废) | 好 | 好 |
| 可归因性 | 强,召回/排序可分离 | 中 | 弱 |
| 推理成本 | 高(候选数 × 一次前向) | 低(一次前向) | 低 |
| 适合阶段 | 需求验证、数据量少 | 有稳定数据、追求性价比 | 数据充足、追求精度上限 |
我自己的习惯是:先用两阶段跑出一个能看的数字,用它来验证标注质量到底行不行。如果两阶段在验证集上连 0.5 IoU 的线都摸不到,八成不是模型问题,是标注一致性或者特征分辨率的问题,这时候换再花哨的结构都是白搭。等确认数据没问题,再决定往单阶段还是集合预测上走。
3. 数据这一关:标注规范、一致性校准与样本构造
3.1 标注规范要写到“边界对齐什么动作”,而不是“标出相关片段”
我见过最省事也最坑的标注规范只有一句话:“请标出与描述对应的视频片段”。结果就是标注员 A 把“他起身走向门口”从身体开始前倾算起,标注员 B 从脚迈出第一步算起,同一个视频两条标注差了三秒。模型学到的就是“平均偏保守”或者“平均偏激进”,边界永远差一点点。
后来我改的规范是这样的:边界对齐到语义动作的起势与收势,并给出三个具体判据——出现可辨识的动作启动信号(身体位移、手部离开静止位置、声音事件起始)为起点;动作完成并进入静止或下一动作的过渡帧为终点;如果描述里包含两个连续动作,以第一个动作的起点到最后一个动作的终点为区间,中间的停顿不切开。另外强制要求标注员给每条标注打一个“边界置信度”(高/中/低),低置信度的样本在训练时降权或者直接不进训练集。这一条改动听起来麻烦,实际把标注返工率降了一大截。
3.2 一致性校准:先量一遍标注员之间的偏差,再决定模型能到哪
在正式训模型之前,我会做一件很多人跳过的事:抽 200 条样本,让三个人独立标一遍,然后两两算时序 IoU。这个数字非常能说明问题。如果平均 IoU 只有 0.6,那说明人类自己的上限就在 0.6 附近,你的模型能到 0.55 就已经很不错了,别拿 0.8 当目标折磨自己。如果平均 IoU 有 0.85,那说明任务定义清晰,模型还有很大提升空间。
这个校准还有一个副作用很有用:能揪出“特别难标”的那批样本。通常是两类——描述里带指代(“他拿起刚才那个东西”)和描述里含否定(“他没有站起来的那段”)。前者的解法是把指代改写成具体名词,后者我一般建议产品侧直接不接受,因为否定的时序范围在逻辑上是不连续的,模型很难学。
3.3 训练样本怎么造:正样本阈值、忽略区间与负样本配比
如果是稠密预测方案,每个时间步都要给标签。我的设法是:与真值 IoU 大于 0.7 的候选片段记为正样本,小于 0.3 的记为负样本,中间 0.3 到 0.7 的设为“忽略”,不参与损失。为什么留这个灰区?因为 0.5 左右的片段语义上既不算对也不算错,硬给它一个标签,等于把标注噪声直接灌进梯度里。
负样本也不是随便抓。纯随机采样会让模型见到的负样本大多是“明显无关”的(比如完全不同的场景),学到的判别力很弱。我的做法是难负样本采样:优先取与真值时间上相邻但不重叠的片段(IoU 在 0 到 0.3 之间),以及同一视频里语义相似但对象不同的片段。这类负样本才真正逼着模型去看文本细节。正负比控制在 1:3 到 1:5 之间比较舒服,比例再高容易把模型训成“保守派”,分数普遍偏低。
3.4 数据切分里的隐形地雷:按视频切,不要按片段切
这条我踩过,代价是两周的实验全部作废。当时的做法是把所有 (视频, 查询, 区间) 三元组打散后随机切 train/val,结果验证集指标高得离谱,上线后直接掉一半。原因很简单:同一个视频的不同查询会同时出现在训练集和验证集里,模型记住了这段视频的视觉特征,等于开卷考试。
正确做法是以视频为单位切分,保证验证集里的视频在训练集里一帧都没出现过。如果业务场景是“同一批视频里新增查询”,那还要额外准备一个“同视频不同查询”的测试子集,单独看这个子集上的表现——它反映的是模型的文本泛化能力,而不是视觉记忆能力。这两个数字经常差很多,只看总分会被误导。
4. 特征与编码:视频侧怎么压,文本侧怎么对齐
4.1 抽帧率是第一个要算清楚的账
设 1 fps 还是 2 fps,不是拍脑袋定的,要看你业务里最短的目标片段有多长。如果最短片段是 3 秒,1 fps 只给你 3 个特征点,边界回归的精度极限就在 ±0.5 秒;如果要做到 ±0.2 秒,抽帧率得提到 4 fps 以上。但抽帧率翻倍,特征存储和前向计算也跟着翻倍。
算一笔具体的账:一段 600 秒的视频,1 fps 得到 600 个时间步,视觉特征取 512 维、float32 存储,占用约 600 × 512 × 4 字节 ≈ 1.2 MB;换成 2 fps 就是 2.4 MB。单看不多,但如果有 50 万条视频,就是 600 GB 和 1.2 TB 的差别,而且这些特征要反复读、反复喂给模型,IO 会变成瓶颈。我的经验是用 1 fps 做粗定位、用 4 fps 只在候选区间内做精修,两级配合,存储和精度都能兼顾。
4.2 冻结视觉编码器还是微调:先算清楚成本再谈效果
微调视觉编码器通常能带来一两个点的提升,但代价是特征不能预先离线算好,每条样本都要过一遍视觉主干。假设主干在单卡上的吞吐是 60 帧/秒,一条 600 秒的样本要 10 秒,一个 batch 32 条就要 5 分钟以上,一轮训练按千级样本算就是几十小时。这个成本在验证阶段基本不可接受。
我的折中是:主实验全程冻结视觉编码器,用预提取特征训练时序头和跨模态对齐层;等结构定下来、确实需要冲最后一两个点了,再做一次小学习率的端到端微调,并且只解冻主干的最后两三个 block。冻结方案还有个额外好处——特征可以预先算好存成内存映射文件,训练时读盘速度能提升一个数量级,迭代节奏完全不一样。
4.3 多尺度时序特征:别只用一个时间粒度
单一时序粒度会同时吃亏在两头:粒度太粗,短片段被抹平;粒度太细,长片段的整体语义看不出来。我一般取三层:原始帧级特征、2 倍下采样的片段级特征、8 倍下采样的段落级特征,然后拼接或者用轻量的融合层加权。做融合的时候注意各层的维度要统一到同一空间(比如都投到 256 维再拼),否则高维那层会主导梯度。
文本侧相对简单,用预训练语言编码器取 token 级输出再做一个注意力池化就够了。但有个细节常被忽略:文本长度分布要和训练集对齐。线上查询平均 12 个词,训练集里全是被截断到 40 个词的样本,模型对短查询的敏感度会下降。我的做法是在训练时按真实长度分布采样(长查询降采样、短查询过采样),比单纯截断有效。
5. 模型结构与训练细节:那些真正决定成败的参数
5.1 边界回归头:预测“距离”比预测“坐标”稳得多
直接回归绝对时间戳(第 47.3 秒)在长视频上表现很差,因为输出范围的量纲随视频长度剧烈变化。改成预测相对锚点的偏移量——当前时间步到左边界的秒数、到右边界的秒数,并且对偏移做对数变换(log(1 + Δ))——训练稳定性和精度都会好一大截。对数变换的作用是把“差 0.2 秒”和“差 20 秒”的惩罚拉到同一个量级,避免长片段样本主导损失。
另一个必做的处理是给偏移量设上限并做截断。如果不设上限,模型在遇到极端样本(比如真值区间跨了整个视频)时会产出巨大的梯度,一次就把这一批的更新带偏。我一般把最大偏移设成 128 秒,超出的截断,同时在损失里对这些样本降权。
5.2 损失配比:分类、回归、对齐三条线要分开调
我用的组合大致是三项:帧级二分类用带 focal 加权的交叉熵(正样本占比通常只有 5% 到 15%,不加权会被负样本淹没);边界回归用 L1 加广义 IoU 的组合,L1 负责把位置拉近,GIoU 负责整体重叠度,两者比例大概 1:1;跨模态对齐用对比损失,让匹配的(视频片段,文本)在特征空间靠近,不匹配的推远。
一维的 GIoU 计算很简单:设预测区间为 [a, b],真值为 [c, d],交集 I = max(0, min(b,d) − max(a,c)),并集 U = (b−a) + (d−c) − I,IoU = I/U;再取能同时包住两者的最小闭区间长度 C = max(b,d) − min(a,c),GIoU = IoU − (C − U)/C。范围在 [−1, 1],预测完全错位时是负值,这个负值信号比单纯的 IoU 有用得多——它告诉模型“你不仅没重叠,还错得很远”。
调这三项权重的时候有个经验:先固定对齐损失,只调分类和回归,等定位指标稳定了再打开对齐损失微调。三个一起从零开始调,你很难判断指标不动是谁的问题。
5.3 后处理:分数平滑、峰值扩展与时序 NMS
推理阶段拿到的是一条分数曲线。我的标准流程是四步。第一步做长度为 5 的滑动平均平滑,抹掉单点毛刺;第二步找所有局部极大值,每个极大值向两侧扩展,直到分数跌破峰值的 0.6 倍,得到一个候选区间;第三步用回归头预测的边界对这些区间做一次收缩或扩张修正,修正幅度限制在原长度的 ±30% 以内,防止跑飞;第四步做时序 NMS,IoU 阈值取 0.5 到 0.7。
NMS 阈值的选择有讲究:如果你要输出 top-1,阈值可以设高一点(0.7),保留更多细节候选;如果要输出 top-3 给用户选,阈值要设低一点(0.5),否则三个结果高度重叠,用户体验很差。另外滑窗方案在重叠区会产生重复预测,我不用硬 NMS,而是对落在窗口边缘的预测乘一个惩罚系数(比如 0.8),因为边缘预测的上下文不完整,可靠性天然更低。这个细节对最终指标的影响比换模型结构还明显。
6. 评测指标与业务对齐:数字好看不等于能用
6.1 R@n IoU=m 到底在衡量什么
最常用的指标是 R@1 IoU=0.5:取模型置信度最高的那个预测,如果它和真值的 IoU 大于等于 0.5,算命中,最后统计命中比例。还有 R@5、IoU=0.3、IoU=0.7 这几档。看这几个数字的组合能判断出问题在哪:IoU=0.3 高但 IoU=0.7 低,说明粗定位没问题、边界不准,该去调回归头和后处理;IoU=0.3 就低,说明根本没找对地方,问题在特征或跨模态对齐。R@1 和 R@5 差距大,说明正确结果在候选里但排序错了,那就该去调排序模型或者加对比学习。
我自己看实验的第一眼永远是这两组对比,比看 loss 曲线有用得多。
6.2 阈值不是拍出来的,是从业务成本倒推的
模型输出的置信度是个连续值,最终要不要返回给用户,得定一个阈值。这个阈值不能看验证集上哪个点指标最高,要看业务侧的错误成本。如果误报的代价是“用户多看一眼、滑走”,那阈值可以放低,宁可多给;如果误报意味着下游流程要处理一段完全无关的视频(比如自动剪辑、自动打点),那阈值必须拉高,宁缺毋滥。
我一般的做法是画一条“阈值—精确率—召回率”的曲线,然后跟业务方一起在曲线上挑点,而不是自己关起门来定。这个过程还能顺带暴露一个常见误解:很多人以为模型输出的是一个“正确的概率”,其实它只是排序用的相对分数,跨视频、跨查询都不一定可比,不要拿它做绝对判断。
7. 常见问题与排查清单:这些坑我基本都趟过
7.1 loss 一直在降,指标纹丝不动
这是最高频的问题。第一嫌疑是数据泄漏的反面——验证集和训练集分布不一致,比如验证集里长视频占比高,而模型对长视频的外推能力差。第二嫌疑是分类损失在主导:因为负样本多,模型很快学会“全都预测为负”或者“都在视频中间给个中庸的分数”,loss 降得漂亮但分数曲线是平的。排查方法很直接:把某一批的预测分数曲线画出来,如果多条样本的曲线形状高度相似,基本可以确诊。解法是提高 focal 的聚焦参数、加大正样本权重,或者换成排序损失。
第三个嫌疑容易被忽略:标签本身的问题。我遇到过一次,原因是标注文件里时间单位混用,一部分是秒、一部分是毫秒,导致部分真值区间超出视频总长。跑之前加一个断言(0 ≤ t_s < t_e ≤ 时长)能省下好几天。
7.2 边界总是差那么一两秒
如果 IoU=0.3 很高但 0.5 上不去,先看真值区间的长度分布。短片段(小于 4 秒)占比高的话,边界误差一两秒就是致命的。这时候的解法不是换模型,而是提高时序分辨率:把抽帧率从 1 fps 提到 2 或 4 fps,并且在候选区间内做局部重采样。另一条路是给短片段样本单独加权,让模型在短区间上多学几遍。
还有一个隐蔽原因是标注本身偏保守。前面说的标注一致性校准里,如果三个人标的区间都倾向于比语义动作短一点点,那模型学出来的也会短。这时候可以试一个常数补偿(比如两端各扩 0.3 秒),在验证集上看是否整体提升,如果有,说明是系统性偏差,值得补偿。
7.3 问题速查表
| 现象 | 最可能的原因 | 优先动作 |
|---|---|---|
| 分数曲线全平 | 负样本主导、正样本权重过低 | 提高正样本权重、改用排序损失 |
| 预测全在视频中部 | 回归退化解、匹配抖动 | 加时序位置先验、预热只训分类 |
| IoU=0.3 高、0.7 低 | 时序分辨率不足 | 提高抽帧率、局部重采样 |
| 验证集高、线上掉一半 | 按片段切分导致泄漏 | 改为按视频切分,加同视频异查询子集 |
| 短片段几乎召不回 | 最小候选粒度太粗 | 换稠密预测、给短片段加权 |
| 输出结果高度重叠 | NMS 阈值过高 | 降到 0.5,加窗口边缘惩罚 |
| 长视频整体偏后 | 位置编码外推差 | 加相对位置编码、滑窗分块处理 |
7.4 一个反直觉的经验:先修数据,再修模型
我统计过自己参与过的几个类似项目,指标卡住的原因里,数据问题的占比远高于模型结构问题。标注一致性差、切分泄漏、单位错误、时间戳越界、文本长度分布偏移——这些都不需要改一行模型代码,但带来的提升往往是几个点级别的。所以我的排查顺序永远是:先画 20 条样本的预测和真值对比图,肉眼看看错在哪;再统计错误类型分布;最后才动模型。直接上去换结构,等于在脏数据上做无用功。
8. 工程落地:长视频、切片与成本控制
8.1 分钟级以上的视频必须走滑窗,但边界要处理
单次前向能处理的时间步长度是有限的(受显存和注意力复杂度约束),超过就得切。我的设法是窗口 128 秒、步长 64 秒,50% 重叠。这样任何长度不超过 128 秒的目标至少完整落在某一个窗口里。重叠区会产生两份预测,处理方式是保留分数高的那个,同时对完全落在窗口切分点附近的预测做降权,因为它们的上下文被截断了。
切分带来的一个副作用是跨窗口的长目标会被切断。如果业务里有超过窗口长度的目标,得额外加一个“跨窗口合并”逻辑:相邻窗口边缘的预测如果时间上连续、文本相似度高,就尝试合并成一个大区间。这个逻辑不难写,但如果不做,长目标的召回会直接掉到接近零。
8.2 特征缓存与预计算:把在线成本压到可接受
线上服务的延迟大头往往不是模型推理,而是视频解码和特征提取。我的做法是把特征提取做成离线异步任务:新视频入库时触发一次抽帧和特征计算,结果按(视频 ID,抽帧率,编码器版本)三元组做缓存。查询时只跑文本编码和跨模态对齐,延迟能压到几十毫秒级。
这里有个版本管理的坑:换了视觉编码器之后,旧缓存的特征和新模型的输入空间不匹配,必须整批重算。所以缓存 key 里一定要带上编码器版本号,否则会拿到一堆“能跑但效果莫名其妙变差”的结果,排查起来非常痛苦。
8.3 验收怎么做:别只看平均值
最后交付前的验收,我坚持看三个切片维度:按目标时长分桶(小于 5 秒、5 到 30 秒、30 秒以上)、按视频总长分桶(短视频、长视频)、按查询词数分桶(短查询、长查询)。平均值好看但某个桶特别差的情况太常见了,而业务侧的真实流量分布往往正好压在差的那个桶上。
我印象最深的一次是整体指标 0.62,看着还行,但按查询长度一切,15 词以上的长查询只有 0.31。原因是训练集里长查询极少,模型对复杂描述的解析能力基本没建立起来。后来补了一批长查询标注,再对文本侧做了一轮针对性训练,那一桶才补上来。如果当时只看总分就上线,长查询那部分流量会全军覆没。
我在实际做这类项目的体会是,Video Grounding 的难点从来不在“模型能不能搭出来”,而在“你能不能把一段模糊的人类描述和一段同样模糊的人类标注对齐到同一个坐标系里”。前三次迭代我几乎都在改标注规范、做一致性校准、修切分逻辑,模型结构反而是最后才动的东西。如果你正准备开这个坑,我的建议是先花一周时间把 200 条样本的标注一致性量出来,把按视频切分的流程搭好,把单位校验的断言写上——这三件事做完,后面无论你选哪条技术路线,都会顺很多。至于边界那最后一两秒的差距,别急着换模型,先去数一数你的数据里,人和人之间本来差多少。