☰
空战对抗中的Q学习:从Q表到状态离散化与奖励塑形实战解析
2026/10/4 1:02:07 网站建设 项目流程

简介:面向空战博弈与强化学习研究者,这份工程以Q学习算法为核心,实现了智能体在空战对抗环境中的自主决策,并附完整Python代码。资源共384个文件,压缩包大小16.28MB,其中包含276个Python脚本、67个pyc编译文件、16个XML环境参数配置、7个Markdown说明文档、6个文本文件以及训练生成的ckpt权重和TensorBoard事件文件;从算法实现、环境配置到训练记录一应俱全。已有91人下载学习,适合希望快速上手强化学习空战项目的开发者。代码采用参数化编程,注释详尽,调参方便,可直接运行案例数据复现对抗效果;Markdown文档梳理了代码结构与使用要点,XML文件定义了仿真环境,ckpt权重支持加载已训练的模型继续迭代。读者还可借助TensorBoard观察训练曲线,深入理解Q学习的收敛过程,并在此基础上修改奖励函数、调整探索策略,开展更深入的算法对比与改进实验。

1. 空战对抗里的Q学习:不靠战术公式,靠一张Q表打赢缠斗

先泼一盆冷水:如果你期待用Q学习让无人机像王牌飞行员一样做眼镜蛇机动,那大概率会翻车。Q学习擅长的是在状态空间和动作空间都被切碎之后,通过反复试错学出一个“查表式”的格斗策略,而不是在连续高动态环境中做端到端控制。这个标题里的“空战对抗”指的是简化后的单机对单机博弈:双方在同一片空域,用一个固定或者半智能的敌机策略,让我方通过奖励信号学会咬尾、占位、脱离锁定这些基本动作。

适合读这篇的人,是手里已经有Python基础、想从零把强化学习的经典算法落进一个看得见博弈过程的仿真场景里,并搞清楚“状态怎么做网格化、奖励怎么设计、参数怎么调、为什么训练曲线跑飞”的这些问题的从业者。这里的Q学习不是噱头,它是整个博弈闭环里的决策核心;把这张Q表喂好,比调一堆超参数更能直接改变对抗胜率。

2. 空战问题怎么改写成Q学习能吃的状态、动作和奖励

2.1 空战对抗为什么适合用Q学习:从MDP五元组说起

强化学习里最常见的建模方式是马尔可夫决策过程,空战对抗的单个决策周期恰好能对应上这个五元组:状态、动作、转移概率、奖励、折扣因子。敌我双方每一时刻都有一个联合状态,我方根据这个状态选择一个机动动作,环境返回新的状态和一个即时奖励。对Q学习来说,它不需要知道敌机的真实控制逻辑,也不需要显式建模空气动力学模型,它只需要“当前长什么样、做了什么、拿到多少分、接下来变成什么样”,这就是model-free的核心优势。

很多做过工程的人会问:空战是连续状态连续动作,Q学习不是只能处理离散的吗?答案是先把连续空间切成离散网格,这也是这个标题里能给出Python代码、并且能在一台普通笔记本上跑起来的原因。状态网格化的代价是精度,换来的是可复现和可解释。你不必去对抗复杂的气动模型,先在一个二维平面上验证博弈逻辑,再把状态维度和网格分辨率逐步加回来。这个简化路线对于研发验证阶段完全够用。

2.2 状态空间设计:位置、速度、航向和相对几何关系

设计状态空间时,最容易犯的错是“什么都往里塞”:绝对坐标、绝对速度、绝对航向全部进Q表,结果状态数量爆炸,训练一万个回合都在重复相同轨迹。我一般会优先取“相对状态”,因为它天然消除了地图位置带来的冗余,也让Q表泛化能力更强。最常用的最小状态集合是相对距离、相对方位角、速度差、高度差,如果是二维平台就省掉高度差。

下面这个离散化函数可以说明怎么把连续量变成表格索引:

