poolcoach / scripts /gen_bc_dataset.py
masterdanh's picture
deploy: snapshot for HF Space
78738de
Raw
History Blame Contribute Delete
13.1 kB
#!/usr/bin/env python3
"""Sinh dataset BC từ oracle grid search (stage 2a — oracle-guided warm start).
Bối cảnh (16/07/2026): chuỗi 3 giả thuyết về Q|pot dính 0.53 đều bị bác;
smoking gun: agent chọn NGẪU NHIÊN trong tập pot combo (Q combo random
0.524 ≈ agent 0.522 ≈ blind 0.531), trong khi trần thật 0.869. Lối ra
(Documents/PoolCoach_Stage2_BC_WarmStart_Design.md): oracle grid search =
expert demonstrations rẻ → behavior cloning → PPO fine-tune.
Mỗi bàn: grid search Y HỆT oracle_controllability (phi ghost-ball mọi lỗ
khả thi × V0 × side × vert), label = action của combo (pot && !scratch) có
Q TỐT NHẤT — KỂ CẢ b2-potted (Q=1, khớp đúng reward env đang tối ưu).
Bàn 0 pot combo (hiếm; 0/100 ở oracle run 16/07): bỏ qua.
Action label đổi về [-1,1]^4 — nghịch đảo đúng map trong env.step:
a0 = phi/180 - 1 (phi ∈ [0,360))
a1 = (V0 - 0.5)/3.5*2 - 1 (V0 ∈ [0.5, 4.0])
a2 = side/0.4, a3 = vert/0.4
Chạy từ gốc repo (Numba JIT ~40s/worker lúc khởi động):
python scripts/gen_bc_dataset.py --tables 2000 --seed 123 --workers 4 # v1 (~35 phút)
python scripts/gen_bc_dataset.py --tables 20 --workers 2 # smoke
python scripts/gen_bc_dataset.py --tables 20000 --seed 124 --workers 4 # run chính (qua đêm)
Seed 123 ≠ 42 (oracle run) để không trùng 100 bàn đã phân tích.
Output: data/bc_dataset_<N>_<seed>.npz — 2 BỘ LABEL trong 1 lần chạy
(smoke 20/07: b2-lucky chiếm ~50% argmax → lưu cả 2, train_bc chọn):
obs (N,6) float32 — chuẩn hoá [0,1] y hệt env._obs
actions (N,4), q (N,) — argmax Q KỂ CẢ b2-potted (= reward env)
actions_xb2 (N,4), q_xb2 — argmax Q LOẠI b2-lucky (điều bi "thật")
n_pot (N,), b2_lucky (N,) + grid metadata
combos (M,6) [phi,v0,side,vert,q,b2p], combo_row (M,) — MỌI pot combo,
nhiên liệu cho relabel_bc_dataset.py (BC v1 fail G1 vì multi-modality
→ đổi label rule post-hoc, không cần re-simulate)
+ data/bc_dataset_<N>_<seed>_sanity.png (hist Q label + action components)
"""
from __future__ import annotations
import argparse
import multiprocessing as mp
import sys
import time
from pathlib import Path
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
sys.path.insert(0, str(ROOT / "scripts"))
# Tái dùng grid-search core của oracle (checklist §9 — pattern import thẳng,
# không refactor script oracle đã ra kết quả 16/07 để tránh regression):
# _init_worker (env + JIT warmup per worker), _simulate_shot, sample_tables.
import oracle_controllability as oc
def _to_action(phi, v0, side, vert):
"""Nghịch đảo map của env.step → [-1,1]^4 (verify round-trip 20/07)."""
import numpy as np
return np.clip(
np.array([phi / 180.0 - 1.0,
(v0 - 0.5) / 3.5 * 2.0 - 1.0,
side / 0.4,
vert / 0.4], dtype=np.float32),
-1.0, 1.0)
def _label_table(args):
"""Grid search 1 bàn → 2 bộ label BC (gồm b2-lucky / loại b2-lucky).
Smoke 20/07: b2-lucky (Q=1) chiếm ~50% label argmax — lưu CẢ HAI trong
1 lần chạy để train_bc chọn (--labels b2|xb2), ablation cho luận văn.
Trả (idx, obs6, act_b2, q_b2, act_xb2, q_xb2, n_pot, b2_lucky, n_sims);
obs = None nếu bàn không có pot combo nào (bỏ qua bàn đó).
Chạy trong worker — oc._ENV/_GRIDS đã được oc._init_worker dựng sẵn.
"""
import numpy as np
idx, cue_xy, b1_xy, b2_xy = args
v0_grid, side_grid, vert_grid, _ = oc._GRIDS
cue_xy, b1_xy, b2_xy = map(np.asarray, (cue_xy, b1_xy, b2_xy))
# phi ứng viên: ghost-ball của mọi lỗ khả thi (cùng logic oracle/aim any)
phis = [
float(np.degrees(np.arctan2(d[1], d[0])) % 360.0)
for d in oc._ENV._ghost_dirs_any(cue_xy, b1_xy)
]
n_sims = n_pot = 0
rows = [] # MỌI pot combo (phi, v0, side, vert, q, b2p) — lưu vào npz
# để relabel_bc_dataset.py đổi label rule không cần re-simulate
best_q, best_act, best_b2 = -1.0, None, False # argmax KỂ CẢ b2 (= env)
best_xq, best_xact = -1.0, None # argmax LOẠI b2-lucky
for phi in phis:
for v0 in v0_grid:
for a in side_grid:
for b in vert_grid:
n_sims += 1
potted, scratch, b2p, q = oc._simulate_shot(
cue_xy, b1_xy, b2_xy, phi, float(v0), float(a), float(b))
if not (potted and not scratch):
continue
n_pot += 1
rows.append((phi, float(v0), float(a), float(b),
q, float(b2p)))
if q > best_q:
best_q, best_b2 = q, bool(b2p)
best_act = (phi, float(v0), float(a), float(b))
if not b2p and q > best_xq:
best_xq = q
best_xact = (phi, float(v0), float(a), float(b))
if best_act is None:
return idx, None, None, 0.0, None, 0.0, 0, False, n_sims, None
# obs chuẩn hoá [0,1] — y hệt PositionPlayEnv._obs (vị trí BAN ĐẦU)
w, l = oc._ENV.w, oc._ENV.l
obs = np.clip(
np.array([cue_xy[0] / w, cue_xy[1] / l,
b1_xy[0] / w, b1_xy[1] / l,
b2_xy[0] / w, b2_xy[1] / l], dtype=np.float32),
0.0, 1.0)
if best_xact is None: # hiếm: MỌI pot combo đều b2-lucky → fallback b2
best_xact, best_xq = best_act, best_q
return (idx, obs, _to_action(*best_act), float(best_q),
_to_action(*best_xact), float(best_xq), n_pot, best_b2, n_sims,
np.array(rows, dtype=np.float32))
def main():
p = argparse.ArgumentParser()
p.add_argument("--tables", type=int, default=2000)
p.add_argument("--seed", type=int, default=123,
help="seed sample bàn; ≠ 42 (oracle run 16/07)")
p.add_argument("--v0-steps", type=int, default=10,
help="số mức V0 trong [0.5, 4.0] (khớp oracle)")
p.add_argument("--spin-steps", type=int, default=5,
help="số mức side/vert trong [-0.4, 0.4]; nên LẺ để có 0")
p.add_argument("--workers", type=int, default=4)
p.add_argument("--out", default=None,
help="mặc định data/bc_dataset_<N>_<seed>.npz")
args = p.parse_args()
import numpy as np
out = (Path(args.out) if args.out
else ROOT / "data" / f"bc_dataset_{args.tables}_{args.seed}.npz")
out.parent.mkdir(parents=True, exist_ok=True)
v0_grid = np.linspace(0.5, 4.0, args.v0_steps)
side_grid = np.linspace(-0.4, 0.4, args.spin_steps)
vert_grid = np.linspace(-0.4, 0.4, args.spin_steps)
tables = oc.sample_tables(args.tables, args.seed)
per_pocket = args.v0_steps * args.spin_steps ** 2
print(f"== Sinh BC dataset: {args.tables} bàn, seed {args.seed}, "
f"~{per_pocket} sim/lỗ khả thi (TB ~2.2 lỗ/bàn) ==")
print(f" grid: V0 {args.v0_steps} mức x side/vert {args.spin_steps} mức "
f"— label = argmax Q (kể cả b2-potted)")
print(f" {args.workers} worker — JIT Numba ~40s lúc khởi động...\n")
t0 = time.time()
results = {}
skipped = total_sims = 0
step = max(1, args.tables // 100)
with mp.Pool(args.workers, initializer=oc._init_worker,
initargs=(v0_grid, side_grid, vert_grid, 0.0)) as pool:
for k, (idx, obs, act, q, act_x, q_x, n_pot, b2, n_sims,
comb) in enumerate(
pool.imap_unordered(_label_table, tables), 1):
total_sims += n_sims
if obs is None:
skipped += 1
else:
results[idx] = (obs, act, q, act_x, q_x, n_pot, b2, comb)
if k % step == 0 or k == len(tables):
el = time.time() - t0
eta = el / k * (len(tables) - k)
print(f" {k}/{len(tables)} bàn — {len(results)} label, "
f"{skipped} bỏ qua [{el/60:.1f} phút, còn ~{eta/60:.1f}]")
el = time.time() - t0
print(f"\nXong {total_sims} sim trong {el/60:.1f} phút "
f"({total_sims/el:.0f} sim/s)")
if not results:
print("KHÔNG có bàn nào pot được — kiểm tra lại grid/env!")
return
order = sorted(results)
obs_arr = np.stack([results[i][0] for i in order])
act_arr = np.stack([results[i][1] for i in order])
q_arr = np.array([results[i][2] for i in order], dtype=np.float32)
actx_arr = np.stack([results[i][3] for i in order])
qx_arr = np.array([results[i][4] for i in order], dtype=np.float32)
npot_arr = np.array([results[i][5] for i in order], dtype=np.int32)
b2_arr = np.array([results[i][6] for i in order], dtype=np.int8)
# MỌI pot combo — nhiên liệu cho relabel_bc_dataset.py (fail path §7:
# đổi label rule là post-processing, không phải re-simulate 45 phút)
combos = np.concatenate([results[i][7] for i in order])
combo_row = np.concatenate([
np.full(len(results[i][7]), r, dtype=np.int32)
for r, i in enumerate(order)
])
np.savez_compressed(out, obs=obs_arr, actions=act_arr, q=q_arr,
actions_xb2=actx_arr, q_xb2=qx_arr,
n_pot=npot_arr, b2_lucky=b2_arr,
combos=combos, combo_row=combo_row,
seed=np.int64(args.seed), v0_grid=v0_grid,
side_grid=side_grid, vert_grid=vert_grid)
print(f"Dataset -> {out} ({len(order)} sample, 2 bộ label b2+xb2, "
f"{len(combos)} pot combo lưu kèm cho relabel)")
# ------------------------------------------------------- sanity stats
v0_lbl = 0.5 + (act_arr[:, 1] + 1.0) / 2.0 * 3.5
wrap = float(np.mean(np.abs(act_arr[:, 0]) > 0.95))
print(f"\n== Sanity (kỳ vọng theo design doc §8) ==")
print(f" Q label b2 : mean {q_arr.mean():.3f} | "
f"median {np.median(q_arr):.3f} | "
f"p10/p90 {np.percentile(q_arr, 10):.3f}/{np.percentile(q_arr, 90):.3f} | "
f"b2 lucky (Q=1): {float(b2_arr.mean()):.1%}")
print(f" Q label xb2 : mean {qx_arr.mean():.3f} "
f"(kỳ vọng ~0.87 = trần thật oracle 16/07) | "
f"median {np.median(qx_arr):.3f}")
print(f" Q > 0.7 (b2) : {float(np.mean(q_arr > 0.7)):.1%} | "
f"Q > 0.7 (xb2): {float(np.mean(qx_arr > 0.7)):.1%}")
print(f" pot combo/bàn: mean {npot_arr.mean():.1f} "
f"(multi-modality material — design §7)")
print(f" V0 label : mean {v0_lbl.mean():.2f} m/s | "
f"mức max (4.0): {float(np.mean(v0_lbl > 3.9)):.1%} | "
f"mức min (0.5): {float(np.mean(v0_lbl < 0.6)):.1%}")
print(f" |side|/|vert|: {np.abs(act_arr[:, 2]).mean():.2f} / "
f"{np.abs(act_arr[:, 3]).mean():.2f} (0-1) | "
f"spin != 0: {float(np.mean((act_arr[:, 2] != 0) | (act_arr[:, 3] != 0))):.1%}")
print(f" phi wraparound risk (|a0| > 0.95): {wrap:.1%} "
f"(cao → cân nhắc label cos/sin, design §7)")
# ------------------------------------------------------- sanity plot
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 3, figsize=(14, 8))
ax = axes[0][0]
ax.hist(q_arr, bins=np.linspace(0, 1, 21), alpha=0.6,
label=f"b2 (mean {q_arr.mean():.3f})")
ax.hist(qx_arr, bins=np.linspace(0, 1, 21), alpha=0.6,
label=f"xb2 (mean {qx_arr.mean():.3f})")
ax.set_title("Q label — xb2 kỳ vọng mean ~0.87")
ax.legend(loc="upper left")
ax = axes[0][1]
ax.hist((act_arr[:, 0] + 1.0) * 180.0, bins=36, alpha=0.75)
ax.set_title("phi label (độ) — kỳ vọng trải đều")
def bar_levels(ax, values, grid, title):
lv, cnt = np.unique(np.round(values, 3), return_counts=True)
width = 0.6 * (grid[1] - grid[0]) if len(grid) > 1 else 0.1
ax.bar(lv, cnt, width=width, alpha=0.75)
ax.set_title(title)
bar_levels(axes[0][2], v0_lbl, v0_grid, "V0 label (m/s) — có đa dạng không?")
bar_levels(axes[1][0], act_arr[:, 2] * 0.4, side_grid, "side label")
bar_levels(axes[1][1], act_arr[:, 3] * 0.4, vert_grid, "vert label")
ax = axes[1][2]
ax.hist(npot_arr, bins=30, alpha=0.75)
ax.set_title("pot combo / bàn")
fig.suptitle(f"BC dataset sanity — {len(order)} sample, seed {args.seed}")
fig.tight_layout()
png = out.with_name(out.stem + "_sanity.png")
fig.savefig(png, dpi=120)
print(f"\nSanity plot -> {png}")
print(f"\nBước kế: python scripts/train_bc.py --dataset {out.relative_to(ROOT)}")
if __name__ == "__main__":
main()