☰
Python深度神经网络五子棋引擎:从训练到部署全流程
2026/10/2 2:36:52 网站建设 项目流程

简介:这份资源是面向计算机相关专业学生与开发者的五子棋博弈引擎项目源码,采用Python结合深度神经网络与蒙特卡洛树搜索实现,适合作为毕业设计、课程设计或人工智能入门进阶的实战参考。压缩包共39个文件,约2.49MB,包含15个py源码文件、13个pyc编译文件、4个model模型文件,以及md部署文档、gif演示动图和log训练日志,覆盖从策略价值网络、MCTS搜索到人机对弈的完整链路。项目已通过导师评审,答辩评分达95分,代码经测试可正常运行。读者可获得可复现的训练与对弈脚本、预训练模型、Django与Flask两套部署文档,以及纯MCTS与AlphaZero风格实现的对照代码,便于理解强化学习在棋类博弈中的应用,也可在此基础上修改扩展功能。目前已有134人学习关注。

1. 从一份五子棋引擎压缩包说起:Python 深度神经网络博弈到底能跑出什么水平

很多人第一次看到「基于 Python 深度神经网络的五子棋博弈引擎」这类标题,第一反应是:五子棋规则简单、状态空间比围棋小得多,用传统极小化极大加 Alpha-Beta 剪枝就能下得不错,为什么还要上深度神经网络?我最初也是这个判断,直到自己动手把纯搜索版本和神经网络版本摆在一起对弈,才发现问题的关键不在「能不能赢人」,而在「评估函数怎么写」。传统引擎靠人工设计的棋型打分表,活三、冲四、双三这些模式要一条条枚举,遇到复杂局面时打分表之间互相打架,调参调到怀疑人生。深度神经网络的价值在于把「棋型识别」这件事交给网络自己学,你只需要给它足够的棋谱和清晰的输入编码,它就能输出一个相对平滑的胜率估计,省掉大量手工特征工程。

这份资料包面向的是想完整走一遍「数据准备 → 网络设计 → 训练 → 接入搜索 → 打包部署」流程的开发者。它不要求你有强化学习背景,但要求你能读懂 Python、装得动 PyTorch 或 TensorFlow、愿意花几个小时等训练收敛。如果你只是想找一个能直接双击运行的成品软件,那这份东西会让你失望;但如果你想搞清楚神经网络怎么和博弈搜索结合、模型怎么导出成可分发文件,那它正好覆盖了这条链路。下面我按自己复现时的顺序,把每个环节拆开讲。

2. 棋盘编码与网络结构:把 15×15 的棋局喂给神经网络

2.1 为什么不能直接把棋盘矩阵丢进去

五子棋棋盘是 15×15,最直觉的做法是搞一个 15×15 的二维数组,黑棋填 1、白棋填 -1、空位填 0,然后接几个卷积层。这个做法能跑,但效果一般,原因是它丢掉了「轮到谁走」这个关键信息。同一个棋盘局面,黑棋走和白棋走的胜率完全不同,如果输入里不体现当前行棋方,网络学到的就是一个平均化的、模糊的评估。常见做法是把输入做成两个通道:通道 0 表示「当前行棋方的棋子位置」,通道 1 表示「对手的棋子位置」。这样无论当前是黑是白,网络看到的都是「我方」和「敌方」的相对关系,泛化能力会好很多。

再进一步,有些实现会加第三个通道表示「上一手落子位置」,帮助网络捕捉对手的即时威胁。这个通道对五子棋特别有用,因为五子棋的胜负往往在连续几手内决定,知道对手刚下在哪里,网络更容易判断当前是否存在必须应对的冲四或活三。我实测下来,三通道输入比两通道在验证集上的准确率能高出三到五个百分点,训练时间增加不多,值得加。

2.2 一个能跑通的卷积网络结构

下面这个结构是我在复现时用的版本,输入 4 个通道(我方、敌方、上一手、行棋方标识广播),输出一个标量胜率。它不算深,但在五子棋这种局部模式主导的任务上够用,训练也快。