def discretize_state(rel_dist, rel_angle, speed_diff, grid_size=8): dist_bin = min(int(rel_dist / 500.0), grid_size - 1) angle_bin = int((rel_angle + np.pi) / (2 * np.pi / grid_size)) % grid_size speed_bin = np.clip(int((speed_diff + 100) / 25), 0, grid_size - 1) return dist_bin, angle_bin, speed_bin

逻辑上,rel_dist的单位是米,500米一格,超过范围就截断到最后一格;rel_angle的取值范围是[-pi, pi],被均匀切成8个扇区,对接近正尾后的角度和正头前的角度做了区分;speed_diff被限制在-100到100之间,每25米/秒一格。这套离散化参数看起来“拍脑袋”,但它决定Q表的状态总量:8的3次方等于512个状态,每个状态有5个动作,整张表只有2560个格值,训练效率会高很多。

2.3 动作空间与奖励塑形:赢一局才能拿到奖励,太稀疏了

空战动作如果做成连续油门加连续舵面,Q学习基本学不动。常见做法是把动作离散成几个战术级的基本机动:左侧转、右侧转、平飞、加速、减速。方向上如果把三维动作压到二维,就只保留航向变化;如果后面要加高度维度,再加一个爬升/俯冲动作。动作不是越细越好,动作数越多,每一个动作被采样到的次数越少,训练方差就越大。

奖励设计是一个典型“玄学”环节。空战最自然的奖励是“击落敌机+1,被击落-1”,但这个奖励太稀疏了,可能几百个回合里一次都没打中,Q表完全得不到有效梯度。实际工程里需要做奖励塑形,也就是把中期过程信号拆出来。下面这段奖励函数是一个可用的起点:

def compute_reward(prev_dist, now_dist, angle_to_enemy, locked_on): reward = 0.0 reward += 0.5 * (prev_dist - now_dist) / 100.0 # 接近奖励 if abs(angle_to_enemy) < np.pi / 6: reward += 0.3 # 进入敌机后向6点钟区域 if locked_on: reward -= 0.5 # 被敌机锁定,给负反馈 return reward

这个函数的核心逻辑是给“逼近”的每一步都发小额正奖励,给“进入尾巴区域”发额外奖励,给“被锁定”施加惩罚。注意这里没有把击落事件当成唯一奖励,而是在击落时额外再加一个大的正奖励;否则Q表会倾向于原地绕圈,因为绕圈也能靠接近奖励刷分。奖励塑形最怕的是agent找到“刷分漏洞”,比如反复横跳获得接近奖励,这一点在后面的避坑章节会专门展开。

2.4 为什么先选Q表而不是DQN或PPO:可解释性和调参成本

同类任务里DQN和PPO现在更常见,但在这个标题限定“附python代码”、而且是教学和验证导向的场景下,经典Q学习反而更合理。Q表是一个完全透明的决策矩阵,某个状态下最优动作是什么、Q值是多少,可以直接打印出来看;而DQN是个黑匣子,你看到一个loss数字根本无法判断它是否学会了“咬尾”。PPO虽然训练稳定,但要配神经网络、GAE、clip参数,调试成本高出一大截。

另一个关键点是训练效率。Q表更新是单步时序差分,每次交互立刻回传,不需要像DQN那样维护经验回放缓冲区。对状态空间只有几百到几千个格子的空战简化环境,Q表通常几千个回合就能看到明确趋势;如果换成神经网络,同样的训练量大概率还在欠拟合。当你把Q表跑明白了,再去切DQN,你会很清楚哪些问题来自环境建模,哪些问题来自网络结构,而不是所有锅都甩给“神经网络玄学”。

3. 最小可跑的Python实现:从飞机运动模型到Q表更新全流程

3.1 环境搭建与坐标约定:先用NumPy写一个轻量二维对战环

