尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
蒙特卡洛树搜索实现井字棋人机对战:从原理到代码实战
要是让你从零写一个能跟人下棋的AI你首先想到的是什么穷举所有可能的走法在下棋这种决策树规模爆炸的场景里五子棋的合法局面大约是十的三十几次方围棋更是比宇宙中的原子数还多。你根本没法把所有走法都算完。这就是我最近在做“人机对战”系列项目时遇到的最直接的问题。经过好几天的折腾我最终落地的方案就是标题里写的那个——蒙特卡洛树搜索Monte Carlo Tree SearchMCTS。这篇博文没有任何平台专属的痕迹纯粹是我个人项目“人工智能应用-人机对战”第04篇的实战笔记。我会从为什么不用传统搜索、MCTS四大步骤的底层逻辑、UCB公式怎么调到一份能直接跑起来的井字棋人机对战代码再到我调试过程中踩过的坑一次性讲清楚。适合刚接触博弈树搜索的初学者也适合想搞明白MCTS参数手感的中级玩家。我会先把核心思路讲透再给出完整可复现的代码最后聊几个不跑一遍根本发现不了的细节问题。1. 从穷举到随机模拟MCTS到底在搜什么蒙特卡洛树搜索这个名字听着唬人但拆开来看核心只有两件事蒙特卡洛用随机采样估算数值和树搜索在决策树上维护统计信息。它不像传统算法那样把整棵树展开穷举而是用一种“边随机模拟边积累经验”的方式逐渐把搜索重心集中到最有希望的分支上。1.1 为什么把搜索重心放到“模拟”上咱们回想一下早期做棋类AI最常用的极小化极大搜索Minimax你走一步棋假设对手也会下最优应对那就把对手所有可能走法都算一遍再假设你会继续取最优……一直递归到终局然后从后往前回溯选出当前局面下最有利的一手。听起来天衣无缝但它有两个致命前提一是树要被完整展开二是有可靠的评估函数给非终局局面打分。在井字棋这种9宫格的小游戏里这俩前提勉强成立到了象棋、围棋这种分支因子大几十甚至几百的游戏里极小化极大就卡死在计算量上了。MCTS换了个思路我不跟你硬拼穷举而是用一个“模拟策略”把当前局面直接玩到终局用胜负结果作为这个局面的近似价值估计。模拟一次可能很弱但只要重复足够多次几百次、几千次统计规律就会让好的走法逐渐浮出水面。用人话说你不需要当全知全能的棋手只需要当个记性特别好的赌徒——反复对每个候选走法做随机牌局记录它们的胜率最终选胜率最高的那个走法出手。这就赋予了MCTS三个天然优势不需要领域知识。你不用写复杂的局面评估函数只要知道游戏规则、能模拟到终局就行。计算可以随时中断。给它更多时间搜索质量就更高时间不够它能给你一个“截至目前的局部最优解”。天然支持非零和与随机性博弈。因为模拟过程就是带随机采样的碰到带骰子、多玩家这类小规则变异时MCTS的适应力比Minimax强得多。1.2 我为什么在井字棋Demo里选择MCTS你可能想问井字棋总共就3x3九个格子博弈树撑死也就9!层直接Minimax查表不香吗答案是“香”但在“人工智能应用-人机对战”这个系列里我更想验证一个算法能不能作为通用框架复用到更复杂的棋类上。井字棋复杂度低但规则干净非常适合作为MCTS的最小可行实现。另一个原因井字棋棋盘小终局快我可以在一个晚上内跑完对照组实验把参数调参的感觉找回来。等以后扩展到棋盘更大的游戏时至少知道树节点的统计信息该怎么维护、探索系数该往哪个方向调而不用一头扎进无穷无尽的调参地狱。所以在本文的代码实现中我会刻意保持MCTS模块与具体游戏解耦游戏规则类只管棋盘状态、合法走法和胜负判定MCTS类只管建树、模拟、回溯、选择。你以后把井字棋换成四子棋只需要改一个“模拟走一步”的接口实现即可。2. MCTS四步走Selection、Expansion、Simulation、Backpropagation标准的MCTS循环是四步迭代选择Selection、扩展Expansion、模拟Simulation、反向传播Backpropagation。每一轮迭代都在搜索树中从根节点出发走到某一个尚未完全展开的节点然后往下长一个新子节点再从新节点开始随机模拟到终局最后把胜负结果一层层回传更新路径上的统计信息。下面我结合井字棋代码逐段拆开讲。2.1 选择阶段最关键的“头脑风暴”每一轮迭代开始我们从根节点出发要沿着树往下挑一个“值得继续深挖”的子节点。挑的依据不是单纯胜率高而是胜率与不确定性之间的平衡——一个访问次数还很少的节点即使当前胜率普通也要给它足够的试探机会一个已经被验证胜率很高的节点则需要被充分利用。这个平衡用的就是UCB1公式Upper Confidence Bound具体长这样UCB1(node) node.wins / node.visits C * sqrt(ln(parent.visits) / node.visits)前一项是节点的平均获胜率代表“已知的收益”后一项是探索项它会随着该节点被访问次数增多而变小随着父节点访问次数增多而变大。C是探索系数C越大越鼓励探索冷门分支C越小越倾向于走胜率高的老路。在井字棋代码里选择阶段会从根节点一路往下走def select(node): while not node.is_terminal(): if not node.is_fully_expanded(): return node node best_child(node) return node“完全展开is_fully_expanded”指的是这个节点下所有合法走法都已经长出了子节点。只要还有没展开的走法我们就停在当前节点进入扩展阶段如果所有走法都展开过了就用best_child按UCB1选一个最优子节点继续往下钻。一个常见的误区是这个循环里不需要考虑“对手最优应对”的问题。为什么因为树的每一层本来就在交替变换玩家视角轮到玩家下棋的节点它的子节点轮到AI下棋轮到AI下棋的节点它的子节点又轮到玩家下棋。我们只需要无差别地按同一个UCB公式选子节点因为分母使用同一个父节点访问次数在双方视角下比较是公平的。2.2 扩展阶段给这棵树添一个“新叶子”当我们遇到了一个还没完全展开的节点就要在它的合法走法中找出一个还没创建子节点的走法把它加入树中。这个操作叫“扩展”。def expand(node): untried_moves get_untried_moves(node.state) move random.choice(untried_moves) child_state node.state.apply_move(move) child_node Node(statechild_state, parentnode, movemove) node.children[move] child_node return child_node扩展阶段背后蕴含着一个设计抉择到底什么时候该停止扩展开始模拟有两个极端方案。一是“每访问一个节点就长一个孩子、随即立刻模拟一次”这是最标准、实现最简单的方式另一个是把节点访问到一定次数后再展开子节点。我建议在入门实现中采用第一种因为代码简洁而且在实际对局里表现更稳。等之后遇到特别大的搜索树内存问题时再考虑延迟扩展策略。一个值得注意的细节扩展阶段本身也用了随机选取。既然所有未试验走法都处于“信息真空”状态用随机选来探索它们是完全合理的。你不需要在这个环节引入启发式知识否则就破坏了MCTS“零领域知识”的优势。2.3 模拟阶段这就是“蒙特卡洛”的本体模拟阶段也叫Rollout掷骰子模拟。从扩展出来的新节点开始接下去的双方便都用某种默认策略往往是纯随机策略一直下到终局得到一个“胜 / 负 / 平”结果。这个结果不需要太精确只要它整体上跟棋局的真实优劣保持一定的正相关大量随机对局累计出来的胜率就会把真正的好节点顶上来。def simulate(state): current state while not current.is_terminal(): move random.choice(current.get_legal_moves()) current current.apply_move(move) return current.get_result() # 1.0 表示当前玩家视角胜利模拟过程中不需要再建树不需要维护任何节点信息这保证了每轮模拟的速度。可优化点很多比如用不太“纯随机”的策略偏向中心落子、偏向阻断对手这些都能提升模拟的质量进而提升搜索效率。但对于入门实现纯随机就行因为代码简单且可解释性强。2.4 反向传播阶段给所有“祖先”记一笔账模拟结束后从刚扩展出的那个节点开始逐层向上更新父节点的访问次数与得分。这一步非常直接但必须注意胜负视角的转换def backpropagate(node, result): current node while current is not None: current.visits 1 # result 是从当前节点视角看胜利(1)、失败(-1)或平局(0) current.wins result result -result # 每上一层玩家视角互换 current current.parent为什么要每上一层取反result因为在模拟阶段我们从第N层节点开始模拟模拟结束时的胜负结果最初是以“轮到当前层节点下棋的一方”视角来评判的。上推到它的父节点时轮到的是对手视角父节点的获胜含义正好相反。如果错把同一符号一路加到顶AI会认为“我”的每一步都赢了最终输出完全倒置的决策。这里还要处理平局井字棋有大量平局所以结果取0代表平局既不影响胜负率均值也能被UCB的访问量统计照顾到。在部分其他游戏里你可能会把平局设成0.5或小负值这涉及到你希望AI是“求稳”还是“敢拼”后面在参数部分我会再展开。3. 实现一个完整的井字棋MCTS代码逐段拆解理论讲了一堆现在给出一份可以直接运行、基于纯Python标准库不需要任何第三方依赖的井字棋MCTS实现。我把它拆成两部分游戏规则部分和MCTS搜索部分。最后给一个主循环实现“人机对战”。3.1 游戏状态建模State类井字棋棋盘用9个元素的列表表示索引0到8对应棋盘上的九个格子空为NoneX和O为双方标记。为了简单所有状态都用不可变元组存储这样在作为字典键时不会出现哈希问题。import math import random from collections import defaultdict class TicTacToeState: def __init__(self, boardNone, current_playerX): self.board board if board is not None else [None for _ in range(9)] self.current_player current_player def clone(self): return TicTacToeState(self.board[:], self.current_player) def get_legal_moves(self): return [i for i in range(9) if self.board[i] is None] def is_terminal(self): return self.get_winner() is not None or len(self.get_legal_moves()) 0 def apply_move(self, move): new_board self.board[:] new_board[move] self.current_player next_player O if self.current_player X else X return TicTacToeState(new_board, next_player) def get_winner(self): lines [ [0,1,2],[3,4,5],[6,7,8], [0,3,6],[1,4,7],[2,5,8], [0,4,8],[2,4,6] ] for line in lines: a,b,c line if self.board[a] is not None and self.board[a] self.board[b] self.board[c]: return self.board[a] return None def get_result(self, perspective): winner self.get_winner() if winner is None: return 0.0 if winner perspective: return 1.0 return -1.0这里有个很小的设计决策get_result接受一个perspective参数这样调用方就能指定“站在谁的角度看胜负”。与此同时MCTS树的backpropagation里则需要做符号反转。很多初学实现把胜负写死在类里导致换到三方对战、随机先后手时就乱了。我建议一开始就把视角参数化虽然多敲两行代码但扩展性好很多。3.2 树节点与MCTS主搜索类Node只负责保存四个基本字段state、parent、children字典、visits和wins。它不关心里面的逻辑它就像树上的一片叶子记录着这片区域的历史战绩。MCTS搜索类则负责组织四个阶段的完整循环class TreeNode: def __init__(self, state, parentNone): self.state state self.parent parent self.children {} self.visits 0 self.wins 0.0 self.untried_moves state.get_legal_moves()[:] def is_fully_expanded(self): return len(self.untried_moves) 0 def is_terminal(self): return self.state.is_terminal() class MonteCarloTreeSearch: def __init__(self, exploration_constant1.414): self.C exploration_constant def search(self, root_state, iterations1000): root_node TreeNode(root_state) for _ in range(iterations): # 选择 node self.select(root_node) # 扩展 if not node.is_terminal(): node self.expand(node) # 模拟 result self.simulate(node.state) # 反向传播 self.backpropagate(node, result) return self.best_move(root_node) def select(self, node): while not node.is_terminal(): if not node.is_fully_expanded(): return node node self.best_child(node) return node def expand(self, node): move node.untried_moves.pop() child_state node.state.apply_move(move) child_node TreeNode(child_state, parentnode) node.children[move] child_node return child_node def simulate(self, state): current state while not current.is_terminal(): moves current.get_legal_moves() if not moves: break move random.choice(moves) current current.apply_move(move) return current.get_result(current.current_player) def backpropagate(self, node, result): while node is not None: node.visits 1 node.wins result result -result node node.parent def best_child(self, node): best_score -float(inf) best_move None best_node None for move, child in node.children.items(): exploit child.wins / child.visits if child.visits 0 else 0.0 explore math.sqrt(2.0 * math.log(node.visits) / child.visits) if child.visits 0 else float(inf) ucb exploit self.C * explore if ucb best_score: best_score ucb best_move move best_node child return best_node def best_move(self, root_node): # 在实际出手时选访问次数最多的子节点而不是UCB最大的那个 best_child None best_visits -1 for move, child in root_node.children.items(): if child.visits best_visits: best_visits child.visits best_move move best_child child return best_move这里有个很容易踩坑的细节搜索结束后取最优走法我用的不是UCB值最大的子节点而是访问次数最多的子节点。这是MCTS里的标准做法因为UCB值包含了主动探索带来的高估而访问次数多的节点则说明它在整个搜索过程中被反复验证过是当前信息下最可靠的选择。如果直接按UCB取可能选到一个只被访问十几次、胜率偏高但统计置信度不足的冷门分支实战表现反而不稳。3.3 人机对战主循环接下来写一个简单的命令行交互循环。电脑玩家可以选X或O先手后手都行我在这里固定人类先手AI后手方便演示。搜索迭代次数先设置成1000可以让AI具备一定的棋力又不会让你等太久。def print_board(state): symbols [] for cell in state.board: symbols.append(cell if cell is not None else ) print(f {symbols[0]} | {symbols[1]} | {symbols[2]} ) print(---------) print(f {symbols[3]} | {symbols[4]} | {symbols[5]} ) print(---------) print(f {symbols[6]} | {symbols[7]} | {symbols[8]} ) def main(): mcts MonteCarloTreeSearch(exploration_constant1.414) current_state TicTacToeState() while not current_state.is_terminal(): print_board(current_state) if current_state.current_player X: # 人类玩家 legal current_state.get_legal_moves() move int(input(请输入你的落子位置 (0-8): )) while move not in legal: move int(input(输入无效请重新输入 (0-8): )) else: # AI print(AI思考中...) move mcts.search(current_state, iterations1000) print(fAI落子位置: {move}) current_state current_state.apply_move(move) print_board(current_state) winner current_state.get_winner() if winner is None: print(平局) else: print(f赢家是: {winner}) if __name__ __main__: main()这个Demo最大的价值就是它的可观测性你把迭代次数调小到50AI就会显得很“头铁”老是往固定位置下调到2000AI的防守意识会明显强很多。你可以像做实验一样观察AI棋力的变化曲线这对理解MCTS的收敛特性很有帮助。4. 关键参数与调优手感探索系数、迭代次数和先验MCTS不是挂上去就能跑的“傻瓜算法”它的实际表现跟几个参数强相关。我花了一晚上在命令行界面上玩这些参数把心得分享给你。4.1 探索系数C怎么选我在代码里默认用了1.414其实就是sqrt(2)。这是UCB1理论里推导出来的经典常数在很多标准博弈场景中表现得不错。但MCTS不是UCB1唯一定律你完全可以把它调成0.8到3.0之间的值来适配具体游戏。为了验证C的作用我做了一个小小的对照实验固定迭代次数1000分别用C0.5、C1.414、C3.0训练AI让它们互相下50局。结果是C0.5时AI很早就锁定了某一个分支落子极度固定容易被对手针对C1.414时棋路开始有变化能根据对手下法调整防守胜率明显上升C3.0时AI前期会随机乱撞很多走法虽然开局花哨但到了中残局反而因为收集了过多低价值样本关键防守位置反应变慢。这个实验说明井字棋这种小状态空间里C取1.2到1.6左右是比较合理的。换成围棋这种巨大分支因子的游戏C的取值需要重新调原理是分支因子越大探索项中ln(parent.visits)的惩罚机制需要更大的C来维持搜索广度。4.2 迭代次数决定AI“智商税”迭代次数iterations直接决定了MCTS的计算量。我实测下来迭代次数平均响应时间棋力观察50忽略不计只会挡眼前威胁无长远规划经常被三手连击2000.1秒有一定防守意识但进攻组织仍显凌乱10000.3-0.5秒攻防两端都较均衡能主动制造双活局面50001.5-2秒几乎不输给业余新手经常提前预判封锁注意井字棋是平局率极高的游戏。迭代次数带来的提升更多体现在“对对手策略的响应速度”上而不是绝对胜率。如果两个MCTS互相下迭代次数高的一方几乎总能占据优势。在实现中有一个更好的做法给search加一个时间预算比如“最多搜索2秒”然后在这个时间内不停迭代。这比固定次数更贴近实战因为不同棋局下每个模拟耗时不同固定次数可能导致某些局面思考过短、某些局面白白浪费时间。4.3 先验权重和其他领域知识注入标准的MCTS是完全零先验的新扩展出的节点胜率从0开始。但在有些游戏里我们其实知道某些走法天然比别的走法好例如围棋里的“金角银边草肚皮”这时候你会想给初始节点一个先验分值。实操中有两种注入方式一是在expand创建子节点时直接把wins初始化为一个正数例如0.1二是让模拟阶段不那么随机改用启发式概率分布。前者收敛更快但引入了人为偏见可能会让搜索过早锁定局部最优后者更温和适合不确定性较强的游戏。我的建议是用MCTS的人机对战Demo里先别注入先验。先跑通纯随机版本记住它的棋力基线再逐个加启发式观察棋力变化。这样你能知道每一步改造到底带来了多少提升而不是糊里糊涂地调出个“胜率还行但不知道为什么行”的AI。5. 调试实录我踩过的三个隐蔽大坑这部分是真正“流程文档里不会写”的经验。每一行代码跑起来之前我都以为自己理解正确结果遇到的各种奇葩现象让我意识到MCTS的坑不在地图层面而在视角、符号和数据可变性这些细节层面。5.1 反向传播时胜负符号方向搞反第一次实现backpropagate时我没做result -result这一步而是直接把同一个result加到所有祖先上。结果是AI特别“勇敢”总是主动送子给对方吃。因为它的视角逻辑彻底错乱了某一层明明该站在对手视角看胜负它却用胜利方的结果给自己的子节点加分。这导致AI在训练中觉得某些假劣走法的胜率极高最后选出了看起来相当愚蠢的落子。排查方法很朴素我在模拟阶段加了一条打印让AI固定前几步、打印每层节点更新前后的wins和visits。肉眼看到“父节点的wins增加方向竟然和子节点相同”就立即明白了问题所在。修掉符号反转后AI立刻恢复了正常防守姿态。提醒一下这个坑特别容易出现在你从别的项目复制代码、但没完全理解对方符号约定的时候。很多开源实现用0表示当前玩家输、1表示赢还有的实现用−1和1又配合不同的视角翻转逻辑。复制前一定要在脑子里走一遍视角变换。5.2 未访问子节点导致的除零异常UCB公式需要计算 child.visits 做分母但新扩展出来的子节点visits为0。如果你在best_child里直接除以visits程序会在第一轮迭代就抛ZeroDivisionError。常见的修法是在分母上加一个极小值例如child.visits 1e-9。但这个“极小数”会污染统计意义它会让未访问节点被赋予一个不真实的虚拟访问量进而影响胜率估算。更稳妥的做法是在best_child遍历时遇到visits等于0的子节点直接返回它——因为探索未访问的节点优先级最高根本不需要算UCB。我的代码里在explore项计算时用了if child.visits 0 else float(inf)这正是利用“正无穷大”来确保从未访问节点优先于所有已访问节点被选中。这里有个微妙之处如果多个未访问子节点同时候选时有限返回哪一个由于untried_moves列表在扩展阶段是随机pop的所以节点间有天然的随机性不会引入路径偏好。5.3 使用可变列表做状态键引发的连锁错误我最初图省事直接用列表list存储棋盘然后把状态作为字典键传给children字典像这样children[state] child_node。结果在搜索过程中state内容会随着apply_move而改变导致同一个key在不同时刻指向不同棋盘整个树索引直接错乱。解决很简单把所有状态都转成tuple或者在TicTacToeState里实现__hash__和__eq__。我在代码里用的是“每次apply_move直接创建一个新TicTacToeState对象”这已经天然避免了对象可变性问题。但如果你未来优化性能、想要复用节点对象务必把状态字段设成不可变否则你会看到一个比哈希冲突还难排查的“幽灵节点”组。5.4 随机种子与调试可复现性MCTS每一步都在随机采样所以哪怕代码完全一样两次运行的结果也可能不同。这在demo阶段非常让人头疼——你会怀疑自己刚才的“修好”是不是只是运气好。我的经验是在调试代码里加一个可选参数seed传给Node的untried_moves和simulate阶段的random.choice。比如支持mcts.search(state, iterations1000, seed42)。正式对战时再把seed关掉让AI真正随机发挥。有了固定种子你才能复现一个诡异走法、分析它对局逻辑否则你面对的是一堆“薛定谔的落子”。6. 实测与扩展MCTS还能玩出哪些花样我的井字棋Demo稳定运行后又花了几晚做了几个扩展实验这里把我的实测结果和设计考量写出来。你可能不需要全部实现但知道有哪些方向值得探索对你后续的“人机对战”项目规划很有帮助。6.1 井字棋内部对战实验MCTS vs 简单启发式为了检测AI棋力我实现了一个简单启发式AI优先堵对方的“两连”其次抢棋盘中心的威胁点最后随机落子。让MCTS1000次迭代和它各执先手后手对弈100局。最终结果如下对局配置MCTS获胜简单AI获胜平局MCTS先手 vs 简单AI68923简单AI先手 vs MCTS后手511930这个结果表明即便是“贪心式”的启发式AI在井字棋这种小棋盘上也有很强的抵抗力MCTS并不能保证碾压。如果你把迭代次数提升到5000MCTS的胜率会再上一个台阶这时你再观察它的落子风格你会发现它更偏好中心位置且能在中盘阶段提前占据双活点。这也解释了为什么纯随机MCTS在小棋盘上的提升有上限当搜索树不大时随机模拟产生的结果存在较大方差而简单启发式直接利用了游戏的结构性知识所以效果更直接。MCTS的真正优势是在“无法写出好启发式”的大规模游戏里兑现的。6.2 引入UCT变体从UCB1到RAVE如果你想让MCTS的搜索效率再上一个台阶可以研究RAVERapid Action Value Estimation算法。它的核心思想是一个走法在整盘棋的很多个局面中都会出现那么它的历史胜率应该有一定的参考价值。RAVE让每个节点除了维护“本局面下走出这一手的胜负”外还维护一个“任意局面下走出这一手的胜负”的全局统计两者加权融合作为UCB的选择依据。我在一个简化版四子棋里做了测试RAVE确实能显著减少早期搜索的随机性让AI在前几十手就集中在合理区域。但RAVE不是银弹它要求游戏存在“走法平移性”即同一手的价值在不同局面中具有一定相关性井字棋这种棋盘上平移性较弱收益不明显。6.3 从井字棋迁移到四子棋状态接口的复用最后说说迁移。把上面的TicTacToeState换成一个ConnectFourStateMCTS代码几乎不用改只需要保证新State类提供四个方法get_legal_moves()、apply_move()、is_terminal()、get_result(perspective)。我把这一套迁移到我另一个项目后四子棋AI在5000次迭代下已经能跟休闲玩家打得有来有回。这个迁移体验是我最想强调的一点MCTS作为算法框架它和具体游戏的耦合度非常低。你只要维护好状态转移接口的一致性剩下的事情几乎全是“调参加点启发式”。这让你在投入大量时间到某个复杂游戏之前可以先用这个框架快速做出一个能对战的原型验证游戏设计本身是否有趣。7. 调参对照表与最终建议调试了这么久的MCTS越到后期越清楚一个事实调参结果高度依赖你的具体游戏、棋力基线、时间预算和硬件环境。所以与其告诉你某个铁律不如整理一个我能放心推荐的初始参数表你照着跑一遍再根据自己的观察做微调。# 推荐初始参数 exploration_constant 1.414 # 小棋盘用1.2-1.6大棋盘可尝试2.0-3.0 iterations 1000 # 井字棋推荐1000-2000大棋盘可提升到10000 simulation_policy random # 先用纯随机后续可替换为启发式概率 先验权重 0 # 初期不要注入领域先验真要说有什么放之四海皆准的经验我觉得只有一条做一个调试可视化工具。不管是命令行打印树结构、还是把胜率最高的几个走法按热力图展示可视化带来的直觉判断速度远超看数字。我在调参时加了一个“胜率热力表”直接在棋盘上打印每个格子的平均胜率很快就能看出AI的偏好分布这个习惯我一直保留至今。项目做到现在我越来越觉得MCTS的精髓不是算法结构本身而是“用统计思维代替精确计算”这个哲学。你不需要知道一个局面的绝对价值你只需要在大量的抽样中把这个价值估算得越来越准并用它引导搜索方向。这与我们做很多工程决策时的“贝叶斯式”思维惊人地相似小步快跑、频繁反馈、动态调整。我个人在实际操作中的体会是如果你正在做任何涉及“在巨大决策空间中选一个动作”的AI项目MCTS绝对是最值得先试的框架之一。它不需要专家知识、实现成本低、还能随时中断这种特质让它天然适合做原型验证。而井字棋这个Demo虽然简单却把MCTS的所有关键机制都暴露得清清楚楚拿它练手比直接啃围棋论文要实用得多。最后再分享一个小技巧如果你在实验中发现AI的落子时好时坏先去检查它每次搜索的根节点是不是从零开始建树。如果根节点每次都新建AI只能看到当前局面这一个视角如果你想让AI对某个特定局面有“记忆”可以把历史搜索保留的子树直接挂到新的根节点下这样就能用之前算过的统计信息加速收敛。这种“增量搜索”的玩法留给正在读文章的你自己探索吧。
RELATED

