feat+test: 换向率 reverse_rate 进闭环指标(按轴方向翻转/活动步,站住不重置),带正对照单测

This commit is contained in:
cbb
2026-10-20 22:26:34 +08:00
parent 078f7cd451
commit 3e015b7485
4 changed files with 20 additions and 1 deletions
+7
View File
@@ -52,6 +52,13 @@
1. 新加的 `backoff` 指标一度在产物里根本没有——`ml/eval/eval_cl.py:run()` 的 `out` 是**自己显式列键**的,不是从 `rollout` 返回的字典通抄,所以我往 `res` 加的 `backoff_mean` 被无声丢掉。已改成从逐题 `per['backoff']` 汇总(`backoff_mean` + `reject_all_mean = backoff+brakes`,后者才是与"原来全拒次数"可比的量)。**教训:加指标要顺着输出链路走到底,别只改生产端。**
2. 停后台 GPU 作业时,`TaskStop` 之后 `bash` 脚本链会活下来并继续拉起下一个 python;而 `nvidia-smi --query-compute-apps` 在这台机器上列不出这些 python(只列 ShellHost/wallpaper32 之类),据此判断"卡已空"会误判。我第一次按 PID 杀子进程后,链条 30 秒后又起了一个新的 eval。第二次我按进程树杀,但**把父子关系读反了**(Windows 的 PPID 在父进程已死时是陈旧值),结果误杀了新链的第一臂。正确做法:重启前用命令行正则列出全量候选、数 python 个数确认单租户,重启后再数一次;杀之前先按 `StartTime` 判新旧。backoff 那一臂因此要补跑。
## 换向率进了评估指标(轮次 39 续)
- 分解既然指到"来回抖",每个臂都要直接报这个数,不能每次回头读转储:`rollout` 里按轴记方向,`reverse_rate` = 同轴方向翻转的步数 / 活动步数(站住的步不重置方向,所以"停了再同向走"不算换向);`eval_cl` 报 `reverse_p50`/`reverse_mean`。
- **正对照**(`tests/test_eval_cl.py`,42 项全绿):抖动轨迹报 >0.4、匀速轨迹报 ≈0——指标本身要证明有灵敏度,不然等于没测。
- 待交叉验证:新指标在对照臂上应复现转储离线算的 **23.5%**(p50);对不上就是两个口径有一个错。
- 生产端改了、输出端没改=没改:上一次 `backoff_mean` 就是这么丢的,这次加指标同时改了 `closed_loop.per` 与 `eval_cl` 的显式键列表。
## 行程超出的成因分解(轮次 39 续,纯 CPU 读轮次 33 base 转储)
- 口径先对齐:只在"有老师参考且到达"的题上算,自测 **2.425** 与 eval 报的 2.445 同量级(差别在我按 `t_done/DT` 取窗端,eval 逐步累加),后面的数都在这个口径上。
+3
View File
@@ -49,6 +49,9 @@ def run(model, kin, tasks, device, bs=256, **kw):
out.update({'speed_ratio_p50': median(v), 'speed_ratio_p95': v.quantile(0.95).item(),
'speed_ratio_max': v.max().item(), 'over_vmax_rate': (v > 1.001).float().mean().item(),
'accel_ratio_p95': acc.quantile(0.95).item(), 'accel_ratio_max': acc.max().item()})
if 'reverse_rate' in flat: # 换向步占比:行程超出主要是同一轴来回抖(轮次 39 分解),所以每个臂都要直接报它
rr = flat['reverse_rate']
out.update({'reverse_p50': median(rr), 'reverse_mean': rr.mean().item()})
if 'backoff' in flat and flat['backoff'].any(): # backoff=缩步顶上的次数;brakes 现在只记连最小档都不安全的
out['backoff_mean'] = flat['backoff'].mean().item() # ⇒ 原来的"全拒"事件数 = 两者之和
out['reject_all_mean'] = (flat['backoff'] + flat['brakes']).mean().item()
+7 -1
View File
@@ -202,6 +202,7 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
first_hit = torch.full((B,), float('nan'), device=device); dls_it = torch.full((B,), -1, dtype=torch.long, device=device)
# 2026-10-20(轮次33):速度/加速度按「实际执行位移」算,不看指令——第30轮证明反馈是在限幅之后叠加的
vratio = torch.zeros(B, device=device); aratio = torch.zeros(B, device=device); prev_exec = torch.zeros(B, 6, device=device)
rev = torch.zeros(B, device=device); nact = torch.zeros(B, device=device); prev_sgn = torch.zeros(B, 6, device=device)
traj = [q.clone()] if return_traj else None
plans = {k: [] for k in ('it', 'r', 'qe', 'commit', 'points', 'midpoints', 'gaps', 'certified')} if return_traj else None
n_start = int(round(0.5 / DT)); moved_start = None # R2 起步失败:0.5 s 内关节总行程 < 1°
@@ -478,6 +479,10 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
vratio = torch.maximum(vratio, (exec_d / step).max(-1).values * act_) # 实际速度 / VMAX
aratio = torch.maximum(aratio, ((exec_d - prev_exec) / step).max(-1).values * act_) # 每控制步的速度变化 / VMAX
prev_exec = exec_d
sgn = torch.sign(q - qp) # 换向率:轮次 39 的分解证明"行程超出 2.4 倍"主要是同一轴来回抖,不是绕远
rev += ((sgn != 0) & (prev_sgn != 0) & (sgn == -prev_sgn)).float().sum(-1) * act_
prev_sgn = torch.where(sgn != 0, sgn, prev_sgn) # 零步不重置方向(站住再走同向不算换向)
nact += act_
e, oe = goal_err(kin, q, T, R)
if it + 1 == k2s: e2 = e.clone()
newly = (~done) & (e < 1) & (oe < 0.5)
@@ -506,7 +511,8 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
side = torch.where(j1_first != 0, j1_first, torch.where(j1_max >= -j1_min, torch.ones_like(j1_max), -torch.ones_like(j1_max)))
per = {'done': done.cpu(), 'ok': ok.cpu(), 't_done': t_done.cpu(), 'limit': limit.cpu(), 'col': col.cpu(), 'travel': travel.cpu(), 'err': e_end.cpu(),
'side': side.cpu(), 'start_fail': start_fail.cpu(), 'brakes': brakes.cpu(), 'backoff': bo.cpu(), 'first_hit': first_hit.cpu(), 'dls_it': dls_it.cpu(),
'goal_ratio_2s': ratio2.cpu(), 'speed_ratio': vratio.cpu(), 'accel_ratio': aratio.cpu()}
'goal_ratio_2s': ratio2.cpu(), 'speed_ratio': vratio.cpu(), 'accel_ratio': aratio.cpu(),
'reverse_rate': (rev / nact.clamp_min(1.0)).cpu()}
if return_traj:
per['traj'] = torch.stack(traj, 1).cpu()
per['plans'] = {k: torch.tensor(v) if k in ('it', 'r') else torch.stack(v, 1).cpu() for k, v in plans.items()}
+3
View File
@@ -820,6 +820,9 @@ class TravelPenaltySelectTest(unittest.TestCase):
self.assertAlmostEqual(p0['traj'][0, 1, 0].item(), 0.9 * 1.2, places=5)
p1 = self.run1(1.0) # 加惩罚:行程 10.8 vs 3.6 ⇒ 选匀速那条
self.assertAlmostEqual(p1['traj'][0, 1, 0].item(), 0.225 * 1.2, places=5)
# 换向率指标要有灵敏度:抖动轨迹必须报出高换向,匀速轨迹必须接近 0(正对照,不然等于没测)
self.assertGreater(float(p0['reverse_rate'][0]), 0.4)
self.assertLess(float(p1['reverse_rate'][0]), 1e-6)
def test_waste_penalty_also_separates_them(self):
"""换成"白费比例"(1 − 同轴净位移/累计行程)也必须能分开,且罚度按当前误差缩放: