ml: 数据生成 + 可微运动学对拍 + MeanFlow/WTA 策略与闭环仿真 + APG/挖错/ONNX 导出与运行手册

- M1 数据生成器:采样/老师/距离下界认证/时间律/统计/一致性门禁;确定性老师(碰撞次数计时 + resetSession)、pos 任务冻结 J6、--consensus 邻域共识过滤、Dr 类。
- 全流程代码与运行手册:数据打包/可微运动学与对拍/MeanFlow+WTA 模型/闭环仿真/APG/挖错循环/ONNX 导出;webapp 策略运行时 ik-policy.js、?policy=1 接入、tests/policy_bench.cjs;ik-solver 增 pairList/clusterExport/criticalPairs/pairGap/leverTable。
This commit is contained in:
Hark
2026-10-18 06:40:23 +08:00
committed by cbb
parent cc43b3d3be
commit 2a0a2e55ad
56 changed files with 2990 additions and 97 deletions
View File
+53
View File
@@ -0,0 +1,53 @@
"""门禁(方案 §7.2 第 6 步):新模型须 ①门禁集到达率不降 ②A 类降幅 ≤ 0.5 个百分点 ③上一轮修好的错题 95% 仍对。
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
import torch
from ml.common.kin_torch import Kin
from ml.model.policy import build
from ml.sim.closed_loop import evaluate
def load(path, dev):
ck = torch.load(path, map_location=dev)
m = build(ck['cfg']).to(dev); m.load_state_dict(ck['model']); return m.eval()
def per_task_success(pers):
return torch.cat([p['done'] & (p['col'] == 0) for _, p in pers]).tolist()
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--old', required=True); ap.add_argument('--new', required=True); ap.add_argument('--tasks', required=True)
ap.add_argument('--fixed', default=None); ap.add_argument('--out', required=True); ap.add_argument('--current', default='ml/runs/current.pt')
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
a = ap.parse_args()
kin = Kin(device=a.device)
tasks = [json.loads(l) for l in open(a.tasks) if l.strip()]
ro, po = evaluate(load(a.old, a.device), kin, tasks, a.device)
rn, pn = evaluate(load(a.new, a.device), kin, tasks, a.device)
so, sn = per_task_success(po), per_task_success(pn)
acc = lambda s, c: (sum(x for x, t in zip(s, tasks) if t['cat'] == c) / max(1, sum(t['cat'] == c for t in tasks)))
gain = (sum(sn) - sum(so)) / len(tasks) * 100
a_drop = (acc(so, 'A') - acc(sn, 'A')) * 100
keep = None
if a.fixed:
ft = [json.loads(l) for l in open(a.fixed) if l.strip()]
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}
# 本轮新修好的题(旧错新对)留给下一轮做"仍对"检查
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')}))
if __name__ == '__main__':
main()
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# 后训练主循环(方案 §7):APG 首轮 → 挖错 3–6 轮(每轮:挖错 → [E2 老师补路径] → APG 微调 → 门禁/回滚)
# bash ml/mining/loop.sh [轮数=6] [每轮 APG 步数=1000] [每轮出题数=50000]
set -e
cd "$(dirname "$0")/../.."
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/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
# 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
FIXED="--fixed $D/gate_fixed.jsonl"
gain=$(python -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 [ $small -ge 2 ]; then echo "连续 2 轮提升 < 0.5 pp,停止"; break; fi
done
echo "最终模型:ml/runs/current.pt"
+78
View File
@@ -0,0 +1,78 @@
"""无老师挖错一轮(方案 §7.2):出题 → 闭环作答(关安全层)→ 验算 → 错题分类 → 错题+邻居进 APG 采样池。
python -m ml.mining.mine_round --ckpt ml/runs/apg/best.pt --seed 1001 --n 50000 --out ml/runs/mine/r1
造固定门禁集(只做一次):python -m ml.mining.mine_round --make-gate 2000 --seed 777 --out ml/runs
输出:pool.jsonl(错题与 ±5 mm/±3° 邻居,E1/E6 ×2)、e2_tasks.jsonl(卡在局部极小,可离线请老师补路径)、report.json/.md
错题类:E1 撞 / E2 没到且离目标 > 20 mm(局部极小)/ E3 用时 > 4 s / E4 没到但 < 20 mm(末端精度)/ E5 J1 换向 ≥ 3 次(摇摆)"""
import argparse, json, os
import torch
from ml.common.kin_torch import Kin
from ml.model.policy import build
from ml.sim.closed_loop import rollout, goal_err, tasks_to_tensors
from ml.mining.tasks import sample_tasks, neighbours
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--ckpt'); ap.add_argument('--seed', type=int, default=1001); ap.add_argument('--n', type=int, default=50000)
ap.add_argument('--out', required=True); ap.add_argument('--bs', type=int, default=512); ap.add_argument('--neigh', type=int, default=10)
ap.add_argument('--make-gate', type=int, default=0)
ap.add_argument('--mix', default='0.3,0.3,0.3,0.1', help='A,B,C,D 占比;贴立柱门禁集用 0,0,1,0')
ap.add_argument('--name', default='gate_tasks.jsonl')
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
a = ap.parse_args()
os.makedirs(a.out, exist_ok=True)
kin = Kin(device=a.device)
if a.make_gate:
ts = sample_tasks(kin, a.make_gate, a.seed, mix=tuple(float(x) for x in a.mix.split(',')))
with open(os.path.join(a.out, a.name), 'w') as f:
for t in ts: f.write(json.dumps(t) + '\n')
print('gate tasks', len(ts)); return
ck = torch.load(a.ckpt, map_location=a.device)
model = build(ck['cfg']).to(a.device); model.load_state_dict(ck['model']); model.eval()
tasks = sample_tasks(kin, a.n, a.seed)
g = torch.Generator(); g.manual_seed(a.seed)
cls = {k: [] for k in ('E1', 'E2', 'E3', 'E4', 'E5', 'ok')}
for i in range(0, len(tasks), a.bs):
tb = tasks[i:i + a.bs]
_, per = rollout(model, kin, tb, a.device, K=8, safety=False, seed=a.seed + i, max_sec=6.0, return_traj=True)
q0, T, R, _, _ = tasks_to_tensors(tb, a.device)
tr = per['traj']
e, _ = goal_err(kin, tr[:, -1].to(a.device), T, R)
d1 = tr[:, 1:, 0] - tr[:, :-1, 0]
sg = torch.sign(torch.where(d1.abs() > 0.05, d1, torch.zeros_like(d1)))
sw = torch.zeros(len(tb))
for b in range(len(tb)):
s = sg[b][sg[b] != 0]
sw[b] = (s[1:] != s[:-1]).sum() if s.numel() > 1 else 0
for b, t in enumerate(tb):
if per['col'][b] > 0: cls['E1'].append(t)
elif not per['done'][b]: (cls['E2'] if e[b] > 20 else cls['E4']).append(t)
elif per['t_done'][b] > 4.0: cls['E3'].append(t)
elif sw[b] >= 3: cls['E5'].append(t)
else: cls['ok'].append(t)
pool = []
for k, lst in cls.items():
if k == 'ok': continue
rep = 2 if k == 'E1' else 1
for t in lst:
t = dict(t); t['hard'] = True; t['err'] = k
for _ in range(rep): pool.append(t); pool += neighbours(t, a.neigh, g)
with open(os.path.join(a.out, 'pool.jsonl'), 'w') as f:
for t in pool: f.write(json.dumps(t) + '\n')
with open(os.path.join(a.out, 'e2_tasks.jsonl'), 'w') as f:
for t in cls['E2'][:2000]: f.write(json.dumps(t) + '\n')
n = len(tasks)
rep = {'n': n, **{k: len(v) / n for k, v in cls.items()}, 'pool': len(pool)}
bycat = {}
for k, lst in cls.items():
for t in lst: bycat.setdefault(t['cat'], {}).setdefault(k, 0); bycat[t['cat']][k] += 1
rep['by_cat'] = bycat
json.dump(rep, open(os.path.join(a.out, 'report.json'), 'w'), indent=1)
md = ['# 挖错报告', '', f'任务 {n},错题池 {len(pool)}', '', '| 类 | 占比 |', '|---|---:|'] + [f'| {k} | {len(v) / n:.2%} |' for k, v in cls.items()]
open(os.path.join(a.out, 'report.md'), 'w').write('\n'.join(md) + '\n')
print(json.dumps(rep, indent=1))
if __name__ == '__main__':
main()
+79
View File
@@ -0,0 +1,79 @@
"""无老师出题(PyTorch):A 均匀 / B 直线被挡 / C 贴立柱 / D 工作空间外;pos/pose 各半。"""
import math
import torch
from ml.common.features import wrap180
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):
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
out.append(q[ok])
return torch.cat(out)[:n]
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)
def column_gap(kin, q):
gm = kin.gaps(q, 40)['gm']
return torch.stack([gm[:, g] for g in (2, 3, 4, 5, 6)], -1).min(-1).values # 组 0(立柱/底座)× 组 2..6
@torch.no_grad()
def sample_tasks(kin, n, seed, mix=(0.3, 0.3, 0.3, 0.1)):
g = torch.Generator(device=kin.lo.device); g.manual_seed(seed)
want = [int(round(n * m)) for m in mix]; want[0] += n - sum(want)
tasks = []
def emit(cat, q0, b, T=None):
pose = torch.rand(q0.shape[0], device=q0.device, generator=g) < 0.5
if cat == 'D': pose[:] = False
xf = kin.cum_xf(b)
Tb = kin.fk(b, xf) if T is None else T
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})
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])):
got_a, got_b, have = [], [], 0
while have < k:
a, b = free_q(kin, 2048, g), free_q(kin, 2048, g)
m = straight_blocked(kin, a, b)
if cat == 'C': m &= column_gap(kin, b) < 8
got_a.append(a[m]); got_b.append(b[m]); have += int(m.sum())
if k: emit(cat, torch.cat(got_a)[:k], torch.cat(got_b)[:k])
if want[3]:
a, b = free_q(kin, want[3], g), rand_q(kin, want[3], g)
o = kin.fk(torch.zeros(1, 6, device=a.device))
T = o + (kin.fk(b) - o) * 1.6
emit('D', a, b, T)
return tasks
def neighbours(t, k, g):
"""±5 mm / ±3° 邻居"""
out = []
for _ in range(k):
u = torch.randn(3, generator=g); u = u / u.norm() * 5.0
nt = dict(t); nt['T'] = [x + float(d) for x, d in zip(t['T'], u)]
if t.get('R'):
ax = torch.randn(3, generator=g); ax = ax / ax.norm(); ang = math.radians(3.0) * float(torch.rand(1, generator=g))
K = torch.tensor([[0, -ax[2], ax[1]], [ax[2], 0, -ax[0]], [-ax[1], ax[0], 0]])
Rr = torch.eye(3) + math.sin(ang) * K + (1 - math.cos(ang)) * K @ K
nt['R'] = (Rr @ torch.tensor(t['R']).view(3, 3)).flatten().tolist()
nt['hard'] = True
out.append(nt)
return out