相关推荐

Flutter跨端视频列表应用:OpenHarmony播放器适配与性能优化实践

Flutter跨端视频列表应用:OpenHarmony播放器适配与性能优化实践

视频类应用开发现场,十次有九次会遇到同一个问题:一套逻辑,三种设备,要同时保交付质量和开发效率。这次我面对的目标,是让一个 Flutter 编写的视频列表应用,跑在包括 OpenHarmony 在内的多个系统上&#xf…

📅 2026/10/17 19:10:17
LedshowTW DLL二次开发指南:从环境配置到避坑实战

LedshowTW DLL二次开发指南:从环境配置到避坑实战

简介:LedshowTW 2014 二次开发包DLL动态库,是仰邦(BX)LED显示屏控制软件面向应用开发者的扩展接口集合。它通过DLL封装屏幕内容更新、亮度调节、颜色变换、播放效果等核心API,帮助开发者摆脱底层协议细节,快…

📅 2026/10/17 19:10:17
SpringMVC请求链路全拆解:从DispatcherServlet到视图渲染

SpringMVC请求链路全拆解:从DispatcherServlet到视图渲染

简介:这是一份面向 Java Web 初学者的 SpringMVC 入门项目,通过一个经典的 Hello World 示例,完整演示了 SpringMVC 框架从请求接收到视图响应的核心流程。项目采用 MVC 分层架构,涵盖前端控制器 DispatcherServlet、处理器映射器…

