게시판바둑 on-policy 학습: 대국 로그를 KataGo 라벨로 바꾸기예제 소스

예제 소스

바둑 on-policy 학습: 대국 로그를 KataGo 라벨로 바꾸기 · 파일 169개

dataset_r5_moves.py

#!/usr/bin/env python3
"""R5가 sqlite에 둔 수를 학습 레코드로 만들고, 좌표 혼합 데이터에 넣는다.

휴리스틱 대국에서 R5는 백이다. gemma4 대국은 server_rec.model 이 r5 인 수만 쓴다.
패스·불법·이미 돌이 있는 칸은 넣지 않는다.

  python scripts/dataset_r5_moves.py
"""

from __future__ import annotations

import json
import random
import sqlite3
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))

from scripts.dataset_coords_game import mon, record  # noqa: E402
from vlm_server.dataset_baduk import assistant_text  # noqa: E402
from vlm_server.server import SYSTEM_PROMPT_ALIGNED, build_prompt  # noqa: E402

N = 19
MIX = ROOT / "sft_data" / "mix_gemma_coord_r2" / "train.jsonl"
# 돌아가는 학습 루프가 앞에서부터 2600개만 읽는다.
TRAIN_N = 2600
R5_N = 600
COORD = {"lookup", "row", "col", "locate", "locate_hard"}
SOURCES = (
    (ROOT / "logs/heuristic_r5_eye_20261008/matches.sqlite", "W", None),
    (ROOT / "logs/heuristic_r5_sqlite_nolimit_20261007/matches.sqlite", "W", None),
    (ROOT / "logs/gemma4_r5_20261009/matches.sqlite", "W", "r5"),
)


def r5_record(cells: bytes, row: int, col: int, src: str) -> dict | None:
    if not (0 <= row < N and 0 <= col < N) or len(cells) != N * N:
        return None
    if cells[row * N + col] != 0:
        return None
    ok, *_ = mon.try_play(list(cells), row, col, 2, None)
    if not ok:
        return None
    user = build_prompt(cells, 2, board_repr="aligned")
    return record(
        "r5move",
        SYSTEM_PROMPT_ALIGNED,
        user,
        assistant_text((row, col)),
        src,
        {"row": row, "col": col, "side": 2},
    )


def load_r5_moves(sources=SOURCES) -> list[dict]:
    seen: set[tuple[str, int, int]] = set()
    out: list[dict] = []
    for path, color, model in sources:
        con = sqlite3.connect(path)
        rows = con.execute(
            "select game_id, ply, color, kind, row, col, board, server_rec "
            "from moves where kind='move' and board is not null order by game_id, ply"
        )
        for gid, ply, move_color, _kind, row, col, board, server_rec in rows:
            if move_color != color or not board or len(board) != N * N:
                continue
            if model is not None:
                try:
                    got = json.loads(server_rec or "{}").get("model")
                except json.JSONDecodeError:
                    continue
                if got != model:
                    continue
            if row is None or col is None:
                continue
            key = (board, int(row), int(col))
            if key in seen:
                continue
            seen.add(key)
            try:
                cells = bytes(".XO".index(ch) for ch in board)
            except ValueError:
                continue
            rec = r5_record(cells, int(row), int(col), f"{path.parent.name}:{gid}:{ply}")
            if rec:
                rec["board"] = board
                out.append(rec)
        con.close()
    return out


def mix_with_r5(existing: list[dict], r5: list[dict], train_n: int = TRAIN_N, r5_n: int = R5_N, seed: int = 21) -> list[dict]:
    coord = [r for r in existing if r.get("task") in COORD]
    moves = [r for r in existing if r.get("task") not in COORD and r.get("task") != "r5move"]
    rng = random.Random(seed)
    rng.shuffle(r5)
    chosen_r5 = r5[:r5_n]
    room = train_n - len(coord) - len(chosen_r5)
    if room < 0:
        raise SystemExit(f"coord {len(coord)} + r5 {len(chosen_r5)} exceeds {train_n}")
    rng.shuffle(moves)
    chosen = coord + moves[:room] + chosen_r5
    rng.shuffle(chosen)
    return chosen


def main() -> int:
    r5 = load_r5_moves()
    existing = [json.loads(x) for x in MIX.read_text(encoding="utf-8").splitlines() if x.strip()]
    mixed = mix_with_r5(existing, r5)
    MIX.write_text("".join(json.dumps(r, ensure_ascii=False) + "\n" for r in mixed), encoding="utf-8")
    from collections import Counter
    counts = Counter(r.get("task") or "none" for r in mixed)
    print(f"r5_legal {len(r5)} train {len(mixed)} {dict(counts)}", flush=True)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())