ml: 按运行手册审查修订——头分目录不互相覆盖(ml/runs/<CFG>、selected_ckpt/CKPT);统一 $PYTHON(Windows Git Bash 可跑);gen 按实测样本/任务自动补量;R10 改终态判据(≥5k 样本/s≈60k 步≤3.5h);m5 纯策略/仿真安全层双口径;无老师题定长 6 s 口径;APG 展开随批时长、端点+中点铰链、余量 3 mm(投影同步);挖错池只用本轮;门禁加 R7;bench 加 R8 对抗集与刹停率、--nc-n/--route-n/--r8-n;新增 check_r2 与 acceptance;export 默认 fp32;出题分块防 OOM;val 为空保护
This commit is contained in:
+94
-50
@@ -3,32 +3,34 @@
|
||||
目标:在你的机器上从零跑出 Transformer 避障策略,并在查看器里直接用上。
|
||||
方案依据见 `docs/Transformer轨迹拟合训练方案.md`。所有命令在**仓库根目录**执行。
|
||||
|
||||
最省事的跑法是按阶段执行 `bash ml/pipeline.sh <阶段>`,每一阶段完成后看一眼本手册对应小节的「检查」再进下一步。
|
||||
按阶段执行 `bash ml/pipeline.sh <阶段>`,每一阶段完成后看本手册对应小节的「检查」再进下一步。
|
||||
|
||||
| 阶段 | 命令 | 预计耗时(16 核 CPU + RTX 3090) | 产出 |
|
||||
| 阶段 | 命令 | 预计耗时(28 核 CPU + RTX 3090) | 产出 |
|
||||
|---|---|---|---|
|
||||
| 1 环境 | `bash ml/pipeline.sh setup` | 10 分钟 | 依赖 |
|
||||
| 2 对拍 | `bash ml/pipeline.sh kin` | 2 分钟 | `ml/assets/kin.json`、对拍 PASS |
|
||||
| 2 对拍 | `bash ml/pipeline.sh kin` | 1 分钟(实测 7.6 s 对拍) | `ml/assets/kin.json`、对拍 PASS |
|
||||
| 3 老师门禁 | `bash ml/pipeline.sh gate1` | 25–30 分钟(单核) | `ml/data/consistency.json` |
|
||||
| 4 生成数据 | `bash ml/pipeline.sh gen` | 1–2 小时;加共识过滤 5–9 小时 | `ml/data/shard_*.jsonl`、`stats.json` |
|
||||
| 5 打包样本 | `bash ml/pipeline.sh prep` | 15–30 分钟 | `ml/data/samples/` |
|
||||
| 6 预训练 | `bash ml/pipeline.sh train` | 先 2000 步测速,正式约 1.5–6 小时 | `ml/runs/base/best.pt` |
|
||||
| 7 M5 门禁 | `bash ml/pipeline.sh m5` | 10 分钟 | `ml/runs/m5_gate.json` |
|
||||
| 8 APG 微调 | `bash ml/pipeline.sh apg` | 以前 100 步测速为准(预计 2–4 小时) | `ml/runs/apg/best.pt` |
|
||||
| 4 生成数据 | `bash ml/pipeline.sh gen` | 不共识约 1 小时;共识约 4–5 小时 | `ml/data/shard_*.jsonl`、`stats.json` |
|
||||
| 5 打包样本 | `bash ml/pipeline.sh prep` | 15–30 分钟 | `ml/data/samples/`、`ml/data/r2.json` |
|
||||
| 6 预训练 | `bash ml/pipeline.sh train` | 测速 2000 步约 6 分钟,正式约 3 小时 | `ml/runs/base/best.pt` |
|
||||
| 7 M5 门禁 | `bash ml/pipeline.sh m5` | 10–20 分钟 | `ml/runs/m5_gate_base.json` |
|
||||
| 8 APG 微调 | `bash ml/pipeline.sh apg` | 以前 100 步速度为准(预计 2–4 小时) | `ml/runs/apg/best.pt` |
|
||||
| 9 挖错循环 | `bash ml/pipeline.sh mine` | 每轮约 1–2 小时,3–6 轮 | `ml/runs/current.pt` |
|
||||
| 10 导出 | `bash ml/pipeline.sh export` | 1 分钟 | `webapp/models/traj_policy.onnx` |
|
||||
| 11 评测 | `bash ml/pipeline.sh bench` | 约 1 小时(Node CPU,1070 个任务) | `ml/runs/policy_bench.json` |
|
||||
| 12 使用 | `python ml/serve.py` → 浏览器 `?policy=1` | — | — |
|
||||
| 10 导出 | `bash ml/pipeline.sh export` | 1 分钟 | `webapp/models/traj_policy.onnx`(fp32) |
|
||||
| 11 部署评测 | `bash ml/pipeline.sh bench` | 约 1 小时(Node CPU,1120 个任务) | `ml/runs/policy_bench.json` |
|
||||
| 12 验收汇总 | `bash ml/pipeline.sh accept` | 10–20 分钟 | `ml/runs/acceptance.json` |
|
||||
| 13 使用 | `python ml/serve.py` → 浏览器 `?policy=1` | — | — |
|
||||
|
||||
耗时除第 4 步外均为估算,以每步首次输出的速度为准。
|
||||
第 2/4/5/6 步耗时为 RTX 3090 + 28 核实测换算,其余为估算,以每步首次输出的速度为准。
|
||||
|
||||
---
|
||||
|
||||
## 1. 环境
|
||||
|
||||
- Linux 或 WSL2;NVIDIA 驱动 + CUDA(`nvidia-smi` 能看到 3090)
|
||||
- Linux / WSL2,或 **Windows 原生 + Git Bash**(实测第 2/4/5/6 步与闭环仿真可跑);NVIDIA 驱动 + CUDA(`nvidia-smi` 能看到 3090)
|
||||
- Node ≥ 18,Python 3.10–3.12
|
||||
- 先按 CUDA 版本装 PyTorch(https://pytorch.org ,例:`pip install torch --index-url https://download.pytorch.org/whl/cu124`),再:
|
||||
- 脚本统一用 `$PYTHON`(默认 `python`)。Linux 上没有 `python` 命令时:`export PYTHON=python3`。Windows 不要用 `python3`(那是 WindowsApps 假壳)
|
||||
- 先按 CUDA 版本装 PyTorch(https://pytorch.org ),再:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
@@ -50,59 +52,88 @@ bash ml/pipeline.sh kin
|
||||
bash ml/pipeline.sh gate1
|
||||
```
|
||||
|
||||
看 `consistentByGateRule`:≥ 0.90 直接生成;< 0.90 时第 4 步会自动加 `--consensus 4`(过滤与邻居绕法不一致的样本,生成成本约 5 倍)。也可手动用 `CONSENSUS=0/1` 覆盖。
|
||||
看 `consistentByGateRule`:≥ 0.90 直接生成;< 0.90 时第 4 步自动加 `--consensus 4`(每条任务另跑 4 个 ±5 mm 邻居,与多数绕法不一致的样本丢弃,生成成本约 5 倍)。可用 `CONSENSUS=0/1` 手动覆盖。
|
||||
|
||||
## 4. 生成老师数据
|
||||
|
||||
```bash
|
||||
bash ml/pipeline.sh gen # 默认 5 万条、进程数 = CPU 核数
|
||||
# 自定义:N_TASKS=50000 NPROC=16 CONSENSUS=1 bash ml/pipeline.sh gen
|
||||
# 自定义:N_TASKS=50000 NPROC=28 CONSENSUS=1 TARGET_SAMPLES=1700000 bash ml/pipeline.sh gen
|
||||
```
|
||||
|
||||
每条约 2 秒/核(不共识)。检查 `ml/data/stats.json`:`keepRate` 应在 0.95 左右,`byCatMode` 里 C 类(贴立柱)有足够样本,`projectedSamplesFor50k` 约 180 万。
|
||||
不共识约 1.6 秒/条/核;共识约 8 秒/条/核。生成完自动统计;**样本数不足 `TARGET_SAMPLES`(默认 170 万)时按实测「样本/任务」自动补生成**(最多补 2 次),不用手动加 `N_TASKS`。
|
||||
|
||||
## 5. 打包训练样本
|
||||
检查 `ml/data/stats.json`:
|
||||
|
||||
| 字段 | 不共识(实测) | 共识(估算) | 不正常时 |
|
||||
|---|---|---|---|
|
||||
| `keepRate` | ≈ 0.97 | 0.80–0.88 | 共识 < 0.75:把 `stats.json` 发我(老师左右跳变比预期多) |
|
||||
| `samplesPerTask` | ≈ 36.5 | ≈ 36.5 | — |
|
||||
| `samplesEvery3` | ≥ 170 万(自动补量后) | ≥ 170 万 | 补量后仍不足:发我 |
|
||||
| `consensusDrop` | 0 | 约任务数的 10–20% | — |
|
||||
| `vertexMinGapMm` | p50 ≈ 4.4、p90 ≈ 5.9 mm(p10 预计约 3) | 同 | p10 < 2.5 mm:发我(APG 与投影的 3 mm 余量要随之下调) |
|
||||
| `byCatMode` | C 类(贴立柱)各模式都有数千条 | 同 | — |
|
||||
|
||||
## 5. 打包训练样本 + R2 检查
|
||||
|
||||
```bash
|
||||
bash ml/pipeline.sh prep
|
||||
```
|
||||
|
||||
每条轨迹按 50 Hz 时间律重放,每 3 步取一个样本(特征与部署同一份 JS 代码计算)。检查 `ml/data/samples/index.json`:`train_samples` 约 170 万、`val_samples` 约 9 万。
|
||||
每条轨迹按 50 Hz 时间律重放,每 3 步取一个样本(特征与部署同一份 JS 代码计算),30% 样本加 σ = 1° 噪声增广。
|
||||
检查 `ml/data/samples/index.json`:`train_samples` 约 170 万、`val_samples` 约 9 万(5% 任务划为 val)。
|
||||
随后自动跑 R2 检查(`ml/data/r2.json`):近邻样本前 3 步标签差 `label_std_mean < 0.10`(10% 步长)为通过;不通过先发我,不要直接训练。
|
||||
|
||||
## 6. 预训练
|
||||
|
||||
```bash
|
||||
bash ml/pipeline.sh train
|
||||
bash ml/pipeline.sh train # MeanFlow 头 → ml/runs/bench_base/、ml/runs/base/
|
||||
```
|
||||
|
||||
先跑 2000 步测吞吐(`ml/runs/bench/log.jsonl` 的 `samples_per_s`),再正式训练到 `configs/base.json` 的 60000 步或闭环到达率连续 5 次不升为止。
|
||||
训练中每 2000 步打印一行 `EVAL {...}`:闭环(部署节奏)`success` 到达率、`collision_rate`、`time_ok_1p3`。
|
||||
显存不够就把 `configs/base.json` 的 `batch` 改成 512。中断后加 `--resume ml/runs/base/last.pt` 续训:
|
||||
先跑 2000 步测吞吐(`ml/runs/bench_base/log.jsonl` 的 `samples_per_s`,`eta_h` 是剩余小时数),再正式训练到 60000 步或闭环到达率连续 5 次不升为止。
|
||||
|
||||
- **通过线(R10):`samples_per_s` ≥ 5000,即 60000 步 ≤ 3.5 小时**。参考:3090、batch 1024 实测 6.2 k 样本/s ≈ 2.8 小时。MeanFlow 的 JVP 双前向使训练受算力限制,加大 batch 不会提高样本/s;低于通过线时把 `configs/base.json` 的 `fk_caps` 从 96 降到 48,仍不够再把 `steps` 降到 40000
|
||||
- 训练中每 2000 步打印一行 `EVAL {...}`:val 任务闭环(部署节奏、不开安全层)`success`、`collision_rate`、`time_ok_1p3`、`start_fail`。时限 = 老师时长 × 2(至少 3 s)
|
||||
- 显存不够把 `batch` 改成 512。中断后续训(日志追加;新训练会覆盖旧日志):
|
||||
|
||||
```bash
|
||||
python -m ml.train --samples ml/data/samples --out ml/runs/base --resume ml/runs/base/last.pt
|
||||
```
|
||||
|
||||
## 7. M5 门禁:MeanFlow 头是否可用
|
||||
## 7. M5 门禁:选定动作头
|
||||
|
||||
```bash
|
||||
bash ml/pipeline.sh m5
|
||||
```
|
||||
|
||||
在 1000 个贴立柱任务上关安全层闭环运行。穿柱率 ≤ 2% 通过;不通过(或训练日志出现「训练发散」)则换预定回退的 WTA K=8 头重训:
|
||||
1000 个贴立柱任务(无老师题,定长 6 s 时限)跑两遍,结果在 `ml/runs/m5_gate_base.json`:
|
||||
|
||||
- `policy_only`:关安全层的纯策略指标。**门禁只看这一项:穿柱率 ≤ 2% 通过**,通过后把该模型记为选定模型(`ml/runs/selected_ckpt`),后续阶段自动从它开始
|
||||
- `with_sim_safety`:仿真里的近似安全层(候选逐步干涉就换下一条)。只作参考
|
||||
|
||||
三种口径的关系:M5 是纯策略能力;仿真安全层是近似;**部署能不能过只看第 11 步**(与浏览器同一份运行时,含投影、距离认证、刹停、预算、换向锁)。三者互不替代。
|
||||
|
||||
不通过(或训练日志出现「训练发散」)则换 WTA K=8 头。WTA 的产物在独立目录,不会覆盖 MeanFlow:
|
||||
|
||||
```bash
|
||||
CFG=ml/configs/wta.json bash ml/pipeline.sh train && bash ml/pipeline.sh m5
|
||||
CFG=ml/configs/wta.json bash ml/pipeline.sh train # → ml/runs/bench_wta/、ml/runs/wta/
|
||||
CFG=ml/configs/wta.json bash ml/pipeline.sh m5 # → ml/runs/m5_gate_wta.json;通过则选定 wta
|
||||
```
|
||||
|
||||
需要强制指定后续阶段的起点模型时用 `CKPT=路径 bash ml/pipeline.sh apg`。
|
||||
|
||||
## 8. APG 可微微调
|
||||
|
||||
```bash
|
||||
bash ml/pipeline.sh apg
|
||||
```
|
||||
|
||||
按部署节奏可微展开策略,直接优化到达、碰撞余量(5 mm)、平滑与行程,并保留 BC 锚定。每 500 步闭环验证,按到达率存 `best.pt`,完成后复制为 `ml/runs/current.pt`。
|
||||
显存不够把 `configs/apg.json` 的 `batch` 降到 64。
|
||||
按部署节奏(每次推理提交 3 步、50 Hz)可微展开策略,优化到达、碰撞余量、平滑与行程,并保留 BC 锚定。每 500 步闭环验证,按到达率存 `best.pt`,完成后复制为 `ml/runs/current.pt`。
|
||||
|
||||
- **展开长度**:每批覆盖该批老师时长 p90 × 1.2(无老师题按 6 s),在 60–150 次推理(3.6–9 s)之间;日志 `plans` 字段是本批实际次数
|
||||
- **到达项**:每 10 次推理段末的 TCP 距离与姿态误差,没有「到达即停」。这是有意的:段末项在整条展开上持续给出接近目标的梯度
|
||||
- **碰撞项**:每步端点 + 中点的逐胶囊对铰链 `ReLU(3 mm − 间隙)`。3 mm ≈ 老师轨迹最小间隙的 p10(p50 4.4 mm),避免碰撞项与 BC 标签对抗;运行时投影余量同为 3 mm
|
||||
- 显存不够把 `configs/apg.json` 的 `batch` 降到 64,或把 `seg_plans` 从 10 降到 5
|
||||
|
||||
## 9. 挖错循环
|
||||
|
||||
@@ -110,18 +141,21 @@ bash ml/pipeline.sh apg
|
||||
bash ml/pipeline.sh mine # 默认 6 轮;ROUNDS=3 APG_STEPS=1000 MINE_N=50000 可调
|
||||
```
|
||||
|
||||
每轮:5 万道新题关安全层作答 → 错题分类 E1–E5 → 错题与邻居进 APG 采样池 → E2(局部极小)请老师补少量路径 → APG 微调 → 门禁集比较,不达标自动回滚。连续 2 轮提升 < 0.5 个百分点自动停止。每轮报告在 `ml/runs/mine/r*/report.md` 与 `gate.json`。
|
||||
每轮:5 万道新题关安全层作答 → 错题分类 E1–E5 → **本轮**错题与邻居进 APG 采样池(不累积历史轮,旧错题由门禁的「上轮修好仍对 ≥ 95%」把关)→ E2(局部极小)请老师补少量路径 → APG 微调 → 门禁集比较,不达标自动回滚。连续 2 轮提升 < 0.5 个百分点自动停止。
|
||||
|
||||
门禁(`ml/runs/mine/r*/gate.json`):到达率不降、A 类降幅 ≤ 0.5 个百分点、上轮修好的题 ≥ 95% 仍对、**R7**:B/C 类左右绕行比例变化 ≤ 5 个百分点、行程中位数比 ≤ 1.05。挖错题与门禁集都是无老师题,到达按定长 6 s 判,`time_ok_1p3` 与行程比只在有老师参考的题上统计(门禁集上为 null)。
|
||||
|
||||
## 10. 导出
|
||||
|
||||
```bash
|
||||
bash ml/pipeline.sh export # fp16 ONNX + meta.json → webapp/models/
|
||||
bash ml/pipeline.sh export # fp32 ONNX + meta.json → webapp/models/(约 20 MB)
|
||||
```
|
||||
|
||||
fp16 转换报错时去掉 `--fp16` 手动导出 fp32(约 20 MB,同样可用):
|
||||
默认 fp32:部署走 wasm 与 Node CPU,fp16 在 CPU 后端上不保证更快。只有第 13 步用 `&ep=webgpu` 实测 `__POLICY.latency()` 明显受益时,才另导出 fp16 做对比:
|
||||
|
||||
```bash
|
||||
python -m ml.export_onnx --ckpt ml/runs/current.pt --out webapp/models/traj_policy.onnx
|
||||
pip install onnxconverter-common
|
||||
python -m ml.export_onnx --ckpt ml/runs/current.pt --out webapp/models/traj_policy_fp16.onnx --fp16
|
||||
```
|
||||
|
||||
## 11. 部署形态评测(策略 + 安全层)
|
||||
@@ -130,11 +164,20 @@ python -m ml.export_onnx --ckpt ml/runs/current.pt --out webapp/models/traj_poli
|
||||
bash ml/pipeline.sh bench
|
||||
```
|
||||
|
||||
跑与浏览器同一份运行时(`webapp/js/ik-policy.js`):贴立柱 30 例(pos、pose 各一遍)、直线被挡 40 例、一次性贴立柱扩展集 1000 例。
|
||||
验收看 `ml/runs/policy_bench.json`:各集合 `collisionFrames` 必须为 0;`near_column_*` 的 `ok` 30/30;`near_column_1000` 0 失败为目标。
|
||||
Node 里的推理/安全层耗时只作参考,浏览器延迟用第 12 步实测。
|
||||
跑与浏览器同一份运行时(`webapp/js/ik-policy.js`):贴立柱 30 例(pos、pose 各一遍)、直线被挡 40 例、R8 对抗 50 例(J1 起终差 ≥ 140°、终点贴立柱、两侧都要绕远)、一次性贴立柱扩展集 1000 例。各集合数量可用 `--nc-n` / `--route-n` / `--r8-n` 单独调。
|
||||
|
||||
## 12. 在查看器里使用
|
||||
看 `ml/runs/policy_bench.json`:各集合 `collisionFrames` 为 0;`near_column_*` 与 `route40` 全部 `ok`;`near_column_1000` 0 失败;`brakeRate` < 0.02;`r8_adversarial` 的 `recoveryRate` ≥ 0.95、`livelock` = 0。
|
||||
Node 里的推理/安全层耗时只作参考,浏览器延迟用第 13 步实测。
|
||||
|
||||
## 12. 验收汇总
|
||||
|
||||
```bash
|
||||
bash ml/pipeline.sh accept
|
||||
```
|
||||
|
||||
自己补跑 val 全部任务的闭环(开仿真安全层):可达任务到达率 ≥ 98%、D 类停靠距离 ≤ 老师 + 5 mm、起步失败 = 0;再读第 5–11 步产物核对 R2、R7、R8、R10、M5 与部署形态各项。逐项打印「通过 / 未过」,结果在 `ml/runs/acceptance.json`。
|
||||
|
||||
## 13. 在查看器里使用
|
||||
|
||||
```bash
|
||||
python -m arm.viewer.build_viewer # 若 webapp/generated/00_data.js 还没生成(查看器原有构建步骤)
|
||||
@@ -147,24 +190,23 @@ python ml/serve.py # 带跨源隔离头的本地服务,wasm
|
||||
- 工具偏移 ≠ 0 时自动交回原规划器
|
||||
- 离线环境:把 onnxruntime-web 的 `ort.min.js` 与 wasm 文件放到 `webapp/vendor/ort/`,地址加 `&ortLocal=1`
|
||||
|
||||
## 13. 回传给我
|
||||
|
||||
每完成一步把对应文件发我即可,我据此判断下一步或改代码:
|
||||
## 14. 回传给我
|
||||
|
||||
| 步骤 | 文件 |
|
||||
|---|---|
|
||||
| 2 | 对拍输出(终端 JSON) |
|
||||
| 3–5 | `ml/data/consistency.json`、`ml/data/stats.json`、`ml/data/samples/index.json` |
|
||||
| 6 | `ml/runs/bench/log.jsonl`(测速)、`ml/runs/base/log.jsonl` |
|
||||
| 7 | `ml/runs/m5_gate.json` |
|
||||
| 3–5 | `ml/data/consistency.json`、`ml/data/stats.json`、`ml/data/samples/index.json`、`ml/data/r2.json` |
|
||||
| 6 | `ml/runs/bench_base/log.jsonl`(测速)、`ml/runs/base/log.jsonl` |
|
||||
| 7 | `ml/runs/m5_gate_*.json` |
|
||||
| 8–9 | `ml/runs/apg/log.jsonl`、`ml/runs/mine/r*/report.json` 与 `gate.json` |
|
||||
| 11–12 | `ml/runs/policy_bench.json`、浏览器 `__POLICY.latency()` 输出 |
|
||||
| 11–13 | `ml/runs/policy_bench.json`、`ml/runs/acceptance.json`、浏览器 `__POLICY.latency()` 输出 |
|
||||
|
||||
## 常见问题
|
||||
|
||||
- **指纹不符**:`collide.json` 变了。依次重跑第 2、4、5 步并重训。
|
||||
- **训练发散**(loss 出现 nan/inf,自动停在 `diverged.pt`):先把 `lr` 降到 5e-4 重试一次,仍发散就走 WTA 回退(第 7 步)。
|
||||
- **显存不足**:降 `batch`;APG 还可把 `seg_plans` 从 10 降到 5。
|
||||
- **显存/内存不足**:降 `batch`;APG 还可把 `seg_plans` 降到 5。出题与间隙计算已分块(每块 512 个位形)。
|
||||
- **小样本摸底**:几百条任务时 val 可能为空,训练会提示并跳过闭环验证。
|
||||
- **浏览器只有 1 线程**:没走 `ml/serve.py`(缺 COOP/COEP 头)。
|
||||
- **查看器徽标「策略模式不可用」**:看控制台报错,多为模型文件缺失、指纹或 TCP 不符。
|
||||
|
||||
@@ -172,19 +214,21 @@ python ml/serve.py # 带跨源隔离头的本地服务,wasm
|
||||
|
||||
```
|
||||
ml/
|
||||
pipeline.sh 一条龙阶段脚本
|
||||
gen/ 老师数据:sampler / teacher(确定性 PRM A*)/ timelaw / gen_dataset / consistency / stats / run_parallel.sh
|
||||
prep/ export_kin.cjs(→ assets/kin.json)、build_dataset.cjs(→ samples/*.npy)、run_prep.sh
|
||||
pipeline.sh 一条龙阶段脚本(环境变量见文件头)
|
||||
gen/ 老师数据:sampler / teacher(确定性 PRM A*)/ cert.cjs(距离下界认证,实现在 ik-policy.js)/
|
||||
pathdev.cjs(同伦类判定,共识过滤与一致性门禁用)/ timelaw / gen_dataset / consistency / stats / run_parallel.sh
|
||||
prep/ export_kin.cjs(→ assets/kin.json)、build_dataset.cjs(→ samples/*.npy)、npy.cjs(.npy 写出)、run_prep.sh
|
||||
common/ kin_torch.py(可微运动学 + 胶囊间隙)、features.py(83 维特征,与 JS 逐项一致)
|
||||
model/ policy.py(Transformer + MeanFlow / WTA 头)、meanflow.py
|
||||
data.py train.py 样本加载、预训练
|
||||
sim/closed_loop.py 部署节奏闭环仿真(验证 / 挖错 / 门禁)
|
||||
apg/apg_finetune.py APG 可微微调
|
||||
mining/ tasks.py(无老师出题)、mine_round.py、gate.py、loop.sh
|
||||
eval/ dump_parity.cjs + check_parity.py(JS↔PyTorch 对拍)
|
||||
mining/ tasks.py(无老师出题)、mine_round.py、gate.py(含 R7)、loop.sh
|
||||
eval/ dump_parity.cjs + check_parity.py(JS↔PyTorch 对拍)、check_r2.py(R2)、acceptance.py(验收汇总)
|
||||
toy/toy_bimodal.py 2D 双模态对照实验(动作头选型依据)
|
||||
export_onnx.py serve.py
|
||||
configs/ base.json(MeanFlow)、wta.json(回退)、apg.json
|
||||
webapp/js/ik-policy.js 运行时:特征、距离下界认证、刹停、投影、精修、Runner(浏览器与 Node 共用)
|
||||
webapp/js/ik-policy-glue.js 查看器接入(?policy=1)
|
||||
tests/policy_bench.cjs 部署形态评测
|
||||
tests/policy_bench.cjs 部署形态评测(nc / route / r8 / nc1000)
|
||||
```
|
||||
|
||||
+15
-8
@@ -2,7 +2,10 @@
|
||||
python -m ml.apg.apg_finetune --init ml/runs/base/best.pt --samples ml/data/samples --out ml/runs/apg [--pool ml/runs/mine/pool.jsonl]
|
||||
每条展开固定一个噪声 z(确定性可微映射);每次推理提交 3 步(已提交前缀 3 步,与部署一致);
|
||||
截断反传:每 seg_plans 次推理(=3·seg_plans 步)反传一次并截断状态梯度。
|
||||
损失:段末到达项 + Σ碰撞铰链 ReLU(margin_train − 间隙)(逐胶囊对)+ 平滑(速度²+加速度²)+ 行程 + BC 锚定(MeanFlow 损失)。"""
|
||||
损失:段末到达项 + Σ碰撞铰链 ReLU(margin_train − 间隙)(逐胶囊对,端点+中点)+ 平滑(速度²+加速度²)+ 行程 + BC 锚定(MeanFlow 损失)。
|
||||
2026-10-18:展开长度不再定长 60 次推理(3.6 s,会截掉老师时长 > 3.6 s 的绕远题),改为覆盖本批 exec_s p90×1.2,上限 plans_max;
|
||||
"到达"仍是每 seg_plans 段末的距离项(无到达即停),属有意设计:段末项给出持续的接近梯度。
|
||||
margin_train 5 → 3 mm:老师轨迹自身最小间隙 p50 4.4 / p90 5.9 mm,5 mm 会让约一半 BC 标签与碰撞项对抗;3 mm ≈ 老师 p10。"""
|
||||
import argparse, json, math, os, random, time
|
||||
import torch
|
||||
|
||||
@@ -45,7 +48,7 @@ def main():
|
||||
opt = torch.optim.AdamW(model.parameters(), lr=C['lr'], betas=(0.9, 0.95), weight_decay=0.0)
|
||||
amp = dev.startswith('cuda') and C.get('amp') == 'bf16'
|
||||
step = torch.tensor(STEP, device=dev)
|
||||
log = open(os.path.join(a.out, 'log.jsonl'), 'a')
|
||||
log = open(os.path.join(a.out, 'log.jsonl'), 'w')
|
||||
base, _ = evaluate(model.eval(), kin, val_tasks, dev)
|
||||
print('EVAL0', json.dumps(base), flush=True); log.write(json.dumps({'eval': base, 'step': 0}) + '\n')
|
||||
best = base['success']
|
||||
@@ -62,9 +65,12 @@ def main():
|
||||
subs = torch.zeros(B, 2, 6, device=dev); nsub = torch.zeros(B, dtype=torch.long, device=dev); sub_t = torch.zeros(B, device=dev)
|
||||
opt.zero_grad(set_to_none=True)
|
||||
stats = {'col': 0.0, 'tgt': 0.0, 'smooth': 0.0}
|
||||
nseg = math.ceil(C['plans'] / C['seg_plans'])
|
||||
# 展开长度随本批任务时长:覆盖本批 exec_s 的 p90 × 1.2(无老师参考题按 6 s),夹在 [plans, plans_max]
|
||||
exb = torch.nan_to_num(ex, nan=6.0)
|
||||
n_plans = int(min(C['plans_max'], max(C['plans'], math.ceil(1.2 * torch.quantile(exb, 0.9).item() / (3 * 0.02)))))
|
||||
nseg = math.ceil(n_plans / C['seg_plans'])
|
||||
seg_loss = torch.zeros((), device=dev); prev_d = torch.zeros(B, 6, device=dev)
|
||||
for p in range(C['plans']):
|
||||
for p in range(n_plans):
|
||||
tnow = p * 3 * 0.02
|
||||
r = queue.shape[1]
|
||||
pre = torch.zeros(B, 3, 6, device=dev); pre[:, :r] = queue[:, :r]
|
||||
@@ -92,15 +98,16 @@ def main():
|
||||
for k in range(3):
|
||||
d = queue[:, k]
|
||||
qp = q; q = kin.clamp_box(q + d)
|
||||
G = kin.gaps(q, M)
|
||||
# 碰撞铰链:端点 + 中点(与仿真/运行时的离散检查一致)
|
||||
G = kin.gaps(torch.cat([q, 0.5 * (q + qp)], 0), M)
|
||||
hinge = torch.relu(M - G['gap'])
|
||||
col = torch.zeros(B, device=dev).index_add(0, G['b'], hinge)
|
||||
col = torch.zeros(2 * B, device=dev).index_add(0, G['b'], hinge).view(2, B).sum(0)
|
||||
sm = ((d / step) ** 2).sum(-1) + (((d - prev_d) / step) ** 2).sum(-1)
|
||||
prev_d = d
|
||||
seg_loss = seg_loss + C['w_col'] * col.mean() + C['w_smooth'] * sm.mean() + C['w_len'] * d.abs().sum(-1).mean()
|
||||
stats['col'] += col.mean().item(); stats['smooth'] += sm.mean().item()
|
||||
queue = commit
|
||||
if (p + 1) % C['seg_plans'] == 0 or p == C['plans'] - 1:
|
||||
if (p + 1) % C['seg_plans'] == 0 or p == n_plans - 1:
|
||||
xf = kin.cum_xf(q)
|
||||
pe = kin.fk(q, xf); Rq = kin.orient(q, xf).reshape(B, 9)
|
||||
pose = (R.abs().sum(-1) > 0).float()
|
||||
@@ -122,7 +129,7 @@ def main():
|
||||
opt.step()
|
||||
with torch.no_grad():
|
||||
e, oe = goal_err(kin, q, T, R)
|
||||
rec = {'it': it, 'err_mm_mean': e.mean().item(), 'reach_frac': ((e < 1) & (oe < 0.5)).float().mean().item(), 'bc': l_bc.item(),
|
||||
rec = {'it': it, 'plans': n_plans, 'err_mm_mean': e.mean().item(), 'reach_frac': ((e < 1) & (oe < 0.5)).float().mean().item(), 'bc': l_bc.item(),
|
||||
'gnorm': gn.item(), **stats, 'min': (time.time() - t0) / 60}
|
||||
if it % 10 == 0: print(json.dumps(rec), flush=True)
|
||||
log.write(json.dumps(rec) + '\n'); log.flush()
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"apg": {
|
||||
"batch": 128, "lr": 1e-4, "steps": 3000, "grad_clip": 1.0, "amp": "bf16",
|
||||
"plans": 60, "seg_plans": 10,
|
||||
"margin_train_mm": 5.0,
|
||||
"plans": 60, "plans_max": 150, "seg_plans": 10,
|
||||
"margin_train_mm": 3.0,
|
||||
"w_tgt": 1.0, "w_rot": 0.3, "w_col": 2.0, "w_smooth": 0.02, "w_len": 0.001, "w_bc": 0.5,
|
||||
"cat_weights": {"A": 0.4, "B": 1.0, "C": 1.6, "D": 0.6, "Dr": 0.6},
|
||||
"eval_every": 500, "eval_tasks": 512
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"""验收汇总(方案 §9.2 + §10.1 R2/R7/R8/R10):把各阶段产物里的判据集中核对,输出 ml/runs/acceptance.json。
|
||||
python -m ml.eval.acceptance --ckpt ml/runs/current.pt --samples ml/data/samples --out ml/runs/acceptance.json
|
||||
自己跑的一项:held-out(val 划分)全部任务闭环仿真(开仿真安全层)——可达任务到达率 ≥ 98%、D 类停靠距离 ≤ 老师 + 5 mm、起步失败 = 0。
|
||||
其余项读已有文件(缺文件记为 missing,不算通过):
|
||||
ml/runs/policy_bench.json 部署形态:各集合穿模帧 = 0;near 30/30(pos、pose);route 全部到达;nc1000 0 失败;刹停率 < 2%;R8 恢复 ≥ 95%、活锁 = 0
|
||||
ml/data/r2.json R2 近邻标签 std < 10% 步长
|
||||
ml/runs/bench_*/log.jsonl R10 吞吐 ≥ 5000 样本/s(≈ 60000 步 ≤ 3.5 h)
|
||||
ml/runs/m5_gate_*.json M5 头门禁
|
||||
ml/runs/mine/r*/gate.json R7 最后一轮左右比例 / 行程门禁
|
||||
2026-10-18 新增(审查:§9 验收表在流水线里没有执行点)。"""
|
||||
import argparse, glob, json, os, re
|
||||
import torch
|
||||
|
||||
|
||||
def jload(p):
|
||||
try: return json.load(open(p, encoding='utf-8'))
|
||||
except Exception: return None
|
||||
|
||||
|
||||
def main():
|
||||
from ml.common.kin_torch import Kin
|
||||
from ml.model.policy import build
|
||||
from ml.data import load_tasks
|
||||
from ml.sim.closed_loop import evaluate
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--ckpt', required=True); ap.add_argument('--samples', default='ml/data/samples')
|
||||
ap.add_argument('--out', default='ml/runs/acceptance.json')
|
||||
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
|
||||
a = ap.parse_args()
|
||||
items = []
|
||||
def add(name, value, ok, rule):
|
||||
items.append({'item': name, 'value': value, 'pass': (None if ok is None else bool(ok)), 'rule': rule})
|
||||
|
||||
# 1) held-out 闭环(仿真安全层)
|
||||
ck = torch.load(a.ckpt, map_location=a.device)
|
||||
model = build(ck['cfg']).to(a.device); model.load_state_dict(ck['model']); model.eval()
|
||||
kin = Kin(device=a.device)
|
||||
val = [t for t in load_tasks([a.samples]) if t['split'] == 'val']
|
||||
reach = [t for t in val if t.get('reached') and t['cat'] != 'D']
|
||||
dock = [t for t in val if t['cat'] == 'D']
|
||||
if reach:
|
||||
r, _ = evaluate(model, kin, reach, a.device, safety=True)
|
||||
add('held-out 可达任务到达率', r['success'], r['success'] >= 0.98, '≥ 0.98(仿真安全层)')
|
||||
add('起步失败率(R2)', r['start_fail'], r['start_fail'] == 0, '= 0')
|
||||
else:
|
||||
add('held-out 可达任务到达率', None, False, 'val 中无可达任务')
|
||||
if dock:
|
||||
_, pers = evaluate(model, kin, dock, a.device, safety=True)
|
||||
err = torch.cat([p['err'] for _, p in pers])
|
||||
ref = torch.tensor([t.get('err') if t.get('err') is not None else float('inf') for t in dock])
|
||||
rate = (err <= ref + 5).float().mean().item()
|
||||
add('D 类停靠距离 ≤ 老师 + 5 mm 的比例', rate, rate >= 0.98, '≥ 0.98')
|
||||
|
||||
# 2) 部署形态
|
||||
pb = jload('ml/runs/policy_bench.json')
|
||||
if pb is None:
|
||||
add('部署形态评测', 'missing', False, '先跑 bash ml/pipeline.sh bench')
|
||||
else:
|
||||
sets = {k: v for k, v in pb.items() if isinstance(v, dict) and 'n' in v}
|
||||
cf = sum(v['collisionFrames'] for v in sets.values())
|
||||
add('部署形态穿模帧(全部集合)', cf, cf == 0, '= 0')
|
||||
for k in ('near_column_pos', 'near_column_pose', 'route40', 'near_column_1000'):
|
||||
if k in sets: add(f'{k} 通过', f"{sets[k]['ok']}/{sets[k]['n']}", sets[k]['ok'] == sets[k]['n'], '全部通过')
|
||||
else: add(f'{k} 通过', 'missing', False, '全部通过')
|
||||
plans = sum(v.get('plans', 0) for v in sets.values()); br = sum(v.get('brakes', 0) for v in sets.values())
|
||||
add('刹停发生率(刹停 / 推理)', br / plans if plans else None, (br / plans < 0.02) if plans else False, '< 0.02')
|
||||
r8 = sets.get('r8_adversarial')
|
||||
if r8:
|
||||
add('R8 对抗集 3 s 内恢复率', r8['recoveryRate'], r8['recoveryRate'] >= 0.95, '≥ 0.95')
|
||||
add('R8 活锁', r8['livelock'], r8['livelock'] == 0, '= 0')
|
||||
else: add('R8 对抗集', 'missing', False, 'bench 需含 r8')
|
||||
add('推理延迟 P99(Node CPU,参考)', max((v.get('inferMsP99') or 0) for v in sets.values()), None, '浏览器以 __POLICY.latency() 为准')
|
||||
|
||||
# 3) 数据 / 训练 / 后训练门禁
|
||||
r2 = jload('ml/data/r2.json')
|
||||
add('R2 近邻标签 std(步长比例)', r2['label_std_mean'] if r2 else 'missing', bool(r2 and r2['pass']), '< 0.10')
|
||||
sps = None
|
||||
for f in sorted(glob.glob('ml/runs/bench_*/log.jsonl')):
|
||||
recs = [json.loads(l) for l in open(f) if '"samples_per_s"' in l]
|
||||
if recs: sps = max(sps or 0, recs[-1]['samples_per_s'])
|
||||
add('R10 训练吞吐(样本/s)', sps, sps is not None and sps >= 5000, '≥ 5000(≈ 60000 步 ≤ 3.5 h)')
|
||||
m5 = [jload(f) for f in sorted(glob.glob('ml/runs/m5_gate_*.json'))]
|
||||
m5 = [m for m in m5 if m]
|
||||
add('M5 头门禁', [{'head': m['head'], 'collision_rate': m['policy_only']['collision_rate']} for m in m5] or 'missing',
|
||||
any(m['pass'] for m in m5), '纯策略贴立柱穿柱率 ≤ 0.02')
|
||||
rnum = lambda p: int((re.findall(r'[\\/]r(\d+)[\\/]gate\.json$', p) or ['0'])[0])
|
||||
gates = sorted(glob.glob('ml/runs/mine/r*/gate.json'), key=rnum)
|
||||
if gates:
|
||||
gj = jload(gates[-1])
|
||||
add('R7 左右比例变化(pp)/ 行程比', [gj.get('R7_left_share_shift_pp'), gj.get('R7_travel_median_ratio')], gj.get('R7_ok'), '≤ 5 pp 且 ≤ 1.05')
|
||||
else: add('R7 门禁', 'missing', False, '先跑 mine')
|
||||
|
||||
hard = [i for i in items if i['pass'] is not None]
|
||||
out = {'ckpt': a.ckpt, 'all_pass': all(i['pass'] for i in hard), 'failed': [i['item'] for i in hard if not i['pass']], 'items': items}
|
||||
os.makedirs(os.path.dirname(a.out) or '.', exist_ok=True)
|
||||
json.dump(out, open(a.out, 'w', encoding='utf-8'), indent=1, ensure_ascii=False)
|
||||
for i in items:
|
||||
mark = {True: '通过', False: '未过', None: '参考'}[i['pass']]
|
||||
print(f"[{mark}] {i['item']}: {i['value']} ({i['rule']})")
|
||||
print('全部通过' if out['all_pass'] else '未通过:' + '、'.join(out['failed']))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,46 @@
|
||||
"""方案 R2 数据侧检查:近邻样本的标签是否自洽(时间律不可观测成分会让相同输入对应不同动作)。
|
||||
python -m ml.eval.check_r2 --samples ml/data/samples --out ml/data/r2.json
|
||||
做法:特征逐维标准化;随机取 Q 个查询样本,在 P 个样本池里找最近邻(排除同一任务,避免同轨迹相邻帧冒充"近邻");
|
||||
取距离最近的 5% 近邻对,比较前 3 步(实际提交的动作)标签:std ≈ RMS(ΔY)/√2,单位为每步最大步长 STEP 的比例。
|
||||
判据:label_std < 0.10(即 < 10% 步长)。起步失败率在闭环仿真结果的 start_fail 字段(acceptance 汇总)。
|
||||
2026-10-18 新增(审查:R2 无执行点)。GPU 上约 1 分钟;CPU 上把 --pool 降到 50000。"""
|
||||
import argparse, json, math
|
||||
import torch
|
||||
|
||||
from ml.data import Samples
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--samples', nargs='+', default=['ml/data/samples']); ap.add_argument('--out', default='ml/data/r2.json')
|
||||
ap.add_argument('--queries', type=int, default=20000); ap.add_argument('--pool', type=int, default=200000)
|
||||
ap.add_argument('--frac', type=float, default=0.05); ap.add_argument('--seed', type=int, default=0)
|
||||
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
|
||||
a = ap.parse_args()
|
||||
d = Samples(a.samples, device=a.device, split='train')
|
||||
g = torch.Generator(device=a.device); g.manual_seed(a.seed)
|
||||
P = min(a.pool, d.n); Q = min(a.queries, d.n)
|
||||
pi = torch.randperm(d.n, device=a.device, generator=g)[:P]; qi = torch.randperm(d.n, device=a.device, generator=g)[:Q]
|
||||
X = d.X.float(); mu, sd = X[pi].mean(0), X[pi].std(0).clamp(min=1e-6)
|
||||
Xp, Xq = (X[pi] - mu) / sd, (X[qi] - mu) / sd
|
||||
tp, tq = d.TID[pi], d.TID[qi]
|
||||
best_d = torch.empty(Q, device=a.device); best_j = torch.empty(Q, dtype=torch.long, device=a.device)
|
||||
for s in range(0, Q, 1024):
|
||||
D = torch.cdist(Xq[s:s + 1024], Xp)
|
||||
D = torch.where(tq[s:s + 1024, None] == tp[None], torch.full_like(D, float('inf')), D)
|
||||
v, j = D.min(1); best_d[s:s + 1024] = v; best_j[s:s + 1024] = j
|
||||
fin = torch.isfinite(best_d)
|
||||
thr = torch.quantile(best_d[fin], a.frac)
|
||||
m = fin & (best_d <= thr)
|
||||
Yq, Yp = d.Y[qi][m, :3].float(), d.Y[pi][best_j[m], :3].float()
|
||||
rms = (Yq - Yp).pow(2).mean(-1).mean(-1).sqrt() # 每对、6 轴 × 3 步平均
|
||||
std = (rms / math.sqrt(2))
|
||||
out = {'pairs': int(m.sum()), 'nn_dist_threshold': thr.item(), 'label_std_mean': std.mean().item(),
|
||||
'label_std_p90': torch.quantile(std, 0.9).item(), 'pass': std.mean().item() < 0.10,
|
||||
'rule': 'label_std_mean < 0.10(10% 步长)'}
|
||||
print(json.dumps(out, indent=1))
|
||||
json.dump(out, open(a.out, 'w'), indent=1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+3
-2
@@ -3,7 +3,7 @@
|
||||
const fs = require('fs');
|
||||
const files = process.argv.slice(2);
|
||||
const by = {}; let total = 0, keep = 0, steps = 0, samples = 0, padTail = 0;
|
||||
const q = a => { a.sort((x, y) => x - y); return a.length ? { p50: +a[a.length >> 1].toFixed(1), p90: +a[Math.floor(a.length * 0.9)].toFixed(1), max: +a[a.length - 1].toFixed(1) } : null; };
|
||||
const q = a => { a.sort((x, y) => x - y); return a.length ? { p10: +a[Math.floor(a.length * 0.1)].toFixed(1), p50: +a[a.length >> 1].toFixed(1), p90: +a[Math.floor(a.length * 0.9)].toFixed(1), max: +a[a.length - 1].toFixed(1) } : null; };
|
||||
const plan = [], certMs = [], travel = [], exec = [], gaps = [];
|
||||
for (const f of files) for (const line of fs.readFileSync(f, 'utf8').split('\n')) {
|
||||
if (!line) continue; const r = JSON.parse(line); total++;
|
||||
@@ -17,5 +17,6 @@ for (const f of files) for (const line of fs.readFileSync(f, 'utf8').split('\n')
|
||||
const out = { tasks: total, keep, keepRate: +(keep / total).toFixed(3), byCatMode: by,
|
||||
planMs: q(plan), certMs: q(certMs), vertexMinGapMm: q(gaps), travelDeg: q(travel), execS: q(exec),
|
||||
samplesEvery3: samples, samplesPerTask: +(samples / keep).toFixed(1), paddedSampleShare: +(padTail / samples).toFixed(3),
|
||||
projectedSamplesFor50k: Math.round(samples / total * 50000) };
|
||||
projectedSamplesFor50k: Math.round(samples / total * 50000),
|
||||
consensusDrop: Object.values(by).reduce((a, b) => a + b.consensusDrop, 0) };
|
||||
console.log(JSON.stringify(out, null, 1));
|
||||
|
||||
+18
-4
@@ -1,4 +1,6 @@
|
||||
"""门禁(方案 §7.2 第 6 步):新模型须 ①门禁集到达率不降 ②A 类降幅 ≤ 0.5 个百分点 ③上一轮修好的错题 95% 仍对。
|
||||
"""门禁(方案 §7.2 第 6 步):新模型须 ①门禁集到达率不降 ②A 类降幅 ≤ 0.5 个百分点 ③上一轮修好的错题 95% 仍对
|
||||
④R7:B/C 类(需绕行)左右比例变化 ≤ 5 个百分点 ⑤R7:两者都到达的题行程中位数比 ≤ 1.05。
|
||||
门禁集为无老师题,到达按定长 6 s 口径(见 closed_loop.NOREF_SEC)。
|
||||
python -m ml.mining.gate --old cur.pt --new cand.pt --tasks ml/runs/gate_tasks.jsonl --out gate.json [--fixed prev_fixed.jsonl]
|
||||
通过则把 cand 复制为 --current(默认 ml/runs/current.pt)。gate.json 含 accept / gain_pp,loop.sh 据此决定继续或停止。"""
|
||||
import argparse, json, shutil
|
||||
@@ -38,15 +40,27 @@ def main():
|
||||
if ft:
|
||||
r2, p2 = evaluate(load(a.new, a.device), kin, ft, a.device)
|
||||
keep = sum(per_task_success(p2)) / len(ft)
|
||||
accept = gain >= 0 and a_drop <= 0.5 and (keep is None or keep >= 0.95)
|
||||
out = {'old': ro, 'new': rn, 'gain_pp': gain, 'A_drop_pp': a_drop, 'fixed_kept': keep, 'accept': accept}
|
||||
# R7:左右比例与行程
|
||||
cat = [t['cat'] for t in tasks]
|
||||
so_side = torch.cat([p['side'] for _, p in po]); sn_side = torch.cat([p['side'] for _, p in pn])
|
||||
det = torch.tensor([c in ('B', 'C') for c in cat])
|
||||
left = lambda sd, ok: (((sd > 0) & det & ok).sum() / ((det & ok).sum().clamp(min=1))).item()
|
||||
ok_o, ok_n = torch.tensor(so, dtype=torch.bool), torch.tensor(sn, dtype=torch.bool)
|
||||
lr_shift = abs(left(sn_side, ok_n) - left(so_side, ok_o)) * 100
|
||||
both = ok_o & ok_n
|
||||
tro = torch.cat([p['travel'] for _, p in po]); trn = torch.cat([p['travel'] for _, p in pn])
|
||||
travel_ratio = (trn[both].median() / tro[both].median().clamp(min=1e-6)).item() if both.any() else None
|
||||
r7 = lr_shift <= 5.0 and (travel_ratio is None or travel_ratio <= 1.05)
|
||||
accept = gain >= 0 and a_drop <= 0.5 and (keep is None or keep >= 0.95) and r7
|
||||
out = {'old': ro, 'new': rn, 'gain_pp': gain, 'A_drop_pp': a_drop, 'fixed_kept': keep,
|
||||
'R7_left_share_shift_pp': lr_shift, 'R7_travel_median_ratio': travel_ratio, 'R7_ok': r7, 'accept': accept}
|
||||
# 本轮新修好的题(旧错新对)留给下一轮做"仍对"检查
|
||||
fixed_now = [t for t, x, y in zip(tasks, so, sn) if (not x) and y]
|
||||
with open(a.out.replace('.json', '_fixed.jsonl'), 'w') as f:
|
||||
for t in fixed_now: f.write(json.dumps(t) + '\n')
|
||||
json.dump(out, open(a.out, 'w'), indent=1)
|
||||
if accept: shutil.copy(a.new, a.current)
|
||||
print(json.dumps({k: out[k] for k in ('gain_pp', 'A_drop_pp', 'fixed_kept', 'accept')}))
|
||||
print(json.dumps({k: out[k] for k in ('gain_pp', 'A_drop_pp', 'fixed_kept', 'R7_left_share_shift_pp', 'R7_travel_median_ratio', 'accept')}))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
+9
-7
@@ -1,29 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# 后训练主循环(方案 §7):APG 首轮 → 挖错 3–6 轮(每轮:挖错 → [E2 老师补路径] → APG 微调 → 门禁/回滚)
|
||||
# bash ml/mining/loop.sh [轮数=6] [每轮 APG 步数=1000] [每轮出题数=50000]
|
||||
# 2026-10-18:APG 采样池只用本轮错题(此前累积所有历史轮,后期分布偏向老错题);老错题的保持由门禁"上轮修好仍对"负责。
|
||||
# 统一 $PYTHON;门禁加 R7(左右比例 / 行程)。
|
||||
set -e
|
||||
cd "$(dirname "$0")/../.."
|
||||
PY=${PYTHON:-python}
|
||||
ROUNDS=${1:-6}; STEPS=${2:-1000}; N=${3:-50000}
|
||||
mkdir -p ml/runs/mine
|
||||
[ -f ml/runs/gate_tasks.jsonl ] || python -m ml.mining.mine_round --make-gate 2000 --seed 777 --out ml/runs
|
||||
[ -f ml/runs/gate_tasks.jsonl ] || $PY -m ml.mining.mine_round --make-gate 2000 --seed 777 --out ml/runs
|
||||
[ -f ml/runs/current.pt ] || cp ml/runs/apg/best.pt ml/runs/current.pt
|
||||
SAMPLES="ml/data/samples"; FIXED=""; small=0
|
||||
for ((r=1; r<=ROUNDS; r++)); do
|
||||
D=ml/runs/mine/r$r; mkdir -p $D
|
||||
python -m ml.mining.mine_round --ckpt ml/runs/current.pt --seed $((1000+r)) --n $N --out $D
|
||||
$PY -m ml.mining.mine_round --ckpt ml/runs/current.pt --seed $((1000+r)) --n $N --out $D
|
||||
# E2(局部极小)请老师补少量路径(唯一保留的后训练老师用途)
|
||||
if [ -s $D/e2_tasks.jsonl ]; then
|
||||
node ml/gen/gen_dataset.cjs --tasks $D/e2_tasks.jsonl --seed $((5000+r)) --out ml/data/e2/shard_e2_r$r.jsonl
|
||||
node ml/prep/build_dataset.cjs ml/data/e2/shard_e2_r$r.jsonl ml/data/samples_e2
|
||||
SAMPLES="ml/data/samples ml/data/samples_e2"
|
||||
fi
|
||||
POOLS=$(ls ml/runs/mine/r*/pool.jsonl)
|
||||
python -m ml.apg.apg_finetune --init ml/runs/current.pt --samples $SAMPLES --pool $POOLS --out ml/runs/apg_r$r --steps $STEPS
|
||||
python -m ml.mining.gate --old ml/runs/current.pt --new ml/runs/apg_r$r/best.pt --tasks ml/runs/gate_tasks.jsonl --out $D/gate.json $FIXED
|
||||
$PY -m ml.apg.apg_finetune --init ml/runs/current.pt --samples $SAMPLES --pool $D/pool.jsonl --out ml/runs/apg_r$r --steps $STEPS
|
||||
$PY -m ml.mining.gate --old ml/runs/current.pt --new ml/runs/apg_r$r/best.pt --tasks ml/runs/gate_tasks.jsonl --out $D/gate.json $FIXED
|
||||
FIXED="--fixed $D/gate_fixed.jsonl"
|
||||
gain=$(python -c "import json;print(json.load(open('$D/gate.json'))['gain_pp'])")
|
||||
gain=$($PY -c "import json;print(json.load(open('$D/gate.json'))['gain_pp'])")
|
||||
echo "round $r gain ${gain} pp"
|
||||
if python -c "import sys;sys.exit(0 if $gain < 0.5 else 1)"; then small=$((small+1)); else small=0; fi
|
||||
if $PY -c "import sys;sys.exit(0 if $gain < 0.5 else 1)"; then small=$((small+1)); else small=0; fi
|
||||
if [ $small -ge 2 ]; then echo "连续 2 轮提升 < 0.5 pp,停止"; break; fi
|
||||
done
|
||||
echo "最终模型:ml/runs/current.pt"
|
||||
|
||||
+12
-5
@@ -1,20 +1,27 @@
|
||||
"""无老师出题(PyTorch):A 均匀 / B 直线被挡 / C 贴立柱 / D 工作空间外;pos/pose 各半。"""
|
||||
"""无老师出题(PyTorch):A 均匀 / B 直线被挡 / C 贴立柱 / D 工作空间外;pos/pose 各半。
|
||||
2026-10-18:无老师题没有参考时长与行程,exec_s/travel 置 None(此前写死 3.0/0.0 造成假时长预算);
|
||||
闭环仿真对这类题按定长 6 s 口径判到达,time_ok_1p3 / travel_ratio 只在有老师参考的题上统计。"""
|
||||
import math
|
||||
import torch
|
||||
|
||||
from ml.common.features import wrap180
|
||||
|
||||
|
||||
def min_gap_chunked(kin, q, cap=0.5, chunk=512):
|
||||
"""分块算最小间隙:随机位形每个约 2800 个幸存胶囊对、峰值约 1.5 MB/位形,一次 4 万位形会爆内存/显存(2026-10-18 实测出题 OOM),每块 512"""
|
||||
return torch.cat([kin.gaps(q[i:i + chunk], cap)['gm'].min(-1).values for i in range(0, q.shape[0], chunk)])
|
||||
|
||||
|
||||
def rand_q(kin, n, g):
|
||||
u = torch.rand(n, 6, device=kin.lo.device, generator=g)
|
||||
return kin.lo + u * (kin.hi - kin.lo)
|
||||
|
||||
|
||||
def free_q(kin, n, g, chunk=4096):
|
||||
def free_q(kin, n, g, chunk=1024):
|
||||
out = []
|
||||
while sum(x.shape[0] for x in out) < n:
|
||||
q = rand_q(kin, chunk, g)
|
||||
ok = kin.gaps(q, 0.5)['gm'].min(-1).values > 0
|
||||
ok = min_gap_chunked(kin, q) > 0
|
||||
out.append(q[ok])
|
||||
return torch.cat(out)[:n]
|
||||
|
||||
@@ -23,7 +30,7 @@ def straight_blocked(kin, a, b, n=24):
|
||||
d = torch.where(kin.cont, wrap180(b - a), b - a)
|
||||
s = torch.linspace(0, 1, n, device=a.device)
|
||||
pts = (a[:, None] + s[None, :, None] * d[:, None]).reshape(-1, 6)
|
||||
return (kin.gaps(pts, 0.5)['gm'].min(-1).values <= 0).view(a.shape[0], n).any(-1)
|
||||
return (min_gap_chunked(kin, pts) <= 0).view(a.shape[0], n).any(-1)
|
||||
|
||||
|
||||
def column_gap(kin, q):
|
||||
@@ -44,7 +51,7 @@ def sample_tasks(kin, n, seed, mix=(0.3, 0.3, 0.3, 0.1)):
|
||||
Rb = kin.orient(b, xf).reshape(-1, 9)
|
||||
for i in range(q0.shape[0]):
|
||||
tasks.append({'cat': cat, 'mode': 'pose' if pose[i] else 'pos', 'q0': q0[i].tolist(), 'T': Tb[i].tolist(),
|
||||
'R': Rb[i].tolist() if pose[i] else None, 'exec_s': 3.0, 'travel': 0.0})
|
||||
'R': Rb[i].tolist() if pose[i] else None, 'exec_s': None, 'travel': None})
|
||||
if want[0]:
|
||||
emit('A', free_q(kin, want[0], g), free_q(kin, want[0], g))
|
||||
for cat, k in (('B', want[1]), ('C', want[2])):
|
||||
|
||||
+66
-27
@@ -3,49 +3,88 @@
|
||||
# setup 安装 Python/Node 依赖(torch 需先按 CUDA 版本装好)
|
||||
# kin 导出运动学/碰撞模型 + JS↔PyTorch 对拍(必须 PASS)
|
||||
# gate1 老师一致性门禁(决定生成时是否加 --consensus 4)
|
||||
# gen 生成 5 万条老师轨迹(NPROC 进程;CONSENSUS=1 时加共识过滤)
|
||||
# prep 打包训练样本
|
||||
# train 预训练(先 2000 步摸吞吐,再正式训练)
|
||||
# m5 MeanFlow 头门禁:贴立柱集关安全层穿柱率 ≤ 2%(不过 → 用 configs/wta.json 重训)
|
||||
# apg APG 首轮微调
|
||||
# gen 生成老师轨迹(NPROC 进程;CONSENSUS=1 时加共识过滤;样本不足 TARGET_SAMPLES 自动补生成)
|
||||
# prep 打包训练样本 + R2 标签一致性检查
|
||||
# train 预训练(先 2000 步摸吞吐,再正式训练);输出目录随头分开:ml/runs/<CFG 名>
|
||||
# m5 头门禁:贴立柱集穿柱率 ≤ 2%(纯策略口径),同时报仿真安全层口径;通过则记为选定模型
|
||||
# apg APG 首轮微调(从选定模型开始)
|
||||
# mine 挖错循环(3–6 轮,含门禁回滚)
|
||||
# export 导出 ONNX(fp16)到 webapp/models/
|
||||
# bench 部署形态评测(Node,策略 + 安全层)
|
||||
# all 依次执行 kin → bench
|
||||
# export 导出 ONNX(fp32)到 webapp/models/
|
||||
# bench 部署形态评测(Node,策略 + 安全层,含 R8 对抗集)
|
||||
# accept 验收汇总(方案 §9.2 各项 + R2/R7/R8/R10)→ ml/runs/acceptance.json
|
||||
# all 依次执行 kin → accept
|
||||
# 可调环境变量:PYTHON(默认 python;Linux 无 python 时设 PYTHON=python3)、NPROC、N_TASKS、TARGET_SAMPLES、
|
||||
# CFG(ml/configs/base.json | wta.json)、CKPT(覆盖后续阶段的起点模型)、CONSENSUS
|
||||
# 2026-10-18:按另一台机器的审查改——头分目录不互相覆盖、统一 $PYTHON(Windows Git Bash 可跑)、
|
||||
# gen 自动补量、m5 双口径、export 默认 fp32、新增 accept。
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
PY=${PYTHON:-python}
|
||||
command -v "$PY" >/dev/null 2>&1 && "$PY" -c "import sys" 2>/dev/null || { echo "找不到可用的 Python($PY),请设 PYTHON=..."; exit 1; }
|
||||
export PYTHON=$PY
|
||||
NPROC=${NPROC:-$(nproc)}
|
||||
N_TASKS=${N_TASKS:-50000}
|
||||
TARGET_SAMPLES=${TARGET_SAMPLES:-1700000}
|
||||
CFG=${CFG:-ml/configs/base.json}
|
||||
TAG=$(basename "$CFG" .json)
|
||||
SEL=ml/runs/selected_ckpt
|
||||
ckpt() { if [ -n "${CKPT:-}" ]; then echo "$CKPT"; elif [ -f $SEL ]; then cat $SEL; else echo ml/runs/$TAG/best.pt; fi; }
|
||||
stage=${1:-help}
|
||||
run() { echo "== $*"; "$@"; }
|
||||
case $stage in
|
||||
setup) run pip install -r ml/requirements.txt; (cd ml && npm install --no-audit --no-fund) ;;
|
||||
kin) run node ml/prep/export_kin.cjs; run node ml/eval/dump_parity.cjs 2000; run python -m ml.eval.check_parity ;;
|
||||
setup) run $PY -m pip install -r ml/requirements.txt; (cd ml && npm install --no-audit --no-fund) ;;
|
||||
kin) run node ml/prep/export_kin.cjs; run node ml/eval/dump_parity.cjs 2000; run $PY -m ml.eval.check_parity ;;
|
||||
gate1) mkdir -p ml/data; run node ml/gen/consistency.cjs 300 4242 | tee ml/data/consistency.json ;;
|
||||
gen) per=$(( (N_TASKS + NPROC - 1) / NPROC ))
|
||||
if [ -z "${CONSENSUS:-}" ] && [ -f ml/data/consistency.json ]; then
|
||||
CONSENSUS=$(python -c "import json;print(1 if json.load(open('ml/data/consistency.json'))['consistentByGateRule']<0.9 else 0)")
|
||||
CONSENSUS=$($PY -c "import json;print(1 if json.load(open('ml/data/consistency.json'))['consistentByGateRule']<0.9 else 0)")
|
||||
echo "一致性门禁结果 → CONSENSUS=$CONSENSUS"
|
||||
fi
|
||||
extra=""; [ "${CONSENSUS:-0}" = "1" ] && extra="--consensus 4"
|
||||
run bash ml/gen/run_parallel.sh $per $NPROC 1 "$extra" ;;
|
||||
prep) run bash ml/prep/run_prep.sh $NPROC ;;
|
||||
train) run python -m ml.train --samples ml/data/samples --out ml/runs/bench --steps 2000 --no-eval --config $CFG
|
||||
run python -m ml.train --samples ml/data/samples --out ml/runs/base --config $CFG ;;
|
||||
m5) [ -f ml/runs/nc_gate.jsonl ] || run python -m ml.mining.mine_round --make-gate 1000 --seed 4321 --mix 0,0,1,0 --name nc_gate.jsonl --out ml/runs
|
||||
run python -m ml.sim.closed_loop --ckpt ml/runs/base/best.pt --tasks ml/runs/nc_gate.jsonl --n 1000 --out ml/runs/m5_gate.json
|
||||
python - <<'PY'
|
||||
import json; r = json.load(open('ml/runs/m5_gate.json'))
|
||||
print('贴立柱集(关安全层)穿柱率 %.2f%%,到达率 %.2f%%' % (100 * r['collision_rate'], 100 * r['success']))
|
||||
print('M5 门禁:' + ('通过' if r['collision_rate'] <= 0.02 else '不通过 → CFG=ml/configs/wta.json bash ml/pipeline.sh train'))
|
||||
run bash ml/gen/run_parallel.sh $per $NPROC 1 "$extra"
|
||||
# 补量:共识过滤会降低保留率,样本不足 TARGET_SAMPLES 时按实测"样本/任务"补生成(最多 2 次)
|
||||
s0=$((1 + NPROC))
|
||||
for k in 1 2; do
|
||||
need=$($PY -c "import json,math;s=json.load(open('ml/data/stats.json'));d=$TARGET_SAMPLES-s['samplesEvery3'];print(0 if d<=0 else math.ceil(d/(s['samplesEvery3']/s['tasks'])*1.05))")
|
||||
[ "$need" = "0" ] && break
|
||||
echo "样本不足目标 $TARGET_SAMPLES,补生成 $need 条任务"
|
||||
run bash ml/gen/run_parallel.sh $(( (need + NPROC - 1) / NPROC )) $NPROC $s0 "$extra"
|
||||
s0=$((s0 + NPROC))
|
||||
done ;;
|
||||
prep) run bash ml/prep/run_prep.sh $NPROC
|
||||
run $PY -m ml.eval.check_r2 --samples ml/data/samples --out ml/data/r2.json ;;
|
||||
train) run $PY -m ml.train --samples ml/data/samples --out ml/runs/bench_$TAG --steps 2000 --no-eval --config $CFG
|
||||
run $PY -m ml.train --samples ml/data/samples --out ml/runs/$TAG --config $CFG ;;
|
||||
m5) [ -f ml/runs/nc_gate.jsonl ] || run $PY -m ml.mining.mine_round --make-gate 1000 --seed 4321 --mix 0,0,1,0 --name nc_gate.jsonl --out ml/runs
|
||||
ck=${CKPT:-ml/runs/$TAG/best.pt}
|
||||
run $PY -m ml.sim.closed_loop --ckpt $ck --tasks ml/runs/nc_gate.jsonl --n 1000 --out ml/runs/m5_${TAG}_policy.json
|
||||
run $PY -m ml.sim.closed_loop --ckpt $ck --tasks ml/runs/nc_gate.jsonl --n 1000 --safety --out ml/runs/m5_${TAG}_safety.json
|
||||
$PY - "$TAG" "$ck" <<'PY'
|
||||
import json, sys
|
||||
tag, ck = sys.argv[1], sys.argv[2]
|
||||
p = json.load(open(f'ml/runs/m5_{tag}_policy.json')); s = json.load(open(f'ml/runs/m5_{tag}_safety.json'))
|
||||
ok = p['collision_rate'] <= 0.02
|
||||
out = {'head': tag, 'ckpt': ck, 'pass': ok,
|
||||
'policy_only': {k: p[k] for k in ('n', 'success', 'collision_rate', 'start_fail')},
|
||||
'with_sim_safety': {k: s[k] for k in ('n', 'success', 'collision_rate', 'brakes_mean', 'start_fail')},
|
||||
'note': '门禁只看 policy_only(纯策略穿柱率);with_sim_safety 为仿真近似安全层,部署口径以 bench 为准'}
|
||||
json.dump(out, open(f'ml/runs/m5_gate_{tag}.json', 'w'), indent=1)
|
||||
print('[%s] 纯策略穿柱率 %.2f%%、到达 %.2f%%;加仿真安全层 穿柱 %.2f%%、到达 %.2f%%' % (
|
||||
tag, 100 * p['collision_rate'], 100 * p['success'], 100 * s['collision_rate'], 100 * s['success']))
|
||||
if ok:
|
||||
open('ml/runs/selected_ckpt', 'w').write(ck); print('M5 门禁:通过 → 后续阶段使用', ck)
|
||||
else:
|
||||
print('M5 门禁:不通过' + (' → CFG=ml/configs/wta.json bash ml/pipeline.sh train && CFG=ml/configs/wta.json bash ml/pipeline.sh m5' if tag != 'wta' else '(两种头都未过,把 m5_gate_*.json 发我)'))
|
||||
sys.exit(3)
|
||||
PY
|
||||
;;
|
||||
apg) run python -m ml.apg.apg_finetune --init ml/runs/base/best.pt --samples ml/data/samples --out ml/runs/apg; cp ml/runs/apg/best.pt ml/runs/current.pt ;;
|
||||
apg) ck=$(ckpt); run $PY -m ml.apg.apg_finetune --init $ck --samples ml/data/samples --out ml/runs/apg; cp ml/runs/apg/best.pt ml/runs/current.pt ;;
|
||||
mine) run bash ml/mining/loop.sh ${ROUNDS:-6} ${APG_STEPS:-1000} ${MINE_N:-50000} ;;
|
||||
export) ck=ml/runs/current.pt; [ -f $ck ] || ck=ml/runs/base/best.pt
|
||||
run python -m ml.export_onnx --ckpt $ck --out webapp/models/traj_policy.onnx --fp16 ;;
|
||||
bench) run node tests/policy_bench.cjs webapp/models/traj_policy.onnx nc,route --nc1000 --out ml/runs/policy_bench.json ;;
|
||||
all) for s in kin gate1 gen prep train m5 apg mine export bench; do bash ml/pipeline.sh $s; done ;;
|
||||
*) sed -n '2,16p' "$0" ;;
|
||||
export) ck=ml/runs/current.pt; [ -f $ck ] || ck=$(ckpt)
|
||||
run $PY -m ml.export_onnx --ckpt $ck --out webapp/models/traj_policy.onnx ;;
|
||||
bench) run node tests/policy_bench.cjs webapp/models/traj_policy.onnx nc,route,r8 --nc1000 --out ml/runs/policy_bench.json ;;
|
||||
accept) ck=ml/runs/current.pt; [ -f $ck ] || ck=$(ckpt)
|
||||
run $PY -m ml.eval.acceptance --ckpt $ck --samples ml/data/samples --out ml/runs/acceptance.json ;;
|
||||
all) for s in kin gate1 gen prep train m5 apg mine export bench accept; do bash ml/pipeline.sh $s; done ;;
|
||||
*) sed -n '2,20p' "$0" ;;
|
||||
esac
|
||||
|
||||
@@ -34,7 +34,7 @@ for (const line of lines) {
|
||||
const P = r.path;
|
||||
const tp = timeParam(P);
|
||||
const traj = tp.traj, taus = tp.taus, N = traj.length - 1;
|
||||
tasks.push({ tid, id: r.id, cat: r.cat, mode: r.mode, q0: r.q0, T: r.T, R: r.R, exec_s: N * POL.DT, travel: r.travel_deg, reached: r.reached, split });
|
||||
tasks.push({ tid, id: r.id, cat: r.cat, mode: r.mode, q0: r.q0, T: r.T, R: r.R, exec_s: N * POL.DT, travel: r.travel_deg, reached: r.reached, err: r.err, split });
|
||||
const vt = tp.cum; // 顶点 τ
|
||||
const inter = P.slice(1, -1).map((p, i) => ({ p, tau: vt[i + 1] }));
|
||||
for (let t = 0; t <= N; t += 3) {
|
||||
|
||||
+1
-1
@@ -5,4 +5,4 @@ cd "$(dirname "$0")/../.."
|
||||
P=${1:-$(nproc)}
|
||||
mkdir -p ml/data/samples
|
||||
ls ml/data/shard_*.jsonl | xargs -P "$P" -I{} node ml/prep/build_dataset.cjs {} ml/data/samples
|
||||
python3 -m ml.data --index ml/data/samples
|
||||
${PYTHON:-python} -m ml.data --index ml/data/samples
|
||||
|
||||
+31
-9
@@ -17,11 +17,15 @@ def tasks_to_tensors(tasks, device):
|
||||
q0 = torch.tensor([t['q0'] for t in tasks], dtype=torch.float32, device=device)
|
||||
T = torch.tensor([t['T'] for t in tasks], dtype=torch.float32, device=device)
|
||||
R = torch.tensor([t['R'] if t.get('R') else [0.] * 9 for t in tasks], dtype=torch.float32, device=device)
|
||||
ex = torch.tensor([t.get('exec_s', 3.0) or 3.0 for t in tasks], dtype=torch.float32, device=device)
|
||||
tr = torch.tensor([t.get('travel', 0.0) or 0.0 for t in tasks], dtype=torch.float32, device=device)
|
||||
# 无老师参考(挖错/门禁题)时 exec_s/travel 为 None → NaN;调用方据此区分口径
|
||||
ex = torch.tensor([t['exec_s'] if t.get('exec_s') else float('nan') for t in tasks], dtype=torch.float32, device=device)
|
||||
tr = torch.tensor([t['travel'] if t.get('travel') else float('nan') for t in tasks], dtype=torch.float32, device=device)
|
||||
return q0, T, R, ex, tr
|
||||
|
||||
|
||||
NOREF_SEC = 6.0 # 无老师参考题的定长时限(s)
|
||||
|
||||
|
||||
def goal_err(kin, q, T, R):
|
||||
xf = kin.cum_xf(q)
|
||||
p = kin.fk(q, xf); Rq = kin.orient(q, xf).reshape(-1, 9)
|
||||
@@ -64,7 +68,8 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
|
||||
q0, T, R, ex, tr = tasks_to_tensors(tasks, device)
|
||||
B = q0.shape[0]
|
||||
step = torch.tensor(STEP, device=device)
|
||||
limit = torch.clamp(2 * ex, min=3.0) if max_sec is None else torch.full_like(ex, max_sec)
|
||||
has_ref = torch.isfinite(ex)
|
||||
limit = torch.where(has_ref, torch.clamp(2 * ex, min=3.0), torch.full_like(ex, NOREF_SEC)) if max_sec is None else torch.full_like(ex, max_sec)
|
||||
nmax = int(math.ceil(limit.max().item() / DT))
|
||||
q, qp = q0.clone(), q0.clone()
|
||||
queue = torch.zeros(B, 0, 6, device=device)
|
||||
@@ -73,6 +78,8 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
|
||||
done = torch.zeros(B, dtype=torch.bool, device=device); t_done = torch.full((B,), float('nan'), device=device)
|
||||
col = torch.zeros(B, device=device); travel = torch.zeros(B, device=device); brakes = torch.zeros(B, device=device)
|
||||
traj = [q.clone()] if return_traj else None
|
||||
n_start = int(round(0.5 / DT)); moved_start = None # R2 起步失败:0.5 s 内关节总行程 < 1°
|
||||
j1_acc = torch.zeros(B, device=device); j1_max = torch.zeros(B, device=device); j1_min = torch.zeros(B, device=device)
|
||||
cold = True
|
||||
for it in range(nmax):
|
||||
tnow = it * DT
|
||||
@@ -138,6 +145,8 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
|
||||
d = torch.where(done[:, None], torch.zeros_like(d), d)
|
||||
qp = q; q = kin.clamp_box(q + d)
|
||||
travel += d.abs().sum(-1)
|
||||
j1_acc = j1_acc + d[:, 0]; j1_max = torch.maximum(j1_max, j1_acc); j1_min = torch.minimum(j1_min, j1_acc)
|
||||
if it + 1 == n_start: moved_start = travel.clone()
|
||||
if return_traj: traj.append(q.clone())
|
||||
mid = qp + 0.5 * (q - qp)
|
||||
gm = kin.gaps(torch.cat([q, mid], 0), 0.5)['gm'].min(-1).values
|
||||
@@ -148,26 +157,39 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
|
||||
t_done = torch.where(newly, torch.full_like(t_done, (it + 1) * DT), t_done); done |= newly
|
||||
if done.all(): break
|
||||
ok = done & (t_done <= limit)
|
||||
res = {'n': B, 'success': ok.float().mean().item(),
|
||||
'time_ok_1p3': (ok & (t_done <= 1.3 * ex)).float().mean().item(),
|
||||
if moved_start is None: moved_start = travel.clone()
|
||||
e_end, _ = goal_err(kin, q, T, R)
|
||||
start_fail = (moved_start < 1.0) & ~(done & (t_done <= 0.5))
|
||||
okr = ok & has_ref & torch.isfinite(tr)
|
||||
res = {'n': B, 'n_ref': int(has_ref.sum()), 'success': ok.float().mean().item(),
|
||||
'time_ok_1p3': (ok & has_ref & (t_done <= 1.3 * ex)).float().sum().item() / max(1, int(has_ref.sum())) if has_ref.any() else None,
|
||||
'collision_rate': (col > 0).float().mean().item(), 'collision_steps_mean': col.mean().item(),
|
||||
'travel_ratio_median': (travel[ok] / tr[ok].clamp(min=1)).median().item() if ok.any() and (tr > 0).any() else None,
|
||||
'time_mean_s': t_done[ok].mean().item() if ok.any() else None, 'brakes_mean': brakes.mean().item()}
|
||||
per = {'done': done.cpu(), 't_done': t_done.cpu(), 'col': col.cpu(), 'travel': travel.cpu()}
|
||||
'travel_ratio_median': (travel[okr] / tr[okr].clamp(min=1)).median().item() if okr.any() else None,
|
||||
'time_mean_s': t_done[ok].mean().item() if ok.any() else None, 'brakes_mean': brakes.mean().item(),
|
||||
'start_fail': start_fail.float().mean().item(),
|
||||
'time_budget': 'teacher×2(≥3 s)' if bool(has_ref.all()) else ('fixed %.0f s' % (max_sec or NOREF_SEC) if not has_ref.any() else 'mixed')}
|
||||
# 绕行侧:J1 累计位移的正向/负向极值哪个更大(+1 / −1),供 R7 左右比例门禁
|
||||
side = torch.where(j1_max >= -j1_min, torch.ones_like(j1_max), -torch.ones_like(j1_max))
|
||||
per = {'done': done.cpu(), 't_done': t_done.cpu(), 'col': col.cpu(), 'travel': travel.cpu(), 'err': e_end.cpu(),
|
||||
'side': side.cpu(), 'start_fail': start_fail.cpu(), 'brakes': brakes.cpu()}
|
||||
if return_traj: per['traj'] = torch.stack(traj, 1).cpu()
|
||||
return res, per
|
||||
|
||||
|
||||
def evaluate(model, kin, tasks, device, K=8, safety=False, bs=256, seed=0):
|
||||
if not tasks: # 小样本摸底时 val 可能为空
|
||||
return {'n': 0, 'success': float('nan'), 'collision_rate': float('nan'), 'start_fail': float('nan'), 'brakes_mean': float('nan')}, []
|
||||
agg, pers = {}, []
|
||||
for i in range(0, len(tasks), bs):
|
||||
r, p = rollout(model, kin, tasks[i:i + bs], device, K=K, safety=safety, seed=seed + i)
|
||||
w = r['n']; pers.append((r, p))
|
||||
for k, v in r.items():
|
||||
if k == 'n' or v is None: continue
|
||||
if k == 'n' or v is None or isinstance(v, str): continue
|
||||
agg[k] = agg.get(k, 0.0) + v * w
|
||||
n = sum(r['n'] for r, _ in pers)
|
||||
out = {k: v / n for k, v in agg.items()}; out['n'] = n
|
||||
out['n_ref'] = sum(r['n_ref'] for r, _ in pers)
|
||||
out['time_budget'] = pers[0][0]['time_budget'] if len({r['time_budget'] for r, _ in pers}) == 1 else 'mixed'
|
||||
return out, pers
|
||||
|
||||
|
||||
|
||||
+7
-3
@@ -1,6 +1,7 @@
|
||||
"""预训练(方案 §6):MeanFlow 一步头 + FK 损失 + 平滑 + 价值头 + 子目标头。
|
||||
python -m ml.train --samples ml/data/samples --out ml/runs/base [--config ml/configs/base.json]
|
||||
吞吐摸底:python -m ml.train --samples ml/data/samples --out ml/runs/bench --steps 2000 --no-eval
|
||||
吞吐摸底:python -m ml.train --samples ml/data/samples --out ml/runs/bench_base --steps 2000 --no-eval
|
||||
通过线(方案 R10):全程 60000 步 ≤ 3.5 h,即 samples_per_s ≥ 5000(3090 实测 6.2 k/s ≈ 2.8 h);日志 eta_h 为剩余小时数。
|
||||
每 eval_every 步做一次闭环验证(ml/sim/closed_loop.py,部署节奏),按 success 存 best.pt;连续 patience_evals 次不升即停。"""
|
||||
import argparse, json, math, os, time
|
||||
import torch
|
||||
@@ -54,7 +55,9 @@ def main():
|
||||
amp = dev.startswith('cuda') and tc.get('amp') == 'bf16'
|
||||
stepv = torch.tensor(STEP, device=dev)
|
||||
fk_idx = torch.linspace(0, kin.cap_a.shape[0] - 1, tc['fk_caps'], device=dev).long()
|
||||
log = open(os.path.join(a.out, 'log.jsonl'), 'a')
|
||||
log = open(os.path.join(a.out, 'log.jsonl'), 'a' if a.resume else 'w') # 新训练覆盖旧日志,续训才追加
|
||||
if not val_tasks and not a.no_eval:
|
||||
print('没有 val 任务(样本太少),本次跳过闭环验证', flush=True); a.no_eval = True
|
||||
bad_evals, t0, seen = 0, time.time(), 0
|
||||
def save(name, step, extra=None):
|
||||
d = {'model': model.state_dict(), 'opt': opt.state_dict(), 'cfg': cfg, 'step': step, 'best': best, 'fp': kin.fp}
|
||||
@@ -95,7 +98,8 @@ def main():
|
||||
if step % tc['log_every'] == 0:
|
||||
el = time.time() - t0
|
||||
rec = {'step': step, 'loss': loss.item(), 'mf': mf_raw.item(), 'fk_cm': l_fk.item(), 'smooth': l_sm.item(),
|
||||
'value': l_val.item(), 'sub': l_sub.item(), 'gnorm': gn.item(), 'lr': lr_at(step), 'samples_per_s': seen / max(el, 1e-6)}
|
||||
'value': l_val.item(), 'sub': l_sub.item(), 'gnorm': gn.item(), 'lr': lr_at(step), 'samples_per_s': seen / max(el, 1e-6),
|
||||
'eta_h': (total - step - 1) * tc['batch'] / max(seen / max(el, 1e-6), 1e-6) / 3600}
|
||||
print(json.dumps(rec), flush=True); log.write(json.dumps(rec) + '\n'); log.flush()
|
||||
if not a.no_eval and (step + 1) % tc['eval_every'] == 0:
|
||||
model.eval()
|
||||
|
||||
Reference in New Issue
Block a user