1. 项目概述:当视频理解遇上标注瓶颈

如果你做过视频时序动作分割,或者任何需要帧级标注的视频分析任务,一定会对标注成本感到头疼。一个小时的视频,每秒30帧,要一帧一帧地圈出“拿起杯子”、“喝水”、“放下杯子”的起止时间,这工作量想想就让人望而却步。传统的做法要么是堆人力,要么是用大量未标注数据去预训练模型,但前者贵,后者对数据质量要求高且未必精准。“边界中心主动学习”这个项目,瞄准的就是这个痛点。它不是一个全新的算法,而是一套高效的工程化策略,核心思想是:我们不追求标注所有帧,而是让模型自己告诉我们,哪些帧的标注最能提升它的性能,尤其是那些决定动作边界的“关键帧”。

简单来说,这就像一位经验丰富的老师教学生。老师不会把整本字典让学生背,而是先出几道题摸摸底,发现学生在“动词时态”和“介词搭配”上特别薄弱,于是接下来就重点讲解和练习这部分内容。边界中心主动学习中的“模型”就是学生,“标注者”就是老师,而“主动学习”的查询策略,就是那套精准的“摸底考试”和“重点练习”方案。它的目标是以最小的标注代价(人工标注的帧数),换取模型性能的最大化提升,特别适用于动作分割这类边界模糊、标注代价极高的任务。

2. 核心思路与方案选型背后的考量

2.1 为什么是“边界中心”?

在视频时序动作分割任务中,最难标注、也是对模型性能影响最大的部分,往往是动作发生转换的边界区域。例如,从“走路”到“跑步”的过渡帧,模型很容易混淆。如果大量标注数据都是“走路”或“跑步”的稳定状态帧,模型可能学得很好,但一到边界就“翻车”。反之,如果我们能精准地标注这些边界帧,模型就能更清晰地学会区分不同动作的临界特征。

因此,“边界中心”的策略应运而生。它改变了传统主动学习随机或基于不确定度采样帧的思路,将查询(即请求人工标注)的焦点,主动引导至模型预测的动作边界附近。这里的“边界”不是指视频的物理边界,而是时序上不同动作类别的转换点。这种策略的假设是:标注边界区域的收益远大于标注动作内部稳定区域的收益。

2.2 主动学习循环的工程化设计

一个完整的边界中心主动学习系统,是一个迭代的闭环,通常包含以下几个核心步骤,我将其拆解如下:

  1. 初始化 :用一个非常小的、已标注的数据集(种子集)训练一个初始的动作分割模型。这个数据集可能只包含每个动作类别的几个片段。
  2. 在未标注池上推理 :用当前模型对海量的未标注视频进行预测,得到每一帧的预测类别和置信度。
  3. 边界帧检测与评分 :这是“边界中心”策略的核心。系统需要从模型预测的序列中,自动检测出哪些帧位于预测的动作边界附近。同时,要为这些候选边界帧计算一个“价值分数”,用于衡量标注该帧的潜在收益。这个分数通常会融合 预测不确定性 (模型自己有多不确定)和 边界影响力 (该帧处于边界的位置重要性)。
  4. 查询与人工标注 :根据评分,选择价值最高的一批帧(例如,Top-K帧),提交给人工标注员进行标注。
  5. 模型更新 :将新标注的帧加入到已标注训练集中,重新训练或微调模型。
  6. 循环迭代 :重复步骤2-5,直到达到预设的标注预算或模型性能满足要求。

这个循环的关键在于第3步——如何定义和计算边界帧的价值。方案选型的不同,直接决定了整个系统的效率。

2.3 方案选型:从理论到实践

