以下内容将从原理到实践,带你一步步了解 Monte Carlo Tree Search (MCTS) 的思路,并提供一个可运行的示例代码(以经典的井字棋(Tic-Tac-Toe)为例)。你可以基于它去扩展更复杂的场景,如围棋、国际象棋或自定义游戏。
一、什么是 Monte Carlo Tree Search (MCTS)?
Monte Carlo Tree Search(简称 MCTS)是一种在决策树中进行搜索和决策的通用算法,常用于棋类、游戏以及部分规划场景。它的主要思路是通过模拟来获取某个动作(Action)在多次模拟下的表现,从而评估该动作的质量。
MCTS 由四个核心步骤构成(一般称为四大流程):
- 选择(Selection):从根节点开始,根据一定的策略(如 Upper Confidence Bound for Trees, UCT),逐步选择访问次数少但潜力大的子节点,直到到达一个还未被完全展开或代表终止状态的节点。
- 扩展(Expansion):如果所到达的节点不是终局、且可以继续向下模拟,那么选取其中一个未访问过的子节点进行扩展。
- 模拟(Simulation):从新扩展的节点开始,使用随机或启发式策略,模拟游戏或决策的后续步骤,直到到达终止状态(胜负或无法再进行)。
- 回溯(Backpropagation):将模拟的结果返回并更新路径上各节点的统计值(如胜率、访问次数),从而让那些有利可图的节点得到更高评分。
通过不断重复上述流程,节点会获得更精确的评估。在足够的模拟次数下,MCTS 可以在复杂的状态空间中找到近似最优或者高质量的走法。
MCTS 的优点
- 无需评估函数:MCTS 不像 Minimax 需要明确定义的启发式评估函数,它依赖大量随机模拟来估算胜率;
- 渐进式收敛:随着模拟次数的增多,结果越发可靠;
- 易于迁移:适用于任意可模拟的离散决策场景,特别是像围棋这类搜索空间极大的场景。
MCTS 的常见应用
- 围棋:AlphaGo 一战成名离不开 MCTS 思想的加持;
- 国际象棋、将棋:与神经网络或其它策略方法结合;
- 各种自定义博弈场景:如复杂机器人规划、AI 智能对手设计等。
二、井字棋(Tic-Tac-Toe)示例
下面我们用井字棋来演示一个简单版本的 MCTS。井字棋盘 3x3,双方轮流在空格上放置 X 或 O,先连续 3 个相同符号成线者胜。MCTS 可以帮助我们为玩家提供一个较为智能的走法。
示例代码结构
MCTSNode
:表示搜索树中的节点,保存:- 当前游戏状态(棋盘、当前落子方)
- 子节点
- 访问次数
- 胜率总和(用于计算平均胜率)
UCT
函数:用于在选择阶段决定下一个探索的节点。playout
函数:执行模拟,从当前节点持续随机走子,直到终局。backpropagation
函数:将模拟结果更新到搜索路径上的每个节点。- 主流程:对给定局面多次执行上述步骤,选择访问次数最多或胜率最高的节点做为下一步落子动作。
下面代码可以直接运行(Python 3 环境):
import math
import random
class TicTacToe:
def __init__(self):
# 3x3 棋盘,用列表保存
# 空格用 '.' 表示,'X' 和 'O' 分别表示两个玩家
self.board = [['.' for _ in range(3)] for _ in range(3)]
self.current_player = 'X' # 先手为 X
def clone(self):
# 返回棋盘的复制,用于模拟
new_game = TicTacToe()
new_game.board = [row[:] for row in self.board]
new_game.current_player = self.current_player
return new_game
def get_legal_actions(self):
# 返回当前可落子的所有 (row, col) 位置
actions = []
for r in range(3):
for c in range(3):
if self.board[r][c] == '.':
actions.append((r, c))
return actions
def do_action(self, action):
# 落子 action 是 (row, col) 坐标
(r, c) = action
self.board[r][c] = self.current_player
# 检查是否有人胜利或平局再切换玩家
self.current_player = 'O' if self.current_player == 'X' else 'X'
def get_winner(self):
# 检查胜利或平局
# 胜利返回 'X' 或 'O',平局返回 'D',没结束返回 None
lines = []
# 行、列
for i in range(3):
lines.append(self.board[i]) # 第 i 行
lines.append([self.board[r][i] for r in range(3)]) # 第 i 列
# 两条对角线
lines.append([self.board[i][i] for i in range(3)])
lines.append([self.board[i][2 - i] for i in range(3)])
for line in lines:
if line == ['X', 'X', 'X']:
return 'X'
if line == ['O', 'O', 'O']:
return 'O'
# 检查是否平局
if all(self.board[r][c] != '.' for r in range(3) for c in range(3)):
return 'D'
return None
def is_terminal(self):
return self.get_winner() is not None
class MCTSNode:
def __init__(self, game_state: TicTacToe, parent=None):
self.game_state = game_state
self.parent = parent
self.children = {}
self.visit_count = 0
self.win_score = 0 # 对应“获胜”的累计分数
def is_fully_expanded(self):
# 当所有合法动作都有对应子节点时,视为 fully expanded
actions = self.game_state.get_legal_actions()
return len(actions) == len(self.children)
def best_child(self, c_param=1.4):
# 选择 UCT 最高的子节点
best = None
best_uct = -9999999
for action, child in self.children.items():
score = child.win_score
visits = child.visit_count
parent_visits = self.visit_count
# UCT = Q + c_param * sqrt(ln(N)/n_i)
# Q = score / visits
exploit = score / (visits + 1e-8)
explore = math.sqrt(math.log(parent_visits + 1) / (visits + 1e-8))
uct = exploit + c_param * explore
if uct > best_uct:
best_uct = uct
best = (action, child)
return best
def mcts_search(root: MCTSNode, max_iter=1000):
"""
1. 选择(Selection) -> 2. 扩展(Expansion) -> 3. 模拟(Simulation) -> 4. 回溯(Backpropagation)
"""
for _ in range(max_iter):
node = root
# 1) Selection: 找到还未扩展或是终局的节点
while not node.game_state.is_terminal() and node.is_fully_expanded():
_, node = node.best_child()
# 2) Expansion: 如果还没结束,则扩展新的子节点
if not node.game_state.is_terminal():
actions = node.game_state.get_legal_actions()
for action in actions:
if action not in node.children:
# 找到一个尚未添加的动作
new_game_state = node.game_state.clone()
new_game_state.do_action(action)
child_node = MCTSNode(new_game_state, parent=node)
node.children[action] = child_node
node = child_node
break
# 3) Simulation: 从该节点开始随机模拟到终局
sim_game = node.game_state.clone()
winner = simulation(sim_game)
# 4) Backpropagation: 将结果回传到路径上的各节点
backpropagate(node, winner)
# 最后选择访问次数最多的子节点作为根节点要执行的动作
best_action, best_child = None, None
best_visits = -1
for action, child in root.children.items():
if child.visit_count > best_visits:
best_action = action
best_child = child
best_visits = child.visit_count
return best_action, best_child
def simulation(game_state: TicTacToe):
# 从当前局面开始随机走到终局,并返回最终获胜者('X','O'或'D')
while True:
winner = game_state.get_winner()
if winner is not None:
return winner
actions = game_state.get_legal_actions()
action = random.choice(actions)
game_state.do_action(action)
def backpropagate(node: MCTSNode, winner):
"""
若 winner 与当前节点的玩家相同,则给 win_score + 1
如果是平局或输,则给 0 或其他定义
"""
current_player = node.game_state.current_player
while node is not None:
node.visit_count += 1
# node.game_state.current_player 指的是“轮到谁下”
# 但要区分节点代表的玩家是谁,可以根据 parent 或其它信息判断
# 简单做法:若 node 的父节点玩家 = winner,就给 +1
# 为了演示,这里假设:
# 'X' 赢就给 +1 'O' 赢给 -1 'D' 则 0
if winner == 'X':
node.win_score += 1
elif winner == 'O':
node.win_score += -1
else:
node.win_score += 0
node = node.parent
def play_tictactoe():
game = TicTacToe()
root = MCTSNode(game)
while not game.is_terminal():
# 当前玩家走一步(MCTS)
if game.current_player == 'X':
best_action, best_child = mcts_search(root, max_iter=500)
game.do_action(best_action)
# 更新根节点
root = best_child
if not root:
root = MCTSNode(game) # 避免 child 找不到时重新创建
else:
root.parent = None
else:
# 敌方或AI2随便落子
actions = game.get_legal_actions()
action = random.choice(actions)
game.do_action(action)
# 对面行动后,我们也要重新创建 root
root = MCTSNode(game)
print_board(game.board)
if game.is_terminal():
winner = game.get_winner()
print("Winner:", winner if winner != 'D' else "Draw")
break
def print_board(board):
print("\n".join([" ".join(row) for row in board]))
print("-----")
if __name__ == "__main__":
play_tictactoe()
如何运行
- 将上述代码保存为一个
.py
文件(如mcts_tictactoe.py
)。 - 打开终端,运行
python mcts_tictactoe.py
。 - 你会看到井字棋对局的控制台输出,每步均显示当前棋盘状态以及最终胜负。
需要注意的是,这里我们对“X”给正分、“O”给负分是一种简单约定,一般会更精细地判断是哪个节点下的棋,并区分是谁的胜利。
三、扩展与提示
- UCT 函数的改进
- 除了
UCT = Q + c_param * sqrt(ln(N)/n_i)
公式,你也可以加入改进的探索策略或温度系数等。
- 除了
- 融合神经网络
- 类似 AlphaZero 的做法,用策略网络和价值网络结合 MCTS,一方面引导走子分布,一方面提供更好的状态评估,而不只是随机模拟。
- 并行化
- 如果模拟成本高,可以通过多线程或多进程加速 MCTS,或者使用 GPU 加速模拟部分。
- 适用范围
- MCTS 不仅限于两人零和对弈游戏,也可推广到多玩家或非对称游戏,甚至在一些规划任务中同样适用。
总结
Monte Carlo Tree Search(MCTS)采用了“重复模拟 + 回溯更新”的思路,适用于多种博弈和决策场景。
- 对比 Alpha-Beta/Minimax 等传统搜索,MCTS 更灵活、更易扩展;
- 在没有强力评估函数或状态树极其庞大时,MCTS 可以通过“随机模拟”得到可行解;
- 搭配神经网络策略/价值评估时(例如 AlphaZero),威力更是显著提升。
通过本文的井字棋示例,你可以快速入门 MCTS,理解它的四大流程和主要数据结构。之后,只需替换“游戏逻辑”和“模拟策略”,就能将该框架扩展到更复杂的应用场景,或者与深度学习算法相结合,打造更强大的智能体。祝你在 MCTS 的研究与实践中一路畅通!
MCTS的局限性
在前面我们介绍了 MCTS 的基本流程和示例代码,从中可看到它在中小规模场景下表现相当不错,特别是用于棋类、决策类游戏。然而,在大规模推理、尤其是自然语言生成的场景中,MCTS 也暴露出一些局限性。根据《DeepSeek-R1》论文(特别是其中对大规模 RL 与搜索方法的经验分享)可以总结出 MCTS 在更复杂的 LLM 推理任务中容易遇到以下几个主要问题:
1. 搜索空间过于庞大
- 问题表现
在象棋或围棋这类棋类游戏中,每步可行走法数目虽然大,但仍然有限,状态的离散程度也较高。然而,对于自然语言生成、代码生成这类场景,可能每一步都存在上百甚至更多可选 Token,形成一个难以穷举的巨大搜索树。 - 原因分析
MCTS 依赖在搜索树上“选择 - 扩展 - 模拟”来收敛到优质节点,如果每一层的分支数目过多,搜索就会变得异常缓慢,而且“蒙特卡洛模拟”也很难在合理时间内覆盖足够的分支。 - 潜在影响
模型可能会耗费海量计算资源却只能探索到极小的分支,从而无法找到具有全局最优意义的推理路径。或者为了适应超大分支,不得不做非常激进的剪枝与近似,导致算法失去精度。
2. 对价值(Value)模型的依赖
- 问题表现
传统的 MCTS 可以在无评估函数的情况下通过随机模拟最终胜负来进行回溯。但在自然语言、开放性问答或工具调用等场景,常常并不存在一个明确定义的“输赢”终局,或需要一个价值网络来评估局面优劣。 - 原因分析
当缺乏准确的价值估计时,MCTS 的模拟往往演变为“随机产生大量 Token”,然后很难确定其好坏,最终难以回溯到根节点形成有效决策;而如果使用一个训练不充分或偏弱的价值模型,搜索往往会走入局部最优或出现“奖励作弊”。 - 潜在影响
模型性能高度依赖价值网络的质量,一旦价值网络不准确,就会大幅放大误差,使 MCTS 在语言模型推理等复杂场景中变得效果有限。
3. 训练和推理的成本高
- 问题表现
MCTS 在每个决策点都可能进行多次模拟(Playout)才能得到相对稳定的估计;当我们应用到每个 Token 的生成时,计算开销将呈指数级增长。对于大模型(LLM)来说,生成一次 Token 就已相当昂贵,若再叠加 MCTS 的数千甚至更多次模拟,系统整体就难以承受。 - 原因分析
- 大模型本身的推理成本就高(如 70B+ 参数)。
- MCTS 需要反复地与环境(或模型)进行交互、模拟和回溯。
- 潜在影响
实际部署中会出现超时或资源耗尽的问题,难以在工业级应用场景下普及。
4. 易陷入局部搜索和停滞
- 问题表现
当搜索空间极大且缺乏良好的评估或抽象机制时,MCTS 的选择策略(例如 UCT)可能会频繁地在相对有限的一部分区域反复搜索,无法有效探索潜在更优的分支。 - 原因分析
- 大量无信息或噪声的分支混入,容易导致搜索效率降低。
- 在语言生成中,“有效解”和“无效解”之间可能界限不清,导致搜索重复地访问某些毫无进展的路径。
- 潜在影响
搜索停滞或只在浅层进行,缺乏深入拓展,从而无法像在博弈中那样,逐步朝更优解收敛。
5. 工程实现与算法迭代的复杂度
- 问题表现
若想在大模型推理中使用 MCTS,往往需要对模型进行“多轮调用”或引入在线价值网络,这大大提高了工程实现的复杂度,也会让训练(或推理)流程变得繁琐。 - 原因分析
- 与普通 beam search 等方法相比,MCTS 是一种“拓展 + 模拟 + 回溯”的循环过程;
- 对每个节点都要维护访问次数、胜率等统计量;
- 可能需要并行化、异步化才能在合理时间内完成搜索。
- 潜在影响
当我们想持续迭代训练一个策略模型和价值模型,比如类 AlphaZero 方法,需要搭建完整的自对弈管线、价值网络更新管线等。对于语言任务,这些流程都尚不成熟,出错率也相对更高。
总结
Monte Carlo Tree Search(MCTS)确实在棋类、经典博弈中大放异彩,也为之后的 AlphaZero 等深度强化学习作品奠定了重要基础。但在大规模语言模型(LLM)领域,我们同样看到它的种种局限:
- 搜索空间庞大:难以高效覆盖;
- 依赖价值网络:价值模型难训练且容易被放大误差;
- 计算成本高:需要大量模拟,LLM 推理本就昂贵;
- 易局部陷入:缺乏明确终局判定可能导致反复搜索;
- 实现复杂:工程部署和调试成本显著提升。
在《DeepSeek-R1》论文中,研究者也提到,MCTS 在自搜索(self-search)的任务中并没有达到预期的提升,因为它难以在大模型的 token 级推理中高效展开,并且对价值模型的需求高、错误风险大。
因此,像 DeepSeek-R1 最终选择了更直接的强化学习(如 GRPO)配合规则/答案判定的思路,而非借助 MCTS 去做大规模自搜索。对于语言模型而言,如果没有稳定、可扩展的价值网络来支撑大规模搜索,MCTS 会在应用中面临种种瓶颈。
总之,MCTS 并非万能,它更适合有限动作的离散环境或有完备胜负规则的博弈场景。面对大语言模型所处的开放性生成任务,我们在实践中应评估成本、可行性与收益再做取舍。如果真要采用 MCTS,也需要配合高质量的价值评估模型以及强力并行化手段,才能在极大搜索空间中更好地发挥它的威力。
后记
2025年2月24日14点31分于上海,在GPT o1大模型辅助下完成。