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

예제 소스

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

TRAINING.md

# Baduk VLM 학습 (Gemma 4 + LoRA + KGS 4d+ 프로 SGF)

`baduk` Qt 클라이언트 + `vlm_server` 가 사용하는 VLM 을
**프로(KGS 4단 이상) 기보** 로 LoRA 파인튜닝하는 절차입니다.

---

## 0) 학습 정상화 (baduk_v3) — 2026-08 권장

오늘(2026-08-05) self-play 평가에서 **BR(우하) ~40%**, 패스 0, 전부 `max_moves` 종료가
관측되었습니다. 학습 라벨 사분면은 균등(~25%)이므로 **모델 능력이라기보다
프롬프트 격자 형식 붕괴**가 1차 원인입니다.

| 문제 | 조치 |
|------|------|
| train=`compact`(0123456789…) / serve=`spaced` 불일치 | train=serve=`aligned` 로 통일 |
| compact 헤더 col10–18 이 0–8로 재등장 | `00 01 … 18` 두 자리 정렬 격자 |
| 예제 좌표 고정 `col:15` | 사분면 순환 예제 |
| Qwen 이미지 경로 Metal hang | `--text-only` 데이터 + serve `--text-only-move` |

### 0-A) 데이터셋 생성 (텍스트 전용, 정렬 격자)

```bash
cd ~/project
source .venv/bin/activate

# 스모크 (수분)
python -m vlm_server.dataset_baduk \
    --sgf-dir sgf_data \
    --out-dir sft_data/baduk_v3_smoke \
    --max-games 80 --moves-per-game 12 \
    --board-format aligned --text-only --no-render-png \
    --winner-only --tactical-bias --dedup \
    --val-split 0.05 --shuffle

# 본 학습용 (수 시간, PNG 없음 → 디스크 가벼움)
python -m vlm_server.dataset_baduk \
    --sgf-dir sgf_data \
    --out-dir sft_data/baduk_v3 \
    --max-games 5000 --moves-per-game 24 \
    --board-format aligned --text-only --no-render-png \
    --winner-only --tactical-bias --dedup \
    --oversample-tactical 2 \
    --val-split 0.05 --shuffle
```

### 0-B) LoRA 학습 — Qwen3-VL-8B (스토쿠에서 우수했던 베이스, text-only)

```bash
# 스모크
python -m vlm_server.train_lora \
    --dataset-jsonl sft_data/baduk_v3_smoke/train.jsonl \
    --model vlm_models/Qwen__Qwen3-VL-8B-Instruct \
    --output-dir vlm_models/lora_baduk_v3_qwen_smoke \
    --epochs 1 --batch-size 1 --grad-accum 8 --lr 1e-4 \
    --lora-rank 16 --lora-alpha 32 \
    --max-seq-length 4096 --max-samples 400 --grad-checkpoint

# 본 학습 (밤에 돌리기 권장)
python -m vlm_server.train_lora \
    --dataset-jsonl sft_data/baduk_v3/train.jsonl \
    --val-jsonl     sft_data/baduk_v3/val.jsonl \
    --model vlm_models/Qwen__Qwen3-VL-8B-Instruct \
    --output-dir vlm_models/lora_baduk_v3_qwen \
    --epochs 2 --batch-size 1 --grad-accum 16 --lr 5e-5 \
    --lora-rank 16 --lora-alpha 32 \
    --max-seq-length 4096 --grad-checkpoint
```

Gemma 로 빠르게 검증하려면 `--model` 만
`vlm_models/models--mlx-community--gemma-4-e4b-it-4bit/snapshots/cc3b666c01c20395e0dcebd53854504c7d9821f9`
로 바꾸면 됩니다 (깨진 HF revision `475b…` 피하기).

### 0-C) 서빙 (학습과 동일 형식)

