ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

阿尔法零原理剖析:Ghost-master中的双头网络与蒙特卡洛树搜索

阿尔法零原理剖析:Ghost-master中的双头网络与蒙特卡洛树搜索 简介一份基于Minigo框架、以Python实现的幻影围棋项目面向围棋爱好者、AI学习者与Python开发者。核心目标是让用户快速上手围棋AI的人机与机机对战通过Minigo的卷积神经网络与蒙特卡洛树搜索MCTS组合训练并演化出具备不可预测性的智能棋风也可与随机落子对手对比基础策略强度。压缩包共18个文件以14个Python脚本为主体覆盖对弈入口、网络模型、特征提取、对称处理、裁判逻辑、GUI界面等模块另含TensorFlow模型权重及说明文档整体仅约1.47MB轻量便于阅读与二次开发。已有926人学习下载。从中可获得完整的Minigo围棋AI代码骨架直接运行人机对战、让两个AI互弈以观察策略进化或参考随机对战脚本理解对抗样本设计项目目录结构清晰附带说明文档适合作为深度学习与博弈算法入门的实践素材。无论是课堂教学、个人研究还是AI性能对比都不用再从头搭建环境。1. 一个傻瓜式围棋 AI 项目也藏着 AlphaGo Zero 的完整骨架装好 Python 环境拉下 models 目录里的 000496 权重运行 ghost_vs_human.py 就能看到黑棋落子的速度与姿态这个叫 Ghost-master 的幻影围棋项目表面是幽灵风格的下棋程序底子其实是 DeepMind 开源 Minigo 的教学级重组。它把围棋 AI 拆成了几个可以单独阅读的 Python 文件dual_net.py 负责网络结构features.py 把棋局编码成特征平面Ghost.py 承载蒙特卡洛树搜索再通过三个人机/机机/随机对局脚本把整个闭环串起来。没有分布式训练集群也没有复杂的多进程调度适合想在一台机器上看清 AlphaGo Zero 原理、又不想啃大工程的开发者也适合需要批量制造自对弈数据做特征工程的算法工程师。下面按网络特征 → 搜索决策 → 对弈入口 → 权重验证四段拆完这个包。2. 棋感从哪来dual_net 双头网络与特征平面的拼法2.1 策略头与价值头一次推理给出两份判断AlphaGo Zero 最有辨识度的设计是把策略网络和价值网络合并成一个双头残差网络。Ghost-master 的 dual_net.py 就是这一结构的落地主干网络吃进同一个局面在最后一层分叉成两个输出。策略头输出 19×191362 个概率值对应棋盘上 361 个落点以及停一手pass价值头输出一个 [-1,1] 区间内的标量站在当前执棋方的视角评估局面1 表示接近必胜-1 表示接近必败。把策略和价值放在同一个网络里不只是省一次推理开销。训练时两份监督信号共享同一个特征提取器策略梯度能反向传播到价值分支价值误差也能反过来约束策略分支学习到的表征比两个独立网络更容易收敛。这是后来大量开源围棋 AI 沿用双头结构的原因。Minigo 把整套结构精简到可单机运行的程度Ghost-master 又在它外面包了一层更直白的 Python 类便于本地调试和逐步断点。2.2 features.py把一局棋编码成 17 个特征平面卷积网络吃不了谁在第几手下在哪这种离散事件需要先把局面转成多通道的特征图。features.py 做的就是这件事。按照 Minigo 的经典设置它取最近 T8 个回合站在当前执棋方的视角把局面拼成一个形状为 (17, 19, 19) 的张量。平面索引内容说明0 - 7当前执棋方过去 8 手的落子位置最新一手在索引 0越早越靠后8 - 15对手方过去 8 手的落子位置同样按时间倒序排列16当前执棋方颜色平面执黑全 1执白全 0颜色平面是新手最容易漏掉的一项。围棋的价值判断完全依赖视角同一个盘面黑棋视角和白棋视角的价值结论是相反的策略头输出也随视角变化。有了第 16 个平面网络才不需要靠数谁的子多来猜测自己执黑还是执白。# 以 Ghost-master 的 features 逻辑为例示意双视角特征拼装 def get_features(board, player_color, move_history): # board: 19x19 数组0空 1黑 2白 # move_history: [(row, col, color), ...] 按时间正序 features np.zeros((17, 19, 19), dtypenp.float32) my_moves [m for m in move_history if m[2] player_color][-8:] opp_moves [m for m in move_history if m[2] ! player_color][-8:] for i, (r, c, _) in enumerate(my_moves): features[7 - i, r, c] 1.0 # 最新一手放索引0按时间倒排 for i, (r, c, _) in enumerate(opp_moves): features[15 - i, r, c] 1.0 features[16] 1.0 if player_color 1 else 0.0 return features # (17, 19, 19)这里把最新落子放在索引 0、越早的放在越靠后是为了让网络在卷积时最先感知到刚发生的变化与人类看棋先看最后几手的习惯一致。T8 是平衡点窗口太小看不到中盘战斗的来龙去脉窗口太大比如 16则特征张量和推理时间线性增长对业余棋力的提升有限。如果发现 AI 在局部战斗中总是失忆可以先检查这个窗口内是不是漏了关键一手。2.3 symmetries.py一次对局当八次用棋盘对旋转和镜像保持不变围棋的特征平面和策略输出都满足这一对称性。symmetries.py 实现了 D4 对称群的 8 个变换4 个旋转角度乘以 2 个镜像方向。训练侧常见做法是对每条自对弈样本随机做一次对称变换再喂给网络相当于把数据集扩大了 8 倍。推理侧同样可以利用对称性。同一个局面做 8 次对称分别推理把策略输出反变换回去再取平均能明显压低单次推理的随机波动。代价是耗时乘以 8所以适合用在形势接近、需要稳定判断胜负的官子阶段而不是每一手都用。import random from symmetries import apply_symmetry, invert_symmetry sym random.randrange(8) aug_features apply_symmetry(features, sym) # 随机增广 # 网络输出需要逆变换回原棋盘坐标系再参与下一步 policy_back invert_symmetry(policy_logits, sym)这段代码说明两件事第一训练样本必须成对变换特征变了标签棋谱里的下一步坐标也要跟着变第二推理聚合时每次对称变换后要 invert 回去否则 8 份结果对不齐。实际使用中random.randrange(8)换成固定枚举 0 到 7 就是对称投票的推理模式。3. 落子决策的核心Ghost 类与蒙特卡洛树搜索3.1 MCTS 四步循环与 PUCT 公式Ghost.py 是整个程序的决策中枢。每次轮到它落子它都在内存里从根节点出发执行若干次模拟模拟次数由readouts参数控制。每次模拟走完选择、扩展、评估、回传四个步骤选择Select从根节点出发按 PUCT 公式逐层选子直到走到一个未被展开的叶子节点。扩展Expand调用 dual_net 拿到叶子局面的策略先验 P 和价值 V并创建子节点。评估Evaluate用价值头的输出 V 作为该叶子的初始评估。回传Backup沿路径反向更新每一个经过节点的访问次数 N 和累计价值 W平均价值 Q W / N。选择阶段用的 PUCT 公式是score Q(s,a) c_puct * P(s,a) * sqrt(N(s)) / (1 N(s,a))这里的含义是Q 是子节点已有的平均价值代表被验证过有多好后一项是探索奖励先验概率越高、来访次数越少的节点越容易被选中。c_puct 控制两者权重。def select_child(node, c_puct1.0): best_score, best_child float(-inf), None for child in node.children.values(): score child.Q c_puct * child.prior * math.sqrt(node.N) / (1 child.N) if score best_score: best_score, best_child score, child return best_childc_puct1.0是常见起步值。调大后搜索更发散愿意尝试冷门着法适合探索新定式调小则更依赖策略头先验落子风格更保守。多线程版本还会在选中节点上临时叠加 virtual loss避免多个线程同时展开同一个叶子Ghost-master 单进程运行不需要这个机制但读 Minigo 源码时容易碰到这个概念。3.2 go.py规则引擎是搜索的地基MCTS 每次模拟都要执行落子因此规则引擎的完备性直接决定搜索是否可信。go.py 维护棋盘的全部状态至少覆盖以下三条核心逻辑落子合法性目标点必须为空落子后己方棋串不能无气自杀禁手提子判定落子后先提掉对方无气的棋串再检查己方棋串是否无气打劫禁止全局只允许一个劫且立即提回被判定为不合法。常见实现里棋盘用 19×19 的 numpy 数组保存气的计算用 BFS 或并查集按连通块标记。搜索频率很高时go.py 里每一次数组拷贝和连通块扫描都会变成性能瓶颈优化时优先看这里。coords.py 是坐标翻译层。同一个棋盘点在项目里有三套表达棋盘数组的行列索引、GTP 协议通用的字母数字坐标如 Q16、SGF 棋谱坐标。人机对弈时玩家输入走法先经过 coords.py 解析成内部索引再交给 go.py 执行对局记录写 SGF 时再反向翻译。这层抽象看似繁琐机机对弈和 GUI 显示全靠它对齐删掉后到处是ord和chr的硬编码换算。3.3 搜索参数怎么调readouts、c_puct 与认输阈值note.txt 里一般记录了作者调参时留下的经验值常见做法是把这些参数做成命令行选项方便对比实验。三个最值得调的参数如下。参数常见默认值作用调整建议readouts400 - 800每次落子前的搜索模拟次数数值翻倍约等于多算一层棋棋力增加耗时线性增长c_puct1.0PUCT 探索系数同权重自对弈时适当加大到 1.2 - 1.5避免布局重复resign_threshold-0.9根节点价值低于该值立即认输想看完残局就设成 -1.0AI 会一直下到终局# ghost_vs_human.py 中常见的 argparse 覆盖方式 parser argparse.ArgumentParser() parser.add_argument(--readouts, typeint, default800) parser.add_argument(--c-puct, typefloat, default1.0) parser.add_argument(--resign-threshold, typefloat, default-0.9) args parser.parse_args() ghost Ghost(weightsmodels/000496, readoutsargs.readouts, c_puctargs.c_puct, resign_thresholdargs.resign_threshold)把参数暴露在命令行而不是硬编码在源码里最大的好处是可以写脚本批量跑对照实验。比如固定权重分别用 readouts200、400、800 与同一个对手下 20 盘就能画出一条算力 vs 胜率的曲线这也是判断当前网络棋力上限最直接的办法。4. 三套对弈入口人机、机机与随机对局的正确打开方式4.1 环境准备与 ghost_vs_human.py 人机实战动手前先把 Python 环境整理干净。建议用虚拟环境建一个独立空间避免和系统 Python 的包冲突项目依赖虽然不多但 numpy、tensorflow 这类库的版本错位很容易在 import 阶段才暴露。装好依赖后直接启动人机对弈pip install -r requirements.txt python ghost_vs_human.py --weights models/000496 --readouts 800程序启动后会打印棋盘和坐标提示人类执白输入坐标应遵循 GTP 风格例如黑棋第一手常见写法是D4或Q16输入后回车AI 立即开始搜索并返回落子。如果输入了非法坐标落在已有棋子上或格式错误coords.py 会抛出解析异常正常流程里应该捕获并重新提示输入而不是直接让整个进程退出。--readouts 800表示每手最多搜索 800 个叶子节点在普通 CPU 上大约是每手几秒到十几秒的思考时间棋力比 200 时要强不少。4.2 ghost_vs_ghost.py机机对弈的三种用法机机对弈是这个项目里最有实验价值的部分它不只是看两个程序下棋至少能派三种用场同权重同参数互下用于验证程序稳定性连续 10 盘不崩溃、不出现非法落子说明规则引擎和搜索逻辑基本可靠不同 readouts 对撞固定权重一方用 800 次搜索另一方用 200 次可以测出算力对胜率的边际贡献棋谱导出对局落子会按 SGF 格式写入文件这些棋谱就是训练数据的原材料。# ghost_vs_ghost.py 的简化主循环 player1 Ghost(weightsmodels/000496, readouts800, nameghost-A) player2 Ghost(weightsmodels/000496, readouts400, nameghost-B) while not game.is_over(): if game.current_player 1: move player1.select_move(game) else: move player2.select_move(game) game.apply_move(move)select_move内部会执行完整的 MCTS 并返回一个坐标game.apply_move同步更新棋盘并切换执棋方。注意两个 Ghost 实例共享同一个权重文件时每次推理是独立的不会互相污染因此可以放心并发或交替调用。如果后续要做训练需要在select_move里额外返回每个节点的访问次数分布而不是只返回最终落点——访问次数分布才是策略标签。4.3 ghost_vs_randomPlayer.py冒烟测试的胜率基线随机对局常常被低估它在整个链路里的角色是冒烟测试加基线锚点。随机玩家没有任何棋力但它的行为完全合法每次都在合法空点里均匀随机选一个。拿正式权重去和它下 20 盘有意义的不是 AI 能赢多少而是 AI 能不能以稳定且不违反规则的方式赢下来。随机对局还能暴露一个隐蔽问题如果 AI 在优势局面下频繁点空、不下完可能是认输阈值设得太高如果随机玩家经常意外提掉AI 的大龙则要回去检查 go.py 的提子逻辑。胜率记录建议单独存成文本格式保持timestamp, black, white, result四列后续做棋力回归分析时可以直接读入 pandas。4.4 Referee.py 与 AI_GUI.py判官与可视化排错对局不能没有裁判。Referee.py 就是这层判官逻辑终局时数清双方实空、判断劫材是否消化完毕、输出胜负结果。Shell 环境下直接看文本输出足够但当 AI 走出幽灵一手时文本日志很难还原棋局脉络这时候 AI_GUI.py 就派上用场它把棋盘渲染成图形界面能直观看到每一步的落子位置和提子过程。如果早期版本在 Windows 下运行对局脚本时遇到internal error 25002这类 internal error优先怀疑 go.py 的状态同步问题尤其是打劫和提子后棋盘哈希值没有刷新。常见做法是在 apply_move 之后主动对棋盘做一次一致性校验重新从初始状态按棋谱回放比对当前棋盘是否一致。这个校验很昂贵只在调试模式开启。入口脚本对手主要用途典型命令行ghost_vs_human.py人类人机对战、演示python ghost_vs_human.py --readouts 800ghost_vs_ghost.py另一 AI 实例自对弈棋谱生成、能力评估python ghost_vs_ghost.py --games 10ghost_vs_randomPlayer.py随机合法落子冒烟测试、胜率基线python ghost_vs_randomPlayer.py --games 20AI_GUI.py可视化对局复现棋局、直观调试python AI_GUI.py --weights models/000496四个入口共享同一套 Ghost 类和 go.py 规则引擎区别只在对手从哪来。写新的入口脚本时也只需替换对弈双方的类型不需要改动搜索逻辑。5. 用 000496 权重做棋力验证胜率、对称一致性与进阶技巧5.1 models/000496 是什么水平按 Minigo 的惯例checkpoint 文件名中的数字代表训练到第几步或保存到第几轮。000496 这个量级说明模型还处在训练早期棋力上限不高但已经具备完整的分块、定式雏形和基本死活判断。拿到权重后别急着下结论先用脚本批量对局让数据说话。5.2 对称一致性检查量化网络对局面的把握程度一个局面下8 个对称变换后的策略输出应该几乎一致。如果网络对同一局面的不同对称视角给出差异很大的落子概率说明它对这个局面没有形成稳定理解。这个指标可以作为训练质量的早期预警。# 用 symmetries 做策略一致性检查 policy_views [] for sym in range(8): feats apply_symmetry(features, sym) _, policy model.predict(feats) policy_views.append(invert_symmetry(policy, sym)) policy_mean np.mean(policy_views, axis0) policy_var np.var(policy_views, axis0)输出中高方差的落点就是网络分歧最大的候选着法。如果某个局面的 top 落点方差一直很大通常意味着该局面在训练数据里出现频率偏低是主动补充相似局面样本的信号。5.3 最小胜率评估十盘随机对手加三盘自对弈快速验证权重文件是否正常不需要完整训练流程。先跑 20 盘 ghost_vs_randomPlayer 确认胜率在 95% 以上再让 000496 与更高搜索次数的自己下三盘记录是否出现明显的前半盘崩溃。进阶技巧是只把对称一致性检查应用在每局第 100 手之后的官子阶段此时棋盘空旷度低策略头方差以及对称方差都能显著反映局面把握度称得上是成本低且信息量最大的一处模型质量探针。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进