처음 본 풀이

[소개](/about/)에 적은 장면이 이 모델입니다. 스도쿠를 푸는 AI가 첫 칸부터 가로로 차례차례 답을 채워 나갔습니다. 답을 이미 아는 어린아이 같았습니다.

정답표로 만든 학습 데이터

첫 학습 데이터는 dataset_sudoku.py로 만들었습니다. 엔진이 퍼즐과 정답을 함께 만들고, 판에서 가장 앞에 있는 빈칸(왼쪽 위부터 가로 순서)에 정답 숫자를 적은 것을 한 문제로 저장합니다. 그 칸을 채우고 다음 빈칸으로 넘어가 판이 찰 때까지 반복합니다.

쉬움·보통·어려움 300판에서 13,497문제가 나왔고, Qwen3-VL-2B-Instruct에 LoRA로 학습했습니다(lora_stoku).

프롬프트에는 둘 수 있는 수 목록도 넣었습니다. 하지만 정답은 늘 첫 빈칸이었습니다. (0,3)에 5와 9가 모두 들어갈 수 있어도 정답표가 9라면 9가 정답입니다. 왜 9인지는 데이터 어디에도 없습니다.

어려움 퍼즐 한 판을 채운 순서. 붉은 칸은 그 순간 판만 보고는 정할 수 없던 칸
어려움 퍼즐 한 판을 채운 순서. 붉은 칸은 그 순간 판만 보고는 정할 수 없던 칸

그 순간 풀 수 있는 칸이었나

13,497문제의 정답 칸을 그 순간의 판만 보고 다시 따져 봤습니다.

  • 후보 숫자가 하나뿐인 칸: 9,502개(70%)
  • 행·열·박스 안에서 그 숫자가 들어갈 자리가 하나뿐인 칸: 2,127개(16%)
  • 두 방법으로 정할 수 없어 정답표가 있어야 하는 칸: 1,868개(14%)

정답표가 있어야 하는 칸은 어려움에서 21%, 보통에서 12%, 쉬움에서 5% 정도였습니다. 사람이라면 이런 칸은 건너뛰고 확실한 칸부터 채웁니다. 모델은 같은 자리에 정답표의 숫자를 적도록 배웠습니다.

난이도별로 정답 칸이 그 순간 확정 가능했는지
난이도별로 정답 칸이 그 순간 확정 가능했는지

이 단계의 실제 코드

첫 학습 데이터를 만드는 함수입니다. empties[0]이 왼쪽 위부터 센 첫 빈칸이고, 값은 engine.solution에서 가져옵니다.

dataset_sudoku.pypython · 42줄
def generate_game_samples(
    engine: SudokuEngine,
    difficulty: str,
    game_id: int,
    image_dir: Path,
) -> list[dict]:
    engine.new_game(difficulty)
    board = [row[:] for row in engine.puzzle]
    given_mask = [[engine.is_given(r, c) for c in range(9)] for r in range(9)]
    samples: list[dict] = []
    step = 0

    while True:
        empties = [(r, c) for r in range(9) for c in range(9) if board[r][c] == 0]
        if not empties:
            break

        r, c = empties[0]
        value = engine.solution[r][c]

        img = board_to_image(board, given_mask)
        img_name = f"g{game_id:05d}_s{step:02d}.png"
        img_path = image_dir / img_name
        img.save(img_path)

        answer = json.dumps({"row": r, "col": c, "value": value}, ensure_ascii=False)
        prompt = build_sudoku_prompt(board, given_mask)

        samples.append(
            {
                "id": f"g{game_id:05d}_s{step:02d}",
                "image_path": str(img_path.resolve()),
                "prompt": prompt,
                "answer": answer,
                "difficulty": difficulty,
            }
        )

        board[r][c] = value
        step += 1

    return samples

다음 글은 ‘2. 시행착오로는 배우지 못했다’입니다.