```bash
python -m vlm_server serve \
    --host 127.0.0.1 --port 9319 \
    --model vlm_models/Qwen__Qwen3-VL-8B-Instruct \
    --lora-dir vlm_models/lora_baduk_v3_qwen \
    --text-only-move --board-repr aligned \
    --temperature 0.2 --max-new-tokens 64
```

### 0-D) 정상화 후 우선순위

0. **좌표 grounding 게이트** (다음 수 학습 전에 필수)
1. **VLM 학습 정상화** (이 절) → probe / self-play 로 BR%·합법수·전술 확인  
2. 계가·연속 패스 종국·자기 집 메우기 금지 (엔진/클라)  
3. 전술 보조 셋·온라인 RL

#### 좌표 grounding 검증

SGF 라벨이 맞아도 모델이 격자 축을 못 읽으면 학습이 무의미합니다.
(과거 Gemma-E4B 등에서 관측된 좌표 붕괴.)

```bash
# 스모크 학습이 끝난 뒤 — Qwen base vs LoRA 비교 권장
python -m vlm_server verify-coords \
    --model vlm_models/Qwen__Qwen3-VL-8B-Instruct \
    --board-format aligned --trials 48 \
    --out-jsonl logs/vlm_eval_day/coord_qwen_base.jsonl \
    --out-summary logs/vlm_eval_day/coord_qwen_base_summary.json

python -m vlm_server verify-coords \
    --model vlm_models/Qwen__Qwen3-VL-8B-Instruct \
    --lora-dir vlm_models/lora_baduk_v3_qwen_smoke \
    --board-format aligned --trials 48 \
    --out-jsonl logs/vlm_eval_day/coord_qwen_smoke.jsonl \
    --out-summary logs/vlm_eval_day/coord_qwen_smoke_summary.json
```

- `locate` 정확도 **≥ 90%** 를 게이트로 둡니다 (미달 시 exit 3).
- 사분면별(`TL/TR/BL/BR`) 정확도가 한쪽으로 무너지면 포맷/모델 재검토.
- `compact`/`spaced` 도 `--board-format` 으로 비교 가능.

> 기존 `sft_data/baduk_v2` + `lora_baduk_v2` 는 compact 레거시입니다.
> 새 학습은 **반드시** `aligned` + serve `--board-repr aligned` 짝을 맞추세요.
> **좌표 게이트를 통과한 뒤에만** 본학습(16만 샘플)을 돌리세요.

---

## 전체 흐름 (레거시 포함)

```
[1] u-go.net 에서 KGS4d 월별 SGF 다운로드  → sgf_data/
[2] SGF → 학습용 jsonl (+ 선택 PNG) 변환   → sft_data/baduk_v3/
[3] mlx-vlm LoRA 학습                      → vlm_models/lora_baduk_v3_*/
[4] 서버 --lora-dir + --board-repr aligned
```

전제: `vlm_server/requirements.txt` 가 이미 설치되어 있고 venv 가 살아있다고 가정.

```bash
cd ~/mywork/rl/go
source .venv/bin/activate
```

---

## 1) SGF 다운로드 (u-go.net KGS4d)

```bash
# 1년치 (월별 12개) 받기 — 권장 시작값. 약 70MB → 압축해제 후 ~150MB, 게임 ≈ 14만판.
python -m vlm_server.sgf_download --years 2018 --out-dir sgf_data

# 더 가볍게 시작하려면 한두 달만:
python -m vlm_server.sgf_download --months 2018-06 2018-07 --out-dir sgf_data

# 인덱스만 먼저 보고 결정:
python -m vlm_server.sgf_download --years 2018 --list-only
```

`sgf_data/` 안에 `*.sgf` 파일들이, `sgf_data/_archives/` 안에 원본 tar.gz 들이 들어갑니다.

---

## 2) SGF → SFT 데이터셋

### 기본

```bash
python -m vlm_server.dataset_baduk \
    --sgf-dir sgf_data \
    --out-dir sft_data/baduk_sft \
    --max-games 500 --moves-per-game 16 --shuffle
```

