feat: pre-flight filter of untrainable task types before gate
This commit is contained in:
@@ -41,6 +41,7 @@ _STRUCTURAL_KEYS = (
|
||||
"diag_size",
|
||||
"val_size",
|
||||
"batch_correct_ratio",
|
||||
"trainable_min_units",
|
||||
)
|
||||
|
||||
_DECISION_KEYS = (
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user