feat: pre-flight filter of untrainable task types before gate

This commit is contained in:
2026-07-16 06:16:02 -04:00
parent c44f6010eb
commit caea9150b3
10 changed files with 148 additions and 7 deletions
+1
View File
@@ -41,6 +41,7 @@ _STRUCTURAL_KEYS = (
"diag_size",
"val_size",
"batch_correct_ratio",
"trainable_min_units",
)
_DECISION_KEYS = (
+4
View File
@@ -60,6 +60,7 @@ class RunConfig:
batch_size: mini-batch 单批题目数。
min_class_per_batch: 单批中每个任务类型至少保留的题目数(< batch_size)。
eval_min_per_class: 验证池中每个任务类型至少保底的题目数。
trainable_min_units: 可训练性预检:每题型 diag+val 单元数下限,低于则剔除该题型。
early_stop_patience: 全局 best 连续未提升的容忍轮数,达到即早停。
test_size: held-out 测试池题目数。
use_slow_momentum: 是否启用快慢双速进化中的慢速 momentum 更新。
@@ -114,6 +115,7 @@ class RunConfig:
batch_size: int
min_class_per_batch: int
eval_min_per_class: int
trainable_min_units: int
early_stop_patience: int
test_size: int
use_slow_momentum: bool
@@ -297,6 +299,8 @@ def _validate_minibatch(config: RunConfig) -> None:
)
if config.eval_min_per_class < 1:
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":
floor = config.eval_min_per_class * _VIDEO_MME_TASK_TYPE_COUNT
if config.val_size < floor:
+80 -7
View File
@@ -18,7 +18,8 @@ import random
import shutil
import sqlite3
import tempfile
from dataclasses import dataclass, field
from collections import Counter
from dataclasses import dataclass, field, replace
from pathlib import Path
from typing import TYPE_CHECKING, Any
@@ -288,6 +289,52 @@ def _snapshot_current_skills(skills_dir: Path) -> dict[str, str]:
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(
workspace_dir: Path,
epoch: int,
@@ -790,8 +837,20 @@ class Runner:
三级嵌套:epoch → batch(step) → per-skill。
epoch 末 _slow_update_cycle 十步序。
训练收尾 _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):
if epoch == plan["resume_epoch"]:
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 准备训练起点。
参数:
pools: 已过可训练性预检的三池。
filtered_task_types: 预检后保留的题型(None 表示不限,由 gate 从 diag 推导)。
返回:
(state, total_steps, plan, saved_batches)。
"""
ckpt = load_checkpoint(self._config.workspace_dir) if self._config.resume else None
if self._config.resume and ckpt is None:
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:
state = self._init_train_state(pools, gate_pools, baseline_cache)
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"]
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 信息量阶梯与基线缓存。
副作用:设置 self._gate_questions_by_id(不进 checkpoint)。
参数:
pools: 冻结三池。
pools: 冻结三池(已过可训练性预检)
filtered_task_types: 预检保留的题型;None 时从 pools.diagnosis 推导。
返回:
(GatePools, BaselineCache)。
@@ -931,7 +999,12 @@ class Runner:
)
baseline_correctness = {r["question_id"]: r["prediction"] == r["answer"] for r in rows}
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(
workspace_dir=self._config.workspace_dir,
questions=questions,