"""无老师挖错一轮(方案 §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()