尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

强化学习实战:用DQN从零训练坦克大战智能体

强化学习实战:用DQN从零训练坦克大战智能体 简介这份资源面向想入门强化学习与游戏AI的Python开发者以经典坦克大战为载体演示如何用BP神经网络结合强化学习训练AI坦克自动索敌与攻击。包内共84个文件以41张png贴图、18个py脚本、14个pyc缓存为主另含xml配置、gif录屏与字体文件压缩包约55.7MB。运行run_examples.py可切换人工操控与AI对战两种模式WASD移动、空格发射run_RF.py则完整呈现强化学习模型的训练流程配合Q矩阵与TD_Q_FOR_TANK.py可观察决策更新过程。已有1561人学习下载适合希望理解神经网络与强化学习如何落地到游戏场景的读者可据此复现训练、调试参数并观察AI行为变化。1. 用强化学习打坦克大战为什么这个练手项目值得你花一个周末很多人第一次接触强化学习都是从打游戏开始的。坦克大战这个场景特别适合入门状态空间不大、动作离散、奖励信号清晰而且你能肉眼看到智能体从乱撞到学会走位、瞄准、开火的完整过程。相比那些动辄要跑几天、烧几张显卡的深度强化学习项目用 Python 实现一个基于强化学习的坦克大战智能体一台普通笔记本就能跑通训练几十分钟就能看到明显进步。这篇文章面向的是想真正动手跑一遍强化学习闭环的开发者——不管你是刚学完 Q-learning 理论想找个项目落地还是已经用过 Stable-Baselines3 想搞清楚底层每一步在干什么。我会从环境搭建讲到训练调参把能抄的代码和踩过的坑都摆出来让你少走弯路。2. 环境与算法选型为什么是 Q-learning 而不是 PPO2.1 坦克大战的状态、动作与奖励怎么定义强化学习的核心是把问题建模成马尔可夫决策过程说白了就是定义清楚三件事智能体看到什么状态、能做什么动作、做了之后得到什么反馈奖励。坦克大战这个场景状态可以设计得比较简单我方坦克坐标、敌方坦克坐标、我方朝向、敌方朝向、双方血量、是否有子弹在飞。如果要做视觉输入那就得用 CNN 提特征复杂度直接上一个台阶。入门阶段我建议先用结构化状态把强化学习的闭环跑通再说。动作空间通常设计成离散的 6 个上、下、左、右、开火、不动。有些实现会把移动和开火拆成两个并行的动作头但离散组合更简单Q-learning 直接就能用。奖励函数是决定训练效果的关键我一般这样设击中敌方 10被击中 -10击毁敌方 50自己被击毁 -50每走一步 -0.01 作为时间惩罚鼓励智能体尽快结束战斗而不是绕圈。这个时间惩罚系数别设太大否则智能体会变得过于激进还没瞄准就冲上去送。# 状态编码示例把游戏画面转成固定长度的向量 def get_state(self, game): # 归一化坐标到 [0,1]避免数值范围差异过大影响 Q 值收敛 my_tank game.my_tank enemy_tank game.enemy_tank state [ my_tank.x / SCREEN_WIDTH, my_tank.y / SCREEN_HEIGHT, enemy_tank.x / SCREEN_WIDTH, enemy_tank.y / SCREEN_HEIGHT, my_tank.direction / 4.0, # 朝向有 4 个方向归一化 enemy_tank.direction / 4.0, my_tank.hp / MAX_HP, enemy_tank.hp / MAX_HP, 1.0 if game.bullet_in_flight else 0.0, ] return np.array(state, dtypenp.float32)这段代码把游戏内的关键信息压成一个 9 维向量。归一化这一步很多人会忽略但对 Q-learning 影响很大——如果坐标是 0 到 800、血量是 0 到 100Q 网络在拟合时会被大数值主导小数值的特征几乎学不到。归一化之后所有特征都在同一量级收敛速度会快不少。bullet_in_flight这个标志位是告诉智能体当前有没有子弹在飞避免它连续开火浪费冷却时间。2.2 表格型 Q-learning 和 DQN 的取舍状态是 9 维连续向量理论上可以用表格型 Q-learning但得先离散化。比如每个维度分 10 个桶9 个维度就是 10^9 个状态表格根本存不下。所以连续状态必须上函数逼近最直接的就是 DQN——用神经网络来拟合 Q(s,a)。DQN 的核心技巧就三个经验回放、目标网络、ε-贪婪探索。经验回放把 (s, a, r, s, done) 存进缓冲区训练时随机采样打破样本之间的时间相关性目标网络每隔 N 步同步一次参数避免 Q 值追着自己跑导致发散ε-贪婪让智能体以一定概率随机探索前期 ε 大一点后期逐渐减小。如果你只是想快速看到效果用 Stable-Baselines3 的 DQN 几行代码就能跑起来。但如果你想真正理解每一步在干什么我建议手写一遍 DQN 的训练循环。下面这个网络结构是我常用的输入 9 维状态两个隐藏层各 128 个神经元输出 6 个动作的 Q 值。import torch import torch.nn as nn class QNetwork(nn.Module): def __init__(self, state_dim9, action_dim6): super().__init__() self.net nn.Sequential( nn.Linear(state_dim, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, action_dim) ) def forward(self, x): return self.net(x) # 目标网络结构相同参数定期从主网络复制 target_net QNetwork() target_net.load_state_dict(policy_net.state_dict()) target_net.eval() # 目标网络不参与梯度更新隐藏层用 128 是经验值状态维度才 9网络再大就容易过拟合训练反而不稳定。ReLU 是默认选择别用 sigmoid容易梯度消失。目标网络一定要调eval()否则 BatchNorm 或 Dropout 会干扰推理结果——虽然这个简单网络没有这些层但养成习惯没坏处。2.3 经验回放缓冲区的大小怎么定经验回放缓冲区replay buffer存的是历史 transition容量一般设 10000 到 100000。太小了样本多样性不够太大了早期垃圾数据会拖累训练。坦克大战这种单局时长几十步的场景我一般设 50000。采样时用均匀随机采样就行优先经验回放Prioritized Experience Replay虽然理论上更好但实现复杂入门阶段没必要。from collections import deque import random class ReplayBuffer: def __init__(self, capacity50000): self.buffer deque(maxlencapacity) def push(self, state, action, reward, next_state, done): self.buffer.append((state, action, reward, next_state, done)) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) states, actions, rewards, next_states, dones zip(*batch) return (np.array(states), np.array(actions), np.array(rewards), np.array(next_states), np.array(dones)) def __len__(self): return len(self.buffer)deque设了maxlen之后会自动淘汰旧数据不用手动管理。sample返回的是 numpy 数组后面直接转 tensor 喂给网络。batch_size 一般设 64 或 128太小梯度噪声大太大显存吃紧且更新次数少。我一般用 64训练频率高一些。3. 训练循环与参数调优把智能体从乱撞练到会走位3.1 主训练循环的完整代码结构训练循环的骨架就是智能体与环境交互、存经验、采样训练、更新目标网络。每一局结束后重置环境记录累计奖励。下面是一个可运行的最小实现import torch.optim as optim import numpy as np policy_net QNetwork() target_net QNetwork() target_net.load_state_dict(policy_net.state_dict()) optimizer optim.Adam(policy_net.parameters(), lr1e-3) buffer ReplayBuffer(capacity50000) BATCH_SIZE 64 GAMMA 0.99 # 折扣因子 EPSILON_START 1.0 EPSILON_END 0.05 EPSILON_DECAY 0.995 TARGET_UPDATE 10 # 每 10 局同步一次目标网络 epsilon EPSILON_START for episode in range(2000): state env.reset() total_reward 0 done False while not done: # ε-贪婪策略选择动作 if np.random.random() epsilon: action np.random.randint(6) else: with torch.no_grad(): q_values policy_net(torch.FloatTensor(state)) action q_values.argmax().item() next_state, reward, done env.step(action) buffer.push(state, action, reward, next_state, done) state next_state total_reward reward # 缓冲区足够大才开始训练 if len(buffer) BATCH_SIZE: states, actions, rewards, next_states, dones buffer.sample(BATCH_SIZE) states torch.FloatTensor(states) actions torch.LongTensor(actions) rewards torch.FloatTensor(rewards) next_states torch.FloatTensor(next_states) dones torch.FloatTensor(dones) # 当前 Q 值 q_values policy_net(states).gather(1, actions.unsqueeze(1)).squeeze(1) # 目标 Q 值终止状态没有未来奖励 with torch.no_grad(): next_q_values target_net(next_states).max(1)[0] target_q rewards GAMMA * next_q_values * (1 - dones) loss nn.MSELoss()(q_values, target_q) optimizer.zero_grad() loss.backward() # 梯度裁剪防止 Q 值爆炸 torch.nn.utils.clip_grad_norm_(policy_net.parameters(), max_norm10) optimizer.step() # 衰减 epsilon epsilon max(EPSILON_END, epsilon * EPSILON_DECAY) # 定期同步目标网络 if episode % TARGET_UPDATE 0: target_net.load_state_dict(policy_net.state_dict()) print(fEpisode {episode}, Reward: {total_reward:.1f}, Epsilon: {epsilon:.3f})这段代码有几个关键点值得展开。gather(1, actions.unsqueeze(1))是从网络输出的 6 个 Q 值里挑出实际执行的那个动作对应的 Q 值这是 DQN 计算损失的标准操作。(1 - dones)这一项保证终止状态的 target 就是即时奖励不会加上不存在的未来价值。梯度裁剪clip_grad_norm_是防止 Q 值更新过猛导致发散max_norm 设 10 比较稳妥。3.2 学习率、折扣因子、探索率的调参经验学习率是 DQN 最敏感的参数。1e-3 是 Adam 的常用起点如果训练曲线震荡厉害就降到 1e-4如果学得太慢就升到 3e-3。我一般先用 1e-3 跑 500 局看趋势不收敛再调。折扣因子 γ 决定智能体有多看重未来奖励。0.99 意味着它愿意为了 100 步后的击杀放弃眼前的小利适合坦克大战这种需要走位瞄准的场景。如果设成 0.9智能体会变得短视看到敌人就冲不管自己血量。但 γ 也不能太接近 1否则 Q 值容易发散0.99 是个平衡点。探索率 ε 的衰减策略直接影响训练效率。从 1.0 开始每局乘 0.995大概 600 局后降到 0.05 左右。衰减太快智能体还没探索够就陷入局部最优衰减太慢则训练前期大量时间浪费在随机动作上。如果你发现智能体一直学不会开火很可能是 ε 还太高随机动作太多导致有效样本太少。3.3 训练不收敛时先查这三个地方训练 DQN 最常见的翻车现场就是 loss 不降反升或者奖励曲线一直趴在地上。遇到这种情况按顺序查三件事。第一检查奖励函数有没有设计缺陷。如果每步的时间惩罚太大智能体会学会原地不动因为动一下扣分、不动也扣分但动还可能被打。如果击杀奖励太小智能体没有动力去瞄准。我一般会把奖励打印出来看看一局下来正负奖励的比例是否合理。第二检查状态归一化。前面提过坐标和血量的数值范围差异会导致网络学偏。把所有输入都压到 [-1, 1] 或 [0, 1] 之间是最基本的操作。第三检查目标网络同步频率。同步太频繁比如每局都同步目标 Q 值变化太快训练不稳定同步太慢比如 100 局才同步目标 Q 值太陈旧学习信号滞后。TARGET_UPDATE 设 10 到 50 之间比较常见具体看单局步数。4. 避坑指南训练坦克大战智能体时我踩过的五个坑4.1 现象智能体学会原地转圈从不主动进攻原因时间惩罚系数设得太大或者击杀奖励相对太小。智能体发现原地转圈每步只扣 0.01冲上去可能被扣 10算下来还是苟着划算。解决把时间惩罚降到 0.001 甚至去掉同时提高击杀奖励到 100 以上。也可以加一个「靠近敌人」的塑形奖励每接近一点给个小正反馈引导它主动接敌。4.2 现象Q 值越来越大最后变成 NaN原因学习率太高或者没有做梯度裁剪Q 值在自举更新中不断放大。DQN 的 target 里包含了对下一状态 Q 值的估计如果估计偏高这个偏差会通过贝尔曼方程不断累积。解决学习率降到 1e-4加上clip_grad_norm_同时把目标网络的同步频率调高一些。如果还不行试试 Double DQN——用主网络选动作、目标网络算价值能有效缓解 Q 值高估。4.3 现象训练前期奖励涨得很快后面突然崩掉原因经验回放缓冲区里早期的高奖励样本被反复采样网络过拟合到那些特定场景。或者 ε 衰减太快智能体过早停止探索陷入局部最优。解决缓冲区容量别设太小保证样本多样性。ε 的衰减下限设 0.05 而不是 0保留一点随机探索。另外可以定期清空缓冲区里最旧的数据让训练分布跟上当前策略。4.4 现象智能体学会了开火但命中率极低原因动作空间里开火和移动是独立的智能体不知道开火前需要先对准方向。奖励信号太稀疏只有击中才有反馈中间过程没有引导。解决加一个「朝向与敌人夹角」的塑形奖励夹角越小奖励越高。或者把动作空间改成「移动转向开火」的组合让智能体更容易学到「先瞄准再开火」的序列。4.5 现象换了台机器重新训练结果完全不一样原因没有固定随机种子。神经网络的初始化、经验回放的采样、ε-贪婪的随机数都会影响训练轨迹。深度强化学习对随机性非常敏感同一份代码跑两次结果可能差很多。解决在代码开头固定torch.manual_seed(42)、np.random.seed(42)、random.seed(42)。虽然不能保证完全可复现CUDA 的某些操作仍有随机性但至少能把方差控制住。另外评估时用确定性策略ε0别用训练时的随机策略。5. 进阶技巧用 Double DQN 和奖励塑形把胜率再提一截基础版 DQN 跑通之后你会发现智能体虽然能打但胜率不稳定有时候会做出一些匪夷所思的决策。这时候可以上两个改进Double DQN 和奖励塑形。Double DQN 改的是 target 的计算方式。原始 DQN 用目标网络同时选动作和算价值容易高估 Q 值。Double DQN 改成用主网络选动作、目标网络算价值# 原始 DQN 的 target next_q_values target_net(next_states).max(1)[0] # Double DQN 的 target with torch.no_grad(): best_actions policy_net(next_states).argmax(1, keepdimTrue) next_q_values target_net(next_states).gather(1, best_actions).squeeze(1) target_q rewards GAMMA * next_q_values * (1 - dones)改动很小但效果明显。Q 值高估被抑制之后训练曲线更平滑最终胜率通常能提升 10 到 20 个百分点。奖励塑形是另一个立竿见影的手段。除了击中/被击中的稀疏奖励再加两个稠密奖励一是朝向奖励我方炮口方向与敌人方向的夹角余弦值每步给0.1 * cos(angle)二是距离奖励双方距离越近给的正反馈越大但别给太多否则智能体会贴脸送人头。这两个塑形项加起来能让智能体在训练早期就获得有意义的梯度信号收敛速度大概快一倍。改进项改动位置预期收益注意事项Double DQNtarget 计算抑制 Q 值高估胜率 10~20%几乎无额外计算开销朝向奖励奖励函数加快瞄准学习前期收敛快系数别超过 0.2距离奖励奖励函数鼓励主动接敌系数太大会导致贴脸送死梯度裁剪反向传播后防止 Q 值爆炸max_norm 设 10 左右目标网络软更新参数同步训练更平滑用 0.001 的软更新系数软更新是目标网络同步的另一种方式每次训练不直接复制参数而是让目标网络慢慢靠近主网络target_param 0.001 * policy_param 0.999 * target_param。相比每 N 局硬同步一次软更新让目标 Q 值的变化更连续训练曲线更稳。代价是要多写几行代码但值得。最后说一个我自己的习惯每次改完超参数先跑 200 局看奖励曲线的斜率别一上来就盯着最终胜率。强化学习的训练方差很大单次结果说明不了问题至少跑三个随机种子取平均。我一般会把每次实验的配置和曲线存下来方便回溯——这个习惯帮我省了很多「上次那个参数是多少来着」的后悔药时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表