在实际构建时,有几个关键决策点:

  • 边界检测方法 :是直接用模型预测的类别变化点作为边界,还是用一个滑动窗口计算预测概率的分布变化(如熵的变化率)?后者通常更鲁棒,能发现“软边界”。
  • 评分函数设计 :如何量化“价值”?常见做法是结合:
    • 不确定性度量 :如预测熵(Entropy)、类别间概率差值(Margin)、最小置信度(Least Confidence)。边界帧的不确定性通常更高。
    • 代表性度量 :该帧的特征是否与未标注池中大量其他帧相似?标注它可能惠及更多帧。
    • 多样性度量 :避免选出的所有帧都来自同一段视频或同一边界类型。 一个实用的策略是给不确定性高的边界帧赋予更高的基础分,然后用代表性和多样性进行加权或排序调整。
  • 模型架构选择 :用于特征提取和时序建模的骨干网络是什么?是使用经典的TSN、TSM,还是基于Transformer的模型如TimeSformer?骨干网络的能力决定了初始特征的质量,进而影响边界检测的准确性。在主动学习初期,一个轻量但有效的骨干(如I3D)可能比一个庞大复杂的模型更合适,因为训练数据少,大模型容易过拟合。

实操心得 :在项目初期,不要过度追求评分函数的复杂性。一个“高预测熵 + 位于预测类别跳变点附近”的简单规则,往往能带来80%的收益。先把主动学习的闭环跑通,再逐步迭代优化评分策略。

3. 核心模块拆解与实操要点

3.1 边界帧检测模块的实现

这是整个系统的“眼睛”。我们的目标是:给定一个视频模型预测的帧级概率序列 P = [p1, p2, ..., pT] ,其中 pt 是一个C维向量(C是动作类别数),找出所有可能是真实动作边界的帧索引。

一个经过实践验证的有效方法是 基于预测熵的滑动窗口变化检测

  1. 计算每帧的预测熵 Entropy(t) = - sum_c (p_t[c] * log(p_t[c])) 。熵值越高,表示模型越不确定。
  2. 构建熵序列 :得到 E = [e1, e2, ..., eT]
  3. 滑动窗口计算局部变化 :定义一个窗口大小W(例如,W=15帧,约0.5秒)。对于第t帧,计算其前后窗口内熵的统计量差异。一个简单的指标是 均值差 Change(t) = |mean(E[t: t+W]) - mean(E[t-W: t])| 这个值在熵分布发生剧烈变化(即边界)的位置会呈现峰值。
  4. 峰值检测与筛选 :对 Change 序列进行平滑(如高斯滤波)后,使用峰值查找算法(如 scipy.signal.find_peaks )找到局部极大值点。设定一个高度阈值和距离阈值,过滤掉噪声引起的微小波动,剩下的峰值点就是候选边界帧。
import numpy as np
from scipy.ndimage import gaussian_filter1d
from scipy.signal import find_peaks

def detect_boundary_frames(prob_sequence, window_size=15, sigma=3.0, height_threshold=0.05, distance_threshold=30):
    """
    基于预测熵变化检测边界帧。
    :param prob_sequence: 形状为 (T, C) 的模型预测概率序列
    :return: 候选边界帧的索引列表
    """
    T, C = prob_sequence.shape
    # 1. 计算每帧熵
    epsilon = 1e-10
    entropy_seq = -np.sum(prob_sequence * np.log(prob_sequence + epsilon), axis=1)
    
    # 2. 计算滑动窗口熵均值差
    change_seq = np.zeros(T)
    half_w = window_size // 2
    for t in range(half_w, T - half_w):
        front_mean = np.mean(entropy_seq[t - half_w: t])
        back_mean = np.mean(entropy_seq[t: t + half_w])
        change_seq[t] = abs(back_mean - front_mean)
    
    # 3. 平滑并寻找峰值
    smoothed_change = gaussian_filter1d(change_seq, sigma=sigma)
    peaks, properties = find_peaks(smoothed_change, height=height_threshold, distance=distance_threshold)
    
    return peaks.tolist()

3.2 价值评分与查询策略模块

找到候选边界帧后,我们需要给它们打分排序。一个融合了不确定性和边界显著性的评分函数可以这样设计:

