AlphaZero 五子棋实战(一):单卡从零自举,我的v36 最终版配置
这是一个用消费级显卡跑了2个月多的 AlphaZero 五子棋自举实验:不喂棋谱、不装外部引擎当老师、不用开局库 ,就靠"网络 + MCTS + 自己跟自己下"从零长棋力。这篇文章把最终版 v36 的全套配置、参数、训练策略一次性摊开------包括每个参数现在是多少、为什么是这个值、以及它把外部引擎 Rapfi 打到了什么水平。
全文约 6000 字,所有配置都来自线上训练脚本的真实取值,所有战绩都有对局记录,无水分。
适合人群
- 想动手复现 AlphaZero 但卡在"配置不知道填多少"的朋友
- 自己训过自对弈,被"棋力不涨 / 后手崩盘 / loss 降了棋力却降了"折磨过的朋友
- 对分布式自对弈工程落地(单卡怎么榨出产局效率)感兴趣的朋友
你将收获
① 一套可直接复现的 15×15 五子棋 AlphaZero 完整配置(网络 / MCTS / 训练 / 开局规则逐项列全)
② 单张消费级显卡 + 9 个工兵进程的分布式产局工程细节
③ 一套"打 PK 不算白打"的评估方法论(20 局、ring 规则、黑白拆解)
④ 一条真实的外部棋力锚点曲线:同一个引擎,从"我们被虐"到"我们稳赢"
目录
- [1. 先说清楚:这个项目到底在验证什么](#1. 先说清楚:这个项目到底在验证什么)
- [2. 工程架构:一张 2080Ti + 9 个工兵](#2. 工程架构:一张 2080Ti + 9 个工兵)
- [3. 网络结构:4 通道输入 + 8 个残差块](#3. 网络结构:4 通道输入 + 8 个残差块)
- [4. MCTS:为什么死死定在 800 次搜索](#4. MCTS:为什么死死定在 800 次搜索)
- [5. 训练循环:32 批次 × 512 样本](#5. 训练循环:32 批次 × 512 样本)
- [6. 开局规则:前 3 手随机落子(整套配置最关键的一条)](#6. 开局规则:前 3 手随机落子(整套配置最关键的一条))
- [7. 诊断体系:别只盯着 loss](#7. 诊断体系:别只盯着 loss)
- [8. 评估方法论:怎么打 PK 才不算白打](#8. 评估方法论:怎么打 PK 才不算白打)
- [9. 外部锚点 Rapfi:从被打崩到稳赢 depth10](#9. 外部锚点 Rapfi:从被打崩到稳赢 depth10)
- [10. 训练结果:白棋终于不崩了,但故事没这么简单](#10. 训练结果:白棋终于不崩了,但故事没这么简单)
- [11. 踩坑速查表(建议收藏)](#11. 踩坑速查表(建议收藏))
1. 先说清楚:这个项目到底在验证什么
2017 年 AlphaZero 出来的时候,我正好在 Leela Zero 那个众筹项目里帮忙------但干的是算账的活,收钱、租机器、记账,技术讨论插不上嘴。那种感觉挺难受的:眼看着一个"纯自对弈就能长棋力"的命题摆在面前,自己却只能在外面看着。
后来有了时间和卡,就想把当年那个问题亲手验证一遍:
一个网络,喂给它的只有棋盘和规则,没有人类棋谱、没有引擎当陪练,它到底能长到多强?
所以这个项目有一条自始至终没破过的铁律:训练闭环里禁止任何外部知识注入。
- ❌ 不拿 Rapfi(开源五子棋引擎)当训练对手------那是"复制外部能力",不是自举
- ❌ 不喂人类棋谱当开局库
- ❌ 不用求解器给败局标注"正确应手"
- ✅ 能用的是:开局规则、网络结构、超参、经验池管理、搜索次数(这些都在"自举"命题之内)
Rapfi 在这个项目里只有一个身份:期末考卷。平时绝对不碰,只在需要知道"现在到底多强"的时候拉出来考一次。
这个原则听起来有点轴,但它决定了后面所有的设计------包括最关键的那条"随机开局"规则(v36 的解),其实也是这条原则逼出来的。
2. 工程架构:一张 2080Ti + 9 个工兵
先把硬件摊开,免得后面看数据没感觉。
| 项 | 配置 |
|---|---|
| 训练卡 | RTX 2080 Ti 22GB(云上按小时租) |
| CPU | 多核(用于并行产局) |
| 系统 | Linux + miniconda + PyTorch 2.x |
| 本轮训练周期 | 09-06 08:18 起,连续约 144 小时 |
产局方式:1 个主进程 + 9 个工兵进程。
| 角色 | 每代局数 | 说明 |
|---|---|---|
| 主进程 | 10 局 | 自己也要下,顺便维护经验池和存档 |
| 工兵 × 9 | 9 × 10 = 90 局 | 只负责产局存盘,不训练 |
| 合计 | 100 局/代 | 每代约 33 分钟,主要时间花在自对弈搜索上 |
为什么是"每代 100 局"这个量级?
因为 MCTS 是 CPU 上的串行搜索,自对弈产局是整条链路上最慢的一环------800 次模拟 × 每局几十手,一局下来就是几十秒到一分多钟(棋力越强对局越长,后期单局能到 71 秒)。所以工程上的核心思路就一句话:让 GPU 只负责前向推理和训练,自对弈的搜索活儿全部摊给并行进程。
工兵的设计上有个坑值得先提一句:工兵是通过 importlib 从主脚本里导入网络定义的,必须和主脚本放在同一个目录,否则导入直接失败。这种"看起来像没问题的路径问题",半夜挂训练的时候特别容易卡住你。
3. 网络结构:4 通道输入 + 8 个残差块
这是 v36 的网络全貌:
| 模块 | 结构 | 参数量 |
|---|---|---|
| 输入 | 4 通道 15×15 | --- |
| 主干入口 | Conv2d(4→128, k3, pad1) + BN + ReLU | ~4.6K |
| 残差主干 | 8 × ResBlock(conv3×3 → BN → ReLU → conv3×3 → BN → 加法 → ReLU) | ~2.36M |
| Policy 头 | 双分支:conv(128→1, k3) + conv(128→1, k1) → concat(2ch) → Linear(450→225) → log_softmax | ~101K |
| Value 头 | conv(128→1, k3) + BN → fc1(225→128) → ReLU → fc2(128→1) → tanh | ~29K |
| 合计 | 约 240 万参数 |
3.1 4 个输入通道分别是什么
| 通道 | 含义 |
|---|---|
| ch0 | 当前行动方的棋子(1 = 有子) |
| ch1 | 对手的棋子 |
| ch2 | 恒为 1.0(常量平面) |
| ch3 | 上一步落子位置 |
第 2 个通道恒为 1 看着很奇怪,这是踩过大坑之后的取舍------早期版本曾经在这里编码"我是黑还是白",结果网络学会了"抄执色"这条捷径。这块血泪史第二篇会专门讲。
颜色对称性是这套编码的核心收益:棋盘和棋子做颜色翻转,等价于把 ch0 和 ch1 交换。也就是说,网络对"我执黑"还是"我执白"在结构上就是对称的------白棋视角不需要网络额外学一遍。
3.2 Policy 头为什么长这样
它的形状有点怪:两个分支(一个 3×3 卷积、一个 1×1 卷积)拼成 2 通道,再 flatten 成 450 维,过一个 Linear(450→225) 输出 225 个落点。
- 3×3 分支负责局部棋形感知(活三、冲四这种形状得看邻域)
- 1×1 分支负责"当前点本身"的信息
- 那层 Linear 是全局组合层------它让网络能把"左边有个活三"和"右下角有个冲四"这两件事组合起来判断,而不是各看各的
⚠️ 注意一个细节:policy 输出是
log_softmax(对数概率域)。这在所有下游都要小心------搜索里想拿概率,必须先exp一次。这个细节后来炸出了一个污染 7 个版本的 bug(第二篇详述)。
3.3 Value 头
conv → BN → fc1(225→128) → fc2(128→1) → tanh,输出当前局面在当前行动方视角下的胜率期望 -1, 1。
中间那层 128 维隐藏层是有来历的:最早是单层 fc(225→1) 直出,后来换成了带隐藏层的版本,再后来又试过把它压到只有 226 个参数的极简版------结论是容量不是瓶颈,真正的瓶颈在标签怎么给(也是第二篇的内容)。
4. MCTS:为什么死死定在 800 次搜索
搜索这块的参数不多,但每一个都有故事。
| 参数 | v36 取值 | 备注 |
|---|---|---|
| 模拟次数 sim | 800,全程固定不变 | 从第 0 代到第 288 代都是 800 |
| c_init | 1.25 | PUCT 常数,标准值 |
| c_base | 19652 | 标准值 |
| 根节点噪声比例 | 0.25 | 标准值 |
| Dirichlet α | 0.05 | α × 225 ≈ 11.25 |
| 噪声注入次数 | 每步只注入一次(根先验),800 次模拟共用 | 关键 |
| 每步搜索量 | for _ in range(800) 真跑满 800 次 |
关键 |
| 树复用 | 每局开始必须 reset_mcts() |
关键 |
4.1 sim 为什么是 800
一个直觉问题:搜索次数越大,老师越强,数据质量越好------那为什么不往 8000 冲?
因为搜索次数和训练速度是直接对立的两端,我们做过完整的对照:
| 版本 | sim 策略 | 结果 |
|---|---|---|
| v1 | 动态 50→200→400→800 | 早期 50 次模拟时,搜索出来的落点分布接近均匀,网络从这种数据里学不到任何东西(policy 熵 96 代没动过) |
| v2 | 固定 400 | 稳定下来了,但 400 次模拟给出的信号还是偏"糊" |
| v6 | 固定 800 | 只用了 18 代就追平了 v2 跑 202 代的水平 |
"18 代追平 202 代"这个数据是决定性的------它说明在 400 到 800 这个区间,搜索质量对训练效率的边际收益还是正的,所以直接把 800 定成常数。至于 1600 / 3200,每代耗时直接翻倍,而当时试验预算不支持,就留成了"后面再说的选项"。
一句话总结:800 不是拍脑袋,是"再低一点数据就糊了,再高一点跑不动了"的甜点。
4.2 温度调度:三段式快速降温
自对弈里每一步都要"从 MCTS 的访问分布里采样落子",温度决定采样的随机程度。v36 用的是一条手动设计的三段曲线:
| 手序 | 温度 | 设计意图 |
|---|---|---|
| 第 0 手 | 1.00 | 开局放开探索 |
| 第 1 手 | 0.87 | ↓ |
| 第 2 手 | 0.73 | ↓ |
| 第 3 手 | 0.60 | 开局结束,快速收 |
| 第 4 手 | 0.49 | ↓ |
| 第 5 手 | 0.38 | ↓ |
| 第 6 手 | 0.26 | ↓ |
| 第 7 手 | 0.15 | 进入稳定段 |
| 第 8 手及以后 | 0.15(平台) | 一直到开局阶段结束 |
为什么要搞这么激进的前段降温?这来自一次很实在的数据:有一版训练里 42% 的对局在第 11 手之前就结束了------双方像背了定式一样互相速杀,中盘根本没机会展开。
调完温度之后:
| 指标 | 调之前 | 调之后 |
|---|---|---|
| 超短局(≤11 手)占比 | 42% | 7% |
| 中位手数 | 14 手 | 23 手 |
对局变长,意味着网络真正在有来有回的局面里积累经验------这个改动的收益是全局性的。
4.3 三个"必须小心"的实现细节
这三条都是踩出来的,不是抄来的:
- 噪声只注入一次 。Dirichlet 噪声要在根节点的先验上加,一次就够,800 次模拟共用这个根先验。早期版本照着某本书的写法"每次模拟都重新采样噪声覆盖根先验",等于把探索放大了 800 倍------树根本稳定不下来,最后把 value 网络教成了只会分"赢/输"两类的分类器。
- 每步必须真跑满 800 次 。判断循环条件如果用
while count.sum() < 800,当这个局面在之前的搜索里已经被访问过(父节点搜索灌了不少次数进来),循环会直接跳过------等于这一步 0 次搜索 ,直接复用旧分布。正确写法是for _ in range(800),树可以复用,但搜索量一次都不能打折。 - 每局开始清空搜索树 。局与局之间必须
reset_mcts(),否则第 2 局开局时会发现问题被"上一局的搜索结果"污染了。
5. 训练循环:32 批次 × 512 样本
| 参数 | v36 取值 | 说明 |
|---|---|---|
| 每代自对弈局数 | 100 局(主 10 + 工兵 90) | |
| 每代训练批次 | 32 批 | |
| 批大小 | 512 | |
| 优化器 | Adam | |
| 学习率 | 0.001,全程固定 | |
| weight_decay | 1e-4 | 但 BatchNorm 参数单独分组,wd = 0 |
| 经验回放池 | 最近 5 代数据混合 | |
| 数据增强 | 8 方向对称(4 旋转 × 2 翻转) | |
| Value 标签 | 该步搜索 Q 值(软标签) | 不是终局的 ±1 |
| Policy 标签 | 该步 MCTS 访问计数分布 | |
| 代间是否冻结 | 不冻结,全网络一起训 |
这里面有三个点值得单独说。
5.1 BatchNorm 参数必须单独分组、weight_decay 设 0
这是用一次"0:10 惨败"换来的结论。
BatchNorm 里有 γ(缩放)和 β(平移)两组参数。如果让它们和卷积权重一起吃 weight_decay,会出现一个死亡螺旋:γ 被持续往 0 压 → 深层 BN 归一化失效 → 某些通道反相(γ 变负数)→ 网络表达力崩塌。
我们做过一次反向实验:把一个已经训练好的强权重里最深那层 BN 的 γ 复位成 1,看会怎样------被 0:10 碾压 。这说明深层 γ 收缩是网络自己学到的必要机制,不是病。所以正确的做法不是"禁止它收缩",而是:把 BN 参数单独分组,weight_decay 设为 0,让网络自己决定收缩到什么程度。
5.2 Value 标签用"搜索 Q 值",不用终局 ±1
这是 v33 之后才定下来的,也是治好一个慢性病的关键。
早期 value 标签是"这局最后我是赢还是输"→ ±1。问题在于:一盘棋输掉,不代表这一步下得差;反过来赢的棋里也有一堆烂手。网络拿着"整局结果"去拟合"每一步的价值",学到的是噪声。
改成 Q 软标签 之后:每一步的 value 目标 = 这一步 800 次搜索给出的 Q 值(即"在当前局面下,下这里大概能赢多少")。这个信号是密集的、和具体局面绑定的。
效果立竿见影:空盘局面的 value 极端化从 52.8% 直接掉到 0%------网络不再无脑输出"必胜/必败",而是学会了给中间局面一个合理的胜率。
5.3 8 方向对称增强
棋盘本身有 8 种对称变换(4 个旋转 × 2 个翻转),对每一步训练样本都做这个增强,等于样本量直接放大 8 倍。
有个小坑顺嘴一提:早期实现里翻转是用 fliplr 对四维张量操作的,实际上没生效 ------等于 8 个分支里有 4 个是重复的。后来改成显式 np.flip(axis=-1) 才真正对齐。
6. 开局规则:前 3 手随机落子(整套配置最关键的一条)
如果整篇文章你只记住一条,记这条。
v36 相比 v35 的唯一改动就是它:
前 3 手(黑 1、白 1、黑 2)100% 随机落子,不经过任何网络或搜索决策。
- 黑 1:落在距天元 ≤ 4 的范围内(9×9,81 个点)
- 白 1 / 黑 2:落在距天元 ≤ 3 的范围内(中心 7×7,49 个点)
- 三个点互不重合
6.1 为什么这么干
五子棋 15×15 没有贴目,黑棋先手在顶级搜索下是必胜 的。我们实测过:两个同代强权重,sim 800 对打,执黑方 100% 机械必胜。
这个"先手红利"给训练带来的灾难是:白棋赢的样本越来越少 → 网络越学越觉得"白棋没救" → 白棋下得更烂 → 白棋赢的样本更少。这就是个数据单边化的死亡螺旋。在 v18 到 v35 的六个大版本里,"白棋崩盘"反复出现,怎么调都按不住。
而"随机开局"这一招的逻辑非常直接:
既然胜负被"谁执黑"决定了,那就把这个决定权交给随机数 ------开局前三手随机撒在中心区,局面质量从一开始就是随机的,谁赢取决于接下来怎么下 ,而不是取决于谁执黑。
6.2 效果
| 指标 | 随机开局之前(v35) | 随机开局之后(v36) |
|---|---|---|
| 白棋胜率 | 反复崩到 < 30%(最差个位数) | 稳定在 39-51% |
| 黑胜样本占比 | 严重单边化 | 恢复平衡 |
| 连续不崩 | 最长也就几十代 | 近 290 代没崩 |
更狠的证据在 PK 上:v36 的 it11(才训了 11 代)就以 20:0 完封了历史最强版本 v18 的王座权重 ------包括执白 10:0。也就是说,这个改动不是在"慢慢改善",而是直接把一个长期困局解开了。
随机范围的量也调过:一开始用 7×7,试过放大到 13×13(发现太散,白棋胜率被推到 56-64% 过头了),最后回调成"黑 1 用 9×9、白 1 和黑 2 用 7×7"------刚好落在 45% 左右的平衡带。
7. 诊断体系:别只盯着 loss
这部分是我觉得最有价值、也最容易被人忽略的。loss 降了棋力不涨、甚至棋力下降,在这个项目里发生过不止一次。
所以除了最基础的 loss,我们搭了一套"体检"工具,每次想知道网络什么状态,跑这几项:
| 项目 | 看什么 | v36 后期实测 |
|---|---|---|
| 做题集 | 40-43 道固定题(活三该不该堵、冲四该不该防),看 top1 命中率 | 67.4% |
| 败局库静态 | 257 个已知败局局面,看网络给的分 | 31.9%,均值 +0.338(⚠️ 偏乐观) |
| 威胁分级 | 网络对不同威胁等级(活三/冲四/活四)的敏感度 | 0.985 / 0.989 / 0.997(⚠️ 接近饱和) |
| 有效秩 | 关键层权重矩阵的有效秩,看是否塌缩成低秩 | value_fc1 = 33.3(≈26%)⚠️ 单调低秩化 |
| D60/D80 | 用当前权重跑 MCTS,能提前多少步发现必败 | 12-13 步 / 7 步(⚠️ 停滞) |
三个"⚠️"是有意标的:这些指标到了后期全都显示"停滞"或"有偏",但实际棋力还在涨(下一节讲)。
还有一个教训特别值得写下来:探针喂数据必须走正规的输入构造路径 。我们有一次直接拿全零张量喂进网络做"空盘评估",得出的结论是"空盘 value +0.74,病态!"------结果正确的 4 通道构造方式一跑,实际是 -0.001,完全正常。探针自己写错了,比不做探针更危险。
8. 评估方法论:怎么打 PK 才不算白打
"两个权重打 20 局谁赢"这件事,我们踩了太多坑,最后沉淀成六条硬规矩。你如果要自己训,这六条可以直接抄。
| # | 规矩 | 为什么 |
|---|---|---|
| 1 | 必须 20 局/场 | 10 局口径的噪声有 ±1~2 局。实测 6 场里就有 1 场结论被反转 |
| 2 | 必须开 ring(换先手)规则 | 标准规则下,两个强权重执黑 100% 必胜,那还测什么棋力------测的是谁运气好抽到黑 |
| 3 | 必须拆黑白看 | 就算用了 ring,执黑方仍然占 55-75% 胜率,不拆开看会被平均数骗 |
| 4 | 静态指标平坦 ≠ 训练无效 | 白胜率、做题分数都会被自对弈的自我平衡吸收掉,只有对外 PK 能照出真实成长 |
| 5 | 单代 PK 不能判定进步 | 实力曲线是锯齿状的:it120 垫底,it150 反而反超 it100 |
| 6 | 链式推理会失效 | 存在"非传递环":A 赢 B、B 赢 C、C 又赢 A。"最强"这个词依赖对手是谁 |
第 6 条不是理论------我们实测出了一个完整的环:
it190 > it160 > it259 > it240 > it230 > it210 > it190 ⟲
it160 以 14:6 碾压 it259,自己却输给 it190。所以任何"it_X 比 it_Y 强所以我用 X"的链条推理,在自对弈里都可能翻车。
9. 外部锚点 Rapfi:从被打崩到稳赢 depth10
前面说了,Rapfi 在这个项目里是唯一的期末考卷------不作为训练对手,只在做评估时拉出来考。
Rapfi 有个 max_search_depth 参数,可以理解成"限制它想几步",depth 越大越强。我们用这条阶梯做绝对棋力锚点。
9.1 战绩进化线
| 时间 | 权重 | Rapfi depth | 结果 |
|---|---|---|---|
| 08-25 | v18 it156 | d1 | ✅ 首次真实击败(历史性突破) |
| 09-08 | v36 it81 | d1 / d2 | ❌ 0:2 / 0:2 告负 |
| 09-08 | v36 it81 | d3 | ✅ 1 胜(执黑 33 手取胜) |
| 09-10 | v36 it167 | d10 | ❌ 重大失误 |
| 09-10 | v36 it193 | d10 | 🟡 势均力敌 |
| 09-11 | v36 it241 | d10 | 🟡 棋差一招 |
| 09-12 | v36 it283 | d10 | ✅ 开局杀(29 手) / 开局爆杀(35 手)------执黑基本稳赢 |
看这条线最有意思的地方:从 it167 到 it283,只隔了 116 代训练 ,而且这 116 代在"自己人打自己人"的口径下完全看不出进步------但对固定标尺 Rapfi d10,从"重大失误"一路走到了"稳赢"。
9.2 一个坦白的限定
上面这些 d10 战绩全是执黑(先手)打的,执白对 d10 还没测。
这不是疏忽,是实事求是------五子棋先手优势太大,"执黑稳赢 d10"和"棋力已经稳赢 d10"是两句话。执白那边等到有结果再报。
9.3 所以现在是什么水平
按"能稳定击败 Rapfi depth10"这个口径,基本可以算摸到了中级水平------离引擎的全力深度还差得远,但已经不是"三岁小孩"了。
顺便说个对比:早期版本(还是 v12 那会儿)拿 Rapfi 最弱档测,棋谱存下来文件名直接就是诊断报告------黑棋无视白棋、看不见活三、活三不堵。那时候是真的连防守都不会。
10. 训练结果:白棋终于不崩了,但故事没这么简单
10.1 好消息:白棋不崩了
| 阶段 | 白胜率 | 状态 |
|---|---|---|
| it0-13 | 39-47% | 健康 |
| it14-21 | 32-39% | 轻微回落 |
| it35-44 | 56-58% | 反超(黑反而弱了) |
| it64-108 | 39-46% | 稳定 |
| it184-241 | 37-51% | 稳定 |
| 总结 | ~290 代没有崩过 | 家族史上第一个稳定版本 |
10.2 能力曲线:it160 是个真峰值
四轮 PK(每场 20 局、ring 规则、sim 800)打下来:
it0 (继承的起点)
→ it60 (十局赛 7:3 拿下)
→ it70 (13:7 大胜 it60)
→ it100 / 110 / 120 / 130 / 140 / 150 (对 it60 净胜 2~4 局)
→ it160 ★ 真峰值(循环赛 61.3%,3 胜 1 平)
→ it190 / it210 / it230 / it240 (链条上各有胜负)
20 局直接对战排序:it160 > it259 > it240。
10.3 不太好听的部分:内部对抗饱和了
it160 之后的约 100 代,在"自己人打自己人"这个口径下,没能产出比 it160 更强的通用棋力:
it160 vs it240 = 11:9 (it160 胜)
it160 vs it259 = 14:6 (it160 碾压)
it190 vs it160 = 12:8 (it190 胜) ← 非传递
三线证据都指向同一件事:D80 停滞(it165 → it282 全是 7 步)、value_fc1 有效秩持续走低、白胜率和做题分数平坦。
我的理解是:自对弈在"追一个移动靶"------网络不断适应自己最新的棋风,产出的更多是"横向的风格差异",而不是"纵向的绝对提升"。
10.4 但这一条才是最值钱的发现
上面那个"100 代没进步"的结论,被 Rapfi 推翻了。
同一批权重,内部对抗看不出增长,对固定标尺 Rapfi 却在实打实地变强(116 代从重大失误到稳赢 d10)。
📌 结论修正:"100 代无增益"应该理解成**"内部对抗饱和"**,而不是"训练无效"。
Rapfi 是固定标尺,内部对手是移动标尺。 只跟自己打,你会误判自己停了;有一个不动的参照物,才看得见真实的进步。
这条是我做这个项目最大的方法论收获,也是下一篇文章要重点展开的------整个 v1 到 v36 的配置,几乎每一行都是被类似的实验纠正过来的。
11. 踩坑速查表(建议收藏)
| # | 坑 | 一句话修复 |
|---|---|---|
| 1 | 搜索里把 log 概率当概率用 | 先验反转 → MCTS 退化纯 value → policy 饿死 99 代;所有 log_softmax 输出用前必须 exp |
| 2 | 每次模拟都重采样 Dirichlet 噪声 | 探索被放大 800 倍,树不稳定;噪声只在根节点注入一次,全部模拟共用 |
| 3 | while count.sum() < 800 判搜索量 |
局面被访问过时直接跳过 = 0 次搜索;改 for _ in range(800) 真跑满 |
| 4 | 局与局之间不重置搜索树 | 第 2 局开局就被上一局污染;每局开头必须 reset_mcts() |
| 5 | BN 参数跟着吃 weight_decay | γ 被压进死亡螺旋、深层通道反相;BN 参数单独分组,wd = 0 |
| 6 | value 标签用终局 ±1 | 输局里的好手也吃 -1;改成该步搜索 Q 软标签,密集监督 |
| 7 | 强权重 PK 用 greedy=True |
每局走出完全一样的棋,10:0 实际只有 1 局;强权重必须采样 |
| 8 | 10 局就下结论 | 噪声 ±1~2 局,实测 6 场里 1 场结论反转;至少 20 局 |
| 9 | 标准规则下测强权重棋力 | 执黑 100% 必胜,测的是抽签;必须开 ring 换先手 |
| 10 | 权重按文件名判"已存在" | 截断文件被永久跳过,还以为"已最新";必须校验大小 + zip 完整性 |
| 11 | 新目录继承种子只拷 weights.pt |
工兵等 metadata.json 死等、主进程等工兵,互相死锁;先写 metadata 再启主进程 |
| 12 | 8 方向增强用 fliplr 对 4D 张量 |
反射分支根本没生效,样本只放大 4 倍;改 np.flip(axis=-1) |
| 13 | 探针直接喂全零张量 | 得出"空盘 value +0.74 病态"的假结论,正确构造实际是 -0.001;探针必须走正规输入构造 |
| 14 | 只看 loss 判断训练好坏 | loss 创新低而棋力下降是常态;必须配合 PK 和做题集 |
| 15 | 静态指标平坦就说"训练无效" | 可能只是内部对抗饱和;换一个固定外部标尺再看 |
写在最后
这个项目从 v1 跑到 v36,中间经历过"白棋崩盘 → 换网络 → 换超参 → 换标签 → 全都不管用"的漫长阶段,一直到 v36 那条"前 3 手随机"的规则落地,才第一次看到白棋胜率稳稳停在 40% 出头不动。
回头看最有意思的一点:真正解决问题的不是更深的网络、更聪明的搜索、更精细的损失函数,而是一条改变"题目难度分布"的规则。前面三十多个版本都在调"模型侧"的旋钮,答案一直在"题目侧"。
如果你也在跑自对弈,希望这篇配置清单能帮你少走点弯路。
👍 如果这篇对你有用,点个赞让我知道
⭐ 配置细节建议收藏,后面第二篇会讲每个参数的血泪史
💬 评论区聊聊:你训自对弈遇到过最诡异的坑是什么?
🔗 **代码 & 全部对局记录在 Gitee:alpha zero gomoku
下一篇 :《AlphaZero 五子棋实战(二):每个参数都是血泪史------v1→v36 参数考古》
讲清楚 sim 为什么定 800、温度调度怎么来的、视觉通道为什么是 4 个、value 标签为什么从 ±1 换成 Q、开局规则怎么从"让手"一路试到"随机"------每一个配置项背后都是一串对照实验。