恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
基于DQN的导弹目标选择:从MDP建模到训练调参实战
首页
资讯中心
/
基于DQN的导弹目标选择:从MDP建模到训练调参实战
基于DQN的导弹目标选择:从MDP建模到训练调参实战
发布时间:2026/10/10 14:05:53
简介这份资源面向计算机、自动化等专业的学生与开发者提供基于Python与DQN强化学习实现海防场景导弹目标选择任务的完整项目。任务中敌方舰艇以固定阵型排列我方18枚导弹需依次选择攻击目标并沿直线轨迹飞行突防时可能被防御舰艇按概率拦截舰艇被命中若干次后沉没且可通过调整不同舰艇价值来改变攻击侧重点核心是合理规划攻击目标与顺序以最大化期望伤害。资源包共474个文件约80.7MB包含15个Python源码文件、108组模型训练相关的index、data与checkpoint文件、6个yml配置、演示视频及项目文档覆盖从环境搭建到模型训练与推理的完整链路。已有40人学习关注。配套md文档与算法解析可帮助读者理解DQN网络结构、奖励设计与训练流程源码经测试可直接运行适合作为毕业设计、课程设计或强化学习入门项目的参考模板并在此基础上延伸改造。1. 从导弹目标选择说起为什么用 DQN 而不是规则表导弹目标选择本质是一个在多个候选目标之间做动态决策的问题。目标有威胁度、距离、角度、机动状态我方有剩余燃料、时间窗口、突防概率这些量互相耦合靠一张 if-else 规则表根本写不完。规则表在目标数少、态势简单时能跑一旦目标数上到十几个、态势每秒都在变规则就会互相打架出现「该打的不打、不该打的抢着打」这种翻车现场。强化学习把这件事建模成序贯决策状态是当前战场态势动作是选哪个目标奖励由毁伤效果、资源消耗、时间成本共同决定。DQN 用神经网络逼近 Q 值能在高维状态里学到「先打谁、后打谁」的策略而不是靠人把规则一条条码出来。这套思路适合做毕业设计、课程设计也适合想入门强化学习又不想只跑 CartPole 的开发者。下面从环境建模一路讲到训练、调参和避坑源码和文档按常见工程结构组织你可以照着复现。2. 把导弹目标选择建成 MDP状态、动作、奖励怎么定2.1 状态空间别把原始坐标直接塞进网络状态设计是这类任务里最容易埋雷的地方。直接把所有目标的绝对坐标丢进网络模型学到的往往是「目标在屏幕哪个位置」而不是「目标相对我方的威胁关系」。常见做法是做相对量归一化每个目标相对我方的位置差、速度差、距离、角度、威胁度拼成一个定长向量。假设场景里最多 N 个目标每个目标取 6 个特征相对距离、相对方位角、相对速度、威胁度、是否已被打击、剩余拦截时间。加上我方自身的 3 个状态量剩余燃料、当前速度、剩余时间状态维度就是 6N3。N 取 8 时维度 51对 DQN 来说刚好再大就要考虑用注意力或图网络了。import numpy as np def build_state(missile, targets, max_targets8): # missile: dict, targets: list of dict feats [] for i in range(max_targets): if i len(targets): t targets[i] dx t[x] - missile[x] dy t[y] - missile[y] dist np.hypot(dx, dy) / 1000.0 # 归一化到千米量级 angle np.arctan2(dy, dx) / np.pi # 归一化到 [-1,1] dv (t[v] - missile[v]) / 300.0 feats [dist, angle, dv, t[threat], t[hit], t[ttl] / 60.0] else: feats [0.0] * 6 # 空位补零保证定长 self_state [missile[fuel] / 100.0, missile[v] / 300.0, missile[t_left] / 60.0] return np.array(feats self_state, dtypenp.float32)逻辑说明每个目标用 6 维相对特征描述空位补零让状态维度固定这是 DQN 全连接网络能吃的格式。参数上距离除以 1000、速度除以 300、时间除以 60都是把量纲压到 0 到 1 附近避免某一维数值过大主导梯度。威胁度和 hit 标志本身就在 0 到 1不用再处理。如果你把 max_targets 改成 12网络输入层跟着改就行但训练样本需求也会上升。2.2 动作空间与掩码无效目标必须屏蔽动作就是「选第 i 个目标」动作数等于 max_targets。但已经打掉的目标、超出射程的目标不能再选否则模型会学到「反复选一个死目标」这种废策略。做法是加动作掩码在选动作和算 target Q 时把无效动作的 Q 值设成负无穷。def act_with_mask(policy_net, state, valid_mask, epsilon): if np.random.rand() epsilon: valid_idx np.where(valid_mask)[0] return int(np.random.choice(valid_idx)) import torch with torch.no_grad(): q policy_net(torch.from_numpy(state).unsqueeze(0)) q q.squeeze(0).numpy() q[~valid_mask] -1e9 # 屏蔽无效动作 return int(np.argmax(q))逻辑说明valid_mask 是布尔数组True 表示该目标可选。探索时只在有效动作里随机利用时把无效动作 Q 值压到负无穷再取 argmax。参数 1e9 只要远大于正常 Q 值范围即可别用 float(-inf)某些框架在反向传播时会出 NaN。这个掩码在计算 TD 目标时也要用否则 target Q 会从无效动作里取最大值训练直接跑偏。2.3 奖励函数稀疏奖励是训练不收敛的头号原因如果只在命中目标时给 1其余时刻给 0DQN 在几十步的回合里几乎学不到东西这就是稀疏奖励的坑。常见做法是塑形奖励命中给大正奖脱靶或超时给负奖每一步根据距离变化、威胁度下降给小额引导。def compute_reward(prev_dist, curr_dist, hit, threat_drop, fuel_used, done): r 0.0 r (prev_dist - curr_dist) * 0.1 # 接近目标给正引导 r threat_drop * 2.0 # 威胁度下降给奖励 r - fuel_used * 0.05 # 燃料消耗给惩罚 if hit: r 10.0 if done and not hit: r - 5.0 return r逻辑说明距离项系数 0.1、威胁项 2.0、燃料项 0.05、命中 10、失败 -5这组数是经验值不是唯一解。原则是命中奖励要明显大于过程奖励之和否则模型会学会「绕圈刷距离奖励」而不去命中。调参时先固定命中奖励再调过程项系数观察回合回报曲线是否稳定上升。如果回报震荡先把过程奖励整体调小。3. DQN 网络与训练循环从经验回放到目标网络3.1 网络结构两层全连接够用别一上来就上 CNN状态是定长向量不是图像用全连接网络就够。常见结构是输入层 → 128 → 128 → 动作数中间用 ReLU。层数再深在小规模任务上收益不明显反而更容易过拟合。输出层不加激活因为 Q 值可正可负。import torch import torch.nn as nn class QNet(nn.Module): def __init__(self, state_dim, action_dim): 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)逻辑说明state_dim 由 2.1 的状态维度决定action_dim 等于 max_targets。隐藏层 128 是这类任务的常用起点状态维度到 100 以上可以加到 256。初始化用 PyTorch 默认的 Kaiming 就行不用手动改。如果你发现 loss 一直不降先检查 state_dim 和实际输入维度是否一致这是最常见的低级错误。3.2 经验回放与目标网络两个稳定训练的关键件DQN 相比普通 Q 学习靠的就是经验回放池和目标网络。回放池打乱样本相关性目标网络延迟更新避免 Q 值追着自己跑。回放池容量常见 10000 到 50000目标网络每 200 到 500 步同步一次。import random from collections import deque class ReplayBuffer: def __init__(self, capacity20000): self.buf deque(maxlencapacity) def push(self, s, a, r, s_next, done, mask_next): self.buf.append((s, a, r, s_next, done, mask_next)) def sample(self, batch_size64): batch random.sample(self.buf, batch_size) s, a, r, s_next, done, mask_next zip(*batch) return (np.stack(s), np.array(a), np.array(r, dtypenp.float32), np.stack(s_next), np.array(done, dtypenp.float32), np.stack(mask_next))逻辑说明capacity 取 20000 是中等规模任务的稳妥值太小样本相关性去不掉太大旧策略样本拖后腿。batch_size 64 是常见起点显存够可以上 128。mask_next 存下来是为了算 target Q 时屏蔽无效动作这一步漏了训练会不稳定。push 时如果池满deque 自动丢最旧的不用手动管理。3.3 训练循环TD 目标、损失和软更新训练循环里每一步做四件事和环境交互存样本、从池里采样、算 TD 目标、反向传播。目标网络用软更新或硬更新都行硬更新实现简单软更新更平滑。def train_step(policy_net, target_net, optimizer, batch, gamma0.99): s, a, r, s_next, done, mask_next batch s torch.as_tensor(s) s_next torch.as_tensor(s_next) a torch.as_tensor(a, dtypetorch.long) r torch.as_tensor(r) done torch.as_tensor(done) mask_next torch.as_tensor(mask_next) q policy_net(s).gather(1, a.unsqueeze(1)).squeeze(1) with torch.no_grad(): q_next target_net(s_next) q_next[~mask_next] -1e9 q_next_max q_next.max(dim1)[0] target r gamma * q_next_max * (1 - done) loss nn.functional.smooth_l1_loss(q, target) optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(policy_net.parameters(), 10.0) optimizer.step() return loss.item()逻辑说明gamma 取 0.99 表示看重长期回报回合步数短可以降到 0.95。smooth_l1_loss 比 MSE 对异常 TD 误差更稳。梯度裁剪阈值 10.0 是防梯度爆炸的后悔药尤其奖励尺度大时必加。mask_next 在 target 计算里同样要屏蔽否则 target 会高估。目标网络同步建议每 300 步硬拷贝一次或者用 tau0.005 软更新。4. 训练不收敛、奖励震荡怎么排查5 个血泪踩坑记录4.1 现象回报曲线一直贴地loss 不降原因奖励太稀疏或者状态归一化没做某一维数值几百梯度被带偏。解决先确认状态每一维都在相近量级打印 state 的 min/max再把过程奖励加上让模型每步都有反馈。如果还不行把学习率从 1e-3 降到 1e-4 试一轮。4.2 现象Q 值越来越大最后变成 NaN原因TD 目标里没屏蔽无效动作或者没做梯度裁剪Q 值自我放大。解决检查 target 计算是否用了 mask_next加上梯度裁剪奖励整体缩放到个位数。Q 值正常范围应该在几十以内超过几百就要警惕。4.3 现象模型学会「绕圈」不命中原因距离引导奖励给太多命中奖励相对不够大模型发现刷距离比命中更划算。解决把距离项系数调小命中奖励调大或者给回合设最大步数超时直接负奖励。这是奖励塑形里最经典的翻车调参时盯住命中率而不是只看回报。4.4 现象训练前期还行后期突然崩原因回放池里旧策略样本太多或者目标网络太久没同步。解决缩小回放池容量提高目标网络同步频率或者用软更新。如果崩之前刚好改了奖励先回退奖励再观察。4.5 现象换一组目标数就完全不能用原因状态维度写死网络输入层和实际状态不匹配或者归一化系数是按旧场景定的。解决把 max_targets 做成配置项状态构建和网络初始化都读同一个配置归一化系数按新场景重新统计。别把维度硬编码在多个文件里这是维护噩梦。5. 进阶技巧用双 DQN 和优先回放把命中率再抬一档基础 DQN 跑通之后想再提命中率两个改动性价比最高Double DQN 和优先经验回放。Double DQN 解决 Q 值高估问题改动很小——用策略网络选动作用目标网络算该动作的 Q 值。with torch.no_grad(): q_next_policy policy_net(s_next) q_next_policy[~mask_next] -1e9 best_a q_next_policy.argmax(dim1, keepdimTrue) q_next_target target_net(s_next) q_next_target[~mask_next] -1e9 q_next_max q_next_target.gather(1, best_a).squeeze(1) target r gamma * q_next_max * (1 - done)逻辑说明和 3.3 的区别只在 target 计算这两行。argmax 来自策略网络取值来自目标网络这样能压住高估。实测在目标选择任务里命中率通常能涨几个百分点训练也更稳。优先回放按 TD 误差给样本加权误差大的样本多采。实现上可以用 SumTree也可以用简化版按概率采样。下面给一个不依赖第三方库的简化实现思路。class PrioritizedBuffer: def __init__(self, capacity20000, alpha0.6): self.capacity capacity self.alpha alpha self.buf [] self.priorities np.zeros(capacity, dtypenp.float32) self.pos 0 def push(self, transition, td_error1.0): if len(self.buf) self.capacity: self.buf.append(transition) else: self.buf[self.pos] transition self.priorities[self.pos] (abs(td_error) 1e-5) ** self.alpha self.pos (self.pos 1) % self.capacity def sample(self, batch_size64, beta0.4): p self.priorities[:len(self.buf)] p p / p.sum() idx np.random.choice(len(self.buf), batch_size, pp) samples [self.buf[i] for i in idx] weights (len(self.buf) * p[idx]) ** (-beta) weights weights / weights.max() return samples, idx, weights.astype(np.float32)逻辑说明alpha 控制优先级强度0.6 是常用值取 0 就退化成均匀采样。beta 控制重要性采样权重训练初期取 0.4后期可以线性升到 1.0。weights 要乘到 loss 上否则优先采样会引入偏差。这个实现每次 sample 都重算概率样本量大时慢但两万容量够用。验证改动有没有效别只看回报曲线要看三个指标命中率、平均回合步数、无效动作占比。命中率涨、步数降、无效动作接近零才算真提升。我一般会固定随机种子跑三组取平均单次结果好看不算数。最后说个习惯每次改奖励或网络结构先把配置和随机种子记下来跑完对比。强化学习的玄学时刻很多没有记录根本分不清是改动生效还是随机波动。这套导弹目标选择的 DQN 方案跑通不难跑稳靠的是这些细节。希望帮到你。本文还有配套的精品资源点击获取