ONCE AI PUBLISHING디지털 핸드북 시리즈 #01
CHAPTER 06

오목으로 이해하는 적대적 강화학습

상대가 있는 게임에서 정책은 어떻게 강해지는가

왜 오목인가

오목은 규칙이 단순하다. 흑과 백이 번갈아 돌을 두고, 정확히 다섯 개를 먼저 연결하면 이긴다. 하지만 단순한 규칙과 달리 실제 판단은 매우 적대적이다. 내가 네 개를 만들면 상대는 막아야 하고, 상대가 활삼을 만들면 나는 다음 수를 예측해야 한다. 즉, 오목의 환경은 가만히 있는 물리 시스템이 아니라 나를 이기려는 상대다.

이 프로젝트는 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 등 여러 상대 구성을 지원한다.

로컬 AI: 첫 번째 상대

적대적 학습에는 상대가 필요하다. 이 프로젝트의 첫 상대는 src/aiplayer.cpp의 로컬 AI다. 이 AI는 신경망은 아니지만 중요한 역할을 한다. VLM이 처음부터 무작위로만 대국하면 좋은 수를 배우기 어렵기 때문에, 어느 정도 규칙을 아는 상대가 필요하다.

  • 내가 즉시 이길 수 있는 자리가 있으면 그곳에 둔다.
  • 상대가 다음 수에 이길 수 있으면 먼저 막는다.
  • 내 패턴 점수와 상대 패턴 점수를 함께 계산한다.
  • 벽에 붙은 약한 3/4 모양에는 큰 페널티를 준다.
  • 금수 위치에는 두지 않는다.

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 데이터로 저장된 오목 보드 이미지 예시

LoRA 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처럼 여러 구현 방식이 있다는 것을 이해하게 된다.

실행 흐름

  1. 오목 앱 실행: Qt 오목 프로그램을 실행하고 AI vs VLM 또는 VLM vs VLM 모드를 선택한다.
  2. VLM 서버 실행: vlm_pva_server.py를 --log-jsonl 옵션과 함께 실행한다.
  3. 대국 수집: 자동 재시작을 켜서 여러 판의 승패와 수순을 누적한다.
  4. 데이터셋 변환: dataset_omook.py로 승자의 수를 SFT 예제로 변환한다.
  5. LoRA 학습: train_vlm.py로 VLM 정책을 미세조정한다.
  6. 재평가: 새 LoRA를 서버에 얹고 다시 같은 상대와 대국시켜 승률 변화를 본다.

실행 예시

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 출력을 감지하고 막는다.

평가

같은 상대와 반복 대국해 승률과 오류율을 확인한다.

주의할 점

  • 승자의 모든 수가 항상 좋은 수는 아니다. 상대 실수 덕분에 이긴 수가 섞일 수 있다.
  • VLM 자기 응답을 그대로 학습하면 모델의 나쁜 습관이 강화될 수 있다.
  • 데이터가 한 상대에게만 치우치면 그 상대에게만 강한 정책이 될 수 있다.
  • LoRA 학습률이 너무 크면 출력이 collapse될 수 있어 손실과 출력 검사가 필요하다.
  • 좌표 형식과 보드 좌표계가 흔들리면 좋은 정책도 실제 게임에서 실패한다.

마무리

오목 프로그램은 적대적 강화학습의 핵심을 잘 보여 준다. 에이전트는 혼자 환경을 탐험하는 것이 아니라, 자신을 이기려는 상대와 계속 부딪힌다. 상대가 바뀌면 좋은 수의 의미도 바뀐다. 그래서 오목에서 학습은 단순한 정답 암기가 아니라, 상대의 위협을 읽고 자신의 위협을 만드는 정책 개선 과정이다.

이 프로젝트의 학습 방식은 전통적인 강화학습 알고리즘과 완전히 같지는 않다. 하지만 대국으로 데이터를 만들고, 승리한 행동을 모아 정책을 업데이트하고, 다시 강해진 정책을 상대와 붙이는 반복 루프는 적대적 학습의 감각을 매우 잘 전달한다. 이 장의 목표는 바로 그 구조를 독자가 손에 잡히게 이해하도록 만드는 것이다.

연습 문제

  • 패배한 쪽의 수까지 학습 데이터에 포함하면 어떤 장단점이 생길지 설명해 보자.
  • VLM vs VLM 데이터만으로 학습하면 모델의 약점이 어떻게 고착될 수 있을까?
  • 승자의 모든 수를 같은 가중치로 학습하는 대신, 승리 직전 5수에 더 큰 가중치를 주면 어떤 변화가 생길까?
  • 불법 좌표 보정을 무작위 빈 칸이 아니라 휴리스틱 AI 후보 중 하나로 바꾸면 어떤 효과가 있을까?
CHAPTER 06 · 오목으로 이해하는 적대적 강화학습1 / 1
책장을 누르거나 좌우로 밀어 넘기기
CHAPTER 06

오목으로 이해하는 적대적 강화학습

댓글 0

아직 댓글이 없습니다.