
简介面向强化学习与智能决策领域研究者代码包实现了Q学习算法在空战对抗场景中的机动决策仿真。核心控制逻辑由Python编写采用参数化编程风格各项训练参数可方便更改并配合详尽注释与可直接运行的案例数据适合无人机博弈、飞行器自主决策方向的算法验证与教学演示。压缩包内共384个文件整体大小约16.28MB其中276个py源文件构成算法主体67个pyc为编译产物16个xml与6个txt用于环境配置与数据说明7个md提供文档指引另有ckpt模型权重及sh运行脚本便于复现完整的训练与推理流程。内容还保留TensorFlow事件记录可查看模型收敛曲线并复盘结果。目前已有91人学习下载适合需要可读性较强、结构清晰的Q学习空战项目作为修改基础的开发者。1. Q学习实现空战对抗用一张表教会飞机自主占位攻击把空战对抗简化为二维平面上的追击占位问题Q学习Q-Learning是强化学习里最经典的免模型算法几十行Python代码、一张状态动作价值表就能让一架战机学会调整航向、控制速度和对方缠斗直到进入武器包线完成攻击。这个方案特别适合两类人——刚学完强化学习理论、想跳过CartPole直接上手对抗场景的学习者以及在仿真里需要可解释baseline、暂时不想上神经网络的从业者。难点不在算法本身而在状态空间离散化和奖励函数的设计这两个点直接决定训练能不能收敛。2. 空战对抗建模状态空间、动作空间与奖励函数怎么定2.1 为什么空战对抗这个场景适合用Q学习而不是规则表空战本质上是一个序贯决策问题战机每隔一小段时间就要根据当前态势选择一个机动动作动作的影响要过几秒才显现。传统做法是写决策树或有限状态机把每种态势映射到固定动作。一个简单的追击规则表确实能跑但只要初始态势一变——敌机从正前方变成侧方或者双方速度差拉大规则表的表现立刻崩掉。Q学习把这些手工规则变成一张自动更新的表智能体不需要知道对手的完整逻辑只需要不断试错从奖励信号里反推每个动作的长期价值。另一个关键前提是环境可被视作单智能体MDP。空战仿真里如果对手也在实时学习就成了双智能体博弈问题Q表不一定收敛。常见的做法是把对手固定成一个带简单规则的AI——比如始终朝我机方向飞、速度恒定——这样整个环境等价于单智能体强化学习Q学习的收敛性才有保障。训练完成后再把这个固定对手换成更难缠的规则策略来评估泛化能力。2.2 状态空间离散化把连续态势切成Q表的行Q学习是查表算法表的一个维度对应一种状态特征。空战中连续量很多相对方位角、相对距离、双方速度差、双高度差。二维平面场景可以去掉高度通常取三个核心特征。离散化做得好不好直接决定Q表的规模和学习速度。分档太少不同态势被混在一起Q值互相干扰分档太多Q表维度爆炸样本永远填不满。我一般的设计是相对方位角分8档、相对距离分8档、速度差分5档总共320个状态。相对距离要和场景尺度匹配仿真区域4000单位武器射程300单位追击、缠斗、脱离几个关键区间至少要各占一档。下面这段代码是状态离散化的核心逻辑import numpy as np N_ANGLE, N_DIST, N_DIFF 8, 8, 5 # 每维的分档数 angle_bins np.linspace(-np.pi, np.pi, N_ANGLE 1) dist_bins np.linspace(0, 4000.0, N_DIST 1) diff_bins np.linspace(-200.0, 200.0, N_DIFF 1) def get_state_idx(rel_angle, dist, speed_diff): # 归一化到 [0, N-1] 区间np.digitize 的末端边界会返回 N要裁掉 a_idx min(np.digitize(rel_angle, angle_bins) - 1, N_ANGLE - 1) d_idx min(np.digitize(dist, dist_bins) - 1, N_DIST - 1) s_idx min(np.digitize(speed_diff, diff_bins) - 1, N_DIFF - 1) return a_idx * N_DIST * N_DIFF d_idx * N_DIFF s_idx这里有个很容易踩的坑np.digitize对恰好等于最大值边界的情况会返回N不加min裁剪后面索引Q表时直接越界。把三个离散指标合并成一个state_idx是为了让Q表保持二维结构——行是状态列是动作这样q_table[state_idx, action]的定位效率最高也方便后续做np.argmax。2.3 动作空间与奖励函数角度优势优先于距离优势动作空间设计成五个离散动作左转、右转、加速、减速、保持航向。转弯用最大角速度固定偏转这是空战仿真里最常见的简化做法——实际空战飞行员做的是最大过载转弯仿真里等效为固定角速度偏转。速度和转弯率都设上限模拟真实飞行包线。奖励函数是整个项目的灵魂也是最玄学的部分。经验是分阶奖励比稀疏事件奖励可靠得多如果只有命中给100、被击中给-100前期随机试探几百局都碰不到一次命中Q表完全学不到梯度。常见做法是把奖励拆成两部分。角度奖励用cos(相对方位角)机头正对目标时取最大值1距离奖励以理想攻击距离300为中心设计一个倒钟形太远拿不到奖励太近同样扣分。再加命中100、被命中-100的大事件奖励权重上事件奖励要远大于过程奖励否则智能体学不到打死对手这个终极目标。def compute_reward(rel_angle, dist, hitFalse, be_hitFalse): angle_reward np.cos(rel_angle) # 机头对准目标 - 1 dist_reward 1.0 - 2.0 * abs(dist - 300.0) / 4000.0 # 越接近300越接近1 r 0.6 * angle_reward 0.4 * dist_reward if hit: r 100.0 if be_hit: r - 100.0 return r两个权重的含义值得强调角度系数0.6高于距离系数0.4意思是宁可距离不完美也要先把机头对准对方这符合空战中先占位再攻击的基本原则。权重可以调但角度优先这个次序在二维空战里基本不要动。2.4 Q值更新一张表是怎么被填出来的Q表初始化为全零矩阵。如果全部初始化为一个较大常数前期的探索会更激进全零初始化则偏保守智能体在没见过的状态下倾向维持已有经验。两种初始化各有适用场景二维空战里全零初始化配合高探索率效果更稳。训练时每走一步执行一次Q值更新新Q值等于旧Q值加上学习率乘以时序差分误差。关键点在于下一状态的最大Q值这一项——它把未来收益递归地带回当前状态这正是Q学习能处理延迟回报的机制。写在代码里就是四行核心运算。3. 用Python写空战仿真环境运动方程、攻击判定与回合控制3.1 飞机运动学模型二维空战仿真的飞机运动学不复杂每个时刻根据当前速度和航向在x、y轴上积分位移动作通过改变航向或速度间接影响下一帧的位置。代码里我用dt0.1步长做离散积分1秒仿真10帧一局60秒最多600步训练速度和物理精度都能接受。class Fighter: def __init__(self, x, y, heading, speed): self.x, self.y x, y self.heading heading # 弧度0表示朝x轴正方向 self.speed speed # 单位/秒 self.max_turn np.radians(60.0) # 最大转向角速度弧度/秒 self.max_speed 600.0 self.min_speed 150.0 def step(self, action, dt0.1): # 动作: 0左转, 1右转, 2加速, 3减速, 4保持 if action 0: self.heading - self.max_turn * dt elif action 1: self.heading self.max_turn * dt elif action 2: self.speed min(self.max_speed, self.speed 300 * dt) elif action 3: self.speed max(self.min_speed, self.speed - 300 * dt) # 按当前速度与航向积分位移 self.x self.speed * np.cos(self.heading) * dt self.y self.speed * np.sin(self.heading) * dt加速和减速的步长设成300 * dt即每秒速度变化300单位大约2秒能从最小速度拉到最大速度这个灵敏度够让智能体做出明显的速度策略又不至于一帧内速度剧烈跳变。注意转弯和加减速是互斥动作避免同时转向和变速带来的复合效应简化了Q表的学习难度。3.2 相对几何计算与攻击判定攻击判定是整个环境里最容易写错的部分。判定的核心条件有两个一是目标机头指向我机的角度差用相对方位角度量小于攻击锥角二是双方距离小于武器射程。很多新手第一个版本只判断距离忽略了方位角导致智能体学会了飞到敌人旁边而不是咬住敌人尾巴。def attack_judge(mine, enemy, weapon_range300.0): dx, dy enemy.x - mine.x, enemy.y - mine.y dist np.hypot(dx, dy) sight_angle np.arctan2(dy, dx) # 视线角绝对坐标系 rel_angle (sight_angle - mine.heading np.pi) % (2 * np.pi) - np.pi # 相对方位角绝对值小于30度60度攻击锥且距离在射程内 if abs(rel_angle) np.radians(30.0) and dist weapon_range: return True, dist, rel_angle return False, dist, rel_angle攻击锥设成60度全角机头左右各30度这个值比真实的导弹离轴发射角小一些但恰好能让Q学习收敛出的策略是尽量绕到敌机正后方而不是从正面迎头对冲。如果要模拟超视距攻击可以把攻击角放宽到90度但那样战术就变成迎头对飞了观赏性和学习难度都不如咬尾追击。3.3 回合初始化与一局空战的流程每局开始我机在左下角敌机在右上角双方都是随机航向、随机速度但保证初始距离在1500到2500之间。这个区间的设定很重要太近一开局就在射程内智能体没机会学习机动太远前面几十步都在匀速接近有效学习样本变少。一局空战的主循环逻辑是我机根据策略选动作并step敌机用固定规则step始终转朝向我机并全速接近计算相对角度与距离判断命中或被命中最后更新Q表和状态。加了最大步数600的上限防止双方兜圈子死循环。每局结束记录总奖励用于观察学习曲线。def reset_episode(): mine Fighter(0, 0, np.random.uniform(-np.pi, np.pi), np.random.uniform(300, 500)) enemy Fighter(np.random.uniform(1500, 2500), np.random.uniform(1500, 2500), np.random.uniform(-np.pi, np.pi), np.random.uniform(300, 500)) # 确保敌机初始距离在合理范围 while np.hypot(enemy.x - mine.x, enemy.y - mine.y) 1500: enemy.x np.random.uniform(1500, 2500) enemy.y np.random.uniform(1500, 2500) return mine, enemy注意敌方初始位置限制在x、y都在1500到2500的区块而不是整个仿真区域均匀撒点这样每局初始态势差异可控训练过程稳定。随机起手让智能体不会过拟合到单一初始态势这是后面评估泛化能力的基础。4. Q学习训练主循环三参数调优与日志绘制4.1 训练主循环代码主循环写在train_qlearning.py里训练回合数设2000到5000之间。每回合先重置环境然后进入步进循环核心更新就四行但周围的选择逻辑和探索率控制才是决定能否收敛的关键。q_table np.zeros((N_ANGLE * N_DIST * N_DIFF, 5)) alpha, gamma 0.15, 0.95 # 学习率、折扣因子 epsilon, eps_min, eps_decay 1.0, 0.05, 0.998 for episode in range(3000): mine, enemy reset_episode() state get_state_idx(env_rel_angle(mine, enemy), dist, diff) total_reward, done 0, False while not done: # epsilon贪心随机探索或按Q表利用 if np.random.rand() epsilon: action np.random.randint(5) else: action np.argmax(q_table[state]) # 执行动作并计算奖励 mine.step(action) enemy_step(enemy, mine) # 敌机规则AI: 转向接近 hit, dist, rel_angle attack_judge(mine, enemy) be_hit, _, _ attack_judge(enemy, mine) done hit or be_hit or step_count 600 reward compute_reward(rel_angle, dist, hit, be_hit) next_state get_state_idx(rel_angle, dist, mine.speed - enemy.speed) # Q学习更新公式 q_table[state, action] alpha * ( reward gamma * np.max(q_table[next_state]) - q_table[state, action] ) state next_state total_reward reward epsilon max(eps_min, epsilon * eps_decay)整个训练循环里最有技术含量的一行是q_table[state, action] ...后面的时序差分目标reward gamma * np.max(q_table[next_state])。np.max取下一状态所有动作的最大Q值代表从下一状态开始用最优策略能拿到的未来收益这一项让当前状态的动作能间接学习到几步之后才发生的命中奖励。4.2 三个关键参数的设置与调优三个参数怎么设直接看下表。这些参数是业内常见经验值但不是银弹不同状态分档数、不同奖励权重下要微调。参数经验范围说明学习率α0.1 ~ 0.2太大Q值震荡剧烈太小收敛太慢折扣因子γ0.9 ~ 0.99决定未来奖励的衰减速度空战里0.95就够探索率ε衰减0.995 ~ 0.999衰减到0.05后停止衰减保留最低探索度学习率0.15是一个平衡点前期训练时每次更新迈的步子够大学到后面又不会因为单次意外奖励大幅改写已有Q值。折扣因子取0.95意味着10步之后的奖励折算成当前的0.6倍左右一局600步里这个远期视野足够让智能体学到绕后需要时间但值得等待。探索率从1.0起步每回合乘以0.998大约500回合后降到0.61500回合后到0.1附近这个节奏刚好匹配前期大量试探、后期利用经验的训练范式。4.3 训练日志与曲线绘制避坑训练日志至少要记录三项每回合总奖励、每回合是否命中、当前探索率。总奖励曲线能看整体趋势命中率曲线看策略是否真的打到了人二者可能出现背离——比如奖励曲线上升但命中率没变化说明智能体学会了靠近但没学会精确瞄准。画训练曲线时有个真实痛点3000回合的奖励曲线横坐标会堆得极密尤其是用matplotlib默认绘图时前面的点挤成一根黑柱子完全看不出下降趋势。常见做法不是降低绘图频率而是做等间隔抽样加移动平均平滑import matplotlib.pyplot as plt import pandas as pd rewards pd.Series(episode_rewards) smoothed rewards.rolling(50).mean() # 50回合移动平均压掉噪声 plt.plot(smoothed[::10]) # 每10个点采一次样 plt.xlabel(episode (x10)) plt.ylabel(smoothed reward) plt.title(Q-learning training curve) plt.savefig(training_curve.png, dpi150)rolling(50).mean()做移动平均把单回合奖励的高频抖动压平[::10]做等比抽样把横坐标密度降一个量级曲线可读性立刻上来。这一步不复杂但很多初学者在这里耗掉半天时间。5. Q学习空战实战的五个常见坑现象到排查5.1 训练几百回合奖励纹丝不动现象总奖励曲线一直在负值区间震荡没有上升趋势。原因十有八九是奖励函数太稀疏或者权重失衡——命中奖励100确实存在但前期随机策略下几百步都摸不到一次命中Q表根本没机会把绕后行为和一个正数关联起来。解决检查过程奖励是否覆盖了整个状态空间。角度奖励和距离奖励加进去后智能体每步都能获得一个可分的反馈信号梯度自然就出现了。再不行就降低事件奖励的权重到50让过程奖励的梯度占主导。5.2 状态索引进到Q表外面现象训练中段突然抛出IndexError或者是numpy数组越界。原因几乎都出在np.digitize的边界返回上——某个状态值刚好等于bins[-1]时返回N。解决在get_state_idx里对每个维度的结果做min(idx, N-1)裁剪。我在代码里就是把这个坑提前埋了但不提醒的话很多人会自己写一遍然后踩一次。5.3 探索率衰减过快策略锁死现象前1000回合学得还行但之后命中率一直卡住上不去。看日志发现epsilon早就衰减到0.05了。解决把衰减系数从0.998调慢到0.999。更稳的方案是分阶段衰减前1000回合保持0.8以上之后每回合衰减0.999。空战这种对抗场景敌方规则AI的战术单一但初始态势随机性大过快的探索衰减会让智能体只学会了应对一种初始态势换起手就懵。5.4 奖励权重不对智能体变成距离狂现象命中率上升但战术很怪——飞机一直保持正对敌机飞行、距离却不拉近像中世纪骑士对冲。原因角度奖励权重太大导致智能体发现只要机头对准就有奖励不再需要靠近到射程内发起攻击绕后和追击行为消失。解决把距离奖励的权重提上去特别是要保持距离项在理想射程300处的峰值足够突出。我一般再补一条逻辑距离超过武器射程1.5倍时距离奖励直接设为-0.5的常数强逼智能体靠近。5.5 训练曲线看起来收敛但换初始态势就翻车现象训练时用固定起手点位曲线漂亮得很一换初始位置智能体立刻变成无头苍蝇。原因是对单一起手的过拟合——固定初始态势让Q表里的边缘状态从未被访问那个区域的Q值一直是零零值在np.argmax里被当作最高优先级处理于是动作选择变成随机。解决训练阶段就引入随机初始态势每局随机化我机和敌机的位置、航向、速度让Q表被充分覆盖。评估时再用训练时没出现过的起手点位测试统计击杀率才算数。6. 评估你的空战智能体击杀率才是唯一的验收标准训练曲线好看不等于策略好用。我一般把训练完的Q表拿去做独立评估固定一组全新初始态势让智能体以完全贪心策略epsilon0和敌机各打50局统计击杀率、平均被击杀率、平均每局步数三个数字。只有击杀率超过70%的策略才值得存下来否则就是训练日志里自嗨的产物。def evaluate(q_table, n_episodes50): wins 0 for _ in range(n_episodes): mine, enemy reset_episode() done False while not done: state get_state_idx(rel_angle, dist, speed_diff) action np.argmax(q_table[state]) # 纯利用 mine.step(action) enemy_step(enemy, mine) hit, dist, rel_angle attack_judge(mine, enemy) be_hit, _, _ attack_judge(enemy, mine) done hit or be_hit or step_count 600 if hit: wins 1 return wins / n_episodes评估时把action选择里去掉随机分支只用np.argmax这样才能反映训练后策略的真实水平。我自己的血泪经验是训练时很容易被看起来像样的奖励曲线骗到押上50局评估不行的策略立刻现原形。这个方向整体难在调参和建模核心算法反而是最简单的一环。我的习惯是每改动一个奖励权重或状态分档就跑一次完整训练加一次评估所有结果记录在一个表格里不靠感觉调参。如果你跑完发现击杀率一直在50%附近下不去优先检查敌方AI是不是太强——把敌方转弯率降20%通常立刻能看到明显提升。希望帮到你。本文还有配套的精品资源点击获取