import torch import torch.nn as nn import torch.nn.functional as F class GomokuNet(nn.Module): def __init__(self, board_size=15, in_channels=4): super().__init__() # 第一层用 3x3 卷积抓局部棋型,比如活三、冲四 self.conv1 = nn.Conv2d(in_channels, 32, kernel_size=3, padding=1) self.bn1 = nn.BatchNorm2d(32) # 第二层感受野扩大到 5x5,能覆盖一个方向上的连续棋子 self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) self.bn2 = nn.BatchNorm2d(64) # 第三层继续加深,捕捉多方向组合威胁 self.conv3 = nn.Conv2d(64, 64, kernel_size=3, padding=1) self.bn3 = nn.BatchNorm2d(64) # 全局平均池化把空间维度压掉,避免全连接层参数爆炸 self.global_pool = nn.AdaptiveAvgPool2d(1) self.fc1 = nn.Linear(64, 128) self.fc2 = nn.Linear(128, 1) def forward(self, x): x = F.relu(self.bn1(self.conv1(x))) x = F.relu(self.bn2(self.conv2(x))) x = F.relu(self.bn3(self.conv3(x))) x = self.global_pool(x).flatten(1) x = F.relu(self.fc1(x)) # 输出用 tanh 压到 [-1, 1],-1 表示必败,1 表示必胜 return torch.tanh(self.fc2(x))

这段代码里几个参数值得说明。in_channels=4对应前面说的四个通道,如果你只做两通道,改成 2 即可,但后面数据生成也要同步改。kernel_size=3, padding=1保证卷积后特征图尺寸不变,15×15 进、15×15 出,方便堆叠。AdaptiveAvgPool2d(1)把每个通道压成一个数,这样全连接层输入固定是 64 维,不用关心棋盘大小,换成 19×19 也能跑。输出用tanh而不是sigmoid,是因为胜率评估需要区分「我赢」和「我输」,用 [-1, 1] 比 [0, 1] 更直观,后面和搜索结合时也方便做符号判断。

2.3 训练数据的两种来源与标签怎么打

数据来源无非两种:人类棋谱和自对弈。人类棋谱质量高但数量有限,自对弈可以无限生成但早期模型水平差、数据噪声大。我一般先用人类棋谱做预训练,让网络学会基本棋型,再用自对弈数据做微调。标签的打法有两种常见方案:一种是「结果标签」,整盘棋结束后,赢的一方所有局面标 1,输的一方标 -1;另一种是「搜索标签」,用传统 Alpha-Beta 搜索对每个局面算一个分值,把分值归一化后当标签。结果标签简单但稀疏,一盘棋只有一个结果;搜索标签密集但依赖搜索深度,深度不够时标签本身就不准。

我的经验是:先用结果标签训练一个基础模型,收敛后用它辅助搜索,再用搜索结果生成更高质量的标签做第二轮训练。这个过程可以迭代两到三轮,每轮模型都会比上一轮强一点。注意别一次性把自对弈数据全灌进去,早期模型自对弈出来的棋谱质量很差,容易把网络带偏。比较稳的做法是每轮只取最近一批自对弈数据,和人类棋谱按 1:1 混合。

3. 从训练到对弈:把模型接进搜索循环的完整链路

3.1 训练脚本的关键参数与收敛判断

训练脚本本身不复杂,难的是判断什么时候停。五子棋的验证集准确率往往在 70% 到 80% 之间就上不去了,这时候继续训练只会过拟合。我一般看两个指标:验证集损失和自对弈胜率。验证集损失连续三轮不下降就停;自对弈胜率是让新模型和旧模型对弈 100 局,胜率超过 55% 就认为有提升。下面是一个简化的训练循环。

import torch.optim as optim from torch.utils.data import DataLoader def train_one_epoch(model, loader, optimizer, device): model.train() total_loss = 0.0 criterion = nn.MSELoss() # 回归任务用均方误差 for boards, labels in loader: boards = boards.to(device) labels = labels.to(device).unsqueeze(1) # 对齐输出维度 optimizer.zero_grad() preds = model(boards) loss = criterion(preds, labels) loss.backward() # 梯度裁剪防止早期训练梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() total_loss += loss.item() * boards.size(0) return total_loss / len(loader.dataset) # 学习率用 1e-3 起步,配合 StepLR 每 5 轮降一半 optimizer = optim.Adam(model.parameters(), lr=1e-3) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5)

