Soma Zero Tutorials
🔍 搜索功能尚未开启,敬请期待。

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  =  (zv)2  −  πT log p  +  c‖θ‖2

三项各管一件事,正好和代码一一对应:

  • 价值项 (zv)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 对弈程序,让它们互相对战排个名,并把贯穿全课的“开放信息接口”正式定稿——它将是第二、三章直接复用的模板。