feat: pre-flight filter of untrainable task types before gate
This commit is contained in:
@@ -41,6 +41,7 @@ _STRUCTURAL_KEYS = (
|
|||||||
"diag_size",
|
"diag_size",
|
||||||
"val_size",
|
"val_size",
|
||||||
"batch_correct_ratio",
|
"batch_correct_ratio",
|
||||||
|
"trainable_min_units",
|
||||||
)
|
)
|
||||||
|
|
||||||
_DECISION_KEYS = (
|
_DECISION_KEYS = (
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ class RunConfig:
|
|||||||
batch_size: mini-batch 单批题目数。
|
batch_size: mini-batch 单批题目数。
|
||||||
min_class_per_batch: 单批中每个任务类型至少保留的题目数(< batch_size)。
|
min_class_per_batch: 单批中每个任务类型至少保留的题目数(< batch_size)。
|
||||||
eval_min_per_class: 验证池中每个任务类型至少保底的题目数。
|
eval_min_per_class: 验证池中每个任务类型至少保底的题目数。
|
||||||
|
trainable_min_units: 可训练性预检:每题型 diag+val 单元数下限,低于则剔除该题型。
|
||||||
early_stop_patience: 全局 best 连续未提升的容忍轮数,达到即早停。
|
early_stop_patience: 全局 best 连续未提升的容忍轮数,达到即早停。
|
||||||
test_size: held-out 测试池题目数。
|
test_size: held-out 测试池题目数。
|
||||||
use_slow_momentum: 是否启用快慢双速进化中的慢速 momentum 更新。
|
use_slow_momentum: 是否启用快慢双速进化中的慢速 momentum 更新。
|
||||||
@@ -114,6 +115,7 @@ class RunConfig:
|
|||||||
batch_size: int
|
batch_size: int
|
||||||
min_class_per_batch: int
|
min_class_per_batch: int
|
||||||
eval_min_per_class: int
|
eval_min_per_class: int
|
||||||
|
trainable_min_units: int
|
||||||
early_stop_patience: int
|
early_stop_patience: int
|
||||||
test_size: int
|
test_size: int
|
||||||
use_slow_momentum: bool
|
use_slow_momentum: bool
|
||||||
@@ -297,6 +299,8 @@ def _validate_minibatch(config: RunConfig) -> None:
|
|||||||
)
|
)
|
||||||
if config.eval_min_per_class < 1:
|
if config.eval_min_per_class < 1:
|
||||||
raise ValueError(f"eval_min_per_class 必须 >= 1,实际: {config.eval_min_per_class}")
|
raise ValueError(f"eval_min_per_class 必须 >= 1,实际: {config.eval_min_per_class}")
|
||||||
|
if config.trainable_min_units < 1:
|
||||||
|
raise ValueError(f"trainable_min_units 必须 >= 1,实际: {config.trainable_min_units}")
|
||||||
if config.pool_split_mode != "per_category":
|
if config.pool_split_mode != "per_category":
|
||||||
floor = config.eval_min_per_class * _VIDEO_MME_TASK_TYPE_COUNT
|
floor = config.eval_min_per_class * _VIDEO_MME_TASK_TYPE_COUNT
|
||||||
if config.val_size < floor:
|
if config.val_size < floor:
|
||||||
|
|||||||
+80
-7
@@ -18,7 +18,8 @@ import random
|
|||||||
import shutil
|
import shutil
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import tempfile
|
import tempfile
|
||||||
from dataclasses import dataclass, field
|
from collections import Counter
|
||||||
|
from dataclasses import dataclass, field, replace
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
@@ -288,6 +289,52 @@ def _snapshot_current_skills(skills_dir: Path) -> dict[str, str]:
|
|||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_untrainable_types(
|
||||||
|
pools: Pools,
|
||||||
|
task_types: list[str] | None,
|
||||||
|
eval_min_per_class: int,
|
||||||
|
trainable_min_units: int,
|
||||||
|
) -> tuple[Pools, list[str] | None]:
|
||||||
|
"""剔除不可训练题型(val<eval_min_per_class 或 非test单元<trainable_min_units)。
|
||||||
|
|
||||||
|
非test单元数 = 该题型 diag+val 题数(single 题 unit==题;等于 gate 阶梯该类候选数)。
|
||||||
|
test 池不过滤(继续报告全题型准确率)。在 gate 建立前调用,避免样本不足的
|
||||||
|
微型题型进入信息量阶梯导致门控崩溃。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
pools: 冻结三池。
|
||||||
|
task_types: 显式题型子集(None 表示全部),过滤后按 keep 收窄。
|
||||||
|
eval_min_per_class: 验证池每类保底题数下限。
|
||||||
|
trainable_min_units: 每类可训练所需最小 diag+val 单元数。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
过滤后的 (pools, task_types):pools.diagnosis/validation 仅保留 keep 题型,
|
||||||
|
test 原样;task_types 收窄为 keep(原 None 时返回 sorted(keep))。
|
||||||
|
"""
|
||||||
|
diag_by_type = Counter(q.task_type for q in pools.diagnosis)
|
||||||
|
val_by_type = Counter(q.task_type for q in pools.validation)
|
||||||
|
keep: set[str] = set()
|
||||||
|
dropped: list[tuple[str, str]] = []
|
||||||
|
for tt in set(diag_by_type) | set(val_by_type):
|
||||||
|
n_val = val_by_type.get(tt, 0)
|
||||||
|
n_units = diag_by_type.get(tt, 0) + n_val
|
||||||
|
if n_val < eval_min_per_class:
|
||||||
|
dropped.append((tt, f"val={n_val}<{eval_min_per_class}"))
|
||||||
|
elif n_units < trainable_min_units:
|
||||||
|
dropped.append((tt, f"units={n_units}<{trainable_min_units}"))
|
||||||
|
else:
|
||||||
|
keep.add(tt)
|
||||||
|
for tt, why in sorted(dropped):
|
||||||
|
logger.warning("可训练性预检剔除题型 {}({})", tt, why)
|
||||||
|
new_pools = replace(
|
||||||
|
pools,
|
||||||
|
diagnosis=[q for q in pools.diagnosis if q.task_type in keep],
|
||||||
|
validation=[q for q in pools.validation if q.task_type in keep],
|
||||||
|
)
|
||||||
|
new_types = [t for t in task_types if t in keep] if task_types is not None else sorted(keep)
|
||||||
|
return new_pools, new_types
|
||||||
|
|
||||||
|
|
||||||
def _should_early_stop(
|
def _should_early_stop(
|
||||||
workspace_dir: Path,
|
workspace_dir: Path,
|
||||||
epoch: int,
|
epoch: int,
|
||||||
@@ -790,8 +837,20 @@ class Runner:
|
|||||||
三级嵌套:epoch → batch(step) → per-skill。
|
三级嵌套:epoch → batch(step) → per-skill。
|
||||||
epoch 末 _slow_update_cycle 十步序。
|
epoch 末 _slow_update_cycle 十步序。
|
||||||
训练收尾 _deliver_best + _final_test_eval。
|
训练收尾 _deliver_best + _final_test_eval。
|
||||||
|
|
||||||
|
入口第一步做可训练性预检:剔除样本不足的微型题型(防 gate 阶梯崩溃),
|
||||||
|
过滤后的 pools 贯穿 batch/step/slow-update/final-eval 全部消费;filtered_task_types
|
||||||
|
透传到 gate 建立(不改 frozen RunConfig)。
|
||||||
"""
|
"""
|
||||||
state, total_steps, plan, saved_batches = await self._setup_train_run(pools)
|
pools, filtered_task_types = _filter_untrainable_types(
|
||||||
|
pools,
|
||||||
|
list(self._config.task_types) if self._config.task_types is not None else None,
|
||||||
|
self._config.eval_min_per_class,
|
||||||
|
self._config.trainable_min_units,
|
||||||
|
)
|
||||||
|
state, total_steps, plan, saved_batches = await self._setup_train_run(
|
||||||
|
pools, filtered_task_types
|
||||||
|
)
|
||||||
for epoch in range(plan["first_epoch"], self._config.epochs + 1):
|
for epoch in range(plan["first_epoch"], self._config.epochs + 1):
|
||||||
if epoch == plan["resume_epoch"]:
|
if epoch == plan["resume_epoch"]:
|
||||||
batches = [_batch_from_ids(pools, ids) for ids in saved_batches]
|
batches = [_batch_from_ids(pools, ids) for ids in saved_batches]
|
||||||
@@ -859,16 +918,22 @@ class Runner:
|
|||||||
# 训练初始化
|
# 训练初始化
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
|
||||||
async def _setup_train_run(self, pools: Pools) -> tuple[_TrainState, int, dict, list | None]:
|
async def _setup_train_run(
|
||||||
|
self, pools: Pools, filtered_task_types: list[str] | None
|
||||||
|
) -> tuple[_TrainState, int, dict, list | None]:
|
||||||
"""据是否 --resume 准备训练起点。
|
"""据是否 --resume 准备训练起点。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
pools: 已过可训练性预检的三池。
|
||||||
|
filtered_task_types: 预检后保留的题型(None 表示不限,由 gate 从 diag 推导)。
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
(state, total_steps, plan, saved_batches)。
|
(state, total_steps, plan, saved_batches)。
|
||||||
"""
|
"""
|
||||||
ckpt = load_checkpoint(self._config.workspace_dir) if self._config.resume else None
|
ckpt = load_checkpoint(self._config.workspace_dir) if self._config.resume else None
|
||||||
if self._config.resume and ckpt is None:
|
if self._config.resume and ckpt is None:
|
||||||
raise RuntimeError("--resume 但 checkpoint.json 不存在,拒绝静默从头重训")
|
raise RuntimeError("--resume 但 checkpoint.json 不存在,拒绝静默从头重训")
|
||||||
gate_pools, baseline_cache = self._init_gate_pools(pools)
|
gate_pools, baseline_cache = self._init_gate_pools(pools, filtered_task_types)
|
||||||
if not ckpt:
|
if not ckpt:
|
||||||
state = self._init_train_state(pools, gate_pools, baseline_cache)
|
state = self._init_train_state(pools, gate_pools, baseline_cache)
|
||||||
total_steps = _compute_total_steps(pools, state.correctness, self._config)
|
total_steps = _compute_total_steps(pools, state.correctness, self._config)
|
||||||
@@ -894,13 +959,16 @@ class Runner:
|
|||||||
)
|
)
|
||||||
return state, ckpt["progress"]["total_steps"], plan, ckpt["epoch_batches"]
|
return state, ckpt["progress"]["total_steps"], plan, ckpt["epoch_batches"]
|
||||||
|
|
||||||
def _init_gate_pools(self, pools: Pools) -> tuple[GatePools, BaselineCache]:
|
def _init_gate_pools(
|
||||||
|
self, pools: Pools, filtered_task_types: list[str] | None
|
||||||
|
) -> tuple[GatePools, BaselineCache]:
|
||||||
"""构建/加载 CE-Gate 信息量阶梯与基线缓存。
|
"""构建/加载 CE-Gate 信息量阶梯与基线缓存。
|
||||||
|
|
||||||
副作用:设置 self._gate_questions_by_id(不进 checkpoint)。
|
副作用:设置 self._gate_questions_by_id(不进 checkpoint)。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
pools: 冻结三池。
|
pools: 冻结三池(已过可训练性预检)。
|
||||||
|
filtered_task_types: 预检保留的题型;None 时从 pools.diagnosis 推导。
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
(GatePools, BaselineCache)。
|
(GatePools, BaselineCache)。
|
||||||
@@ -931,7 +999,12 @@ class Runner:
|
|||||||
)
|
)
|
||||||
baseline_correctness = {r["question_id"]: r["prediction"] == r["answer"] for r in rows}
|
baseline_correctness = {r["question_id"]: r["prediction"] == r["answer"] for r in rows}
|
||||||
logger.info("gate 阶梯基线对错覆盖 {} 题", len(baseline_correctness))
|
logger.info("gate 阶梯基线对错覆盖 {} 题", len(baseline_correctness))
|
||||||
gate_task_types = sorted({q.task_type for q in pools.diagnosis})
|
# 预检保留的题型优先;None 时从(已过滤的)诊断池推导,二者一致
|
||||||
|
gate_task_types = (
|
||||||
|
sorted(filtered_task_types)
|
||||||
|
if filtered_task_types is not None
|
||||||
|
else sorted({q.task_type for q in pools.diagnosis})
|
||||||
|
)
|
||||||
gate_pools = build_or_load_gate_pools(
|
gate_pools = build_or_load_gate_pools(
|
||||||
workspace_dir=self._config.workspace_dir,
|
workspace_dir=self._config.workspace_dir,
|
||||||
questions=questions,
|
questions=questions,
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ harness:
|
|||||||
batch_correct_ratio: 0.5
|
batch_correct_ratio: 0.5
|
||||||
momentum_samples: 20
|
momentum_samples: 20
|
||||||
eval_min_per_class: 2
|
eval_min_per_class: 2
|
||||||
|
trainable_min_units: 8
|
||||||
early_stop_patience: 8
|
early_stop_patience: 8
|
||||||
use_slow_momentum: true
|
use_slow_momentum: true
|
||||||
# 池构建策略
|
# 池构建策略
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ harness:
|
|||||||
batch_correct_ratio: 0.5
|
batch_correct_ratio: 0.5
|
||||||
momentum_samples: 20
|
momentum_samples: 20
|
||||||
eval_min_per_class: 2
|
eval_min_per_class: 2
|
||||||
|
trainable_min_units: 8
|
||||||
early_stop_patience: 4
|
early_stop_patience: 4
|
||||||
test_size: 63
|
test_size: 63
|
||||||
diag_size: 20
|
diag_size: 20
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ class _FakeConfig:
|
|||||||
diag_size: int = 30
|
diag_size: int = 30
|
||||||
val_size: int = 50
|
val_size: int = 50
|
||||||
batch_correct_ratio: float = 0.5
|
batch_correct_ratio: float = 0.5
|
||||||
|
trainable_min_units: int = 8
|
||||||
edit_budget_start: int = 6
|
edit_budget_start: int = 6
|
||||||
edit_budget_end: int = 3
|
edit_budget_end: int = 3
|
||||||
early_stop_patience: int = 3
|
early_stop_patience: int = 3
|
||||||
@@ -289,6 +290,7 @@ class TestFingerprintStructuralVsDecision:
|
|||||||
"diag_size",
|
"diag_size",
|
||||||
"val_size",
|
"val_size",
|
||||||
"batch_correct_ratio",
|
"batch_correct_ratio",
|
||||||
|
"trainable_min_units",
|
||||||
}
|
}
|
||||||
decision = {
|
decision = {
|
||||||
"edit_budget_start",
|
"edit_budget_start",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ def _valid_kwargs() -> dict:
|
|||||||
"batch_size": 15,
|
"batch_size": 15,
|
||||||
"min_class_per_batch": 2,
|
"min_class_per_batch": 2,
|
||||||
"eval_min_per_class": 2,
|
"eval_min_per_class": 2,
|
||||||
|
"trainable_min_units": 8,
|
||||||
"early_stop_patience": 8,
|
"early_stop_patience": 8,
|
||||||
"test_size": 60,
|
"test_size": 60,
|
||||||
"use_slow_momentum": True,
|
"use_slow_momentum": True,
|
||||||
|
|||||||
@@ -317,6 +317,7 @@ class TestBuildOrLoadPoolsFrozen:
|
|||||||
batch_size=15,
|
batch_size=15,
|
||||||
min_class_per_batch=2,
|
min_class_per_batch=2,
|
||||||
eval_min_per_class=2,
|
eval_min_per_class=2,
|
||||||
|
trainable_min_units=8,
|
||||||
early_stop_patience=4,
|
early_stop_patience=4,
|
||||||
test_size=10,
|
test_size=10,
|
||||||
use_slow_momentum=True,
|
use_slow_momentum=True,
|
||||||
@@ -870,6 +871,7 @@ class TestRunHoldoutEvalConfig:
|
|||||||
batch_size=15,
|
batch_size=15,
|
||||||
min_class_per_batch=2,
|
min_class_per_batch=2,
|
||||||
eval_min_per_class=2,
|
eval_min_per_class=2,
|
||||||
|
trainable_min_units=8,
|
||||||
early_stop_patience=4,
|
early_stop_patience=4,
|
||||||
test_size=30,
|
test_size=30,
|
||||||
use_slow_momentum=True,
|
use_slow_momentum=True,
|
||||||
@@ -920,6 +922,7 @@ class TestRunHoldoutEvalConfig:
|
|||||||
batch_size=15,
|
batch_size=15,
|
||||||
min_class_per_batch=2,
|
min_class_per_batch=2,
|
||||||
eval_min_per_class=2,
|
eval_min_per_class=2,
|
||||||
|
trainable_min_units=8,
|
||||||
early_stop_patience=4,
|
early_stop_patience=4,
|
||||||
test_size=30,
|
test_size=30,
|
||||||
use_slow_momentum=True,
|
use_slow_momentum=True,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from app.harness.runner import (
|
|||||||
_build_comparison_pairs,
|
_build_comparison_pairs,
|
||||||
_compute_total_steps,
|
_compute_total_steps,
|
||||||
_fallback_summary,
|
_fallback_summary,
|
||||||
|
_filter_untrainable_types,
|
||||||
_format_applied_edits,
|
_format_applied_edits,
|
||||||
_guard_infra_failures,
|
_guard_infra_failures,
|
||||||
_outcome_to_quadrant_pairs,
|
_outcome_to_quadrant_pairs,
|
||||||
@@ -401,6 +402,58 @@ class TestShouldEarlyStop:
|
|||||||
assert state.epochs_since_best_improved == 2
|
assert state.epochs_since_best_improved == 2
|
||||||
|
|
||||||
|
|
||||||
|
class TestFilterUntrainableTypes:
|
||||||
|
"""_filter_untrainable_types 可训练性预检纯函数。"""
|
||||||
|
|
||||||
|
def test_untrainable_types_filtered_before_gate(self) -> None:
|
||||||
|
"""val<eval_min_per_class 或 非test单元<trainable_min_units 的题型被剔除。"""
|
||||||
|
# 题型 A:diag=5 + val=5 → units=10、val=5,可训
|
||||||
|
diag = [_FakeQuestion(question_id=f"A-d{i}", task_type="A") for i in range(5)]
|
||||||
|
val = [_FakeQuestion(question_id=f"A-v{i}", task_type="A") for i in range(5)]
|
||||||
|
# 题型 B:val=0(<eval_min_per_class)不可训
|
||||||
|
diag += [_FakeQuestion(question_id=f"B-d{i}", task_type="B") for i in range(2)]
|
||||||
|
pools = _FakePools(diagnosis=diag, validation=val, test=[])
|
||||||
|
|
||||||
|
new_pools, new_types = _filter_untrainable_types(
|
||||||
|
pools,
|
||||||
|
task_types=["A", "B"],
|
||||||
|
eval_min_per_class=2,
|
||||||
|
trainable_min_units=8,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {q.task_type for q in new_pools.diagnosis} == {"A"}
|
||||||
|
assert {q.task_type for q in new_pools.validation} == {"A"}
|
||||||
|
assert new_types == ["A"]
|
||||||
|
|
||||||
|
def test_units_below_threshold_filtered(self) -> None:
|
||||||
|
"""val 达标但 diag+val 单元数 < trainable_min_units 的题型被剔除。"""
|
||||||
|
# 题型 C:val=2(>=2)但 units=2+0=... 补 diag 使总数不足
|
||||||
|
val = [_FakeQuestion(question_id=f"C-v{i}", task_type="C") for i in range(2)]
|
||||||
|
diag = [_FakeQuestion(question_id="C-d0", task_type="C")] # units=3 < 8
|
||||||
|
pools = _FakePools(diagnosis=diag, validation=val, test=[])
|
||||||
|
|
||||||
|
new_pools, new_types = _filter_untrainable_types(
|
||||||
|
pools, task_types=None, eval_min_per_class=2, trainable_min_units=8
|
||||||
|
)
|
||||||
|
|
||||||
|
assert new_pools.diagnosis == []
|
||||||
|
assert new_pools.validation == []
|
||||||
|
assert new_types == []
|
||||||
|
|
||||||
|
def test_test_pool_untouched(self) -> None:
|
||||||
|
"""test 池不参与过滤(继续报告全题型准确率)。"""
|
||||||
|
val = [_FakeQuestion(question_id=f"A-v{i}", task_type="A") for i in range(8)]
|
||||||
|
diag = [_FakeQuestion(question_id=f"A-d{i}", task_type="A") for i in range(8)]
|
||||||
|
test = [_FakeQuestion(question_id="B-t0", task_type="B")]
|
||||||
|
pools = _FakePools(diagnosis=diag, validation=val, test=test)
|
||||||
|
|
||||||
|
new_pools, _ = _filter_untrainable_types(
|
||||||
|
pools, task_types=None, eval_min_per_class=2, trainable_min_units=8
|
||||||
|
)
|
||||||
|
|
||||||
|
assert new_pools.test == test
|
||||||
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# 13c: Probation 数据结构测试
|
# 13c: Probation 数据结构测试
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -690,6 +743,7 @@ class TestRunnerFactoryInjection:
|
|||||||
"batch_size": 5,
|
"batch_size": 5,
|
||||||
"min_class_per_batch": 2,
|
"min_class_per_batch": 2,
|
||||||
"eval_min_per_class": 2,
|
"eval_min_per_class": 2,
|
||||||
|
"trainable_min_units": 8,
|
||||||
"early_stop_patience": 3,
|
"early_stop_patience": 3,
|
||||||
"test_size": 10,
|
"test_size": 10,
|
||||||
"use_slow_momentum": False,
|
"use_slow_momentum": False,
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ def _base_config(workspace_dir: Path, store_dir: Path) -> RunConfig:
|
|||||||
batch_size=5,
|
batch_size=5,
|
||||||
min_class_per_batch=2,
|
min_class_per_batch=2,
|
||||||
eval_min_per_class=2,
|
eval_min_per_class=2,
|
||||||
|
trainable_min_units=8,
|
||||||
early_stop_patience=3,
|
early_stop_patience=3,
|
||||||
test_size=10,
|
test_size=10,
|
||||||
use_slow_momentum=False,
|
use_slow_momentum=False,
|
||||||
|
|||||||
Reference in New Issue
Block a user