简介:一套基于AlphaZero方法构建的中国象棋强化学习项目代码包,面向想动手复现棋类AI的深度学习开发者、强化学习研究者及业余象棋AI爱好者。项目参考DeepMind AlphaZero论文与多个开源实现,在Python版中国象棋环境中集成神经网络训练、蒙特卡洛树搜索及GUI对弈界面,帮助理解自对弈强化学习的完整流程。
包体共169个文件,约29.15MB,以Python脚本(49个py)为核心,涵盖训练、模型、棋局逻辑等模块;104个gif为棋谱动画或界面演示;另有json配置、md说明、png示意图及h5模型权重等,便于按需阅读与运行。目前已有1850人学习/下载。
资源内附带分布式训练指引和实时状态说明,适合希望进一步扩大算力协作训练的用户;结合预览中的多张棋谱动画,可直观看到AI对弈过程,快速验证训练效果,也可基于现有代码改进网络结构或探索其他棋类项目。
1. 在中国象棋上复现 AlphaZero:零知识强化学习,单机到底能不能跑?
去年我把围棋版 AlphaZero 复现跑通之后,顺手把同样的套路丢到中国象棋上试了一次,结果第一盘自我对弈就翻了车:红方用炮打了黑方的马,黑方完全不躲,下一步直接把帅走到和对方将同一条直线的位置。问题不是网络太笨,而是我的棋盘环境把规则写漏了。ChineseChess-AlphaZero 这个方向,核心不是「复现一个强大的棋手」,而是验证 AlphaZero 的零知识方法能不能在另一套棋类规则上靠自己长出棋力。中国象棋状态空间比围棋小一个量级,规则却更琐碎,单机加一块普通 GPU 就有机会跑出像样的结果。这篇笔记从体面编码、动作掩码、双头网络、MCTS 到训练闭环,把能直接抄的代码和踩过的坑一起摊开。
2. 零方法凭什么立得住:自举闭环与中国象棋的适配性
2.1 零知识不等于零规则:AlphaZero 真正需要的是什么
很多第一次接触 AlphaZero 的人会把「零」理解成「什么都不给,让 AI 凭空学会下棋」。实际不是。AlphaZero 的 zero 指零人类知识:不需要棋谱、不需要人工设计的局面特征、不需要开局库,但规则是硬编码进环境的。它真正需要的东西只有三样:一个能返回合法走法的棋盘状态表示、一套终局判定逻辑、一个不断更新的神经网络。规则越精确,AI 学的棋越像这门棋,而不是某种「四不像」博弈。
中国象棋恰好适合做这个实验,原因是它的棋盘小、状态有限。10 乘 9 的格子一共 90 个交叉点,对比围棋 19 乘 19 的 361 个交叉点,网络输入和搜索负载都小一个量级。但这不意味着问题更简单。中国象棋的规则约束密度很高:马有蹩马腿,象有塞象眼,炮要吃子必须先跳架子,将帅只能在九宫里移动,双方将帅不能直接照面。每一条规则都在缩小合法动作空间,也让神经网络更容易学到「什么局面能动、什么局面不能动」。把这些规则写进环境之后,剩下的就可以完全交给自我对弈。
2.2 先验随机也敢自举:MCTS 如何把烂先验拉回正轨
AlphaZero 的自举循环可以被压缩成四个步骤:神经网络对当前局面输出一个先验策略 P 和一个局面价值 V;蒙特卡洛树搜索用 P 引导探索,通过不断模拟对局得到改进后的策略 π;自我对弈用 π 去走棋,得到最终胜负结果 z;训练目标被更新为让 P 逼近 π、让 V 逼近 z。关键在于,即使网络一开始的 P 完全是随机权重给出的,MCTS 也会通过「多搜几步、多看几个后续局面」把随机性平滑掉,输出的 π 会比原始 P 靠谱一点,哪怕只靠谱一点点。
这一点点进步在围棋上已经验证过一轮:从随机权重出发,AI 在完全不知道人类定式的情况下,学会了星位、小目这些下法。中国象棋的规则约束更强,合法走法数量平均只有几十种,MCTS 的搜索深度能比围棋更深,所以自举的「信号质量」理论上比围棋更好。真正容易出问题的反而是第二步之后的工程细节:数据怎么存、权重多久更新一次、重复局面怎么判。这些坑后面专门开了一章,先不展开。
2.3 围棋搬到中国象棋:动作空间变小,规则器反而更重
AlphaZero 原版处理的是围棋、将棋、国际象棋三合一,三个棋类共用一套代码框架。把代码从围棋切到将棋,改动最大的是动作空间定义和终局判定。围棋落子就是「在某个交叉点放一颗子」,动作空间是 19 乘 19 等于 361 维。国际象棋和将棋是「从格子 A 移动到格子 B」,动作空间按原版的处理方式是起点乘终点,维度暴涨。中国象棋同样属于移动类棋,动作空间需要按这个思路重新建模。
另一点差异容易被忽略:中国象棋的终局类型比围棋多。围棋只有「单官收完比目」或中盘认输,平局罕见。中国象棋有将死、困毙、自然限着和循环重复。一方无子可动但没被将军,按规则算困毙,直接判负,这种局面如果环境不识别,AI 会以为还能继续走,并且永远等不到终局信号。将帅照面、长将循环这些规则,如果不在合法走法生成器里拦掉,网络会把「送将」学成合理着法。所以复现中国象棋 AlphaZero 的真实工作量,一大半在规则器上,另一大半在数据管线。网络本身反而是最简单的部分。
3. 把中国象棋棋盘变成张量:局面编码与动作掩码的取舍
3.1 特征平面设计:11 类棋子拆成 14 个通道,外加 4 个全局通道
围棋版 AlphaZero 用 17 个特征平面,把「当前玩家视角」编码进网络输入。中国象棋这里沿用同一个思路,但棋子类别更多,需要重新设计通道布局。中国象棋有红黑两方,每方 7 种棋子:帅、仕、相、马、车、炮、兵,合计 14 种角色。我采用的做法是给每种角色单独一个平面,同时把棋盘按「当前轮到的一方」做视角切分:当前方的 7 种棋子占 7 个通道,对方的 7 种占另外 7 个通道,再加 4 个全局平面,总共 18 个 10 乘 9 的平面。以下是编码函数的核心实现。
import numpy as np BOARD_H, BOARD_W = 10, 9 PIECE_TYPES = ['K', 'A', 'B', 'N', 'R', 'C', 'P'] # 帅、仕、相、马、车、炮、兵 def encode_board(board, side_to_move, repetition_count, last_move_was_capture): """ board: 10x9 的字符数组,'RK' 表示红帅,'BN' 表示黑马,'.' 表示空 side_to_move: 'R' 或 'B' 返回 shape=(18, 10, 9) 的 np.ndarray """ planes = np.zeros((len(PIECE_TYPES) * 2 + 4, BOARD_H, BOARD_W), dtype=np.float32) for row in range(BOARD_H): for col in range(BOARD_W): piece = board[row][col] if piece == '.': continue color, ptype = piece[0], piece[1] if color == side_to_move: idx = PIECE_TYPES.index(ptype) * 2 # 当前方棋子通道 else: idx = PIECE_TYPES.index(ptype) * 2 + 1 # 对方棋子通道 planes[idx, row, col] = 1.0 planes[14] = 1.0 if side_to_move == 'R' else 0.0 # 轮到红方 planes[15] = 1.0 if is_in_check(board, side_to_move) else 0.0 planes[16] = 1.0 / (1.0 + repetition_count) # 重复局面次数,归一化 planes[17] = 1.0 if last_move_was_capture else 0.0 # 上一步是否吃子 return planes这段代码把每个棋子按「当前玩家视角」落进 10 乘 9 平面。通道 14 和 15 分别标记行棋方和是否被将军,通道 16 给重复局面一个连续的归一化值,通道 17 记录上一步是否吃子。这里最容易忽略的是视角问题:不要用红方视角做全局固定编码,而是始终以「当前轮到的玩家」为主视角。否则同一个局面轮到红方和轮到黑方会被表示成两套完全不同的张量,网络需要额外学一个「颜色翻转」的映射,训练效率会明显下降。
3.2 动作掩码与索引映射:为什么不能照抄围棋的落子矩阵
围棋的策略头输出一个 361 维概率分布就够了,因为落子就是选一个点。中国象棋是「从格子 A 移动到格子 B」,如果按起点 90 乘终点 90 展开,动作空间是 8100 维,但其中绝大多数动作非法。直接在这个空间上做 softmax,网络必须花大量容量去学「哪些动作永远不会合法」,等于把规则重新学一遍,纯属浪费。常见做法有两种:一种是原版将棋采用的固定动作列表,另一种是只在合法走法上做窄 softmax。中国象棋的合法走法平均只有几十种,我推荐后者,索引映射代码可以这样写。
MAX_MOVE_INDEX = 90 * 90 # 8100,起点格子id * 90 + 终点格子id def encode_legal_moves(moves): """ moves: [(from_row, from_col, to_row, to_col), ...] 返回 (index_to_move, policy_mask) index_to_move: 把稀疏动作变成稠密索引的映射表 policy_mask: shape=(8100,) 的 0/1 掩码 """ move_to_index = {} index_to_move = [] for move in moves: fr, fc, tr, tc = move move_idx = (fr * 9 + fc) * 90 + (tr * 9 + tc) if move_idx not in move_to_index: move_to_index[move_idx] = len(index_to_move) index_to_move.append(move) policy_mask = np.zeros(MAX_MOVE_INDEX, dtype=np.float32) for move in moves: fr, fc, tr, tc = move move_idx = (fr * 9 + fc) * 90 + (tr * 9 + tc) policy_mask[move_to_index[move_idx]] = 1.0 return index_to_move, policy_mask这里把每个走法编码成一个 0 到 8099 的整数下标,再用掩码标记当前局面哪些下标合法。策略网络输出的 8100 维 logits 在 softmax 前,把非法位置的 logits 置为负无穷,模型只需要在合法动作上分配概率。index_to_move 表同时承担了「概率索引到实际走法」的反查功能,MCTS 选中某个动作后可以直接从表里取出落子坐标。相比直接在合法走法列表上做 softmax,这种写法的好处是 mini-batch 训练时张量形状固定,不用处理变长序列,代价只是浪费一点显存。
3.3 独有规则干扰编码:将帅照面、困毙与重复局面怎么进张量
编码只是把棋盘搬进张量,真正的分水岭在合法走法生成器。中国象棋三条特殊规则必须在这里处理干净。第一条是将帅照面:两将之间没有任何棋子时,移动造成这种局面直接非法。第二条是困毙:轮到某方且无任何合法走法时,该方判负。第三条是重复局面:三次重复按规则判和或判负,MCTS 搜索时必须能识别。将帅照面检测不复杂,就是沿着将帅所在的列扫描中间是否有子,但容易漏。
def is_king_face_to_face(board): red_king = find_piece(board, 'RK') black_king = find_piece(board, 'BK') if red_king[1] != black_king[1]: # 不在同一列 return False col = red_king[1] row_min = min(red_king[0], black_king[0]) row_max = max(red_king[0], black_king[0]) for row in range(row_min + 1, row_max): if board[row][col] != '.': return False return True这段函数检查两个将帅是否在同一列且中间无棋子。每次走子后先做这个检测,如果走完导致照面,该走法必须从候选列表里删掉。困毙判定则是在每次轮到一方行棋时,如果合法走法列表为空,直接返回该方输。这两条规则很多开源实现都漏掉,漏掉的后果不是棋力变弱,而是 AI 学到的根本不是一个合规的中国象棋棋手,后面在避坑章会展开讲。
4. 双头网络与 MCTS 搜索:让评估和搜索互相成就
4.1 网络结构:小残差塔同时输出策略与价值
中国象棋的输入是 18 通道 10 乘 9,比围棋小很多,不需要原版 20 个残差块的规模。我通常用 8 个残差块起步,每块 64 个卷积核,主干输出的特征图分别接策略头和价值头。策略头先做 1 乘 1 卷积降到 32 通道,再展平接一个全连接层输出 8100 维 logits;价值头同样先卷到 32 通道,展平后接 64 维全连接,最后用 tanh 压缩到负一到正一。完整的网络定义如下。
import torch import torch.nn as nn import torch.nn.functional as F class ConvBlock(nn.Module): def __init__(self, channels): super().__init__() self.conv = nn.Conv2d(channels, channels, 3, padding=1, bias=False) self.bn = nn.BatchNorm2d(channels) def forward(self, x): return F.relu(self.bn(self.conv(x)), inplace=True) class ResBlock(nn.Module): def __init__(self, channels): super().__init__() self.conv1 = nn.Conv2d(channels, channels, 3, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(channels) self.conv2 = nn.Conv2d(channels, channels, 3, padding=1, bias=False) self.bn2 = nn.BatchNorm2d(channels) def forward(self, x): residual = x x = F.relu(self.bn1(self.conv1(x)), inplace=True) x = self.bn2(self.conv2(x)) return F.relu(x + residual, inplace=True) class ChessNet(nn.Module): def __init__(self, blocks=8, channels=64, num_moves=8100): super().__init__() self.head = ConvBlock(channels) self.blocks = nn.Sequential(*[ResBlock(channels) for _ in range(blocks)]) self.policy_conv = nn.Conv2d(channels, 32, 1) self.policy_fc = nn.Linear(32 * 10 * 9, num_moves) self.value_conv = nn.Conv2d(channels, 32, 1) self.value_fc1 = nn.Linear(32 * 10 * 9, 64) self.value_fc2 = nn.Linear(64, 1) def forward(self, x): x = self.head(x) x = self.blocks(x) p = F.relu(self.policy_conv(x), inplace=True) p = self.policy_fc(p.flatten(1)) # (batch, 8100) v = F.relu(self.value_conv(x), inplace=True) v = self.value_fc1(v.flatten(1)) v = torch.tanh(self.value_fc2(v)) # (batch, 1) return p, v主干用残差结构让梯度跨层传导更顺,策略头顶部没有自带 softmax,因为训练时的交叉熵损失会内部做 softmax。价值头的 tanh 输出对应胜率,负一代表当前玩家必败,正一代表必胜。块数和通道数这两个参数值得注意:8 块 64 通道是我在单卡环境下的折中选择,显存够的话可以升到 10 块 96 通道,但再往上收益很小,因为中国象棋局面复杂度有限。
4.2 节点设计与选择公式:UCT 里每个变量都影响棋力
MCTS 节点需要记录五样东西:先验概率 P、访问次数 N、累计价值 W、平均价值 Q、子节点字典。搜索时从根节点出发,逐层选择得分最高的子节点,直到叶子节点,然后用网络评估叶子节点并展开。选择公式沿用 AlphaZero 的 PUCT:得分等于 Q 加一个探索项,探索项由先验概率和父节点访问次数共同调节。实现代码如下。
import math class MCTSNode: def __init__(self, prior): self.prior = prior self.N = 0 self.W = 0.0 self.Q = 0.0 self.children = {} def select_best_child(node, c_puct): total_n = sum(child.N for child in node.children.values()) best_score = -float('inf') best_action = None best_child = None for action, child in node.children.items(): exploration = c_puct * child.prior * math.sqrt(total_n) / (1 + child.N) score = child.Q + exploration if score > best_score: best_score = score best_action = action best_child = child return best_action, best_child这里 c_puct 是探索常数,控制先验概率和实际胜率之间的权重。c_puct 设太大,搜索会过度相信网络先验,忽略对局真实反馈;设太小,搜索会变成纯随机探索,网络评估信号用不上。原版围棋默认 2.5 左右,中国象棋我习惯从 2.5 开始调,效果不好再往 1.5 方向降。节点在搜索结束后会把叶子节点的价值评估沿路径回传,每个经过节点的 W 都累加,N 都加一,这是搜索信息被逐步修正的核心机制。
4.3 搜索次数、温度与噪声:三个参数定调数据质量
自我对弈阶段的数据质量由三个参数共同决定,调参顺序比具体取值更重要。第一个是每次走子前的搜索次数 simulations,第二个是最终走子采样用的温度 temperature,第三个是根节点加 Dirichlet 噪声的强度。它们的目标是同一个:在「充分利用当前网络」和「探索未见过的局面」之间找平衡。下面这张参数表是我在单机环境里跑得比较顺的起点。
| 参数 | 训练期自我对弈 | 评估期/正式对局 | 说明 |
|---|---|---|---|
| simulations | 400 到 800 次 | 800 到 1600 次 | 太少则先验主导,太多则数据产出慢 |
| temperature | 开局 1.0,逐渐降到 0.3 以下 | 固定 0 | 训练期保持探索,评估期完全贪心 |
| Dirichlet 噪声 | alpha 0.3,权重 0.25 | 不加 | 只加在根节点,避免开局完全重复 |
搜索次数直接决定每一步的决策质量。400 次是单机 CPU 多头并行下的务实选择,能跑 800 次时数据质量会有可见提升,但训练周期也翻倍。温度从 1.0 逐步降到 0.3 是为了让前期对局覆盖更多开局变化,后期逐渐收敛到确定性走法。Dirichlet 噪声加在根节点的先验上,alpha 取 0.3 会让概率分布有适度扰动,避免同一网络反复走出同一盘棋导致经验池多样性崩塌。
5. 自我对弈与训练闭环:让经验池成为真正的学习燃料
5.1 数据流设计:一局棋如何变成一批训练样本
自我对弈的产出不是「胜者列表」,而是每一步棋的完整轨迹。每走一步,要记录三样东西:落子前的局面张量、根节点搜索后得到的改进策略 π、以及整局游戏最终结果 z(胜 1、负 -1、和 0)。这三样绑成一个样本放进经验池。注意 z 的价值是对「当前走子方」而言的,局面张量也是按当前玩家视角编码,所以训练时价值头的目标就是 z 本身,不需要再做颜色翻转。一局棋通常几十步,会产生几十个样本。
经验池容量需要限制,不能无限累积。原版 AlphaZero 用了一个固定容量、不断淘汰旧数据的滑动窗口。原因很直接:网络权重在持续更新,早期版本的烂棋数据对新网络来说已经没有学习价值,留着反而干扰分布稳定。我一般把容量设在一万局左右,每完成一批自我对弈就训练几百个 batch,训练完不立刻让对弈进程更新权重,而是攒到一定局数再同步一次,避免训练震荡被自我对弈放大。
5.2 训练目标:策略损失加价值损失,再加一点点正则
AlphaZero 的损失函数由三部分拼起来。价值部分用均方误差衡量网络估分和真实结果之间的距离;策略部分用交叉熵衡量网络输出概率和 MCTS 搜索输出 π 之间的差距;最后加一个 L2 权重正则抑制过拟合。这个组合的含义非常直观:价值头负责回答「现在谁赢面大」,策略头负责回答「如果搜了很多步,哪一步更值得走」。MCTS 的 π 比网络原始输出的 P 更可靠,两者之间的差距就是网络要学习和改进的方向。
训练的 batch 里要混合不同局面。不能只取同一盘棋的连续局面部,否则网络会被这一盘棋的局部模式带偏。我习惯在采样时加一个限制:同一个 batch 中最多允许来自同一局棋的样本占百分之二十。这个细节经常被忽略,但它的作用是让梯度方向更接近全局分布,而不是被某几盘特殊棋局主导。学习率用余弦退火从 0.001 开始降,训练中期不要手动打断,让退火曲线跑完一轮完整的自我对弈周期。
5.3 评估与保存:不要只看训练 loss,要定期让新旧权重对对弈
训练曲线下降不代表棋力提升,这是强化学习项目里最容易出现错觉的地方。loss 反映的是网络对「旧版本自己产生的数据」的拟合程度,数据本身就在变,所以 loss 接近零并不能说明任何实际棋力。可靠做法是每训练完一个周期,让新权重和旧权重各执一方下几盘,谁赢得多谁就留下来。下棋时必须用相同的搜索次数和相同的温度设置,否则搜索量的差异会淹没权重本身的差距。
评估对局我一般下 20 盘左右,红黑各执一盘保证先手公平。新权重胜率超过百分之五十五就替换当前网络,低于这个阈值说明训练还在震荡,继续积累数据。保存时只保留当前权重和它的历史最佳版本,不用把每个周期的权重都留一份,磁盘很快就会吃满。训练跑到后期,如果新权重连续多个周期都没有超过旧权重,就说明数据分布已经接近收敛,这时候加大搜索次数比继续训练更有效。
6. 复现路上的 5 个常见坑与排查手记:从送子到死循环
6.1 坑一:网络训了半天还在乱走子,数据质量没爬坡
现象是 loss 在下降,但自我对弈双方仍然频繁送车送马,对局质量肉眼可见地差。原因在于自举循环的数据质量依赖 MCTS 比网络强:如果搜索次数太少,π 几乎等价于网络的随机先验,自我对弈产生的数据就是「烂网络打烂网络」的垃圾数据,网络再从垃圾里学不到新东西,形成死循环。解决方法是把自我对弈的搜索次数提上去,同时检查根节点的 Dirichlet 噪声是否生效。这套组合拳的原理是让 MCTS 的改进作用真正传递到训练样本里。另外确认一下经验池是否被早期烂数据灌满,如果是,把旧数据比例调低一些。
6.2 坑二:AI 走出将帅照面的非法着法,规则器漏了检测
现象是训练中经常出现红方帅和黑方将同列对峙,AI 还觉得局面正常继续走。原因几乎可以肯定是合法走法生成器没有处理将帅照面规则。中国象棋里任何走法如果导致双方将帅直接照面,这一步本身就是非法的,必须在生成阶段过滤,而不是等到终局判定才报错。解决方式是像前面 3.3 节那样写一个 is_king_face_to_face 函数,在每一步走子之后立即检测,非法就走法排除。这里建议单独写一组单元测试,构造几个经典照面局面,比如双方帅将之间隔一颗炮、隔一颗兵、完全无遮挡三种情况,每次修改规则逻辑后跑一遍,避免改出回归。
6.3 坑三:MCTS 搜索陷入循环,重复局面哈希没做
现象是一盘自我对弈能走几百步不结束,红方和黑方互相长将,双方都愿意重复同一个循环。原因是搜索树和终局判定都没有处理重复局面,MCTS 看到一个循环局面时无法判断这是好事还是坏事,只能继续探索。解决方式是在环境层维护一个局面哈希栈,每走一步压入当前局面的 Zobrist 哈希,当某个局面第三次出现时,按中国象棋规则判和或者按长将规则判负。MCTS 搜索时也要带上这个重复信息,一旦发现搜索路径中重复出现同一局面,立刻把该分支的价值标注为 0 或 -1,阻止树继续膨胀。这一步不做,训练出的 AI 会变成一个「重复局面专家」,而不是真正的棋手。
6.4 坑四:红方明显强于黑方,视角编码和数据增强缺失
现象是评估时当前权重执红胜率百分之六十以上,执黑胜率只有百分之四十不到,两者差距稳定且始终存在。原因通常是局面编码用了绝对视角,网络把「红方在下半盘」这种坐标特征当成规律学进去了。解决方式是把编码改成当前玩家视角,同时在数据增强上做文章。中国象棋棋盘在左右方向上有近似对称性,但河界和九宫决定了它不是完美对称。我实际使用中有效的增强是 180 度旋转加红黑棋子互换,这本质上相当于让网络看到同一局棋从另一方的视角,数据量直接翻倍。水平翻转也能用,但要小心炮和兵的位置在翻转后仍然合法,不能生搬围棋的翻转增强。
6.5 坑五:GPU 闲着、CPU 满载,自我对弈和训练串行导致的资源错配
现象是训练时 GPU 利用率不到百分之十,自我对弈阶段 GPU 完全空闲,整个项目跑一轮要十几个小时。原因是把自我对弈和网络训练做成了严格的串行流程:对弈时用 CPU 跑搜索,攒一批数据再切到 GPU 训练。解决方式是把两个过程拆成异步并行。自我对弈进程持有当前网络权重的一份拷贝,跑完一批数据放入共享队列,训练进程从队列取数据更新权重,每隔一段时间把新权重推给对弈进程。对弈搜索本身也可以开多进程,每个进程负责几局棋,用 Python 的 multiprocessing 池就能实现。这里要说句大实话:工程上最省事的方案是直接用现成的异步自我对弈框架,自己写并行逻辑调试成本不低,但如果想彻底理解数据流动,手写一次很值得。
7. 怎么确认 AI 真的在进步:四个验证窗口与一条进阶路线
7.1 固定权重自我对弈:胜率分布暴露数据健康度
把当前权重固定下来,让它自己和自己下 200 局,看胜负分布。健康的状态是红方胜率接近黑方胜率,和棋占比随训练逐步上升。如果某一方压倒性获胜,优先怀疑视角编码和公平性问题,而不是继续训练。
7.2 搜索量爬坡测试:价值头是否随搜索变稳
取 20 个固定开局局面,分别用 100 次、400 次、1600 次搜索评估价值头输出。如果搜索量增加后价值估计变化巨大,说明网络先验和搜索结论差距很大,训练还没收敛;如果变化很小,说明网络评估已经和搜索基本一致。
7.3 与随机走法和贪吃走法对照:基础棋力的及格线
先和随机走法下 50 盘,再和只会吃子的贪心走法下 50 盘。一个训练合格的中国象棋 AlphaZero 复现应该完胜随机走法,对战贪吃走法胜率至少百分之九十以上。如果连这个都达不到,问题不在棋力,而在规则器或代码逻辑。
7.4 进阶路线:从单机到异步分布式训练
单机跑通只是第一步。想提升棋力,把自我对弈进程扩展到多台机器的 worker,训练集中在带 GPU 的节点上,权重定期广播同步。中国象棋的优势是每局数据生成速度快,分布式的瓶颈反而在队列吞吐和网络通信。
我自己的一次调参经历是,某天训练 loss 一路走低,但评估时新旧权重谁也赢不了谁,卡了整整一天。后来发现是经验池里老数据占比太高,新权重在反复拟合旧时代的数据。把经验池裁掉一半、提高新数据权重后,胜率立刻开始爬坡。这种问题靠看曲线是看不出来的,黑匣子里的数据分布才是真正的胜负手。希望这篇笔记能帮你少走几步弯路,愿你在自己的棋盘上也跑出第一步像样的跳马。
本文还有配套的精品资源,点击获取