Score(t) = α * Uncertainty(t) + β * BoundarySalience(t)

  • Uncertainty(t) :可以直接使用该帧的预测熵 Entropy(t) ,并进行归一化。
  • BoundarySalience(t) :边界显著性。我们可以用上一步计算出的 Change(t) 值,或者更精细地,计算该帧处于“预测动作片段”的端点位置的概率。例如,如果模型预测的一段“跑步”动作从帧a到帧b,那么帧a和帧b的边界显著性就很高。
  • α, β :权衡超参数。通常初期更依赖不确定性(α较大),后期当模型整体不确定性下降后,可以更依赖边界显著性(β增大)。

在实操中,为了鼓励多样性,避免一次性查询同一视频段内的多个相似边界,可以在排序后加入 基于特征的聚类去重 。例如,对所有候选帧提取特征(可以使用骨干网络倒数第二层的特征),进行K-Means聚类,然后从每个簇中选择分数最高的帧。

注意事项 :评分函数中的超参数(α, β, 聚类簇数K)需要在一个小的验证集上进行调优。一个快速的验证方法是:模拟主动学习过程,观察不同的参数设置下,模型性能(如mAP)随标注帧数增长的曲线,选择上升最快的组合。

3.3 人工标注接口与数据管理

这是连接算法和真实世界的桥梁。设计时需要考虑:

  • 标注效率 :标注界面应该极其简洁。对于选出的关键帧,最好能自动预加载其前后几秒的短视频片段,让标注员在连续的上下文中判断动作边界,而不是只看孤立的单帧。
  • 标注内容 :对于时序动作分割,标注员需要指出该帧所属的动作类别,更重要的是,如果它被判定为边界帧,可能需要微调其前后动作的精确起止时间点。因此,接口需要支持快速播放、逐帧前进/后退和打点标注。
  • 数据版本管理 :随着迭代进行,标注数据集不断增长。必须有一套清晰的版本管理机制,记录每一轮新增了哪些帧的标注,以及对应的模型版本。这有助于回溯分析和调试。

一个简单的数据管理表结构可以如下:

视频ID 帧索引 动作类别 开始时间(秒) 结束时间(秒) 标注轮次 标注来源(主动学习/初始)
vid_001 1245 drink_water 41.50 41.83 0 初始种子
vid_001 1251 drink_water 41.70 42.00 3 主动学习查询
vid_002 567 walk 18.90 19.50 1 主动学习查询

4. 完整实操流程与核心环节实现

4.1 环境准备与数据预处理

假设我们使用PyTorch框架,并选择一个流行的视频动作分割数据集(如Breakfast、50Salads)或自建数据集。

  1. 安装依赖

    pip install torch torchvision pytorch-lightning
    pip install scipy scikit-learn opencv-python
    # 可选,用于特征提取或特定模型
    pip install mmcv mmaction2
    
  2. 数据准备

    • 将所有视频文件转换为统一的格式(如.mp4)和帧率(如30fps)。
    • 使用预训练模型(如在Kinetics上预训练的I3D或SlowFast)提取所有视频的帧级特征,保存为 .npy 文件。这一步可以离线完成,极大加速主动学习循环中的推理步骤。
    • 准备一个小的种子标注集(seed set),可能只占全部数据的1%-5%。确保这个种子集覆盖所有动作类别。

4.2 初始模型训练与主动学习循环脚本

以下是一个高度简化的主循环伪代码,展示了核心流程:

import torch
from model import ActionSegmentationModel # 你的动作分割模型
from active_learner import BoundaryAwareActiveLearner # 封装了边界检测和评分策略
from dataloader import get_data_loaders

# 初始化
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = ActionSegmentationModel().to(device)
active_learner = BoundaryAwareActiveLearner(strategy='entropy_change')
labeled_pool, unlabeled_pool = load_initial_data(seed_ratio=0.05)

# 用种子集训练初始模型
train_loader = get_data_loaders(labeled_pool)
train_model(model, train_loader, epochs=50)

