Files
Robot_Arm/ml/dagger/collect.py
T

88 lines
5.6 KiB
Python
Raw Normal View History

"""DAgger 状态采集(2026-10-18 首次正式预训练后新增):用当前策略在 train 任务上闭环跑(关安全层),
从它自己走到的状态里挑起点,交给老师重新规划 → 这些"策略会去、老师数据里没有"的状态拿到正确标签。
python -m ml.dagger.collect --ckpt ml/runs/base/best.pt --samples ml/data/samples --n 8000 --seed 1 --parts 28 --out ml/data/dag1
每题最多取 2 个起点:
- 撞了的题:首次碰撞前 0.2 s 的状态(策略"快要撞上"的地方,最有用);
- 另取一个 [0.3 s, 首撞前或结束] 内均匀随机时刻的状态(偏离老师的一般状态)。
起点必须无碰且间隙 > MIN_GAP_MM(老师从碰撞态出发规划不出来,与 build_dataset 的增广接受同一地板),且离目标 > 5 mm(否则 DLS 接管,不需要老师)。
输出 tasks_XX.jsonl(按 --parts 均分,供 gen_dataset.cjs --tasks 并行),每行 {q0, qPrev, T, R, cat, src}。
2026-10-19(审查 F7 / C10):同时记录策略在该时刻的上一帧 qPrev;老师重规划的时间律从当前速度(v0)起步,
t=0 样本的 dq 特征用真实上一帧——原来一律按静止起步标注,DAgger 样本不是策略真正到过的 (q, dq) 状态。
为什么需要:纯行为克隆在闭环里会偏出老师分布(首次 21850 步闭环到达 0%),原方案指望 APG/挖错修,但它们没法从 0% 起步。
老师只用于训练标注,部署仍然无老师。"""
import argparse, json, os, random
import torch
from ml.common.kin_torch import Kin
from ml.data import load_tasks
from ml.model.policy import build
from ml.sim.closed_loop import rollout, goal_err, tasks_to_tensors, DT
GAP_CHUNK = 1024 # 每次 kin.gaps 的查询点数上限
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--ckpt', required=True); ap.add_argument('--samples', nargs='+', default=['ml/data/samples'])
ap.add_argument('--n', type=int, default=8000); ap.add_argument('--seed', type=int, default=1)
ap.add_argument('--parts', type=int, default=8); ap.add_argument('--out', required=True)
ap.add_argument('--bs', type=int, default=64)
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
a = ap.parse_args()
gap_min = float(os.environ.get('MIN_GAP_MM') or 3)
os.makedirs(a.out, exist_ok=True)
kin = Kin(device=a.device)
from ml.common.ckpt import load_policy # 2026-10-19:EMA 权重 + act_scale
model, ck = load_policy(a.ckpt, a.device)
assert ck.get('fp') in (None, kin.fp), '碰撞模型指纹与 checkpoint 不符'
pool = [t for t in load_tasks(a.samples[:1]) if t['split'] == 'train' and t.get('cat') not in ('D',)]
rng = random.Random(a.seed); rng.shuffle(pool); tasks = pool[:a.n]
out, stat = [], {'tasks': len(tasks), 'collided': 0, 'states': 0, 'rej_gap': 0, 'rej_near': 0}
for i in range(0, len(tasks), a.bs):
tb = tasks[i:i + a.bs]
with torch.no_grad():
_, per = rollout(model, kin, tb, a.device, K=8, safety=False, seed=a.seed * 100003 + i, return_traj=True)
tr = per['traj'].to(a.device) # [B,T,6]
B, Tn, _ = tr.shape
_, T, R, _, _ = tasks_to_tensors(tb, a.device)
# 2026-10-19 修(用户实测):原来一次喂 64 题 × 64 步 = 4096 点、cap=40 mm 给 kin.gaps,40 mm 内幸存的胶囊对太多,
# 峰值把显存 / 内存吃满。这里只需判断"≤0(碰撞)"和"> gap_min(起点地板)",cap 取 gap_min+1 即可,
# 且按 GAP_CHUNK 个点分块,用完即释放。
cap = gap_min + 1.0
flat = tr.reshape(-1, 6)
with torch.no_grad():
g = torch.cat([kin.gaps(flat[s:s + GAP_CHUNK], cap)['gm'].min(-1).values for s in range(0, flat.shape[0], GAP_CHUNK)]).view(B, Tn)
del flat
for b, t in enumerate(tb):
hit = (g[b] <= 0).nonzero()
c = int(hit[0]) if hit.numel() else Tn
if hit.numel(): stat['collided'] += 1
cand = []
if hit.numel(): cand.append(max(0, c - int(round(0.2 / DT))))
lo = int(round(0.3 / DT))
if c - 1 > lo: cand.append(rng.randint(lo, c - 1))
for k in cand:
if g[b, k] <= gap_min: stat['rej_gap'] += 1; continue
q = tr[b, k:k + 1]; qprev = tr[b, max(0, k - 1)]
e, oe = goal_err(kin, q, T[b:b + 1], R[b:b + 1])
if e.item() < 5 and (not t.get('R') or oe.item() < 5): stat['rej_near'] += 1; continue
out.append({'q0': [round(v, 3) for v in q[0].tolist()], 'qPrev': [round(v, 3) for v in qprev.tolist()], 'T': t['T'], 'R': t.get('R'), 'cat': t.get('cat', 'A'),
'src': {'tid': t['tid'], 'k': k, 'col': bool(hit.numel())}})
del tr, g, per
if a.device.startswith('cuda'): torch.cuda.empty_cache()
print(f'[dagger] {min(i + a.bs, len(tasks))}/{len(tasks)} 题,起点 {len(out)}', flush=True)
stat['states'] = len(out)
rng.shuffle(out)
per_part = (len(out) + a.parts - 1) // max(1, a.parts)
for p in range(a.parts):
chunk = out[p * per_part:(p + 1) * per_part]
if not chunk: continue
fn = os.path.join(a.out, 'tasks_%02d.jsonl' % (p + 1))
open(fn + '.tmp', 'w', encoding='utf-8').write(''.join(json.dumps(x) + '\n' for x in chunk)); os.replace(fn + '.tmp', fn)
json.dump(stat, open(os.path.join(a.out, 'collect.json'), 'w', encoding='utf-8'), indent=1) # 最后写:它存在 = 采集完成
print(json.dumps(stat, ensure_ascii=False), flush=True)
if __name__ == '__main__':
main()