- `--max-games 500 --moves-per-game 16` → 약 8000 샘플 (보드 PNG 8000장 + 1 jsonl).
- `--include-pass` 를 주지 않으면 패스 수는 학습 데이터에서 제외됩니다 (기본).

### 더 강한 모델용 권장 (품질 옵션 조합)

```bash
python -m vlm_server.dataset_baduk \
    --sgf-dir sgf_data \
    --out-dir sft_data/baduk_v2 \
    --max-games 5000 --moves-per-game 24 \
    --winner-only --tactical-bias --dedup \
    --val-split 0.05 --shuffle
```

- `--winner-only` : SGF 의 RE 태그를 보고 **이긴 쪽 수만** 학습 (노이즈 제거).
- `--tactical-bias` : 게임당 샘플 중 **절반은 전술적 순간** (잡기/단수/단수회피/끊기 전후) 으로 채움. 일반 오프닝 수 편중을 줄여 중후반 전술이 올라감.
- `--dedup` : (보드, 차례, 정답수) 가 같은 중복 샘플 제거.
- `--val-split 0.05` : 5% 를 `val.jsonl` 로 분리. 학습 시 eval loss 확인용.
- 약 5000 게임 × 24 moves/game (= 12 만 후보) 중 대부분을 쓰면 10 만 샘플 가깝게 나옵니다 (PNG 10 만장 + jsonl). 디스크 20 GB 남짓 확보.

### 단수 응수 / 끊기 잇기 학습 강화 (보조 셋 + 오버샘플링)

VLM 이 **단수에 안 받거나 끊기에 안 잇는** 문제를 줄이려면 메인 셋만으론 비중이 낮아 약합니다 (전체의 5–10% 정도). 다음 두 옵션으로 전술 비중을 올립니다.

#### 방법 A — 메인 셋 안에서 전술 샘플 N배 복제

```bash
python -m vlm_server.dataset_baduk \
    --sgf-dir sgf_data \
    --out-dir sft_data/baduk_v2 \
    --max-games 5000 --moves-per-game 24 \
    --winner-only --tactical-bias --dedup \
    --oversample-tactical 3 \
    --val-split 0.05 --shuffle
```

`--oversample-tactical 3` : 전술 샘플은 jsonl 에 **3번씩** 복제. 비전술은 그대로 1번. 이미지 PNG 는 1장만 저장하고 jsonl 의 같은 줄을 N개 적는 방식이라 디스크 비용 거의 없음. 단수 응수 직답·잇기 응답을 옵티마이저가 N번 보게 되어 가중치가 들어갑니다.

#### 방법 B — 전술 전용 보조 셋 만들고 메인 셋과 합치기

```bash
# 전술 순간(잡기/단수/단수회피/끊기) 만 추출. moves-per-game 무시.
python -m vlm_server.dataset_baduk \
    --sgf-dir sgf_data \
    --out-dir sft_data/baduk_tactics \
    --max-games 5000 \
    --winner-only --tactics-only --dedup \
    --oversample-tactical 2 \
    --val-split 0.05 --shuffle

# 메인 셋과 합치기 (jsonl 만 합치면 됨 — 이미지 경로는 절대경로라서 OK)
cat sft_data/baduk_v2/train.jsonl sft_data/baduk_tactics/train.jsonl \
    > sft_data/baduk_v2_tac/train.jsonl
cat sft_data/baduk_v2/val.jsonl   sft_data/baduk_tactics/val.jsonl \
    > sft_data/baduk_v2_tac/val.jsonl
```

이 합친 셋으로 LoRA 를 학습하면 전술 비중이 메인 5–10% → 30–50% 로 올라갑니다. 다만 **너무 많이 oversample 하면** 일반 오프닝/포석을 잊어버려 초반이 약해질 수 있으니 `oversample-tactical 2~3` 정도가 균형점입니다.