for round_idx in range(total_rounds):
    print(f"=== Active Learning Round {round_idx+1} ===")
    
    # 1. 在未标注池上推理
    all_features, all_probs = infer_on_unlabeled_pool(model, unlabeled_pool)
    
    # 2. 主动学习策略选择最有价值的帧
    query_indices = active_learner.query(
        features=all_features,
        probs=all_probs,
        query_size=budget_per_round # 每轮标注预算,如1000帧
    )
    
    # 3. 模拟或调用真实人工标注接口 (此处模拟)
    new_annotations = simulate_human_annotation(unlabeled_pool, query_indices)
    
    # 4. 更新标注池和未标注池
    labeled_pool.add(new_annotations)
    unlabeled_pool.remove(query_indices)
    
    # 5. 用扩增后的标注集重新训练模型
    train_loader = get_data_loaders(labeled_pool)
    # 通常不会从头训练,而是在上一轮模型权重上微调,以节省时间
    fine_tune_model(model, train_loader, epochs=30)
    
    # 6. 在固定的验证集上评估性能
    val_score = evaluate_on_validation_set(model)
    print(f"Round {round_idx+1} validation mAP: {val_score:.4f}")
    
    # 检查是否提前满足性能要求
    if val_score > target_score:
        print("Performance target reached. Stopping.")
        break

4.3 关键参数与计算过程详解

在循环中,以下几个参数需要仔细设置:

  • 每轮标注预算 ( budget_per_round ) :这是最重要的超参数之一。它决定了学习过程的“粒度”。预算太小,每轮模型更新收益不明显;预算太大,则失去了主动学习“精打细算”的意义。一个经验法则是,初始几轮可以设置较小的预算(如总帧数的0.1%),让模型快速定位最不确定的区域;后期可以适当增大预算,以加速收敛。总预算通常设定为达到目标性能所需全量标注的10%-30%。

  • 模型微调轮数 ( epochs ) :在主动学习的中后期,标注集已经具有一定规模,重新训练(而非微调)成本很高。采用微调策略,并设置一个较小的epoch数(如10-30),可以高效利用新数据,避免过拟合到当前的小批量标注数据上。

  • 边界检测窗口大小 ( window_size ) :这个参数需要与视频中动作转换的典型时长相匹配。对于快速动作(如乒乓球挥拍),窗口应小(如5-10帧);对于缓慢过渡的动作(如坐下到站起),窗口应大(如15-30帧)。可以通过分析种子集标注的边界持续时间分布来设定。

5. 常见问题与排查技巧实录

在实际部署和运行边界中心主动学习系统时,你几乎一定会遇到下面这些问题。这里是我踩过坑后总结的排查清单。

5.1 模型性能不升反降

  • 现象 :新加入一批标注帧后,重新训练的模型在验证集上的性能反而下降了。
  • 可能原因与排查
    1. 标注噪声 :主动学习选出的边界帧本身模棱两可,不同标注员的判断可能不一致,引入了错误标签。 排查 :对同一批查询帧,让多位标注员进行标注,计算标注者间信度(如Cohen‘s Kappa)。如果信度低,说明这批帧“太难”或定义模糊,应考虑修改查询策略,暂时避开这类高争议帧。
    2. 样本偏差 :主动学习策略过于激进,选出的帧全部是某一种特定类型的困难样本(如所有都是“走-跑”边界),导致训练集分布严重偏离真实数据分布。 排查 :可视化每轮选中帧的动作类别分布和视频来源分布。如果分布极度不均,需要在评分函数中增加 多样性约束 ,例如确保每轮从不同视频和不同动作对中选取样本。
    3. 过拟合 :微调时学习率太大或轮数太多,模型过拟合到了新的小批量数据上,遗忘了之前学到的知识。 排查 :使用更小的学习率(如初始学习率的1/10),并监控训练损失和验证损失。如果训练损失持续下降而验证损失很快上升,就是过拟合的典型信号。

