처음 본 풀이
[소개](/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.py
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. 시행착오로는 배우지 못했다’입니다.
COMMENTS · CHAPTER 01
답을 아는 아이처럼 푸는 AI 댓글 0
아직 댓글이 없습니다. 읽다가 궁금한 점이나 보탤 내용을 남겨 주세요.
댓글을 등록하려면 구글로 시작하기를 눌러 닉네임과 메일 주소를 등록해 주세요.
구글로 시작하기