ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Python实现AlphaGo式围棋AI:MCTS与策略价值网络实战

Python实现AlphaGo式围棋AI:MCTS与策略价值网络实战 简介这份压缩包是基于Minigo框架的幻影围棋项目源码面向围棋AI爱好者与Python开发者借助卷积神经网络与蒙特卡洛树搜索实现人机对战和AI自我对弈。包内共18个文件以14个Python脚本为主涵盖棋局特征提取、对称变换、双网络模型、对弈逻辑与图形界面等模块另有模型权重文件与简易说明文档整体仅1.47MB结构紧凑便于快速上手。当前已有925人学习下载。通过阅读Ghost1.py、ghost_vs_human.py与ghost_vs_ghost.py等代码可以了解Minigo在围棋博弈中的完整落地流程掌握从棋盘坐标处理到MCTS决策的工程化实现。项目中幽灵对战、随机对战等Demo还能辅助调试和验证棋力适合作为学习AlphaGo系列算法与Python AI开发的实践范本。1. 幻影围棋 Ghost-master一个把策略与价值装进 MCTS 的 Python 围棋引擎如果你用 gnugo 这类传统引擎和 Ghost-master 下过棋第一感觉会是它的棋风「不传统」——开局不急于守角中盘对杀时会突然脱先抢占大场官子阶段对半目胜负的判断又异常敏感。这不是因为它内置了多少棋谱定式而是因为它的决策核心是「策略网络 价值网络 蒙特卡洛树搜索」这套 AlphaGo 系的架构。Ghost-master幻影围棋正是这样一个用 Python 完整实现这三件套的围棋 AI模型在本地即可训练和推理不需要 Tesla 级别的显卡也能跑出可对弈的棋力。它适合两类读者想彻底搞懂 MCTS 的搜索过程如何与神经网络协同工作的人以及希望在自己笔记本上复现一整套训练闭环的 Python 工程师。2. 棋盘、规则与快速落子Python 中围棋状态的表示与提子判断围棋 AI 的地基不是神经网络而是棋盘状态表示。如果这一步的数据结构选得不对后续无论是 MCTS 的模拟次数还是网络的 batch 推理都会被拖慢。Ghost-master 采用 19 路棋盘但代码里通过参数控制可以随时切换到 13 路或 9 路训练。2.1 棋盘编码二维数组与「气」的快速计算最常见的做法是直接用list嵌套实现二维棋盘0表示空、1表示黑子、2表示白子。这个结构简单直观但在判断「气」的时候需要反复做邻域扫描。Ghost 的优化点是每次落子后只更新局部区域的气而不是整盘重算。class GoBoard: def __init__(self, size19): self.size size self.board [[0] * size for _ in range(size)] self.ko None # 劫争位置 def neighbors(self, x, y): res [] for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)): nx, ny x dx, y dy if 0 nx self.size and 0 ny self.size: res.append((nx, ny)) return res def compute_liberty(self, x, y): 计算包含 (x, y) 的整块棋的气。 color self.board[x][y] if color 0: return 0 visited set() stack [(x, y)] liberty set() while stack: cx, cy stack.pop() if (cx, cy) in visited: continue visited.add((cx, cy)) for nx, ny in self.neighbors(cx, cy): if self.board[nx][ny] 0: liberty.add((nx, ny)) elif self.board[nx][ny] color and (nx, ny) not in visited: stack.append((nx, ny)) return len(liberty)这段代码用深度优先搜索遍历同色连通块并把空点记录为气。visited集合防止重复访问liberty用集合存储天然去重。整个过程的复杂度与该连通块大小成正比落子后只有局部区域需要调用避免了每次全盘扫描的 O(N²) 开销。2.2 提子、打劫与自杀检测的边界处理提子逻辑相对直接落子后先找对手被围死的块并移除再检查自己是否也无气。真正的坑在于劫争处理——如果提子后棋盘状态与上一步完全相同该落子就是违规的。def is_legal(self, x, y, color): if self.board[x][y] ! 0: return False # 模拟落子 self.board[x][y] color opponent 3 - color captured [] for nx, ny in self.neighbors(x, y): if self.board[nx][ny] opponent: if self.compute_liberty(nx, ny) 0: captured.extend(self._collect_group(nx, ny)) # 先提子再判断自己是否无气 for cx, cy in captured: self.board[cx][cy] 0 if self.compute_liberty(x, y) 0: self.board[x][y] 0 return False # 劫争检测比较提子后的状态是否和上一步一致 if captured and self._same_state(self.previous_board): self.board[x][y] 0 return False self.board[x][y] 0 return True注意这里先用模拟落子、再提子、最后恢复现场的顺序。_collect_group返回整块棋的坐标列表_same_state比较当前棋盘与previous_board是否完全一致。实际运行中劫争判断最容易被忽略的是「隔多手后恢复原状」的循环劫但规则只禁止立即重复同一局面所以这种简单比较已经足够。提示在训练自对弈数据时合法落子数组legal_mask是网络输出的关键约束。如果漏掉打劫判断生成的棋谱会有少量非法落子导致模型学到错误的棋形认知。3. MCTS 搜索与策略价值网络Ghost-master 的决策内核上一章的棋盘类解决了「状态怎么存」这一章要解决「下一步走哪」。Ghost-master 的核心是蒙特卡洛树搜索MCTS但它的「模拟」环节并不是随机走子到终局而是用策略网络给出先验概率、用价值网络评估局面两者共同引导搜索方向。这也是它区别于传统 MCTS 围棋程序的关键。3.1 从 UCB 到 PUCT搜索树如何平衡探索与利用标准的 UCB1 公式只适用于胜率已知的拉杆问题棋类搜索需要把先验概率也加入评估。Ghost-master 使用 PUCT 公式Q(s, a) c_puct * P(s, a) * sqrt(N_parent) / (1 N_child)其中Q(s, a)是当前节点的平均胜率P(s, a)是策略网络给出的落子概率N_parent是父节点的访问次数N_child是当前子节点被访问的次数。c_puct控制探索强度——值越大越倾向于探索低访问次数的节点值越小越依赖已有胜率评估。class MCTSNode: def __init__(self, parentNone, prior0.0): self.parent parent self.children {} self.visit_count 0 self.total_value 0.0 self.prior prior def select_child(self, c_puct1.5): best_score -float(inf) best_child None for child in self.children.values(): q_value child.total_value / (child.visit_count 1e-8) ucb_score q_value c_puct * child.prior * \ (self.visit_count ** 0.5) / (1 child.visit_count) if ucb_score best_score: best_score ucb_score best_child child return best_child选择阶段从根节点出发反复调用select_child直到叶节点。total_value累加的是价值网络对局面的评估而不是真实对弈的胜负。每次搜索迭代都会让指向「当前最优」的路径访问次数增加下一轮 PUCT 又会对其他分支适当放行形成动态平衡。3.2 网络结构把棋盘卷积成直觉Ghost-master 的策略价值网络共享前半部分卷积层在末端分成两个头策略头输出 19×191 的落子概率分布价值头输出 [-1, 1] 范围内的胜率估值。import torch import torch.nn as nn class GhostNet(nn.Module): def __init__(self, board_size19, num_filters64): super().__init__() self.block nn.Sequential( nn.Conv2d(4, num_filters, 3, padding1), nn.BatchNorm2d(num_filters), nn.ReLU(), nn.Conv2d(num_filters, num_filters, 3, padding1), nn.BatchNorm2d(num_filters), nn.ReLU(), ) self.policy_head nn.Sequential( nn.Conv2d(num_filters, 2, 1), nn.BatchNorm2d(2), nn.ReLU(), nn.Flatten(), nn.Linear(2 * board_size * board_size, board_size * board_size 1) ) self.value_head nn.Sequential( nn.Conv2d(num_filters, 1, 1), nn.BatchNorm2d(1), nn.ReLU(), nn.Flatten(), nn.Linear(board_size * board_size, 64), nn.ReLU(), nn.Linear(64, 1), nn.Tanh() )输入层的 4 个通道分别是当前玩家棋子、对手棋子、上一手落子标记和当前轮次的颜色标记。policy_head多出的 1 维代表 Pass 落子。这个结构参考了 AlphaGo Zero 的简化版本去掉了 residual block 以降低训练开销棋力会有一定下降但对自学者来说训练速度的收益更明显。3.3 搜索循环中的效率瓶颈批量推理与缓存MCTS 搜索 800 次模拟意味着最多触发 800 次网络前向推理。如果每次只送一个局面进网络Python 的调用开销会远大于 GPU 计算本身。Ghost-master 的常见优化是把同批次多个叶子局面收集起来一次前向传播同时推理。另外网络对「对称局面」的判断可以复用——围棋棋盘有 8 种对称变换同一局面旋转后输入网络会得到不同结果但取平均可以提升稳定性。组件常见参数说明c_puct1.0 ~ 2.0探索权重训练初期调大评估时调小每手模拟次数400 ~ 1600训练时少、对弈时多与 GPU 显存相关温度参数1.0 → 0.1开局高温度鼓励探索终局低温度走最优num_filters64 ~ 128极低资源用 32棋力会明显下降温度参数在torch.softmax之后除以温度后再归一化温度大于 1 让概率分布更平坦小于 1 则聚焦到高概率点。4. 自对弈训练让幻影围棋从随机落子到理解棋理有了网络结构和 MCTS 搜索训练过程的核心是让模型成为自己的老师。Ghost-master 的训练管线采取自对弈数据生成 → 训练更新 → 对手评估的循环每一步都直接影响最终棋力。4.1 自对弈数据采集一条棋谱怎么变成训练样本自对弈时每一手棋都记录三个关键信息当前局面特征、MCTS 搜索后的落子概率分布、最终对局结果。注意这里的「概率分布」不是策略网络的直接输出而是 MCTS 各子节点访问次数归一化后的结果。这比网络原始输出有更多搜索信息相当于用更强的棋手来教网络。def collect_episode(model, num_simulations800): board GoBoard(19) samples [] players [1, 2] current 0 while not board.is_game_over(): temp 1.0 if len(board.move_history) 30 else 0.1 probs, winner mcts_search(model, board, num_simulations, temp) samples.append((make_features(board, players[current]), probs)) # 执行落子注意 P1 视角下黑棋胜率为 1白棋为 -1 board.apply_move(action_to_coord(probs.argmax().item())) current 1 - current # 更新胜率标签 orient -1.0 labeled [] for feature, prob in samples: winner_value 1.0 if board.winner players[0] else -1.0 labeled.append((feature, prob, winner_value)) return labeled这里的关键点是胜率标签始终从 P1 视角给出因此在后半局要乘上-1才能对齐特征。实际训练数据量大时直接把整盘棋的样本存成.npz文件而不是在内存中保留所有局面——Ghost-master 的默认做法是每盘棋存一个压缩包训练时用数据加载器按需读取。4.2 损失函数策略交叉熵加价值 MSEGhost-master 的损失函数由两部分组成policy_loss -torch.mean(torch.sum(target_probs * torch.log(policy_output 1e-8), dim1)) value_loss torch.mean((value_output.squeeze() - winner) ** 2) total_loss policy_loss value_loss策略头与目标分布做交叉熵价值头与胜负标签做均方误差。这里没有权重系数是因为两者的尺度接近加了权重反而需要额外调参。optimizer torch.optim.Adam(model.parameters(), lr0.001) for epoch in range(20): for batch in train_loader: optimizer.zero_grad() policy, value model(batch[features]) loss compute_loss(policy, value, batch[probs], batch[winner]) loss.backward() optimizer.step()训练 20 轮后模型开始表现出「棋感」接下来会进入评估环节。4.3 评估与迭代新老模型打一盘每次完整训练后用新模型与当前最优模型对弈若干局胜率超过 55% 才替换。这里的「对弈」不能只用网络输出直接下棋而要在相同模拟次数下走完整盘否则模型容易过拟合到训练数据的局部模式。Ghost-master 默认对弈 40 局胜率统计使用 95% 置信区间来判断是否显著。提示自对弈数据有一个容易被忽略的坑——如果每一盘棋都是在同一个温度策略下生成的数据分布会偏向「高探索」的下法。正确做法是温度从 1.0 线性递减到 0.2让前期棋谱多样化后期棋谱偏向最优落子。5. 本地部署、GTP 对弈与实战调优技巧训练好模型之后真正的考验是让 Ghost-master 稳定运行在本地并能与主流围棋界面交互。这里给出部署中最容易踩坑的几个环节和对应解法。5.1 通过 GTP 协议接入 SabakiGTPGo Text Protocol是围棋引擎与图形界面之间的标准协议。Ghost-master 实现 GTP 服务端的方式很简单读取标准输入解析play和genmove命令。import sys def gtp_loop(model): while True: line sys.stdin.readline() if not line: break cmd line.strip().split() if cmd[0] genmove: color cmd[1] move select_move(model, board, color) print(f {move}\n, flushTrue) elif cmd[0] play: color, vertex cmd[1], cmd[2] board.apply_move(parse_vertex(vertex)) print(\n, flushTrue)所有输出必须通过flushTrue立即发送否则界面会等待超时。常见报错ghost internal error25002大多出现在 GTP 通信中落子坐标格式不匹配的情况——GTP 使用A1到T19的坐标体系但跳过I字母解析时务必转换。5.2 必调参数与对弈体验优化本地部署时我建议优先调整这三个参数参数推荐范围效果--simulations400 起每手模拟次数翻倍棋力提升约 1 子--c-puct1.0 ~ 2.0调大让棋风更灵活适合测试布局--temperature0.1 固定对弈模式不要用 1.0否则会下出明显缓手如果模型在开局阶段频繁下出「点入空角」这种激进棋多半是训练时温度过高导致数据分布偏向探索。对策是把训练数据中前 30 手的温度上限压到 0.7并增加后期低温度棋谱的比例。反过来如果模型官子阶段总是差半目优先检查价值头的收敛情况——价值 loss 高于 0.25 通常说明局面评估不够准确。对于 GPU 显存不足的情况Ghost-master 支持把num_filters降为 32并减少 MCTS 批处理大小到 16。此时棋力会有明显下降但足以在 CPU 上完成整盘对弈。实际测试中8 线程 CPU 400 次模拟大约每手 3 秒刚好符合快棋对局的要求。最后验证训练是否正常的最直接方法是打开一局自我对弈观察前 50 手是否出现「连续脱先」——这说明价值网络对厚势的估值偏高。将价值损失函数中的胜负标签从 ±1 换成 ±0.8可以在很大程度上缓解这个问题也是 Ghost 社区里常用的一个小技巧。本文还有配套的精品资源点击获取
返回列表