
简介面向高校计算机、人工智能相关专业课程设计与毕业设计这份基于深度强化学习的Hex棋项目提供了一套可运行、可扩展的完整示例。项目将神经网络与强化学习结合可用于训练六角棋盘对弈AI代码中覆盖了CNN等网络结构、Q-learning、策略梯度、Actor-Critic以及蒙特卡洛树搜索等关键方法适合希望从零搭建棋类AI的学习者。资源压缩包共34个文件以C源码.cpp/.h、Visual Studio工程文件.sln/.vcxproj、预训练模型.dat和说明文档为主整体仅102KB结构紧凑清晰。目前已有72人学习下载。通过阅读并运行该项目读者能直观看到深度强化学习在博弈场景中的完整流程既能作为课设/毕设的可靠起点也便于在此基础上调整网络或算法进一步优化Hex棋AI的决策能力。1. 毕设课设项目背后基于深度强化学习的Hex棋示例要解决什么Hex棋规则一句话就能说清两位玩家在菱形网格上轮流落子一人连接左右边界另一人连接上下边界。规则简单到能在一分钟内讲完但 11×11 的标准棋盘复杂度极高长期被当作博弈智能体研究的试验田。深度强化学习在这类问题上的典型解法是让程序自我对弈从胜负结果中反向学习落子策略而这个示例的完整价值就是把“棋盘状态转张量、策略价值网络输出、蒙特卡洛搜索决策、自我对弈数据回传”这一整条链路压缩进一个 zip 交付物。这个压缩包面向两类人一类是毕设学生需要弄懂线路原型并回答“为什么这么设计、参数怎么改”另一类是课设学生时间紧张需要先看到一句能跑通的基线确定自己能做哪部分改进。项目里规则、搜索、网络、训练日志各占一个模块拿到手后先看 README再跑一次人机对弈脚本确认环境之后逐模块改动。我习惯把这批项目叫“有缝的工程”它不追求最高棋力而是要求每个环节单独可解释。训练过程中你会看到网络先随机乱下接着学会抢占中央区域最后才学会沿边界构建通路——这个演进过程本身就是深度强化学习最直观的演示。2. 深度强化学习的棋盘建模与策略价值网络设计在状态数量庞大的博弈里把局面交给神经网络之前必须先决定网络能看见什么。Hex 的盘面可以用两个通道完整编码每个交叉点只有空、己方棋子、对方棋子三种状态站在当前落子方视角看恰好是一张二值图像。2.1 把盘面编码成神经网络输入张量对当前落子方来说任何合法局面都表示成一个 shape 为(2, n, n)的张量n 是棋盘边长。第一个通道中 1 表示“这里有我的棋子”第二个通道中 1 表示“这里有对手的棋子”两通道都为 0 的位置就是合法空位。编码时不需要额外加“轮到谁下”的通道因为 Hex 是零和博弈局面张量永远站在当前落子方视角生成轮到对手时重新编码即可。import numpy as np def board_to_tensor(board, current_player): n board.shape[0] state np.zeros((2, n, n), dtypenp.float32) for r in range(n): for c in range(n): if board[r][c] current_player: state[0][r][c] 1.0 elif board[r][c] ! 0: state[1][r][c] 1.0 return state这个编码不存棋子数量、不存先后手顺序所有信息都能从二维坐标中推理出来。优点是输入尺寸极小11×11 棋盘只有 242 个浮点数卷积核的感受野恰好覆盖相邻位置网络容易学到“两个同色棋子是否形成连接”这类局部关系。正规实现里常用第三个通道标记四条边界告诉网络哪两条边是需要连接的目标边早期调通阶段可以先不加避免引入额外噪音。2.2 利用棋盘对称性做数据增强Hex 棋盘存在多个对称操作旋转 120 度、旋转 240 度、沿三条对角线镜像组合后同一局面有 12 种等价表示。训练中把一局棋的每条样本保存成多种对称变体能显著提升样本利用率也能削弱网络对坐标的过拟合。对称变换必须连同通道一起做己方和对方棋子要一起旋转否则增强后的盘面会丢失回合语义。实际代码里可以维护一组坐标映射比如rot90把(r, c)映射成(n - 1 - c, r)再让两个通道同时走同一个映射。训练时随机选一种变换作为数据增强推理时也可以把盘面多次变换后分别推理再对策略取平均这种“测试时增强”能稳定提升落子质量。2.3 策略头与值头共享的残差网络深度强化学习的经典做法是 AlphaZero 式残差网络主干提取局面特征末端分裂成两个输出头一个输出每个落子位置的概率另一个输出当前玩家的胜率估计。下面的实现做了轻量化方便在个人笔记本上跑通import torch import torch.nn as nn class HexNet(nn.Module): def __init__(self, n11, num_blocks6, hidden_dim64): super().__init__() self.n n self.conv_in nn.Sequential( nn.Conv2d(2, hidden_dim, 3, padding1), nn.BatchNorm2d(hidden_dim), nn.ReLU(inplaceTrue), ) self.blocks nn.ModuleList() for _ in range(num_blocks): self.blocks.append(nn.Sequential( nn.Conv2d(hidden_dim, hidden_dim, 3, padding1), nn.BatchNorm2d(hidden_dim), nn.ReLU(inplaceTrue), nn.Conv2d(hidden_dim, hidden_dim, 3, padding1), nn.BatchNorm2d(hidden_dim), )) self.policy_head nn.Sequential( nn.Conv2d(hidden_dim, hidden_dim, 1), nn.BatchNorm2d(hidden_dim), nn.ReLU(inplaceTrue), nn.Flatten(), nn.Linear(hidden_dim * n * n, n * n), ) self.value_head nn.Sequential( nn.Conv2d(hidden_dim, hidden_dim, 1), nn.BatchNorm2d(hidden_dim), nn.ReLU(inplaceTrue), nn.Flatten(), nn.Linear(hidden_dim * n * n, 64), nn.ReLU(inplaceTrue), nn.Linear(64, 1), nn.Tanh(), ) def forward(self, x): x self.conv_in(x) for block in self.blocks: x x block(x) p self.policy_head(x) v self.value_head(x) return p, v每个残差块内部的x x block(x)保存了一条从输入到输出的跳连能有效缓解深层网络梯度消失。策略头输出的 logits 要在外部加 softmax 才能转成概率分布值头的Tanh将胜率压缩到 -1 到 1对应必输与必胜两个极端。训练时需要把策略头的输出对非法位置做掩码简单做法是把已经被占位置的 logits 设为一个很大的负数再执行 softmax。3. MCTS自对弈训练让深度强化学习从随机落子走向收敛网络本身不会下棋。真正让网络变强的是“搜索 自对弈”的组合用蒙特卡洛树搜索MCTS选出更优落子用这些落子形成的对局结果更新网络参数如此循环。MCTS 提供高质量的决策样本网络负责把搜索经验泛化到新局面上两者交替推进。3.1 搜索树为什么需要策略头提供先验纯随机 MCTS 也能下棋但收敛极慢。深度强化学习的主要加速手段是让策略头为每个候选落子提供先验概率 P(s,a)让搜索树优先访问策略认为合理的分支。树中每个节点保存父节点指针、子节点字典、访问次数 N(s,a)、累计价值 W(s,a)。选择节点时使用 UCB 风格公式a argmax( Q(s,a) c_puct * P(s,a) * sqrt(N(s)) / (1 N(s,a)) )。其中Q(s,a) W(s,a)/N(s,a)是当前搜索统计出的平均胜率c_puct控制探索强度。先验概率 P 越高的位置搜索次数越多但统计量充足后 Q 逐渐占据主导探索自然收敛到高价值分支。import math class MCTSNode: def __init__(self, parentNone, prior0.0): self.parent parent self.prior prior self.visit_count 0 self.total_value 0.0 self.children {} def value(self): return self.total_value / self.visit_count if self.visit_count 0 else 0.0 def select_child(self, legal_moves, c_puct1.0): best_move None best_score -float(inf) for move in legal_moves: child self.children.get(move) if child is None: continue score (child.value() c_puct * child.prior * math.sqrt(self.visit_count) / (1 child.visit_count)) if score best_score: best_score score best_move move return best_movelegal_moves必须由外部棋盘对象提供防止选中已被占据的位置。节点被首次访问时需要用策略网络输出的概率调用expand()初始化全部子节点常见错误是漏掉这一步导致搜索总是退回随机选择。3.2 终局判断用并查集判断谁连接了边界MCTS 每展开一步都要快速判断是否分出胜负。Hex 的胜负等价于图连通性问题把同一方的棋子视为节点落子等于连接相邻节点某方获胜当且仅当它的连通块串联起自己负责的两条对边。常见做法是用并查集Union-Find维护棋子连通关系并在棋盘边界上设置虚拟节点。玩家一方只要和左右两个虚拟节点同根就说明连接了左右边界另一方连接上下边界同理。class HexBoard: def __init__(self, n): self.n n self.parent list(range(n * n 4)) self.virtual_top n * n self.virtual_bottom n * n 1 self.virtual_left n * n 2 self.virtual_right n * n 3 self.board [[0] * n for _ in range(n)] def index(self, r, c): return r * self.n c def find(self, x): while self.parent[x] ! x: self.parent[x] self.parent[self.parent[x]] x self.parent[x] return x def union(self, a, b): ra, rb self.find(a), self.find(b) if ra ! rb: self.parent[ra] rb def place(self, r, c, player): self.board[r][c] player idx self.index(r, c) if r 0: self.union(idx, self.virtual_top) if r self.n - 1: self.union(idx, self.virtual_bottom) # 这里继续处理 4 个相邻方向同色则合并 # 最后检查对应两个虚拟节点是否同根即得胜负这种实现把胜负判断压到接近 O(1)MCTS 跑几千次迭代也不会成为性能瓶颈。新手常把连通判断写成每次搜索都做 DFS全盘扫描一次要遍历几十个格子训练速度会差一个数量级。3.3 自对弈数据的生成与批量更新一次训练迭代由三个阶段组成用当前网络加 MCTS 生成若干局完整棋谱对每步棋保存盘面状态、MCTS 访问概率、最终胜者用这批数据做监督学习更新网络。策略头的目标是 MCTS 访问概率值头的目标是 -1 或 1 的胜负标签。def train_one_batch(network, optimizer, states, policies, values): states torch.tensor(states) # (B, 2, n, n) target_p torch.tensor(policies) # (B, n*n) 概率分布 target_v torch.tensor(values).view(-1, 1) # (B, 1) 取值为 -1 或 1 p_logits, v_pred network(states) loss_p -(target_p * torch.log_softmax(p_logits, dim-1)).sum(dim-1).mean() loss_v torch.nn.functional.mse_loss(v_pred, target_v) loss loss_p loss_v optimizer.zero_grad() loss.backward() optimizer.step() return loss.item()这里用交叉熵约束策略分布用均方误差约束值估计两者权重默认都是 1.0。注意values必须站在当前样本的落子方视角记录局面轮到谁胜负标签就以谁为参照否则网络会学到互相矛盾的值输出。3.4 训练一直不收敛时的排查顺序训练上千局后仍乱走的项目我见过不少多数不是网络结构问题。按顺序检查三处第一确认 MCTS 统计频率求和是否为 1第二确认值标签方向正确第三确认温度参数是否合理。温度 τ 在开局阶段应接近 1使落子概率接近 MCTS 访问频率对局后半段降到 0.1 左右让模型偏好在强招式中落子。τ 始终过大时模型学不到稳定倾向τ 过小则探索不足容易过拟合到单一走法。4. 超参数控制与轻量化把深度强化学习跑在教室和笔记本上深度强化学习调参没有通用银弹但 Hex 任务有一组相对稳定的参考值。如果训练日志里 loss 长期徘徊在 0.7 附近多半是搜索规模与网络容量不匹配而不是代码逻辑出错。4.1 一张可以照抄的参数表以下参数适合 11×11 标准棋盘在单张消费级显卡上训练数小时即可看到策略明显改善。CPU 机器跑标准棋盘会有压力建议先把棋盘改成 5×5 或 7×7。参数建议值影响分析棋盘边长 n11验证用 5/7搜索空间与训练时长近似指数关系单局 MCTS 模拟次数200越大落子越准单局生成时间线性增加c_puct1.0小于 0.5 偏向利用大于 2 偏向探索学习率0.001逐步衰减到 0.0001过高导致震荡过低收敛慢批量大小64小批量更新频繁大批量更稳定温度 τ前 30 步 1.0之后 0.15控制早期探索与后期确定性残差块数63 块可快速验证链路6 块接近可用强度训练局数2000自对弈数据预算随时间动态调整c_puct 是最影响搜索性格的参数。取值偏大时 MCTS 更愿意尝鲜较多依赖策略网络先验取值偏小时搜索很快落到 Q 值最高的分支容易让模型陷入局部循环。初始固定 1.0若发现开局招式过于单一再逐步提到 1.4 左右。4.2 资源受限时的降级策略课程设计最常见的场景只有一台集成显卡笔记本。此时把网络砍到 3 个残差块、单局 MCTS 模拟降到 50 次、棋盘边长改成 5×5整个流程依然成立。窄模型不代表不能出结果答辩时说明“先用小棋盘验证算法链路再迁移到标准棋盘”比硬扛大棋盘更有说服力。训练中不要每局结束就立即更新网络先攒 32 局再更新一次。批量更新能降低样本间的强相关性也能让损失曲线更平滑减少训练发散概率。CPU 训练时要调用torch.set_num_threads(物理核心数)默认线程数会把进程切得细碎MCTS 里频繁的小批量推理会因此慢很多。4.3 小棋盘调通链路后放大我一般建议先让 5×5 棋盘在 100 局内表现出明显进步再切到 7×7最后挑战 11×11。小棋盘单局生成快、张量尺寸小、调试打印直观遇到 bug 时查看最近 5 步行棋就能定位是搜索错误还是网络反向传播错误。棋盘放大后优先提高 MCTS 模拟次数其次才是提高训练局数。模拟次数翻倍让 Q 值估计更精确学习信号质量上升局数翻倍只是让模型看更多同类数据边际收益递减。写报告时用 UCB 公式的置信区间收敛逻辑来解释这个结论比空谈“增加数据量”更站得住脚。5. 胜率验证与ZIP交付给课设画一条可信的结束线训练结束不代表项目完成。合格的毕设或课设必须把模型强度、鲁棒性和可复现性都交代清楚最后再整理成别人能直接跑起来的 zip。5.1 三个评估基准测试棋力时至少跑三组对照随机落子胜率应接近 100%、贪心基线只看一步连通性的策略胜率应达 80% 以上、同权重不同随机种子的自对弈胜率接近 50%。第一组证明模型学会了基本规则第二组证明它有基础战术意识第三组证明它没有退化成固定套路。评估时温度置 0每一步直接选搜索次数最多的落子。5.2 用对称性校验网络是否学歪一个低成本的健康检查因为 Hex 棋盘存在多个对称操作同一局面旋转后网络输出的策略分布应随之旋转值输出应保持近似不变。镜像局面的胜率预测偏差超过 0.2 时说明网络记住了固定路径而不是真正的局面特征。def check_symmetry(network, board_tensor): # 输入 shape: (2, n, n)第一通道为当前玩家 p1, v1 network(board_tensor) rotated board_tensor.permute(0, 2, 1).flip(1) # 旋转 90 度并转置 p2, v2 network(rotated) assert (v1 - v2).abs().item() 1e-2断言的值输出差值小于 0.01 是比较严格的标准若训练噪声较大放宽到 0.05 也可接受。策略分布的对应校验实现较繁琐实际交付时先测值头一致性即可发现大多数隐藏 bug。5.3 ZIP 目录结构与依赖锁定工程整理的常见做法是拆出独立包保存规则和 MCTS用主脚本统一提供训练、对弈、评估入口。目录建议如下hex_rl_example/ ├── README.md ├── requirements.txt ├── main.py ├── hex_game/ │ ├── __init__.py │ ├── board.py │ ├── mcts.py │ ├── network.py │ └── training.py ├── weights/ │ ├── hexnet_5x5.pt │ └── hexnet_11x11.pt └── data/ └── selfplay_games.npzrequirements.txt必须锁定到具体版本例如torch2.2.2、numpy1.26.4不要写torch2.0。课堂环境版本差异是最容易卡住复现第一步的坑。交付前跑一次干净环境安装确认命令一键启动pip install -r requirements.txt python main.py --play --weights weights/hexnet_11x11.pt。本文还有配套的精品资源点击获取