从零实现AlphaZero五子棋:网络设计、MCTS优化与训练踩坑实录
简介一套基于AlphaZero技术实现的五子棋游戏代码包面向人工智能初学者及对强化学习、蒙特卡洛树搜索感兴趣的开发者。代码使用TensorFlow 2搭建神经网络完整提供自对弈训练流程的Python实现但因训练成本极高单机约3700年更适合作为教学代码阅读帮助理解从棋盘环境、神经网络输入输出到MCTS搜索与自我对弈的完整链路。压缩包共10个文件其中9个Python脚本承担游戏主逻辑、模型构建、训练循环与人机对弈等模块另有1个Readme说明文件整体体积仅18KB结构清晰便于对照文章学习。已有1779人学习下载说明该案例在AlphaZero入门话题中具备一定参考热度。通过这套代码读者能直观体会AlphaZero的自我博弈思想也方便在理解基础上自行调整网络结构或训练参数作为深度学习项目入手的实操素材。 搞了三个多月才把这个AlphaZero 五子棋跑稳从最初的能瞎走棋到后来能稳定虐我这种业余选手中间踩的坑比想象中多得多。五子棋规则虽然简单但恰好是验证 AlphaZero 整套思路的绝佳场地15×15 的落子空间、先手优势的天然偏差、自对弈数据的冗余……任何一个环节偷懒训练结果都会直接跑到沟里去。这篇就把我完整实现这套系统的思路、代码细节、训练过程和坑全摊开讲希望对想复现 AlphaGo Zero/AlphaZero 路线的人有点帮助。1. 项目思路拆解为什么选五子棋来跑 AlphaZero1.1 AlphaZero 的核心逻辑用大白话讲AlphaZero 这个名字听起来高深核心其实就三块一个神经网络、一套蒙特卡洛树搜索MCTS、一个自对弈的数据生产线。神经网络负责两件事——预测当前局面下每个落子位置的概率策略头和预测当前胜负的概率价值头。MCTS 负责在决策时通过模拟对局来放大神经网络给出的先验判断。而自对弈就是让当前最强的 AI 自己跟自己下棋不断产生新的棋谱喂回神经网络训练形成闭环。这个过程完全不需要任何人工设计的棋谱特征、开局库或者棋理规则也就是论文里说的从零开始学习。五子棋虽然棋理不像围棋那么深但同样具备完备信息博弈的所有要素很适合用来观察这种纯强化学习方法的收敛过程。选择五子棋还有一个实际考虑棋盘比围棋小训练速度快很多单机 GPU 就能跑不需要动辄上万 TPU 的资源。1.2 与直接写规则棋力用 Minimax 相比的优势很多人会问五子棋现在连手机 App 都能用 alpha-beta 剪枝做到很强为什么还要用 AlphaZero这个问题问得其实挺到位。如果只追求最强棋力那直接写个评估函数加深度搜索就够了。但 AlphaZero 的价值在于它是一种不依赖领域知识的通用博弈框架。你换一个棋类只要改棋盘表示和落子规则其他模块不需要动。我在项目中期做过一次对比实验拿最终的 AlphaZero 模型固定 100 次 MCTS 模拟跟一个用了 5 层剪枝、启发式杀棋判断的传统引擎下棋大概能稳定赢六成。但这不是重点重点是 AlphaZero 这套架构的可迁移性。我把训练好的棋盘尺寸从 15 改成 9 之后只重新训练了不到原来一半的时间新模型就收敛了传统引擎你换个棋盘还得重新调评估函数参数差别就在这里。1.3 项目整体的技术选型整个项目我用 Python 实现深度学习框架选 PyTorch。网络结构参考 AlphaGo Zero 论文里的 ResNet 思路但又做了大幅简化15×15 输入4 个残差块每块 64 个卷积核。MCTS 部分的模拟引擎用纯 Python 实现后期对性能瓶颈做了一些针对性优化。棋盘的表示用 2 通道特征平面当前玩家棋子一个通道对手棋子一个通道。对局结束判定用最简单的五连和棋判定没有引入禁手规则因为禁手会让数据生成逻辑复杂化对理解核心架构没有帮助。训练硬件上我用的是一块普通的 RTX 3060单机训练。整轮训练在 72 小时内可以完成一次完整收敛每次自对弈生成约 5000 局棋谱数据。这个规模对复现 AlphaZero 思路的人来说门槛不算高。2. 核心模块解析网络结构、MCTS 与自对弈机制2.1 神经网络结构从输入到策略和价值一次说清楚神经网络是整个 AlphaZero 系统的大脑它接受一个 15×15×2 的局面张量作为输入。第一维是棋盘的行列第三维是通道第一个通道放当前玩家的棋子位置第二个通道放对手的棋子位置。这样设计的好处是网络不需要关心我是黑棋还是白棋只需要学会从当前玩家的视角评估局面这就天然实现了对弈双方的对称性。网络主干是 4 个残差卷积块。每个残差块的结构是3×3 卷积 - BatchNorm - ReLU - 3×3 卷积 - BatchNorm然后与输入相加再经过 ReLU。残差连接是让 AlphaZero 这类强化学习模型能稳定训练的关键如果去掉这个结构网络很容易随着自对弈数据分布漂移而出现梯度消失或特征遗忘。网络最后分出两个头。策略头先经过一个 1×1 卷积把特征压缩到通道数 2再接一个全连接层输出 225 维向量15×15 展平经过 softmax 变成每个落子位置的概率分布。价值头同样先压缩特征然后接一个全连接层输出一个标量用 tanh 激活映射到 -1 到 1 之间1 代表当前玩家胜势-1 代表败势。class AlphaZeroNet(nn.Module): def __init__(self, board_size15, channels64, blocks4): super().__init__() self.conv_in nn.Conv2d(2, channels, 3, padding1) self.res_blocks nn.ModuleList([ ResidualBlock(channels) for _ in range(blocks) ]) self.policy_conv nn.Conv2d(channels, 2, 1) self.policy_fc nn.Linear(2 * board_size * board_size, board_size * board_size) self.value_conv nn.Conv2d(channels, 1, 1) self.value_fc1 nn.Linear(board_size * board_size, 64) self.value_fc2 nn.Linear(64, 1) def forward(self, x): x F.relu(self.conv_in(x)) for block in self.res_blocks: x block(x) p F.relu(self.policy_conv(x)).view(x.size(0), -1) p F.log_softmax(self.policy_fc(p), dim1) v F.relu(self.value_conv(x)).view(x.size(0), -1) v torch.tanh(self.value_fc2(F.relu(self.value_fc1(v)))) return p, v2.2 蒙特卡洛树搜索让模型在模拟中思考MCTS 在这套系统里的作用是把神经网络给出的先验概率和后续模拟结果结合起来形成一个更稳健的落子策略。每个树节点代表一个棋盘局面节点上保存四条信息该局面的访问次数 N、该局面下每个合法落子位置的访问次数 N(s,a)、每个位置的平均价值 Q(s,a) 和先验概率 P(s,a)。搜索过程分四步反复迭代选择、扩展、模拟、回传。选择阶段从根节点出发逐层按 PUCT 公式挑选子节点往下走公式长这样[ a \arg\max_a \left[ Q(s,a) c_{puct} \cdot P(s,a) \cdot \frac{\sqrt{\sum_b N(s,b)}}{1 N(s,a)} \right] ]这个公式的直觉是既要选择 Q 值高的看起来赢面大的位置又要给先验概率高但访问次数还少的位置一些探索的机会。c_puct 是探索系数我一开始按论文设为 5后来在五子棋场景下调到了 2效果更好。原因是五子棋的分支比围棋小很多同样探索系数会造成过多的随机落子反而拖慢收敛。走到叶节点后用神经网络对该局面做一次前向推理得到策略先验概率和价值估计然后逐层回传更新每个节点的 N、Q 值。根节点做足够多次模拟后根据根节点子节点的访问次数分布来决定实际落子。训练阶段会按访问次数的幂次采样增加随机性对弈阶段则直接选访问次数最多的位置下法更稳定。2.3 自对弈数据生成AI 的棋谱生产线自对弈模块是训练数据的唯一来源。每一局自对弈开始时随机决定先手方然后让当前最强的模型同时担任黑白双方——注意这里不是两个模型互相对弈而是同一个模型在每步都用 MCTS 搜索来决定当前玩家的落子共享全部参数。一局棋走完后终局价值 v 要转成每个中间局面的训练标签。因为神经网络的价值头是从当前玩家视角预测胜负所以每步的标签要乘上当前玩家的视角如果是执黑方走出来的局面且黑棋获胜标签是 1如果该局面轮到白棋走那白棋视角下胜负相反标签就要取 -1。这个视角转换是新手最容易搞错的地方很多人训练半天不收敛问题往往就出在这。我用的自对弈配置是每局棋最多 200 手MCTS 模拟次数 200 次前 12 手采样温度设成 1之后温度降到 0.1最后 5 手直接取 argmax。温度设计的逻辑是开局阶段需要探索不同开局走法避免模型固化到一条路径上中后盘逐渐贪心保证数据质量。每局结束会把整个过程中所有状态、策略概率、最终胜负打包成一条样本存入经验池。3. 训练流程与实操过程3.1 完整的训练循环怎么搭建训练围绕一个主循环展开用当前模型生成一批自对弈数据然后把数据交给优化器做若干轮梯度下降再用新模型和旧模型做评估赛决定是否把新模型推为最佳模型接着用新的最佳模型进入下一轮自对弈。具体执行中我每轮生成 128 局自对弈训练时从经验池里随机采样 batch。经验池容量控制在 5000 局左右采用先进先出的淘汰策略这样既能保证训练数据有足够新鲜度又不至于让模型短期内把某一批数据背熟了导致过拟合。训练损失由三部分组成策略交叉熵损失、价值均方差损失、以及 L2 权重衰减。总损失函数是策略损失加价值损失再加权重项权重衰减系数设 0.0001。优化器用带动量的 SGD初始学习率 0.01训练过程中按余弦退火衰减到 0.0001。这个配置我试了 Adam效果不如 SGD 稳可能是 Adam 的自适应学习率在强化学习这种非平稳目标下容易让网络参数震荡。optimizer torch.optim.SGD(model.parameters(), lr0.01, momentum0.9, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max200) for epoch in range(200): batch sample_from_replay_buffer(batch_size256) policy_label, value_label batch policy_pred, value_pred model(batch_state) loss policy_loss(policy_pred, policy_label) value_loss(value_pred, value_label) l2_loss optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step()3.2 超参数调整中的关键试验记录有几个超参数我从头到尾调了很多遍目前这套配置经过多次对比验证是比较合理的。MCTS 模拟次数从 50 到 400 我都测过模拟次数越多棋力越强但训练周期明显变长。200 次是一个性价比不错的选择单步搜索耗时约 0.3 秒每局 200 手一局 128 盘的 Batch 大概需要 2 小时出头。探索系数 c_puct 我从 5 下调到 2明显改善了开局落子的多样性。原来用 5 的时候自对弈生成的前 20 手经常大量重复星位-小目-星位这类固定开局导致经验池里同质化数据过多模型策略头很难学到多样化的布局直觉。调到 2 之后虽然前几轮棋力看起来略有下降但整体收敛速度反而更快。训练轮数的停靠点我参考了论文的做法——每完成一轮新的自对弈数据生成和训练就把新模型和当前最佳模型做 20 局评估赛新模型胜率超过 55% 就替换。这个机制很关键它阻止了模型在某个局部震荡时把越训越差的版本推上线。实测下来如果不加评估直接拿每轮最新模型去生成数据模型棋力会出现非常明显的周期性退化大概每 30 轮一个低谷。3.3 从随机棋手到稳定碾压的收敛过程训练前期的收敛非常直观。最开始模型完全随机落子白棋和黑棋都没什么章法策略概率分布近似均匀价值输出也基本在 0 附近徘徊训练损失大概在 4.2 左右。前 50 轮训练之后模型开始学会最基本的成三和堵三损失能降到 2.8。这个阶段最容易出幻觉因为模型看到己方有三个连子就觉得赢面大价值输出趋于 0.7但实际上对手也快连五了。到大约 120 轮的时候模型出现了明显的质变。它开始主动做双三和活四这类组合棋形而且价值判断变得准确很多不再盲目给单一连子打高分。我用一个固定棋面的测试集去评估这个阶段模型的准确率已经能到 75% 左右。真正让我觉得这套系统跑通了是在第 200 轮左右拿最终模型跟几个业余棋手下了几局。我本人下五子棋水平一般基本每局都被它抓住双三机会另外一个自认为有段位的朋友下十局赢四局输的六局里多数是开局就被压制。对于一个完全没输入过任何棋理知识的模型来说这个水平已经很说明问题了。4. 实现中的难点、常见问题与避坑技巧4.1 自对弈数据里的先手优势陷阱五子棋没有贴目规则执黑先手有天然优势而且优势比围棋大不少。如果不做处理自对弈生成的数据里会出现一个明显的统计偏差黑棋胜率远高于白棋。模型学到的价值头会倾向于无条件认为黑棋赢面大而不是根据实际局面判断。我尝试过两种处理方法。第一种是在自对弈时随机分配先手让每一局既可能是黑先也可能是白先并在最后计算标签时严格按当前玩家视角换算。第二种是在训练样本里做类似围棋的贴目处理给白棋方的价值标签加一个小的常数偏移但这个常数很难定准调不好反而引入新的偏差。最终我采用了第一种随机先手加视角换算虽然没有完全消除先手偏差但模型的胜率在不同手色下基本能维持在接近五五的水平。4.2 训练不收敛或棋力震荡的原因排查训练不收敛是最让人头疼的问题我排查过几类原因每一条都记录在这里。最常见的是数据生成和模型更新不同步导致的数据分布漂移。如果经验池太小或者更新太快模型容易在近期对弈数据的局部风格上过拟合自我博弈时陷入循环策略失去多样性。解决办法是把经验池调大同时降低每轮训练的迭代次数。第二个常见原因是学习率偏高。强化学习的损失函数是波动的不像监督学习那样稳定下降学习率太大容易让参数直接跨过最优区域。我一开始用 0.02训练到第 40 轮时 loss 突然飙升到 5.8肉眼可见的崩了。把初始学习率调回 0.01 并加余弦退火之后后面就再没出现过这种崩溃。第三个原因是棋盘特征图的方向问题。五子棋有旋转和镜像对称性输入特征图和策略输出如果不做数据增强模型会浪费大量容量去分别学习同一局面的不同镜像版本。我在训练时对每个局面做了 4 个旋转加 2 个镜像共 8 种增广相当于把数据量翻了 8 倍棋力提升非常明显。这个技巧在围棋和五子棋里都同样适用属于必做项。4.3 Python 性能优化模拟速度慢怎么办纯 Python 实现的 MCTS 在模拟次数上到 200 次之后单步耗时开始明显变长。我做了三个优化把单步搜索时间从接近 0.8 秒压到了 0.3 秒以内。第一个优化是树节点的表示。最初我用 Python 字典存所有节点每个节点还创建了复杂的对象结构导致大量内存分配和回收开销。后来改成用整数 ID 索引节点每个节点只存一个列表包括父节点 ID、子节点 ID 列表、访问次数、Q 值、先验概率性能和内存都有明显改善。第二个优化是合法落子集合。五子棋的合法落子区不是整个 15×15 棋盘而是当前所有已有棋子的相邻一圈空棋盘时不成立。提前维护这个候选落子表能大幅减少 MCTS 中的动作遍历时间。我选择每次落子后以该落子位置为中心扩充 2 格半径内的空位就基本覆盖了有价值的候选点。第三个优化是批量前向推理。一次自对弈过程中会多次调用神经网络如果每一步都单独做一次 GPU 推理反而因为 kernel 启动开销变慢。我在生成一局数据的时候把每一步的输入特征先攒成列表再统一分批送入网络再把结果按步对应回来。这样配合 PyTorch 的 no_grad 模式能再省下大约 30% 的时间。4.4 评估机制里的一个隐性 Bug评估赛逻辑里藏着一个很容易忽略的问题新旧模型对弈时如果双方都使用同一套 MCTS 参数棋局的胜负很容易被先后手左右而不是由模型强弱决定。我最初评估时固定旧模型执黑、新模型执白结果导致新模型胜率长期偏低因为黑棋先手优势太占便宜。后来改成双方各自执黑 10 局总共 20 局按双方综合胜率判定是否替换。这一改模型迭代的节奏立刻健康了很多。这条经验也提醒我涉及胜率对比的场景一定要把先后手因素对称化处理否则再好的模型也会被低估。5. 这套系统的后续改造方向与实际使用体会模型跑通到现在我已经在它基础上做了两件事一是把棋盘尺寸参数抽出来直接改 9 路训练一个快速版验证了代码的通用性二是接了一个简单的棋盘界面方便手工对弈观察棋力表现。后续如果要进一步提升棋力有几个明确的方向可以走但不一定都值得做。第一个方向是加深网络和增加模拟次数比如把残差块从 4 个加到 10 个模拟次数提到 800棋力大概率还能上一个台阶但训练时间会成倍增加。第二个方向是引入开局库或者禁手规则这能让棋盘表现更贴近正式五子棋比赛规则但同时会失去一部分从零学习的纯粹性。第三个方向是调整 PUCT 公式里的探索项改成随着训练轮数动态衰减前期多探索、后期多利用这个我试过效果还不错。至于使用体会我最想强调的一点是AlphaZero 这套东西真正难的不是把代码写出来而是让整套系统里的每个环节都能稳定地往同一个方向使劲。数据生成环节的视角标签、MCTS 的探索系数、评估赛的手色平衡、经验池的淘汰策略只要其中一个设置有偏差训练结果就会变得难看。而反过来只要这些细节校正好了哪怕是一块入门级 GPU也完全能训练出一个像模像样的棋盘 AI 来。如果你也想动手复现我最诚恳的建议就是先跑通一个小规模的完整闭环比如 9 路棋盘、30 次模拟、几十轮训练把整个流程的日志和可视化都搭好再逐步放大规模。别一上来就追求 15 路高配置那只会让你在这个领域的第一次强化学习实验变成一次漫长的 debug 循环。本文还有配套的精品资源点击获取