ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

用MCTS+策略价值网络实现五子棋AI:从零到实战

用MCTS+策略价值网络实现五子棋AI:从零到实战 简介这是一套基于蒙特卡洛树搜索与策略价值网络的强化学习五子棋AI实现面向对博弈AI、强化学习感兴趣的开发者与研究者提供从零训练到人机对弈的完整代码。资源采用Python编写共24个文件以10个Python脚本为主分别实现策略价值网络兼容Keras、PyTorch、TensorFlow及Numpy、蒙特卡洛树搜索纯搜索与AlphaZero式、自我对弈训练流程和人机交互界面并附有4个不同棋盘规模的训练好的模型参数、配置文件和说明文档压缩包仅1.64MB轻量易用。目前已有1940人学习代码经过调试可直接运行适合作为课程设计、毕业设计或强化学习入门实践的参考。通过阅读源码与运行脚本可直观理解蒙特卡洛树搜索与深度神经网络如何协作实现从零学习五子棋博弈并能将这种自我对弈、策略优化的思路迁移到其他棋类或复杂决策任务中。1. 为什么五子棋AI值得自己动手写一套MCTS策略价值网络五子棋看起来比围棋简单得多——15路棋盘、落子规则只有一条“五连即胜”。但真要写一个能下过业余强手的AI你会发现一个尴尬的事实暴力搜索撑不住跳点剪枝又容易漏算。你没有必胜开局库也没有线上平台的海量人类棋谱可用。这时候蒙特卡洛树搜索MCTS配合策略价值网络快策略头预测落子概率、价值头评估局面好坏的思路就能从强化学习的公式里走出来变成你可以一行行在自己笔记本上跑通的算法。三五百次模拟就足以吊打浅层剪枝加上一个训练过的网络之后水平还能再上一个台阶。这篇文章就按“树搜索→网络结构→自博弈训练→验证调优”的顺序把全套思路和可直接改用的代码讲清楚。适合已经写过基础递归搜索、想迁移深度学习方法的开发者。2. 蒙特卡洛树搜索核心选择、扩展、模拟与回溯从零实现2.1 MCTS为什么能替代暴力搜索五子棋单回合合法动作上限大约在225个15×15棋盘空点全填满比国际象棋约35个合法动作多出一个量级却远低于围棋的250个左右。用α-β剪枝做深度搜索层数一深就指数爆炸而人类下棋时脑子里其实只计算少数关键变化于是就有了“用随机采样代替全盘搜索”的思路。MCTS每次迭代都从根节点出发沿着某个策略走到叶节点再通过模拟rollout或者价值网络评分来估计该局面的胜率然后把这个估计值沿着路径反向传播回所有经过的节点。迭代次数越多搜索树中那些“总能让胜率上升”的动作的访问次数就会越越多。你可能听过“强化学习rollout”的说法在基础MCTS里它就是“走到叶后随机落子直到终局”的那个过程而在AlphaGo类的混合架构里它被价值网络取代了原因在于老式随机模拟方差太大一盘五子棋需要上千次模拟才能有稳定胜率估计而价值网络一次前向推理就能输出。2.2 节点对象父节点、先验概率与累计价值动手写MCTS之前先把数据结构定好不建议直接用字典散装记录每个节点的状态因为搜索过程中需要频繁更新子节点的访问次数、累计价值和平均价值。我一般定义一个MCTSNode类字段包括当前棋盘快照、对应的动作、以及后文会提到的“先验概率P(s,a)”——最开始网络还没介入时先验概率可以统一设为1/合法动作数这叫均一先验。关键代码是下面这一段它既要在自博弈游戏中进行树搜索也要在最终对局不带探索时复用。import numpy as np from copy import deepcopy class MCTSNode: def __init__(self, parent, action, prior, board): self.parent parent # 父节点根节点的父节点是None self.action action # 到达该节点所执行的落子位置 self.prior prior # 来自策略网络或均一先验的P(s,a) self.board board # 当前棋盘状态副本 self.children {} # 子节点字典键是动作索引 self.visit_count 0 # N(s,a) self.total_value 0.0 # W(s,a) self.value 0.0 # Q(s,a) W / N def is_expanded(self): return len(self.children) 0这段代码背后的算力权衡是每个节点都持有一份棋盘副本空间开销会随着搜索树深度线性增长。但换来的好处是在回溯阶段可以直接看节点自己记录的棋盘状态省去“撤销落子”这种状态管理逻辑。如果你用Python写这一步不优化问题也不大15×15的棋盘可以用一个二维数组表示一次棋盘拷贝在微秒级别。搜索1000次迭代大概产生数百个节点内存占用在几十MB以内。2.3 选择阶段UCT公式与温度参数选择阶段的数学核心是UCT公式它决定“从当前节点应该拐向哪个子节点”。设当前根节点访问次数为N_parent某个子节点c的访问次数为N_c那么它的得分是UCT Q(s, c) CPuct * P(s, c) * sqrt(N_parent) / (1 N_c)其中Q(s, c)是“走到c之后按当前策略估计的胜率”取值在0到1之间P(s, c)是先验概率CPuct是探索常数通常设为1.0到5.0之间。这个公式和强化学习中常见的UCB公式同源区别就在于先验概率乘以了一个根号项让“网络认为前景好但还没被充分探索”的动作也有机会被选中。def select_child(node, cpuct2.5): best_child None best_score -float(inf) total_n sum(child.visit_count for child in node.children.values()) for child in node.children.values(): q_value child.value if child.visit_count 0: q_value 0.0 # 未访问的节点价值先落0 # UCT核心先验概率加根号探索项 exploration cpuct * child.prior * np.sqrt(total_n) / (1 child.visit_count) uct_score q_value exploration if uct_score best_score: best_score uct_score best_child child return best_child参数CPuct不能在训练前期设得太小否则马上会陷入“网络自己的偏见”一旦策略头输出某个动作概率为0.9几乎所有迭代都会涌向那个分支搜索树就失去了多样化。我一般从2.5起步模型训练到中期再降到1.5。你可能看过GPT之类的大模型预训练里也提到“探索和利用的权衡”MCTS这里的做法更直接——把探索项做成权重随着总访问次数变大而减弱。2.4 扩展、模拟与回溯的完整循环一次MCTS迭代跑完四步。第一步select第二步如果到达的节点还没展开就扩展它枚举当前棋盘全部空位对每个动作给出先验概率生成子节点挂到children里第三步评估新扩展出来的那个节点的胜率。如果手头有策略价值网络就做一个前向推理拿价值head输出v否则就用随机落子到终局的轻量级模拟来估计胜率第四步把v从叶节点一路回传更新所有经过节点的total_value和visit_count。def run_mcts_simulation(root, env, networkNone, simulations400, cpuct2.5): root.visit_count 1 node root path [node] while node.is_expanded(): if len(node.children) 0: break node select_child(node, cpuct) path.append(node) # 检查当前节点是否终局 winner env.check_winner(node.board) if node.is_expanded() and winner is None: # 已展开过的就继续选择不二次扩展 pass elif winner is None and network is not None: # 用网络预测价值和策略叶节点评估 policy, value network.predict(node.board) expand_node(node, policy) leaf_value value elif winner is not None: # 终局1胜 0平 -1负 leaf_value 1.0 if winner env.player_from_board(node.board) else -1.0 if winner 0: leaf_value 0.0 else: # 有网络但仍走到这一步随机模拟兜底 leaf_value rollout(node.board, env) # 回溯路径上所有节点更新总价值和访问次数 for n in reversed(path): n.total_value leaf_value n.visit_count 1 n.value n.total_value / n.visit_count这段代码里有个容易被忽略的细节env.player_from_board是根据当前轮到谁下棋来判断节点的“立场”的。因为五子棋是双方轮流落子MCTS在沿路径回溯时不同层节点的value参照系是不同的——奇数层是黑方视角偶数层是白方视角。最简单的处理方式是在回溯时不翻转符号而是让邻近两层的UCT公式里Q值沿用不同符号。上面示例中省略了翻转实际工程里要加一行leaf_value -leaf_value当隔层回溯。否则你会训练出一个永远觉得“对面要赢”的神经网络。不少开源五子棋项目就栽在这个符号问题上产生看起来没什么逻辑的胜率震荡。提示如果你直接把网络输出的价值给root网络输出的是“当前player视角的胜率”那么在回溯过程中每跨越一层就要把value取反。用“交替符号”的方式最简单即路径第0层用正v、第1层用负v、第2层又用正v。3. 策略价值网络结构设计从棋盘编码到策略头与价值头3.1 输入不再是“图片”五子棋状态的张量设计很多初学者会直接把15×15的棋盘当作灰度图喂给卷积网络但这样做会丢掉两个关键信息一是当前该轮到谁落子二是历史落子的先后顺序连珠形状、禁手判断都依赖这些信息。我常用的做法是构造一个N通道的张量通道按“落子历史”排列第0个通道表示“当前玩家最近一步的棋子”第1个通道表示“对手最近一步的棋子”第2和第3个通道是“倒数第二步、倒数第三步”的棋子位置以此类推。还可以加上一个全1或全0的“轮次通道”来告诉网络当前是黑先还是白先。这个设计在AlphaGo Zero论文里被称为“状态表示栈”MCTS在搜索树扩展子节点时不需要重新走一遍棋谱只需要读取这个张量所以速度也快得多。具体到代码里我用一个15×15×8的张量。第0到第3通道索引当前玩家的落子历史第4到第7通道索引对手的历史轮流交替意义。空通道补0。这个设计在强化学习五子棋场景下完全够用如果你后续加双三禁手判断再把通道扩到12多加4个通道给禁手标记即可。def encode_board(board, current_player): # board: 15x15, 1表示黑子-1表示白子0表示空 # current_player: 1或-1表示当前轮到谁 planes np.zeros((8, 15, 15), dtypenp.float32) history get_recent_moves(board) # 返回最近7步的落子位置列表 for i, (r, c) in enumerate(history[:4]): planes[i, r, c] 1.0 for i, (r, c) in enumerate(history[4:8]): planes[4 i, r, c] 1.0 # 当前玩家通道再整体叠加一个“轮次”标记 if current_player 1: planes[7, :, :] 1.0 return planes这里的通道顺序其实可以自己定义但要注意“当前玩家”和“对手玩家”的棋子必须分通道放否则网络要花很大力气才能从同一堆像素里区分敌我。这和围棋AI里的典型做法完全一致也是最容易直接迁移到其他棋类游戏上的编码方式。3.2 网络主体残差卷积块与共享特征层策略网络和价值网络可以合并成一个共享特征提取器只在最后分成两个头head。这样在自博弈的时候一次前向计算同时得到策略分布和局面价值MCTS每个叶节点只做一次推理就能拿到两个输出省掉一半计算量。光学上看这个共享结构也很自然前面若干层学到的“局部形状”、“连珠威胁”等信息对策略预测和胜率估值都是有用的没必要各学一套。我用的基础网络是一个简化版ResNet。15×15输入先过一个3×3卷积层把通道数升到64再加BatchNorm和ReLU然后是6个残差块每个块内部是两个3×3卷积、BatchNorm、ReLU残差连接跳过去加回来。这个深度对五子棋够了再深反而容易过拟合毕竟不像围棋有那么大规模的局面复杂度。import torch import torch.nn as nn import torch.nn.functional as F class ResidualBlock(nn.Module): def __init__(self, channels64): super().__init__() self.conv1 nn.Conv2d(channels, channels, 3, padding1) self.bn1 nn.BatchNorm2d(channels) self.conv2 nn.Conv2d(channels, channels, 3, padding1) self.bn2 nn.BatchNorm2d(channels) def forward(self, x): identity x out F.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) return F.relu(out identity)残差块里第一层卷积后先ReLU再做第二层卷积这是原版ResNet的顺序。你可以在实际训练中发现BatchNorm放在卷积前或卷积后会有细微差别但并不会有质变。五子棋局面局部信息比较稀疏我试验过把卷积核从3×3改成5×5胜率没明显提升反而训练内存翻倍。所以在15路棋盘上3×3是权衡后的推荐值。3.3 策略头与价值头的输出225个落子概率加一个胜率标量共享特征从最后一个残差块出来接两个分支。策略头把一个1×1卷积核降到2通道再展平成15×15×2的Feature Map最后通过一个全连接层映射到225维输出然后用LogSoftmax或者Softmax得到合法动作的概率分布。价值头则做得更简洁一个1×1卷积降到1个通道GlobalAveragePooling拉平后过两个全连接层输出一个标量用tanh把值压到[-1, 1]。这样设计的好处是策略头和价值头的参数量极低整个网络参数量大概在250万左右在纯CPU上也能跑得动推理。class GomokuPolicyValueNet(nn.Module): def __init__(self, board_size15, feature_channels64): super().__init__() self.board_size board_size self.conv_input nn.Conv2d(8, feature_channels, 3, padding1) self.bn_input nn.BatchNorm2d(feature_channels) self.blocks nn.ModuleList([ResidualBlock(feature_channels) for _ in range(6)]) # 策略头 self.policy_conv nn.Conv2d(feature_channels, 2, 1) self.policy_fc nn.Linear(2 * board_size * board_size, board_size * board_size 1) # 价值头 self.value_conv nn.Conv2d(feature_channels, 1, 1) self.value_fc1 nn.Linear(board_size * board_size, 64) self.value_fc2 nn.Linear(64, 1) def forward(self, x): x F.relu(self.bn_input(self.conv_input(x))) for block in self.blocks: x block(x) # 策略头 p F.relu(self.policy_conv(x)) p p.view(p.size(0), -1) p self.policy_fc(p) # 输出2251维最后一位是pass p F.log_softmax(p, dim1) # 价值头 v F.relu(self.value_conv(x)) v v.view(v.size(0), -1) v F.relu(self.value_fc1(v)) v torch.tanh(self.value_fc2(v)) return p, v策略头输出2251维多出来的那个维度是“pass”动作。正常对弈中pass相当于放弃落子、交换手权五子棋里通常没有pass规则但加上这个维度可以防止网络在搜索时出现“没有合法动作时返回空分布”的边界bug。你可以选择保留它在训练中隐隐充当“略微施加正则”权重一般会稳定落在0.001以下不影响对局。3.4 先验概率重加权用合法动作掩码防止模型作弊MCTS在扩展叶节点时拿到的策略输出必须跟“当前棋盘仍然存在的空位”对齐。模型可能预测在已经有棋子的位置落下高概率但这在规则上非法。处理方法是构造一个掩码向量合法位置置0、非法位置置-inf然后加到logits上再softmax。这套操作在强化学习里叫“合法动作掩码”几乎每个棋类AI都要做。def get_legal_mask(board): mask np.zeros(15 * 15 1, dtypenp.float32) for r in range(15): for c in range(15): if board[r, c] 0: mask[r * 15 c] 1.0 return mask def masked_policy(network, board, current_player): logits, value network.predict(board, current_player) mask get_legal_mask(board) logits[:, -1] -1e9 # 禁止pass否则AI可能跳过落子 masked_logits logits (mask - 1) * 1e9 probs torch.softmax(masked_logits, dim-1) return probs[0], value[0]注意掩码的尺寸是2251最后一个维度的pass强制设为非法。当棋盘接近满时合法动作越来越少softmax后的分布理论上会自然集中在剩余空位附近不会出现方差爆炸。提示这里有一个常见误区就是直接在策略输出后乘上掩码再归一化。这样做虽然也能得到合法分布但掩码位置的logits其实已经为负无穷自然不会对梯度产生影响。直接用softmax前的掩码加法更接近数学定义训练也更稳定。4. 强化学习训练闭环自博弈数据生成、损失设计与迭代训练4.1 自博弈是一种“永不枯竭的数据管道”策略价值网络蒙特卡洛树搜索的一个最大优势就是不需要人类棋谱。自己和自己下就产生数据双方都按MCTS的搜索策略加一些随机探索落子然后整局棋的每一步“落子前局面→落子概率分布→最终胜负”组成一条训练样本。这条样本里的落子概率分布是MCTS搜索结束时根节点子节点访问次数的归一化值不是网络预测的策略输出本身。访问次数越高说明多次搜索都认为这一步“有前途”相当于用大量试探镀了一层金。这就是标号质量远高于直接用策略网络输出的原因。自博弈代码通常按这样的流程跑开局初始化15×15空棋盘双方交替用MCTS搜索选择动作每走一手把(s, π, player)存成一条样本一局终局后黑方胜利就把该局所有“轮到黑方落子的样本”标为z1轮到白方的样本标为z-1白胜反向平局全为0。需要注意的是z必须站在落子玩家的视角写也就是说如果当前样本是“轮到白方落子”黑胜的时候这个样本的z-1。def self_play_game(network, env, simulations400, temperature1.0): train_data [] board np.zeros((15, 15), dtypenp.int8) current_player 1 move_count 0 while True: # 用MCTS搜索得到当前局面的策略分布π root MCTSNode(parentNone, actionNone, prior1.0, boarddeepcopy(board)) policy_probs, value_pred run_mcts(root, env, network, simulations) # 根据温度从策略分布中采样动作 action select_action_with_temp(policy_probs, temperature) # 存储训练样本需要把策略分布和当前玩家视角对应 train_data.append((deepcopy(board), current_player, policy_probs)) # 在真实棋盘上执行动作 r, c divmod(action, 15) board[r, c] current_player move_count 1 winner env.check_winner(board) if winner is not None or move_count 225: break current_player -current_player # 给整局所有样本附上决胜奖励 data_with_z [] for s, player, pi in train_data: z 0.0 if winner player: z 1.0 elif winner -player: z -1.0 data_with_z.append((s, player, pi, z)) return data_with_z这段自博弈代码里有一个参数值得解释temperature控制搜索策略的“锐利程度”。设为1.0时按MCTS返回的访问计数比例采样相当于更充分地探索当temperature趋近0时变成“永远选访问次数最多的动作”。训练初期用高温增加多样性训练后期为了积累高质量对局我会把temperature降到0.1甚至0.01。4.2 损失函数价值损失与策略损失的合理组合网络训练时同时做两件事让价值头的输出v尽量接近真实胜负z让策略头输出的概率分布p尽量接近MCTS搜索得到的更好的策略π。两者构成的总损失可以用这个式子表达L (z - v)^2 - π * log(p) λ * ||θ||^2第一项是均方误差损失第二项是交叉熵——注意p是log_softmax的输出所以用负号第三项是L2正则。这里的设计视角是价值网络在试图做回归策略网络在试图做“模仿学习”模仿目标是由MCTS搜索计算出来的π而不是固定的、浅层的棋谱数据。过强的策略损失会压制探索性过强价值损失则会让网络早期崩溃。我通常把两个损失权重都设为1.0L2系数设为1e-4整体优化使用Adam学习率从2e-3开始每2000步降为原来的0.1。def train_step(model, optimizer, data_batch, l2_coef1e-4): states batch_states_to_tensor(data_batch) # 转为张量 target_policies torch.stack([torch.tensor(d[2]) for d in data_batch]) target_values torch.tensor([d[3] for d in data_batch], dtypetorch.float32) log_probs, value_out model(states) value_loss F.mse_loss(value_out.view(-1), target_values) policy_loss -torch.mean(torch.sum(target_policies * log_probs, dim1)) l2_loss sum(p.pow(2).sum() for p in model.parameters()) * l2_coef total_loss value_loss policy_loss l2_loss optimizer.zero_grad() total_loss.backward() optimizer.step() return total_loss.item()初次看到这段代码的人可能会问交叉熵损失按理说应该用F.cross_entropy为什么在这里直接做逐元素相乘再取平均原因在于MCTS输出的π通常不是one-hot而是带小数概率的向量直接用one-hot交叉熵会丢失信息。逐元素相乘公式里π越大则模型在该位置的对数概率越小惩罚越大权重可以根据π动态调节这是标准做法。4.3 数据重放与训练轮次的节奏自博弈每下一盘都会产生几十条到上百条样本五子棋平均每盘60到80手继续拿着同一条数据连续反向传播网络会快速遗忘掉之前学到的内容。所以训练流程是跑完一局自博弈把本局所有样本扔进一个全局的replay buffer然后从buffer里随机抽一个批次更新网络参数。缓冲区设成足够大比如保留最近5000局的数据用完之后把旧样本丢掉避免过拟合。replay_buffer deque(maxlen200000) for epoch in range(1000): game_data self_play_game(network, env, simulations400, temperature1.0) replay_buffer.extend(game_data) batch random.sample(replay_buffer, 512) loss train_step(model, optimizer, batch) if epoch % 50 0: # 进行模型评估保留历史最好模型后面会专门讲 pass这里每次只新加一局自博弈就立刻从中采样训练称为在线强化学习的经典循环。好处是新数据分布紧跟当前模型水平缺点是会让网络朝“最近自己常常走出的棋形”偏移。如果你发现训练集里黑胜率一直是0.99说明温度太高导致随机噪声太多或者黑白开局不对称价值严重跑偏需要调小temperature并确认价值标签没有符号错乱。提示deque(maxlen...)这种环形缓冲非常适合自博弈场景旧样本自动弹出。但玩游戏时别把缓冲区塞太满否则模型更新速度追不上环境分布漂移表现为loss长时间不下降。一般设成单局样本数的500到1000倍是经验安全的范围。4.4 训练常见的三种失败模式与对策训练强化学习棋类AI尤其是五子棋这种绝杀多、棋局较短的棋种最常遇到三种情况。第一种“模式坍塌”策略网络某一时期反复输出“占天元”导致MCTS搜索很少探索别的位置数据很快单一化价值网络误以为天元开局必胜。对策是调高CPuct或者训练时强制禁止前几手落在中心点附近区域强制探索。第二种“价值过拟合”价值头输出对棋盘的具体棋形过于敏感常常出现“一步错判满盘皆输”。根源在损失权重分配太偏价值项可以把价值损失乘0.5。第三种“训练死锁”胜率长期卡在50%附近不动看起来好像每盘都势均力敌但下出来的棋毫无章法。这通常意味着温度过高随机性掩盖了网络学到的东西把温度随训练轮次从1.0线性降到0.05很快能看到策略变得有倾向性。5. 验证与调优离线评估、树宽平衡与实际对局排错技巧5.1 用新旧模型打锦标赛来评估迭代效果直接看loss值无法判断棋力涨跌正确的做法是让当前模型和上一轮保存的旧模型各自用MCTS自我对弈记录胜率。离线评估无需真实GUI直接跑一个模拟对局双方每步行数固定比如各模拟400次不设temperature纯按访问次数选动作。一般跑20盘取胜率黑胜率在55%到60%就算明显提升毕竟五子棋先手优势是存在的——如果后手胜率也飙升那就要考虑是不是网络在乱下了。def evaluate_models(env, model_a, model_b, games20, simulations400): wins_a wins_b draws 0 for g in range(games): board np.zeros((15, 15), dtypenp.int8) current_player 1 mov 0 while True: if current_player 1: action mcts_select_action(board, current_player, model_a, simulations) else: action mcts_select_action(board, current_player, model_b, simulations) r, c divmod(action, 15) board[r, c] current_player winner env.check_winner(board) if winner is not None: if winner 1: wins_a 1 elif winner -1: wins_b 1 else: draws 1 break current_player -current_player mov 1 if mov 225: draws 1 break return wins_a, wins_b, draws这段评估代码里故意不对双方做对称交换先后手的处理就是为了保留先手优势作为观察指标。如果你发现某个版本的后手胜率反而领先明显不合常理那就要怀疑模型是否学会了某种“双方都难察觉的必胜骗招”或者MCTS在双方视角间的价值符号搞错了。让两个版本黑白各下一半通过综合胜率来判断更稳妥。5.2 MCTS树宽与访问次数的工程调参经验MCTS的搜索效率跟“模拟次数”和“树深”密切相关五子棋一局通常只有30到60回合一盘225个空位按跳点落子数量大概60手左右树深不会太长主要瓶颈在于每层子节点数。用策略网络作先验之后子节点数量会集中在那些网络认为有棋威胁的地方所以搜索树宽往往比其他棋类窄很多。这时把模拟次数从100提高到400对单步棋的胜率提升非常明显但超过1000次后会趋于收敛毕竟五子棋局面随机性较小价值网络本身估计得够好了。调参方面我推荐这样一组起点训练阶段CPuct2.5模拟次数400temperature1.0评估阶段CPuct2.0模拟次数800temperature0。如果你用CPU推理800次模拟大约每手消耗1.2秒到2秒已经能打败多数业余玩家。树搜索的计算瓶颈主要在策略网络的前向推理上所以可以在选择阶段加一个“环缓存”对于同一个棋盘的多个展开节点把网络推理结果临时存下来避免重复计算。另一种常见的“深度剪枝”做法是当节点访问次数超过阈值时就提前截断不再继续往下搜直接返回当前节点的平均价值这在训练早期可以明显提速。5.3 一个压缩版验证只看单轮对战的胜负走势如果你只想快速判断模型是否“活了”不用打满20盘可以直接观察单盘对局的落子分布情况。一个正常训练完的模型应该表现出前几手集中下在棋盘中央区域中盘阶段避免长距离跳跃的散子接近终局时会精准找到直接连五的位置。如果模型第一盘就疯狂往边界落子、对明显的三连视而不见多半是价值头坍缩成了“恒定输出0”这时检查一下训练数据的z符号是否随player反转即可不用急着改网络结构。把评估脚本挂到训练循环里每1000局自博弈跑一次拿胜率变化曲线做模型保存依据比用一个固定局数存盘可靠得多。本文还有配套的精品资源点击获取
返回列表