5.2 主动学习“停滞不前”

  • 现象 :连续几轮,模型性能提升微乎其微,仿佛主动学习不再起作用。
  • 可能原因与排查
    1. 模型容量不足 :初始的骨干网络或模型架构太简单,无法从更精细的边界标注中学习到更复杂的模式。 排查 :在种子集上尝试一个更强大的模型(如将CNN骨干换为Transformer),看其上限是否显著更高。如果是,可以在主动学习后期切换或融合更大模型。
    2. 查询策略失效 :当前的评分函数无法再区分出“有价值”的帧。模型对所有未标注帧的预测都变得过于“自信”(熵值很低),或者边界检测模块不再能发现新的显著边界。 排查 :检查未标注池中剩余帧的平均预测熵和边界显著性分数的分布。如果分数普遍很低且平坦,可以考虑:
      • 切换不确定性度量方式,例如从“预测熵”改为“BALD”(贝叶斯主动学习)或使用集成模型计算预测方差。
      • 引入 基于模型的预测变化 (Expected Model Change)作为新指标,即估计标注该帧后会对模型权重产生多大影响。
    3. 需要引入“探索” :策略一直过于“贪婪”,只选择当前模型认为最有价值的帧(利用),缺乏对未知区域的“探索”。 排查与解决 :在评分函数中引入一个随机成分,例如以ε概率随机选择未标注帧,或者优先选择特征空间里距离已有标注样本最远的帧(基于核心集方法)。

5.3 计算与时间开销过大

  • 现象 :每一轮在未标注池上的推理或特征提取耗时过长,无法实现快速迭代。
  • 优化技巧
    1. 特征预提取与缓存 :这是最有效的优化。在开始主动学习循环前,用预训练模型一次性提取所有未标注视频的帧级特征并存入磁盘或数据库。后续每一轮的“推理”实际上只是用当前的小模型(分类头)在这些固定特征上进行前向传播,速度极快。
    2. 子池采样 :如果未标注池极大(如数百万帧),每一轮都对全部数据进行推理不现实。可以采用 子池采样 策略:每一轮随机采样一个子集(如10%的未标注数据)作为当前轮的候选池,仅在这个子池内运行主动学习查询。下一轮再换一个随机子集。这能在保证多样性的同时大幅降低计算量。
    3. 使用更高效的骨干网络 :在保证性能的前提下,选择推理速度更快的模型,如MobileNetV3+TCN的组合,替代沉重的3D CNN或ViT。

5.4 边界检测模块误检率高

  • 现象 :检测出的很多“边界帧”经人工复核,并非真正的动作转换点。
  • 调优方法
    1. 调整平滑参数 :在计算熵变化序列时,高斯滤波的 sigma 参数至关重要。 sigma 太小,对噪声敏感; sigma 太大,会平滑掉真实的边界信号。需要通过验证集调整。
    2. 融合多尺度信息 :动作边界有粗有细。可以并行使用多个不同大小的滑动窗口进行检测,然后综合结果。例如,小窗口检测快速切换,大窗口检测缓慢过渡。
    3. 引入时序一致性约束 :真实的动作边界在时间上是稀疏的。可以使用动态规划或条件随机场(CRF)对初步检测出的边界序列进行后处理,强制其满足最小间隔约束,并使得整个视频的动作片段序列更合理。

最后,边界中心主动学习不是一个“设置好就一劳永逸”的系统。它更像一个需要与标注员、数据以及模型本身不断交互、调优的有机体。我的体会是,最重要的不是追求最复杂的算法,而是建立快速实验和评估的管道。每调整一个策略参数,都能在一天内看到它对模型性能曲线的实际影响,这种快速的反馈循环,才是高效攻克视频标注难题的真正钥匙。在实际操作中,我通常会保留一个完全随机采样的基线模型,作为对比基准,它能清晰地告诉我,我的“聪明”策略到底带来了多少额外的效率提升。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