> 참고: `--tactics-only` 는 게임당 평균 5–15 개 전술 순간만 뽑으므로 5000 게임 → 약 3–8 만 샘플. 메인 셋(약 10 만) 의 30–80%.

#### 추론 시 안전망 (학습과 별개로 즉효)

`baduk` 클라이언트의 `onVlmMoveReady` 에 **단수 응수 안전망** 을 넣어 두었습니다 (이미 적용됨). VLM 이 응답한 좌표가:
- 우리 단수 그룹의 활로에 두는 것도 아니고
- 상대 단수 그룹을 따내는 것도 아니면

VLM 응답을 거부하고 로컬 휴리스틱 AI(atari rescue 가중치 ×800) 의 응수로 대체합니다. 학습이 끝나기 전이라도 최소한 단수에 대마가 잡히는 사고는 막아 줍니다. 끊기 잇기까지는 안전망이 다루지 않으므로(미묘함) 그쪽은 위 데이터 강화로 학습시켜야 합니다.

생성물:

```
sft_data/baduk_sft/
    train.jsonl              # mlx-vlm 호환 (images + messages)
    img_00000000.png ...
    dataset_info.json
```

각 JSONL 한 줄은 다음 형태입니다.

```json
{
  "id": 1234,
  "size": 19,
  "ai_side": 1,
  "target_row": 3, "target_col": 15,
  "is_pass": false,
  "images": ["/abs/path/sft_data/baduk_sft/img_00001234.png"],
  "messages": [
    {"role": "user", "content": [
      {"type": "image", "image": "/abs/path/...img_00001234.png"},
      {"type": "text", "text": "당신은 19×19 바둑... 다음 수를 JSON 한 줄로만..."}
    ]},
    {"role": "assistant", "content": [
      {"type": "text", "text": "{\"row\": 3, \"col\": 15}"}
    ]}
  ]
}
```

---

## 3) LoRA 학습

### 첫 실행 (검증)

```bash
# 200 샘플 1 epoch — MPS 메모리/속도 빨리 확인.
python -m vlm_server.train_lora \
    --dataset-jsonl sft_data/baduk_sft/train.jsonl \
    --model mlx-community/gemma-4-e4b-it-4bit \
    --output-dir vlm_models/lora_baduk \
    --epochs 1 --batch-size 1 --grad-accum 8 --lr 2e-4 \
    --max-samples 200
```

### 본 학습 (기본)

```bash
python -m vlm_server.train_lora \
    --dataset-jsonl sft_data/baduk_sft/train.jsonl \
    --model mlx-community/gemma-4-e4b-it-4bit \
    --output-dir vlm_models/lora_baduk \
    --epochs 2 --batch-size 1 --grad-accum 8 --lr 2e-4 \
    --lora-rank 8
```

### 더 강한 학습 (권장, 새 v2 데이터셋 기준)

```bash
python -m vlm_server.train_lora \
    --dataset-jsonl sft_data/baduk_v2/train.jsonl \
    --val-jsonl     sft_data/baduk_v2/val.jsonl \
    --model mlx-community/gemma-4-e4b-it-4bit \
    --output-dir vlm_models/lora_baduk_v2 \
    --epochs 3 --batch-size 1 --grad-accum 16 --lr 1e-4 \
    --lora-rank 16 --lora-alpha 32 \
    --grad-checkpoint
```

요점:

- `--lora-rank 16 --lora-alpha 32` : 표현 용량을 늘려 전술 패턴 수용.
- `--grad-accum 16` : 실질 배치 16 효과 (MPS 단일 배치 한계 회피).
- `--lr 1e-4` : rank 가 커질수록 학습률은 낮춰야 발산 안 함.
- `--grad-checkpoint` : 메모리 절약.
- 3 epoch 가 되면 동일 샘플을 세 번 보게 되므로 안정적 수렴.

