diag: H 段(老师 oracle 走 rollout / 前缀消融 / 子目标越过顶点规则 / 分轴与分类别指标);训练:EMA、选模不落在 p_neq 预热、闭环分数选模、24000 步余弦、p_neq 0.5 + p_edge 0.1、子目标头切断抄输入、前缀/dq 增广;仿真:last 记真正提交的候选、碰撞与到达按每题时限、回放门禁读 pass+指纹并走 rollout;数据:动作标签按 1.25·STEP 归一化(act_scale 随 meta/ckpt/ONNX 传递,旧模型 1.0)、收回亏空结转、DAgger 带 qPrev 与起步速度、打包签名变更自动重打;ik-policy.js 同步(actScale、子目标规则、DLS 时清 lastPlan);本机执行清单与 README 更新
This commit is contained in:
+217
-69
@@ -1,26 +1,34 @@
|
||||
#!/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 段 + 安全层的闭环对照。
|
||||
用法(仓库根目录):
|
||||
"""闭环失败定位(只读,手动运行,n=256 约 35–50 分钟,显存约 3–6 GB)。
|
||||
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
|
||||
任务 = val 集前 n 题;老师轨迹按任务 id 从 ml/data/shard_*.jsonl 取(取不到的题不进 H 段,数量记在 H_tasks_missing_teacher)。
|
||||
模型经 ckpt.load_policy 载入(有 EMA 用 EMA;act_scale 随 checkpoint,旧 checkpoint = 1.0)。
|
||||
|
||||
段落:
|
||||
A. 特征口径:val 任务起点(非增广、老师状态)上,Python features() 与数据集 X 的逐组最大差 / 平均差。
|
||||
B. 同一噪声下两份特征得到的动作差(16 步末端关节差,度)。
|
||||
C. 旧部署口径(1 段采样、一致性选择、子目标旧规则 timeout)的失败形态:首撞时刻、碰撞步数、首撞组对、最小目标误差、
|
||||
目标误差进度比、开安全层的到达 / 碰撞 / 刹停。到达 = 时限内到达,碰撞只计时限内(2026-10-19 起)。
|
||||
H(2026-10-19,三份审查 review_closedloop / review_model_train / review_data_actions 的决定性实验):
|
||||
H1 老师 oracle 走完整 rollout()(closed_loop.TeacherOracle;select='first'、use_sub=False):
|
||||
H1_oracle:额外含 oracle 字段——DLS 接管前与老师轨迹的最大关节偏差、到达 / 碰撞、完成时刻 − 老师时长;
|
||||
H1_oracle_noise_<σ>(σ = 0.05 / 0.1 / 0.2,STEP 为单位,每次规划重采):闭环对残差噪声的敏感度。
|
||||
H2 前缀消融:H2_open_loop(r=3 的 val 行,前缀 47:68 原样 vs 置零,执行窗口 3..5 的误差);
|
||||
H2_replan_empty_s{1,4}:队列空了才规划(每次 r=0、提交 3 步),其余同部署。
|
||||
H3 子目标:H3_headsub_cross_s{1,4}(子目标头 + 越过顶点规则 = 新部署默认)、H3_headsub_timeout_s{1,4}(旧规则 = 旧部署)、
|
||||
H3_teachersub_cross_s{1,4} / H3_teachersub_timeout_s{1,4}(老师真实途经点全表)、H3_nosub_s{1,4}(不用子目标)。
|
||||
H4 指标(上面每个闭环变体都输出同一组键,见 metrics());H4_open_loop:按前缀长度 r=0..3 的执行窗口误差。
|
||||
_s1 / _s4 = MeanFlow 1 段(部署)/ 4 段采样。"""
|
||||
import argparse, json, math, os
|
||||
|
||||
import torch
|
||||
|
||||
from ml.common.ckpt import load_policy
|
||||
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
|
||||
from ml.data import Samples, load_tasks, read_meta
|
||||
from ml.sim.closed_loop import rollout, goal_err, DT, TeacherOracle
|
||||
from ml.eval.replay_teacher import load_teacher, pad_traj, oracle_raw, oracle_summary
|
||||
|
||||
# 83 维特征分组(与 features.py 拼接顺序一致)
|
||||
GROUPS = [('sincos', 0, 12), ('joint_norm', 12, 15), ('dq', 15, 21), ('p', 21, 24), ('Rq', 24, 30), ('T', 30, 33),
|
||||
@@ -29,26 +37,134 @@ GROUPS = [('sincos', 0, 12), ('joint_norm', 12, 15), ('dq', 15, 21), ('p', 21, 2
|
||||
|
||||
|
||||
PROG_T = (0.5, 1.0, 2.0) # 目标误差 / 起点误差 的取样时刻(s)
|
||||
K1, K2 = int(round(1.0 / DT)), int(round(2.0 / DT)) # 1 s / 2 s 对应的步数
|
||||
|
||||
|
||||
def pct(v, p):
|
||||
v = sorted(v)
|
||||
v = sorted(x for x in v if x is not None and x == x)
|
||||
return v[min(len(v) - 1, int(len(v) * p))] if v else None
|
||||
|
||||
|
||||
def qt(t, p):
|
||||
t = t[torch.isfinite(t)]
|
||||
return round(float(t.kthvalue(max(1, min(len(t), int(math.ceil(p * len(t)))))).values), 4) if len(t) else None
|
||||
|
||||
|
||||
def rnd(x, n=4):
|
||||
if isinstance(x, list): return [rnd(v, n) for v in x]
|
||||
return None if x is None or x != x else round(float(x), n)
|
||||
|
||||
|
||||
def pad_len(tr, n):
|
||||
"""轨迹 [B,T,6] 末帧重复补到至少 n 帧(到达后冻结,补帧与冻结等价)。"""
|
||||
if tr.shape[1] >= n: return tr
|
||||
return torch.cat([tr, tr[:, -1:].expand(-1, n - tr.shape[1], -1)], 1)
|
||||
|
||||
|
||||
def batch_stats(kin, ch, per, TR):
|
||||
"""一批的逐题量(CPU)。窗口:幅度 / 净位移 / TCP 用前 1 s,变号率用前 2 s 的 3 步块。"""
|
||||
dev = TR.device
|
||||
stp = torch.tensor(STEP, device=dev)
|
||||
tr = pad_len(per['traj'].to(dev), K2 + 1); te = pad_len(TR, K2 + 1)
|
||||
B = tr.shape[0]
|
||||
amp = lambda x: (x[:, 1:K1 + 1] - x[:, :K1]).abs().sum(1) # [B,6] 前 1 s 关节行程
|
||||
net = lambda x: (x[:, K1] - x[:, 0]).abs() # [B,6] 前 1 s 净位移
|
||||
def tcp(x):
|
||||
p = kin.fk(x[:, :K1 + 1].reshape(-1, 6)).view(B, K1 + 1, 3)
|
||||
return (p[:, 1:] - p[:, :-1]).norm(dim=-1).sum(1), (p[:, -1] - p[:, 0]).norm(dim=-1)
|
||||
def flips(x):
|
||||
idx = torch.arange(0, K2 + 1, 3, device=dev)
|
||||
b = x[:, idx[1:]] - x[:, idx[:-1]] # [B,nb,6] 3 步块增量
|
||||
big = b.abs() > 0.1 * stp
|
||||
v = big[:, 1:] & big[:, :-1]
|
||||
f = v & (torch.sign(b[:, 1:]) != torch.sign(b[:, :-1]))
|
||||
return f.sum(1).float(), v.sum(1).float()
|
||||
tl_p, tn_p = tcp(tr); tl_t, tn_t = tcp(te)
|
||||
fp_, vp_ = flips(tr); ft_, vt_ = flips(te)
|
||||
ex = torch.tensor([t.get('exec_s') or float('nan') for t in ch])
|
||||
return {'ok': per['ok'], 'col': per['col'] > 0, 'isD': torch.tensor([t.get('cat') == 'D' for t in ch]),
|
||||
'pose': torch.tensor([bool(t.get('R')) for t in ch]), 'key': [t.get('cat', '?') + '/' + t.get('mode', '?') for t in ch],
|
||||
'r2': per['goal_ratio_2s'], 'err': per['err'], 'first_hit': per['first_hit'], 'brakes': per['brakes'],
|
||||
'tratio': per['t_done'] / ex,
|
||||
'amp_p': amp(tr).cpu(), 'amp_t': amp(te).cpu(), 'net_p': net(tr).cpu(), 'net_t': net(te).cpu(),
|
||||
'tl_p': tl_p.cpu(), 'tl_t': tl_t.cpu(), 'tn_p': tn_p.cpu(), 'tn_t': tn_t.cpu(),
|
||||
'fl_p': fp_.cpu(), 'fv_p': vp_.cpu(), 'fl_t': ft_.cpu(), 'fv_t': vt_.cpu()}
|
||||
|
||||
|
||||
def metrics(stats):
|
||||
"""H4 指标(每个闭环变体同一组键):
|
||||
reach / collision:cat≠D、时限内(D 类目标不可达,单列在 D);
|
||||
goal_ratio_2s_p50:2 s 时目标误差 / 起点误差;end_goal_err_mm_p50;first_hit_s_p50(撞了的题);t_done_over_teacher_p50(到达的题);
|
||||
joint_amp_ratio[6]:前 1 s 各轴关节行程 Σ|Δq| 策略 / 老师(全体题合计;J6 只算 pose 题,pos 题老师冻结 J6);
|
||||
joint_net_ratio[6]:前 1 s 各轴净位移 |q(1s) − q(0)| 策略 / 老师(同上);j6_pos_amp_deg_p50:pos 题策略 J6 前 1 s 行程(度,老师为 0);
|
||||
tcp_path_ratio_p50 / tcp_net_ratio_p50:前 1 s TCP 路程 / 净位移 策略 / 老师(逐题比,老师 > 1 mm 的题);
|
||||
sign_flip_rate[6] / sign_flip_rate_teacher[6]:前 2 s 相邻 3 步块增量变号的比例(两块都 > 0.1·STEP 才计);
|
||||
by_cat_mode:各 cat/mode 的 n、reach、collision、goal_ratio_2s_p50(不含 D);D:n、collision、end_goal_err_mm_p50、goal_ratio_2s_p50。"""
|
||||
c = {k: (sum((s[k] for s in stats), []) if isinstance(stats[0][k], list) else torch.cat([s[k] for s in stats])) for k in stats[0]}
|
||||
keys = c['key']
|
||||
m = ~c['isD']; pose = c['pose']
|
||||
def jr(a, b):
|
||||
out = []
|
||||
for j in range(6):
|
||||
sel = m & pose if j == 5 else m
|
||||
den = float(b[sel, j].sum()); out.append(float(a[sel, j].sum()) / den if den > 1e-6 else None)
|
||||
return rnd(out)
|
||||
tl_ok = m & (c['tl_t'] > 1); tn_ok = m & (c['tn_t'] > 1)
|
||||
out = {'n': int(m.sum()),
|
||||
'reach': rnd(c['ok'][m].float().mean()) if m.any() else None,
|
||||
'collision': rnd(c['col'][m].float().mean()) if m.any() else None,
|
||||
'goal_ratio_2s_p50': qt(c['r2'][m], .5), 'end_goal_err_mm_p50': qt(c['err'][m], .5),
|
||||
'first_hit_s_p50': qt(c['first_hit'][m], .5), 't_done_over_teacher_p50': qt(c['tratio'][m & c['ok']], .5),
|
||||
'brakes_p50': qt(c['brakes'][m], .5),
|
||||
'joint_amp_ratio': jr(c['amp_p'], c['amp_t']), 'joint_net_ratio': jr(c['net_p'], c['net_t']),
|
||||
'j6_pos_amp_deg_p50': qt(c['amp_p'][m & ~pose, 5], .5),
|
||||
'tcp_path_ratio_p50': qt(c['tl_p'][tl_ok] / c['tl_t'][tl_ok], .5), 'tcp_net_ratio_p50': qt(c['tn_p'][tn_ok] / c['tn_t'][tn_ok], .5),
|
||||
'sign_flip_rate': rnd((c['fl_p'][m].sum(0) / c['fv_p'][m].sum(0).clamp(min=1)).tolist()),
|
||||
'sign_flip_rate_teacher': rnd((c['fl_t'][m].sum(0) / c['fv_t'][m].sum(0).clamp(min=1)).tolist())}
|
||||
by = {}
|
||||
for i, k in enumerate(keys):
|
||||
if not bool(m[i]): continue
|
||||
by.setdefault(k, []).append(i)
|
||||
out['by_cat_mode'] = {k: {'n': len(ix), 'reach': rnd(c['ok'][ix].float().mean()), 'collision': rnd(c['col'][ix].float().mean()),
|
||||
'goal_ratio_2s_p50': qt(c['r2'][ix], .5)} for k, ix in sorted(by.items())}
|
||||
d = c['isD']
|
||||
out['D'] = {'n': int(d.sum()), 'collision': rnd(c['col'][d].float().mean()) if d.any() else None,
|
||||
'end_goal_err_mm_p50': qt(c['err'][d], .5), 'goal_ratio_2s_p50': qt(c['r2'][d], .5)}
|
||||
return out
|
||||
|
||||
|
||||
def teacher_subs(recs, device):
|
||||
"""老师真实途经点全表(path 去掉起终点)→ (subs [B,M,6], n [B]),供 rollout(sub_override=...)。"""
|
||||
vs = [r['path'][1:-1] for r in recs]
|
||||
M = max(1, max(len(v) for v in vs))
|
||||
subs = torch.zeros(len(vs), M, 6, device=device); n = torch.zeros(len(vs), dtype=torch.long, device=device)
|
||||
for i, v in enumerate(vs):
|
||||
if v:
|
||||
x = torch.tensor(v, dtype=torch.float32, device=device); subs[i, :len(v)] = x; subs[i, len(v):] = x[-1]; n[i] = len(v)
|
||||
return subs, n
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--ckpt', required=True); ap.add_argument('--samples', default='ml/data/samples')
|
||||
ap.add_argument('--shards', default='ml/data/shard_*.jsonl')
|
||||
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()
|
||||
model, ck = load_policy(a.ckpt, dev)
|
||||
kin = Kin(device=dev)
|
||||
out = {'ckpt': a.ckpt, 'step': ck.get('step')}
|
||||
astep = torch.tensor(STEP, device=dev) * model.act_scale # 动作 → 度
|
||||
data_scale = float(read_meta(a.samples).get('act_scale', 1.0))
|
||||
out = {'ckpt': a.ckpt, 'step': ck.get('step'), 'weights': 'ema' if ck.get('ema') is not None else 'model',
|
||||
'act_scale_ckpt': model.act_scale, 'act_scale_data': data_scale}
|
||||
tasks = [t for t in load_tasks([a.samples]) if t['split'] == 'val'][:a.n]
|
||||
by_tid = {t['tid']: t for t in tasks}
|
||||
def save():
|
||||
s = json.dumps(out, indent=1, ensure_ascii=False)
|
||||
os.makedirs(os.path.dirname(a.out) or '.', exist_ok=True)
|
||||
open(a.out + '.tmp', 'w', encoding='utf-8').write(s); os.replace(a.out + '.tmp', a.out)
|
||||
return s
|
||||
|
||||
# ---------- A / B:起点特征口径 ----------
|
||||
vd = Samples(a.samples, device=dev, split='val')
|
||||
@@ -67,35 +183,69 @@ def main():
|
||||
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]
|
||||
qpc = torch.tensor([t.get('qp0') or t['q0'] for t in tc], dtype=torch.float32, device=dev)
|
||||
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)
|
||||
Fp = features(kin, qc, qpc, 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
|
||||
d = ((a1.clamp(-1, 1) - a2.clamp(-1, 1)) * astep).sum(2).abs().max(-1).values # [B,K] 16 步末端关节差(度)
|
||||
mag = (a1.clamp(-1, 1) * astep).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:闭环失败形态 ----------
|
||||
# ---------- H4 / H2 开环:执行窗口误差(val 行,老师状态) ----------
|
||||
# 闭环稳态 r=3,真正执行的是 act[r:r+3];原来的 ol_fk_mm 是 16 步终点误差,前缀那几步几乎不会错,看不出执行窗口。
|
||||
with torch.no_grad():
|
||||
pl_all = vd.X[:, 65:68].sum(-1).round().long()
|
||||
gsel = torch.Generator(device=dev); gsel.manual_seed(321)
|
||||
def rows_with(r, n=1024):
|
||||
ix = (pl_all == r).nonzero().flatten()
|
||||
return ix[torch.randperm(len(ix), device=dev, generator=gsel)[:n]] if len(ix) else ix
|
||||
def win_err(X, Y, r, steps, k0=None):
|
||||
k0 = r if k0 is None else k0
|
||||
z = torch.randn(X.shape[0] * 8, H, 6, device=dev, generator=torch.Generator(device=dev).manual_seed(11))
|
||||
act, _, _ = model.sample_k(X, 8, z, steps=steps); act = act.float().clamp(-1, 1)
|
||||
e = ((act[:, :, k0:k0 + 3] - Y[:, None, k0:k0 + 3]) * astep).sum(2).abs() # [N,K,6] 窗口累计误差(度)
|
||||
emax = e.max(-1).values.flatten()
|
||||
res = {'n_rows': int(X.shape[0]), 'deg_mean': rnd(emax.mean()), 'deg_p90': qt(emax, .9), 'per_joint_deg_mean': rnd(e.mean((0, 1)).tolist())}
|
||||
if r > 0 and k0 == r:
|
||||
ce = ((act[:, :, :r] - Y[:, None, :r]) * astep).abs().max(-1).values.max(-1).values.flatten()
|
||||
res['prefix_copy_err_deg_mean'] = rnd(ce.mean())
|
||||
return res
|
||||
ol4, ol2 = {}, {}
|
||||
for steps in (1, 4):
|
||||
for r in range(4):
|
||||
ix = rows_with(r)
|
||||
if len(ix): ol4['s%d_r%d' % (steps, r)] = win_err(vd.X[ix].float(), vd.Y[ix].float(), r, steps)
|
||||
ix = rows_with(3)
|
||||
if len(ix):
|
||||
X3 = vd.X[ix].float(); Y3 = vd.Y[ix].float(); X3z = X3.clone(); X3z[:, 47:68] = 0
|
||||
ol2['s%d_prefix_orig' % steps] = win_err(X3, Y3, 3, steps)
|
||||
ol2['s%d_prefix_zeroed' % steps] = win_err(X3z, Y3, 0, steps, k0=3)
|
||||
out['H4_open_loop'] = ol4; out['H2_open_loop'] = ol2
|
||||
del vd
|
||||
if dev.startswith('cuda'): torch.cuda.empty_cache()
|
||||
|
||||
# ---------- 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)
|
||||
_, per = rollout(model, kin, ch, dev, K=8, safety=False, seed=i, return_traj=True, sub_rule='timeout')
|
||||
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]
|
||||
inlim = (torch.arange(Tn, device=dev)[None] * DT) <= per['limit'].to(dev)[:, None] + 1e-6
|
||||
hit = (gm.min(-1).values <= 0) & inlim # [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))
|
||||
@@ -103,16 +253,15 @@ def main():
|
||||
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
|
||||
if per['ok'][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()
|
||||
rs, ps = rollout(model, kin, ch, dev, K=8, safety=True, seed=i, sub_rule='timeout')
|
||||
reach_s += int(ps['ok'].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),
|
||||
@@ -122,46 +271,45 @@ def main():
|
||||
'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 = 在靠近目标
|
||||
print(save(), flush=True)
|
||||
|
||||
# ---------- 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]
|
||||
# ---------- H:闭环变体(同一批 val 题,老师轨迹按 id 取) ----------
|
||||
teach = load_teacher(a.shards, [t['id'] for t in tasks])
|
||||
tH = [t for t in tasks if t['id'] in teach]
|
||||
out['H_tasks'] = len(tH); out['H_tasks_missing_teacher'] = len(tasks) - len(tH)
|
||||
|
||||
def variant(oracle_sigma=None, teacher_sub=False, **kw):
|
||||
stats, raws = [], []
|
||||
for i in range(0, len(tH), a.bs):
|
||||
ch = tH[i:i + a.bs]; recs = [teach[t['id']] for t in ch]
|
||||
TR = pad_traj([r['traj'] for r in recs], dev)
|
||||
kw2 = dict(kw)
|
||||
if oracle_sigma is not None:
|
||||
kw2.update(oracle=TeacherOracle(TR, act_scale=data_scale, sigma=oracle_sigma), select='first', use_sub=False)
|
||||
if teacher_sub: kw2['sub_override'] = teacher_subs(recs, dev)
|
||||
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)
|
||||
_, per = rollout(None if oracle_sigma is not None else model, kin, ch, dev, K=8, seed=i, return_traj=True, **kw2)
|
||||
stats.append(batch_stats(kin, ch, per, TR))
|
||||
if oracle_sigma is not None: raws.append(oracle_raw(per, TR, [len(r['traj']) for r in recs], ch))
|
||||
del per
|
||||
res = metrics(stats)
|
||||
if oracle_sigma is not None and oracle_sigma == 0: res['oracle'] = oracle_summary(raws)
|
||||
if dev.startswith('cuda'): torch.cuda.empty_cache()
|
||||
return res
|
||||
|
||||
plan = [('H1_oracle', dict(oracle_sigma=0.0))]
|
||||
plan += [('H1_oracle_noise_%g' % s, dict(oracle_sigma=s)) for s in (0.05, 0.1, 0.2)]
|
||||
for st in (1, 4):
|
||||
plan += [('H3_headsub_cross_s%d' % st, dict(steps=st, sub_rule='cross')),
|
||||
('H3_headsub_timeout_s%d' % st, dict(steps=st, sub_rule='timeout')),
|
||||
('H3_teachersub_cross_s%d' % st, dict(steps=st, sub_rule='cross', teacher_sub=True)),
|
||||
('H3_teachersub_timeout_s%d' % st, dict(steps=st, sub_rule='timeout', teacher_sub=True)),
|
||||
('H3_nosub_s%d' % st, dict(steps=st, use_sub=False)),
|
||||
('H2_replan_empty_s%d' % st, dict(steps=st, sub_rule='cross', replan='empty'))]
|
||||
for name, kw in plan:
|
||||
out[name] = variant(**kw)
|
||||
save(); print('[diag] %s 完成' % name, flush=True)
|
||||
print(save(), flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in New Issue
Block a user