Files
Video-Tree-TRM5/research-wiki/plans/2026-07-14-action-recognition-training.md
iomgaa dec7346da3 feat(harness): add Action Recognition training experiment
- PerCategoryPoolStrategy: filter test pool by task_types
- RunConfig: add run_holdout_eval toggle (default true)
- load_config: fix YAML task_types list-to-tuple conversion
- Runner: conditionally skip _holdout_four_way when disabled
- CLI: add --no-run-holdout-eval flag
- New config/train_action_recognition.yaml (3 epochs, per_category)
- New scripts/train_action_recognition.sh (baseline + seed + train)
2026-07-14 00:58:54 -04:00

662 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Action Recognition 单题型首次训练实验 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 用 Action Recognition 单题型端到端验证训练管线,包含 3 处代码修改、1 套实验配置、1 个前置脚本。
**Architecture:** 修改 `PerCategoryPoolStrategy.build()` 使 test 池受 `task_types` 过滤;RunConfig 新增 `run_holdout_eval` 开关控制 epoch 内四向 held-out`load_config` 修复 YAML list→tuple 转换;新建实验 YAML 和 sh 脚本驱动训练。
**Tech Stack:** Python 3.11, pytest, YAML, bash
---
### Task 0: 清理 v2-360 目录中的 backup 文件
**Files:**
- Modify: `store/questions/generated-v2-360/`(重命名文件)
`load_benchmark()` 会加载目录下所有 `*.json``accepted_questions_backup_220.json` 含 18 道重复 AR 题会干扰训练。
- [ ] **Step 1: 重命名 backup 文件使其不被 load_benchmark 加载**
```bash
mv store/questions/generated-v2-360/accepted_questions_backup_220.json \
store/questions/generated-v2-360/accepted_questions_backup_220.json.bak
```
- [ ] **Step 2: 验证 load_benchmark 只加载 180 题**
```bash
conda activate Video-Tree-TRM & python -c "
from pathlib import Path
from app.question_gen import load_benchmark
qs = load_benchmark(Path('store/questions/generated-v2-360'))
print(f'Total: {len(qs)}')
ar = [q for q in qs if q.task_type == 'Action Recognition']
print(f'Action Recognition: {len(ar)}')
assert len(qs) == 180, f'Expected 180, got {len(qs)}'
assert len(ar) == 30, f'Expected 30 AR, got {len(ar)}'
print('OK')
"
```
预期:Total: 180, Action Recognition: 30, OK
- [ ] **Step 3: 提交**
```bash
git add -A store/questions/generated-v2-360/
git commit -m "chore: rename v2-360 backup JSON to .bak to exclude from load_benchmark"
```
---
### Task 1: PerCategoryPoolStrategy test 池 task_types 过滤
**Files:**
- Modify: `app/harness/pools.py:600-606`
- Test: `tests/unit/test_harness_pools.py`
- [ ] **Step 1: 写失败测试 — test 池按 task_types 过滤**
`tests/unit/test_harness_pools.py``TestPerCategoryPoolStrategy` 类末尾新增。
注意:`load_benchmark` 要求每个 JSON 文件内容为**题目数组**(`[{...}]`),不是单个 dict。
```python
def test_per_category_test_pool_filtered_by_task_types(self, tmp_path: Path):
"""test_questions_dir 含多题型时,test 池只保留 task_types 指定的题型。"""
test_dir = tmp_path / "test_questions"
test_dir.mkdir()
for tt in ("Action Recognition", "Object Reasoning", "Counting Problem"):
items = []
for i in range(10):
qid = f"{tt.replace(' ', '_')}_{i:03d}"
items.append({
"question_id": qid,
"video_id": "v1",
"task_type": tt,
"question": f"Q {qid}?",
"options": ["A. a", "B. b", "C. c", "D. d"],
"answer": "A",
})
slug = tt.lower().replace(" ", "_")
(test_dir / f"{slug}.json").write_text(
json.dumps(items, ensure_ascii=False), encoding="utf-8"
)
questions = [_make_question(f"ar_{i:03d}", "Action Recognition") for i in range(30)]
correctness = {q.question_id: (i < 20) for i, q in enumerate(questions)}
config = PoolConfig(
task_types=("Action Recognition",),
seed=42,
baseline_run_id="bl",
diag_size=0,
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=test_dir,
)
strategy = PerCategoryPoolStrategy()
pools = strategy.build(questions, correctness, config)
assert len(pools.test) == 10
assert all(q.task_type == "Action Recognition" for q in pools.test)
def test_per_category_test_pool_no_filter_when_task_types_none(self, tmp_path: Path):
"""task_types=None 时 test 池不过滤,保留全部题型。"""
test_dir = tmp_path / "test_questions"
test_dir.mkdir()
for tt in ("Action Recognition", "Object Reasoning"):
items = []
for i in range(5):
qid = f"{tt.replace(' ', '_')}_{i:03d}"
items.append({
"question_id": qid,
"video_id": "v1",
"task_type": tt,
"question": f"Q {qid}?",
"options": ["A. a", "B. b", "C. c", "D. d"],
"answer": "A",
})
slug = tt.lower().replace(" ", "_")
(test_dir / f"{slug}.json").write_text(
json.dumps(items, ensure_ascii=False), encoding="utf-8"
)
questions = [_make_question(f"q_{i:03d}", "Action Recognition") for i in range(10)]
correctness = {q.question_id: True for q in questions}
config = PoolConfig(
task_types=None,
seed=42,
baseline_run_id="bl",
diag_size=0,
diag_correct_ratio=0.0,
val_size=0,
val_correct_ratio=0.0,
test_size=0,
eval_min_per_class=0,
train_ratio=0.667,
test_questions_dir=test_dir,
)
strategy = PerCategoryPoolStrategy()
pools = strategy.build(questions, correctness, config)
assert len(pools.test) == 10
```
- [ ] **Step 2: 运行测试验证失败**
```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_harness_pools.py::TestPerCategoryPoolStrategy::test_per_category_test_pool_filtered_by_task_types -v
```
预期:FAIL — `assert len(pools.test) == 10` 失败(实际 30 题,未过滤)。
- [ ] **Step 3: 实现 test 池过滤**
修改 `app/harness/pools.py` `PerCategoryPoolStrategy.build()` 的 Phase 4
```python
# Phase 4: test 池(从外部目录加载,无则空)
test: list[GeneratedQuestion] = []
if config.test_questions_dir is not None:
from app.question_gen import load_benchmark
test = load_benchmark(config.test_questions_dir)
if config.task_types is not None:
allowed = set(config.task_types)
test = [q for q in test if q.task_type in allowed]
```
- [ ] **Step 4: 运行测试验证通过**
```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_harness_pools.py::TestPerCategoryPoolStrategy::test_per_category_test_pool_filtered_by_task_types tests/unit/test_harness_pools.py::TestPerCategoryPoolStrategy::test_per_category_test_pool_no_filter_when_task_types_none -v
```
预期:PASS
- [ ] **Step 5: 运行全部 pool 测试确保无回归**
```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_harness_pools.py tests/integration/test_pool_strategy.py -v
```
预期:全部 PASS
- [ ] **Step 6: 提交**
```bash
git add app/harness/pools.py tests/unit/test_harness_pools.py
git commit -m "feat(pools): filter test pool by task_types in PerCategoryPoolStrategy"
```
---
### Task 2: RunConfig 新增 run_holdout_eval + load_config list→tuple 修复 + Runner 条件跳过
**Files:**
- Modify: `app/harness/config.py:147` (字段) + `app/harness/config.py:437` (list→tuple)
- Modify: `app/harness/runner.py:1371`
- Modify: `main.py:205`
- Test: `tests/unit/test_harness_pools.py`
- [ ] **Step 1: 写失败测试 — RunConfig 新字段**
`tests/unit/test_harness_pools.py` 文件末尾新增:
```python
class TestRunHoldoutEvalConfig:
"""run_holdout_eval 字段校验。"""
def test_default_true(self):
"""run_holdout_eval 默认值为 True。"""
from app.harness.config import RunConfig
config = RunConfig(
workspace_dir=Path("/tmp/ws"),
store_dir=Path("/tmp/store"),
mode="train",
concurrency=4,
max_steps=10,
skill_mode="auto",
n_samples=0,
questions="benchmarks/Video-MME",
skills_version="v1",
prompts_version="v1",
epochs=1,
diag_size=100,
diag_correct_ratio=0.5,
val_size=30,
val_correct_ratio=0.5,
edit_budget_start=5,
edit_budget_end=2,
batch_size=15,
min_class_per_batch=2,
eval_min_per_class=2,
early_stop_patience=4,
test_size=30,
use_slow_momentum=True,
gate_e_confirm=20.0,
gate_e_provisional=3.0,
gate_w_net_min=2,
gate_delta_min=0.02,
gate_lambda_dir=-0.642,
gate_e_rollback=10.0,
gate_block=8,
gate_n_max=40,
gate_p_low=0.05,
gate_p_high=0.95,
gate_probe_quota=0.2,
gate_gamma_decay=0.9,
gate_cooldown_steps=2,
gate_guard_err=0.10,
skill_update_mode="patch",
appendix_consolidate_threshold=6,
run_id="test_run",
)
assert config.run_holdout_eval is True
def test_explicit_false(self):
"""run_holdout_eval 可设为 False。"""
from app.harness.config import RunConfig
config = RunConfig(
workspace_dir=Path("/tmp/ws"),
store_dir=Path("/tmp/store"),
mode="train",
concurrency=4,
max_steps=10,
skill_mode="auto",
n_samples=0,
questions="benchmarks/Video-MME",
skills_version="v1",
prompts_version="v1",
epochs=1,
diag_size=100,
diag_correct_ratio=0.5,
val_size=30,
val_correct_ratio=0.5,
edit_budget_start=5,
edit_budget_end=2,
batch_size=15,
min_class_per_batch=2,
eval_min_per_class=2,
early_stop_patience=4,
test_size=30,
use_slow_momentum=True,
gate_e_confirm=20.0,
gate_e_provisional=3.0,
gate_w_net_min=2,
gate_delta_min=0.02,
gate_lambda_dir=-0.642,
gate_e_rollback=10.0,
gate_block=8,
gate_n_max=40,
gate_p_low=0.05,
gate_p_high=0.95,
gate_probe_quota=0.2,
gate_gamma_decay=0.9,
gate_cooldown_steps=2,
gate_guard_err=0.10,
skill_update_mode="patch",
appendix_consolidate_threshold=6,
run_id="test_run",
run_holdout_eval=False,
)
assert config.run_holdout_eval is False
```
- [ ] **Step 2: 运行测试验证失败**
```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_harness_pools.py::TestRunHoldoutEvalConfig -v
```
预期:FAIL — `TypeError: __init__() got an unexpected keyword argument 'run_holdout_eval'`
- [ ] **Step 3: RunConfig 新增 run_holdout_eval 字段**
`app/harness/config.py` 的有默认值字段区(`test_questions` 后面)新增:
```python
test_questions: str = "benchmarks/Video-MME"
run_holdout_eval: bool = True
```
- [ ] **Step 4: 运行测试验证通过**
```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_harness_pools.py::TestRunHoldoutEvalConfig -v
```
预期:PASS
- [ ] **Step 5: load_config 修复 YAML list→tuple 转换**
`RunConfig.task_types` 类型为 `tuple[str, ...] | None`,但 YAML list 加载后不转换。
`app/harness/config.py` `load_config()` Phase 4(类型转换区)后面新增:
```python
# Phase 4: 类型转换 — 路径字段转 Path
for field_name in _PATH_FIELDS:
if field_name in yaml_data:
yaml_data[field_name] = Path(yaml_data[field_name])
# Phase 4b: 类型转换 — task_types list → tuple
if "task_types" in yaml_data and yaml_data["task_types"] is not None:
yaml_data["task_types"] = tuple(yaml_data["task_types"])
```
- [ ] **Step 6: main.py 新增 CLI 开关**
`main.py` `_build_parser()``--test-questions` 后面新增:
```python
parser.add_argument("--test-questions", type=str, dest="test_questions")
parser.add_argument(
"--no-run-holdout-eval",
action="store_true",
dest="no_run_holdout_eval",
)
return parser
```
`main()``cli_overrides` 构建处(约第 269 行 `cli_overrides = ...` 之后)处理取反映射:
```python
cli_overrides = {k: v for k, v in cli_args.items() if k != "config"}
if cli_overrides.get("no_run_holdout_eval"):
cli_overrides["run_holdout_eval"] = False
cli_overrides.pop("no_run_holdout_eval", None)
```
- [ ] **Step 7: Runner `_slow_update_cycle` 条件跳过 holdout**
修改 `app/harness/runner.py` `_slow_update_cycle` 的 Phase 9(约第 1371 行):
将:
```python
await self._holdout_four_way(epoch, pools, state, eval_skills_version, eval_prompts_version)
```
改为:
```python
if self._config.run_holdout_eval:
await self._holdout_four_way(epoch, pools, state, eval_skills_version, eval_prompts_version)
```
- [ ] **Step 8: 运行全部测试确认无回归**
```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_harness_pools.py tests/unit/test_harness_store.py tests/integration/test_pool_strategy.py -v
```
预期:全部 PASS
- [ ] **Step 9: 提交**
```bash
git add app/harness/config.py app/harness/runner.py main.py tests/unit/test_harness_pools.py
git commit -m "feat(config): add run_holdout_eval toggle and fix YAML task_types list-to-tuple"
```
---
### Task 3: 实验配置文件
**Files:**
- Create: `config/train_action_recognition.yaml`
- [ ] **Step 1: 创建实验 YAML**
```yaml
# config/train_action_recognition.yaml
# Action Recognition 单题型首次训练实验
# 设计文档: research-wiki/designs/2026-07-14-action-recognition-training-design.md
harness:
workspace_dir: "workspaces/train-action-recognition"
store_dir: store
mode: train
run_id: train_ar_v1
concurrency: 24
max_steps: 40
skill_mode: auto
n_samples: 0
questions: "generated-v2-360"
skills_version: v1
prompts_version: v1
epochs: 3
# CE-Gate 参数(沿用 default.yaml
gate_e_confirm: 20.0
gate_e_provisional: 3.0
gate_w_net_min: 2
gate_delta_min: 0.02
gate_lambda_dir: -0.642
gate_e_rollback: 10.0
gate_block: 8
gate_n_max: 40
gate_p_low: 0.05
gate_p_high: 0.95
gate_probe_quota: 0.2
gate_gamma_decay: 0.9
gate_cooldown_steps: 2
gate_guard_err: 0.10
# 进化参数
edit_budget_start: 5
edit_budget_end: 2
skill_update_mode: patch
appendix_consolidate_threshold: 6
# 池配置 — per_category 单题型
pool_split_mode: per_category
task_types:
- "Action Recognition"
train_ratio: 0.667
test_questions: "benchmarks/Video-MME"
run_holdout_eval: false
# mini-batch
batch_size: 10
min_class_per_batch: 2
batch_correct_ratio: 0.5
momentum_samples: 20
eval_min_per_class: 2
early_stop_patience: 4
test_size: 63
diag_size: 20
diag_correct_ratio: 0.5
val_size: 10
val_correct_ratio: 0.5
use_slow_momentum: true
embed:
backend: "local"
model_name: "BAAI/bge-base-zh-v1.5"
embed_dim: 768
device: "cuda"
```
- [ ] **Step 2: 验证 YAML 可正确加载为 RunConfig**
```bash
conda activate Video-Tree-TRM & python -c "
from app.harness.config import load_config
from pathlib import Path
config = load_config(Path('config/train_action_recognition.yaml'))
assert config.task_types == ('Action Recognition',), f'task_types={config.task_types}'
assert config.run_holdout_eval is False
assert config.pool_split_mode == 'per_category'
print('Config loaded OK')
"
```
预期:Config loaded OK
- [ ] **Step 3: 提交**
```bash
git add config/train_action_recognition.yaml
git commit -m "config: add train_action_recognition experiment YAML"
```
---
### Task 4: 前置脚本 — baseline infer + seed 创建 + 训练
**Files:**
- Create: `scripts/train_action_recognition.sh`
- [ ] **Step 1: 创建脚本**
```bash
#!/usr/bin/env bash
# Action Recognition 单题型训练实验
# 设计文档: research-wiki/designs/2026-07-14-action-recognition-training-design.md
#
# 三阶段:
# Phase 0: baseline infer (v2-360 Action Recognition 30 题)
# Phase 1: create seed (v2ar-baseline)
# Phase 2: train (3 epochs, per_category)
#
# 用法:
# CUDA_VISIBLE_DEVICES=0 bash scripts/train_action_recognition.sh
# MODE=mock bash scripts/train_action_recognition.sh # 跳过 Phase 0/1
set -euo pipefail
cd "$(dirname "$0")/.."
CUDA_VISIBLE_DEVICES="${CUDA_VISIBLE_DEVICES:-0}"
export CUDA_VISIBLE_DEVICES
export HF_HUB_OFFLINE=1
export TRANSFORMERS_OFFLINE=1
export PYTHONUNBUFFERED=1
set -a
source .env
set +a
PYTHON="$(conda run -n Video-Tree-TRM which python)"
# ── Phase 0: Baseline infer ──
if [[ "${MODE:-}" != "mock" ]]; then
echo "=== Phase 0: Baseline infer (v2-360 Action Recognition 30 题) ==="
"${PYTHON}" main.py \
--config config/default.yaml \
--workspace-dir workspaces/default \
--store-dir store \
--mode infer \
--concurrency 24 \
--max-steps 40 \
--skill-mode auto \
--n-samples 0 \
--questions "generated-v2-360" \
--skills-version v1 \
--prompts-version v1 \
--run-id v2ar_baseline \
--task-types "Action Recognition"
fi
# ── Phase 1: Create seed ──
if [[ "${MODE:-}" != "mock" && ! -d "store/seeds/v2ar-baseline" ]]; then
echo "=== Phase 1: Create seed v2ar-baseline ==="
"${PYTHON}" -c "
from pathlib import Path
from app.harness.store import extract_run_db, init_seed
import tempfile
tmp = Path(tempfile.mkdtemp()) / 'baseline.db'
extract_run_db(
Path('workspaces/default/harness.db'),
tmp,
'infer_v2ar_baseline',
)
init_seed(
store_dir=Path('store'),
name='v2ar-baseline',
skills_dir=Path('store/skills/v1'),
prompts_dir=Path('store/prompts/v1'),
baseline_db=tmp,
baseline_run_id='infer_v2ar_baseline',
parent=None,
description='v2-360 Action Recognition 30 题 baseline (skills/v1)',
)
tmp.unlink()
print('Seed created: store/seeds/v2ar-baseline/')
"
elif [[ -d "store/seeds/v2ar-baseline" ]]; then
echo "=== Phase 1: Seed v2ar-baseline 已存在,跳过 ==="
fi
# ── Phase 2: Train ──
echo "=== Phase 2: Train (3 epochs, Action Recognition) ==="
"${PYTHON}" main.py \
--config config/train_action_recognition.yaml \
--fresh \
--seed v2ar-baseline
echo "=== 训练完成 ==="
echo "结果查看:"
echo " cat workspaces/train-action-recognition/analyses/final_test_eval.json"
echo " sqlite3 workspaces/train-action-recognition/harness.db 'SELECT * FROM dual_metric'"
```
- [ ] **Step 2: 设置可执行权限并验证语法**
```bash
chmod +x scripts/train_action_recognition.sh
bash -n scripts/train_action_recognition.sh
```
预期:无语法错误
- [ ] **Step 3: 提交**
```bash
git add scripts/train_action_recognition.sh
git commit -m "scripts: add train_action_recognition experiment script"
```
---
### Task 5: lint 检查 + 全量回归测试
- [ ] **Step 1: Ruff 格式化与检查**
```bash
conda activate Video-Tree-TRM & ruff format app/ core/ && ruff check app/ core/ --fix
```
预期:无错误
- [ ] **Step 2: 全量测试**
```bash
conda activate Video-Tree-TRM & pytest tests/unit/ tests/integration/ -v --tb=short
```
预期:全部 PASS
- [ ] **Step 3: 最终提交(如有 lint 修复)**
```bash
git add -A && git commit -m "chore: lint and format training experiment changes"
```
---
## 核心算法保真校验
本计划不涉及核心算法迁移。修改仅限于:
- `PerCategoryPoolStrategy.build()` 新增 3 行 test 池过滤(不改 train/val 切分逻辑)
- `RunConfig` 新增 1 个 bool 字段
- `load_config` 新增 2 行 list→tuple 转换
- `_slow_update_cycle` 新增 1 行 `if` 条件(不改 holdout 内部逻辑)
保真校验不适用。