344 lines
13 KiB
Python
344 lines
13 KiB
Python
"""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 []
|