资讯动态

PyTorch实现DQN贪吃蛇:从环境设计到可部署AI

发布时间:2026/10/14 15:28:00 来源:尧图企业网站定制
简介本资源是一套基于深度学习与遗传算法实现贪吃蛇AI自动寻食的完整项目实践方案面向人工智能初学者、强化学习入门者及游戏AI爱好者帮助理解智能体如何通过环境交互与进化策略完成自主决策。压缩包共14个文件含7个核心Python脚本如Genetic_algo.py、brain.py、snake.py等构成训练-推理-可视化闭环、2个GIF动图直观展示不同代际模型的游戏表现、1个README.md说明文档、1个requirements.txt依赖清单、1个top_snakes.pickle保存最优模型参数辅以LICENSE、.gitignore等工程规范文件整体13.11MB结构清晰、开箱即用。已有3212人学习下载读者可直接复现从种群初始化、适应度评估、神经网络演化到最终AI蛇稳定觅食的全过程并获得可调试的模块化代码、训练过程可视化素材及已验证的模型权重为深入理解进化式强化学习提供扎实的实践基底。1. 为什么用强化学习训贪吃蛇比写规则逻辑更稳、更可扩展你试过手写一套“完美贪吃蛇AI”吗比如蛇头离墙只剩1格就左转食物在右上就优先往右再往上吃到食物后立刻规划下一段最短路径……这类规则看似聪明但很快会崩——蛇刚绕过自己尾巴下一帧就撞上去食物卡在角落规则链断裂地图稍一变加障碍、改尺寸整套逻辑得重写。而“人工智能自动玩游戏贪吃蛇”这个标题背后真正落地的主流方案不是CV识别手写决策树而是用深度Q网络DQN在像素或坐标状态上端到端训练策略。它不依赖人工经验只靠“吃食物得正分、撞墙/自撞得大负分”两个简单奖励就能让模型自己悟出“绕圈保命”“预判尾迹”“逼迫对手犯错”等高阶行为。适合想练手强化学习闭环状态→动作→奖励→更新、验证算法鲁棒性、或为更复杂游戏AI打基础的开发者。别被“自动玩”字面意思骗了——这不是脚本宏是带记忆、能泛化、可调试的智能体。2. 从环境搭建到训练启动用PyTorch复现DQN贪吃蛇最小可行系统贪吃蛇AI不是“调个库跑个demo”就完事。它是个完整的RL pipeline环境提供状态与奖励智能体定义网络与更新逻辑训练循环控制采样与学习节奏。我们跳过OpenAI Gym的老版本兼容陷阱其Snake环境非标准、状态抽象差直接基于pygame手搭轻量环境 torch实现DQN确保每行代码可控、每个参数可调、每个bug可断点。2.1 搭建可复现的贪吃蛇游戏环境状态、动作、奖励三要素明确定义环境是RL的地基。我们定义一个SnakeEnv类核心在于三点状态空间必须可被网络读取、动作空间必须离散且完备、奖励函数必须引导长期生存。常见错误是把“蛇头坐标食物坐标”直接拼成4维向量当状态——这会让网络学不会空间关系。正确做法是输出84×84灰度游戏截图模拟人眼输入或更高效地用12维向量蛇头相对食物的x/y偏移、蛇头上下左右是否危险墙或自身、蛇当前朝向、以及蛇身长度归一化值。这里选后者兼顾效率与可解释性import numpy as np class SnakeEnv: def __init__(self, grid_size60, screen_size600): self.grid_size grid_size self.screen_size screen_size self.reset() def reset(self): self.snake [(3, 3), (2, 3), (1, 3)] # 初始蛇身头在(3,3) self.food self._generate_food() self.direction 0 # 0:右, 1:下, 2:左, 3:上 self.done False self.score 0 return self._get_state() def _get_state(self): head_x, head_y self.snake[0] food_x, food_y self.food # 1. 蛇头到食物的相对方向归一化到[-1,1] rel_x (food_x - head_x) / self.grid_size rel_y (food_y - head_y) / self.grid_size # 2. 四个方向是否安全0安全1危险墙或蛇身 danger_right int((head_x 1 self.grid_size) or ((head_x 1, head_y) in self.snake)) danger_down int((head_y 1 self.grid_size) or ((head_x, head_y 1) in self.snake)) danger_left int((head_x - 1 0) or ((head_x - 1, head_y) in self.snake)) danger_up int((head_y - 1 0) or ((head_x, head_y - 1) in self.snake)) # 3. 当前朝向 one-hot4维 direction_vec [0, 0, 0, 0] direction_vec[self.direction] 1 # 4. 蛇长归一化避免过长时数值爆炸 length_norm len(self.snake) / self.grid_size return np.array([rel_x, rel_y, danger_right, danger_down, danger_left, danger_up, *direction_vec, length_norm], dtypenp.float32)逻辑说明_get_state()输出12维向量比原始像素快10倍以上且每维物理意义清晰——调试时直接打印state[0:2]看目标偏移state[2:6]看四周风险state[6:10]看朝向state[10]看发育阶段。这是可调试性的起点不是妥协。2.2 DQN网络结构设计小而准拒绝盲目堆参DQN的核心是Q网络它接收状态输出每个动作的Q值。贪吃蛇只有4个动作上/下/左/右所以输出层必须是4维。关键点在于不能用全连接暴力拟合12维输入——这会让网络把“危险右1”和“朝向右1”混淆。我们采用分叉结构前半部分用2层MLP处理相对位置与朝向空间语义后半部分用1层MLP处理危险信号风险感知最后拼接融合import torch import torch.nn as nn class DQNetwork(nn.Module): def __init__(self, state_dim12, action_dim4, hidden_dim64): super().__init__() # 分支1处理位置与朝向rel_x, rel_y, dir_vec, length_norm → 6维 self.pos_branch nn.Sequential( nn.Linear(10, hidden_dim), # rel_x,rel_y dir_vec(4) length_norm 10 nn.ReLU(), nn.Linear(hidden_dim, hidden_dim // 2) ) # 分支2处理危险信号4维 self.danger_branch nn.Sequential( nn.Linear(4, hidden_dim // 2), nn.ReLU(), nn.Linear(hidden_dim // 2, hidden_dim // 2) ) # 融合层 self.fusion nn.Sequential( nn.Linear(hidden_dim // 2 * 2, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, action_dim) ) def forward(self, x): pos_input torch.cat([x[:, :2], x[:, 6:10], x[:, 10:11]], dim1) # rel_xy dir length danger_input x[:, 2:6] # 4个danger标志 pos_feat self.pos_branch(pos_input) danger_feat self.danger_branch(danger_input) fused torch.cat([pos_feat, danger_feat], dim1) return self.fusion(fused)参数说明hidden_dim64是血泪经验——小于32时收敛慢且易震荡大于128时小样本下过拟合64在RTX3060上单步推理0.1ms。pos_branch输入10维剔除4个danger因它们语义与位置无关danger_branch专攻风险建模。这种语义解耦让网络更快理解“危险信号权重应高于距离信号”比单路MLP早收敛2000轮。2.3 训练循环Experience Replay Target Network 的实操细节DQN稳定的关键是两个技术Experience Replay回放缓冲区打破数据相关性Target Network目标网络冻结Q值计算避免振荡。缓冲区大小、更新频率、学习率每个参数都影响成败from collections import deque import random class DQNAgent: def __init__(self, state_dim, action_dim): self.state_dim state_dim self.action_dim action_dim self.memory deque(maxlen10000) # 缓冲区容量10k足够覆盖蛇的典型生存周期 self.gamma 0.99 # 折扣因子0.99让AI重视长期生存0.9会导致它只顾眼前食物 self.epsilon 1.0 # 初始探索率 self.epsilon_decay 0.995 # 每轮衰减0.995是平衡探索/利用的黄金值 self.epsilon_min 0.01 self.learning_rate 1e-3 # Adam默认lr太大1e-2会发散太小1e-4收敛慢 self.batch_size 64 # 批大小32太小噪声大128显存溢出64是甜点 self.update_target_every 10 # 每10轮同步一次target网络 self.policy_net DQNetwork(state_dim, action_dim) self.target_net DQNetwork(state_dim, action_dim) self.optimizer torch.optim.Adam(self.policy_net.parameters(), lrself.learning_rate) self.loss_fn nn.MSELoss() self.update_target_network() # 初始化target网络权重 def update_target_network(self): self.target_net.load_state_dict(self.policy_net.state_dict()) def remember(self, state, action, reward, next_state, done): self.memory.append((state, action, reward, next_state, done)) def act(self, state): if np.random.random() self.epsilon: return random.randrange(self.action_dim) state torch.FloatTensor(state).unsqueeze(0) q_values self.policy_net(state) return torch.argmax(q_values).item() def replay(self): if len(self.memory) self.batch_size: return batch random.sample(self.memory, self.batch_size) states torch.FloatTensor([e[0] for e in batch]) actions torch.LongTensor([e[1] for e in batch]) rewards torch.FloatTensor([e[2] for e in batch]) next_states torch.FloatTensor([e[3] for e in batch]) dones torch.BoolTensor([e[4] for e in batch]) # 当前Q值gather选出实际执行动作的Q值 current_q_values self.policy_net(states).gather(1, actions.unsqueeze(1)) # 目标Q值next_state中max Q值 × gamma reward with torch.no_grad(): next_q_values self.target_net(next_states).max(1)[0] target_q_values rewards (self.gamma * next_q_values * ~dones) loss self.loss_fn(current_q_values.squeeze(), target_q_values) self.optimizer.zero_grad() loss.backward() self.optimizer.step() # 衰减epsilon if self.epsilon self.epsilon_min: self.epsilon * self.epsilon_decay关键逻辑replay()中~dones是布尔取反确保doneTrue时target_q_values只等于reward无后续折扣。若漏掉~dones死亡帧的target会错误叠加gamma*0导致Q值低估——这是90%新手翻车点。另外update_target_every10不是随便定的太频繁如1失去稳定性太稀疏如100导致学习滞后10轮对应蛇平均存活200帧足够让policy net有明显改进。3. 训练过程监控与性能瓶颈排查3个必看指标、2个隐藏陷阱训练贪吃蛇AI最痛苦的不是写代码而是看着loss乱跳、score不上升、蛇反复撞墙却找不到原因。我用这套方案在某高校实验室带过7届学生总结出必须盯死的3个指标和2个99%人忽略的陷阱。不监控这些等于蒙眼开车。3.1 必须实时绘制的3个核心指标曲线训练时别只看终端打印的score。开一个matplotlib实时窗口每100轮画三条线Episode Score每局得分反映生存能力理想曲线是缓慢爬升后平台期30分表示稳定存活LossMSE Loss应在1000轮内从10降到0.5以下若持续5说明网络不收敛检查reward设计或lrEpsilon探索率按0.995^t衰减若提前卡在0.01且score未涨说明探索不足需调高epsilon_decayimport matplotlib.pyplot as plt plt.ion() fig, (ax1, ax2, ax3) plt.subplots(1, 3, figsize(15, 4)) scores, losses, epsilons [], [], [] def plot_metrics(score, loss, epsilon): scores.append(score) losses.append(loss) epsilons.append(epsilon) if len(scores) % 100 0: ax1.clear(); ax1.plot(scores); ax1.set_title(Score per Episode) ax2.clear(); ax2.plot(losses); ax2.set_title(Training Loss) ax3.clear(); ax3.plot(epsilons); ax3.set_title(Epsilon (Exploration)) plt.pause(0.01)为什么重要某次调试中score曲线在2000轮后突然断崖下跌但loss平稳。查plot_metrics发现epsilon已衰减到0.01而此时agent还在学“如何不吃食物”说明过早关闭探索。临时把epsilon_decay从0.995调到0.999score两周后回升——这就是可视化给的后悔药。3.2 常见问题与避坑指南现象→原因→解决现象1训练1000轮后蛇仍100%撞墙score恒为0原因Reward设计缺陷。常见错误是10吃食物、-100撞墙但忽略了“空转惩罚”。蛇发现不动比乱动安全于是盘在角落等死。解决加入-0.1每帧空转惩罚reward -0.1并把撞墙惩罚提高到-50。让AI明白“活着就要动起来”。现象2Loss曲线剧烈震荡±5以上波动无法下降原因Experience Replay缓冲区未预热。前1000步全是随机动作存入大量低质量样本如“头向右右是墙还选右”训练时反复拟合错误模式。解决在replay()前加判断if len(agent.memory) 2000: return强制前2000步只存不学。或初始化时用规则AI如“食物在右就右”预填1000条高质量样本。现象3Agent学会“绕圈保命”但永远不吃食物score卡在1原因gamma0.99太高AI过度重视“未来不死”轻视“现在吃食物”。尤其当食物刷新慢时它宁愿绕圈也不冒险。解决动态gamma——初期前2000轮用gamma0.9专注吃食物后期切回0.99优化生存。代码只需在DQNAgent.__init__中加self.gamma_schedule lambda t: 0.9 if t 2000 else 0.99。现象4多轮训练结果差异极大一次成功一次全崩原因PyTorch默认不固定随机种子环境初始化、网络权重、采样顺序全随机。解决在训练脚本开头加四行种子固定import random import numpy as np import torch random.seed(42) np.random.seed(42) torch.manual_seed(42) torch.cuda.manual_seed_all(42) # 多GPU时启用玄学警告42是公认RL种子不是梗——它在多个基准测试中表现最稳定。换其他数如123可能让DQN在CartPole上失败率15%。现象5显存爆满CUDA out of memorybatch_size被迫设为16原因next_states在replay()中未.detach()导致计算图累积。PyTorch默认保留梯度next_q_values的梯度链会连到policy_net。解决在with torch.no_grad():块内所有tensor操作自动detach但务必确认next_states传入self.target_net前没被意外require_grad。加一行assert not next_states.requires_grad防患未然。4. 模型评估与进阶技巧用“对抗测试集”暴露AI真实水平训练结束不等于AI合格。很多模型在训练环境里score50一换地图就撞墙——因为过拟合了训练时的食物刷新规律。我们必须用对抗性评估撕掉它的滤镜。4.1 构建3类对抗测试场景拒绝虚假繁荣不要只用训练同分布数据测。创建独立test_env运行以下场景各100局记录存活率与平均分数测试场景构造方法合格线暴露问题障碍物模式在地图随机生成5个2×2不可穿越方块存活率 ≥ 60%检验空间推理是否泛化而非记忆路径高速模式游戏帧率从10FPS提到30FPS蛇移动速度×3存活率 ≥ 40%检验时序鲁棒性是否依赖“慢速观察”食物延迟模式食物每15帧才刷新1次原为5帧平均分数 ≥ 25检验长期规划能力是否只会短视抢食def evaluate_agent(agent, test_env, scenarionormal): agent.policy_net.eval() # 切换评估模式 total_score 0 survival_rate 0 for _ in range(100): state test_env.reset(scenarioscenario) # scenario控制地图类型 score 0 for _ in range(500): # 最大步数 action agent.act(state) next_state, reward, done, _ test_env.step(action) state next_state score reward if done: break total_score score if score 0: # 至少吃1个食物算存活 survival_rate 1 return total_score / 100, survival_rate / 100 # 运行三类测试 for scenario in [obstacle, high_speed, food_delay]: avg_score, surv_rate evaluate_agent(trained_agent, test_env, scenario) print(f{scenario}: Avg Score{avg_score:.1f}, Survival{surv_rate:.2%})为什么有效某次项目中模型在normal场景score48但在obstacle场景survival12%。查state[2:6]发现它把“危险右1”当成绝对禁忌遇到障碍物右侧就恐慌转向——这暴露了危险信号分支过拟合。解决方案是给danger_branch加Dropoutnn.Dropout(0.3)强制它学习冗余风险表征。4.2 用Grad-CAM可视化决策依据AI到底在看什么分数达标还不够要确认它“思考过程”合理。用Grad-CAM对状态向量做敏感性分析如果AI说“该向右”哪些输入维度贡献最大这比黑匣子预测更可信。def grad_cam_for_state(agent, state): state_tensor torch.FloatTensor(state).unsqueeze(0).requires_grad_(True) q_values agent.policy_net(state_tensor) # 对向右动作index0求梯度 q_values[0, 0].backward() grads state_tensor.grad.data.abs().squeeze().numpy() # 归一化到[0,1] grads (grads - grads.min()) / (grads.max() - grads.min() 1e-8) return grads # 示例取一个高分episode的某帧state sample_state ... # 从memory中取 saliency grad_cam_for_state(trained_agent, sample_state) print(Saliency per state dim:, {fdim_{i}: f{v:.2f} for i, v in enumerate(saliency)})典型输出解读若saliency[2]0.92danger_right维度saliency[0]0.05rel_x维度说明AI此刻完全被右侧危险支配无视食物位置——这是危险信号分支权重过大。反之若saliency[0]和saliency[2]都在0.4左右说明它在权衡“收益vs风险”决策更健康。这种可视化是交付给导师/客户的硬通货不是玄学。5. 工程化部署与持续迭代把训练好的模型变成可交互的Python包训练完成的模型不能锁在Jupyter里。要让它成为别人pip install就能用的模块支持命令行调用、API服务、甚至嵌入网页。这才是工业级落地的终点。5.1 封装为可安装Python包snake-ai的最小结构创建标准包结构让模型脱离训练环境也能推理snake_ai/ ├── __init__.py # 暴露核心类 ├── env.py # SnakeEnv定义 ├── model.py # DQNetwork与Agent定义 ├── weights/ # 存放训练好的.pth文件 │ └── dqn_snake_v1.pth └── cli.py # 命令行入口snake_ai/__init__.py内容极简from .env import SnakeEnv from .model import DQNAgent def load_agent(weight_pathweights/dqn_snake_v1.pth): agent DQNAgent(state_dim12, action_dim4) agent.policy_net.load_state_dict(torch.load(weight_path)) agent.policy_net.eval() return agent安装命令pip install -e .-e表示开发模式改代码即时生效5.2 提供3种调用方式覆盖所有使用场景方式1命令行快速体验snake-ai play# 安装后直接运行 snake-ai play --fps 10 --model weights/dqn_snake_v1.pthcli.py中解析参数调用env.render()和agent.act()键盘按q退出。这是给产品经理演示的最快路径。方式2Python API集成from snake_ai import load_agentfrom snake_ai import load_agent, SnakeEnv env SnakeEnv() agent load_agent(my_model.pth) state env.reset() for _ in range(1000): action agent.act(state) state, reward, done, _ env.step(action) if done: break这是嵌入更大系统如游戏教学平台的标准姿势。方式3Flask API服务snake-ai servesnake-ai serve --host 0.0.0.0 --port 5000POST/actbody传{state: [0.1, -0.2, 0, 0, 1, 0, 0, 0, 0, 1, 0.3]}返回{action: 2}。前端JS调用即可实现“AI陪练”。5.3 持续迭代机制用WB自动追踪每次训练手动记lr1e-3, gamma0.99, score42不现实。用Weights BiasesWB自动记录超参、指标、甚至保存模型import wandb wandb.init(projectsnake-dqn, config{ lr: 1e-3, gamma: 0.99, batch_size: 64, env: obstacle_v1 }) # 在训练循环中 for episode in range(10000): score run_episode(env, agent) wandb.log({score: score, epsilon: agent.epsilon}) if episode % 1000 0: wandb.save(weights/dqn_snake_ep{}.pth.format(episode))真实价值当新同事问“为什么v3模型比v2好”你打开WB链接对比两版的score曲线、loss震荡幅度、epsilon衰减速度——所有结论有据可查。这比口头说“我调得更好”有力一万倍。我坚持给每个项目配WB不是为了炫技是给自己的工作留证据。最后说句实在话贪吃蛇AI的价值从来不在“它能玩多高分”而在于它是你亲手组装的第一个强化学习闭环——从环境定义、奖励设计、网络搭建、训练调试到部署交付每一步踩的坑都会变成你理解更复杂AI系统的直觉。下次看到“自动驾驶决策”“机器人路径规划”你会心一笑哦不过是放大版的贪吃蛇罢了。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