Material a preparar

SGF é um formato de texto que guarda o tamanho do tabuleiro, as jogadas das pretas e das brancas e o resultado da partida. Primeiro, reúna apenas registros cuja origem e condições de reutilização você já verificou. Mais importante do que a quantidade de arquivos é filtrar os registros que falham na leitura, os tabuleiros que não são 19×19 e as partidas com coordenadas de jogada corrompidas.

  1. Verificar se o tamanho do tabuleiro é 19×19.
  2. Reproduzir as jogadas das pretas e das brancas desde o início.
  3. Salvar como entrada o tabuleiro logo antes de cada jogada.
  4. Salvar como resposta a coordenada realmente jogada em seguida.
  5. Guardar também o resultado da partida e o número da jogada.

Etapa 1: Reproduzir as jogadas em um tabuleiro vazio

Ao ler as coordenadas do SGF, converta as coordenadas em letras do arquivo original para linhas e colunas internas de 0 a 18 uma única vez. Depois disso, o motor de regras passa a usar apenas coordenadas numéricas. Salve uma jogada como dado, coloque essa pedra também no tabuleiro real, remova as pedras capturadas e passe para a próxima jogada.

Se a verificação de jogadas legais estiver ligada durante esse processo, os erros dos registros originais e do parser aparecem rapidamente. Se um registro normal for julgado como suicídio, muitas vezes o erro está na direção das coordenadas, na ordem de vez entre pretas e brancas ou na ordem de remoção das pedras.

Etapa 2: Dividir uma partida em vários exemplos de treinamento

Um registro de 100 jogadas não é um exemplo, e sim até 100 pares de entrada e resposta. O primeiro exemplo é o tabuleiro vazio e a primeira jogada das pretas; o segundo é o tabuleiro com uma pedra preta e a resposta das brancas. A entrada precisa sempre indicar de quem é a vez, para que no mesmo formato de tabuleiro dê para distinguir qual cor vai jogar.

Etapa 3: Verificar duplicatas e viés

Se o mesmo tabuleiro se repete em vários arquivos, o peso dele no treinamento pode ficar grande demais. Crie um hash combinando a disposição do tabuleiro, a vez atual e a informação de ko para reduzir as duplicatas. Manter apenas as jogadas do vencedor é simples, mas você perde todos os padrões de defesa e de erro que poderiam ser aprendidos com as jogadas do perdedor. No MariaGO, priorizamos os registros do vencedor, mas as posições com atari e captura recebem uma marcação separada para poderem ser avaliadas.

O que verificar

Sorteie 20 exemplos e marque você mesmo a jogada correta no tabuleiro de entrada. É preciso conferir a olho que esquerda e direita ou cima e baixo não estão invertidos, e que o alinhamento não se desloca nos números de coluna de dois dígitos. Estatísticas dizendo que há muitos dados nunca vão revelar erros de conversão de coordenadas.

O código real desta etapa

Esta função converte as coordenadas em letras do SGF para linhas e colunas de 0 a 18 uma única vez. Em um tabuleiro 19×19, tt é tratado como passe.

vlm_server/sgf_parser.pypython · 16 linhas
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)

Esta função divide uma partida em exemplos por jogada de "tabuleiro anterior, jogador da vez, coordenada correta". winning_side_only corresponde a priorizar as jogadas do vencedor, e tactical_bias a marcar as posições de atari e captura.

vlm_server/dataset_baduk.pypython · 80 linhas
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)

Modelo usado: Qwen3-VL-8B-Instruct + adaptador LoRA (lora_mix_qwen_r5). O código-fonte completo e a descrição do modelo estão no post "8. O código-fonte completo e os modelos usados".