不要一上来就装物理引擎,空战对抗验证阶段用NumPy手写运动学就够。下面这个环境模拟了二维平面上的追逐:飞机用位置、航向、速度三个量描述,转向通过最大角速度限制,速度通过油门增减。代码里刻意省略了重力、惯性和延迟,因为这些因素在当前阶段只会干扰对强化学习逻辑的理解。

import numpy as np class AirCombatEnv: def __init__(self, dt=0.5, max_turn=0.5, max_speed=300.0, min_speed=80.0): self.dt = dt self.max_turn = max_turn self.max_speed = max_speed self.min_speed = min_speed self.my_pos = None self.my_heading = None self.my_speed = None self.enemy_pos = None self.enemy_heading = None self.enemy_speed = None self.reset() def reset(self): angle = np.random.uniform(-np.pi, np.pi) dist = np.random.uniform(2000, 6000) self.my_pos = np.array([0.0, 0.0]) self.my_heading = np.random.uniform(-np.pi, np.pi) self.my_speed = 150.0 self.enemy_pos = self.my_pos + np.array([np.cos(angle), np.sin(angle)]) * dist self.enemy_heading = np.random.uniform(-np.pi, np.pi) self.enemy_speed = 150.0 return self._get_state() def _get_state(self): rel_vec = self.enemy_pos - self.my_pos rel_dist = np.linalg.norm(rel_vec) rel_angle = np.arctan2(rel_vec[1], rel_vec[0]) - self.my_heading return rel_dist, rel_angle, self.my_speed - self.enemy_speed def step(self, my_action, enemy_action): self.my_heading += self.max_turn * my_action self.my_heading = np.arctan2(np.sin(self.my_heading), np.cos(self.my_heading)) self.my_pos += np.array([np.cos(self.my_heading), np.sin(self.my_heading)]) * self.my_speed * self.dt # 敌机用固定策略,偏向正对我方 desired = np.arctan2(self.my_pos[1] - self.enemy_pos[1], self.my_pos[0] - self.enemy_pos[0]) diff = np.arctan2(np.sin(desired - self.enemy_heading), np.cos(desired - self.enemy_heading)) self.enemy_heading += 0.3 * np.clip(diff, -1.0, 1.0) self.enemy_pos += np.array([np.cos(self.enemy_heading), np.sin(self.enemy_heading)]) * self.enemy_speed * self.dt return self._get_state()

my_action这里直接用0、1、2、3、4表示左转、右转、平飞、加速、减速,step内部把动作映射成航向角速度变化和油门变化。敌机策略故意设计成“总是朝我机方向转”,它虽然不聪明,但能逼着Q学习做出连续的规避或占位动作。状态返回的是相对距离、相对角度、速度差三个原始连续量,下一节再把它变成Q表的离散索引。

3.2 Q表初始化与epsilon-greedy动作选择

Q表是一个形状为(state_bins, state_bins, state_bins, n_actions)的NumPy数组。训练开始前全部初始化为0,意味着每个动作都没有先验优势。如果想让训练更稳,也可以给所有初始值加一个极小的随机噪声,比如np.random.rand(*shape) * 0.01,这样可以避免一开始因Q值全等导致动作选择完全随机。

STATE_BINS = 8 N_ACTIONS = 5 q_table = np.zeros((STATE_BINS, STATE_BINS, STATE_BINS, N_ACTIONS)) epsilon = 1.0 epsilon_min = 0.05 epsilon_decay = 0.9995 def choose_action(state_idx, epsilon): if np.random.rand() < epsilon: return np.random.randint(N_ACTIONS) s = np.ravel_multi_index(state_idx, (STATE_BINS, STATE_BINS, STATE_BINS)) flat_start = s * N_ACTIONS q_flat = q_table.reshape(-1) return int(np.argmax(q_flat[flat_start:flat_start + N_ACTIONS]) )