MSELoss适合回归型胜率输出,如果你把输出改成胜负二分类,就换成BCEWithLogitsLoss。clip_grad_norm_的max_norm=1.0是我踩过坑之后加的,早期不加的时候偶尔会出现损失突然变成 NaN,加了之后稳定很多。StepLR的step_size=5表示每 5 个 epoch 学习率乘 0.5,这个节奏在五子棋任务上比较合适,太快会导致还没收敛就降到底,太慢又容易在局部最优附近震荡。

3.2 把网络评估嵌进 Alpha-Beta 搜索

网络输出的是胜率,搜索需要的是分值,中间要做一个映射。最简单的方式是直接把胜率乘以一个系数当分值用,比如score = win_rate * 10000。但这样有个问题:网络在必胜局面可能输出 0.95,在必败局面输出 -0.95,两者差值 1.9,乘以 10000 后是 19000,而一个「五连」的棋型分值通常设成 100000 以上,会导致搜索更信任硬编码的胜负判断而不是网络评估。我的做法是给网络分值设一个上限,比如 ±5000,保证它不会盖过明确的胜负手。

def evaluate_with_net(model, board, current_player, device): # board 是 15x15 的 numpy 数组,1 黑 -1 白 0 空 tensor = encode_board(board, current_player) # 转成 4 通道张量 tensor = tensor.unsqueeze(0).to(device) model.eval() with torch.no_grad(): win_rate = model(tensor).item() # 映射到 [-5000, 5000],避免盖过胜负手 return win_rate * 5000 def alpha_beta(board, depth, alpha, beta, maximizing, model, device): # 先检查是否已有五连,有就直接返回极值 winner = check_winner(board) if winner == 1: return 100000 - (10 - depth) # 越早赢分越高 if winner == -1: return -100000 + (10 - depth) if depth == 0: return evaluate_with_net(model, board, current_player, device) # ... 省略落子生成与递归逻辑

这里100000 - (10 - depth)的写法是为了让搜索倾向于「更快获胜」,而不是拖到后面再赢。depth是剩余搜索深度,越靠近根节点depth越大,减去的值越小,分值越高。这个技巧在五子棋里很重要,因为很多局面下双方都能赢,但先手方必须选最快的那条路。网络评估放在叶子节点,搜索深度一般设 4 到 6 层,再深的话单步耗时会长到无法接受,除非你做缓存或并行。

3.3 模型导出与部署文档里容易漏掉的步骤

训练完的模型要给别人用,不能只丢一个.pth文件,因为对方可能没有和你一样的代码结构。常见做法是导出成 TorchScript 或 ONNX。TorchScript 的好处是保留 PyTorch 运行时,加载方便;ONNX 的好处是跨框架,但五子棋这种小模型用 TorchScript 就够了。

# 导出 TorchScript model.eval() example_input = torch.randn(1, 4, 15, 15) traced = torch.jit.trace(model, example_input) traced.save("gomoku_net.pt") # 加载时不需要原始类定义 loaded = torch.jit.load("gomoku_net.pt") output = loaded(example_input)

导出时注意model.eval()不能省,否则 BatchNorm 会用训练时的统计量,导致推理结果不一致。torch.jit.trace对控制流敏感,如果你的 forward 里有 if 分支依赖输入值,trace 会只记录一条路径,这种情况要用torch.jit.script。部署文档里还要写清楚依赖版本,比如torch>=1.10、numpy>=1.20,以及 Python 版本要求。我见过太多人卡在版本不匹配上,明明代码没问题,就是跑不起来。

4. 避坑与排查:五子棋神经网络训练里最容易翻车的五件事

4.1 损失降到很低但棋力没提升

现象:训练集损失从 0.5 降到 0.01,验证集损失也在降,但模型和传统搜索对弈时胜率不到三成。原因通常是标签和输入编码不一致。比如输入通道里「我方」和「敌方」的定义在数据生成和推理时反了,网络学到的其实是镜像局面。解决方法是写一个单元测试,拿一个已知局面的棋盘,分别用数据生成函数和推理函数编码,打印两个张量对比,确保完全一致。这个坑我踩过两次,每次都是查了半天才发现是通道顺序问题。

