人は数字を一つずつ見る

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. 学習前と学習後の解き方の比較」です。