
What to prepare
SGF is a text format that stores the board size, Black's and White's moves and the game result. First, collect only records whose source and reuse terms you have checked. More important than the number of files is filtering out records that fail to parse, boards that are not 19×19 and games with broken move coordinates.
- Check that the board size is 19×19.
- Replay Black's and White's moves from the start.
- Save the board just before each move as the input.
- Save the coordinate actually played next as the target.
- Keep the game result and the move number as well.
Step 1: Replay the moves on an empty board
When reading SGF coordinates, convert the letter coordinates of the original file into internal 0–18 rows and columns exactly once. After that, the rule engine uses only numeric coordinates. Save one move as data, then place that stone on the real board, remove any captured stones and move on to the next move.
If legal-move checking is switched on during this process, errors in the original records and in the parser show up quickly. If a normal game record is judged to contain suicide, usually the coordinate direction, the turn order or the order of removing stones is wrong.
Step 2: Split one game into many training examples
A 100-move record is not one example but up to 100 input/target pairs. The first example is the empty board and Black's first move; the second is the board with one black stone and White's reply. The input must always show whose turn it is, so that the same board shape can be told apart by which colour is to play.
Step 3: Check for duplicates and bias
If the same board repeats across many files, its weight in training can grow too large. Build a hash from the board layout, the current turn and the ko information to reduce duplicates. Keeping only the winner's moves is simple, but you lose all the defence and mistake patterns that could be learned from the loser's moves. In MariaGO we prefer the winner's records, but positions with atari and captures are tagged separately so they can be evaluated.
What to check
Pick 20 random examples and mark the target move on the input board yourself. You have to check by eye that left and right or top and bottom are not flipped, and that the alignment does not shift at the two-digit column numbers. Statistics saying there is a lot of data will never reveal coordinate conversion errors.
The actual code for this step
This function converts SGF letter coordinates into 0–18 rows and columns exactly once. On a 19×19 board, tt is treated as a pass.
vlm_server/sgf_parser.py
def _decode_point(s: str, size: int) -> Optional[Tuple[int, int]]:
if not s:
return None
s = s.strip().lower()
if not s:
return None
# SGF: 'tt' 는 19x19 에서 패스 관용 표기
if size == 19 and s == "tt":
return None
if len(s) < 2:
return None
col = ord(s[0]) - ord("a")
row = ord(s[1]) - ord("a")
if not (0 <= col < size and 0 <= row < size):
return None
return (row, col)This function splits a game into per-move examples of "previous board, player to move, target coordinate". winning_side_only corresponds to preferring the winner's moves, and tactical_bias to tagging atari and capture positions.
vlm_server/dataset_baduk.py
def iter_training_samples(
game: SgfGame,
max_per_game: int,
rng: random.Random,
winning_side_only: bool = False,
winner: Optional[int] = None,
tactical_bias: bool = False,
tactics_only: bool = False,
) -> Iterator[Tuple[bytes, int, Optional[Tuple[int, int]], bool]]:
"""게임 1개에서 (board_before_move, ai_side, target_move, is_tactical) 4-tuple yield."""
if game.size != BOARD_SIZE:
return
sim = _GoSim(BOARD_SIZE)
for r, c in game.setup_black:
sim.set(r, c, 1)
for r, c in game.setup_white:
sim.set(r, c, 2)
# tactics_only 든 tactical_bias 든 어느 쪽이라도 분류기를 켜야 한다.
need_tactical_flag = tactical_bias or tactics_only
states: List[Tuple[bytes, int, Optional[Tuple[int, int]], bool]] = []
# (snap_before, side, point, is_tactical)
for color, point in game.moves:
side = 1 if color == "B" else 2
if winning_side_only and winner is not None and side != winner:
if point is not None:
sim.play(point[0], point[1], side)
continue
snap = sim.packed()
is_tac = _is_tactical_move(sim, side, point) if need_tactical_flag else False
states.append((snap, side, point, is_tac))
if point is not None:
sim.play(point[0], point[1], side)
if not states:
return
if tactics_only:
# 전술 순간만 yield (max_per_game 무시).
for snap, side, point, is_tac in states:
if is_tac:
yield (snap, side, point, True)
return
if max_per_game <= 0 or len(states) <= max_per_game:
chosen_indices = list(range(len(states)))
else:
if tactical_bias:
# 절반은 전술 순간 우선, 나머지는 균등.
tactical_idx = [i for i, st in enumerate(states) if st[3]]
half = max_per_game // 2
if len(tactical_idx) > half:
rng.shuffle(tactical_idx)
tactical_pick = sorted(tactical_idx[:half])
else:
tactical_pick = sorted(tactical_idx)
need = max_per_game - len(tactical_pick)
# 나머지 균등 샘플 (tactical_pick 와 중복 피함)
taken = set(tactical_pick)
uniform: List[int] = []
if need > 0:
step = max(1.0, len(states) / need)
k = 0
while len(uniform) < need and k < len(states):
idx = int(k * step)
if idx >= len(states):
break
if idx not in taken:
uniform.append(idx)
taken.add(idx)
k += 1
chosen_indices = sorted(set(tactical_pick + uniform))
else:
step = len(states) / max_per_game
chosen_indices = sorted(set(int(i * step) for i in range(max_per_game)))
for i in chosen_indices:
snap, side, point, is_tac = states[i]
yield (snap, side, point, is_tac)Model used: Qwen3-VL-8B-Instruct + LoRA adapter (lora_mix_qwen_r5). The full source and a description of the model are in the post "8. The full source code and the models used".
COMMENTS · CHAPTER 02
Turning SGF game records into training data 0 comments
No comments yet. Leave a question or anything you would like to add.
To post a comment, continue with Google and register a nickname and email address.
Continue with Google