상대가 있는 게임에서 정책은 어떻게 강해지는가
오목은 규칙이 단순하다. 흑과 백이 번갈아 돌을 두고, 정확히 다섯 개를 먼저 연결하면 이긴다. 하지만 단순한 규칙과 달리 실제 판단은 매우 적대적이다. 내가 네 개를 만들면 상대는 막아야 하고, 상대가 활삼을 만들면 나는 다음 수를 예측해야 한다. 즉, 오목의 환경은 가만히 있는 물리 시스템이 아니라 나를 이기려는 상대다.
이 프로젝트는 Qt/C++ 오목 프로그램에 로컬 AI와 VLM 서버를 붙여, 사람 대 AI, AI 대 VLM, VLM 대 VLM 대국을 만들 수 있게 구성되어 있다. 특히 AI vs VLM 모드와 자동 재시작을 켜면 게임이 계속 쌓이고, 그 결과가 다시 학습 데이터가 된다.
강화학습에서 에이전트는 상태를 보고 행동을 선택하고 보상을 받는다. 적대적 강화학습에서는 여기에 상대가 추가된다. 상대도 행동을 선택하고, 내 보상을 줄이거나 자신의 승률을 높이는 방향으로 움직인다.
일반 강화학습 | 적대적 강화학습 |
|---|---|
환경은 보통 규칙에 따라 반응한다. | 상대가 내 약점을 찌르며 반응한다. |
좋은 행동은 비교적 안정적일 수 있다. | 좋은 행동은 상대 정책에 따라 달라진다. |
목표는 보상 최대화다. | 목표는 상대보다 높은 승률이다. |
데이터는 환경 경험에서 나온다. | 데이터는 대국, 승패, 상대 전략 변화에서 나온다. |
파일 | 역할 |
|---|---|
src/boardmodel.* | 오목 규칙, 턴, 승패, 금수, 보드 크기를 관리한다. |
src/aiplayer.cpp | 로컬 휴리스틱 AI. 즉시승, 즉시차단, 패턴 점수로 수를 고른다. |
src/tcpvlmclient.cpp | Qt 앱이 보드 이미지와 격자를 VLM 서버로 보내는 TCP 클라이언트다. |
vlm_pva_server.py | 보드 PNG와 격자를 받아 VLM에게 다음 수를 묻고 좌표를 반환한다. |
dataset_omook.py | 대국 로그를 SFT 학습 예제로 변환한다. |
train_vlm.py | Qwen3-VL 계열 모델에 LoRA SFT를 수행한다. |
TRAINING.md | 데이터 수집, 변환, 학습, 자동 학습 루프를 설명한다. |
오목에서 보상은 명확하다. 이기면 좋고, 지면 나쁘다. 하지만 한 수 한 수마다 보상을 주기는 어렵다. 그래서 이 프로젝트는 게임 결과를 이용한다. 완료된 게임에서 승리한 쪽의 수를 전문가 시연으로 간주하고, 그 수들을 VLM이 따라 배우게 한다.
BoardModel은 13, 15, 19 크기의 보드를 지원하고, 정확히 5목을 승리로 판정한다. 6목 이상은 장목으로 금수 처리되며, 삼삼 금수도 검사한다. 이런 규칙 검사는 적대적 게임에서 중요하다. 모델이 강해지려면 단순히 그럴듯한 좌표가 아니라 규칙상 둘 수 있는 수를 골라야 하기 때문이다.
규칙 요소 | 코드에서의 의미 |
|---|---|
정확히 5목 | winsOnGrid가 길이 5만 승리로 인정한다. |
장목 금수 | 6목 이상은 hasOverline으로 금지한다. |
삼삼 금수 | 활삼 축이 2개 이상이면 거부한다. |
보드 크기 | 13, 15, 19를 런타임에 선택할 수 있다. |
대국 모드 | HumanVsAi, AiVsVlm, VlmVsVlm 등 여러 상대 구성을 지원한다. |
적대적 학습에는 상대가 필요하다. 이 프로젝트의 첫 상대는 src/aiplayer.cpp의 로컬 AI다. 이 AI는 신경망은 아니지만 중요한 역할을 한다. VLM이 처음부터 무작위로만 대국하면 좋은 수를 배우기 어렵기 때문에, 어느 정도 규칙을 아는 상대가 필요하다.
VLM 서버는 Qt 클라이언트에서 보드 PNG와 size*size 격자를 함께 받는다. 이미지는 모델이 판을 시각적으로 이해하는 데 쓰고, 격자는 좌표와 빈 칸 여부를 검증하는 데 쓴다. 서버는 모델에게 현재 보드와 자신의 색을 알려 주고, 다음 수를 두 정수로 답하게 한다.
Reply with EXACTLY two integers separated by a single space: "row col".
row and col are both in 0..18. No other text.모델이 이미 돌이 있는 칸이나 범위 밖 좌표를 답하면 서버는 재질문하거나 인접한 빈 칸으로 보정한다. 적대적 학습에서 이런 검증 장치는 매우 중요하다. 불법 수가 그대로 게임에 들어가면 학습 데이터 전체가 오염되기 때문이다.
서버를 --log-jsonl 옵션으로 실행하면 대국 중 발생한 수가 JSONL 파일에 누적된다. 현재 로그 파일에는 61,423줄이 쌓여 있다. 각 기록에는 보드 상태, 둔 좌표, 원래 모델이 답한 좌표, 보정 여부, 모델 정보가 들어 있다.
{
"size": 19,
"ai_side": 2,
"board": [0, 0, ...],
"move_row": 10,
"move_col": 9,
"corrected": true
}이 로그는 단순 기록이 아니라 다음 학습의 원료다. 적대적 학습에서는 상대와의 대국이 곧 데이터 수집 과정이 된다.
dataset_omook.py는 로그를 읽어 학습 데이터로 변환한다. 기본 필터는 완료된 게임만 사용하고, 그중 승리한 쪽의 수만 고른다. 패배한 쪽의 수는 기본적으로 제외한다. 승리한 수를 전문가 시연으로 가정하는 것이다.
필터 | 의미 |
|---|---|
완료된 게임만 | BlackWin 또는 WhiteWin으로 끝난 대국만 사용한다. |
승리한 쪽의 수만 | 이긴 플레이어의 행동을 좋은 예시로 본다. |
human/local_ai 우선 | 기본 설정에서는 VLM 자기 응답을 제외한다. |
pre-move 복원 | post-move 보드에서 방금 둔 돌을 제거해 모델 입력을 만든다. |
이미지 저장 | 보드 렌더 이미지를 함께 저장해 VLM SFT에 사용한다. |
현재 자동 데이터셋에는 12,080개의 예제가 있다. 각 예제는 보드 이미지, 텍스트 프롬프트, 정답 좌표로 구성된다.