📅 2026/10/17 19:10:17
MORE NEWS

更多资讯

📰

DeepSeek-R1:面向中文逻辑推演的可验证推理引擎

简介:本资源是北京大学DeepSeek系列研讨讲座的配套讲义《DeepSeek与AIGC应用》,面向AI初学者、教育科研人员及AIGC工具实践者,旨在破除仅停留在操作层面的浅层认知,系统厘清DeepSeek-R1推理模型的技术原理、能力边界与真实应用场景…

📰

TSM6.3+Mhvtl搭建虚拟带库实现Oracle备份与异机恢复

简介:面向企业级备份/灾备工程师,这份原创文档系统演示了在 CentOS 6.5 环境中,如何用 TSM 6.3 结合 Mhvtl 开源虚拟带库完成备份恢复测试。全文围绕四部分展开:Mhvtl 的编译安装与带库可用性验证、TSM 主服务器端配置&#xff08…

📰

SC850SL 图像传感器调试实战:从上电时序到 MIPI 出图

简介:SC850SL数据手册面向安防监控、网络摄像机与车载摄像头领域的硬件工程师、驱动开发者及图像调试人员,帮助其快速掌握这款800万像素CMOS传感器的寄存器配置、时序控制与系统集成方法。资源包共1个文件,为PDF格式的数据手册,压…

📰

MATLAB实现量子近似优化算法:组合优化与最大割问题详解

简介:面向需要求解旅行商、背包、图着色等组合优化问题的技术人员与科研人员,这份PDF文档讲解如何基于MATLAB实现量子近似优化算法(QAOA)。资源共1个文件,文件类型为PDF,资源包约4.01MB,文档支持…

📰

音视频开发入门:从采集到播放的完整链路与实操指南

音视频这个领域,刚接触的时候很容易被一堆名词砸晕:编码、封装、协议、推流、拉流、转码、解码、渲染,每个词单拎出来都能写一本书。但真正上手做项目之后你会发现,日常打交道的东西其实就那么几样,把核心链路跑通一遍…

📰

【AI 工作流】用 TaoToken 统一 Key 打通 Gemini、Claude Code 与 Codex 的配置实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