
需要准备的资料
SGF是一种以文本形式保存棋盘大小、黑白落子和对局结果的格式。首先,只收集已经确认过出处和再利用条件的棋谱。比文件数量更重要的,是筛掉解析失败的棋谱、不是19×19的棋盘以及落子坐标损坏的对局。
- 检查棋盘大小是否为19×19。
- 从头重放黑白双方的着法顺序。
- 把每一手落子之前的盘面保存为输入。
- 把实际下出的下一个坐标保存为正确答案。
- 同时保留对局结果和手数编号。
第1步:在空棋盘上重放着法
读取SGF坐标时,只把原始文件中的字母坐标一次性转换为内部的0~18行、列。之后统一让规则引擎只使用数字坐标。把一手保存为数据后,再把那颗棋子放到实际棋盘上,移除被提的棋子,然后进入下一手。
如果在这个过程中开启合法着法检查,就能很快发现原始棋谱和解析器的错误。如果正常棋谱被判定为自杀,多半是坐标方向、黑白轮次或提子顺序中的某一项出了错。
第2步:把一局拆分成多个训练样本
一份100手的棋谱不是一个样本,而是最多100个输入与正确答案的配对。第一个样本是空棋盘和黑棋的第一手,第二个样本是放了一颗黑子的盘面和白棋的应手。输入中必须标明当前轮到谁,这样在相同的盘面形状下才能区分该哪种颜色落子。
第3步:确认重复和偏差
如果同一个盘面在多个文件中反复出现,它在训练中的比重可能会变得过大。把棋盘布局、当前轮次和劫的信息组合起来生成哈希,以减少重复。只保留胜方着法的做法虽然简单,但会失去从败方着法中能学到的所有防守和失误模式。在MariaGO中,我们优先使用胜方棋谱,但会把有叫吃和提子的局面单独做标记,以便进行评估。
需要确认的结果
随机抽取20个样本,亲手在输入盘面上标出正确着法。必须用肉眼确认左右或上下没有颠倒,以及在两位数的列号处对齐没有错位。仅凭数据量很多的统计,是发现不了坐标转换错误的。
这一步的实际代码
这个函数把SGF的字母坐标一次性转换为0~18的行、列。在19×19棋盘上,tt被当作停一手处理。
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)这个函数把一局按每一手拆分成“上一盘面、轮到谁、正确坐标”的样本。winning_side_only对应优先使用胜方着法,tactical_bias对应给叫吃、提子局面做标记。
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)使用的模型:Qwen3-VL-8B-Instruct + LoRA适配器(lora_mix_qwen_r5)。完整源代码和模型说明可以在“8. 完整源代码与使用的模型”一文中查看。
COMMENTS · CHAPTER 02
把SGF棋谱变成训练数据 评论 0
还没有评论。阅读时有疑问或想补充的内容,欢迎留言。
发表评论前,请用 Google 开始并登记昵称和邮箱。
用 Google 开始