动作选择的逻辑是三行:随机概率小于epsilon就乱飞,否则老老实实按当前Q表取最大值。ravel_multi_index的作用是把三维状态索引压平成一维,方便从Q表里切片。实际工程中注意不要每次都reshape整个Q表,性能会差;我只是在这里为了直观,把多维索引压成一维然后切片取argmax。你也可以直接写q_table[state_idx]然后argmax,效果一样。

3.3 Q学习的核心更新:时序差分公式怎么落地

Q学习的更新公式只有一行核心逻辑:

def update_q_table(q_table, state_idx, action, reward, next_state_idx, done, alpha=0.1, gamma=0.9): sa_idx = np.ravel_multi_index(state_idx + (action,), q_table.shape) ns_idx = np.ravel_multi_index(next_state_idx + (np.argmax(q_table[next_state_idx]),), q_table.shape) target = reward if done else reward + gamma * q_table.flat[ns_idx] q_table.flat[sa_idx] += alpha * (target - q_table.flat[sa_idx])

这里的逻辑是用当前状态的Q值与“即时奖励加上下一状态的最大Q值”做差值,再按alpha步长向目标靠近。特别注意:更新时使用的target中下一状态的动作是argmax,这就是Q学习和SARSA的关键差异——Q学习是off-policy,它评估的是“如果下一步也按最优策略走”的价值。另一个细节是“计算target时要用更新前的Q值,还是更新后的Q值”。在单步更新里我使用更新前的下一状态Q值,时序差分本来就是这个定义,如果你在同一回合里连续更新两个状态,不要用刚刚更新过的值去算下一个target,那会带来偏差。

3.4 训练一整个回合:把环境、策略、更新串起来

训练主循环的结构非常简单:重置环境、选动作、执行、拿奖励、更新Q表、进入下一状态。下面是完整的一段训练逻辑,每个回合最多跑200步,超时强制结束:

def train(episodes=5000, max_steps=200): env = AirCombatEnv() global epsilon for ep in range(episodes): rel_dist, rel_angle, speed_diff = env.reset() done = False step = 0 total_reward = 0.0 prev_dist = rel_dist while not done and step < max_steps: rel_angle_norm = (rel_angle + np.pi) % (2 * np.pi) - np.pi state_idx = discretize_state(rel_dist, rel_angle_norm, speed_diff) action = choose_action(state_idx, epsilon) next_rel_dist, next_rel_angle, next_speed_diff = env.step(action, enemy_action=1) next_angle_norm = (next_rel_angle + np.pi) % (2 * np.pi) - np.pi next_state_idx = discretize_state(next_rel_dist, next_angle_norm, next_speed_diff) reward = compute_reward(prev_dist, next_rel_dist, next_angle_norm, locked_on=False) if next_rel_dist < 300.0: reward += 1.0 done = True update_q_table(q_table, state_idx, action, reward, next_state_idx, done) rel_dist, rel_angle, speed_diff = next_rel_dist, next_rel_angle, next_speed_diff total_reward += reward step += 1 epsilon = max(epsilon_min, epsilon * epsilon_decay) if ep % 500 == 0: print(f"episode {ep}, total_reward {total_reward:.2f}, epsilon {epsilon:.3f}")

这段代码把前面几块的逻辑全部粘在一起:discretize_state负责把连续量变成索引,choose_action负责探索与利用的权衡,update_q_table负责学习。enemy_action=1表示敌机策略固定朝我方转向;距离小于300视为近距相遇,给一个击落等价奖励并结束回合。打印的训练曲线用于观察reward是否随训练上升,若一直没变化,优先检查状态索引有没有越界或者奖励函数是不是恒为0。

4. 让Q表跑出咬尾动作的四个参数:alpha、gamma、epsilon和网格分辨率

4.1 学习率alpha:调大了抖动,调小了便秘

