Files
Video-Tree-TRM5/app/question_gen/strategy_action_recognition.py
T

344 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Action Recognition 特化出题策略。
靶向 Agent 在动作识别类题目上的 6 种典型失败机制,通过加权随机
SubPattern 选择为 VLM 出题提供聚焦指令。
与 BaseTaskTypeStrategy 完全自包含,不依赖 QuestionFamilySpec。
采样从 L3 提升至 L2(跨段推理需要更广视野),3 个 SubPattern
进一步覆盖至 L1 以测试全局追踪能力。
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from app.question_gen.families import SamplingConstraint
from app.question_gen.strategy import SubPattern
if TYPE_CHECKING:
import random
# ---------------------------------------------------------------------------
# 6 个 SubPattern 定义
# ---------------------------------------------------------------------------
_PREMATURE_EVIDENCE_ANCHORING = SubPattern(
name="premature_evidence_anchoring",
weight=0.20,
sampling_level_override=1,
constraint_override=None,
instruction=(
"设计一道需要考察视频中完整证据链的动作识别题。"
"正确答案的关键证据出现在视频后半段或跨多个片段,"
"但视频前段包含一个看似合理的局部匹配——"
"Agent 若仅凭首条匹配停止搜索就会出错。"
),
positive_examples=[
{
"question": "视频中厨师最终采用了哪种烹饪方式?",
"answer": "B. 蒸",
"why": "厨师先演示了炒(前段),但最终菜品使用蒸制(后段),"
"锚定首段证据的 Agent 会错选'炒'。",
},
],
negative_examples=[
{
"question": "视频开头厨师在做什么?",
"why": "答案仅需前段信息,不会触发过早锚定失败。",
},
],
distractor_rules=(
"将视频前段真实出现的局部匹配动作设为强干扰项——它真实发生,"
"仅在【时点】这一单一维度上与正解不同(前段 vs 最终结论段)。"
"其余干扰项亦须是视频中真实发生的动作,严禁缺席事件。"
),
)
_TEMPORAL_REASONING_FAILURE = SubPattern(
name="temporal_reasoning_failure",
weight=0.20,
sampling_level_override=1,
constraint_override=None,
instruction=(
"设计一道要求正确排序或定位第 N 次出现的动作识别题。"
"题目需要 Agent 追踪事件的时间先后顺序,"
"或准确识别某动作在视频中第几次出现。"
"打乱时序或错误计数即会答错。"
),
positive_examples=[
{
"question": "运动员第三次尝试起跳前做了什么准备动作?",
"answer": "C. 深蹲热身",
"why": "需要准确定位'第三次'起跳而非其他次,"
"时序推理失败的 Agent 会混淆不同次尝试的准备动作。",
},
],
negative_examples=[
{
"question": "运动员在视频中做了什么?",
"why": "不涉及时序排序或计数,Agent 无需追踪顺序。",
},
],
distractor_rules=(
"干扰项必须是视频中真实发生的事件,仅在【事件时序/顺序】这一单一维度上与正解不同——"
"即同一组真实事件的错误排列或错误的第 N 次定位。"
"严禁使用视频中未出现的缺席事件作为干扰项。"
),
)
_SEMANTIC_RIGIDITY = SubPattern(
name="semantic_rigidity",
weight=0.15,
sampling_level_override=None,
constraint_override=None,
instruction=(
"设计一道动作识别题,正确选项使用与视频原始描述不同的同义表达。"
"Agent 需理解语义等价而非依赖字面匹配——"
"例如视频字幕说'奔跑',正确选项写作'快速移动'。"
),
positive_examples=[
{
"question": "工人对墙面进行了什么操作?",
"answer": "A. 涂覆保护层",
"why": "视频中字幕描述为'刷漆',正确答案改写为'涂覆保护层'"
"依赖字面匹配的 Agent 会因找不到完全一致的表述而错选。",
},
],
negative_examples=[
{
"question": "工人在刷漆吗?",
"why": "选项直接复用视频原文,不考察语义理解。",
},
],
distractor_rules=(
"干扰项须基于视频真实内容,仅在【表述/语义】这一单一维度上做文章:"
"保留一个复用视频原始字幕字面、但在题干限定下语义为假的选项作为陷阱,"
"其余选项描述真实动作的不同同义表述。严禁凭空编造缺席动作。"
),
)
_FINE_GRAINED_VISUAL_ACTION = SubPattern(
name="fine_grained_visual_action",
weight=0.15,
sampling_level_override=None,
constraint_override=None,
instruction=(
"设计一道需要区分细粒度动作方式的识别题。"
"题目聚焦于 HOW(怎么做)而非 WHAT(做什么),"
"如区分'搅拌'与'翻炒'、'拧'与'拉'等视觉上相似但方式不同的动作。"
),
positive_examples=[
{
"question": "维修人员是如何拆卸螺丝的?",
"answer": "D. 用扳手逆时针旋转",
"why": "需要区分拆卸的具体方式(扳手 vs 螺丝刀、顺时针 vs 逆时针),"
"粗粒度识别只能判断'在拆螺丝',无法区分方式。",
},
],
negative_examples=[
{
"question": "维修人员在做什么?",
"why": "只需粗粒度动作识别('拆螺丝'),不考察具体方式。",
},
],
distractor_rules=(
"四个选项须是【同一大类动作】的不同执行方式,全部为视频中真实可见的做法,"
"仅在【执行方式】这一单一维度上不同(如顺/逆时针、扳手/螺丝刀)。"
"严禁使用明显不相关或视频中未出现的动作作为干扰项。"
),
)
_CROSS_SEGMENT_ENTITY_TRACKING = SubPattern(
name="cross_segment_entity_tracking",
weight=0.15,
sampling_level_override=1,
constraint_override=None,
instruction=(
"设计一道需要跨视频段落追踪同一实体动作的识别题。"
"目标实体在不同片段中外观、称呼或上下文发生变化,"
"Agent 需要将多段信息合并才能正确回答关于该实体的动作问题。"
),
positive_examples=[
{
"question": "穿红色外套的人在视频中总共完成了哪些动作?",
"answer": "B. 先讲解、后示范、最后总结",
"why": "该人物在前段穿外套讲解,中段脱外套示范,后段重新穿上总结,"
"无法跨段追踪的 Agent 会遗漏某段动作。",
},
{
"question": "主持人在节目不同环节中分别做了什么?",
"answer": "A. 开场介绍、采访嘉宾、总结点评",
"why": "主持人在不同场景切换中持续出现,需要跨段聚合。",
},
],
negative_examples=[
{
"question": "视频第一个片段中的人在做什么?",
"why": "仅需单段信息,不考察跨段追踪。",
},
],
distractor_rules=(
"干扰项须是视频中【另一真实实体】在相应片段真实做过的动作,"
"仅在【动作主体】这一单一维度上与正解不同;或构造只覆盖部分片段的真实子集。"
"严禁编造任何实体未做过的缺席动作。"
),
)
_EVIDENCE_GAP_CONFABULATION = SubPattern(
name="evidence_gap_confabulation",
weight=0.15,
sampling_level_override=None,
constraint_override=None,
instruction=(
"设计一道动作识别题,视频中存在证据空缺(如遮挡、跳切、画外音)。"
"正确答案承认信息不足或基于间接证据推断,"
"而非凭空编造因果链。Agent 若虚构缺失证据即会出错。"
),
positive_examples=[
{
"question": "画面切走后,演讲者下一步做了什么?",
"answer": "C. 无法从视频中直接确定",
"why": "画面跳切导致该动作无直接视觉证据,"
"倾向于虚构的 Agent 会编造一个看似合理的动作。",
},
],
negative_examples=[
{
"question": "画面中演讲者正在做什么?",
"why": "动作在画面中可见,不存在证据空缺。",
},
],
distractor_rules=(
"正解仅陈述视频中可观测的事实或诚实承认证据不足;"
"干扰项在【因果完整性】这一单一维度上越界——补上一段视频未展示的因果链,"
"但其前提元素仍取自视频真实内容(诱导 Agent 顺势编造),而非完全凭空的缺席事件。"
),
)
AR_SUB_PATTERNS: tuple[SubPattern, ...] = (
_PREMATURE_EVIDENCE_ANCHORING,
_TEMPORAL_REASONING_FAILURE,
_SEMANTIC_RIGIDITY,
_FINE_GRAINED_VISUAL_ACTION,
_CROSS_SEGMENT_ENTITY_TRACKING,
_EVIDENCE_GAP_CONFABULATION,
)
# 预计算:名称列表 + 权重列表(避免每次 select 重新构建)
_AR_PATTERN_NAMES: list[str] = [sp.name for sp in AR_SUB_PATTERNS]
_AR_PATTERN_WEIGHTS: list[float] = [sp.weight for sp in AR_SUB_PATTERNS]
_AR_PATTERN_BY_NAME: dict[str, SubPattern] = {sp.name: sp for sp in AR_SUB_PATTERNS}
# ---------------------------------------------------------------------------
# ActionRecognitionStrategy
# ---------------------------------------------------------------------------
_SAMPLING_CONSTRAINT = SamplingConstraint(
min_subtitles=3,
min_l3_nodes=5,
require_frames=True,
cross_l2_span=True,
)
class ActionRecognitionStrategy:
"""Action Recognition 特化出题策略。
自包含实现,不依赖 BaseTaskTypeStrategy 或 QuestionFamilySpec。
靶向 6 种典型失败机制,通过加权随机 SubPattern 引导 VLM 出题。
属性:
task_type: 题型名 — "Action Recognition"。
strategy_name: 策略标识 — "ACTION_RECOGNITION"。
skill_target: 目标失败机制 — "M1_AR"。
sampling_level: 采样层级 — 2(L2,从 L3 升级以获得跨段视野)。
sampling_constraint: 采样约束(min_subtitles=3, min_l3_nodes=5, require_frames, cross_l2_span)。
prompt_template: 出题模板 — "action_recognition.md"。
leak_probe_template: 泄漏探测模板 — "gate_leak_retrieval.md"。
"""
@property
def task_type(self) -> str:
"""返回题型名。"""
return "Action Recognition"
@property
def strategy_name(self) -> str:
"""返回策略标识名。"""
return "ACTION_RECOGNITION"
@property
def skill_target(self) -> str:
"""返回目标失败机制编号。"""
return "M1_AR"
@property
def sampling_level(self) -> int:
"""返回采样层级(L2)。"""
return 2
@property
def sampling_constraint(self) -> SamplingConstraint:
"""返回采样约束。"""
return _SAMPLING_CONSTRAINT
@property
def prompt_template(self) -> str:
"""返回出题 prompt 模板文件名。"""
return "action_recognition.md"
@property
def leak_probe_template(self) -> str:
"""返回泄漏探测模板文件名。"""
return "gate_leak_retrieval.md"
@property
def uses_grounded_selector(self) -> bool:
"""AR 启用候选池 + VLM 视觉打分 selector。"""
return True
def select_sub_pattern(self, rng: random.Random) -> SubPattern:
"""按权重随机选择一个 SubPattern。
参数:
rng: 随机数生成器(确保可复现)。
返回:
选中的 SubPattern 实例(永不为 None)。
"""
chosen_name = rng.choices(
_AR_PATTERN_NAMES,
weights=_AR_PATTERN_WEIGHTS,
k=1,
)[0]
return _AR_PATTERN_BY_NAME[chosen_name]
def build_prompt_context(self, material: Any, sub_pattern: SubPattern | None) -> dict:
"""构建 prompt 上下文字典。
参数:
material: 采样素材(当前未使用,留给管线扩展)。
sub_pattern: 选中的子模式。
返回:
包含 family_name, prompt_template, sub_pattern 的字典。
"""
return {
"family_name": self.strategy_name,
"prompt_template": self.prompt_template,
"sub_pattern": sub_pattern.name if sub_pattern is not None else None,
}
def extra_gates(self, candidate: Any) -> list:
"""返回额外门控列表(当前为空)。
参数:
candidate: 候选题目。
返回:
空列表。
"""
return []