MCTS + CNN 自对弈强化学习实战总结
MCTS + CNN 自对弈强化学习实战总结
本文是对一个五子棋自对弈强化学习项目(GomokuCnn)的完整复盘。 项目在一台 RTX 3060 Laptop 上,用 AlphaZero 式"自对弈 → 训练 → 评估" 循环从零训出强 AI:最终模型对战传统 NNUE 引擎 rapfi,先手执黑基本保持 100% 胜率。 训练方案参考了 zhoukangyang233/Gomoku (nn_012 管线),在此之上做了大量机制研究与工程优化。
这个项目最有价值的部分不是"训出了一个能下棋的 AI",而是过程中沉淀的 几条反直觉结论——它们大多来自失败的 run,每一条都有实测数据背书:
- 标签质量不来自 sims 数,来自标签生成机制;棋力不来自单轮精度,来自总轮次;
- 双盲期不是要"解决"的问题,而是要"穿越"的阶段;
- 物化 D4 对称增强是强 AI 的信息论硬约束,Online D4 + 经验回放无法替代;
- 防杀能力不生活在搜索里,它生活在训练标签里;
- 跨轮持久的低学习率是棋力退化的隐形杀手。
下面按管线逐一展开。
一、管线总览
整体是标准 AlphaZero 架构的轻量版:
▼text复制代码自对弈(MCTS + 当前模型生成对局数据) ↓ 标签生成(全节点收集 + prior/Q 分层重分配) ↓ 物化 D4 八倍对称增强 ↓ 训练(每轮重建 Adam + ReduceLROnPlateau 轮内调度) ↓ arena 竞技评估(对 champion 胜率 ≥52% 晋升) ↺ 回到自对弈
关键配置(低 sims 三支柱,缺一不可):
| 支柱 | 配置 | 作用 |
|---|---|---|
| 低算力搜索 | 30 sims/步 | 单轮快 → 总轮次多 |
| 标签机制 | 全节点收集 + 分层重分配 | 低 visits 数据可用 |
| 小网络 | 32 通道 / 5 残差块 | 容量匹配噪声标签 |
三支柱是绑定的。只搬其中两根会得到最差组合:首次启动时曾用 128ch/6blk 大网络配 30 sims,单轮 8 分钟(大网络吃不动 28 万全收样本), 且大容量在噪声标签上只会过拟合——128ch 的训练段计算量是 32ch 的 19 倍。 换 32ch 后全程平均约 22 秒/轮(自对弈 ~6s + 训练 ~15s)。
二、低 sims 的经济学:总轮次 > 单轮标签精度
选择 30 sims 是一场明示的赌注。自对弈训练的总学习信号可以分解为:
▼text复制代码总学习信号量 ≈ 总轮次 × 每轮数据量 × 每样本信息密度
高 sims 管线的每样本信息密度高,但每轮慢;低 sims 管线的前期标签 基本是噪声(30 次模拟摊在 20 个子节点上,每个 1~3 次访问,毫无分辨力), 但换来的是 7 倍以上的轮次吞吐。
一组真实的对照实验:
- run_8:800 sims,268 轮——arena 胜率停在 59% 天花板,且其 base 本身停滞后,新 run 只能摸到 base 的水平;
- nn_012(参考管线):30 sims,约 20000 轮——训出强 AI,对弈仅需 200 sims 就能稳定防杀。
轮次差 7.5 倍,sims 差 27 倍,结果完全倒挂。单轮标签精度的短板 可以靠机制补(见下文重分配),总轮次的短板没有任何东西能补。
代价也要有预期:低 sims 的双盲期更长(30 sims 树深约 3 层,搜不出 战术序列,战术能力只能等网络从数据里"背会"棋形)。这是设计内风险, 不是 bug。
三、双盲期:定义、诊断与穿越
什么是双盲期
自对弈冷启动阶段,对局双方(同一模型的自我复制)都弱到数据分布中 不含任何有意义的对抗行为——双方都不堵、不攻,谁先随手连五谁赢。 模型从这样的数据里学不到进步方向,形成自我强化的低水平均衡:
▼text复制代码模型弱 → 搜索树里没有战术信号 → 训练数据里没有"防守收益"的正例 → 模型学不会防守 → 模型依然弱
诊断指标(不要靠肉眼感觉)
run_1 实测的双盲期指纹:
| 特征 | 实测值 | 说明 |
|---|---|---|
| 对局极短 | ~3.5 s/100 局 | 折合每局仅 ~10 手搜索 |
| 胜率暴涨假象 | arena 对随机 base 99:1 | 赢随机对手不需要会下棋 |
| value/policy 分离 | val value 0.088→0.029,policy 反而恶化 | 终局信号先教会 value 头;policy 无信号可学 |
三个关键判据:
- 对局长度(最灵敏):防守涌现 = 互堵 = 对局拉锯变长。自对弈 耗时台阶式上升且不回落才是涌现(平台期单次冲高不算数);
- arena 对 champion 的持续区分度:50
60% 震荡且晋升不断 = 代际 进步;长期 4550% 停滞 = 警惕; - val value loss 在降:说明逃逸引擎在转。注意 val policy loss 不能当判据——重分配标签是移动靶(value 变准 → 标签本身变锐), run_1 全程 policy loss 没有掉头向下,模型照样逃逸了。
穿越手段
按"重要性 × 容易被忽视"排序:
- 随机开局(opening_max_moves=20,保留空盘开局概率):纯空盘自 对弈的开局标签退化成"先验 top-k 摊平",且所有对局开局段高度相似。 随机预落子把对局起点推进中盘,并用网络估值筛选最均势局面。踩坑 提醒:每个候选必须独立采样——参考实现曾把 shuffle 放在循环外, 1000 个候选实际只有 ~11 种不同局面;
- 温度:每局随机 0.1~0.8,让整个数据集覆盖从贪心到乱下的行为谱 (固定两阶段温度会导致中后期轨迹趋同,中盘残局偏弱);
- 根节点 Dirichlet 噪声(α=0.1,混合比 0.25):只加在根节点, 防止搜索被先验锁死;
- 层次重分配标签:这是低 sims 可行的支点——30 sims 的原始访问 分布是噪声,但每个子节点下方的树内 Q 是连续信号,把访问概率 按"Q 对比 + prior 层级"重新分配给树认为更好的分支,配合全节点 收集(visits 门槛设 0,可信度进样本权重 √(visits/sims)), 低 visits 数据不用丢弃。双盲期里它救不了命(Q 也是噪声),但 value 头一旦变准,policy 标签立刻变准,不用等数据分布先改变 ——这把逃逸正反馈链的长度砍了一半;
- value 头是逃逸的正反馈引擎:终局胜负 z 是唯一永远真实的监督 信号。逃逸链条:终局 z → value 变准 → 树内 Q 变准 → 重分配标签 变准 → policy 学会堵活三 → 对局拉锯 → 数据出现真战术 → 正循环。
run_1 完整逃逸时间线(1200 轮,约 12 万局,8.4 小时)
| 阶段 | 轮次 | 自对弈耗时/轮 | arena 表现 | 状态 |
|---|---|---|---|---|
| 双盲期 | r0~99 | ~3.6 s | 频繁晋升(假进步) | 随手连五 |
| 长平台 | r100~799 | 3.6→9.6→5.5 s 冲高回落 | 晋升频繁但幅度小 | 行为在变 |
| 深平台 | r800~1049 | ~3.4 s | 胜率 50% 无区分 | 全程最像"失败" |
| 逃逸启动 | r1050~1149 | 3.5→5.0 s | 场均 82.5%,连续大胜 | 突破 |
| 巩固 | r1150~1379 | 稳定 7~8 s | 54~63% 震荡晋升 | 中等 AI 强度 |
最深刻的教训:深平台段(胜率 50% 无区分 + 耗时回落到双盲水平)是 全程最像"该止损"的阶段,但它恰恰是逃逸前夜。防守是模式识别问题, 网络需要"见过足够多活三/冲四被惩罚"的对局——run_1 逃逸时累计约 12 万局,与参考实现的 20 万局同一数量级。数据量门槛真实存在,管线 优化只是压低它,没有取消它。
四、物化 D4:强 AI 的信息论硬约束
逃出双盲期之后,下一个瓶颈是自对弈闭环的对称性破缺:
▼text复制代码模型有微小方位偏置(如左上角着法略多) → 自对弈生成的局面里,左上方战术出现频次更高 → 训练后偏置被巩固甚至放大 → 下次自对弈继续生成偏置数据 → 闭环正反馈:偏置永久固化,盲点永远存在
关键在于这个偏置发生在数据生成端。训练端的 Online D4(取样时 随机翻一个方位)只翻已有的偏置数据,改变不了模型下次下棋的偏置。
硬约束 vs 软约束
- 物化 D4:每个原始局面物化 8 份方位副本,8 个方位在同一次 forward/backward 里一起反传——梯度直接竞争,模型没有任何机会 选择性忽略某个方位;
- Online D4 + replay_rounds=7:数字上等效(每样本训练 24 次, 方位覆盖 ~96%,Coupon Collector 问题),但 8 个方位分散在不同 epoch、不同轮次,每次训练步模型权重都不同——梯度永远不直接 竞争,偏置有充分的生存空间。且 replay 旧数据的重分配标签是 历史 value 头生成的,已经过时。
分阶段收益
| 阶段 | 核心瓶颈 | 物化 D4 收益 | 性价比 |
|---|---|---|---|
| 冷启动期 | 标签全是噪声 | 8 份噪声平均,信噪比 +√8 | 差(每轮样本 ×8) |
| 逃出冷启动后 | 闭环对称性破缺 | 打破偏置正反馈,消除盲点 | 极高 |
所以正确的姿势是:没学会防守之前 Online D4(省钱熬双盲期), 学会防守之后必须物化 D4(强 AI 的关键)。这个约束与 sims 数 无关——高 sims 管线标签干净后,同样会被闭环偏置卡在强 AI 门槛前。
五、value 标签前缀重分配:防杀能力住在标签里
这是全项目最硬的一条结论,证据来自一次搜索预算完全倒挂的对抗测试:
- 参考管线:约 2 万轮训练,对弈 200 sims——稳定防杀;
- 新管线:6000 轮训练,对弈 3600 sims——反复失守。
18 倍搜索预算差距。修复前的新管线对强算杀对手呈现出两个不对称:
▼text复制代码对手的杀点落子之前:树里完全搜不到 10~20 步杀(3600 sims 如同不存在) 对手的杀点落子之后:立刻判断对面胜率 99%(一次前向就完成)
"下之后"只是模式识别(威胁已成型,CNN 一眼可见);"下之前"需要的 是前瞻推理,MCTS 能算深杀的唯一前提是 value 头在杀路早期节点上 给出强烈负值信号,引导搜索分配模拟。而嗅杀能力是训练标签教出来的。
问题出在 value 标签的"半套重分配"
原管线只重分配了 policy 标签,value 标签仍用访问量加权树 Q:
▼text复制代码子节点 A:prior=0.7(正常回应),Q=+0.2,访问 7 次 子节点 B:prior=0.2(潜伏杀点),Q=+0.99,访问 3 次 访问量加权 Q:(7×0.2 + 3×0.99)/10 = 0.437 → 杀点信号被访问占比稀释,再被平庸回应平均掉 前缀 max 重分配:value ≈ 0.551,杀点权重 0.44,远超其 0.3 访问占比 → max 结构保证极端值不被平均掉
更尖锐的情况:杀点只被访问 1 次时(30 sims 常态),旧标签里它的 权重是 1/30 ≈ 3%,信号基本消失;前缀 max 里只要它 prior 靠前, 包含它的层级价值就全额等于它的极端价值。
修复方法:value 标签与 policy 标签从同一个分层循环产出——按 prior 降序逐层差分切分概率预算,每层的前缀 max 价值以同源权重累入 value 标签。数学上有 telescoping 恒等式保证权重和恰为已访问 prior 之和,标签是子节点取反 Q 的凸组合,恒在 [-1,1],与 tanh 输出域对齐。
为什么堆数据补不上这块?自对弈闭环会系统性饥饿这类样本:双方都 看不见长杀 → 树内 Q 没信号 → 对局在杀路成型前就结束了 →"被正确 标注的潜伏威胁"样本极其稀疏 → 模型更看不见。与 D4 闭环偏置同构: 数据生成端缺失的东西,训练端技巧补不齐。前缀 max 有效恰恰是 因为它不需要额外数据——它把树里已经存在但被埋没的信息以正确权重 暴露出来。
教训:3600 sims 防不住的,36000 也防不住——搜索预算由 policy 先验驱动分配,杀点先验低的前提下,堆预算是无效堆砌。
六、学习率:一次退化的教训
run_2 曾采用"余弦下降 + 谷底停留 + arena 驱动跳回"的跨轮 LR 状态机, 结果是:前 324 轮 9 次晋升后,LR 长期压在谷底 lr_min=1e-5 长达 460 轮,期间 arena 对 champion 胜率 45/49/39/50/51%(含明显连败), 棋力巅峰停在前 300 轮。
根因:跨轮持久优化器 + 谷底低 LR = Adam 动量长期累积 + LR 过低, 双重退化。
最终方案(nn_012)极其朴素:
▼python复制代码# 每一轮训练(每次调用)都重建优化器与调度器 optimizer = optim.Adam(model.parameters(), lr=1e-4) # 每轮回 1e-4 scheduler = ReduceLROnPlateau(optimizer, mode='min', patience=2, factor=0.5) # 每个 epoch 用当轮验证损失 val_total = 2*val_value + val_policy step 一次
优化器每轮重建带来三个性质:LR 每轮回到 1e-4、Adam 动量不跨轮累积、 scheduler 每轮重置。与旧方案的本质区别:旧方案在固定评估集上做 跨轮调度(锚定 base,结构上跟踪不了进步);新方案在当轮新鲜自对弈 验证集上做轮内调度(3 个 epoch 内动态下调)。
另外一个相关教训:固定评估集 loss 与模型进步是结构性脱钩的——模型 变强后会偏离 base 的着法偏好,考卷 loss 反而上升。它可以留着看趋势, 但永远不能驱动 LR 调度和早停。
七、工程优化速览
消费级显卡上跑通这套管线,工程优化占了一半功劳:
- C++ MCTS 核心(pybind11):树操作加速 33.9 倍,端到端 2.18 倍;
- 批量叶子推理(infer_batch_size=16):30 sims 树浅,批 64 会 覆盖整棵树导致 Q 反馈进不了访问分布;
- bf16 autocast 训练 + channels_last:batch 2048 下 3.64 倍;
- fp16 + channels_last 推理:forward 2.49 倍;
- 4 worker 并行自对弈:~20 分钟/100 局训练轮(优化前配置下)。
八、最终成果与已知短板
最终模型(models/run_6/950.pth)对战 NNUE 引擎 rapfi 的实测:
- 先手执黑:基本 100% 胜率;
- 后手执白与平衡开局:表现不佳。
短板的原因很清晰:自对弈管线的数据分布由模型自己决定,后手防守 局面与人为构造的平衡开局不在自然分布内。补强需要另做这两项的 专项训练——这本质上是又一处"数据生成端缺失"问题。
另一个值得记录的观察:修复 value 标签后,自对弈能稳定下到残局, run_5 的 6300 局里出现了 2 局和棋——终局覆盖和防守能力的改善 会直接反映在对局长尾上。
九、可迁移的结论
如果把整个项目压缩成几句话:
- 标签机制 > sims 数,总轮次 > 单轮精度。 低 sims 的标签噪声 靠重分配机制补,高轮次的学习信号没有任何替代品;
- 双盲期靠穿越,不靠解决。 盯住对局长度、arena 区分度、 val value 三个指标;最像失败的深平台往往是逃逸前夜;
- 自对弈闭环里,数据生成端缺的东西训练端补不齐。 方位偏置 要靠物化 D4 硬约束,潜伏威胁要靠 value 标签前缀 max,后手与 平衡开局要靠专项数据——三条同构;
- 防杀能力住在标签里,不住在搜索里。 搜索预算由先验驱动, 先验看不见的,预算堆多少都是无效堆砌;
- 跨轮持久的低 LR 是退化的隐形杀手。 每轮重建优化器 + 轮内调度,朴素但有效。
参考与深入阅读
- 项目仓库:https://github.com/YorickFin/GomokuCnn
- 参考管线:zhoukangyang233/Gomoku(nn_012)
- 对抗测试对手:dhbloo/rapfi(NNUE)
本文每个主题在项目仓库 docs 目录下都有一篇完整的机制分析, 含全部实测数据、推导过程与代码落位:
