1.8 迷你 AlphaZero(自对弈 RL)
上一节我们说,给 MCTS 配一个神经网络,让它替代“随机模拟”和“均匀先验”,就成了 AlphaZero。这一节我们把它真正训出来——而且就在井字棋这个小沙盘上,用你的 5070 Ti 跑,几分钟见效。井字棋小到我们能验证它学得对不对,这正是理解 AlphaZero 最好的起点。本节也是全课第一次上 GPU,环境怎么搭也一并讲清。
一、总体架构:一台自我进化的“飞轮”
AlphaZero 的核心是一个神经网络,我们记作 f(局面) → (策略 p, 价值 v)。它一次吐出两样东西:策略 p 是“各个走法各有多大概率是好棋”的分布;价值 v 是“当前这方的胜算有多大”(一个 −1 到 +1 的数)。这两样恰好补上了上一节 MCTS 的两个软肋——用 p 当先验来引导“选择”,用 v 替代“随机走到底”来估值。
真正精妙的是它怎么自己变强,这是一个转起来就停不下的飞轮:
┌─────────────────────────────────────────────────────────┐
│ │
│ 神经网络 f ──指导──► MCTS 搜索 ──► 比网络本身更强的走法 │
│ ▲ │ │
│ │ ▼ │
│ 训练网络 ◄──作为学习目标── 自对弈,记录每步(局面, MCTS策略, 最终胜负) │
│ │ │
│ └────────► 得到更强的网络,回到顶上,再转一圈 ──────────┘
└─────────────────────────────────────────────────────────┘
为什么这个圈会“越转越强”?关键在于:MCTS 借助网络搜一搜之后,给出的走法总比网络张口就来要好一点(搜索本身有提升作用)。于是我们就拿“MCTS 搜出来的走法分布”当老师,去训练网络,让它下次张口就能更接近搜索的水平。网络强了,它指导的 MCTS 又更强,又能当更好的老师……如此循环。整个过程不需要任何人类棋谱,也不需要人写评估函数——它从零开始,自己跟自己下,自己教自己。
二、先把 GPU 环境搭好,再写一个极小网络
这是我们第一次用到显卡,先把环境弄对。你的卡是 RTX 5070 Ti(Blackwell 架构,算力 sm_120),它要求 CUDA 12.8 及以上。好消息是本机已经验证过 cu128 能用(IsaacLab 环境里的 torch 2.7.0+cu128 就跑在这张卡上),所以我们装稳定版 cu128 轮子即可,不必折腾 nightly。按规矩开一个独立环境,别和 ROS / IsaacLab 混在一起:
# 1) 建一个独立 conda 环境(与 ROS / IsaacLab 隔离)
conda create -n animachess python=3.11 -y
conda activate animachess
# 2) 装 cu128 版 PyTorch —— Blackwell(sm_120) 需要 CUDA 12.8+
pip install torch --index-url https://download.pytorch.org/whl/cu128
pip install numpy
# 3) 验证显卡真的能用(这一步务必先过,再谈训练)
python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"
# 期望输出类似: True NVIDIA GeForce RTX 5070 Ti
环境就绪,来写网络。井字棋极简,一个两三层的小 MLP 就绰绰有余——输入 9 个格子,分出“策略头”和“价值头”两个出口:
import torch
import torch.nn as nn
import torch.nn.functional as F
class TicTacToeNet(nn.Module):
"""输入 9 格局面,输出 策略(9 个落子倾向) + 价值(1 个胜算预测)。"""
def __init__(self):
super().__init__()
self.body = nn.Sequential(
nn.Linear(9, 64), nn.ReLU(),
nn.Linear(64, 64), nn.ReLU(),
)
self.policy_head = nn.Linear(64, 9) # 9 个格子的走子 logits
self.value_head = nn.Linear(64, 1) # 一个标量:当前方的胜算
def forward(self, x):
h = self.body(x)
p = self.policy_head(h) # logits,用的时候再 softmax
v = torch.tanh(self.value_head(h)) # 用 tanh 压到 [-1, 1]
return p, v
三、自对弈、训练、再迭代
飞轮分三个动作,我们逐个看。第一,自对弈产数据。让网络指导 MCTS 自己跟自己下,每走一步就记下三样东西:当时的局面、这步 MCTS 给出的走子分布、以及这盘棋最终的胜负。这三样就是一条训练样本 (局面, MCTS策略, 最终结果)。
第二,训练网络。目标很直白:让网络的策略输出去逼近“MCTS 那个更强的分布”(用交叉熵),让网络的价值输出去逼近“这盘棋真实的胜负”(用均方误差)。两个损失加在一起:
def loss_fn(pred_p, pred_v, target_p, target_z):
# 策略损失:网络的落子分布 → 逼近 MCTS 搜出来的分布(交叉熵)
policy_loss = -(target_p * F.log_softmax(pred_p, dim=1)).sum(dim=1).mean()
# 价值损失:网络预测的胜算 → 逼近这盘棋的真实结果(均方误差)
value_loss = F.mse_loss(pred_v.squeeze(1), target_z)
return policy_loss + value_loss
这段代码对应的,就是 AlphaZero 那个经典的损失函数,写成公式是:
L = (z − v)2 − πT log p + c‖θ‖2
三项各管一件事,正好和代码一一对应:
- 价值项 (z − v)2:让网络预测的胜算 v 逼近这盘棋的真实结果 z——就是代码里的
value_loss(均方误差)。 - 策略项 − πT log p:让网络的落子分布 p 逼近 MCTS 搜出来的分布 π——就是代码里的
policy_loss(交叉熵)。 - 正则项 c‖θ‖2:抑制过拟合。代码里没显式写它,因为它由优化器的
weight_decay参数代劳。
第三,迭代。把上面两步套进一个大循环:自对弈一批棋 → 用这批数据训练网络 → (可选)让新网络和旧网络对几盘、谁强留谁 → 回到第一步。结构大致如此:
net = TicTacToeNet().cuda() # 模型搬到 GPU
opt = torch.optim.Adam(net.parameters(), lr=1e-3)
for iteration in range(20): # 井字棋几十轮就够
data = []
for _ in range(100): # 自对弈 100 盘
data += self_play_one_game(net) # 每盘产出若干 (局面, 策略, 结果)
for state, target_p, target_z in make_batches(data):
state = state.cuda(); target_p = target_p.cuda(); target_z = target_z.cuda()
pred_p, pred_v = net(state)
loss = loss_fn(pred_p, pred_v, target_p, target_z)
opt.zero_grad(); loss.backward(); opt.step()
print(f"第 {iteration} 轮完成,loss={loss.item():.3f}")
井字棋的状态空间小得可怜,所以这套流程在 5070 Ti 上几分钟就能跑完,显卡几乎不喘气。这当然是杀鸡用牛刀——但你跑通的,是和当年战胜人类的 AlphaZero 一模一样的完整流程,只是缩小版。第二章把同样这套搬到 15×15 五子棋,显卡才会真正火力全开、训上几天。
四、验证:它真的学到“必和”了吗
为什么非要在井字棋上先练这一遍?因为井字棋是已解的——完美对弈必然和棋(还记得 1.1 那张表吗)。这给了我们一把别处没有的“标准答案尺”,能实打实地检验网络学得对不对:
- 胜负该收敛到和棋:训练到后期,网络自对弈的结果应当几乎全是平局——因为它两边都越下越接近完美。
- 空棋盘的价值该趋近 0:把初始空局面喂给网络,它输出的价值 v 应该接近 0(谁也赢不了,就是和)。如果它信誓旦旦说先手能赢,那一定是没学好。
- 走法该和 Minimax 一致:拿 1.3 那个完美的 Minimax 当裁判,逐一比对关键局面下网络的选择,看它是否每步都走在最优着法上。
这种“拿已知答案当裁判”的可验证性,是小游戏独有的奢侈。到了五子棋、国际象棋,我们就没有这把尺子了,只能靠引擎之间对战的 Elo 来间接判断强弱。所以趁现在能验证,务必把这套流程的对错吃透——你在这里建立的信心,会一路撑着你走完后面两章。
五、小结与下一节
- 架构:一个网络同时输出策略 p 与价值 v,补上 MCTS 的两个软肋。
- 飞轮:网络指导 MCTS → MCTS 更强 → 当老师训网络 → 网络更强,循环自我进化,无需人类棋谱与评估函数。
- GPU 环境:5070 Ti(sm_120) 用稳定版 cu128 PyTorch,独立 conda 环境,先验证
cuda.is_available()再训。 - 可验证:井字棋已解(必和),可用胜负收敛、空盘价值趋 0、对照 Minimax 三招检验——这把尺子到大棋盘就没有了。
第一章的算法到这里就全了:从 Minimax、α-β,到评估/排序、迭代加深、MCTS,再到 AlphaZero。下一节 1.9 Capstone,我们把这些引擎收进一个可玩的 pygame 对弈程序,让它们互相对战排个名,并把贯穿全课的“开放信息接口”正式定稿——它将是第二、三章直接复用的模板。