147 lines
6.0 KiB
Python
147 lines
6.0 KiB
Python
"""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_pair(fail-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 ValueError;single 单元无需额外
|
||
校验(构造时即为 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
|
||
KeyError;False 缺键计 False(宽松口径)。
|
||
|
||
返回:
|
||
unit_id → 单元级正确性。single 的 unit_id 等于其 question_id,
|
||
pair 的 unit_id 等于共享 pair_id。
|
||
|
||
关键实现:
|
||
逐单元复用 unit_correctness(strict 透传),默认 strict 禁静默兜底、
|
||
强制上游先补齐全部单题结果。
|
||
"""
|
||
return {u.unit_id: unit_correctness(u, per_q, strict=strict) for u in units}
|