Files
Video-Tree-TRM5/app/harness/question_units.py
T

147 lines
6.0 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.
"""QuestionUnit 组装/展开/校验/单元正确性——pair 契约的唯一入口。
pool 构建、批处理、推理、评测(Task 3+)均通过本模块聚合/展开孪生对,
保证 AR pair 的"两题作为整体调度"契约只在一处实现、fail-fast 暴露非法配对。
"""
from collections import defaultdict
from core.types import GeneratedQuestion, QuestionUnit
def _assemble_pair(pair_id: str, group: list[GeneratedQuestion]) -> QuestionUnit:
"""校验单个 pair 分组的数量/角色并组装为 pair 单元(fail-fast)。
参数:
pair_id: 该分组共享的孪生对标识。
group: 归属同一 pair_id 的题目列表。
返回:
kind="pair" 的 QuestionUnit。
关键实现:
- 分组必须恰好 2 条,否则视为孤儿/超员,raise ValueError。
- 显式检查 pair_original / pair_mirror 角色齐备且唯一,缺失或重复
直接 raise ValueError(防 next(...) 静默 StopIteration)。
- 合法孪生对交由 QuestionUnit.from_pair 做 video_id/task_type/flip_axis
一致性断言。
"""
if len(group) != 2:
raise ValueError(f"pair {pair_id} 数量={len(group)}≠2(孤儿或超员)")
originals = [q for q in group if q.question_role == "pair_original"]
mirrors = [q for q in group if q.question_role == "pair_mirror"]
if len(originals) != 1 or len(mirrors) != 1:
raise ValueError(
f"pair {pair_id} 角色非法:original={len(originals)} mirror={len(mirrors)}"
"需各恰好 1 条"
)
return QuestionUnit.from_pair(originals[0], mirrors[0])
def build_units(questions: list[GeneratedQuestion]) -> list[QuestionUnit]:
"""将扁平题目列表聚合为单元列表:single 单封、pair 按 pair_id 成对聚合。
参数:
questions: 待聚合的题目列表,可混含 single 与孪生对成员。
返回:
单元列表,先 single 后 pair,顺序稳定(single 保留输入顺序,
pair 按首次出现的 pair_id 顺序)。
关键实现:
pair_id 为空 → single 单元;非空 → 归入对应 pair 桶。各 pair 桶的
数量/角色校验与组装下沉到 _assemble_pairfail-fast),本体只做分组
与派发。
"""
by_pair: dict[str, list[GeneratedQuestion]] = defaultdict(list)
singles: list[QuestionUnit] = []
for q in questions:
if q.pair_id:
by_pair[q.pair_id].append(q)
else:
singles.append(QuestionUnit.from_single(q))
pairs = [_assemble_pair(pid, qs) for pid, qs in by_pair.items()]
return singles + pairs
def flatten_units(units: list[QuestionUnit]) -> list[GeneratedQuestion]:
"""将单元列表无损展开回扁平题目列表。
参数:
units: 单元列表。
返回:
展开后的题目列表,保持单元顺序及单元内题目顺序。
"""
return [q for u in units for q in u.questions]
def validate_units(units: list[QuestionUnit]) -> list[QuestionUnit]:
"""校验单元列表结构合法性,通过则原样返回(便于链式调用)。
参数:
units: 待校验单元列表。
返回:
校验通过的原单元列表。
关键实现:
pair 单元必须恰好含 2 题,否则 raise ValueErrorsingle 单元无需额外
校验(构造时即为 1 题)。用于消费方在使用前做一道防御闸门。
"""
for u in units:
if u.kind == "pair" and u.size != 2:
raise ValueError(f"unit {u.unit_id} pair 不成对(size={u.size}")
return units
def unit_correctness(unit: QuestionUnit, per_q: dict[str, bool], *, strict: bool = True) -> bool:
"""计算单元级正确性:AR pair 走双向 AND,single 即单题正确性。
参数:
unit: 目标单元。
per_q: 题目 question_id → 该题是否作答正确的映射。
strict: 缺键策略。True(默认)时以 per_q[q.question_id] 取值,缺任一题
触发 KeyError(防静默兜底,强制上游先补齐全部单题结果);False 时以
per_q.get(q.question_id, False) 取值,缺键计 False(宽松口径,供池
构建 / gate 冷启动 / 采样等"缺基线对错即视为未答对"的调用点复用)。
返回:
单元内所有题目均正确时为 True,否则 False。
关键实现:
pool 构建(pools)、gate 冷启动(gate_ladder)、分层采样(loader)三处
原各自持有的 loose 版 _unit_correct 副本统一收敛到本函数 strict=False 分支,
消除重复逻辑与 missing-key 策略分叉。
"""
if strict:
return all(per_q[q.question_id] for q in unit.questions)
return all(per_q.get(q.question_id, False) for q in unit.questions)
def unit_correctness_view(
units: list[QuestionUnit], per_q: dict[str, bool], *, strict: bool = True
) -> dict[str, bool]:
"""把逐题对错折叠成单元级视图:unit_id → 单元是否整体正确。
进化引擎(gate e-process / quadrant / probation / pair_block / compute_accuracy
统一消费此单元视图,保证 AR pair 双向 AND、非 AR single 单题,混格池中
孪生对折叠为一个单元、不被 P/Q 单题计分污染(核心算法保真 #5)。
参数:
units: 目标单元列表(single 或 pair)。
per_q: 题目 question_id → 该题是否作答正确(唯一逐题溯源来源)。
strict: 缺键策略,透传给 unit_correctness。True(默认)缺任一题 raise
KeyErrorFalse 缺键计 False(宽松口径)。
返回:
unit_id → 单元级正确性。single 的 unit_id 等于其 question_id
pair 的 unit_id 等于共享 pair_id。
关键实现:
逐单元复用 unit_correctnessstrict 透传),默认 strict 禁静默兜底、
强制上游先补齐全部单题结果。
"""
return {u.unit_id: unit_correctness(u, per_q, strict=strict) for u in units}