그림. SFT 데이터로 저장된 오목 보드 이미지 예시
train_vlm.py는 dataset_omook.py가 만든 train.jsonl을 사용해 Qwen3-VL 모델에 LoRA SFT를 수행한다. 전체 모델을 다시 학습하는 대신 작은 어댑터만 학습하므로, 개인 장비에서도 반복 실험이 가능하다.
python train_vlm.py \
--dataset-jsonl sft_data/omook_sft_auto/train.jsonl \
--base-model vlm_models/Qwen__Qwen3-VL-2B-Instruct \
--output-dir vlm_models/lora_omook_auto \
--epochs 1 --batch-size 1 --grad-accum 8학습 스크립트는 user prompt 구간을 loss에서 제외하고 assistant 응답 좌표만 학습하도록 마스킹한다. 이 처리가 없으면 모델이 긴 보드 설명을 외우려 하다가 짧은 반복 토큰만 출력하는 collapse에 빠질 수 있다.
이 프로젝트의 핵심은 한 번 학습하고 끝내는 것이 아니라 반복하는 것이다. AI vs VLM 모드와 자동 재시작을 켜면 게임이 계속 진행되고, 서버는 게임이 끝날 때마다 데이터셋 변환과 LoRA 학습을 자동으로 실행할 수 있다.
대국 실행
-> 로그 누적
-> 승자 수 추출
-> LoRA SFT
-> 새 VLM 정책 적용
-> 다시 대국오목에서는 매 수마다 점수를 주기 어렵다. 어떤 수는 당장 좋아 보이지 않아도 5수 뒤 공격의 시작이 될 수 있고, 어떤 수는 현재 점수는 높지만 상대에게 더 큰 위협을 허용할 수도 있다. 그래서 이 프로젝트는 세밀한 즉시 보상 대신 게임 결과를 사용한다.
승리한 쪽의 수를 모아 학습하는 방식은 엄밀히 말해 value-based RL은 아니지만, 적대적 환경에서 경험을 수집하고 정책을 개선한다는 점에서 자기대국 강화 루프와 닮아 있다. 책에서는 이 차이를 분명히 짚는 것이 좋다. 독자는 '강화학습'이라는 이름 아래에도 Q-learning, policy gradient, imitation, self-play fine-tuning처럼 여러 구현 방식이 있다는 것을 이해하게 된다.
python vlm_pva_server.py \
--host 127.0.0.1 --port 9299 --device mps \
--model-id vlm_models/Qwen__Qwen3-VL-2B-Instruct \
--log-jsonl logs/omook_vlm.jsonl \
--lora-dir vlm_models/lora_omook_auto \
--auto-train \
--auto-train-max-samples 200개념 | 오목 프로젝트에서의 모습 |
|---|---|
상대 정책 | 로컬 AI, VLM, 사람 플레이어가 모두 상대가 될 수 있다. |
비정상 환경 | 상대가 학습하면 환경의 반응도 계속 바뀐다. |
자기대국 | AI vs VLM 또는 VLM vs VLM으로 데이터를 계속 만든다. |
정책 개선 | 승자의 수를 LoRA SFT로 학습해 다음 정책에 반영한다. |
검증과 보정 | 불법 좌표, 점유 칸, collapse 출력을 감지하고 막는다. |
평가 | 같은 상대와 반복 대국해 승률과 오류율을 확인한다. |
오목 프로그램은 적대적 강화학습의 핵심을 잘 보여 준다. 에이전트는 혼자 환경을 탐험하는 것이 아니라, 자신을 이기려는 상대와 계속 부딪힌다. 상대가 바뀌면 좋은 수의 의미도 바뀐다. 그래서 오목에서 학습은 단순한 정답 암기가 아니라, 상대의 위협을 읽고 자신의 위협을 만드는 정책 개선 과정이다.
이 프로젝트의 학습 방식은 전통적인 강화학습 알고리즘과 완전히 같지는 않다. 하지만 대국으로 데이터를 만들고, 승리한 행동을 모아 정책을 업데이트하고, 다시 강해진 정책을 상대와 붙이는 반복 루프는 적대적 학습의 감각을 매우 잘 전달한다. 이 장의 목표는 바로 그 구조를 독자가 손에 잡히게 이해하도록 만드는 것이다.
댓글 0
아직 댓글이 없습니다.