从零搭建深度学习围棋AI:特征编码、网络训练与MCTS联调实战
把特征、网络、搜索这三件事从听说过变成跑得通是我写这篇续篇最想解决的问题。上一篇写深度学习与围棋的结合思路时有不少读者留言说原理能看懂但一到自己动手搭系统就卡壳棋盘状态怎么编码策略网络和价值网络到底怎么分工训练好的网络又如何跟蒙特卡洛树搜索接上这些问题单独拎出来都不算难但串在一起就容易顾此失彼。所以我这篇直接换个写法不聊太多理论铺垫而是按从零搭一个能下棋的深度学习围棋程序这条路走把每一步的关键逻辑、参数选择和踩坑经历都摊开讲。我自己在这个项目里也反复折返过好几轮有些坑纯粹是经验问题文档里根本不会写。这篇的内容适合两类人一类是接触过深度学习、想找一个完整项目练手的开发者另一类是懂围棋规则、想搞清楚现代围棋AI内部机理的爱好者。无论你属于哪一类这篇文章都能给你一条清晰可执行的路径。1. 整体设计思路为什么围棋AI不能只用深度网络1.1 围棋的复杂性决定了单模型撑不住场面很多刚入门的人会有一个直觉既然深度学习这么强训练一个网络直接学会下棋不就行了吗真实情况远没有这么简单。围棋的合法局面数量级在10的170次方左右棋盘上任意一个落子都会连锁改变整盘的攻防态势。单靠一个神经网络直接输出最终胜率中间会丢失掉大量计算信息尤其在对杀、打劫、收官等精确计算场景里纯网络的直觉判断很不可靠。所以在现代围棋AI的架构里深度学习从来不是单打独斗。策略网络负责提供当前局面下哪些点值得考虑的先验概率价值网络负责给出当前局面谁占优的整体评估而具体的落子决策还需要借助蒙特卡洛树搜索把这些信息组合起来通过多次模拟推演找到相对靠谱的那手棋。1.2 双网络分工与搜索协同我最初设计系统时也犹豫过能不能只用策略网络加简单搜索后来发现效果很一般。原因在于策略网络擅长模仿人类会下哪里但它并不真正理解下这里之后会发生什么。价值网络则相反它不关心具体哪手棋好只关心局面发展到最后谁赢。这两个网络单独拿出来都有短板合在一起刚好互补。策略网络裁剪搜索分支只沿着有意义的路径展开价值网络在搜索叶子节点给一个整体评估避免每次都要把棋局下到终局才知道胜负。搜索过程则像一个慢思考模块把两个网络的判断反复组合验证形成最终的落子决策。1.3 我踩过的第一个坑搜索深度与网络速度的平衡刚开始联调时我陷入了一个误区为了追求更强的棋力不断增大搜索模拟次数。结果每步棋思考时间从1秒涨到10秒棋力提升却不明显。后来分析才发现问题出在网络推理速度上——搜索的每一轮都要调用策略网络和价值网络网络推理太慢模拟次数再高也跑不动。这个阶段的教训是先把网络的推理速度优化到可接受的范围再考虑增加搜索次数。一般来说单步搜索控制在100到200次模拟之间配合一个不太深的网络就能达到一个相对可用的棋力水平。2. 棋盘状态编码从二维棋盘到特征平面的转换2.1 基础特征设计19×19的N层张量把围棋棋盘送进神经网络首先要解决的是怎么用数字表示盘面。最直接的做法是构造一个形状为19×19×N的特征张量其中N是特征通道数。每个通道对应一种棋盘信息的二值或连续值表示。我实际用的特征平面包括当前方棋子位置、对手棋子位置、当前方棋子的气数、对手棋子的气数、劫争位置、上一手落子位置以及当前方最近两步落子位置。这些通道叠加起来网络的输入层就是一个完整的局面快照。这里有一个容易被忽略的点特征表示必须始终以当前行棋方为视角。换句话说轮到黑棋时黑棋是当前方轮到白棋时白棋是当前方。如果不做这个视角转换网络学到的规律会被镜像盘面干扰模型泛化能力大打折扣。2.2 气的计算与合法落子掩码围棋的气是棋块存活的根基。计算气时需要找出与棋子直接相连的空的交叉点。我常用的方法是并查集加上邻居统计先对棋盘做连通块标记然后每个连通块统计周围空点的数量。除了气还要维护一个合法落子掩码。围棋的合法落子有两个硬约束不能落在已有棋子的位置落子后不能造成自己无气自杀以及不能违反打劫规则。这个掩码在训练和推理阶段都要用。训练时网络输出的所有落子概率乘以掩码把非法位置遮掉再计算损失搜索时掩码直接决定哪些子节点可以展开。提示打劫判断的细节很容易出错。我的做法是维护一个全局的劫争位置标记每次检测到提子后如果形成的局面跟上一手之前完全相同就认定这个落子属于打劫需要禁手。2.3 棋谱数据预处理与增强训练数据如果没有处理好后面网络怎么调都很难有起色。我自己用的是公开的棋谱数据集预处理时有三个重点切分局面、标准化视角、数据增强。切分局面是遍历每一手棋截取当时的棋盘状态和对应的落子位置。标准化视角是上面提到的遇到黑棋落子时当前方是黑棋遇到白棋落子时当前方是白棋。数据增强相对简单围棋具有旋转和镜像对称性一张棋谱可以扩展出8种等价变换我在训练时随机选取一种变换输入网络相当于免费扩充了8倍训练数据。3. 网络结构选型与训练细节3.1 主干网络卷积加残差块的组合处理围棋这种网格状数据卷积神经网络是天然的选择。基础的卷积层可以捕捉局部棋形特征比如小飞、跳、挂角这些基本棋形深层的卷积核堆叠后则能感知全局态势。我的网络主干部分采用了一个相对轻量但稳定的结构第一层是一个3×3卷积输出32个通道中间堆叠了5个残差块每个残差块包含两个3×3卷积、批归一化和ReLU激活。选择残差块而不是普通卷积块的原因很直接——残差连接能让梯度在深层网络中传播得更顺畅训练起来更稳定收敛速度也快不少。3.2 策略头与价值头共享主干独立输出网络在主干之后分成两个头。策略头负责输出每个落子点的概率分布它先经过一个1×1卷积降维然后接一个全连接层输出维度是棋盘大小加1——多出来的那1维是停一手/跳过在某些规则中存在的选择在实际训练中如果不考虑停一手输出维度就直接是361。价值头负责输出当前局面的预估胜率。它同样先经过1×1卷积再接两个全连接层最后通过tanh激活函数把输出压缩到-1到1之间对应黑棋视角的胜率。负值表示黑棋劣势正值表示黑棋优势。3.3 监督学习的损失函数与优化器选择在第一阶段我用人类棋谱做监督学习。策略网络部分用的是交叉熵损失让网络输出的落子概率尽量贴合人类高手的落子选择价值网络部分则用均方误差损失训练目标是棋谱对应的最终对局结果。我把两个损失按一定权重相加作为总的优化目标。权重设置我有一些心得最开始我让价值损失占比太高结果网络只顾着评估局面落子选择变得很平庸后来把策略损失权重调高到大约4比1棋风才正常起来。优化器选的是带动量项的SGD学习率初始设置为0.01训练过程中用了学习率衰减策略。Adam在围棋任务上没有SGD加动量的效果稳定这一点我实测下来比较明显。3.4 训练过程中的过拟合应对围棋棋谱数据量虽然不小但用卷积网络训练仍然存在过拟合风险。我采用的办法包括每轮训练随机抽取棋谱变换组合把Dropout加在策略头和价值头的全连接层之间以及在训练中监控验证集上的损失变化。一旦发现验证集损失开始回升而训练集损失还在下降就果断降低学习率并减少训练轮数。过拟合的另一个信号是网络在验证集上预测的落子概率分布变得过于自信也就是某个点的概率接近1而其他点几乎为0。这时候我会增强数据增强的强度同时抑制网络容量。4. 强化学习阶段自我对弈与数据生成4.1 为什么需要自我对弈监督学习阶段得到的网络水平最高也只能逼近人类棋谱所覆盖的棋风。它有一个明显短板面对人类棋谱中很少出现的局面网络会变得很茫然。解决这个问题的标准做法是引入强化学习让当前网络自己跟自己对弈不断生成新的棋局数据来迭代改进。自我对弈的价值在于网络能在探索中碰到大量非人类式的局面并学会应对。这些经验反过来强化网络对局面的理解逐步突破监督学习的上限。正式训练中自我对弈最终产生的棋力贡献往往超过监督学习阶段。4.2 自我对弈的奖励设置与样本组织自我对弈中每盘棋结束后会得到一个最终结果通常用1和-1分别表示当前视角的胜与负。整盘棋的每一个中间局面都标注该盘的最终结果这些局面-结果对就是价值网络的训练样本。策略网络的强化信号相对间接。我采用的方法是计算搜索后的访问次数分布并以其作为目标概率来训练策略网络——因为高访问次数的落子点往往是搜索推演后被认为更好的着法。这种方式比直接使用最终胜负做策略梯度更平滑训练也更稳定。这里有一个组织样本的细节虽然棋盘上每一步的局面差异巨大但相邻几步之间高度相关如果按时间顺序连续抽取样本会让网络在局部反复拟合同一盘棋的走势。我的做法是把自我对弈生成的棋局拆成局面组每组抽样时打乱局面顺序再随机抽取训练批次这样能有效降低相关性。4.3 一个简化版的自我对弈循环我自己实验时用的循环可以概括成下面这个流程for iteration in range(total_iterations): # 第一步用当前网络进行自我对弈生成棋谱 games self_play(current_network, num_games200) # 第二步从棋谱中提取训练样本 train_samples extract_samples(games) # 第三步用样本训练网络更新参数 train_network(train_samples) # 第四步每隔若干轮评估一次新网络的棋力 evaluate_network(current_network, best_network)每次迭代后生成的棋谱会存入一个样本池训练时从中随机抽取。样本池的容量我控制在最近50到100次迭代生成的棋谱总量太老的数据会干扰新策略的学习。4.4 评估策略防止强化学习震荡强化学习训练中经常出现能力回退现象某个迭代的网络在自我对弈中表现很好但真实对弈却输给了上一个版本。为了防止这种震荡我每次迭代结束后会固定地让新旧网络对弈若干局只有新网络的胜率达到一定标准才更新原有网络。这个评估机制的代价是会额外消耗一些算力但在训练早期非常值得。如果省略评估直接更新我遇到的情况是网络策略在几轮迭代中反复横跳最终收敛到很糟糕的状态。评估机制相当于一个保险阀确保每一步更新都是稳定向前的。5. 与蒙特卡洛树搜索的联调5.1 MCTS的基础流程选择、扩展、评估、回传蒙特卡洛树搜索的四个阶段值得重新理一遍。首先是选择从根节点出发按照一定的评分公式不断选择子节点直到到达一个叶子节点。然后是扩展在叶子节点处根据策略网络的输出创建子节点并为每个子节点记录先验概率。然后是评估调用价值网络对叶子节点局面做胜率预测同时也可以使用快速走盘做一个补充估计。最后是回传把评估得到的价值沿着路径逐层向上更新累计访问次数和累计价值。这个过程循环执行直到搜索预算用完最终根据根节点子节点的访问次数分布来选择落子。5.2 UCT公式与先验概率的融合选择阶段是整个搜索的核心。常用的评分公式是UCT变体其中关键部分是score(a) Q(a) C_puct * P(a) * sqrt(sum_N) / (1 N(a))这里Q(a)表示子节点a的平均价值P(a)是策略网络给出的先验概率N(a)是该子节点的访问次数C_puct是一个平衡探索与利用的常数。我实验中C_puct取1.0到2.0之间比较合适取值过小会导致搜索过早收敛到局部最优取值过大则会让搜索过于分散棋风变得杂乱。这里有一个细节值得注意搜索阶段使用的先验概率需要做一个温度处理。监督学习阶段网络输出的概率分布往往偏尖锐直接用于搜索会让某些分支几乎不被探索。我会对策略网络的输出取对数并除以温度系数再做归一化让概率分布适度平滑这样可以显著提升搜索的多样性。5.3 联调时的三个关键调试点第一个调试点是根节点落子策略。搜索结束后可以直接选择访问次数最多的子节点也可以根据访问次数分布按概率采样。访问次数最多相对稳健适合正式对弈概率采样增加了随机性适合自我对弈数据生成阶段可以让棋谱更丰富。第二个调试点是价值网络与搜索深度的配合。如果搜索深度很浅价值网络的评估就承担了过多压力如果搜索深度很深价值网络的作用会被稀释。我自己在9路棋盘上实验时把搜索模拟次数设到100左右价值网络的作用刚刚好体现出来。第三个调试点是搜索线程与网络推理的并发。网络上每个局面推理都是独立的可以并行处理。我在实现时用了批推理的方式搜索过程中把多个局面积攒成一个批次一次性交给网络处理能有效利用GPU并行能力整体速度能提升好几倍。5.4 开局库与搜索的衔接纯搜索在开局阶段会非常慢热因为开局分支太多而策略网络给出的先验概率又不稳定。一个省力的做法是维护一个简易开局库在开局前若干手直接走库内着法。开局库可以由职业棋谱中常见布局整理而来也可以由自我对弈高频选点自动统计生成。我倾向于先用棋谱高频选点生成开局库等到强化学习训练到一定阶段再把开局库逐渐关闭这样可以让搜索在开局阶段也有充分探索。这个过渡需要小心不要突然关闭否则网络在开局会有一段明显的棋力下滑期。6. 常见问题与排查思路实录6.1 特征对齐错误导致训练崩溃我在最早训练策略网络时遇到过一个非常隐蔽的bug特征平面中的当前方视角与训练标签中的落子方不一致。具体来说有些局面我按黑棋视角编码但标签却是白棋的落子结果训练损失始终降不下去。排查了很久才发现数据管道里一个索引偏差。排查这种问题有一个笨但有效的方法在训练循环中定期把网络输入的可视化图打印出来把特征平面还原成棋盘图然后人工检查几盘棋的标签是否符合棋谱实际走子。一旦发现视角不一致就能快速定位问题。提示特征编码是整个系统的地基。地基出错后面网络设计和搜索调参再怎么折腾都没用。动手前先仔细核对特征平面的维度、视角和取值范围。6.2 损失不下降或下降极慢损失不下降的原因通常有几个方面。一个常见原因是学习率设置不合理可以尝试把学习率从0.01降到0.001或者反过来调大观察损失的响应。另一个原因是网络结构太浅或太深太浅无法拟合数据规律太深在数据量不够时难以优化。还有一个容易被忽略的原因是批归一化的momentum参数。默认的0.1在小批量训练时会让均值方差估计抖动过大影响收敛。我把momentum调到0.2左右之后训练稳定性有了明显提升。6.3 MCTS访问次数分布极端化搜索结束时如果几乎所有访问次数都集中在某一个子节点上并不一定是好事。这可能意味着先验概率的温度参数设置过低策略网络在搜索早期就过度引导搜索到单一分支导致搜索缺乏多样性。我遇到过的一个具体场景是训练后期网络变得过于自信先验概率几乎变成了one-hot。把温度参数从1.0调到1.5之后搜索重新恢复了探索能力棋力反而提升了。这个反直觉的调整说明搜索的多样性有时比即时准确率更重要。6.4 对弈中反复走同一块棋如果搜索在局部反复走棋形成打将式的循环最常见的原因是搜索模拟次数太少网络价值评估对局部战斗的判断不准确导致搜索反复认为某一手棋能扭转局面。增加搜索次数通常能缓解但更根本的办法是优化价值网络的评估质量。我自己的排查顺序是先增加搜索模拟次数看是否改善如果改善说明问题在搜索深度如果没有改善则需要回头检查价值网络在局部战斗局面的评估是否过于极端必要时在训练数据中加强对杀局面的采样比例。写在最后的实践建议这几轮项目做下来我最大的体会是深度学习围棋系统的难点不在于某一个单独的模块而在于模块之间的衔接和数据流转。特征编码、网络训练、自我对弈、蒙特卡洛树搜索任何一环出问题整体棋力都会断崖式下跌。如果你也准备动手做类似的项目我给一个最实际的建议先从小棋盘开始。9路棋盘的局面复杂度远低于19路迭代速度快、实验成本低特别适合验证网络结构和训练流程。在9路上把整套流程跑通、把坑都踩过一遍之后再迁移到19路难度会小很多。另外一个小技巧训练过程中定期保存网络权重文件和对应的评估结果方便回滚到历史最优状态。强化学习训练中网络会经历起伏手动记录关键节点的棋力表现能帮助你更清晰地判断哪些改动是真正有效的。围棋AI这条路很长但每一步都很有收获。希望这篇内容能帮你少走一些弯路也期待你跑出自己的结果。