Files
Robot_Arm/ml/eval/diag_cl.py
T

169 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
"""闭环失败定位(只读,手动运行,约 5–8 分钟;训练进行中也可以另开终端跑,显存约 2–3 GB)。
2026-10-19 新增:第二次预训练(979a581)开环已经正常(候选0 末端误差 ~6 mm、K 中最好 ~2.9 mm、多样性 0.65),
闭环却仍 100% 碰撞、0% 到达。开环用的是 JS 生成的特征 X,闭环用的是 Python features() 现算的特征,
两者之间只要有一处口径不同,模型在闭环里看到的就是没见过的输入。本脚本回答三件事:
A. 特征口径:val 任务起点(非增广、老师状态)上,Python features() 与数据集 X 的逐组最大差;
B. 同一噪声下,用两份特征得到的动作差(16 步末端关节差,度)——A 有差但 B 很小说明差异无害;
C. 闭环失败形态:首次碰撞时刻分布、碰撞步数、首次碰撞的组对、全程最小目标误差、目标误差进度比;
以及同一批题开安全层(safety=True,近似部署)的到达率 / 刹停次数。
G. 候选选择与采样段数:一致性打分错位修正后,1 段/4 段采样 × consist/value/first 三种选法,以及 4 段 + 安全层的闭环对照。
用法(仓库根目录):
PYTHONUTF8=1 python -m ml.eval.diag_cl --ckpt ml/runs/base/last.pt --n 256 --out ml/runs/diag_cl.json
"""
import argparse, json, os
import torch
from ml.common.kin_torch import Kin
from ml.common.features import features, STEP, H
from ml.data import Samples, load_tasks
from ml.model.policy import build
from ml.sim.closed_loop import rollout, goal_err, DT
# 83 维特征分组(与 features.py 拼接顺序一致)
GROUPS = [('sincos', 0, 12), ('joint_norm', 12, 15), ('dq', 15, 21), ('p', 21, 24), ('Rq', 24, 30), ('T', 30, 33),
('R', 33, 39), ('T-p', 39, 42), ('T-p_clamp', 42, 45), ('mode', 45, 47), ('prefix', 47, 65), ('mask', 65, 68),
('sub', 68, 74), ('sub_flag', 74, 75), ('gaps', 75, 83)]
PROG_T = (0.5, 1.0, 2.0) # 目标误差 / 起点误差 的取样时刻(s)
def pct(v, p):
v = sorted(v)
return v[min(len(v) - 1, int(len(v) * p))] if v else None
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--ckpt', required=True); ap.add_argument('--samples', default='ml/data/samples')
ap.add_argument('--n', type=int, default=256); ap.add_argument('--bs', type=int, default=64)
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu'); ap.add_argument('--out', default='ml/runs/diag_cl.json')
a = ap.parse_args()
dev = a.device
ck = torch.load(a.ckpt, map_location=dev)
model = build(ck['cfg']).to(dev); model.load_state_dict(ck['model']); model.eval()
kin = Kin(device=dev)
out = {'ckpt': a.ckpt, 'step': ck.get('step')}
tasks = [t for t in load_tasks([a.samples]) if t['split'] == 'val'][:a.n]
by_tid = {t['tid']: t for t in tasks}
# ---------- A / B:起点特征口径 ----------
vd = Samples(a.samples, device=dev, split='val')
tid = vd.TID.long()
first = torch.ones_like(tid, dtype=torch.bool); first[1:] = tid[1:] != tid[:-1] # 每题第一行 = t=0 样本
idx = [i for i in first.nonzero().flatten().tolist() if int(tid[i]) in by_tid]
X0 = vd.X[idx].float()
tk = [by_tid[int(tid[i])] for i in idx]
q0 = torch.tensor([t['q0'] for t in tk], dtype=torch.float32, device=dev)
qx = torch.atan2(X0[:, 0:12:2], X0[:, 1:12:2]) * 180 / torch.pi
dqx = torch.remainder(qx - q0 + 180, 360) - 180
clean = dqx.abs().max(-1).values < 1e-2 # 未被噪声增广的起点
out['A_rows'] = int(clean.sum()); out['A_rows_aug_skipped'] = int((~clean).sum())
if clean.any():
Xc = X0[clean]; qc = q0[clean]; tc = [t for t, c in zip(tk, clean.tolist()) if c]
T = torch.tensor([t['T'] for t in tc], dtype=torch.float32, device=dev)
R = torch.tensor([t['R'] if t.get('R') else [0.] * 9 for t in tc], dtype=torch.float32, device=dev)
B = qc.shape[0]
pre = Xc[:, 47:65].view(B, 3, 6) * torch.tensor(STEP, device=dev) # 前缀 / 子目标直接取数据集里的,只比状态相关部分
pl = Xc[:, 65:68].sum(-1).round()
sf = Xc[:, 74]
sub = qc + Xc[:, 68:74] * 180
Fp = features(kin, qc, qc, T, R, pre, pl, sub, sf)
out['A_maxdiff'] = {g: (Fp[:, s:e] - Xc[:, s:e]).abs().max().item() for g, s, e in GROUPS}
out['A_meandiff'] = {g: (Fp[:, s:e] - Xc[:, s:e]).abs().mean().item() for g, s, e in GROUPS}
with torch.no_grad():
z = torch.randn(B * 8, H, 6, device=dev, generator=torch.Generator(device=dev).manual_seed(7))
a1, _, _ = model.sample_k(Xc, 8, z); a2, _, _ = model.sample_k(Fp, 8, z)
stepv = torch.tensor(STEP, device=dev)
d = ((a1.clamp(-1, 1) - a2.clamp(-1, 1)) * stepv).sum(2).abs().max(-1).values # [B,K] 16 步末端关节差(度)
mag = (a1.clamp(-1, 1) * stepv).sum(2).abs().max(-1).values
out['B_act_diff_deg'] = {'mean': d.mean().item(), 'p90': d.flatten().kthvalue(max(1, int(0.9 * d.numel()))).values.item(),
'ref_motion_deg_mean': mag.mean().item()}
# ---------- C:闭环失败形态 ----------
first_t, col_steps, pairs, min_err, reach = [], [], {}, [], 0
prog = {ts: [] for ts in PROG_T}
reach_s, brakes_s, col_s = 0, [], 0
for i in range(0, len(tasks), a.bs):
ch = tasks[i:i + a.bs]
with torch.no_grad():
_, per = rollout(model, kin, ch, dev, K=8, safety=False, seed=i, return_traj=True)
tr = per['traj'].to(dev) # [B,Tn,6]
B, Tn, _ = tr.shape
flat = tr.reshape(-1, 6) # 分块 ≤1024 点(同 dagger/collect.py 的显存修复)
gm = torch.cat([kin.gaps(flat[s:s + 1024], 0.5)['gm'] for s in range(0, flat.shape[0], 1024)]).view(B, Tn, 49)
hit = gm.min(-1).values <= 0 # [B,Tn]
Tt = torch.tensor([t['T'] for t in ch], dtype=torch.float32, device=dev)
Rt = torch.tensor([t['R'] if t.get('R') else [0.] * 9 for t in ch], dtype=torch.float32, device=dev)
e, _ = goal_err(kin, tr.reshape(-1, 6), Tt.repeat_interleave(Tn, 0), Rt.repeat_interleave(Tn, 0))
e = e.view(B, Tn)
for b in range(B):
for ts in PROG_T:
k = min(Tn - 1, int(round(ts / DT))); prog[ts].append((e[b, k] / e[b, 0].clamp(min=1e-3)).item())
# 到达后轨迹被冻结(d=0),之后不再计
for b in range(B):
if per['done'][b]: reach += 1
min_err.append(e[b].min().item()); col_steps.append(int(per['col'][b]))
h = hit[b].nonzero().flatten()
if len(h):
k = int(h[0]); first_t.append(k * DT)
pr = int(gm[b, k].argmin()); key = '%d-%d' % (pr // 7, pr % 7); pairs[key] = pairs.get(key, 0) + 1
rs, ps = rollout(model, kin, ch, dev, K=8, safety=True, seed=i)
reach_s += int(ps['done'].sum()); col_s += int((ps['col'] > 0).sum()); brakes_s += ps['brakes'].tolist()
n = len(tasks)
out['C'] = {'n': n, 'reach': reach / n, 'collided': len(first_t) / n,
'first_hit_s': {'p10': pct(first_t, .1), 'p50': pct(first_t, .5), 'p90': pct(first_t, .9),
'within_0.1s': sum(t <= 0.1 for t in first_t) / max(1, len(first_t))},
'col_steps': {'p50': pct(col_steps, .5), 'p90': pct(col_steps, .9)},
'first_hit_pair(group a-b)': dict(sorted(pairs.items(), key=lambda kv: -kv[1])[:8]),
'min_goal_err_mm': {'p10': pct(min_err, .1), 'p50': pct(min_err, .5), 'p90': pct(min_err, .9)},
'safety_on': {'reach': reach_s / n, 'collided': col_s / n, 'brakes_p50': pct(brakes_s, .5)}}
out['C']['goal_err_ratio_p50'] = {'%.1fs' % ts: pct(v, .5) for ts, v in prog.items()} # <1 = 在靠近目标
# ---------- G:候选选择与采样段数(2026-10-19 加) ----------
# 第四轮 F 段:一步采样残留噪声真实(抖动 42×、残差-噪声相关 −0.86),4 段采样闭环碰撞 99.6%→93%,但到达仍 0%;
# 零噪声(8 条候选完全相同,选择不起作用)0%/100%——均值样本本身偏老师 ~4°(多峰平均)。
# 复查闭环发现一致性打分错位一步(对齐 last[3],应为 last[4]),等于每次重规划奖励「比上一计划慢一步」的候选;
# 已在 closed_loop.py 与 ik-policy.js 修正,上面 C 段即修正后的一步采样基线。
# G 段在同一批题上比较:4 段采样 × 三种选法(consist 部署口径 / value 每次价值前瞻 / first 不选),以及 4 段 + 安全层。
# 若 consist≈first,说明选择不是主因;若 value 明显好,说明一致性选法本身把闭环带偏;三者都 0% 则问题在策略本身(转向协变量偏移)。
def variant(**kw):
rc = cc_ = 0; me, r2, fh, sp, br = [], [], [], [], []
for i in range(0, len(tasks), a.bs):
ch = tasks[i:i + a.bs]
with torch.no_grad():
_, pp = rollout(model, kin, ch, dev, K=8, seed=i, return_traj=True, **kw)
tr = pp['traj'].to(dev); B, Tn, _ = tr.shape
k2 = min(Tn - 1, int(round(2.0 / DT))); k1 = min(Tn - 1, int(round(1.0 / DT)))
Tt = torch.tensor([t['T'] for t in ch], dtype=torch.float32, device=dev)
Rt = torch.tensor([t['R'] if t.get('R') else [0.] * 9 for t in ch], dtype=torch.float32, device=dev)
e0, _ = goal_err(kin, tr[:, 0], Tt, Rt); e2, _ = goal_err(kin, tr[:, k2], Tt, Rt)
r2 += (e2 / e0.clamp(min=1e-3)).tolist()
# 前 1 s 关节行程速度 / 老师平均速度(travel/exec_s)
v1 = (tr[:, 1:k1 + 1] - tr[:, :k1]).abs().sum((1, 2)) / (k1 * DT)
sp += [float(v1[b]) / max(1e-6, ch[b]['travel'] / ch[b]['exec_s']) for b in range(B) if ch[b].get('travel') and ch[b].get('exec_s')]
flat = tr.reshape(-1, 6)
gm = torch.cat([kin.gaps(flat[s0:s0 + 1024], 0.5)['gm'] for s0 in range(0, flat.shape[0], 1024)]).view(B, Tn, 49)
hit = gm.min(-1).values <= 0
for b in range(B):
h = hit[b].nonzero().flatten()
if len(h): fh.append(int(h[0]) * DT)
rc += int(pp['done'].sum()); cc_ += int((pp['col'] > 0).sum()); me += pp['err'].tolist(); br += pp['brakes'].tolist()
return {'reach': rc / n, 'collided': cc_ / n, 'goal_err_ratio_2s_p50': pct(r2, .5), 'end_goal_err_mm_p50': pct(me, .5),
'first_hit_s_p50': pct(fh, .5), 'speed_ratio_1s_p50': pct(sp, .5), 'brakes_p50': pct(br, .5)}
out['G_1step_consist'] = variant(steps=1, select='consist')
out['G_4step_consist'] = variant(steps=4, select='consist')
out['G_4step_value'] = variant(steps=4, select='value')
out['G_4step_first'] = variant(steps=4, select='first')
out['G_4step_consist_safety'] = variant(steps=4, select='consist', safety=True)
s = json.dumps(out, indent=1, ensure_ascii=False); print(s)
os.makedirs(os.path.dirname(a.out) or '.', exist_ok=True)
open(a.out, 'w', encoding='utf-8').write(s)
if __name__ == '__main__':
main()