Las personas miran un número cada vez
La forma de resolver de las personas descrita en solve.md es así. Se lee el tablero y se elige un número. Si se tachan las filas, columnas y cajas donde ese número ya está, aparece una caja o una línea en la que solo queda un sitio. Esa casilla es la respuesta al 100%. Se rellena y se vuelve a leer el tablero.
Así que reduje a dos las tareas que se le encargan al VLM.
- Leer el tablero: leer tal cual los números de las 81 casillas.
- Elegir un número y escribir todas las casillas donde ese número no puede ir.
Nunca se le pregunta qué casilla es la respuesta. Es el código el que cuenta si, tras tachar las casillas donde no puede ir el número, queda un solo sitio.

El 8B antes de entrenar
Cambié el modelo a Qwen3-VL-8B-Instruct. Al ponerle 20 problemas sin LoRA, acertó el 100% de las coordenadas, pero solo leyó correctamente las 81 casillas en el 20% de los tableros (83% por casilla). En las casillas donde no puede ir el número 1, acertó un 91% por casilla, pero no hubo ni un tablero acertado sin fallos.
Entrenamiento por etapas
Entrené las dos tareas por separado. En cada ciclo se hace una prueba, se añaden los problemas fallados y se vuelve a entrenar.
- Leer el tablero: el 2B pasó del 17,5% al 100% en 4 ciclos, y el 8B del 25,2% al 100% en 2 ciclos.
- Casillas donde no puede ir el número 1: el 2B subió hasta el 69,5% en 9 ciclos, y el 8B hasta el 97,6% en 15 ciclos. Por casilla, eso es un 99,98%.
A diferencia de la entrada 2, cuanto más entrenaba, más mejoraba. Es porque el fundamento de la respuesta está dentro del tablero.

El código real de este paso
Este es el prompt que se le da al VLM. Hace que elija un número y escriba solo las casillas donde no puede ir.
vlm_prompt.py
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)Esta es la función que genera las respuestas correctas para el entrenamiento. Se deciden solo según si ese número ya está en la fila, la columna o la caja.
vlm_board.py
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 outLa siguiente entrada es "4. Comparación de las resoluciones antes y después del entrenamiento".
COMMENTS · CHAPTER 03
Convertir la forma de resolver de las personas en reglas 0 comentarios
Todavía no hay comentarios. Deja una pregunta o algo que quieras añadir.
Para publicar un comentario, empieza con Google y registra un apodo y un correo.
Empezar con Google