个人消费级 AlphaZero 五子棋实战总结:从零训练一个会下五子棋的 AI
摘要:本人在两张消费级 GPU(RTX 4090 + RTX 2080 Ti)上,从零训练出了一个 15×15 五子棋 AlphaZero AI。本文记录了完整实战过程:从框架选型、网络架构、分布式训练,到 14 个真实踩坑(每个都有症状+根因+修复)、一套可靠的评估方法论(loss 下降≠棋力上升),以及 v1→v2→v3 的对照实验设计与早期数据。全文 7000+ 字,全部结论均有实测数据支撑,无水分。
适合人群:对 AlphaZero / 强化学习 / 自对弈感兴趣,想自己动手训练棋类 AI 的开发者;被 loss 迷惑过、想知道"怎么判断模型真的变强了"的 RL 学习者。
你将收获:① 消费级硬件训 AlphaZero 的完整方案;② 14 个高频踩坑及修复;③ 一套"探针 + 激活率 + PK"三方互证的评估方法;④ 用对照实验归因"策略 vs 架构"的思考方式。
作者 : 个人实战项目时间 : 2026-08
关键词: AlphaZero、五子棋、强化学习、MCTS、PyTorch、自对弈、价值网络、探针检测
目录
- 项目背景与目标
- [环境与架构:消费级硬件怎么撑起 AlphaZero](#环境与架构:消费级硬件怎么撑起 AlphaZero)
- 训练策略:关键超参数的设计与取舍
- [性能优化:从每代 4 小时到 30 分钟](#性能优化:从每代 4 小时到 30 分钟)
- 踩坑实录:那些让我差点放弃的问题
- [评估方法论:loss 下降≠棋力上升](#评估方法论:loss 下降≠棋力上升)
- [版本演进:v1 → v2 → v3 对照实验](#版本演进:v1 → v2 → v3 对照实验)
- 总结与展望
一、项目背景与目标
1.1 为什么做这件事
AlphaZero(DeepMind, 2017)是深度强化学习领域最优雅的算法之一:不需要任何人类棋谱,只靠"自己跟自己下棋"就能从零学会一项棋类游戏。它用一个神经网络同时承担两个角色:
- Policy 网络:给定局面,输出每个落子位置的概率("我该下哪")
- Value 网络:给定局面,输出胜率估计("我现在是赢是输")
- MCTS 搜索:用网络指导的蒙特卡洛树搜索,在"直觉"和"深度推演"之间取得平衡
五子棋(15×15)是入门 AlphaZero 的最佳棋盘:规则简单(五连即胜)、动作空间适中(225 个落点)、单局时长短(几十步),非常适合个人开发者在一两张消费级 GPU 上完整跑通"从零训练到能下棋"的全流程。
1.2 项目目标
- 用个人可及的硬件(1 张 RTX 4090 云 GPU 或 1 张 RTX 2080 Ti)从零训练一个 15×15 五子棋 AI
- 完整走通 AlphaZero 训练闭环:自对弈 → MCTS → 训练 → 评估 → 迭代
- 建立一套可靠的评估方法(loss 不可信,要用探针 + PK + 激活率三方验证)
- 通过对照实验弄清楚:训练策略和网络架构,到底哪个对最终棋力贡献更大
1.3 关键规模数字
| 项目 | 数值 |
|---|---|
| 棋盘 | 15×15(225 动作空间) |
| 网络 | 128 通道 × 8 残差块 |
| 参数量 | 2.37M(简化版)/ 2.8M(论文版) |
| 每代对局 | 100 局(主进程 10 + 9 工兵 90) |
| 训练目标 | 100 代 |
| 单代耗时 | 25-30 分钟(4090) 或 45-50 分钟(2080Ti) |
| 全程 | v2 约 2 天(100 代,每代100局,MCTS-400模拟) |
二、环境与架构:消费级硬件怎么撑起 AlphaZero
2.1 硬件与框架选型
| 老训练机 | 新测试机 | |
|---|---|---|
| GPU | RTX 4090 24GB | RTX 2080 Ti 22GB |
| 算力 | ~82.6 TFLOPS (FP32) | ~13.4 TFLOPS (FP32) |
| 相对算力 | 1.0 | 约 1/6 |
| PyTorch | 2.13 + CUDA 12 | 2.12.1 + cu130 |
| 角色 | v2 主线训练 | v3 对照实验 |
框架选择:PyTorch 而非 TensorFlow。 这是一个重要决策------TensorFlow 2.12 只为 sm_80(Ampere)编译了 CUDA kernel,在 RTX 4090(sm_89, Ada Lovelace)上回退到通用 kernel,完全用不上 Tensor Core ,单次前向 ~20ms;而 PyTorch 原生支持 sm_89,前向仅 ~1.9ms,快约 10 倍。这个坑的详细分析见 5.1 节。
2.2 网络架构
主干是标准的 ResNet 风格:Conv3×3 → BN → ReLU 后接 8 个残差块(每个含 2 层 3×3 卷积),128 通道。3×3 卷积堆叠 8 层的感受野是 17×17,恰好覆盖整个 15×15 棋盘,网络可以"看到"全局。

Policy/Value 头有两种设计,这是本项目后期对照实验的核心变量:
| 组件 | 简化版(v1/v3) | 论文版(v2) |
|---|---|---|
| Policy 头 | conv3×3 → 1ch → softmax 出概率 | 1×1conv → 4ch → BN+ReLU → Linear(900→225) → log_softmax |
| Value 头 | conv3×3 → 1ch → Linear(225→1) 单层 | 1×1conv → 2ch → BN+ReLU → Linear(450→128) → Linear(128→1) 双层 |
| 参数量 | 2.37M | ~2.8M |
论文版的设计思想(AlphaZero 原文):1×1 conv 做逐位置特征提取(局部感知),FC 层做全局整合(全局决策) ;value 头用 128 维隐藏层表达非线性组合特征。简化版则是"单通道 + 单层线性",表达力弱一档------但这也正是我们做对照实验的意义:在同样策略下,架构差异对棋力的实际影响到底有多大?(见第七章)
2.3 分布式训练架构(1 主 + 9 工兵)
AlphaZero 训练的瓶颈不是 GPU 推理,而是 MCTS 自对弈 (单线程 CPU 循环)。单进程 100 局/代需要 4-5 小时,完全不现实。解决方案是分布式:

┌─────────────────────────────────────────────────────┐
│ 主进程 (Gomoku-v0_pt.py) │
│ ├── 自对弈 10 局 (MCTS) │
│ ├── 等待工兵: X/90局已到 (轮询 worker_data/) │
│ ├── 收集齐 100 局 → agent.learn() 训练 │
│ ├── 存档 weights_it{N}.pt + 体检 health_check() │
│ └── 杀旧工兵 → 重启 9 个新工兵 (下一轮) │
└─────────────────────────────────────────────────────┘
▲ 文件 IPC (worker_data/iter{N}_{wid}_game_{M}.pkl)
│
┌───────┴─────────────────────────────────────────────┐
│ 工兵 × 9 (Gomoku-worker.py) │
│ 每个: 加载最新权重 → 自对弈 10 局 → 逐局写 pkl → 退出 │
└─────────────────────────────────────────────────────┘
关键设计决策:
- 文件即 IPC :工兵逐局保存
iter{N}_{wid}_game_{M}.pkl,crash-safe(训练崩溃不丢已产数据) - 主进程训练前杀工兵、训完重启:避免 9 工兵 + 主进程同时训练导致显存 OOM
- 9 工兵并行:GPU 推理时间占比小,每个工兵分到算力即可,总量吞吐优先
三、训练策略:关键超参数的设计与取舍
AlphaZero 算法的"配方"里有几个关键超参数,直接决定训练数据的质量和多样性。这一节讲我们踩过的坑和最终的取舍。
3.1 MCTS 模拟量:固定 400 还是动态调度?
v1 的做法(动态调度) :代1-19: sim=50 → 代20-49: sim=200 → 代50-70: sim=400 → 代71+: sim=800。
理由看起来合理:早期网络弱,深搜浪费;后期网络强,需要深搜。但实际引入了问题------每次切换模拟量,数据分布就漂移一次,value 网络需要 3-5 代重新适应(value_loss 跳升),且日志里 50/200/400/800 混杂,不好追踪。
v2 的做法(固定 400):最终拍板固定 400 全程。理由:
- 追踪每代体检曲线更干净,不做模拟量对照的混杂变量
- 400 是质量/速度的甜点区(v1 阶段 200 的棋质量不够、800 局数受限)
- 数据量受速度限制------模拟量减半 → 同时间数据量翻倍,但质量下降
模拟量权衡实测:50 模拟 ≈ MCTS 只是 policy 直觉 (数据多样性高但质量差);200 = 质量/速度甜点 ;400 = 质量高但局数受限 ;800 ≈ 67min/代(100 局)。
3.2 Dirichlet 噪声 α:从 1/N 到 0.03
AlphaZero 在根节点加 Dirichlet 噪声促进探索。α 控制噪声的"尖锐度"------α 越小,噪声越集中到少数几个位置(探索更激进)。
v1 的坑 :用 α = 1/合法落子数 ≈ 0.0045(15×15 空盘 225 个合法点)。这比标准值 0.03 小 6 倍多,导致:
- 开局乱下边角 :实测 v1 后期(代87-96)黑第一手边角占比高达 50.5% ,天元仅 1.5%
- policy 没学会"占中"棋理:第一手分布近均匀,定式重复率仅 5%(无任何开局偏好)
v2 的修复 :固定 α = 0.03(AlphaZero 国际象棋标准值),噪声温和地集中在 ~10-15 个位置,既保证探索又贴近可信区域。
3.3 Temperature 调度
v1 :全程按概率采样(np.random.choice),无温度调度------中期棋力起来后仍然乱采样,数据质量上不去。
v2:标准 AlphaZero 式温度退火:
前 15 手: T=1.0 (均匀采样,保证开局多样性)
中段: T 线性降到 0.25
最后 10 手: T=0.05 (近乎贪心,保证终局质量)
3.4 回放池:最近 5 代混合
v1:只训当前代 100 局------每代自对弈分布都在变,模型容易过拟合"这一代"的棋风。
v2 :保留最近 5 代数据混合训练。效果显著:v2 代12 就打出黑活四转正 + 冲四连续转正,价值判断的稳定性明显好于 v1 同期。
3.5 每代自动健康体检
v2 起每训练完一代,自动跑一次 health_check() 写入 health_log.json,无需手动拉权重:
- 7 局面价值盲测:黑/白活四、黑/白活三、黑/白冲四、空盘(探针设计原则见 6.2 节)
- 4 层激活率:res3-res5 的 BN 输出 >0 比例
- BN 死神经元计数:γ 参数 <0.01 的数量
这套"每代自动体检"是后续所有评估工作的基础------不用等 100 代训完,每一代都能看到价值判断的健康状况。
四、性能优化:从每代 4 小时到 30 分钟
这一节是纯工程干货。AlphaZero 训练的瓶颈链条:环境交互 → 棋盘编码 → MCTS 树遍历 → GPU 推理。每一环都有优化空间。
4.1 boardgame2 环境优化(端到端 ~3-5×)
boardgame2 是项目使用的 gym 环境库,原始实现有两个致命性能问题:
| 优化 | 优化前 | 优化后 | 提速 |
|---|---|---|---|
copy.deepcopy(board) → board.copy() |
深拷贝整个对象图 | numpy 浅拷贝 | ~100× per copy |
get_winner 全盘扫描 → 只查落子点附近 |
遍历全部 1800 个起始点 | 4 方向检查 ~32 步 | ~280× per call |
get_winner 全盘扫描是隐藏杀手:15×15 空盘一次调用要 5.8ms ,一局 150 手就是 ~870ms。100 局/代 × 100 代 = 24 小时浪费在胜负判断上。改为只检查最后一手落子点周围 4 个方向后,开销降到可忽略。
4.2 MCTS Python 层优化(+33~64%)
当 GPU 推理已经极快(~2.5μs/次)时,真正的瓶颈在 Python 层。用 profiler 定位到两个巨慢组件:
| 组件 | 优化前 | 优化后 | 提速 |
|---|---|---|---|
strfboard() 棋盘→字符串 key |
54.9μs | 0.1μs (board.tobytes()) |
402× |
get_valid() 双重循环 |
77.7μs | 1.3μs (向量化) | 58× |
strfboard把 15×15 棋盘编码成字符串做 MCTS 字典 key,每步调用 ~800 次(800 sim);改用board.tobytes()(225 字节二进制,可直接 hash)后近乎免费get_valid双层 for 循环遍历 225 格判断空位,改用(board == 0).astype(np.float32)向量化
实测加速(RTX 2080 Ti, 2.4M 模型):
| 配置 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| MCTS(50) | 49ms | 33ms | +33% |
| MCTS(200) | 130ms | 83ms | +36% |
| MCTS(800) | ~520ms | 187ms | +64% |
4.3 torch.compile + FP16 + GradScaler(~3.4×)
| 优化 | 预期加速 |
|---|---|
torch.compile(mode='max-autotune') |
1.3-2× |
| FP16 autocast 推理 | 1.5-2× |
| TF32 + cudnn.benchmark + channels_last | ~1.2× |
实测(RTX 4090, sim=400):优化版首局 595s → 173s (约 3.4×),每代从 ~100 分钟降到 ~30 分钟。
关键工程决策 :主进程开 torch.compile(训练需要),工兵全部 disable_compile=True------因为 9 个工兵同时编译会互相抢 GPU 导致死锁(见 5.5 节)。
4.4 性能真相:重启 > 代码优化(重要教训)
这是本项目最反直觉的发现。某次"优化后 3 倍加速",拆解归因:
| 因素 | 贡献 |
|---|---|
| 杀死运行 27h 的残留进程(含 compile_worker 空转一天) | ~50% |
| 清除 15.7GB GPU 显存碎片化 | ~30% |
| strfboard→tobytes + get_valid 向量化 | ~20% |
教训 :训练连续运行超 24h 后,进程内存碎片化、GPU 显存碎片、PyTorch 编译缓存膨胀会持续拖慢速度。每 10 代重启一次能恢复大部分性能损失。但不要盲目重启------"等到真的变慢了再说"。
五、踩坑实录:那些让我差点放弃的问题
这一节是全文最值钱的部分------14 个真实踩坑,每个都有症状、根因、修复。很多坑不亲历根本想不到。
5.1 TensorFlow 在 RTX 4090 上慢 10 倍
症状:128×8 残差网络前向推理 ~20ms/次,GPU 利用率极低,训练慢到无法接受。
根因 :TF 2.12 只为 CUDA 11.8 + sm_80(Ampere)编译了 kernel。RTX 4090 是 sm_89(Ada Lovelace),TF 回退到通用 kernel,Tensor Core 完全没用上。PyTorch 2.13 原生支持 sm_89,前向仅 ~1.9ms。
修复 :全面迁移 PyTorch。tf.sysconfig.get_build_info() 检查 cuda_compute_capabilities 即可确认。
5.2 batches 太低导致 96% 数据浪费
症状:policy_loss 几乎不动(5.40→5.33),value_loss 卡在 1.0,学习阶段 0-1 秒完成。
根因 :默认配置 batches=2, batch_size=128 → 每代只训练 256 条经验,而每代生成 ~6800 条(100 局 × ~68 步),利用率仅 3.8%。GPU 99% 时间在闲置。
修复 :batches = 6800/128 ≈ 50,让 batches × batch_size ≈ 每代样本量 × 90%。改后学习阶段 10-30 秒,value_loss 5-10 代内开始下降。
连带坑 :改 batches 会改变 config_hash,导致已有 30 代检查点全部失效从头训练!正确做法是把 batches/batch_size 从 config_hash 计算中移除(它们不影响网络结构)。
5.3 torch.compile + CUDA Graph 死锁
症状:进程无限挂起,CPU 100%、GPU 0%、无任何报错日志。
根因 :torch.compile(mode='max-autotune') 生成动态执行路径(按输入数据选 kernel),而 CUDA Graph 的 replay() 冻结了 capture 时的 kernel 配置。两者组合时,新数据触发编译函数调度到不同 kernel path,与冻结的 graph 冲突 → GPU 死锁。
修复 :二选一------只用 torch.compile(推荐),或只用 CUDA Graph。删掉 CUDAGraphCache 类即可。
5.4 FP16 缺 GradScaler,梯度下溢
症状:训练不收敛,policy softmax 输出的极小概率(~1e-5)在小棋盘上梯度消失。
根因:用了 FP16 autocast 但没配 GradScaler。policy 交叉熵在 15×15 上有 ~1e-5 量级的小值,FP16 下梯度直接下溢为零,部分参数永不更新。TF32 只保护 matmul/conv,不救 softmax/log。
修复:
python
self.scaler = torch.cuda.amp.GradScaler()
...
self.scaler.scale(total_loss).backward()
self.scaler.step(self.optimizer)
self.scaler.update()
同时 log(pred_p + 1e-8) 改为 pred_p.clamp(min=1e-7),精度提升 10×(1e-8 在 FP16 下接近最小正规数)。
5.5 9 工兵并行 torch.compile 编译死锁
症状:9 个工兵日志全部 0 字节,GPU <5%,重启后训练停滞 30 分钟以上。
根因 :每个工兵启动都做 torch.compile(mode='max-autotune'),各自穷举 kernel 配置(8-15 分钟),9 个进程互相抢 GPU → 全部超时。
修复 :Agent 加 disable_compile 参数,工兵推理不需要编译(编译只加速 ~20-30%,且工兵有 9 个并行实例,单实例速度无关紧要):
python
# 主进程(需要训练速度)
agent = AlphaZeroAgent(..., disable_compile=False)
# 工兵(只做推理,立即启动)
agent = AlphaZeroAgent(..., disable_compile=True)
5.6 search() vs decide() 陷阱:MCTS 没跑满模拟量
症状:MCTS(50) 和 MCTS(200) 速度一样,PK 结果全部失效。
根因 :agent.search(board) 只做 1 次 递归遍历(到叶子→推理→回传),不是 sim_count 次循环。只有 agent.decide(observation) 会 while count < sim_count: search()。
自查 :MCTS(N) 和 MCTS(2N) 速度应差约 2 倍。如果一样,就是误用了 search()。后果:所有棋力对比用的实际是 1 次搜索而不是 N 次。
5.7 日志被 > 覆盖,历史丢失
症状 :重启训练后,training.log 只剩几行,代0-11 的全部过程日志消失。
根因 :手动重启命令写成了 > training.log(覆盖)而非 >> training.log(追加)。
修复 :所有日志重定向一律 >>。损失数据(loss/体检/权重)还在 metadata.json 和 health_log.json 里,但过程日志永远找不回了。这个教训后来写进了所有部署文档。
5.8 工兵重启衔接 bug:整代跳过
症状:重启后工兵不补产缺失局,主进程永远卡在"等待工兵: 89/90 局已到"。
根因 :工兵启动时 last_iteration = max_seen(扫描到自己产出的最大代),于是判断"当前代已完成"整代跳过。但重启前可能只产了 19/90 局。
修复 :last_iteration = max_seen - 1,让工兵重新进入当前代,靠"局级跳过已存在文件"补产缺失局。这个 bug 让主进程等了 40 分钟才发现。
5.9 假赢调查:90/90 局全误报
症状:审计训练数据时发现"90/90 局无五连却判胜",怀疑环境判胜 bug。
根因 :trajectory 里每条记录的 board 是落子前 局面(成五 → done → break,append 的是落子前棋盘)。直接拿 boards[-1] 当终局棋盘检查五连,必然找不到五连。
修复:在最后一行 board 上找胜方"差一手成五"的空位 = 最后一手,补上再数连子。修正后 100% 真五连赢、0 假赢、0 平局。
连带洞察 :训练数据里"已五连"棋盘状态数 = 0(成五即终局),所以 value 探针绝不能测终局局面(OOD)------这是探针设计的重要原则(见 6.2 节)。
5.10 config_hash 失效:30 代训练白费
症状 :修改代码后重启,训练从 0 开始,weights.pt 被随机权重覆盖。
根因 :config_hash 是训练参数的 SHA256。改任何参数(含 batches/batch_size)后 hash 变化,load_checkpoint() 静默返回 None,训练从随机权重重开。
修复 :① 从 hash 函数移除不影响网络结构的字段(batches/batch_size);② 灾难恢复:用 weights_it{N}.pt 覆盖 weights.pt 并验证 md5,更新 metadata 的 config_hash,把 iteration 设回上一代。
5.11 神经元死亡:激活率断崖(代52→53)
症状:中层 BN 激活率从 74%(代51 巅峰)断崖式跌到 52%(代53),棋力随之下降。
根因 :代50 切 MCTS 200→400 + 每代仅 10 局 → 数据多样性不足,中层神经元成批死亡(res4_bn2 从 80%→51%)。对比:代71 切 400→800 但有 90 局/代,无死亡。
结论 :模拟量翻倍不是死因,数据多样性才是。激活率 <50% = 神经元坏死,此时 loss 更低但实战更弱。
5.12 CUDAGraph 2080Ti 专属坑
症状 :RuntimeError: accessing tensor output of CUDAGraphs that has been overwritten by a subsequent run------4090 不触发,2080Ti 必现。
根因 :learn() 里用 torch.compile 后的 self.net 连续多次 forward,CUDAGraph 输出张量被覆盖。与 5.3 不同,这是编译网络的 CUDAGraph 内存复用问题。
修复 :learn() 里改用 self.raw_net(未编译网络)。优化器绑定 self.net 的参数,但 compile 与 raw_net 共享底层张量,backward 安全。
5.13 纯网络评估在 15×15 上不可靠
症状:纯网络 argmax 评估 100 局 100% 平局,价值网络看起来完全没学会。
根因 :15×15 上 policy 输出近均匀分布(top1 ≈ 0.7% vs 均匀 0.44%),argmax 选中哪个位置 ≈ 随机。纯网络评估只适用于小棋盘(井字棋 3×3、黑白棋 4×4)。
修复:评估必须用 MCTS(哪怕 10-20 sim),搜索放大微弱的 policy 差异。
5.14 模拟量不匹配导致排名反转
症状:400/800 模拟训练的模型,用 MCTS(50) 评估反而输给早期模型(代69 输给代31/41)。
根因 :高模拟量训练的 value 特征需要深搜才能发挥------在 50 模拟下,精细的 value 预测还没传播开就结束搜索,成了噪声。更好网络 + 不足搜索 < 较差网络 + 足够搜索。
修复:评估模拟量 ≥ 训练模拟量。规则:训练 400 就至少用 400 评估;低模拟量排名与训练时序矛盾时,信高模拟量结果。
踩坑总纲 :这 14 个坑可以归成三类------环境兼容 (5.1/5.3/5.4/5.5/5.12)、逻辑 bug (5.2/5.6/5.7/5.8/5.9/5.10)、方法论陷阱(5.11/5.13/5.14)。前两类让训练"跑不起来或跑错",第三类让结果"看起来对其实错"------第三类最危险。
六、评估方法论:loss 下降≠棋力上升
这是整个项目最深刻的教训,单独成章。如果不读这一节,前面的训练都白做。
6.1 核心教训:loss 是自证预言
实测铁证 :v1 训练到 91/92 代时,value_loss 降到 0.41 新低 (看起来完美),但实战 PK 排序是 85代 > 51代 > 91代------91 代的 loss 比 85 代还低,棋力却垫底。
为什么? loss 是在自对弈生成的数据分布 上测的。随着训练进行,模型棋风固化,自对弈双方越来越像,输赢越来越接近随机------loss 自然下降,但这是"自我印证"的下降,不代表棋力提升。数据里已经学不到新东西了,loss 只是在一遍遍确认旧认知。
所以报告训练状态时,loss 趋势必须搭配盲测或 PK 验证,否则会给出错误的好消息。 这是本项目方法论的核心。
6.2 探针检测(价值盲测):每代的"健康体检"
既然 loss 不可信,我们就给网络做"体检"------构造几个确定性局面(谁胜谁负是明确的),看 value 网络预测的方向对不对。
7 局面探针(全部用训练中真实出现的中间局面,非终局):
| 探针 | 期望 value | 含义 |
|---|---|---|
| 黑活四 | +1.0 | 黑四连两端开放,必胜 |
| 白活四 | -1.0 | 白四连,黑必败 |
| 黑活三 | +0.6 | 黑三连两端开放,优势 |
| 白活三 | -0.6 | 白三连,黑劣势 |
| 黑冲四 | +0.8 | 黑四连一端被堵,威胁 |
| 白冲四 | -0.8 | 白四连一端被堵 |
| 空盘 | ~0 | 开局均衡 |
判读规则 :符号方向正确 + 幅度合理;空盘要求 |value| < 0.3。
探针设计原则(重要) :只用训练数据里真实出现的状态 。一开始我们用了"黑五连/白五连"当探针,结果 value 随代恶化(-0.35→-0.78)------后来才发现是探针无效而非网络退化:训练数据里"已成五连"的状态数 = 0(成五即终局,trajectory 记录的是落子前棋盘),五连局面是分布外数据(OOD),网络从未见过。
活四 vs 冲四的学习顺序(一个有趣的观察) :网络先学会冲四、后学会活四 。因为训练数据里活四是"瞬时状态"(一形成对手立刻堵一头变冲四),出现频率低;冲四是"稳定状态"(被堵的四连),出现频率高。所以早期活四探针判断反/震荡是正常的,要看冲四/活三是否转正作为早期信号。
v2 实测(探针逐代转正的漂亮轨迹):
| 代 | 黑冲四 | 白冲四 | 黑活四 | 备注 |
|---|---|---|---|---|
| 12 | +0.32 ✅ | -0.53 ✅ | -0.32 ❌ | 冲四先转正 |
| 13 | +0.46 ✅ | -0.51 ✅ | -0.34 ❌ | 冲四持续强化 |
| 14 | +0.85 ✅ | +0.25 ✅ | +0.40 ✅ | 爆发:三探针同时转正 |
| 15 | +0.79 ✅ | -0.25 ✅ | -0.49 ❌ | 波动但冲四稳定 |
v2 代14 黑活四转正------14 代就达到 v1 巅峰期(85 代)都未必有的探针强度,策略优化的效果非常显著。
6.3 激活率诊断:比 loss 更靠谱的健康指标
中层 BN 激活率(res3-5 的 ReLU 输出 >0 比例)是比 loss 更优的棋力预测指标。
- 代51(激活率 69%)在 PK 中 6:4 战胜代75(激活率 48%),尽管代75 的 value_loss 更低(0.473 vs 0.580)
- 激活率 < 50% = 神经元成批坏死(见 5.11),此时 loss 越低实战越弱
- 激活率 43-47% 是 v2 的健康稳态,0 死神经元
6.4 PK 实战验证:终极裁判
探针和激活率是"间接指标",PK 对弈才是终极裁判。但 PK 有严格的方法论:
- 分先赛制:先手 3 局 + 后手 3 局,控制先手优势
- 模拟量 ≥ 训练模拟量:训练 400 就用 400 评估(见 5.14 的反转陷阱)
- 控制变量:双方同 MCTS、同 greedy、同 noise
- 多局统计:3 局噪声极大(实测 85代 3-0 赢 51 代,但 91 代同设置 0-3 输),≥10 局才有意义
- 每局前 reset_mcts():防止残树污染
v2 早期 PK 结果(sim=400,分先 4 局):
| 对手 | 比分 | 解读 |
|---|---|---|
| v1 代85(巅峰) | 1-3 | 输但赢的局 45 步(稳) |
| v1 代51 | 2-2 | 打平 |
| v1 代94(末期) | 2-2 | 打平 |
才 12 代就打平了 v1 训练 100 代的 51/94 代,成长曲线健康。v2 赢的局偏长(稳扎稳打),v1 赢的局偏短(快攻连五)。
6.5 三方验证体系
最终我们建立了一套**"三方互证"评估体系**,任何结论必须至少两方支持:
① Loss 趋势 ------ 快但不可信(自证预言)
② 探针 + 激活率 ------ 快、可信、每代自动跑(体检)
③ PK 实战 ------ 慢但终极可信(金标准)

诊断路径:loss 降但 PK 输 → 先跑探针(看是否全反)→ 再看激活率(是否坏死)。三步定位问题根源。
七、版本演进:v1 → v2 → v3 对照实验
前面所有铺垫,都是为了这一章------用对照实验回答一个核心问题:训练策略和网络架构,到底哪个对棋力贡献更大?
7.1 v1 的失败与诊断
v1 是第一个完整跑通 100 代训练的版本,但它暴露出一连串问题:
| 问题 | 证据 |
|---|---|
| 开局乱下 | 黑第一手边角 50.5%、天元仅 1.5%、定式重复率 5% |
| value 头退化 | 89 代短暂学会后,91/92 代盲测全反(且 -0.9 级信心满满地反) |
| 神经元死亡 | 代52→53 激活率断崖(74%→52%) |
| loss 与棋力背离 | 91 代 loss 新低但 PK 垫底 |
| 实战防守失灵 | 代94 与人实战,对方中心连下 1-3-5-7-9 五连全程不拦截 |
根因诊断(逐条排查后):
- 无 temperature 调度------全程概率采样,中期棋力起来后仍乱采样,数据质量上不去
- Dirichlet α 过小(1/N ≈ 0.0045 vs 标准 0.03)------探索过强,开局乱下边角,policy 学不会占中
- 每代仅 10 局(v1 前期)------数据多样性不足,神经元死亡
7.2 v2:策略全面修复
v2 保留 v1 的网络结构思路,但把训练策略全面升级到 AlphaZero 标准:
| 策略 | v1 | v2 |
|---|---|---|
| MCTS 模拟量 | 动态 50→200→400→800 | 固定 400 |
| Dirichlet α | 1/N ≈ 0.0045 | 0.03 |
| Temperature | 无(全程采样) | 前15手 T=1 → 线性 0.25 → 末10手 0.05 |
| 回放池 | 只训当前代 | 最近 5 代混合 |
| 每代局数 | 10(前期) | 100(主10+工兵90) |
| 自动体检 | 无 | 每代 health_check |
效果立竿见影:
- 探针检测:代12 冲四转正,代14 三探针同时转正(见 6.2 表)------14 代达到 v1 巅峰期水平
- PK:代12 打平 v1 代51/94(2-2),才 12 代
- 激活率:稳定 43-47%,0 死神经元
7.3 一个意外的发现:网络结构真相
v2 用的其实是论文版网络架构(policy 1×1conv→4ch→Linear(900→225)、value 双层 fc),而 v1 是简化版(conv3x3→1ch 直连 softmax、value 单层 fc)。这个差异是在做 PK 时发现的:v2 权重 120 个 key vs v1 的 116 个 key,无法互相加载。
这带来了一个混杂变量:v2 的进步到底是"策略优化"带来的,还是"架构升级"带来的?于是设计了 v3 对照实验。
7.4 v3:解耦"策略 vs 架构"
对照实验设计(控制变量法):
v2 = 新架构(论文版)+ 新策略 ← 老机器 RTX 4090
v3 = 老架构(简化版)+ 新策略 ← 新机器 RTX 2080 Ti
↑ 策略相同,架构不同
- 如果 v3 探针也转正 → 策略优化是主因(架构贡献小)
- 如果 v3 探针平庸而 v2 强 → 架构是主因
v3 的部署:复制 v2 训练脚本,把网络换回 v1 简化版结构(2.37M 参数),其余策略参数完全不动。跑在 RTX 2080 Ti 新机器上(算力约为 4090 的 1/6,每代 ~45-50 分钟)。
v3 遇到的新坑 (2080Ti 专属):torch.compile 触发 CUDAGraph 输出覆盖错误(4090 不触发),修复为 learn() 用 raw_net(见 5.12)。
7.5 初步结果:v1/v2/v3 早期(代1-4)探针对比
v3 跑到代1、代3、代4 时分别拉权重与 v2 同代对比;同时补拉了 v1 的代3/4 权重(v1 与 v3 同为简化版网络,可直接对比)。三个版本都用完全一致的 7 局面探针,各自网络结构加载权重。
代1(v3 vs v2):
| 探针 | 期望 | v3代1 | v2代1 |
|---|---|---|---|
| 白活四 | -1.0 | -0.327 ✅ | +0.298 ❌ |
| 白活三 | -0.6 | -0.310 ✅ | -0.199 ✅ |
| 白冲四 | -0.8 | -0.324 ✅ | +0.089 ❌ |
| 通过 | 3/6 | 1/6 |
代3/代4(三方):
| 代 | v1 通过 | v2 通过 | v3 通过 | v1 激活率 | v2 激活率 | v3 激活率 |
|---|---|---|---|---|---|---|
| 3 | 3/6 | 2/6 | 3/6 | 49.9% | 40.0% | 47.7% |
| 4 | 3/6 | 0/6 | 3/6 | 53.0% | 43.0% | 47.7% |
观察:
- v1 和 v3(同为简化版网络)初代探针模式几乎一样:白方探针(白活四/白活三/白冲四)全对、黑方全错------同架构同代 → 学习轨迹相似
- v2(论文版)初代震荡更厉害:代3 通过 2/6,代4 全错 0/6(白方探针全部翻反)------论文架构参数更多、随机初始化影响更大,早期价值头更不稳定
- v1 的 value 输出幅度极小(-0.04~-0.13) ,接近初始化小值、符号只是碰巧对;v3 幅度明显更大(-0.25~-0.34),说明 v3 在代3 已形成"较确定的判断"(虽然黑方方向还错)
⚠️ 保守结论(重要):
- 代1-4 阶段:v1 ≈ v3 > v2------早期差异主要来自网络结构 (简化版初代更稳),策略优化的红利要等几代才显现(v2 到代12-15 才爆发转正)
- 单代/双代对比噪声大 ,两边自对弈数据完全独立,不能据此断言架构优劣
- 这是"进行中的实验"而非定论:v2 的价值震荡是否会收敛、v3 的黑方探针能否像 v2 一样转正,要等 v3 跑到 5-10 代再做一次对比才能看清
- 最初预期"策略优化是主因"------早期数据并不完全支持,但策略红利可能滞后显现;最终结论待 v3 中期数据
7.6 最终结论:v3 跑到代10,三时间点 PK 定案(2026-08-01)
v3 按计划跑到代10 收尾(新机停训),期间在代5/代8/代10 三个时间点与 v2 同代 PK(sim=400、分先10局、GPU 实战):
| 代 | PK 结果 | v3 胜率 | 性质 |
|---|---|---|---|
| 代5 | 0-4 | 0% | 爆杀 |
| 代8 | 2-8 | 20% | 爆杀(7局速杀) |
| 代10 | 1-9 | 10% | 爆杀(8局≤19步) |
代10 探针(同场验证) :v3 黑活四 -0.31 ≈ 白活四 -0.34(镜像相等,颜色失明依旧);空盘 -0.24(悲观偏置依旧)。v2 空盘 +0.04、黑活四 +0.13(方向转对,幅度待涨)。v3 跑了 10 代,value 头依然"看不懂"局面。
最终定论:
- "策略优化是主因"------被正式否决。v2/v3 共享完全相同的策略(固定 400 模拟、温度退火、α=0.03、回放池5代、100局/代),唯一变量是网络架构,结果棋力差出 0-4 / 2-8 / 1-9。
- 架构差异(value 头单层 fc vs 双层 fc)是价值判断能力的主要决定因素。v3 简化版 value 头 10 代训练未突破表达力天花板:颜色失明、悲观偏置、PK 全时段被速杀。
- v3 的"追赶"是假象:代8 的 2 胜全靠长盘苟局(35-43步,不犯错磨赢),从不主动杀;代10 回归 10% 胜率,且唯一胜局又是 35 步长盘。
- v2 的价值头在代14 前后转正 (黑方探针翻正、value_loss 代28 到 0.09),论文版双层 fc 的表达力红利随训练逐步兑现;简化版单层 fc 则从代1 到代10 纹丝不动------这是架构决定论最干净的证据。
一句话总结 :策略优化让"能学会"的架构学得更快更好,但救不了"学不会"的架构。想提升棋力,先确认你的网络结构有足够的表达力,再谈调参。
八、总结与展望
8.1 这一路最大的几个体会
1. 一张 4090 就够训出能下棋的 AI,真的不用 5000 块 TPU
128 通道 × 8 残差块(2.4M 参数),每代 25-50 分钟,两张卡 2 天跑完 100 代。DeepMind 的配置是给围棋准备的,五子棋这种小棋盘,个人玩家完全玩得转。
2. 策略和架构都重要,但架构决定天花板
同样的网络,把模拟量固定、加上 temperature 退火、Dirichlet α 调到标准值、回放池留 5 代------v2 用 14 代就追上了 v1 练 100 代的水平。但 v3 对照实验证明:这套策略救不了简化版 value 头 (单层 fc)------同样跑 10 代,v3 被 v2 打出 1-9,value 头依旧"看不懂"局面。策略优化让"能学会"的架构学得更快更好,架构表达力不够时策略只是锦上添花。
3. loss 降了别高兴太早
这是血泪教训。v1 的 91 代 loss 全场最低,实战却是三个代里最菜的。loss 低 ≠ 棋力强,它只是"模型对自己的棋风越学越熟"。真要判断强弱,探针、激活率、PK 对弈,至少两个对上了再下结论。
4. 半夜 3 点训练挂了,救你的不是算法是工程
日志覆盖、工兵不重启、重启后整代跳过......每一个"小问题"都能让 2 天的训练白跑。日志一律追加、脚本能断点续跑、改完代码记得 git------这些"无聊"的事,才是长时训练的保命符。
5. 想搞清楚"为什么有效",就做对照实验
v1 进步了,但说不清是策略的功劳还是架构的功劳,于是搞了 v3 对照实验(同策略、不同架构)。想归因就控制变量,别拍脑袋。
8.2 踩坑清单速查表(建议收藏)
| # | 坑 | 一句话修复 |
|---|---|---|
| 1 | TF 在 4090 慢 10× | 换 PyTorch(原生支持 Ada Lovelace) |
| 2 | batches 太低 | batches × batch_size ≈ 每代样本量 × 90% |
| 3 | torch.compile + CUDA Graph 死锁 | 只用编译或只用 Graph,二选一 |
| 4 | FP16 缺 GradScaler | 加 scaler;log 用 clamp(min=1e-7) |
| 5 | 9 工兵并行编译死锁 | 工兵 disable_compile=True |
| 6 | search() vs decide() | 评估必须用 decide();速度差自查 |
| 7 | 日志被覆盖 | 一律 >> 追加 |
| 8 | 工兵重启整代跳过 | last_iteration = max_seen - 1 |
| 9 | 假赢误报 | trajectory 是落子前棋盘,补最后一手再验证 |
| 10 | config_hash 失效 | batches/batch_size 移出 hash;恢复用 weights_it{N}.pt |
| 11 | 神经元死亡 | 数据多样性 > 模拟量;激活率 <50% 警告 |
| 12 | CUDAGraph 2080Ti 坑 | learn() 用 raw_net |
| 13 | 纯网络评估不可靠 | 15×15 必须用 MCTS |
| 14 | 模拟量不匹配排名反转 | 评估 sim ≥ 训练 sim |
8.3 接下来想搞的事
- v3 跑到 3-5 代,三方拉出来对比 ------ 已完成(2026-08-01):v3 跑满代10,三时间点 PK(0-4 / 2-8 / 1-9)正式定案:架构(value 头深度)是主因。详见 7.6 节
- 试试更大的网络(256 通道 × 19 残差块,~10M 参数)------4090 应该带得动,看能不能再涨一截棋力
- 上 challenger-vs-best 淘汰机制------像 Leela Zero 那样,新代打赢旧代才上位,从根上防神经元死亡
- 部署到 Web 对战------Flask 应用已经写好了,手机浏览器就能跟 AI 下,欢迎来虐
- 换游戏接着玩------同一套代码(boardgame2)改个环境就能训黑白棋、井字棋,想试试
8.4 代码与资源
项目脚本在 Gitee:kid141212/alpha-zero-gomoku,主要文件:
Gomoku-v0_pt_v2.py/Gomoku-worker_v2.py--- v2 主线训练Gomoku-v0_pt_v3.py/Gomoku-worker_v3.py--- v3 对照实验pk_v2_12_vs_v1.py/compare_v2_v3_itN.py/pk_v2_v3_it5.py--- PK 与探针对比脚本(支持任意代)verify_v3.py/net_analyze.py/value_test_real.py--- 体检分析工具
全文完。从一张 4090 开始,到一个会下五子棋的 AI,中间隔着的不是算力,是 14 个坑和一套可靠的评估方法。
💬 写在最后
这篇文章能写出来,靠的是一个星期里一局一局跑出来的真实数据,没有一个结论是拍脑袋的。
写到这里,想多说几句心里话。
2017 年 AlphaZero 横空出世那天,我还在读书,盯着那篇论文和它碾压围棋世界的棋谱看了很久。后来 Leela Zero 开源了,社区众筹买算力训练棋谱,需要志愿者在各自的电脑上跑自对弈。我挤进那个圈子,干的是最不起眼的活------收钱、租机器、算账。看着别人讨论棋谱、分析棋局,我心里想:也许有一天,我不是算账的那个,而是参与讨论的那个。
但那时候没有条件。学业、找工作、口袋里也没多少闲钱买算力。只能断断续续地学点深度学习,买一本强化学习的书放在桌上,慢慢地看。
这一看,就是七年。
直到今年,终于有了自己的时间和一点预算。我把书重新翻出来,租了两张 GPU,从 AlphaZero 五子棋开始,一局一局地跑。跑出了 v1、v2、v3,跑出了 0-4、2-8、1-9 这三组 PK 比分,也跑出了这篇长文。
所以这不仅仅是一篇技术总结。它是我和八年前那个深夜看 AlphaZero 论文的自己,之间的一次对话。
如果你也在折腾 AlphaZero / 自对弈 / 强化学习,欢迎:
- 👍 点赞 + ⭐ 收藏:14 个坑的速查表在 8.2 节,遇到问题回来翻
- 💬 评论区聊聊 :你训练时遇到过什么奇葩坑?或者对"策略 vs 架构"的对照实验结论有不同看法?三时间点 PK(0-4 / 2-8 / 1-9)已经给出答案:架构(value 头深度)决定天花板
- 🔗 关注:后续会更新更大网络的尝试、Web 对战部署
项目代码在 Gitee:kid141212/alpha-zero-gomoku,感兴趣的可以拉下来自己跑。