WARNING
🧪 Beta公测版本提示:教程主体已完成,正在优化细节,欢迎大家提Issue反馈问题或建议。
AlphaGo:自我对弈与 MCTS — demo.py 代码详解
运行方式
cd docs/nn-decision/rl/alphago/code
python demo.pyCPU 即可。井字棋上的 PUCT-MCTS:不训练策略/价值网络,先验均匀、叶子随机滚出。先画四张正文示意图,再打印空盘 400 次模拟的根访问 select/expand/backup,树的代码以本页为准。
代码逐段详解
第1步:棋盘约定与胜负
EMPTY, X, O = 0, 1, -1
N_SIM = 200
C_PUCT = 1.5格子 9 个,一维列表。X=1、O=-1,三人成线时和为 C_PUCT 是探索系数
def winner(b):
for i, j, k in lines:
s = b[i] + b[j] + b[k]
if s == 3: return X
if s == -3: return O
if all(v != EMPTY for v in b):
return 0
return None- 返回
None:未结束,还可以下。 - 返回
0:平局(棋盘满且没人三连)。和「空」要分开,后面if w is None才扩展/滚出。 legal:enumerate出值为EMPTY的下标。copy_play:list(b)浅拷贝再改一格——树的每个节点必须有独立棋盘,原地改会把祖先也改脏。
np.random.seed 管 Dirichlet;random.seed 管 random.choice 滚出。两套都要钉。
第2步:Node — 、 、 、
class Node:
def __init__(self, board, player, parent=None, action=None, prior=1.0):
self.n = 0 # 访问次数 N
self.w = 0.0 # 累计价值 W
self.prior = prior
self.children = {}
self.expanded = False| 符号 | 代码 | 含义 |
|---|---|---|
child.n | 这条边被走过多少次 | |
child.w | 累计回报(从走进该节点的棋手看) | |
q() | 平均价值 | |
prior | 先验;本章均匀,真 AlphaGo 来自策略网 |
def q(self):
return 0.0 if self.n == 0 else self.w / self.n
def puct(self, c):
parent_n = self.parent.n if self.parent else 1
u = c * self.prior * math.sqrt(parent_n) / (1 + self.n)
return self.q() + u:利用——平均打分高的边优先。 :探索——先验 大、自己还没被走( 小)、父亲已经很热( )时加分。分母 1+n避免除零,也让从未访问的边最大。 - 根节点没有父:
parent_n退回 1,根自己不算 PUCT(选择从根的孩子开始)。
children 是 dict:键是动作 0–8,值是子 Node。
第3步:expand — 写入均匀先验
def expand(node):
acts = legal(node.board)
if not acts:
return
prior = 1.0 / len(acts)
for a in acts:
child_board = copy_play(node.board, a, node.player)
node.children[a] = Node(child_board, -node.player, node, a, prior)
node.expanded = True每个合法动作建一个孩子:棋盘已经下了这一手,轮到对方(player 取负)。先验均分
真 AlphaGo 这里会写入网络的
第4步:select — 沿 PUCT 走到叶子
def select(node, c):
while node.expanded and node.children:
node = max(node.children.values(), key=lambda ch: ch.puct(c))
return nodewhile expanded and children:只要还能往下走,就在孩子里取 PUCT 最大的。撞上未扩展节点(或没有孩子)就停,这就是本轮叶子。max(..., key=lambda ch: ch.puct(c)):key告诉max用什么当比较键。lambda ch: ...是内联小函数,参数ch是每一个子节点。- 选择不改棋盘,只在已有树上走指针。
第5步:rollout — 随机走完,返回根视角的
def rollout(b, to_move, root_player):
player = to_move
board = list(b)
while True:
w = winner(board)
if w is not None:
if w == 0:
return 0.0
return 1.0 if w == root_player else -1.0
board = copy_play(board, random.choice(legal(board)), player)
player = -player从叶子的 to_move 开始,均匀随机落到终局。
井字棋状态极少,随机滚出方差还能忍。围棋上 AlphaGo 才用价值网缩短滚出;本 demo 故意不训网,好把树循环看清。
第6步:backup — 路径上累加 、
def backup(node, z_root, root_player):
while node is not None:
node.n += 1
if node.parent is None:
node.w += z_root
else:
mover = node.parent.player
node.w += z_root if mover == root_player else -z_root
node = node.parent从叶子一路 parent 爬到根。每经过一个节点
为什么 argmax Q。父节点的 player 是刚要下棋的人,孩子是他走完之后的局面。所以孩子的
若全程只加
第7步:mcts_move — 一棵树、若干次模拟、按 落子
root = Node(board, player)
expand(root)
noise = np.random.dirichlet([0.3] * len(acts))
ch.prior = 0.75 * ch.prior + 0.25 * float(n)根上混 Dirichlet 噪声(Zero 也这么干):避免第一次模拟把 [0.3]*len(acts) 浓度小 → 噪声尖,探索更野。0.75/0.25 是先验与噪声的混合比。
for _ in range(n_sim):
leaf = select(root, c)
w = winner(leaf.board)
if w is None:
if not leaf.expanded:
expand(leaf)
z = rollout(leaf.board, leaf.player, root.player)
backup(leaf, z, root.player)
else:
z = 0.0 if w == 0 else (1.0 if w == root.player else -1.0)
backup(leaf, z, root.player)一轮 = 选择 →(未终局则扩展)→ 评估 → 回传。已终局的叶子不再滚出,直接从棋面算 rollout。本 demo 没有
visits = {a: ch.n for a, ch in root.children.items()}
best = max(visits, key=visits.get)对局按根的访问次数 max(dict, key=dict.get):按值(次数)取最大键(动作)。
第8步:对手、对局、主函数
random_move:合法手里 random.choice。greedy_center:手写优先级,中心 4 最先,再角再边——弱而确定的基线,用来看 MCTS 是否强于「人类常识启发式」。
play_game(x_policy, o_policy):双方都是 policy(board, player) -> action。mcts_policy 丢掉 visits,只返回 best。
eval_match:MCTS 执 X,对随机 / 中心贪心各 N_GAMES 局。{X:0, 0:1, O:2}[w] 把胜平负映射到长度为 3 的计数器。
main:空盘 n_sim=400 打一次根 eval_match,画 mcts_tic_tac_toe.png。draw_three_networks 等四张是正文「三件套 / 自我对弈 / MCTS 循环 / 先验炼成
关键概念速查表
| 概念 | 数学 / 直觉 | 代码 |
|---|---|---|
Node.q | ||
| PUCT | Node.puct | |
| 选择 | 沿 PUCT 走到未扩展叶子 | select |
| 扩展 | 合法手各建子节点,均分 | expand |
| 滚出 | 随机走到终局, | rollout |
| 回传 | 路径 | backup |
| 落子 | max(visits, key=visits.get) | |
| Dirichlet | 根先验掺噪声 | 0.75*P + 0.25*noise |
| vs MuZero | 真棋盘滚出 vs 潜空间 | 本页树循环 / MuZero 章框图 |
源码位置
clone 后打开(相对仓库根目录):
docs/nn-decision/rl/alphago/code/demo.py