### 누적 학습 (이전 LoRA 위에 추가 학습)

기존 `lora_baduk` 는 그대로 두고, **새 데이터로 이어서 학습** 하고 싶을 때:

```bash
python -m vlm_server.train_lora \
    --dataset-jsonl sft_data/baduk_v2/train.jsonl \
    --resume-from   vlm_models/lora_baduk/adapters.safetensors \
    --output-dir    vlm_models/lora_baduk_v2 \
    --epochs 1 --lr 5e-5 \
    --lora-rank 16 --lora-alpha 32
```

- `--resume-from` 은 `vlm_models/lora_xxx` **폴더** 또는 `.../adapters.safetensors` 파일 경로를 줄 수 있습니다. 내부적으로 mlx-vlm 의 `--adapter-path`(폴더)로 넘깁니다.
- 이어서 학습할 때는 학습률을 한 단계 낮춰 (`5e-5`) 기존 weight 를 과하게 덮지 않게 합니다.

> 메모리/속도 팁
> - `--batch-size 1 --grad-accum 8~16` 이 Apple Silicon 에서 안정적.
> - OOM 이면 `--max-samples` 를 줄이거나 `--lora-rank 8` 로.
> - 학습 자체는 mlx-vlm 의 `lora.py` 가 수행 — 추가 옵션은 `--mlx-vlm-extra ...` 뒤에 그대로 전달 가능.

---

## 4) 학습 결과로 추론

서버를 띄울 때 `--lora-dir` 만 추가:

```bash
HF_HOME=$PWD/vlm_models HF_HUB_CACHE=$PWD/vlm_models \
python -m vlm_server serve --port 9319 \
    --model mlx-community/gemma-4-e4b-it-4bit \
    --lora-dir vlm_models/lora_baduk \
    --log-jsonl logs/baduk_play.jsonl
```

서버 시작 로그에 `LoRA 어댑터 적용: vlm_models/lora_baduk` 가 보이면 OK.

---

> 주의: 아래 **4b)** 는 `baduk_v2` 로 본 학습 (= `lora_baduk_v2` 생성) 을 **이미 마친 뒤** 에 수행하는
> "자가 대국으로 더 강화" 단계입니다. 순서를 건너뛰면 `sft_data/baduk_v3/train.jsonl` 이 없다고 에러가 납니다.

## 4b) 실전 기보로 데이터 늘리기 (학습 모드)

`baduk` Qt 클라이언트 상단바의 **"학습 모드 (기보 저장)"** 체크박스를 켜면, 종국되는
모든 판이 자동으로 SGF 로 저장됩니다.

- 저장 위치: `baduk` 실행 위치 기준 `recordings/baduk-<timestamp>.sgf`
- 모드 무관: 사람 vs 사람 / 사람 vs AI / 사람 vs VLM / AI vs VLM 전부 기록됨
- RE 태그(승자)와 PB/PW 라벨이 기보에 포함되므로 `--winner-only` 와 호환

### 연속 자가 대국 수집 (AI vs VLM 또는 VLM vs VLM)

같은 체크박스 + **"자동 재시작"** 체크박스를 함께 켜고 다음 둘 중 하나로 운용:

- `AI 대 VLM`: 로컬 휴리스틱 AI 가 한쪽, VLM 이 다른 쪽. **AI 가 강하니 VLM 의 약점이 잘 드러나는 데이터** 가 쌓입니다.
- `VLM 대 VLM`:
  - 백 측 host/port 를 비워 두면 **단일 서버 self-play** — 같은 모델이 흑/백 둘 다 둠.
  - 백 측 host/port 를 채워 두면 **두 서버 정책 비교** — 예: 9319 = `lora_baduk`, 9320 = `lora_baduk_v2`.

밤새 돌려 두면 PB/PW 라벨에 어떤 endpoint 가 둔지도 같이 SGF 에 기록됩니다.