学习率是每个更新步对目标的信任程度。alpha=0.1是比较稳妥的起点,意味着每次更新只向目标移动10%;alpha=0.5时学得快,但Q值会来回振荡,尤其是在奖励函数有噪声的空战环境里。判断alpha是否过大的一个直观信号是:训练后期奖励曲线不看趋势,光看震荡幅度,并且同一个固定策略下,连续两个回合的Q表表现差别极大。

调alpha有一个工程小技巧:不用固定值,而是把alpha随训练步数从0.5线性衰减到0.05。前期大学习率快速覆盖状态空间,后期小学习率稳定收敛。你可以在训练主循环里按回合数更新alpha,或者用一个十分简单的alpha = max(0.05, 0.5 * (1 - ep / total_episodes))。需要注意的是alpha和epsilon的衰减速度要错开,如果epsilon降太快、alpha还很大,Q表会在探索结束时剧烈改动策略,导致“看起来收敛实则震荡”。

4.2 折扣因子gamma:看得远还是看得近

gamma决定了agent多大程度上重视未来奖励。空战对抗里一个常见失误是gamma设太低,比如gamma=0.7,导致agent完全只盯着“下一秒能不能靠近”,而放弃了绕到敌机尾巴这种需要绕一个大圈才能获得高额奖励的机动。我自己常用的取值是0.9起步,如果发现agent动作短视,就往上加到0.95或0.99。

但gamma不是越大越好。gamma接近1时,Q值会趋向于累积总奖励的期望,而空战环境每回合长度不一、奖励塑形信号有正有负,很容易出现Q值绝对值膨胀,使得Q表对奖励函数中的常数偏移特别敏感。判断gamma是否合适的办法是看训练后期不同状态的Q值分布:如果所有状态Q值都很大、几乎不分胜负,那多半是gamma太高加上奖励塑形信号叠加太多,可以试着把即时奖励缩小,或者降低gamma。

4.3 epsilon探索率:从初期的乱飞到最后的高冷

空战环境里探索尤其重要,因为“咬尾”是一条需要多步连续动作才能走通的轨迹,如果探索太少,agent只能学到“直线冲向敌人”这种局部最优。初始epsilon=1.0,然后每回合衰减,通常3000到5000个回合后衰减到0.05。衰减系数0.9995意味着每回合只剩原来的99.95%,跑5000回合后大约0.08,这个节奏和上面代码匹配。

探索率最大的坑是衰减和训练长度不匹配。如果你总回合数是1000,用0.9995这个衰减,训练结束时epsilon还在0.6左右,Q表根本没有充分利用;如果回合数到20000,epsilon已经撞到0.05下限很久,后半段完全靠当前策略,对意外情况的适应力就弱了。工程里可以加一个简单判断:如果训练中后期固定策略对战的胜率不再上升,而epsilon还在下降,多半是探索已经耗尽。我自己的习惯是把epsilon衰减系数做成可输入参数,每次训练完记录最终epsilon,避免“因为跑太久导致探索提前停止”。

4.4 状态离散化分辨率:网格不是越细越好

空战的Q学习和扫地机器人最大的不同在于,状态空间的高维性非常致命。把距离、角度、速度差都切成8格,状态总数512;如果贪心一点,距离20格、角度16格、速度差8格,就变成2560个状态,乘上5个动作,Q表仍然不算大,但每个状态被访问的次数会显著下降。

问题的本质是“表格型方法的样本效率”:Q表中每个格子的Q值都必须经过多次访问才能收敛。网格越细,状态被重复命中的次数越少,训练回合数必须同比例增加。更合理的做法是先用粗网格跑通全流程,观察哪些状态格子被频繁访问,再对热点区域做细分;比如距离近(<1500米)时距离格子加密,距离远时保持粗粒度。在QLearning空战里,这比一开始就用高分辨率网格要实用得多。

下面是对照表,能直接看出分辨率和训练成本的权衡关系:

