Files
Robot_Arm/ml/eval/check_r2.py
T

82 lines
5.7 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.
"""方案 R2 数据侧检查:近邻样本的标签是否自洽(时间律不可观测成分会让相同输入对应不同动作)。
python -m ml.eval.check_r2 --samples ml/data/samples --out ml/data/r2.json
做法:特征逐维标准化;随机取 Q 个查询样本,在 P 个样本池里找最近邻(排除同一任务,避免同轨迹相邻帧冒充"近邻");
取距离最近的 5% 近邻对,比较前 3 步(实际提交的动作)标签:std ≈ RMS(ΔY)/√2,单位为每步最大步长 STEP 的比例。
判据:label_std < 0.10(即 < 10% 步长)。起步失败率在闭环仿真结果的 start_fail 字段(acceptance 汇总)。
2026-10-18 第五轮(审查 N-2:判据与搜索池耦合):最近 5% 的近邻距离随池大小变,label_std 随之系统偏大/偏小。
改为按"近邻距离 → 标签差"做线性回归,取距离 → 0 的截距 label_std_norm 作判据(与池大小无关:池大只是近邻更近、点更多,
截距估计的是"同一输入下的固有标签分歧");旧口径 label_std_mean 保留作参考。pipeline 默认只警告,R2_STRICT=1 才拦。
自检(第七轮起每次自动做):--pool 50000 与 4 倍池 200000 各测一次,label_std_norm 差 < 0.02 记 pool_check.independent=true。
2026-10-18 新增(审查:R2 无执行点)。默认 5000 查询 × 50000 池,分块 256:GPU 峰值 < 0.5 GB,连同 4 倍池自检 CPU 约 5–10 分钟、GPU 约 1 分钟;近邻对为空时输出 pass=null 而不报错。"""
import argparse, json, math
import torch
from ml.data import Samples
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--samples', nargs='+', default=['ml/data/samples']); ap.add_argument('--out', default='ml/data/r2.json')
ap.add_argument('--queries', type=int, default=5000); ap.add_argument('--pool', type=int, default=50000)
ap.add_argument('--pool2', type=int, default=0, help='池无关性自检的大池(默认 4×pool)')
ap.add_argument('--frac', type=float, default=0.05); ap.add_argument('--seed', type=int, default=0)
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
a = ap.parse_args()
d = Samples(a.samples, device=a.device, split='train')
P2 = a.pool2 if a.pool2 else 4 * a.pool
r1 = measure(d, a, a.pool)
if r1.get('pairs', 0) == 0:
out = r1
else:
# 2026-10-18 第七轮(遗留 C-3):池无关性自检并入每次运行——同一批查询、池放大 4 倍再测一次,
# 截距差 < 0.02 才说明 label_std_norm 这把尺可信(independent=true);判定用大池的数(近邻更近、截距更稳)。
r2 = measure(d, a, P2) if min(P2, d.n) > r1['pool'] else None
out = dict(r2 or r1)
out['pool_check'] = {'pool_small': r1['pool'], 'norm_small': r1['label_std_norm'],
'pool_big': r2['pool'] if r2 else None, 'norm_big': r2['label_std_norm'] if r2 else None,
'diff': abs(r2['label_std_norm'] - r1['label_std_norm']) if r2 else None,
'independent': (abs(r2['label_std_norm'] - r1['label_std_norm']) < 0.02) if r2 else None,
'note': '样本数不足 4 倍池时只测一次' if not r2 else '差 < 0.02 → 判据与池无关,R2_STRICT=1 可以放心打开'}
print(json.dumps(out, indent=1))
json.dump(out, open(a.out, 'w'), indent=1)
def measure(d, a, pool):
g = torch.Generator(device=a.device); g.manual_seed(a.seed)
P = min(pool, d.n); Q = min(a.queries, d.n)
qi = torch.randperm(d.n, device=a.device, generator=g)[:Q] # 查询集与池大小无关(先抽查询)
pi = torch.randperm(d.n, device=a.device, generator=g)[:P]
X = d.X.float(); mu, sd = X[pi].mean(0), X[pi].std(0).clamp(min=1e-6)
Xp, Xq = (X[pi] - mu) / sd, (X[qi] - mu) / sd
tp, tq = d.TID[pi], d.TID[qi]
best_d = torch.empty(Q, device=a.device); best_j = torch.empty(Q, dtype=torch.long, device=a.device)
for s in range(0, Q, 256):
D = torch.cdist(Xq[s:s + 256], Xp)
D.masked_fill_(tq[s:s + 256, None] == tp[None], float('inf'))
v, j = D.min(1); best_d[s:s + 256] = v; best_j[s:s + 256] = j
fin = torch.isfinite(best_d)
if fin.sum() < 20:
return {'pairs': 0, 'label_std_mean': None, 'pass': None, 'pool': P, 'rule': 'label_std_norm < 0.10', 'note': '近邻对不足(样本太少),未判定'}
# 与池无关的口径:全部有效近邻对(取距离较近的一半,排除远处非线性段)上回归 std ~ a + b·dist,截距 a
dd = best_d[fin]; jj = best_j[fin]; qf = torch.nonzero(fin).squeeze(1)
half = dd <= torch.quantile(dd, 0.5)
Ya, Yb = d.Y[qi][qf[half], :3].float(), d.Y[pi][jj[half], :3].float()
sa = (Ya - Yb).pow(2).mean(-1).mean(-1).sqrt() / math.sqrt(2)
A = torch.stack([torch.ones_like(dd[half]), dd[half]], 1)
coef = torch.linalg.lstsq(A.cpu(), sa.cpu()[:, None]).solution.squeeze(1)
std_norm = max(0.0, coef[0].item())
thr = torch.quantile(best_d[fin], a.frac)
m = fin & (best_d <= thr)
Yq, Yp = d.Y[qi][m, :3].float(), d.Y[pi][best_j[m], :3].float()
rms = (Yq - Yp).pow(2).mean(-1).mean(-1).sqrt() # 每对、6 轴 × 3 步平均
std = (rms / math.sqrt(2))
return {'pairs': int(m.sum()), 'pool': P, 'queries': Q, 'nn_dist_threshold': thr.item(),
'label_std_norm': std_norm, 'label_std_slope': coef[1].item(),
'label_std_mean': std.mean().item(), 'label_std_p90': torch.quantile(std, 0.9).item(),
'pass': std_norm < 0.10,
'rule': 'label_std_norm(近邻距离→0 的回归截距)< 0.10(10% 步长);label_std_mean 为旧口径(随池大小变),仅参考'}
if __name__ == '__main__':
main()