revert: 移除全部机器学习代码,回退到无 ML 版本

删除 RL/HRL/训练前端及其文档、脚本、测试与依赖:
- src/mcm26b/{rl,hrl,training_ui}/、planner/{rl,hrl}_loop.py、process_control.py
- project/(PPO 策略、训练、环境、评估)
- scripts/{train_rl,train_hrl_*,benchmark_rl,benchmark_hrl,pretrain_*_expert 等}
- tests/{test_rl*,test_hrl*,test_training_ui*}、tests/js/training_ui_harness.cjs
- docs/{algo-rl,hrl-*,training-ui*} 与相关计划条目、论文 PDF 回退

CLI 恢复为 grid/seven/bayes + directional,移除 --strategy rl|hrl、
--q4-strategy、--rl-model、--hrl-* 等参数;pyproject 删除 rl 依赖组与
PyTorch CUDA index,uv.lock 同步重生成。

保留此后的非 ML 改动(referee 回放、Q1/Q3 脚本、代码清理等)。
验证:pytest 232 passed;Q3/Q4 --self-test 全清通过。
This commit is contained in:
Horldsence
2026-09-12 08:47:51 +08:00
parent d975309a81
commit 61fd22598a
84 changed files with 9 additions and 13159 deletions
-1
View File
@@ -194,7 +194,6 @@ AI 的适合用法是把这些模块逐一变成可测试程序、生成边界
| P4 全向闭环 | 49点覆盖记录、定位队列、清除、停止、日志统计 | 10/16源、边界源和错误响应案例;官方演练全清并正常退出 | 自建闭环已完成:`planner.OmnidirectionalLoop`(10/16/0 源、近边界源、预算耗尽、故障注入测试通过);官方演练全清待团队执行 |
| P5 定向扩展 | no_signal联合约束、覆盖发现、有限清除后备与预算 | 朝外边界源不漏;真源不误删;格数与退出预算检查 | 已实现:`planner.DirectionalExtension`(49点覆盖对定向源成立、保证接收盘内 no_signal 记为观测到定向、对跖+环探梯子、`fallback_clear_cells` 有限圆盘覆盖后备清除带格数/预算双硬顶);`tests/test_loop_q4.py` 全过;官方混合源演练待执行 |
| P6 加速消融 | 共享测点、自适应覆盖、目标排序、提前清除 | 配对案例全清不退化,时间/请求数改善可复算 | 可删减;闭环基线数据已可用于消融对照 |
| P6b RL 选点 | 问题3/4 测量点选择的 PPO+CUDA 训练环境与闭环接入 | 自建全清;与手写策略配对消融有可复算报告 | 已完成:`scripts/train_rl.py`/`benchmark_rl.py`,模型 `output/rl/q{3,4}_ppo.zip`,说明 `docs/algo-rl.md` |
| P7 正式与论文 | 固定版本、论文表1、日志原名、上传核查 | 问题3/4各3份正式记录与程序版本对应 | 团队后续安排;本任务不执行正式测试 |
按2026年9月10日起的剩余时间,建议10日补齐P1并衔接P3,11日完成P4自建边界案例与全向演练,12日完成P5及混合源演练;未达验收就删减P6并优先修正闭环。演练不限3次,正式机会为问题3和4各3次。团队在版本稳定后自行安排正式记录,留出失败恢复和论文整理时间。附件建议9月13日15:30前完成正式测试,17:30后不能启动新的演练或正式测试;已进入上传队列的日志仍会继续上传。此处引用所给附件安排,运行前核对模拟器界面公告;这份排期不改变当前禁止使用正式测试的限制。
-167
View File
@@ -1,167 +0,0 @@
# 问题3/4 强化学习选点(PPO + CUDA)
## 0. 摘要
**2026-09-11审核更新:**本文件下方的训练成绩与BC准确率是第一版历史记录。
复核发现训练/部署特征错位、BC激活函数不一致以及评测只检查completed等问题。
当前修复、Maskable PPO新模型和独立配对验证见
[强化学习适用性审核与改进](强化学习适用性审核与改进-20260911.md)。
新模型未替换原默认检查点;通过`--rl-model`显式加载。
本项目在保持 P0–P5 保守性与可解释证明不变的前提下,为问题3(全向源)与
问题4(定向源)增加了一个可选的**强化学习测量点选择策略**:
- 把“下一检测点去哪”建模为离散动作的局部决策问题;401维特征是压缩观测,
不能认定为完整马尔可夫状态,尤其第四问仍有未知朝向和被省略的历史约束;
- 状态由逐频道信念的几何特征 + 60 个候选检测点的几何特征组成(401 维);
- 动作为 60 选 1(保证接收盘环候选、对称交会候选、定向遮挡环探候选);
- 使用 Stable-Baselines3 的 PPO 训练,MLP 策略与价值网络在 **CUDA GPU**
(本机 RTX 4060 Laptop, CUDA 12.8 / torch 2.11.0+cu128)上更新;
- 训练环境基于自建评测机 `referee`(内存回环,与真实 HTTP 协议同边界);
- 运行闭环仅替换既有选点排序器,覆盖证明、清除证书、定向后备清除全部保留。
历史结果(第一版自建配对消融,5 个种子):Q3 用 12 万步随机初始化 PPO(与 `bayes`
基本持平,+1.18%);Q4 用**专家行为克隆初始化 + PPO 微调**,混合源平均
虚拟时间相对 `directional` 基线降低 **7.16%**(14250.1s → 13230.2s)。
Q3 的 BC 初始化在消融中反而退化,因此保留随机初始化模型;Q4 的 BC 初始化
是当时五场比较中均值最好的版本。官方演练验证见 §5:两次 Q4 演练(3 定向混合源、10 定向全定向
场景)与一次 Q3 演练均全清,且通过离线审计。
## 1. 环境搭建(CUDA)
```powershell
# 安装可选依赖组 rl(PyTorch CUDA 12.8 + stable-baselines3 + gymnasium)
uv sync --group rl --group dev
# 验证 CUDA
uv run python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"
```
`pyproject.toml` 中 `[tool.uv.sources] torch = { index = "pytorch-cu128" }`
与 `[[tool.uv.index]]` 指向 PyTorch 官方 CUDA 12.8 轮子;下载约 2.6 GB,
首次同步建议 `$env:UV_HTTP_TIMEOUT='300'`。默认 PyPI 的 Windows torch 轮子
为 `+cpu`,可以使用CPU训练;本轮修订模型即采用CPU,CUDA并非训练前提。
## 2. 第一版局部决策环境
训练环境:`src/mcm26b/rl/env.py::LocalizationEnv`。
| 项 | 定义 |
| --- | --- |
| 初始状态 | 自建模拟器随机布设一个源(Q3 全向;Q4 定向比例可调,训练默认 1.0),机器狗完成一次保证得到 `direction` 的首测 |
| 观测 | 401 维:21 个全局特征 + 20 维频道 one-hot + 60×6 维候选点特征(见 `features.py`) |
| 动作 | `Discrete(60)`:0=候选集 MEC 圆心,1..36=保证接收盘 3 环×12 方向,37..51=对称交会 15 点,52..59=定向遮挡环探 8 点 |
| 转移 | 按动作位置调用 `/measure`,用 `BeliefState` 保守更新候选集;Q4 保证接收盘内 `no_signal` 记为朝向遮挡并进入 `blocked` 序列;证书达标后自动 `/clear`;遮挡梯子用尽后模拟 20m 网格后备清除 |
| 奖励 | `-Δvirtual_time/100`(移动+检测+换频+清除真实耗时)、候选半径收缩加成、首次遮挡证据 +2、重复测点 -0.75、清除成功 +10、失败/截断负奖励 |
约束说明:候选点全部来自既有几何生成器(`planner.candidate_points`、
`planner.safe_disk_radius`、定向环探梯子),RL 不生成新的空间候选;它只做
排序。清除仍须通过几何证书;但“保证收到方向”只适用于全向源且实际点位于
距离保证盘内。第一版的52—59号环探点可能在盘外,第四问的定向遮挡也不受
距离盘保证。v2对第三问加入盘外动作屏蔽,第四问保留探索分支。
## 3. 训练与自建模拟器逻辑利用
```powershell
# 1) 用自建模拟器上的手写专家生成 (观测, 动作) 并行为克隆
uv run python scripts/pretrain_rl_expert.py --problem 3 --episodes 3000
uv run python scripts/pretrain_rl_expert.py --problem 4 --episodes 3000
# 2) 从当前v2 BC初始化继续普通PPO(算法类型需显式匹配)
uv run python scripts/train_rl.py --problem 4 --algorithm ppo --timesteps 40000 \
--init output/rl/q4_ppo_bc_init.zip
# Q3 保留随机初始化长训练版本:
uv run python scripts/train_rl.py --problem 3 --timesteps 120000
```
专家逻辑与自建模拟器同源:Q3 用 worst-posterior 选点器,Q4 用对跖/环探
遮挡梯子;`pretrain_rl_expert.py` 在 `LocalizationEnv` 上记录每个状态的
专家动作,训练与 PPO policy_net/action_net 同构的 MLP,再把权重拷入 PPO
作为初始化。产物位于 `output/rl/bc/` 与 `output/rl/q{3,4}_ppo_bc_init.zip`。
实测:Q3 专家数据 5141 条、BC 验证准确率 38.9%,微调后自建消融退化
(保留 12 万步随机初始化模型);Q4 专家数据 14181 条、BC 验证准确率
65.3%,微调后自建消融从 −3.84% 提升到 **−7.16%**。但审核确认BC与PPO的
激活函数不一致,这些数字不能证明成功迁移了专家策略,也不足以判断全向分支已最优。
输出:`output/rl/q3_ppo.zip`、`output/rl/q4_ppo.zip` 及同名 JSON 报告;
训练脚本先打印 `train_start`(含 device/gpu 字段),再执行 PPO 并在独立
种子场景上确定性评估成功率、平均步数与平均回报。Q4 4 万步微调约 236s;
Q3 12 万步约 625s(RTX 4060 Laptop,2 个 CPU 环境)。
## 4. 运行闭环接入
- Q3:`uv run mcm26b run --robot-id 2026001 --self-test --strategy rl`
`RLBayesLoop` 继承 `BayesLoop`(七点覆盖 + 网格粒子滤波 + 滚动 2-opt),
仅 `_select_point` 改由 PPO 输出候选编号。
- Q4:`uv run mcm26b run --robot-id 2026001 --self-test --problem 4 --q4-strategy rl`
`RLDirectionalLoop` 继承 `DirectionalExtension`(49 点覆盖 + 遮挡梯子 +
20m 有限圆盘后备清除),仅 `_next_probe` 改由 PPO 输出。
- 模型加载时校验观测 401 维 / 动作 60 维,失配直接拒绝运行。
- 默认运行路径不导入 torch/referee;只有显式选择 `rl` 才惰性导入,项目
运行时依赖仍保持 `requests` 一项。
## 5. 消融基准
```powershell
uv run python scripts/benchmark_rl.py --seeds 5
```
同种子同场景配对比较,结果写入 `output/rl/benchmark.json`。以下为第一版历史结果:
| 场景 | 基线 | 平均虚拟时间(基线 / RL) | 相对差 | 全清 |
| --- | --- | --- | --- | --- |
| Q3(10 源全向 ×5 种子,scratch PPO) | `bayes` | 3088.3s / 3124.8s | +1.18% | 5/5 |
| Q4(10 源、3 定向 ×5 种子,BC+PPO) | `directional` | 14250.1s / 13230.2s | −7.16% | 5/5 |
训练局评估:Q3 单源 40 局成功率 100%、平均 3.55 步;Q4 定向单源 40 局
成功率 90%、平均 4.45 步;其余单源训练局未完成,不能由此宣称已兜底成功。随机策略对照:
Q3 平均回报 13.28 vs PPO 17.71;Q4 平均回报 8.72 vs BC+PPO 12.62。
### 5.1 官方模拟器演练验证(仅演练,未触碰正式测试)
使用目录内官方 `jammers-simulator-full` 的**演练测试**(不限次数),通过
官方 HTTP 接口运行同一闭环;官方产物按原名归档并离线审计:
| 问题 | 案例 | 场景真值(演练结束后可见) | 结果 | 虚拟时间 | 请求数 | 审计 |
| --- | --- | --- | --- | --- | --- | --- |
| Q3 | `F38Y-9Q9J-B82V-CB9Z` | 13 全向 | 13/13 全清 | 3538.8s | 124 | ✅ |
| Q4 | `X43X-VMRF-8GRZ-3SYA` | 7 全向 + 3 定向 | 10/10 全清 | 14003.1s | 619 | ✅ |
| Q4(BC) | `BXSA-TFQ3-2DJ6-DHH7` | **10 定向** | 10/10 全清 | 14562.8s | 691 | ✅ |
前两次为随机初始化模型,第三次为 BC 初始化微调模型。两个 Q4 官方案例的
源构成不同(3 定向 vs 10 定向),**虚拟时间不可直接相减比较**;可确认的
是 RL 在官方混合源与全定向源演练中都能完成全部清除,且
`all_timestamps_match_microsecond_recomputation`、官方 `.jlog` 的
SHA-256 校验全部通过。归档目录:`output/rl/official-practice/`。
### 5.2 自建 referee 演练验证
`uv run mcm26b run --local-judge` 走自建 referee 的 `q3_practice` /
`q4_practice` 模块(与官方相同的演练生命周期,真值在结束后可见):
| 问题 | 案例 | 场景 | 结果 | 虚拟时间 | 归档 |
| --- | --- | --- | --- | --- | --- |
| Q3 | `FKUY-K8KJ-RXRL-FMBM` | 10 全向 | 10/10 全清 | 3317.6s | `output/rl/local-practice/q3/20260911-rl-01` |
| Q4 | `3J8T-L9GY-HRGL-LYVZ` | 7 全向 + 3 定向 | 10/10 全清 | 11942.8s | `output/rl/local-practice/q4/20260911-rl-01` |
| Q4(BC) | `VM2G-UVC6-4N7R-TVGW` | 7 全向 + 3 定向 | 10/10 全清 | 12520.8s | `output/rl/local-practice/q4/20260911-rl-bc-01` |
不同案例仍不可直接比较;成对可比的提升证据是 §5 的 5 种子同场景消融。
## 6. 结论与边界
以下是第一版结论;BC权重拷贝缺陷使“同构初始化”的解释不成立,性能判断
应以新审核报告的同场景验证及不确定性为准,而不能由BC分类准确率推出。
- RL 已接入问题3/4 完整闭环,并在自建 referee 演练与官方模拟器演练中
全清验证;这是方法验证,不是对正式成绩的承诺。
- 第一版Q4的BC初始化后均值变化为−3.84% → −7.16%,但初始化存在激活函数
缺陷,样本数也不足以支持“明确收益”的归因;当前判断见新审核报告。
- 训练环境使用自建场景假设(源均匀分布、逐点固定误差场、定向源朝向随机);
官方隐藏分布未必相同。正式测试仍应默认使用经过演练验证的手写策略;
`--strategy rl` / `--q4-strategy rl` 适合消融、方法对比与后续调参。
- 官方演练仅使用“演练测试”按钮与开放接口,从未点击“正式测试”;官方
`.jlog/.result.json` 保持原样,未解密。
- 保守性不变式:RL 只改变选点排序,不改变覆盖账本、候选集更新、清除证书
与后备清除的数学证明;所有策略均以 `/exit` 与完整日志收尾。
-117
View File
@@ -1,117 +0,0 @@
# HRL P0+P1 执行报告(本地自建评测机)
日期:2026-09-11(本机工作区)
范围:按用户确认,仅完成 P0 正确性回归 + P1 最小可训练/可评测闭环;
P2 的 BC、完整课程长训、Optuna、正式消融与正式测试表留待下一阶段。
## 1. 本轮代码交付
| 文件 | 内容 |
| --- | --- |
| `scripts/benchmark_hrl.py` | 同一 manifest 真值场景复用;`hrl`/`hrl_heuristic`/`bayes`/`directional` 配对评测;写 `results.json`、`aggregate.json/csv`、每局 `summary.json` 与 `session.jsonl` |
| `scripts/train_hrl_high.py` | 高层 MaskablePPO 训练;`--problem/--stage/--envs/--vec-env/--device/--config-profile/--reward-profile/--model/--resume/--init/--eval-manifest`;模型元数据固化 feature/action/reward 版本;墙钟硬上限 |
| `src/mcm26b/hrl/constants.py` | 新增 `ACTION_LAYOUT_VERSION=1`、`REWARD_VERSION=1` |
| `src/mcm26b/hrl/policy.py` | 高层模型严格校验元数据;低层模型保持独立基础校验 |
| `src/mcm26b/cli.py` | 新增 `--strategy hrl`、`--q4-strategy hrl`、`--hrl-model`、`--hrl-low-model`、`--hrl-device`、`--trace-low` |
| `src/mcm26b/hrl/hierarchical_env.py` | 新增 `HRLReward.time_only()`:P0.4 建议的纯时间目标,去掉逐源清除/发现/熵减塑形 |
| `project/` | 严格目录树薄兼容层,转发真实实现,不复制算法 |
| `tests/test_hrl_p1.py` | manifest 往返、真值隔离、纯时间奖励、project re-export 回归 |
## 2. 全量回归
```bash
.venv/bin/python -m pytest -q --tb=short
# 275 passed in 124.57s
```
## 3. P1 smoke 训练(本机 Apple MPS)
```bash
.venv/bin/python scripts/train_hrl_high.py \
--problem 3 --stage 1 --timesteps 2048 --seed 320001 \
--envs 1 --vec-env dummy --device mps \
--config-profile production --reward-profile time \
--model output/hrl/p1-smoke-time/q3.zip \
--eval-episodes 2 --rollout-steps 128 --batch-size 128 \
--n-epochs 3 --max-wall-s 800 --verbose 0
```
结果:
- `actual_timesteps=2048`,墙钟 `260.069 s`;
- 保存后确定性评估 2/2 成功,`accounted=20`,无异常;
- 模型元数据含 `feature_version=4`、`action_layout_version=1`、
`reward_version=1`、`reward_profile=time`、`model_sha256`。
模型重载已由以下两条路径验证;`--resume` 的 `--timesteps` 是增量步数
(SB3 `reset_num_timesteps=False` 语义),元数据写 `timesteps_semantics="additional"`:
```bash
.venv/bin/python -m mcm26b.cli run \
--robot-id 2026001 --self-test --strategy hrl \
--hrl-model output/hrl/p1-smoke-time/q3.zip --hrl-device mps \
--sources 3 --seed 1 --summary-steps 5
python scripts/benchmark_hrl.py --variants hrl \
--hrl-model output/hrl/p1-smoke-time/q3.zip --hrl-device mps \
--problem 3 --scenes 1 --sources 3 --output output/hrl/p1-bench-hrl
```
## 4. 多轮本地配对测试
### Q3:3 个独立场景 × 3 个变体
```bash
python scripts/benchmark_hrl.py --variants hrl,hrl_heuristic,bayes \
--hrl-model output/hrl/p1-smoke-time/q3.zip --hrl-device mps \
--problem 3 --scenes 3 --sources 3 --start-seed 91000 \
--output output/hrl/p1-multiround-q3
```
| variant | episodes | valid_rate | cleared_mean | virtual_time_mean_s | virtual_time_std_s | program_run_time_mean_s |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| bayes | 3 | 1.0 | 3.0 | 2518.371 | 74.183 | 0.748 |
| hrl_heuristic | 3 | 1.0 | 3.0 | 2518.371 | 74.183 | 0.883 |
| hrl | 3 | 1.0 | 3.0 | 3731.380 | 120.536 | 1.853 |
结论:P1 模型只在 stage 1 单源课程上训练 2048 步,可直接重载并在 3 源场景保持 3/3 有效全清;
但用时明显未优于 bayes/heuristic,符合本轮不做正式性能声明的边界。
### Q4:3 个独立混合源场景 × 2 个变体
```bash
python scripts/benchmark_hrl.py --variants hrl_heuristic,directional \
--problem 4 --scenes 3 --sources 3 --directional 1 --start-seed 92000 \
--output output/hrl/p1-multiround-q4 --force
```
| variant | episodes | valid_rate | cleared_mean | virtual_time_mean_s | virtual_time_std_s | program_run_time_mean_s |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| directional | 3 | 1.0 | 3.0 | 13442.997 | 263.902 | 1.063 |
| hrl_heuristic | 3 | 1.0 | 3.0 | 12292.198 | 344.827 | 6.284 |
说明:Q4 高层 PPO 尚未训练;本轮只验证 Q4 的 `hrl_heuristic` 与 `directional` 闭环、真值隔离与计时审计。
## 5. P0 状态
- `--resume` 重载:16 步起步模型续训 32 增量步后 `actual_timesteps=48`,
校验 `feature_version/action_layout_version/reward_version` 后保存;
- 槽位唯一性:`test_hrl.py` 覆盖 channel 17–20 与 1–4 同时 clear/locate 不丢;
- 清除事件一次且仅一次:fallback 成功写 `_step_clears` 已有回归;
- 候选快照:`test_opportunity_measure_precedes_candidate_snapshot` 已覆盖;
- 奖励对齐:新增 `HRLReward.time_only()`,训练脚本默认 `--reward-profile time`;
- 终止/安全网:现有 `HierarchicalEnv` 回合测试继续通过;
- 训练/部署配置:`--config-profile production` 使用生产配置,`coarse-pretrain`
显式映射到 fast 评分配置并写入模型元数据;
- 依赖守卫:全量测试 275 项在 dev+rl 环境通过。
## 6. 未完成 / P2 待办
- `scripts/pretrain_hrl_expert.py` 已建可 `--help` 的接口占位,专家轨迹 + BC 功能待实现;
- `scripts/ablate_hrl.py` 已建接口占位,无信息增益/无掩码/无课程/无粒子统计消融待实现;
- `scripts/tune_hrl.py` 已建接口占位,Optuna TPE 搜索待实现;
- Q3→Q4 迁移训练与配对实验;
- 完整 10–16 源课程训练、多个训练种子、冻结测试清单;
- 官方三次正式测试表:必须有真实官方案例码与导出日志后才能填写;
本地 `referee` 结果只标 `local_self_test`。
-267
View File
@@ -1,267 +0,0 @@
# HRL 续作执行说明:交给 Sol 的工程任务书
适用日期:2026-09-11。工作目录:本仓库根目录。先检查当前代码与本轮测试结果,再按 P0 → P1 → P2 → P3 顺序执行。
目标是交付可运行、可审计的整场调度学习系统;训练收益由完整场景验证决定,不把训练脚本存在或局部奖励上升视为完成。
本说明更新了原粘贴 Prompt 与 `.zcode/plans/plan-sess_cf390556-4ef3-4ce6-b91e-782aee1718f9.md` 中不符合现有协议或证据的部分。
## 1. 现在有什么,缺什么
| 组件 | 当前状态 | Sol 的操作 |
| --- | --- | --- |
| `protocol/`、`referee/`、`harness.py` | 已有客户端、独立评测机、内存回环与计时审计 | 复用,不另建模拟器客户端 |
| `belief/`、`particles.py`、`planner/` | 已有保守候选、网格幸存集、覆盖证明、证书清除与后备清除 | 保持证明前提;改调度时复用 |
| `rl/`、`train_rl.py`、现有 RL 检查点 | 已有单源选点 PPO/MaskablePPO 与完整对照 | 作为局部策略和对照,勿命名为端到端 PPO |
| `hrl/constants.py`、`scheduler.py`、`planner/hrl_loop.py` | 已有高层候选、特征、执行与闭环骨架;存在本轮需修复的 P0 项 | 先完成第 4 节门槛 |
| `hrl/hierarchical_env.py`、`curriculum.py` | 已有训练环境和课程;奖励、终止、训练配置仍需审核 | 在长训前修正,覆盖独立环境/部署一致性 |
| `hrl/low_level_env.py`、`executor.py`、`train_hrl_low.py` | 有八方向到达实验;训练入口尚有掩码等缺陷 | 作为可选离线实验,不能卡住主线 |
| `tests/test_hrl.py` | 有接口和启发式 episode 测试 | 增补行为验证,不能只检验公式与实现一致 |
| `train_hrl_high.py`、`pretrain_hrl_expert.py` | 尚未实现 | P1 编写最小可训练闭环,P2 再加入 BC |
| `benchmark_hrl.py`、`ablate_hrl.py`、`tune_hrl.py` | 尚未实现 | 先评测,再训练,再做消融,最后受限调参 |
| CLI 的 `hrl` 策略和 `--hrl-model` | 当前入口尚未接入 | P1 接入并检验 `--help` 与离线闭环 |
| `output/hrl/` 的高层模型、曲线、评测表 | 本次审阅未发现这些产物 | 不声称已有 HRL 模型或训练成绩 |
现有证据:修订单步 RL 的 Q3 独立验证均值比 `bayes` 低 0.69%,区间跨 0;Q4 比 `directional` 低 14.38%,比旧 PPO 低 2.26%,后一比较区间跨 0。
这些来自固定模型、本地 18 场/题的验证;新 Q4 的后备清除次数更多,边界压力性能有退化。详情见 `docs/强化学习适用性审核与改进-20260911.md`。
没有证据证明 HRL 已优于上述基线,没有证据支持“Q4 迁移节约 80% 训练成本”或“全部合法场景小于 4000 秒”。
## 2. 必须纠正的原 Prompt 假设
1. `/measure` 与 `/clear` 提交终点,移动按两次实际动作位置间的直线距离除以 5 计费;没有朝向、转弯或八方向移动协议。
2. 八方向策略仅在本地推演再强制补上终点,无法影响模拟器实际路径;不能把这段推演称为比赛收益来源。
3. `/clear` 不切换接收机频道,目标频道与当前接收机频道应分别记录;`near` 是测量结果,清除仍需独立请求。
4. 接收半径逐源未知,动作掩码只能使用观测与保守界,不能读真实半径、源位置、类型或朝向。
5. Q3 的 `no_signal` 只能按已证明的保守半径规则删格;Q4 的 `no_signal` 可能由方向遮挡造成,不能直接删空间候选。
6. Q4 联合位置/半径/朝向粒子是后续研究选项;普通二维粒子加“角度门控”并不会自动得到正确联合后验。
7. 固定地点误差固定;同地重测不是新独立样本。保留读数舍入裕量,不收紧真实的 ±1° 误差界来追求表面收敛。
8. 源数最多 16 不等于只有 16 个频道;所有按频道编号索引的动作必须覆盖 1–20。
9. 连续无信号次数不能作为全场结束条件;覆盖参数不能随意调到破坏已证明的发现覆盖。
10. “无信息增益”“无动作掩码”“无粒子滤波”必须精确定义;不能通过取消清除证书或真源保留约束制造消融。
11. 原 Prompt 的 6 个状态与实现里的状态枚举不一致;以 `ChannelStatus` 为准,额外 TRACKING/LOCATED 只能定义为派生特征。
12. 协议文档 §6 仍写持续搜索默认开启;当前 `config.py` 为 `persistent_search=False`,CLI 用 `--persistent` 显式开启。先同步文档,不恢复过时默认值。
## 3. 推荐架构与工程边界
采用“高层学习调度 + 低层局部选点 + 协议执行”的分层架构。高层学习先处理哪个频道、继续覆盖还是定位、何时使用后备清除。
低层默认调用成熟几何选点;可切换现有 `PointSelectionPolicy` 学习局部检测点,形成第二个学习层。直线移动与设备动作继续由协议客户端执行。
Q3 首版低层用 `bayes` 几何选点;Q4 先保留 `directional` 的遮挡处理,待接线一致性通过后对比旧/新局部 PPO。
```text
SimulatorClient.state + belief + particles + coverage ledger
↓ 同一份候选快照和动作掩码
高层 MaskablePPO:选择频道/覆盖站/清除任务
↓
局部选点:geometry | 已有 v2 PointSelectionPolicy
↓
证书/预算/覆盖约束 + 实际 measure/clear 宏动作
↓
accepted 请求与响应 → 时间、信念、清除与覆盖账本
```
八方向 PPO 保留为 `low_level_env.py` 的离线附录实验;生产默认无需加载。若论文要求字面双 PPO,用高层调度 PPO + 局部选点 PPO 对应真正可影响成绩的两个层次。
现有高层一个动作可以包含整站逐频道扫描或多次后备清除,因此是变长宏动作。记录每个宏动作的实际持续时间、请求数和中断原因。
先采用 `gamma=1` 与实际时间成本,避免把“每宏动作折扣一次”误当成“每秒折扣一次”;只有明确改变目标才引入按持续时间折扣。
第一版保留 120 槽位 MLP,不同时重写注意力网络或连续动作;需要多候选/频道时另升动作版本并独立比较。
建议新增的共享接口如下;这些是待实现契约,避免训练和 CLI 分别拼接策略。
```python
@dataclass(frozen=True)
class HRLPolicyConfig:
local_selector: str = "geometry" # geometry | rl
local_model_path: str | None = None
def make_hrl_loop(client, config, *, problem: int,
high_policy=None, policy_config=HRLPolicyConfig()) -> HRLLoop: ...
def validate_episode(summary: dict, events: list[dict],
truth_channels: set[int]) -> dict: ... # 仅评测模块
def run_episode(scene: dict, variant: dict, *, output_dir: Path) -> dict: ...
```
## 4. P0:修复正确性,未过门槛不得长训
以下表中的状态以本轮提交和测试报告为准;Sol 接手时先查看 diff,已完成的项只做回归,不重复实现。
| 项目 | 精确入口与问题 | 目标与验收 |
| --- | --- | --- |
| P0.1 槽位冲突 | `constants.py`;`scheduler.build_candidates/heuristic_action`:clear 频道 17–20 与 locate 频道 1–4 重叠 | 本轮优先修复;每类按 20 频道分配,构造同时就绪冲突场景验证任务均保留 |
| P0.2 清除事件遗漏 | `scheduler._hrl_execute_fallback` 成功未写 `_step_clears` | 本轮优先修复;证书/后备/宏动作内部清除均由相同成功事件口径计数,一次且仅一次 |
| P0.3 候选过期 | `HRLLoop._coverage_phase` 在构建候选后 `_measure_at_stop` 可改变信念 | 本轮优先修复;选动作前候选/特征/掩码属于同一个状态快照 |
| P0.4 奖励对齐 | `HierarchicalEnv.step/_reward_from/HRLReward` | 改默认奖励,成功/异常/安全网/截断均计入已经发生的真实时间 |
| P0.5 回合终止 | `HierarchicalEnv.step/reset/close` | 实际完成、失败终止与采样截断分开;收尾不丢 /exit,不把零观测用于有效截断 bootstrap |
| P0.6 训练部署配置 | `curriculum.training_config` 默认网格 29.3m,部署 14.65m | 首个可比较模型使用生产几何配置;粗网格只能显式快速预训练,后接完整保真微调与评测 |
| P0.7 无进展循环 | `scheduler` 的 locate/probe/fallback 掩码及预算检查 | 去重、已清除/无效证书、后备预算不足在动作前处理;合法动作不能无限零成本自循环 |
| P0.8 测试与依赖 | `tests/test_hrl.py` 导入守卫、`policy.py/scheduler.py` 顶层 numpy | 不安装 RL 组时基础 CLI 可导入;完整 RL 环境运行测试时不得意外全部 skip |
建议槽位布局为 clear 0–19、locate 20–39、station 40–88、fallback 89–108、保留 109–119;所有槽位互斥且小于 120。
槽位含义改变必须升 `FEATURE_VERSION`,即使仍是 1248 维/120 动作,也必须拒绝旧语义检查点。
模型元数据应要求 `level/problem/algorithm/feature_version/action_layout_version/reward_version`,并记录模型 SHA、配置、依赖版本、代码版本。
缺字段不能默认为兼容;Q3 权重初始化 Q4 必须走显式训练迁移参数,部署加载仍严格检查题号。
默认奖励先用:`r = -实际本步虚拟时间增量 / 100`;证明完成时加固定完成奖,未完成的真实终止加失败罚。
失败惩罚按冻结的 episode 最大成本界设置并记录,不随意用不足以覆盖数千秒的 `-100`。
相同全清场景里逐源常数奖可能只是常数偏移,但不同终止路径、事件漏记和失败场景会改变激励;故首版去掉新发现/熵减/每源清除附加奖。
如需塑形,另做势函数差 `gamma * Phi(next_state) - Phi(state)`,真正终止态统一势函数值,并检验总和望远镜消去。
如果优化题目平均时间,用整场时间作为训练目标时明确这是所选代理;不同源数的跨场景均值与 `T/N` 均值不等价,不能暗称完全一致。
人工步数上限若仅为采样截断,返回可用的终末观测并允许 bootstrap;若定义为任务失败期限,应作为真实终止计罚。按该语义设计测试。
P0 验收至少覆盖以下行为,不只验证 reward 按代码相加:
- 20 个频道的各类槽位唯一;频道 17–20 与 1–4 同时有 clear/locate 不丢任务。
- 同一观测轨迹分别构建独立训练环境与部署闭环,比较候选、mask、特征、下一实际请求及状态。
- 固定动作序列跨成功、失败、后备与安全网路径:reward 的时间分项总和等于 accepted 日志复算时间。
- Q4 保证盘内/盘外无信号不误删真源;正读数重置遮挡序列;无效证书不能执行清除。
- 10/16 源、频道含 20、边界源与预算中断均检查真实源集合、缺席集合、正常退出、异常与计时。
- 评测端真值仅进 `validate_episode`,策略构建、候选、mask、观测与模型选择均不得读取真值。
## 5. P1:最小完整闭环,先评测再短训
先实现 `scripts/benchmark_hrl.py` 与共享审计函数,使 `HRLLoop(high_policy=None)` 可以与原 `bayes/directional` 同场景对比。
这个启发式 HRL 骨架是重要对照:若差距来自任务候选、机会测量、Q4 保底或预算语义变化,先修或如实量化,不归因给神经网络。
每次运行将 scene、误差种子、配置、summary、完整 JSONL/accepted 事件、时间复算和模型 SHA 写入新目录;目录已存在则拒绝覆盖。
单场异常也写结果;聚合存在任一无效局时 `all_valid=false` 且脚本非零退出,不从平均值表中静默删除失败场景。
随后实现 `scripts/train_hrl_high.py`,第一版提供以下参数并通过 `--help` 验证:
```text
--problem {3,4} --stage {1,2,3,4} --timesteps INT --seed INT
--envs INT --vec-env {dummy,subproc} --device {cpu,cuda,mps,auto}
--config-profile {production,coarse-pretrain}
--model PATH --resume PATH --eval-manifest PATH --verbose INT
--local-selector {geometry,rl} --local-model PATH
```
阶段 1 即保持 20 频道与固定观测维度,只改变布景复杂度;早期课程可减少源数,但完整训练必须覆盖 10/13/16 源与实际 ±1° 固定地点误差。
Q4 完整课程同时覆盖少量/半数/大量定向源及近边界向外朝向;Q3→Q4 迁移与 Q4 随机初始化作为配对实验。
禁止让策略知道课程场景真实源数或隐藏的占用频道列表;初始位置保持官方 `(0,0)`,除非任务被明确标为局部交接课程。
先测 1/2/4 个环境的采样吞吐、内存和每次决策耗时,再选择 Dummy/Subproc;小 MLP 不假定 GPU 更快。
Subproc 环境类内部提供 `action_masks()`;Windows/macOS 使用入口守卫及可序列化工厂。
随机种子覆盖 Python/NumPy/Torch、PPO、每个环境、场景误差;每个 worker 使用独立 Monitor 文件。
保存实际 `model.num_timesteps` 与请求步数,恢复时说明增量还是累计;续训加载正确算法/特征版本且不覆盖原模型。
使用掩码评估接口,如 `MaskableEvalCallback`;同时在确定性推理中传入同源 mask。
CLI 加入 `--strategy hrl`、`--q4-strategy hrl`、`--hrl-model` 与局部策略选项,经 `make_hrl_loop` 统一构建;默认策略保持现状。
显式指定缺失模型应报可操作错误,不能把启发式降级结果记成 HRL 模型成绩;启发式骨架单独命名 `hrl_heuristic`。
P1 门槛:启发式骨架全场审计通过;512–2048 步训练冒烟可保存、重载、确定性推理;CLI 内存回环至少 Q3/Q4 各一场通过。
短训先用单个种子验证流程,不用这次模型的成功率作论文结论;完成后记录吞吐再定时间预算,不预设 1–2 小时足够。
## 6. P2:完整保真训练、独立评测与消融
实现 `pretrain_hrl_expert.py`:运行 `hrl_heuristic`,保存 `(episode_id, state, candidates, mask, action)`;按 episode 划分训练/验证。
BC 与 PPO 的网络层、激活、归一化、mask 处理保持一致;复制后在真实样本上逐位比较 logits,critic 单独初始化。
记录专家动作无法映射到候选的比率与原因;若大量回退到最近候选,先改专家映射,不用假专家标签继续训练。
至少比较 BC+PPO 与随机初始化 PPO;BC 准确率单独报告,不能代替独立闭环用时与有效全清率。
冻结数据集分工:
| 集合 | 用途与约束 |
| --- | --- |
| 已有 700–702 开发场景 | 查实现错误、探索配置与性能分解 |
| 已有 91000–91005 场景 | 历史回归;结果已经被审阅,不再宣称是本次全新盲测 |
| 新开发/选择清单 | 固定 JSON manifest,记录 seed、源数、定向组成、error_seed;用于训练选择和调参 |
| 新冻结最终测试清单 | 与训练、BC、调参及选择清单不重叠;模型冻结后运行,失败后修改则新开一轮测试 |
| 压力场景 | 圆周边界、1000m 半径、向外/切向定向、高频道、固定误差与预算边界;单独汇总 |
同场景每个变体用相同位置、频道、半径、朝向和地点误差函数。每个模型至少跑 30 个完整场景,再按预算扩大;“30”是工程起点,不是统计充分性证明。
最终用于稳定收益结论的主方法至少 5 个独立训练种子;场景种子与训练种子分开报告,不能用 5 个场景替代 5 次训练。
Q3、Q4 分开统计,并按源数/定向占比分层;所有基线与候选使用相同停止、预算和计时口径。
| 指标 | 固定定义 |
| --- | --- |
| 有效全清 | 成功清除频道集合等于评测真值;缺席集合与真值不交;completed、正常 exit、无异常、计时一致 |
| `virtual_time_s` | 实际退出前的完整虚拟时间;主竞赛口径显式 `persistent_search=False` |
| `avg_locate_clear_time_s` | 每场 `T / 实际清除数`;有效全清主表再做逐场平均,另报源数分组 |
| `accounted_virtual_time_s` | 首次完成证明的时间;与最终时间并列,不悄悄用它替换正式计分 |
| 运行时间 | 每场实际墙钟、模型推理耗时分位数;本地内存回环时间不冒充官方程序运行时间 |
| 成本分解 | 移动秒、测量秒、换频秒、成功/失败 clear 秒、后备尝试、重复测量、总请求数 |
| 尾部与失败 | 中位数/P90/P95/最大值、有效全清率、失败明细;小样本尾分位注明估计局限 |
平均 `T/N` 不等于 `平均 T / 平均 N`;若另用总体总时间/总源数,明确标为按源数加权口径。
配对效应以同场景时间差为基础,报告均值相对变化、胜出场数、95% 配对 bootstrap 区间;相同场景家族共享种子时按家族重采样。
多训练种子结果报告种子间分散程度;必要时对训练种子和场景做分层重采样,不能用单一场景区间代表训练稳定性。
最小比较矩阵:`bayes/directional`、旧局部 PPO、修订局部 MaskablePPO、`hrl_heuristic`、高层 PPO+几何、后续高层 PPO+局部 PPO。
此矩阵先回答“改调度是否有用”和“增加局部学习是否有用”;既有单步 RL 不能命名为无几何的端到端基线。
若仍要端到端 PPO,必须单独定义输入、实际动作、安全执行边界并训练,标记预算;它不是 P1 完成条件。
每项消融使用相同训练步数/场景预算/种子和评测集,单次只改变一个因素:
- 调度:学习高层 vs `hrl_heuristic`,保持候选生成和局部选点一致。
- 局部策略:同一高层与执行接口下,geometry vs 现有局部 PPO;报告是冻结替换还是联合重训。
- 塑形:纯时间目标 vs 经过恒等式核验的势函数塑形;无信息增益奖励就是系数为 0。
- 课程:相同总交互步数的课程训练 vs 从完整场景开始训练;Q4 迁移另列,不预设节约百分比。
- 掩码:只取消重复/低效候选等策略性屏蔽;协议合法性、证书和预算硬守卫始终保留,标明实际取消范围。
- 粒子特征:保留几何守恒内核,先将粒子统计输入和评分消融;若真的移除粒子模块,另实现保守几何替代并重新验证。
- 八方向:只报告离线到达率/路径效率/推演耗时,以及实际协议终点相同时成绩相同的事实。
P2 推广门槛:所有最终与压力场景有效,Q4 相对最强已冻结基线的时间差区间上界小于 0;尾部容忍度先写入 manifest。
建议初始尾部门槛为 P95 不退化超过 5%,这是可调整的工程标准,不是题设要求;任何调整都在重跑最终测试前完成。
Q3 若未显示稳定收益,保留 `bayes` 默认并交付负结果。仅比手写基线快、却未优于现有 PPO,不能宣布替换默认模型。
## 7. P3:受限调参、论文与官方验证
只有 P2 主方法具备竞争力才实现并运行 `tune_hrl.py`。先搜索学习率、熵系数、网络宽度、GAE、合法候选排序参数。
不搜索真实误差上限、光学清除半径、无信号停止次数或破坏覆盖证明的网格间距;确定性网格没有粒子重采样阈值可调。
Optuna 只用开发/选择清单,失败配置先按有效性淘汰;预算与失败/剪枝逻辑写入结果。最终模型重新用固定配置多种子训练。
文档、流程图与论文表明确:学习负责哪些决策、证明由哪些模块提供、本地生成分布未知是否对应官方分布。
训练曲线、实际步数、配置、全部对照与失败明细均归档;不要只挑最好种子或去掉不利的边界场景。
官方三次测试需要对应演练已经启动、真实案例码与官方导出日志;本地 referee 的三场不能填写成“三次正式测试”。
不覆盖旧官方归档;官方结果与本地结果分表,不对不同官方案例直接计算改进百分比。
## 8. 命令:已经存在与待实现的严格区分
以下入口当前已经存在;依赖需先安装,命令失败要修复或如实报告,不能跳过错误声称通过:
```bash
uv sync --group dev --group rl
uv run python -m pytest -q --tb=short
uv run python -m pytest -q tests/test_hrl.py --tb=short
uv run mcm26b run --help
uv run mcm26b run --robot-id 2026001 --self-test --strategy bayes
uv run mcm26b run --robot-id 2026001 --self-test --problem 4 --q4-strategy directional
uv run python scripts/train_rl.py --help
uv run python scripts/train_hrl_low.py --help
```
`train_hrl_low.py` 目前仅 `--help` 可用于检查入口,不把完整训练列为已可用:
其 `MaskablePPO.learn()` 需要环境掩码,当前 `LowLevelEnv` 没有 `action_masks()`;改普通 PPO 或提供全 True 掩码后再试。
还需修 BC 按 episode 划分、统一随机种子、每 worker 独立 Monitor 文件、评估 seed 生效、实际路径长度与成功率分开统计。
低层到达失败不能靠 executor 最后强制补终点伪装成策略到达;环境裁剪后的实际移动长度也不能简单记为 `steps*100m`。
下面是 P1/P2 实现后必须支持并实测的命令契约,当前尚不能直接运行:
```bash
# P1:先验证不加载模型的共享调度闭环
uv run python scripts/benchmark_hrl.py --variants hrl_heuristic,bayes,directional --manifest output/hrl/manifests/dev.json --output output/hrl/p1-heuristic
# P1:完整保真冒烟;模型目录必须是新目录
uv run python scripts/train_hrl_high.py --problem 3 --stage 1 --timesteps 2048 --seed 320001 --envs 1 --vec-env dummy --device cpu --config-profile production --model output/hrl/p1-smoke/q3.zip --verbose 0
# P1:CLI 明确加载高层模型
uv run mcm26b run --robot-id 2026001 --self-test --strategy hrl --hrl-model output/hrl/p1-smoke/q3.zip
# P2:正式阶段训练;步数先依据采样吞吐与学习曲线再调整
uv run python scripts/train_hrl_high.py --problem 4 --stage 4 --timesteps 60000 --seed 420001 --envs 2 --vec-env dummy --device cpu --config-profile production --model output/hrl/p2/q4-seed420001.zip --eval-manifest output/hrl/manifests/select.json --verbose 0
# P2:冻结模型完整对照;variants 的模型位置来自同目录的配置清单
uv run python scripts/benchmark_hrl.py --variants hrl,hrl_heuristic,legacy,revised,baseline --manifest output/hrl/manifests/final.json --output output/hrl/p2-final
```
命令使用的 manifests 也要由脚本生成、校验并归档;尚不存在的模型不能当作初始化输入。
添加 `--full` 一键课程入口时,它应展开并记录每阶段命令/配置/检查点链,失败即停,可继续,不是忽略错误的 shell 包装。
参考现有依赖配置:基础运行依赖仅 requests;RL 为可选组。固定环境版本并记录硬件,勿假定当前执行机就是旧报告中的 RTX 4060。
## 9. 可以直接粘贴给 Sol 的续作 Prompt
```text
继续当前仓库的 HRL 工作。先读 docs/hrl-sol-execution.md、最新测试结果与 git diff,保留现有修改。
已有客户端/referee/几何/PPO 都要复用;不要重写骨架,不把缺失脚本或 CLI 说成已完成。
先完成 P0 尚未关闭的奖励、终止、完整保真配置、掩码和依赖问题;本轮已修的槽位/清除事件/候选过期只回归。
采用高层 MaskablePPO 调度 + 几何/已有局部 PPO 选点 + 协议直线执行;八方向只作为可选离线实验。
按 P1 先实现 benchmark_hrl 与真值隔离审计,再实现 train_hrl_high、模型元数据和 CLI,跑可重载的短训冒烟。
P0/P1 没通过就修实现,不用加训练步数掩盖问题。通过后再开展 BC、完整保真课程、多训练种子、冻结场景配对评测。
保持 Q3 bayes/Q4 现有默认策略;完整实验胜出前不覆盖默认模型。主指标包含有效全清、T、逐场 T/N、请求分解和尾部。
每阶段交付具体文件、实际执行命令、测试结果、模型/日志路径与未完成项;不预设 Q4 80% 节约或 4000 秒保证。
官方测试只有真实演练开始并获得案例码/导出日志才记正式结果,本地实验单独标注。
```
接口参考:[MaskablePPO 官方文档](https://sb3-contrib.readthedocs.io/en/master/modules/ppo_mask.html),[Gymnasium 终止与时间限制](https://gymnasium.farama.org/tutorials/gymnasium_basics/handling_time_limits/)。
Binary file not shown.
-261
View File
@@ -1,261 +0,0 @@
# 训练前端 Windows 兼容性与「暂停」审计(结论 + 修复方案)
> 审计对象:`src/mcm26b/training_ui/`、`src/mcm26b/process_control.py`、
> `scripts/train_hrl_high.py`。
> 触发问题:「无法暂停」「频繁出现 os 无 killpg 等」。
> 结论一句话:**这两个现象同源——训练前端的停止/暂停路径是纯 POSIX 设计;
> Windows 上 `os.killpg` 根本不存在,异常又没被兜住,所以点了停止等于没点。**
---
## 0. 结论摘要
| # | 现象 | 根因 | 严重度 |
| --- | --- | --- | --- |
| 1 | 点「停止训练」返回 500 / 无效果,训练照跑,于是「无法暂停」 | `TrainingJob.stop()` 直接调用 `os.killpg(os.getpgid(...))`;这两个符号**只存在于 POSIX**,Windows 上抛 `AttributeError`,而旧 `except` 只接 `ProcessLookupError/PermissionError` | 阻断 |
| 2 | 控制台/日志反复出现 `module 'os' has no attribute 'killpg'` | 同上:错误最终落到 `_UIHandler.do_POST` 的兜底 `except Exception`,返回 500 并带出 `AttributeError` 文本;每点一次复现一次 | 阻断 |
| 3 | 即使把 killpg 换成 `terminate()`,停止后模型/元数据仍然丢失(Windows 上) | Windows 的 `os.kill(pid, SIGTERM)` 语义是 `TerminateProcess` **硬杀**,训练脚本里"收到信号后在 step 边界保存"的处理器不会执行 | 高 |
| 4 | 界面根本没有「暂停」能力(POSIX 也没有) | 原设计只有 start/stop;且 `SIGSTOP/SIGCONT` 在 Windows 上不存在,**原生暂停在 Windows 上无法实现**,必须改成协作式 | 高 |
| 5 | 子进程进程组隔离在 Windows 上失效 | `Popen(start_new_session=True)` 在 Windows 被**静默忽略**(CPython 的 Windows `_execute_child` 形参名就是 `unused_start_new_session`) | 中 |
| 6 | subproc worker 在 Windows 下可能抢在主进程收尾前退出 | worker 只忽略 `SIGTERM/SIGINT`,没处理 Windows 的 `SIGBREAK`(`CTRL_BREAK_EVENT` 的落点) | 中 |
**修复原则**:把"暂停/继续/停止"从**信号语义**改成**控制文件语义**。
控制文件在 POSIX 与 Windows 上都只是普通文件 IO,行为完全一致;信号与强杀退居
兜底。这样 Windows 上暂停、继续、优雅停止、落盘保证全部成立。
---
## 1. 失败链复现(旧代码)
```
用户点「停止训练」
└─ POST /api/train/stop
└─ TrainingUI.stop() → TrainingJob.stop()
└─ os.killpg(os.getpgid(process.pid), signal.SIGTERM)
├─ POSIX :成功,进程组收到 SIGTERM,脚本优雅收尾
└─ Windows:AttributeError: module 'os' has no attribute 'killpg'
↑ 只 except (ProcessLookupError, PermissionError),接不住
└─ _UIHandler.do_POST 兜底 except Exception → HTTP 500
└─ 训练子进程完全没收到任何指令,继续跑 → 「无法暂停 / 停止没反应」
```
关键代码(修复前,`src/mcm26b/training_ui/server.py`):
```python
def stop(self) -> bool:
with self._lock:
process = self.process
if process is None or process.poll() is not None:
return False
try:
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
except (ProcessLookupError, PermissionError):
process.terminate()
self.status = "stopping"
return True
```
---
## 2. 根因逐条(含证据)
### 2.1 `os.killpg` / `os.getpgid` 是 POSIX-only
CPython 只在 `posix` 构建里注册这两个函数;Windows 的 `os` 模块没有它们。
访问会得到 `AttributeError`(不是 `OSError` 子类),所以旧 `except` 接不住。
### 2.2 `start_new_session=True` 在 Windows 被静默忽略
`subprocess.Popen` 接受该参数,但 Windows 的 `_execute_child` 形参名是
`unused_start_new_session`,也就是**根本不读**。要隔离进程组,Windows 必须传
`creationflags=CREATE_NEW_PROCESS_GROUP`。
### 2.3 Windows 的 `SIGTERM` 是硬杀
`os.kill(pid, sig)` 在 Windows 上只有 `CTRL_C_EVENT` / `CTRL_BREAK_EVENT` 是
"软"信号;**任何其它值**(含 `SIGTERM`)都会走 `TerminateProcess`。因此
`scripts/train_hrl_high.py` 里的:
```python
for _signum in (signal.SIGTERM, signal.SIGINT): # 旧代码
signal.signal(_signum, _on_stop_signal)
```
在 Windows 上形同虚设——处理器注册了,但没人会用它。这也解释了为什么"停止后
模型没保存"。
### 2.4 Windows 没有 `SIGSTOP`,暂停无法用信号实现
暂停(挂起/继续)在 POSIX 上可以用 `SIGSTOP/SIGCONT`,但 Windows 既无这两个
信号,也无等价的"无侵入挂起"用户态 API(真正的挂起要 `SuspendThread`/调试 API)。
所以**任何跨平台暂停都必须是协作式的**:由训练进程自己在 step 边界检查标志并
让出执行。本项目原先没有这条通道,界面也没有暂停按钮。
### 2.5 `SIGBREAK` 未处理
Windows 的 `CTRL_BREAK_EVENT`(发给进程组的软信号)在 Python 里落到
`signal.SIGBREAK`。旧的 handler 安装与 worker 屏蔽列表都只含
`SIGTERM/SIGINT`,Windows 下等于裸奔。
### 2.6 异常类型没兜住
`except (ProcessLookupError, PermissionError)` 漏掉 `AttributeError`、以及
更一般的 `OSError`;`_hard_kill` 里 `signal.SIGKILL` 在 Windows 上也不存在
(`getattr(signal, "SIGKILL", None) is None`),同样会 `AttributeError`。
---
## 3. 修复方案(已实施)
### 3.1 新增 `src/mcm26b/process_control.py`(零第三方依赖)
| 能力 | 实现 | 作用 |
| --- | --- | --- |
| 子进程隔离 | `spawn_kwargs()`:POSIX `start_new_session=True`;Windows `creationflags=CREATE_NEW_PROCESS_GROUP` | 前端 Ctrl+C 不误伤训练;Windows 也能组杀进程树 |
| 控制协议读写 | `write_control()` / `read_control()`:JSON + 同目录临时文件 + `os.replace` 原子替换;损坏/缺失一律视为"无指令" | 两端统一的暂停/停止通道 |
| 强杀兜底 | `terminate_process_tree(process, grace_s)`:先等宽限期;超时后 POSIX 走 `killpg(SIGKILL)`、Windows 走 `taskkill /F /T /PID`;所有 POSIX 符号用 `getattr` 探测 | 彻底移除 `os.killpg` 无条件调用;返回"是否动用了强杀" |
`IS_WINDOWS` 是运行期常量而非导入期分支,便于测试直接 monkeypatch 模拟 Windows。
### 3.2 控制文件协议
路径由前端生成并随命令下发:`--control-file <abs>`(默认落在
`output/hrl/ui/control/<job_id>.control.json`,`output/` 已 gitignore)。
```json
{"paused": false, "stop": false, "seq": 1712345678901234567, "ts": 1712345678.9}
```
- 前端**只写**:`pause()` → `paused=true`;`resume()` → `paused=false`;
`stop()` → `stop=true`(同时清零 paused,保证阻塞中的暂停循环能被唤醒)。
- 训练脚本**只读**:`_ControlPoll` 每 32 个 step 抽样读一次盘(避免每 step 一次
文件 IO);暂停时在 `_on_step` 内阻塞轮询(`poll_s=0.2`),进程存活、显存仍占用,
但不消耗算力。
- 墙钟上限 `--max-wall-s` **扣除累计暂停时长**,避免"挂起一小时回来直接判超时"。
### 3.3 训练脚本 `scripts/train_hrl_high.py`
- 新增 `--control-file`;`_ControlPoll`(纯标准库、可单测)+ 薄薄的
`_ControlChannel(BaseCallback)` 接到 SB3 回调链**最前面**。
- 收尾信号集合扩为 `_stop_signals()`:POSIX `SIGTERM/SIGINT`;Windows 追加
`SIGBREAK`。worker 屏蔽同一集合,保证主进程 `vec_env.close()` 之前 worker 不先退。
- 落盘保证不变:停止后仍写最终模型 + 元数据,`stopped_by_signal=true`,
`stop_signal` 记 `control_file` 或信号名;异常仍落 `*_emergency.zip`。
### 3.4 训练前端 `server.py` / `static/`
- 新增 `POST /api/train/pause`、`POST /api/train/resume`;`stop` 语义改为
"写控制文件 → 宽限期内自行退出;超时强杀",强杀在**独立看门狗线程**里做,
HTTP 请求不会被 15s 等待卡住。
- 状态机新增 `paused`;`_sync_control_events()` 用训练脚本回显的
`paused/resumed/stop_requested` 事件校正状态。
- 快照新增 `forced_kill`,前端横幅区分"优雅停止"与"超时强杀"。
- 前端新增「暂停训练 / 继续训练」按钮、暂停横幅;停止确认文案更新。
### 3.5 停止/暂停时序
```
pause: UI 写 {paused:true} ──► 脚本 _on_step 读到 ──► 回显 paused,阻塞轮询
resume: UI 写 {paused:false} ─► 暂停循环退出 ────────► 回显 resumed,继续训练
stop: UI 写 {stop:true} ────► 脚本 step 边界收尾落盘 ──► 退出码 0
│
└─(宽限 15s 未退)─► terminate_process_tree 强杀;forced_kill=true
```
### 3.6 急停(强杀)也必须可恢复
宽限期一到就强杀,训练脚本**没有任何 Python 收尾机会**——不写最终模型、不写
元数据、不写 `training_failure.json`。如果磁盘上只有 `--checkpoint-every` 的稀疏
历史 checkpoint,急停就可能丢掉上万步,或者在"保存途中被杀"时留下一个打不开的
半截 zip。为此加了四条保证:
| 保证 | 实现 | 效果 |
| --- | --- | --- |
| 起步基线 | `recovery_every > 0` 时,`learn()` 之前先写一份 `<stem>_latest.zip` | 训练刚起就急停,也有可加载、可 `--resume` 的产物 |
| 滚动恢复点 | 每 `--recovery-every`(默认 2000)环境步覆盖写同一份 `<stem>_latest.zip` | 急停最多丢 `recovery_every` 步,而不是 `checkpoint-every` 步 |
| 原子落盘 | 模型 zip / 旁挂 json / `resume.json` 一律「同目录 `.tmp-` + `os.replace`」 | 保存途中被强杀,目标路径仍是上一份**完整**快照;不会出现半截文件 |
| 权威恢复指针 | 每次落盘刷新 `<run_dir>/resume.json`(model / timesteps / 增量语义 / 生效 PPO 超参);前端 `/api/train/state` 暴露为 `recovery` | 无论怎么停,都有一份明确的"从哪继续";界面一键「用最近检查点续训」 |
前端在强杀后还会补写 `<run_dir>/forced_stop.json`(`job_id`、`pid`、`reason`、
`recovery.model`),因为此时训练脚本已经无法记录任何事情。
---
## 4. 平台兼容性矩阵(修复后)
| 能力 | POSIX | Windows |
| --- | --- | --- |
| 启动子进程并隔离 | `start_new_session=True`(setsid) | `CREATE_NEW_PROCESS_GROUP` |
| 暂停 / 继续 | 控制文件(协作式) | 控制文件(协作式) |
| 优雅停止 + 落盘 | 控制文件;`SIGTERM` 亦可 | 控制文件;`CTRL_BREAK_EVENT`→`SIGBREAK` 亦可 |
| 超时强杀进程树 | `killpg(SIGKILL)` | `taskkill /F /T /PID` |
| `os.killpg` 依赖 | 有 | **无(不再调用)** |
| 暂停期间是否算墙钟 | 否(扣除) | 否(扣除) |
---
## 5. 验证
- 新增/更新回归测试:
- `tests/test_training_ui.py`:控制文件协议往返与损坏容错、`spawn_kwargs`
平台分支、Windows 强杀走 `taskkill`、**无 `killpg` 环境下强杀不抛异常**
(直接复现旧 bug)、暂停/继续/停止全链路、控制文件被忽略时强杀并写
`forced_stop.json` 恢复指引、`discover_recovery` 的
`resume.json`/扫描/悬空指针三分支、HTTP `pause/resume` 端到端。
- `tests/test_hrl_checkpoint.py`:控制文件状态机单测;原子保存失败不破坏上一份
快照;JSON 原子替换;历史清理不误删滚动恢复点;起步基线可加载;
**强杀急停后 `resume.json` + 滚动恢复点仍可 `--resume`**;控制文件停止后
模型/元数据落盘(`stop_signal=control_file`)与暂停→继续;POSIX 信号路径
单列并加平台跳过。以上集成用例都用**真实训练脚本**跑 subproc。
- 手工验证:`--recovery-every 32` 短程实跑后强杀,`resume.json` 指向
`q3_ckpt_latest.zip`;以它为 `--resume` 目标再跑 32 步,元数据
`actual_timesteps` 从 64 变 96、`timesteps_semantics=additional`。
- 全量测试:`pytest tests referee/tests` 全绿。
- 全仓库 `pylint src scripts project tests referee main.py` 达到 10.00/10
(口径见 §7)。
---
## 6. 残余风险与后续建议
1. **暂停粒度** = `poll_every` 个 env step(默认 32,约 0.1–3s 量级),不是即时;
若要更快可调小 `_ControlChannel(poll_every=...)`,代价是更频繁的文件 stat。
2. **宽限期固定 15s**(`server.STOP_GRACE_S`)。大模型保存可能更久;但因为落盘是
原子的,即使宽限期到点被强杀,也只是"新的最终模型没写成",**上一份滚动恢复点
与历史 checkpoint 完好**,不会出现半截文件。需要更长可调大该常量。
3. **急停的损失上界 = `--recovery-every`**(默认 2000 环境步)。要更小就调小它;
调到 0 表示放弃滚动恢复点(此时只剩 `--checkpoint-every` 的稀疏历史
checkpoint 与起步基线)。
4. **Windows 强杀依赖 `taskkill`**(系统自带)。若被安全策略禁用,会退回
`Popen.kill()`(只杀主进程,spawn 的 worker 可能残留)。
5. 更严格的 Windows 方案是 **Job Object**(子进程随 job 句柄一起销毁),但需要
`ctypes`/`pywin32`,本仓库基础依赖刻意保持轻量,未引入。
6. **前端进程重启后**,内存中的任务对象没了,「用最近检查点续训」按钮不会自动
出现;但恢复信息都落在磁盘上(`<run_dir>/resume.json`、`forced_stop.json`、
`checkpoints/<stem>_latest.zip`),在表单里填同一个模型输出路径并勾选
`--resume` 即可继续。
7. 界面 `--device mps` 选项在 Windows 上无意义;`auto` 会回退 cuda/cpu,属既有
行为,未改。
8. `_ControlPoll` 的暂停循环会占用一个 Python 线程的调度(每 0.2s 一次读盘),
长暂停期间 CPU 占用可忽略。
---
## 7. pylint 口径说明
配置写在 `pyproject.toml` 的 `[tool.pylint.*]`,目标是
`pylint src scripts project tests referee main.py` = 10.00/10。
**改代码修掉的真实问题**(不是 disable):未使用的 import/变量/参数、重复字典键
(`reward_version`)、`value != value` 判 NaN、`__dataclass_fields__` 私有访问、
`raise` 非异常类型、`pytest.importorskip` 之外的赋值不确定、过长行、无谓括号、
`len(x)` 判空、循环闭包变量、`subprocess.run` 未显式 `check`、`os.path` 循环导入等。
**有意 disable 的类别**(每条在 pyproject 里写了理由):项目刻意的懒加载
(`import-outside-toplevel`)、防御式宽异常(`broad-exception-caught`)、测试访问
内部状态(`protected-access`)、pytest fixture 同名(`redefined-outer-name`)、
无头环境不实现 `gym.Env.render`(`abstract-method`)、以及静态分析的已知误报
(`possibly-used-before-assignment`)。
**注意**:`good-names` 不覆盖 `redefined-builtin`,stdlib 回调签名里的
`log_message(self, format, ...)` 只能在对应 `def` 上行内 disable。
-160
View File
@@ -1,160 +0,0 @@
# 模型训练前端(training-ui)
本地回环 Web UI,用于**实时观察一次 HRL 高层 PPO 训练**:子进程状态、步进进度、
参数收敛曲线、逐回合记录与完整 stdout 日志。设计与评测前端(`referee/` WebUI)
同构——同一套暗色面板语言、同一个「只监听 127.0.0.1 + 静态白名单 + JSON 轮询
API」模式——差别在于本页托管的是一个**训练子进程**而不是评测会话。
## 启动
```bash
uv run python -m mcm26b.training_ui --ui-port 8036 # 打开 http://127.0.0.1:8036
uv run python scripts/train_hrl_ui.py --ui-port 8036 # 等价薄入口(支撑材料命令)
uv run python -m mcm26b.training_ui --print-schema # 只看配置契约,不起服务
uv run python -m mcm26b.training_ui --print-preview # 只看离线示例快照
```
`--no-browser` 阻止自动打开浏览器;Ctrl+C 退出时会先停止训练子进程。
无训练进程时,界面顶栏「离线预览」可渲染三组合成快照(上升 / 平台期 / KL 过猛),
用于检查曲线、指标卡、时间线与日志着色——预览不启动任何进程、不写任何文件。
## 界面结构
| 区块 | 内容 |
| --- | --- |
| 顶栏 | 任务摘要、进程/进度/墙钟/设备/连接五个状态 chip、刷新·预览·暂停/继续·停止 |
| 执行进度与进程 | 步进进度条、剩余时间与预计结束、PID/吞吐/退出码、PPO 阶段时间线、完整命令行 |
| 训练配置 | 由 `/api/train/schema` 生成的五组表单 + 四个预设,运行中锁定 |
| 参数收敛状态 | 判读 chip + 判据说明,每个指标一张卡片(最新值、增量、迷你趋势线、异常高亮) |
| 曲线 | 主指标(平均回合奖励)+ 可切换诊断量,悬停十字线读数、KL=0.05 等参考线 |
| 实时运行日志 | 关键字过滤、错误/进度 JSON 开关、自动跟随、最近 4000 行 |
| 逐回合记录 | Monitor CSV 的每回合奖励/长度/时刻 |
## 数据来源与契约
三个端点都在 `src/mcm26b/training_ui/server.py`:
| 端点 | 说明 |
| --- | --- |
| `GET /api/train/schema` | 字段/分组/预设/指标字典,**与 `TrainConfig` 同源**(`schema.py`) |
| `GET /api/train/state` | 快照;支持 `log_tail`、`metric_limit`、`after_metric` 游标与 `preview=1` |
| `POST /api/train/start` | 允许只提交部分字段(其余取默认值),校验失败返回 409 |
| `POST /api/train/pause` | 写控制文件请求在 step 边界挂起(跨平台) |
| `POST /api/train/resume` | 解除挂起,从暂停处继续 |
| `POST /api/train/stop` | 请求优雅停止;宽限期(`STOP_GRACE_S`,默认 15s)后强杀进程树 |
暂停/停止走**控制文件**而不是信号,原因与平台差异见
[`training-ui-windows-audit.md`](training-ui-windows-audit.md):Windows 没有
`SIGSTOP`,且 `os.kill(pid, SIGTERM)` 会退化成 `TerminateProcess` 硬杀,训练脚本
"保存模型再退出"的收尾分支不会执行。控制文件在 POSIX 与 Windows 上都只是普通
文件 IO,语义完全一致;信号(POSIX `SIGTERM/SIGINT`、Windows `SIGBREAK`)只作兜底。
进度数据的两个来源,服务端都已解析:
1. **progress JSON**(主来源):`train_hrl_high.py --progress-json` 每若干环境步打印
一行 `{"event": "progress", "timesteps": ..., "ep_rew_mean": ...}`,字段最全;
热身阶段间隔会自动缩小,短程 smoke 也能看到完整曲线。
2. **SB3 verbose 表格**(补充来源):解析 `| key | value |` 表格,用于补
`clip_fraction`/`learning_rate` 等诊断量。表格按段分组缓存、等本表的
`total_timesteps` 到位后统一打横轴刻度;与 JSON 同一步数的表格版本会被跳过,
避免重复点。
`timesteps` 是整个前端的横轴:服务端不伪造刻度(缺刻度的表宁可丢弃,截断的表
按末尾已知刻度兜底并在收尾提交),前端不再自行编号。
## 收敛判读口径
`summarize_convergence()` 只做**可解释的启发式判读**,不替代正式学习曲线分析:
| 状态 | 触发条件 |
| --- | --- |
| 等待首个 rollout | 尚无任何指标 |
| 预热阶段 | 进度点 < 3,无法判断趋势 |
| KL 偏大,策略更新可能过猛 | 最近 `approx_kl` > 0.05(判定优先级最高) |
| 价值函数尚未拟合 | 最近 `explained_variance` < 0 |
| 奖励未形成上升趋势 | 最近窗口奖励斜率为负 |
| 已趋于平稳(疑似收敛) | 窗口对半位移 ≤ 逐点噪声、变异系数 ≤ 0.15、极差 ≤ 15% 量级 |
| 收敛趋势良好 | 其余(斜率正向) |
平台判据特意用「对半窗口位移 / 逐点噪声」而不是单看斜率绝对值:慢速但持续的
提升(斜率很小)不会被误判成收敛。所有判据数字都会写进 `notes`,便于人工复核。
## 安全边界
- 只监听 `127.0.0.1`;静态资源是固定白名单,不提供目录遍历。
- 命令由服务端以**参数列表**构造,不经过 shell;字符串参数只拒绝控制字符。
- 同一时刻只允许一个训练任务;暂停中的任务仍算「在跑」,必须先停止才能开新一局。
- 暂停/停止走控制文件(`--control-file`,默认 `output/hrl/ui/control/<job_id>.control.json`),
进程组隔离按平台选择(POSIX `setsid` / Windows `CREATE_NEW_PROCESS_GROUP`),
超时强杀按平台选择(POSIX `killpg(SIGKILL)` / Windows `taskkill /F /T`)。
- 产物路径限定在仓库根下解析,模型输出沿用训练脚本自身的 `--force`/`--resume`
语义(两者互斥,由后端校验)。
## 暂停、停止与落盘保证
「暂停」是**协作式**的:前端原子写控制文件,`train_hrl_high.py` 的
`_ControlPoll` 每 32 个环境步抽样读一次;暂停时训练阻塞在 `_on_step` 的轮询里,
进程与显存保留、不消耗算力,点「继续」从同一个 step 边界恢复。暂停时长不计入
`--max-wall-s`(否则"挂起一小时回来直接判超时")。
「停止」同样是控制文件请求(**跨平台**)。`train_hrl_high.py` 有多层兜底,
点一下「停止」不再等于这一局白跑;而**急停**(强杀,例如 `SIGKILL` /
`taskkill /F`,进程没有任何收尾机会)也保证可恢复:
| 机制 | 行为 |
| --- | --- |
| 滚动恢复点 | `--recovery-every`(默认 2000 环境步)把 `<checkpoints>/<stem>_latest.zip` **原子覆盖写**一次;训练开始前先写一份**起步基线**。因此急停后最多只丢 `--recovery-every` 个环境步,且磁盘上始终有一份完整、可加载、可 `--resume` 的产物 |
| 原子落盘 | 模型 zip、旁挂 json、`resume.json` 都是「同目录临时文件 + `os.replace`」:保存途中被强杀不会留下半截文件,也不会破坏上一份完整快照 |
| 恢复指针 | `<run_dir>/resume.json` 记录当前最佳恢复目标 + 步数(增量语义)+ 生效过的 PPO 超参;前端顶部的「用最近检查点续训」按钮直接读它 |
| 定期 checkpoint | `--checkpoint-every`(默认 20000 环境步)另留 `<stem>_<步数>.zip`,`--checkpoint-keep` 只保留最近 K 个(默认 3),供返祖对比与多档恢复;滚动恢复点与起步基线不受清理影响 |
| 优雅停止 | 控制文件 `stop=true`(或 POSIX 信号 / Windows `SIGBREAK`)后,主进程在 step 边界收尾,保存最终模型与元数据(`stopped_by_signal=true`、`stop_signal`、`checkpoint_files`、`recovery_model`)并正常关闭环境 |
| 超时强杀 | 宽限期(`STOP_GRACE_S`,默认 15s)内没退出则强杀整棵进程树,快照 `forced_kill=true`,并在 `<run_dir>/forced_stop.json` 写明 `recovery.model`(可直接续训的目标) |
| 异常兜底 | `learn()` 抛出任何异常时先落 `*_emergency.zip` + 元数据 + `resume.json`,再写 `training_failure.json` 并抛出 |
急停后恢复的操作方式:界面点「用最近检查点续训」;命令行则
`--resume --model <resume.json 里的 model>`,`--timesteps` 按**追加**步数理解。
checkpoint 与最终模型共用同一份 json 契约,因此 `--resume <checkpoint.zip>` 直接
续训(`--timesteps` 为增量步数)。注意 `--resume`/`--init` 会沿用 checkpoint 内的
PPO 超参(`n_steps`/`batch_size`/`n_epochs`):命令行这三项被忽略,脚本会打一条
`hyperparams_from_checkpoint` 事件说明差异,元数据 `ppo_hyperparams` 记录**实际生效**值。
回归测试见 `tests/test_hrl_checkpoint.py`,其中端到端用例真起 subproc 子进程,覆盖
控制文件停止、POSIX 信号停止、**强杀后仍可续训**与暂停→继续(信号路径在 Windows
上自动跳过)。
## 测试
```bash
.venv/bin/python -m pytest tests/test_training_ui.py tests/test_training_ui_js.py tests/test_hrl_checkpoint.py -q
```
- `tests/test_training_ui.py`:配置契约同源、预设可直接提交、参数校验、SB3 表格
刻度打点、游标分页、`describe_process` 进度/ETA、示例快照与真实快照同键;
用**桩训练脚本真起子进程**覆盖启动/轮询/暂停/继续/停止/退出码全链路;并覆盖
跨平台进程控制——控制文件往返与损坏容错、`spawn_kwargs` 平台分支、Windows 走
`taskkill`、**无 `killpg` 环境下强杀不抛异常**(旧 bug 的直接回归)、控制文件被
忽略时强杀并写 `forced_stop.json` 恢复指引、`discover_recovery` 的
`resume.json`/扫描/悬空指针三分支、HTTP `pause/resume`。
- `tests/js/training_ui_harness.cjs`(经 `tests/test_training_ui_js.py` 调起):
DOM 桩加载发布的 `charts.js`/`app.js` 原文,用服务端生成的真实快照驱动渲染,
断言表单/指标卡/曲线/日志/逐回合表都拿到数据,覆盖增量游标三条分支与
`classList`、HTML 转义、极端数值等边界。
- `tests/test_hrl_checkpoint.py`:落盘与恢复的端到端回归——原子保存失败不破坏
上一份快照、JSON 原子替换、历史清理不误删滚动恢复点、起步基线可加载、
**强杀急停后 `resume.json` 与滚动恢复点仍可继续训练**、控制文件停止落盘、
暂停→继续、POSIX 信号停止。
## 已知取舍
- 进度点保留上限 5000(服务端 deque 与前端本地缓存同值),超长训练只保留最近一段。
- 表格解析依赖 SB3 的段名(`rollout/`/`time/`/`train/`/`system/`);SB3 若改表格
格式,退化路径是「只显示 progress JSON」,不会导致界面崩溃。
- 收敛判读是启发式;正式结论仍需用 `scripts/benchmark_hrl.py` 的配对评测与
学习曲线人工复核。
- 暂停粒度是 `poll_every` 个环境步(默认 32),不是即时挂起;停止宽限期默认 15s。
- Windows 强杀依赖系统自带的 `taskkill`;若被策略禁用会退回 `Popen.kill()`
(只杀主进程)。更严格的 Job Object 方案需要额外依赖,未引入。
Windows 兼容性问题的完整根因与修复记录见
[`training-ui-windows-audit.md`](training-ui-windows-audit.md)。
@@ -1,165 +0,0 @@
# 第三、四问强化学习适用性审核与改进
本轮审核第一版强化学习代码、专家数据、模型及官方演练归档,修复训练与部署不一致的问题,训练两个新的 Maskable PPO 模型,并用独立实现的本地 `referee` 完成同场景对照。所有新性能数字均为本地结果,没有启动新的官方演练。
结论:强化学习适合在现有几何算法内尝试“下一检测点”的排序,第四问比第三问更有收益空间。当前证据支持第四问的混合方案优于手写 `directional` 基线;尚不能证明新模型稳定优于旧 PPO。第三问没有稳定增益,应保留 `bayes` 默认策略。新模型作为实验版本显式加载,没有覆盖旧默认模型。
## 1. 题设和方法适用范围
附录2(3)的交会必须使用带误差的角域,不能把两条示向线的交点当作真值;同地重测不会提供新的独立误差样本。附录2(4)的单接收机逐频道检测,结合动作规则,须计入移动、5秒检测及实际1秒换频。第三问的全向假设允许用无信号排除接收圆盘;第四问的无信号还可能来自方向遮挡,不能沿用这一排除结论。题设及几何算法的完整审核见[算法审核与优化](算法审核与优化-20260911.md)。
当前 RL 实际只替换 `RLBayesLoop._select_point` 和 `RLDirectionalLoop._next_probe`。七站/49站覆盖、频道发现、全局任务调度、保守候选集、清除证书和第四问后备清除仍由原算法负责。因此,完整场景全清验证的是混合系统,不是神经网络独立获得了完备性证明。
401维输入概括几何、历史读数和60个候选点,但省略完整历史、候选多边形细节及位置—朝向联合约束,不能直接称为完整马尔可夫状态。局部单源训练也没有学习全场覆盖路线与多源调度。PPO可以作为这种压缩观测下的近似策略尝试,但其目标并不自动等于全场最短时间;评估必须使用独立完整场景。[SB3官方训练与评估建议](https://stable-baselines3.readthedocs.io/en/master/guide/rl_tips.html)也强调独立评估、多随机种子和状态信息完整性。
## 2. 确认的实现缺陷及修复
| 缺陷 | 证据和影响 | 已实施修复 |
| --- | --- | --- |
| 训练传入整个 `BeliefState`,部署传入 `ChannelBelief` | 同一状态的特征索引7、14、15、17、18不同;训练丢失测量次数、末次结果和方位信息 | `env._observe` 使用目标频道信念;测试逐位比较部署输入,并覆盖跨频道交接 |
| BC使用ReLU,接收权重的PPO使用Tanh | 仅复制线性权重无法保留函数。原始专家输入探针中,Q3的90.96%、Q4的67.42%出现不同argmax | BC改为Tanh;复制时检查完整前向输出,重新初始化critic;旧ReLU/旧特征检查点拒绝直接迁移 |
| BC按观测行随机拆分 | 同一episode的相关状态可能同时出现在训练和验证中,分类准确率不足以说明独立场景泛化 | 保存episode编号,按episode划分训练/验证 |
| 第四问训练遮挡状态与运行时不一致 | 训练只记录距离保证盘内的无信号,且获得新方向后未正确重置连续遮挡;后备清除触发轨迹偏离部署 | 所有连续无信号记录为探测序列,仅盘内无信号用于确认定向;正观测清空序列,统一4次遮挡/8次定位阈值 |
| 奖励鼓励首次遮挡等中间事件 | “获得遮挡证据”额外+2、半径收缩等奖励会改变实际时间目标;较高回报不一定更快 | 主要奖励改为实际虚拟耗时的负值,成功清除统一+20,失败/截断重罚;取消这些中间奖励,采用gamma=1 |
| 未屏蔽同地重测、重合候选和第三问盘外梯子点 | 浪费固定误差条件下的检测时间;旧文档“所有候选保证接收”不成立 | 训练/推理共用动作屏蔽,排除已测0.5m格位置、重复候选、越界点;第三问存在距离保证盘时排除盘外点 |
| 保证盘半径恰为0时动作数量可能改变 | 依赖去重后的候选生成器会破坏固定60动作接口 | 即使位置重合也保留编号,交由动作屏蔽处理 |
| 只校验401/60维度不能识别语义变化 | 接收机频道被目标频道替代;旧模型与新模型同形但输入含义不同 | 加入特征版本与算法元数据,v2使用实际接收机频道,旧版保留原输入语义;拒绝问题编号或屏蔽模型元数据不匹配 |
| 基准将 `completed` 当作全清 | 内部状态自洽不代表全部真实源已清除 | 用评测端真实频道集合对照成功清除集合,并检查真实频道没有被误判缺席、正常退出、异常及逐步计时;旧基准新增 `all_valid` 和失败退出码 |
| 官方审计没有严格对应问题与策略名称 | 第三问/第四问摘要可能交叉通过名称检查 | `verify_rl_practice.py` 分别要求 `rl` / `rl_q4` |
BC探针是对旧网络和“拷贝后换激活”的网络进行比较,并不是新模型错误率,也不是重新测得的BC验证准确率。旧报告38.9%/65.3%的分类准确率不能直接解释为已迁移PPO的模仿准确率。本轮训练的新模型均从随机初始化开始,未使用BC;BC修复通过前向一致性测试,未据此声称重新训练BC已经带来收益。
动作屏蔽采用官方 `MaskablePPO` 的 `action_masks` 接口,训练和确定性推理都传入相同规则。[SB3-Contrib官方文档](https://sb3-contrib.readthedocs.io/en/master/modules/ppo_mask.html)说明了这两处的用法。第四问仍允许探索盘外候选,距离保证不等于方向可见。退化到所有动作均被屏蔽时保留中心动作,由步数/预算边界处理,因此不能把本次零重复观测扩展为所有可能状态的严格保证。
正常成功episode的累计奖励为 `20 - 局部实际虚拟时间/100`;环境仍保留异常与失败处理,包括证书清除失败的额外惩罚。局部时间从首测与可选交接完成后开始,不能用训练报告中的局部均值替代整场成绩。第三问训练自动清除仍使用证书圆心,而完整 `BayesLoop` 可在证书范围内平移清除点;全局调度、预算与局部环境也有差异,完整闭环验证因此不可省略。
## 3. 训练和数据
第一版Q3为120000步随机初始化PPO;第一版Q4为BC初始化后40000步PPO。原专家数据分别为5141、14181条,原5场完整测试均值分别是Q3:3088.33/3124.76秒、Q4:14250.15/13230.20秒(手写/RL)。这些历史数字受旧环境和特征影响,不与本轮新课程的单源成功率直接比较。
修订课程保留首次有效测向,并加入35%的跨频道、跨位置交接,20%的区域边缘且最小接收半径场景。定向首测覆盖朝向±89.9°,更接近半平面边界。源位置、朝向、半径和逐点误差的抽样仍是自建分布,不是已知的官方生成分布;真值仅用于布设和评测,不进入策略观测或动作屏蔽。
两模型使用 `[128,128]` Tanh MLP、2个内存环境、`n_steps=512`、`batch_size=128`、6轮更新、学习率0.0003。实际均完成60416步,来自完整rollout批次向上取整;元数据中的60000是请求步数。安装版本为SB3 2.9.0、SB3-Contrib 2.9.0、Torch 2.11.0+cu128。本次选择CPU、单Torch线程,机器CUDA可用。
| 项目 | Q3新模型 | Q4新模型 |
| --- | ---: | ---: |
| 训练种子 | 310001 | 410001 |
| 定向概率 | 0 | 0.75 |
| 训练墙钟时间 | 276.6s | 250.6s |
| 独立单源评估成功 | 59/60 | 60/60 |
| 单源平均局部时间 | 270.04s | 349.51s |
| 平均动作步数 | 2.42 | 3.13 |
Q3有一次局部训练任务未完成,不能声称单源成功率100%。完整闭环仍有自身调度与后备路径。两问题各只有一个新模型训练种子,未覆盖训练随机性。
## 4. 完整场景同条件对照
比较三种固定策略:当前手写基线(Q3 `bayes`、Q4 `directional`)、旧默认PPO、新Maskable PPO。相同场景使用相同源位置、频道、半径、朝向及逐点固定误差种子。`referee` 为单独实现,策略不读取模拟器真值。每场保存场景、完整请求/响应、summary、模型哈希和计时核验。
开发集为种子700—702、10/16源,各问题6场。随后固定新模型,不再据验证成绩调参;验证集为种子91000—91005、10/13/16源,各问题18场。第四问定向数覆盖1、约半数、仅一个全向三种组成。另加两种圆周边界压力场景。开发、验证、压力共52个不同场景,三策略共156次完整运行,全部满足真实源全清、缺席无误判、正常exit、零异常、逐步耗时复算一致。
### 开发集平均整场时间
| 问题 | 手写基线 | 旧PPO | 新模型 |
| --- | ---: | ---: | ---: |
| Q3,6场 | 3294.62s | 3206.20s | 3264.62s |
| Q4,6场 | 14289.76s | 12443.43s | 11758.72s |
### 独立验证集
| 问题/指标 | 手写基线 | 旧PPO | 新模型 |
| --- | ---: | ---: | ---: |
| Q3有效全清 | 18/18 | 18/18 | 18/18 |
| Q3平均时间 | 3330.72s | 3254.61s | 3307.65s |
| Q3中位时间 | 3309.16s | 3262.64s | 3222.50s |
| Q3最大时间 | 3690.11s | 3786.98s | 3776.55s |
| Q4有效全清 | 18/18 | 18/18 | 18/18 |
| Q4平均时间 | 13778.82s | 12070.53s | 11797.57s |
| Q4中位时间 | 14587.80s | 12891.83s | 12488.23s |
| Q4最大时间 | 17273.98s | 15365.76s | 15894.44s |
| Q4累计未成功clear尝试 | 616 | 719 | 1380 |
| Q4累计同频道同点重测 | 12 | 28 | 0 |
Q3三组都没有失败clear或同点重测。Q4新模型减少了重测,但更多使用后备光学清除尝试,平均耗时降低的同时尾部表现有所退化。失败clear本身可来自允许的后备搜索,不能仅凭该计数认定证书错误;其移动和3秒动作耗时全部纳入结果。
对独立验证场景做10000次配对bootstrap,负值表示新模型更快:
| 对比 | 均值相对变化 | 95%场景重采样区间 | 新模型更快场数 |
| --- | ---: | ---: | ---: |
| Q3新模型对手写 | -0.69% | [-4.50%, +2.95%] | 9/18 |
| Q3新模型对旧PPO | +1.63% | [-1.64%, +5.19%] | 8/18 |
| Q4新模型对手写 | -14.38% | [-17.93%, -11.48%] | 18/18 |
| Q4新模型对旧PPO | -2.26% | [-6.08%, +1.71%] | 13/18 |
新旧PPO的区间均跨0,不能据小幅平均变化宣称稳定改进。区间只反映固定模型在这些场景上的差异,不覆盖训练种子、未知官方分布或所有合法边界。此外本轮同时修改多项训练因素,这不是能归因到单个修复的消融实验。
### 边界压力与4000秒目标
源全部放在半径1800m圆周、接收半径1000m;第四问隔一个源采用向外发射方向。10/16源各一场,各策略都完成全清。
| 问题 | 手写平均 | 旧PPO平均 | 新模型平均 |
| --- | ---: | ---: | ---: |
| Q3 | 4472.58s | 4429.04s | 4648.26s |
| Q4 | 18248.87s | 14404.01s | 15210.08s |
新Q3的两场分别为4659.06s、4637.46s,仍超过4000s;新Q4为14573.62s、15846.54s,且比旧PPO退化。此前几何优化在另一独立Q3验证组达到平均3315.89s、最大3981.74s,本轮新Q3验证组最大3776.55s;二者只能说明各自验证组达标,不能承诺全部题设场景小于4000s。第四问尚未达到4000s。
## 5. 官方证据与测试
重新审计已有官方归档,三次均通过题号/策略、官方包SHA、源数、完整清除、正常退出、请求时窗及逐步计时检查,未改写原归档:
| 问题 | 既有官方案例 | 全清 | 整场时间 |
| --- | --- | ---: | ---: |
| Q3 | F38Y-9Q9J-B82V-CB9Z | 13/13 | 3538.81s |
| Q4,3个定向源 | X43X-VMRF-8GRZ-3SYA | 10/10 | 14003.09s |
| Q4,10个定向源 | BXSA-TFQ3-2DJ6-DHH7 | 10/10 | 14562.82s |
这些是旧模型的官方可运行证据,不是新模型成绩;不同案例也不能直接计算优化百分比。新模型目前通过本地验证,未在官方新演练中运行。
全量回归:`python -m pytest -q --tb=short`,243项通过。初次沙箱运行有24个临时目录权限导致的fixture错误,获得权限后重跑全部通过。新增5项测试覆盖训练/部署观测、动作屏蔽、方向读数重置遮挡、实际成本奖励和BC完整前向一致性。之后对基准/官方审计入口的小修补分别用4次完整场景冒烟运行及三份官方归档复核通过。
## 6. 产物与复现
主目录为 `output/rl/review-20260911/`:
- `q3_masked.zip` / `.json`、`q4_masked.zip` / `.json`:新模型与训练元数据,必须成对保存。
- `evaluation/development/{controls,revised}/results.json`:开发对照。
- `evaluation/validation/final/results.json`:独立验证与每场完整日志。
- `evaluation/stress/final/results.json`:边界压力结果与日志。
- `review_evidence.json`:配对区间、BC/特征错位探针、旧官方重审结果和代码哈希。
- `benchmark-smoke.json`:旧基准有效性口径修复后的冒烟结果。
新Q3模型SHA-256:`b8b3a2bb103588348d8d7791bde9a2b0635c04778f0ac64f91c6f5c7cf87ca6e`。
新Q4模型SHA-256:`6865d541ff9bdee81886d327345f2660d5f7d72f4b39810a58ae4b867ab37e42`。
从项目根目录复现训练时,使用新目录保留本轮检查点:
```powershell
.venv/Scripts/python.exe scripts/train_rl.py --problem 3 --algorithm maskable_ppo --timesteps 60000 --seed 310001 --device cpu --model output/rl/reproduction/q3_masked.zip --eval-episodes 60 --verbose 0
.venv/Scripts/python.exe scripts/train_rl.py --problem 4 --algorithm maskable_ppo --timesteps 60000 --seed 410001 --directional-probability 0.75 --device cpu --model output/rl/reproduction/q4_masked.zip --eval-episodes 60 --verbose 0
```
复核本轮冻结模型的完整对照(label须使用尚不存在的名称):
```powershell
.venv/Scripts/python.exe scripts/evaluate_rl_revision.py --split validation --label rerun-validation
.venv/Scripts/python.exe scripts/evaluate_rl_revision.py --split stress --label rerun-stress
.venv/Scripts/python.exe scripts/summarize_rl_revision.py
```
本地内存自检可显式加载对应检查点;`--self-test`不会连接官方模拟器:
```powershell
.venv/Scripts/mcm26b.exe run --robot-id 2026001 --self-test --strategy rl --rl-model output/rl/review-20260911/q3_masked.zip
.venv/Scripts/mcm26b.exe run --robot-id 2026001 --self-test --problem 4 --q4-strategy rl --rl-model output/rl/review-20260911/q4_masked.zip
```
在用户另行启动相应问题的官方演练后,使用实际登录队号和`--base-url`连接。例如第三问(路径使用本轮尚未创建的官方演练目录):
```powershell
.venv/Scripts/mcm26b.exe run --robot-id 2026001 --base-url http://127.0.0.1:2026 --problem 3 --strategy rl --rl-model output/rl/review-20260911/q3_masked.zip --log output/rl/review-20260911/practice-q3/session.jsonl
```
该命令保存请求/响应日志,汇总输出到终端;官方结果文件与汇总还需按原归档流程保存。第四问改用`--problem 4 --q4-strategy rl`及对应模型。本轮未执行上述官方连接命令。
进一步研究应先缩小单源训练与完整调度的差异,并处理Q4后备清除和边界退化,再做多个训练种子及新的冻结验证集。当前结果不足以将新模型自动替换为默认策略,也不足以把训练步数增加当作达到4000秒的依据。