参数项粗网格细网格
状态维度距离8格/角度8格/速度差8格距离16格/角度12格/速度差8格
Q表大小2560个Q值7680个Q值
建议回合数3000到500015000以上
对咬尾策略的刻画能学到大致占位能学到更精细的切入角
风险动作切换粗糙,边界处抖动样本不足,训练缓慢甚至不收敛

这张表里的回合数是经验值,不是理论定值。细网格不是不能做,而是你必须同步增加探索回合,并把epsilon衰减拉长。如果你的训练时间预算不够,宁可接受粗网格的次优策略,先验证整体博弈逻辑是否正确。

5. 空战Q学习训练最常见的5个坑:现象、原因与排查路径

5.1 坑一:Q值收敛了,但打不过固定的追踪敌机

现象:训练奖励曲线稳步上升,但拿训练好的Q表去和固定策略敌机打对抗,胜率反而不到三成。原因:奖励函数里“接近奖励”占了大头,agent学会了无限逼近敌人,却牺牲了角度优势;它每次都正面冲向敌机,容易被对方咬住。解决:调整奖励权重,把“进入敌机后向区域”的奖励从0.3提高到接近奖励的两倍,并给正面迎头接近时增加负奖励,逼迫它绕行。

进一步排查时我习惯把某个episode的轨迹打印出来,看看每一帧的相对角度和距离。如果相对角度始终在±30度以内,说明agent就是直线冲锋;如果角度变化有明显的绕着转的趋势,说明筋位在改善。这个坑属于设计题不是bug,别急着调学习方法率。

5.2 坑二:epsilon已经降到0.05,动作仍然随机乱跳

现象:训练进入后期,epsilon可能已经触底到0.05,但每次用相同状态去查Q表,动作却是不一致的。原因:Q表里多个动作的Q值完全相同或非常接近,argmax在面对几乎相等的值时,会受浮点噪声影响来回选中不同动作。这种等值竞争在Q表初始化为全0时特别容易出现,因为没被访问到的状态永远是0,所有动作都一样“最优”。

解决:给Q表初始化加入一个极小的随机扰动,例如np.random.randn(*q_table.shape) * 1e-4,让argmax有明确的选择依据;另一个做法是在决策时加入“平局打破”逻辑,如果最大值和次大值差距小于0.01,就从这两个动作里随机选一个。前者实现简单,后者更符合实际博弈中的不确定性,但会引入额外参数。

5.3 坑三:状态维度太多,Q表内存和训练时间一起爆炸

现象:想增加一个高度状态,状态从三维变成四维,Q表数量直接从几千跳到几万甚至几十万,训练1万回合仍然很多格子没被访问。原因:离散网格维度是乘法关系,每增加一维,格子数量指数级增长。解决:放弃绝对高度,改用一个相对高度差“敌机高度减我机高度”,并只分成5~7格;同时检查状态间相关性,比如速度和高度如果都影响“能量状态”,可以合并成一个“能量差”维度。这一步本质上是在做特征工程,Q学习没法帮你自动筛选特征。

5.4 坑四:训练曲线先升后崩,前期看起来要成了,后期越打越差

现象:前2000回合总奖励在上涨,5000回合后反而持续下滑,或者胜率突然跳水。原因:最常见的是敌机策略固定不变,Q表慢慢记住了固定敌机的具体走位,形成过拟合;当训练数据中某一类初始位置占多数时,agent只对那一片区域有效。解决:在训练中让敌机也有一定的随机策略,比如每回合以20%概率让其使用另一个转向策略,增加状态覆盖范围。另一种原因是epsilon衰减过慢,后期仍在大量探索,刚学到的策略被乱飞动作反复打断;检查epsilon的最终值,如果训练到5000回合还在0.2以上,就要加快衰减。

5.5 坑五:奖励一直为正,但复盘时看不到任何战术动作