> 두 서버 비교를 하고 싶으면 서버를 두 개 띄우면 됩니다:
>
> ```bash
> # 터미널 1
> HF_HOME=$PWD/vlm_models HF_HUB_CACHE=$PWD/vlm_models \
> python -m vlm_server serve --port 9319 \
>     --model mlx-community/gemma-4-e4b-it-4bit \
>     --lora-dir vlm_models/lora_baduk
>
> # 터미널 2
> HF_HOME=$PWD/vlm_models HF_HUB_CACHE=$PWD/vlm_models \
> python -m vlm_server serve --port 9320 \
>     --model mlx-community/gemma-4-e4b-it-4bit \
>     --lora-dir vlm_models/lora_baduk_v2
> ```
> 그리고 `baduk` UI 에서 흑 = `127.0.0.1:9319`, 백 = `127.0.0.1:9320` 으로 입력.

### 수집한 기보를 학습 데이터에 포함시키기

```bash
# 예: recordings/ 를 sgf_data/ 로 합침
cp recordings/*.sgf sgf_data/

# 데이터셋 재생성 (우리 자가 대국은 품질이 낮을 수 있으니 winner-only + tactical-bias 가중 권장)
python -m vlm_server.dataset_baduk \
    --sgf-dir sgf_data \
    --out-dir sft_data/baduk_v3 \
    --max-games 10000 --moves-per-game 24 \
    --winner-only --tactical-bias --dedup \
    --val-split 0.05 --shuffle

# LoRA 추가 학습
python -m vlm_server.train_lora \
    --dataset-jsonl sft_data/baduk_v3/train.jsonl \
    --val-jsonl     sft_data/baduk_v3/val.jsonl \
    --resume-from   vlm_models/lora_baduk_v2/adapters.safetensors \
    --output-dir    vlm_models/lora_baduk_v3 \
    --epochs 1 --lr 5e-5 --lora-rank 16 --lora-alpha 32 \
    --grad-checkpoint
```

> 팁: 본인 기보가 섞이면 VLM 이 사용자 스타일에도 적응합니다.
> 품질이 낮은 AI 자가 대국은 `--winner-only` 가 노이즈를 크게 걸러 줍니다.

---

## 5) 데이터 늘리기 / 추가 학습

- `sgf_download` 로 다른 해(2017, 2019 등) 추가 → 기존 `sft_data/baduk_sft/` 를 비우고 다시 변환하거나, 새 폴더로 변환 후 두 jsonl 을 `cat` 으로 합치면 됩니다.
- 학습은 같은 `--output-dir` 로 다시 돌리면 어댑터를 덮어쓰며 이어서 학습할 수 있습니다 (mlx-vlm 동작에 따라 fresh restart 이거나 resume 인지 다를 수 있음 — `--mlx-vlm-extra --resume` 시도).
- 평가는 `baduk` 클라이언트의 **AI 대 VLM** 모드에 **자동 재시작 체크박스**를 켜고 N판 돌린 뒤 승률을 보면 됩니다.

---

## 6) 자주 보는 문제

- **데이터셋 변환이 너무 느리다** → `dataset_baduk` 에 멀티프로세스를 아직 안 걸어 두었습니다. 큰 데이터셋이면 `--max-games` 와 `--moves-per-game` 으로 적당히 자르세요.
- **`mlx_vlm.lora` 인자 에러** → 우리 래퍼는 mlx-vlm 0.4.x 기준입니다. 버전이 다르면 `python -m mlx_vlm.lora --help` 를 보고 우리 래퍼의 인자명을 맞춰야 할 수 있습니다.
- **학습 후에도 답이 이상하다** → 답 형식 (`{"row": …, "col": …}`) 은 잘 따라오는데 좌표 품질이 낮은 경우, 이는 데이터 양 부족입니다. `--max-games 2000 --moves-per-game 24` 정도로 늘려 보세요.