4.2 自对弈数据越训越差

现象:第一轮自对弈后模型有提升,第二轮之后胜率反而下降。原因是自对弈数据里包含了大量「双方都下得很差」的棋局,网络从这些棋局里学不到正确走法,反而强化了错误模式。解决办法是控制自对弈数据的比例,每轮只取最近 20% 的对局,并且只保留胜负差距在 10 手以内的棋局,差距太大的说明一方失误太多,参考价值低。另外可以引入「温度采样」,让自对弈时不是总选最高分,偶尔选次高分,增加数据多样性。

4.3 搜索速度慢到无法交互

现象:每走一步要等十几秒,用户体验极差。原因通常是搜索深度设得太深,或者网络推理没有做批处理。五子棋分支因子在开局阶段有 200 多个合法落子,深度 6 的搜索节点数轻松上百万。解决办法有三个:一是用 Alpha-Beta 剪枝把无效分支砍掉,这是必须做的;二是对候选落子做排序,把网络评分高的先搜,提高剪枝效率;三是限制候选落子数量,每层只取网络评分前 10 到 15 个位置展开,其余直接丢弃。第三点会损失一点棋力,但速度提升非常明显。

4.4 模型文件在不同机器上加载失败

现象:在自己电脑上跑得好好的.pt文件,换一台机器就报错。原因通常是 PyTorch 版本不一致,或者导出时用了torch.jit.script而加载环境不支持某些算子。解决办法是导出时同时提供 ONNX 版本作为备选,ONNX 的兼容性更好。另外在部署文档里明确写「推荐使用 conda 创建独立环境」,并给出environment.yml文件,比只写pip install torch靠谱得多。

4.5 对弈界面卡死但程序没报错

现象:点击「人机对弈」后界面无响应,等很久才恢复,或者直接卡死。原因是搜索和界面在同一个线程里,搜索占用 CPU 时界面无法刷新。解决办法是把搜索放到独立线程,用队列和主线程通信。Python 的threading模块够用,但要注意 GIL 的限制,搜索是 CPU 密集型任务,多线程不会真正并行,只是让界面不卡。如果追求更快速度,可以用multiprocessing把搜索放到独立进程,但进程间通信会复杂一些。我一般先用线程方案,够用就不折腾进程。

5. 让引擎更强的一点进阶技巧:用搜索结果为网络做二次标注

模型训练到一定程度后,你会发现它的评估在某些局面下仍然不准,尤其是中盘复杂局面。这时候与其继续调网络结构,不如用搜索来「教」网络。具体做法是:用当前模型配合 Alpha-Beta 搜索,对一批局面算出搜索分值,把搜索分值作为新标签,让网络去拟合。这相当于把搜索的「深度思考」蒸馏回网络,网络学会之后,下次搜索时叶子节点的评估会更准,整体棋力就上去了。

这个流程可以迭代:搜索 → 标注 → 训练 → 再搜索。每轮迭代搜索深度可以加一层,但要注意时间成本。我一般迭代两到三轮就停,再往后提升不明显,而且容易过拟合到搜索的特定行为上。验证方法是让新模型和旧模型对弈 200 局,胜率超过 55% 才算有效提升,低于这个数说明改进在噪声范围内。

另一个实用技巧是「开局库」。五子棋开局阶段变化有限,可以把常见开局的前几手固定下来,避免模型在开局阶段浪费搜索时间。开局库不需要很大,覆盖主流的前三手变化就够,后面交给搜索和网络。这样每局的前几步几乎瞬间完成,用户体验会好很多。

最后说一个我自己的习惯:每次训练完新模型,不要直接替换线上版本,先让它和旧版本对弈 100 局,同时人工看几盘棋谱,确认没有明显的「怪招」。神经网络引擎有时候会走出人类看不懂但确实有效的棋,这没问题;但如果走出明显送子的棋,说明训练数据或编码有问题,必须回查。这个习惯帮我拦住了好几次有问题的模型上线。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询