现象:平均回合奖励是正数,可把轨迹打点画出来,飞机只是在原地画圈或者往复抖动。原因:reward shaping给了“接近奖励”和“角度奖励”,但关闭了时间惩罚,agent发现抖动能频繁改变角度刷分。解决:给每一步加一个小的负时间成本,比如-0.01,同时检查“进入尾巴区域”的奖励判定是不是只要角度差小于30度就发;实际上还应该加一个前置条件:敌机处于我机前方一定距离范围内才算有效占位,否则绕圈刷角度没有意义。这种bug靠肉眼盯reward曲线看不出来,要把动作序列打印出来逐帧看。

6. 往工程走一步:SARSA、Double Q学习对照验证与胜率评估

Q学习跑通只是开了头,真正让人信服的是你证明这个策略不是“死记硬背”。我的习惯是同时实现SARSA和Double Q-Learning两个变体,固定同一套环境、同一套奖励函数、同一个初始随机种子,做三组对照。

SARSA和Q学习的差异在更新的target上:SARSA用下一步实际选择的动作去计算Q值,而不是用下一步的最大Q值。修改量非常小,核心更新逻辑如下:

def update_sarsa(q_table, state_idx, action, reward, next_state_idx, next_action, done, alpha=0.1, gamma=0.9): sa_idx = np.ravel_multi_index(state_idx + (action,), q_table.shape) ns_idx = np.ravel_multi_index(next_state_idx + (next_action,), q_table.shape) target = reward if done else reward + gamma * q_table.flat[ns_idx] q_table.flat[sa_idx] += alpha * (target - q_table.flat[sa_idx])

Double Q-Learning则是用两套Q表交替更新,从而消除单表更新里“最大值偏差”带来的乐观估计。对空战这种动作收益差别不大、奖励噪声不小的环境,Double Q通常比普通Q学习更稳:

def update_double_q(q_a, q_b, state_idx, action, reward, next_state_idx, done, alpha=0.1, gamma=0.9): if np.random.rand() < 0.5: best_action = np.argmax(q_a[next_state_idx]) target = reward + gamma * q_b[next_state_idx][best_action] q_a[state_idx + (action,)] += alpha * (target - q_a[state_idx + (action,)]) else: best_action = np.argmax(q_b[next_state_idx]) target = reward + gamma * q_a[next_state_idx][best_action] q_b[state_idx + (action,)] += alpha * (target - q_b[state_idx + (action,)])

验证方法上,每次训练完我会固定Q表,跑1000个测试回合,统计胜率、平均被击落时间、平均回合奖励三个指标。为了让结果可对比,测试回合的初始位置要覆盖不同距离和方位角的组合,而不是沿用训练时的随机分布。胜率表的参考意义比训练曲线更大:Q学习如果胜率不到五成,先保留粗网格方案;SARSA胜率更高说明环境本身有探索需求,Double Q效果更平说明原算法有方差问题。

关于训练可视化,我强烈建议画两条曲线:一条是几个不同epsilon下的reward曲线,一条是Q表的“最大Q值均值”曲线。前者看策略好坏,后者看是否收敛。如果最大Q值均值还在持续上涨,说明算法没有稳定。画出这个图不需要TensorBoard,matplotlib就可以:

import matplotlib.pyplot as plt plt.plot(reward_history, label='episode reward') plt.plot(q_max_history, label='avg max Q') plt.xlabel('episode') plt.legend() plt.title('Q-Learning air combat training curve') plt.show()

我的真实教训是:第一次跑这个空战demo时,我把epsilon衰减设成了0.995,5000回合结束时还在0.9附近,训练曲线漂亮得像说明书,但一测胜率惨不忍睹。后来把衰减改到0.9995,胜率才上来。从那以后我养成了在训练结束时print所有超参数+最终epsilon的习惯,方便复盘。强化学习空战对抗的代码不难,难的是知道哪里需要抠细节,希望这篇里的踩坑记录能帮你少走几段弯路,也希望你把Q表、SARSA和Double Q都跑一遍之后再下结论。

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

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

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

立即咨询