chore: lint and format per-category pool strategy implementation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 22:58:10 -04:00
parent 671db2f88c
commit 37d4519905
2 changed files with 71 additions and 55 deletions
+28 -37
View File
@@ -254,9 +254,7 @@ def save_pools(
ValueError: split_mode 为 "per_category" 但未提供 config。 ValueError: split_mode 为 "per_category" 但未提供 config。
""" """
if split_mode == "per_category" and config is None: if split_mode == "per_category" and config is None:
raise ValueError( raise ValueError("per_category 模式下 save_pools 必须提供 config 参数以写入元数据。")
"per_category 模式下 save_pools 必须提供 config 参数以写入元数据。"
)
data: dict = { data: dict = {
"split_mode": split_mode, "split_mode": split_mode,
@@ -285,9 +283,7 @@ def save_pools(
data["categories"] = categories data["categories"] = categories
data["seed"] = config.seed data["seed"] = config.seed
data["train_ratio"] = config.train_ratio data["train_ratio"] = config.train_ratio
data["test_source"] = ( data["test_source"] = str(config.test_questions_dir) if config.test_questions_dir else None
str(config.test_questions_dir) if config.test_questions_dir else None
)
path.write_text( path.write_text(
json.dumps(data, ensure_ascii=False, indent=2), json.dumps(data, ensure_ascii=False, indent=2),
@@ -402,23 +398,17 @@ def _validate_per_category_consistency(
""" """
mismatches: list[str] = [] mismatches: list[str] = []
if frozen_data.get("seed") != pool_config.seed: if frozen_data.get("seed") != pool_config.seed:
mismatches.append( mismatches.append(f"seed: 冻结={frozen_data.get('seed')}, 当前={pool_config.seed}")
f"seed: 冻结={frozen_data.get('seed')}, 当前={pool_config.seed}"
)
if frozen_data.get("train_ratio") != pool_config.train_ratio: if frozen_data.get("train_ratio") != pool_config.train_ratio:
mismatches.append( mismatches.append(
f"train_ratio: 冻结={frozen_data.get('train_ratio')}, " f"train_ratio: 冻结={frozen_data.get('train_ratio')}, 当前={pool_config.train_ratio}"
f"当前={pool_config.train_ratio}"
) )
if frozen_data.get("baseline_run_id") != baseline_run_id: if frozen_data.get("baseline_run_id") != baseline_run_id:
mismatches.append( mismatches.append(
f"baseline_run_id: 冻结={frozen_data.get('baseline_run_id')}, " f"baseline_run_id: 冻结={frozen_data.get('baseline_run_id')}, 当前={baseline_run_id}"
f"当前={baseline_run_id}"
) )
if frozen_data.get("split_mode") != "per_category": if frozen_data.get("split_mode") != "per_category":
mismatches.append( mismatches.append(f"split_mode: 冻结={frozen_data.get('split_mode')}, 当前=per_category")
f"split_mode: 冻结={frozen_data.get('split_mode')}, 当前=per_category"
)
if mismatches: if mismatches:
raise ValueError( raise ValueError(
"per_category pools.json 与当前配置不一致:\n" "per_category pools.json 与当前配置不一致:\n"
@@ -484,13 +474,13 @@ def build_or_load_pools(
"FROM predictions WHERE run_id=?", "FROM predictions WHERE run_id=?",
(baseline_run_id,), (baseline_run_id,),
) )
correctness = { correctness = {r["question_id"]: r["prediction"] == r["answer"] for r in rows}
r["question_id"]: r["prediction"] == r["answer"]
for r in rows
}
new_cats = strategy.build_incremental( new_cats = strategy.build_incremental(
sorted(new_types), questions, correctness, pool_config, sorted(new_types),
questions,
correctness,
pool_config,
) )
# 合并新类别到 categories # 合并新类别到 categories
frozen_categories.update(new_cats) frozen_categories.update(new_cats)
@@ -525,7 +515,8 @@ def build_or_load_pools(
) )
logger.info( logger.info(
"per_category 增量追加 {} 个新类别: {}", "per_category 增量追加 {} 个新类别: {}",
len(new_types), sorted(new_types), len(new_types),
sorted(new_types),
) )
return load_pools(pools_path) return load_pools(pools_path)
@@ -596,7 +587,10 @@ class PerCategoryPoolStrategy:
for task_type in sorted(groups.keys()): for task_type in sorted(groups.keys()):
train, val = self._split_one_category( train, val = self._split_one_category(
groups[task_type], correctness, config.train_ratio, rng, groups[task_type],
correctness,
config.train_ratio,
rng,
) )
all_train.extend(train) all_train.extend(train)
all_val.extend(val) all_val.extend(val)
@@ -609,9 +603,7 @@ class PerCategoryPoolStrategy:
test = load_benchmark(config.test_questions_dir) test = load_benchmark(config.test_questions_dir)
# Phase 5: 计算 baseline_val_accuracy # Phase 5: 计算 baseline_val_accuracy
val_correct = sum( val_correct = sum(1 for q in all_val if correctness.get(q.question_id, False))
1 for q in all_val if correctness.get(q.question_id, False)
)
baseline_val_accuracy = val_correct / len(all_val) if all_val else 0.0 baseline_val_accuracy = val_correct / len(all_val) if all_val else 0.0
return Pools( return Pools(
@@ -654,9 +646,7 @@ class PerCategoryPoolStrategy:
# 校验 correctness 完整性 # 校验 correctness 完整性
missing = [q.question_id for q in questions if q.question_id not in correctness] missing = [q.question_id for q in questions if q.question_id not in correctness]
if missing: if missing:
raise ValueError( raise ValueError(f"correctness 缺失 {len(missing)} 题: {missing[:5]}")
f"correctness 缺失 {len(missing)} 题: {missing[:5]}"
)
correct_qs = [q for q in questions if correctness[q.question_id]] correct_qs = [q for q in questions if correctness[q.question_id]]
wrong_qs = [q for q in questions if not correctness[q.question_id]] wrong_qs = [q for q in questions if not correctness[q.question_id]]
@@ -667,7 +657,9 @@ class PerCategoryPoolStrategy:
label = "全部正确" if n_correct == n_total else "全部错误" label = "全部正确" if n_correct == n_total else "全部错误"
logger.warning( logger.warning(
"类别 {} {} ({} 题),退化为非分层随机划分", "类别 {} {} ({} 题),退化为非分层随机划分",
questions[0].task_type, label, n_total, questions[0].task_type,
label,
n_total,
) )
shuffled = list(questions) shuffled = list(questions)
rng.shuffle(shuffled) rng.shuffle(shuffled)
@@ -683,12 +675,8 @@ class PerCategoryPoolStrategy:
train = correct_qs[:train_correct] + wrong_qs[:train_wrong] train = correct_qs[:train_correct] + wrong_qs[:train_wrong]
val = correct_qs[train_correct:] + wrong_qs[train_wrong:] val = correct_qs[train_correct:] + wrong_qs[train_wrong:]
assert len(train) == n_train, ( assert len(train) == n_train, f"train 数量不匹配: {len(train)} != {n_train}"
f"train 数量不匹配: {len(train)} != {n_train}" assert len(val) == n_val, f"val 数量不匹配: {len(val)} != {n_val}"
)
assert len(val) == n_val, (
f"val 数量不匹配: {len(val)} != {n_val}"
)
return train, val return train, val
@@ -721,7 +709,10 @@ class PerCategoryPoolStrategy:
for task_type in sorted(groups.keys()): for task_type in sorted(groups.keys()):
train, val = self._split_one_category( train, val = self._split_one_category(
groups[task_type], correctness, config.train_ratio, rng, groups[task_type],
correctness,
config.train_ratio,
rng,
) )
result[task_type] = { result[task_type] = {
"train": [q.question_id for q in train], "train": [q.question_id for q in train],
+43 -18
View File
@@ -7,9 +7,10 @@ from __future__ import annotations
import json import json
from collections import Counter from collections import Counter
from pathlib import Path from typing import TYPE_CHECKING
import pytest if TYPE_CHECKING:
from pathlib import Path
from app.harness.pools import ( from app.harness.pools import (
PerCategoryPoolStrategy, PerCategoryPoolStrategy,
@@ -22,10 +23,14 @@ from core.types import GeneratedQuestion, PoolConfig
def _make_question(qid: str, task_type: str) -> GeneratedQuestion: def _make_question(qid: str, task_type: str) -> GeneratedQuestion:
"""构造测试用 GeneratedQuestion。""" """构造测试用 GeneratedQuestion。"""
return GeneratedQuestion( return GeneratedQuestion(
question_id=qid, video_id="v1", task_type=task_type, question_id=qid,
video_id="v1",
task_type=task_type,
question=f"Q {qid}?", question=f"Q {qid}?",
options=("A. a", "B. b", "C. c", "D. d"), options=("A. a", "B. b", "C. c", "D. d"),
answer="A", source_nodes=("n1",), difficulty="medium", answer="A",
source_nodes=("n1",),
difficulty="medium",
) )
@@ -35,10 +40,18 @@ class TestPerCategoryE2E:
def test_full_flow(self, tmp_path: Path) -> None: def test_full_flow(self, tmp_path: Path) -> None:
"""完整流程:12 类各 30 题 → 策略构建 → 冻结 → 加载 → 三池校验。""" """完整流程:12 类各 30 题 → 策略构建 → 冻结 → 加载 → 三池校验。"""
task_types = [ task_types = [
"Action Prediction", "Action Reasoning", "Action Recognition", "Action Prediction",
"Action Sequence", "Causal Reasoning", "Event Reasoning", "Action Reasoning",
"Object Interaction", "Object Reasoning", "Object Recognition", "Action Recognition",
"Scene Understanding", "Spatial Reasoning", "Temporal Reasoning", "Action Sequence",
"Causal Reasoning",
"Event Reasoning",
"Object Interaction",
"Object Reasoning",
"Object Recognition",
"Scene Understanding",
"Spatial Reasoning",
"Temporal Reasoning",
] ]
questions = [] questions = []
for tt in task_types: for tt in task_types:
@@ -52,11 +65,17 @@ class TestPerCategoryE2E:
correctness[q.question_id] = idx < 18 correctness[q.question_id] = idx < 18
config = PoolConfig( config = PoolConfig(
task_types=None, seed=42, baseline_run_id="baseline_v2", task_types=None,
diag_size=0, diag_correct_ratio=0.0, seed=42,
val_size=0, val_correct_ratio=0.0, baseline_run_id="baseline_v2",
test_size=0, eval_min_per_class=0, diag_size=0,
train_ratio=20 / 30, test_questions_dir=None, diag_correct_ratio=0.0,
val_size=0,
val_correct_ratio=0.0,
test_size=0,
eval_min_per_class=0,
train_ratio=20 / 30,
test_questions_dir=None,
) )
strategy = PerCategoryPoolStrategy() strategy = PerCategoryPoolStrategy()
@@ -107,11 +126,17 @@ class TestPerCategoryE2E:
correctness = {q.question_id: (i < 20) for i, q in enumerate(questions)} correctness = {q.question_id: (i < 20) for i, q in enumerate(questions)}
config = PoolConfig( config = PoolConfig(
task_types=("Object Recognition",), seed=42, baseline_run_id="bl", task_types=("Object Recognition",),
diag_size=0, diag_correct_ratio=0.0, seed=42,
val_size=0, val_correct_ratio=0.0, baseline_run_id="bl",
test_size=0, eval_min_per_class=0, diag_size=0,
train_ratio=20 / 30, test_questions_dir=None, diag_correct_ratio=0.0,
val_size=0,
val_correct_ratio=0.0,
test_size=0,
eval_min_per_class=0,
train_ratio=20 / 30,
test_questions_dir=None,
) )
strategy = PerCategoryPoolStrategy() strategy = PerCategoryPoolStrategy()