처음 실패한 이유

초기 모델은 판의 오른쪽 아래 좌표를 지나치게 자주 답했습니다. 기보 부족처럼 보였지만 실제 원인은 입력 형식이었습니다. 학습 데이터에는 숫자가 붙은 정렬된 판을 사용했고, 실제 서버에는 공백 간격이 다른 판을 보냈습니다. 특히 열 번호가 두 자리로 바뀌는 지점부터 돌의 위치를 한 칸씩 잘못 읽었습니다.

Gemma 4 E4B에 LoRA를 붙인 첫 모델은 좌표 시험 16문항 중 하나도 맞히지 못했습니다. Qwen3-VL-8B로 바꾼 뒤에도 수 데이터 400개만으로 짧게 학습한 어댑터는 2.1%까지 떨어졌습니다. 그래서 좌표 전용 어댑터를 먼저 만들고 그 위에서 수를 학습했습니다.

좌표 시험 정확도. Gemma 4 E4B + LoRA 0%, Qwen 수 데이터 LoRA 2.1%, Qwen3-VL-8B 기본 79.2%, 좌표 LoRA 100%
좌표 시험 정확도. Gemma 4 E4B + LoRA 0%, Qwen 수 데이터 LoRA 2.1%, Qwen3-VL-8B 기본 79.2%, 좌표 LoRA 100%

1단계: 좌표 폭을 고정합니다

행과 열은 0, 1, 2가 아니라 00, 01, 02처럼 두 자리로 표현합니다. 빈 점, 흑돌과 백돌도 각각 한 글자로 고정하고 구분 공백 수를 바꾸지 않습니다. 학습 데이터를 만드는 함수와 실제 대국 요청을 만드는 함수가 따로 있더라도 최종 문자열 생성기는 하나를 공유하는 것이 안전합니다.

  1. 열 머리글을 00부터 18까지 출력합니다.
  2. 각 행 앞에도 00부터 18까지 번호를 붙입니다.
  3. 빈 점·흑·백 기호의 폭을 같게 맞춥니다.
  4. 현재 차례와 금지 좌표는 판 아래 고정 위치에 적습니다.

2단계: 좌표 시험을 먼저 통과시킵니다

기력 학습 전에 임의의 한 점에 돌을 놓고 좌표를 답하게 합니다. 중앙만 시험하지 말고 좌상, 우상, 좌하, 우하 네 구역에서 같은 수의 문제를 만듭니다. 전체 정확도가 높아도 오른쪽 아래만 낮으면 실제 대국에서 방향 편향이 생깁니다.

기본 Qwen3-VL-8B도 오른쪽 아래 정확도가 66.7%로 네 구역 중 가장 낮았습니다.

네 구역별 좌표 정확도. 기본 모델 좌상 80, 우상 69, 좌하 93, 우하 67%. 운영 모델은 모두 100%
네 구역별 좌표 정확도. 기본 모델 좌상 80, 우상 69, 좌하 93, 우하 67%. 운영 모델은 모두 100%

좌표 시험은 모델을 새로 학습하거나 프롬프트를 바꿀 때마다 다시 실행합니다. 정답률과 함께 틀린 답의 방향도 저장합니다. 한 칸 오른쪽으로 반복해서 틀리는지, 행과 열을 바꾸는지에 따라 입력 정렬과 출력 파서를 따로 점검할 수 있습니다.

3단계: 출력 형식을 좁힙니다

모델이 긴 설명을 답하면 좌표 파서가 불안정해집니다. 정상 출력은 하나의 좌표 또는 PASS로 제한합니다. 설명이 붙어도 첫 좌표만 몰래 사용하는 방식은 잘못된 수를 숨길 수 있으므로 형식 오류로 기록하고 정해진 횟수만 다시 요청합니다.

좌표 정확도가 확보되기 전에는 더 많은 기보를 넣어도 잘못 읽는 습관만 강화될 수 있습니다.

이 단계의 실제 코드

열 머리글, 행 번호와 돌 기호를 모두 두 글자 폭으로 맞춘 판 문자열 생성기입니다. 학습 데이터 생성과 실제 수 추천 서버가 이 함수를 함께 사용해 입력 형식이 어긋나지 않게 했습니다.

vlm_server/dataset_baduk.pypython · 16줄
def board_text_aligned(cells: bytes, size: int = BOARD_SIZE) -> str:
    """v3 권장: 열·행·셀이 모두 두 글자 토큰으로 정렬.

    셀 기호는 **왼쪽 정렬** (``X `` / ``. ``). 오른쪽 정렬(`` X``)은
    모델이 열을 한 칸 왼쪽(col-1)으로 읽는 오차를 유발했다.
    """
    col_header = "   " + " ".join(f"{c:02d}" for c in range(size))
    lines: List[str] = [col_header]
    for r in range(size):
        cells_s: List[str] = []
        for c in range(size):
            v = cells[r * size + c]
            ch = "." if v == 0 else ("X" if v == 1 else "O")
            cells_s.append(f"{ch:<2}")
        lines.append(f"{r:02d} " + " ".join(cells_s))
    return "\n".join(lines)

모델의 정답 출력은 좌표 하나 또는 패스만 담은 JSON 한 줄로 제한합니다.

vlm_server/dataset_baduk.pypython · 5줄
def assistant_text(point: Optional[Tuple[int, int]]) -> str:
    if point is None:
        return '{"pass": true}'
    r, c = point
    return f'{{"row": {r}, "col": {c}}}'

사용한 모델: Qwen3-VL-8B-Instruct + LoRA 어댑터(lora_mix_qwen_r5). 전체 소스와 모델 설명은 ‘8. 전체 소스 코드와 사용한 모델’ 글에서 볼 수 있습니다.