사람은 숫자 하나씩 본다

solve.md에 적은 사람의 풀이는 이렇습니다. 판을 읽고 숫자 하나를 고릅니다. 그 숫자가 이미 있는 행·열·박스를 지우면 자리가 하나만 남는 박스나 줄이 생깁니다. 그 칸은 100% 답입니다. 채우고 다시 판을 읽습니다.

그래서 VLM에게 시키는 일을 둘로 줄였습니다.

  1. 판 읽기: 81칸의 숫자를 그대로 읽습니다.
  2. 숫자 하나를 정해 그 숫자를 둘 수 없는 칸을 모두 적습니다.

어느 칸이 정답인지는 묻지 않습니다. 둘 수 없는 칸을 지우고 남은 자리가 하나뿐인지는 코드가 셉니다.

숫자 1을 둘 수 없는 칸을 지우면 남는 자리
숫자 1을 둘 수 없는 칸을 지우면 남는 자리

학습 전의 8B

모델은 Qwen3-VL-8B-Instruct로 바꿨습니다. LoRA 없이 20문제를 시켜 보니 좌표는 100% 맞혔지만 81칸을 모두 맞게 읽은 판은 20%(칸 단위 83%)였습니다. 숫자 1을 둘 수 없는 칸은 칸 단위로 91%를 맞혔지만 빠짐없이 맞힌 판은 하나도 없었습니다.

단계별 학습

두 일을 따로 학습했습니다. 사이클마다 시험을 보고 틀린 문제를 더해 다시 학습합니다.

  • 판 읽기: 2B는 17.5%에서 4사이클 만에 100%, 8B는 25.2%에서 2사이클 만에 100%가 됐습니다.
  • 숫자 1을 둘 수 없는 칸: 2B는 9사이클 동안 69.5%까지 올랐고, 8B는 15사이클에 97.6%까지 올랐습니다. 칸 단위로는 99.98%입니다.

2편과 달리 학습할수록 올랐습니다. 정답의 근거가 판 안에 있는 일이기 때문입니다.

판 읽기와 숫자 1을 둘 수 없는 칸을 빠짐없이 맞힌 비율
판 읽기와 숫자 1을 둘 수 없는 칸을 빠짐없이 맞힌 비율

이 단계의 실제 코드

VLM에 주는 프롬프트입니다. 숫자 하나를 정해 둘 수 없는 칸만 적게 합니다.

vlm_prompt.pypython · 28줄
def build_digit_illegal_prompt(
    board: list[list[int]],
    given_mask: list[list[bool]],
    digit: int,
    *,
    retry_hints: list[str] | None = None,
) -> str:
    """digit 의 비합법수(둘 수 없는 모든 칸) 나열."""
    board_txt = board_to_text(board, given_mask)
    placed = sum(1 for r in range(9) for c in range(9) if board[r][c] == digit)
    prompt = (
        "You are solving Sudoku like a human (solve.md digit tracking).\n"
        f"{_COORD_HEADER}"
        f"{_HUMAN_RULES}"
        "The board has been read. Now track ONE digit on the number pad.\n"
        f"TARGET DIGIT: {digit} (already placed {placed} times on the board).\n\n"
        f"Task: list EVERY cell where digit {digit} CANNOT be placed.\n"
        "A cell is ILLEGAL for this digit if:\n"
        f"- it is filled with a digit other than {digit}, OR\n"
        f"- it is EMPTY but its row, column, or 3×3 box already contains {digit}.\n"
        f"Do NOT list cells that already contain {digit} — those are legal placements.\n"
        "You MUST include ALL illegal cells. Do not omit any.\n"
        "Use compact JSON (no spaces) to fit all cells.\n\n"
        f"Text grid:\n{board_txt}\n\n"
        f'Return ONLY JSON: {{"digit":{digit},"illegal":[[row,col],...]}}\n'
        f'Example: {{"digit":{digit},"illegal":[[0,3],[1,5],[2,8]]}}\n'
    )
    return _append_retry_hints(prompt, retry_hints)

학습 정답을 만드는 함수입니다. 행·열·박스에 그 숫자가 이미 있는지만으로 정해집니다.

vlm_board.pypython · 18줄
def illegal_cells_for_digit(
    board: list[list[int]], digit: int
) -> list[tuple[int, int]]:
    """digit 을 둘 수 없는 모든 칸 (이미 digit 인 칸은 제외)."""
    from sudoku_human_solver import cell_candidates

    out: list[tuple[int, int]] = []
    for row in range(9):
        for col in range(9):
            v = board[row][col]
            if v == digit:
                continue
            if v != 0:
                out.append((row, col))
                continue
            if digit not in cell_candidates(board, row, col):
                out.append((row, col))
    return out

다음 글은 ‘4. 학습 전과 후의 풀이 비교’입니다.