ml: 全流程代码与运行手册(数据打包/可微运动学与对拍/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:
@@ -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()
|
||||
Executable
+29
@@ -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"
|
||||
@@ -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()
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user