简介:一份面向智能空管、机器学习与航空器运行安全领域研究者和开发者的技术方案文档。内容围绕基于深度强化学习的航空器冲突解脱方法,讲解如何通过开源空管平台OpenScope构建冲突场景,利用Gym接口实现智能体通信,并以深度确定性策略梯度(DDPG)算法训练航空器完成对头、交叉等冲突场景的自主解脱;同时梳理了冲突环境生成、智能体通讯、策略网络、价值网络与经验池等模块的设计逻辑,为复现或改进相关仿真系统提供了清晰参考。文档从问题背景、算法原理到模块实施层层展开,便于课题综述、方案设计或毕业设计参考。资源为1个docx文档,压缩包大小约18KB,篇幅紧凑但体系完整。该文档发布后已有87人学习下载,对从事智能空管算法研究和工程实现的人员具有较高参考价值。
1. 深度强化学习做航空器冲突解脱:一个三十秒决策问题,为什么要用算法来解
雷达屏幕上跳出黄色冲突告警,到管制员给出指令通常只有几十秒。航空器冲突解脱的本质是一个序列决策问题:本机要不要转向、转多少度、什么时候回到原航路,每一步都影响后面几步的状态。传统几何法算单冲突点很快,但一旦变成多机并发、间隔约束与航路偏移同时存在,解空间就炸开了。基于深度强化学习的航空器冲突解脱,就是把这套决策写成马尔可夫决策过程,用仿真环境大规模试错训练策略网络,在线推理时几十毫秒给出一条满足间隔约束、代价尽量小的机动建议。适合空管自动化研发、无人机自主避撞和飞行仿真方向的从业者。这篇文章不绕理论,直接讲状态、动作、奖励怎么定义,训练怎么跑,以及最容易翻车的几个地方。
2. 把冲突解脱写成强化学习问题:状态、动作与奖励怎么定义才能训得动
很多人第一次接触这个方向,上来就打开 PPO 的示例代码,导入一个通用环境开始训练。这是个危险的起点。深度强化学习算法本身只是个优化器,它吃进去的是状态、动作和奖励,吐出来的是策略。这三个要素没定义好,后面调什么超参都像在瞎子摸象。
冲突解脱项目里,建模阶段的返工率远高于训练阶段。原因在于,空管场景的决策有明确的序列性:现在转 15 度,五秒后可能就要转回来;现在保持高度,再过两个航路点可能就撞上。一次性寻路算法给不出这种"当前时刻做什么"的答案,而马尔可夫决策过程天然就是为这种按时间步推进的决策设计的。所以第一步不是选算法,而是把飞行冲突场景压缩成一个 MDP。
2.1 为什么冲突解脱是 MDP,而不是单纯的寻路
寻路问题关心的是"从 A 到 B 的一条曲线怎么走",冲突解脱关心的是"每一拍该给航空器什么指令"。两者的差异在于:解脱过程里,航空器状态是动态更新的,风场、指令执行延迟、对方机动都会让下一步的状态带随机性;同时,当前机动会改变后面几步的态势,最优决策必须考虑未来。
MDP 的五个要素——状态、动作、状态转移概率、奖励、折扣因子——恰好覆盖这个需求。把冲突解脱写成 MDP 之后,训练出来的策略是一个从观测到动作的映射:输入当前本机与入侵机的相对态势,输出一个航向/高度/速度调整建议。这个映射不需要在每次决策时重新搜索轨迹,推理成本只有一个前向网络,这是它能落到实时系统的关键原因。
还有一个实际好处是换场景代价低。MDP 建模时,只要状态和奖励定义保持不变,换空域、换导航点、换交通流密度,策略网络往往还能沿用一部分。几何法和最优控制做的是"一次一解",换一个场景通常要重新算一遍。
2.2 状态空间设计:相对坐标比绝对坐标更能泛化
冲突解脱状态设计的第一个原则是:尽量用相对量,少用绝对量。绝对经纬度、绝对航向、绝对高度这些量的分布会随空域位置变化,模型学到的是"某个坐标附近怎么做",换个空域就失效。而相对位置、相对速度、相对航向差这类量的统计分布,在不同空域里是相似的,策略泛化能力明显更好。
一个最小可用的观测向量通常长这样:本机速度、本机航向的正弦和余弦、入侵机相对位置(x、y 两个分量)、入侵机相对速度矢量、相对航向差、高度差、上一步动作。共约 10 到 15 维。航向角不建议直接用 0 到 360 度或 -180 到 180 度的数值,角度在边界处会从 359 跳到 0,数值连续性被破坏。用 sin/cos 编码是更稳的做法,代价只是多两个维度,量纲问题也顺带解决了。
如果你要处理的是多机场景,不要把所有飞机的状态全部拼进观测。常见做法是取距离本机最近的 N 架飞机,或者只保留最危险的一两架,把它们的相对状态按距离排序后拼接。所有飞机全拼进去,向量维度爆炸,而且模型很难从十几架无关飞机里找出真正威胁大的那一架。
2.3 动作空间:离散航向指令才是空管能接受的语言
动作空间的选择直接决定训练难度和落地可行性。纯连续动作——比如输出一个 -60 到 +60 度的航向偏移量——策略理论上更平滑,但有两个实际问题:一是探索空间大,训练初期随机采样的动作五花八门,收敛慢;二是管制员实际发布的指令是"左转 15 度""上升 300 米"这种离散语句,连续输出还得再量化一道,量化误差会吃掉一部分收益。
我更建议的起步方案是中等粒度离散动作集。一个常见的五动作集是:保持当前航向、左转 15 度、右转 15 度、左转 30 度、右转 30 度。需要垂直维度时,再加"上升"和"下降"两个动作,变成七动作集。度数粒度不要小于 10 度,否则动作数太多,策略在相似动作之间徘徊,训练效率很低。
动作频率也要控制。一步决策间隔设在 5 到 10 秒比较合理,和真实空管指令节奏接近。间隔太短,模型会在相近的动作间反复横跳,产生大量指令;间隔太长,对快速接近的冲突又反应不过来。
2.4 奖励函数:把"安全优先、效率次之"翻译成数值
奖励函数是这个项目里最需要反复打磨的地方。它的翻译目标很明确:安全第一、效率第二、机动幅度第三。最基本的冲突奖励是惩罚项加成功项的组合。
安全项这样写:两机距离小于最小间隔(比如 10 公里)时,每步给 -1;距离重新拉大到 1.2 倍最小间隔并持续一段时间,给 +1 并结束回合。这是稀疏部分。但只靠这两个信号,训练初期模型很难学到有效机动,因为随机策略下大部分回合都走不完,成功信号几乎不出现。所以需要一个密集 shaping 项:距离比上一步增大时给一个小正信号,减小时给一个小负信号。这样模型最早学到的不是"怎么解脱",而是"先拉开距离再说"。
效率项和机动项是次级约束。效率可以用航迹偏移量、到达时间延迟或燃油消耗来近似;机动项通常用"当前动作是否与上一步不同"的惩罚,抑制高频抖动。三者权重的量级差异很关键,安全项权重 1.0 的话,效率项参考 0.01 到 0.05,机动项 0.005 到 0.02。效率项权重一旦给到 0.1 以上,模型会发现绕一大圈虽然更安全,但算总账更"划算",于是行为变得极其保守。
3. 深度强化学习算法选型与训练节奏:为什么我默认从 PPO 开始
建模做完之后才轮到算法。深度强化学习算法不是越新越好,也不是效果越花哨越好,要看项目最缺什么。冲突解脱这个场景的特点是:状态空间连续、动作空间离散或低维连续、仿真环境便宜、一次训练可以跑几百万步、对策略稳定性的要求远高于样本效率。这几个特征叠加起来,PPO 几乎是最稳的默认起点。
DQN 也不是不能用,但如果你用的是 15 维连续状态和 5 到 7 个离散动作,DQN 的 Q 值高估问题会随着动作数增多而放大。SAC 在连续动作空间上样本效率很高,但训练时对奖励尺度和温度系数更敏感,冲突解脱项目里奖励函数经常要改,SAC 每次都要跟着调,维护成本高。
3.1 算法对比:DQN、PPO 与 SAC 各自合适的位置
先看一张对比表,按冲突解脱项目的实际需求排优先级。
| 算法 | 动作空间 | 样本效率 | 实现成本 | 稳定性 | 冲突解脱场景优先级 |
|---|---|---|---|---|---|
| DQN | 离散 | 低 | 低 | 中等,Q 值易高估 | 低,离散动作数一多就吃力 |
| PPO | 离散/连续通吃 | 中 | 低 | 高,对奖励尺度变化不敏感 | 高,默认首选 |
| SAC | 连续 | 高 | 中 | 中等,超参敏感 | 中,适合后期做平滑轨迹 |
PPO 还有一个现实优势:改奖励函数之后,往往只需要调一下学习率和熵系数就能继续训。SAC 和 DQN 在奖励尺度大改之后,经常出现训练完全发散、必须从头开始的情况。我见过不止一个项目在奖励权重调整后,把 DQN 从 30 万步重训到 80 万步才恢复,而同样的改动量,PPO 一般 10 万步内就能稳定下来。这也是冲突解脱项目里 PPO 成为事实标准的一个原因。
3.2 PPO 训练循环的标准脚手架
用 stable-baselines3 为例,一个可以直接改着用的训练脚本是这样:
# 冲突解脱 PPO 训练脚手架 import os from stable_baselines3 import PPO from stable_baselines3.common.env_util import make_vec_env from conflict_env import Conflict2DEnv # 用 8 个并行环境采样,模拟不同初始方位的冲突场景 env = make_vec_env(Conflict2DEnv, n_envs=8) model = PPO( "MlpPolicy", env, learning_rate=3e-4, n_steps=2048, batch_size=256, n_epochs=10, gamma=0.99, gae_lambda=0.95, clip_range=0.2, ent_coef=0.01, verbose=1, ) # 每 50 万步存一次 checkpoint,方便回滚 for i in range(10): model.learn(total_timesteps=500_000, reset_num_timesteps=False) model.save(f"checkpoints/ppo_conflict_step_{(i + 1) * 500_000}")这段代码的逻辑是:先建 8 个并行环境,每个环境随机生成不同的入侵机方位和距离,PPO 从这 8 条轨迹里同时采样,提升数据吞吐;训练按 50 万步一个阶段切分,每阶段结束存一个 checkpoint。reset_num_timesteps=False保证总步数累计,否则每轮 learn 都会重新计数,学习率调度也会被重置。checkpoint 是为了给训练回滚留一条退路,奖励函数改崩了可以直接回退到上一个稳定点。
参数说明:n_steps=2048是每轮采样步数,n_epochs=10表示用这批数据反复更新 10 轮,batch_size=256是每轮更新的小批量大小。这个组合在几百到几千维观测规模下都表现稳定。学习率 3e-4 是通用默认值,如果你的状态里包含大量历史帧或图像特征,建议降到 1e-4 附近。
3.3 超参起点与调参方向:先看熵,再看回报
PPO 的超参里,优先级最高的是学习率、clip_range 和 ent_coef。clip_range=0.2是默认值,训练中如果发现损失在某个 epoch 之后剧烈振荡,说明策略更新步长太大,把它降到 0.1 通常能压住。ent_coef=0.01是探索力度,训练中后段可以看一眼熵值曲线:熵降到接近 0 说明策略过早陷入确定性,此时回报曲线往往已经走平;把熵系数提到 0.02 到 0.05 能让策略重新开始探索。
一个更直接的诊断指标是 KL 散度。记录每轮更新前后策略的 KL 散度,如果超过 0.015 左右,说明更新步长过大,优先降学习率而不是降 clip_range。反之 KL 长期低于 0.001,学习率可以适当提高。
GAE lambda 一般固定 0.95 不用动。gamma 0.99 对应大约 100 步的有效回报窗口,冲突解脱回合通常 30 到 120 步结束,够用。如果你的回合设计拉长到几百步,可以提到 0.995,但注意这会让训练对远期奖励更敏感,稀疏信号更容易被噪声淹没。
3.4 训练节奏与并行采样的取舍
训练节奏应该以"快速验证奖励设计"为目标,而不是"一次训到最优"。我一般会把一轮迭代控制在 20 到 40 分钟以内,做法是先用小步数(比如 20 万步)跑通,看 shaping 项是否让回报曲线有向上趋势,再拉长到 100 万步。这个习惯能在一天内试五六轮奖励函数,比一次训练挂一晚上、第二天发现方向错了要高效得多。
并行环境数量的选择也有讲究。8 个并行环境在普通 CPU 上就能跑,吞吐量大约比单环境快 4 到 6 倍。窗口渲染一定要关掉,visualize=False,否则渲染线程会占掉大部分 CPU。环境内部如果做的是 2D 质点模型,这一步的仿真开销很小,瓶颈反而在策略网络的前向和更新计算上,没必要为省时间换 GPU——除非你的观测是雷达图像。
4. 最小可复现环境:搭一个二维冲突解脱仿真并跑通 PPO 训练闭环
为了让上面这套流程真实可跑,这里给出一个最小可复现环境。它只做二维水平冲突解脱,忽略高度层,把航空器简化为带速度与航向的质点。别小看这个简化结构,很多真实项目的 MVP 版本就是这个形态,跑通之后再往上加高度维和飞行性能模型。
4.1 环境骨架:一个 2D 两机冲突解脱的 Gym 环境
# conflict_env.py import numpy as np import gym from gym import spaces class Conflict2DEnv(gym.Env): def __init__(self, min_sep_km=10.0, dt_s=10.0, max_steps=120): super().__init__() self.min_sep_km = min_sep_km # 最小安全间隔,单位 km self.dt_s = dt_s # 决策步长,单位 s self.max_steps = max_steps # 回合最长步数 # 动作: 0=保持 1=左转15度 2=右转15度 3=左转30度 4=右转30度 self.action_space = spaces.Discrete(5) # 观测: 本机速度/航向sin/航向cos + 入侵机相对位置x/y + 相对速度x/y + 上一步动作 obs_dim = 3 + 2 + 2 + 1 self.observation_space = spaces.Box( -np.inf, np.inf, shape=(obs_dim,), dtype=np.float32 ) self.reset() def reset(self): self.own_pos = np.array([0.0, 0.0]) self.own_spd = 250.0 # m/s self.own_heading = 0.0 # rad,0度指向正东 # 入侵机随机方位,初始距离 40 到 80 km bearing = np.random.uniform(-np.pi, np.pi) dist = np.random.uniform(40.0, 80.0) self.intr_pos = np.array([ dist * np.cos(bearing), dist * np.sin(bearing) ]) self.intr_spd = np.random.uniform(230.0, 260.0) self.intr_heading = np.random.uniform(-np.pi, np.pi) self.steps = 0 self.last_action = 0 self.prev_d = np.linalg.norm(self.intr_pos - self.own_pos) return self._get_obs() def _get_obs(self): rel_pos = self.intr_pos - self.own_pos rel_vel = np.array([ self.intr_spd * np.cos(self.intr_heading) - self.own_spd * np.cos(self.own_heading), self.intr_spd * np.sin(self.intr_heading) - self.own_spd * np.sin(self.own_heading) ]) # 相对位置除以 100 做归一化,避免 km 量级压制其他维度 rel_pos_norm = rel_pos / 100.0 rel_vel_norm = rel_vel / 100.0 obs = np.array([ self.own_spd / 300.0, np.sin(self.own_heading), np.cos(self.own_heading), rel_pos_norm[0], rel_pos_norm[1], rel_vel_norm[0], rel_vel_norm[1], self.last_action / 4.0 ], dtype=np.float32) return obs这一段代码做的事是:每回合随机生成一架入侵机的方位、距离、速度和航向,把观测向量组织成本机状态加相对状态的形式。last_action放进观测里很关键,模型需要知道当前执行的动作,才能在下一步决定是否反向修正,否则容易出现左右横跳。
参数说明:初始距离 40 到 80 km,按 250 m/s 的相对接近速度算,大约两到三分钟后才进入冲突区,给策略留出足够的提前量。min_sep_km=10对应水平间隔标准。观测里所有量纲都归一化到大致 [-1, 1] 区间,rel_vel除以 100 而不是除以 300,是因为两机速度差通常远小于速度本身,除以 100 后信号幅度仍然偏小,但不至于被淹没。如果训练发现模型对速度差完全不敏感,可以把这个除数调到 50。
4.2 step 函数与奖励实现
def step(self, action): # 1. 执行航向机动 if action == 1: self.own_heading += np.radians(15) elif action == 2: self.own_heading -= np.radians(15) elif action == 3: self.own_heading += np.radians(30) elif action == 4: self.own_heading -= np.radians(30) # 2. 以当前航向推进一个决策步长 self.own_pos += self.own_spd * self.dt_s * np.array( [np.cos(self.own_heading), np.sin(self.own_heading)] ) / 1000.0 self.intr_pos += self.intr_spd * self.dt_s * np.array( [np.cos(self.intr_heading), np.sin(self.intr_heading)] ) / 1000.0 self.steps += 1 # 3. 计算当前距离与冲突状态 d = np.linalg.norm(self.intr_pos - self.own_pos) conflict = d < self.min_sep_km reward = 0.0 if conflict: reward -= 1.0 # 安全项:违规重罚 if d > self.min_sep_km * 1.2 and self.steps > 10: reward += 1.0 # 解脱成功:正向激励 done = True else: done = False if self.steps >= self.max_steps: done = True # 超时结束,防止永续盘旋 # shaping 项:距离增大给正信号,缩小给负信号 reward += 0.02 * (d - self.prev_d) self.prev_d = d # 机动惩罚:动作改变时扣一点,抑制高频抖动 if action != self.last_action: reward -= 0.01 self.last_action = action return self._get_obs(), reward, done, {}这个 step 函数有三层奖励结构。安全项只在距离小于安全间隔时触发,一次 -1 足够让模型把违规排在所有目标之前。成功项在距离拉开到 1.2 倍最小间隔且已经过了 10 步之后触发,避免刚开局双方本来就远、误判解脱成功。shaping 项0.02 * (d - prev_d)把"离得更远"变成逐帧的密集信号,让模型在稀疏成功信号出现之前就有学习方向。
机动惩罚写的是0.01,比塑造项还小,目的是只压制频繁换向,不压制大角度机动。如果你的项目里动作幅度差异更大,可以改成按动作幅度加权,比如 15 度动作变化扣 0.005,30 度扣 0.01。最大步数 120 意味着一个回合最长 20 分钟,超过这个时间直接截断,防止模型学到"永远绕圈"的死循环。
4.3 训练脚本:从数据采集到模型保存
# 安装依赖 pip install gym numpy stable-baselines3# train.py from stable_baselines3 import PPO from stable_baselines3.common.env_util import make_vec_env from conflict_env import Conflict2DEnv # 8 个并行环境同时采数据 env = make_vec_env(Conflict2DEnv, n_envs=8) model = PPO( "MlpPolicy", env, learning_rate=3e-4, n_steps=2048, batch_size=256, n_epochs=10, gamma=0.99, clip_range=0.2, ent_coef=0.01, verbose=1, ) # 先跑 20 万步验证奖励设计,再决定是否拉长 model.learn(total_timesteps=200_000) model.save("ppo_conflict_quick_check")逻辑说明:make_vec_env会把Conflict2DEnv复制 8 份,每个子进程独立采样,训练数据来自不同的随机冲突场景。MlpPolicy表示用多层感知机处理观测,不涉及雷达图像,CPU 就能训练。跑完 20 万步存一个快速验证模型,先加载模型看几个回合的可视化效果,动作是否在朝"拉开距离"的方向尝试,再决定是否继续训到 100 万步。
这里的参数和前文一致,不再赘述。需要提醒的是:第一次训练大概率会发现模型动作很保守或者很乱,这是正常现象。先看 shaping 项是否让回合平均距离随时间上升,如果距离曲线完全不动,优先检查奖励数值是否被其他项目覆盖,比如机动惩罚绝对值太大,把 shaping 信号吃掉了。
4.4 从二维扩展到多机与三维空域的思路
二维平面对跑步跑通之后,扩展方向有两个:一是加高度维,动作集里加入"上升""下降",观测里加高度差和垂直速度。高度解脱在真实空管里优先级很高,因为改变高度层往往比水平绕飞更经济。二是从两机扩展到多机。多机的关键不是堆状态维度,而是让模型每次只关注最危险的目标。常见做法是对所有入侵机按距离排序,取前 3 架拼成固定长度观测;更进一步可以用注意力机制,让模型自己学会关注威胁最大的目标。
扩展时会遇到一个明显问题:训练时间非线性上涨。两机环境 100 万步可能只要半小时,四机环境可能就要两三小时,因为决策难度和状态空间都变大了。预算有限的话,可以让多机环境里只有一架飞机会真的冲突,其余飞机只是背景流量,先让模型学会在干扰下锁定真正威胁,再逐步增加冲突对数量。
5. 冲突解脱训练中常见的 5 个坑:现象、原因与排查顺序
这个方向的项目,模型学不出来的时候,十有八九不是算法的问题,是场景设计、奖励定义或数据分布的问题。下面五条是我在多次训练排障里验证过的高频原因,按出现频率排序,遇到训练异常可以按这个顺序查。
5.1 稀疏奖励训不出东西:前 50 万步像随机飞
现象:训练几十万步后,动作分布还很均匀,回合平均距离没有上升趋势,成功事件几乎为零。
原因:奖励只有回合结束时的成功 +1 和违规 -1,中间过程没有任何信号。随机策略下,两机从 40 公里外开始接近,通常要一两百步才可能发生冲突,策略完全碰不到成功分支,梯度方向被噪声主导。
解决:加 shaping 项,让每个决策步都产生信号。最小成本的做法是第 4 章里的0.02 * (d - prev_d),让"距离变大"这件事本身成为奖励的一部分。注意 shaping 权重不能高过安全项,否则模型会学成"只要距离在变大就一直转圈",所以安全惩罚要保持在 -1 这个量级,shaping 控制在 0.01 到 0.03。
5.2 状态没有归一化:训练曲线震荡但评估不稳定
现象:回报曲线忽高忽低,训练损失不降反升,换随机种子后结果差异巨大。
原因:状态里同时存在几十公里的距离和几百米每秒的速度,量纲差两个数量级,网络初始阶段被大数值维度主导,小数值维度的信息完全学不到。航向直接用了 0 到 360 度数值的项目,还会遇到角度边界跳变,明明只转了 1 度,数值上却从 359 变成 0,梯度被撕裂。
解决:所有状态分量归一化到 [-1, 1]。位置除以参考距离,速度除以参考速度,角度用 sin/cos 编码。检查时可以打印一个 batch 的观测均值与标准差,任何一维的标准差超过 1 就要处理。顺带提一句,奖励项也应该缩放,否则和状态量纲问题叠加,排查起来非常头痛。
5.3 奖励权重失衡:学着学着绕起大圈子
现象:安全指标很好,但轨迹明显不合理,明明 30 度就能解脱,模型非要绕一个半径几十公里的弧形。
原因:效率项权重偏低时,模型发现绕路虽然多花时间,但风险最低,于是把"绕圈"当成最优策略。反过来,效率权重偏高时,模型会在距离边界很近的地方做激进机动,安全指标会突然恶化。这个坑在奖励函数设计阶段几乎每个人都踩过一次。
解决:把安全项和效率项分开验证。先只保留安全项加 shaping 项,训练一个只追求"避免冲突且距离稳步拉开"的策略,观察行为是否合理;确认没问题后再以 0.01 量级逐步加效率项,每加一次都看轨迹形状。一个大原则是:安全项的梯度量级应该至少是效率项的 10 倍,否则模型会把"少绕路"排在"保持间隔"之前。
5.4 训练场景太单一:换一批流量密度,评估指标直接崩
现象:训练时每回合成功率 95% 以上,换到新的初始距离、新的入侵机速度分布,成功率掉到 60%。
原因:reset 函数里写死了初始距离区间或入侵机速度,模型记忆的是单一场景的模式,而不是通用的解脱策略。最常见的是初始方位角只在某个 90 度扇形内均匀分布,模型学会了处理左侧来机,右侧来机完全不会。
解决:在 reset 里做域随机化。初始距离、方位角、速度、航向全部随机,分布范围要覆盖你期望模型处理的边界情况。训练和评估用两套不同的随机种子和分布参数,训练分布比评估分布更宽。一个容易被忽略的点是:入侵机速度不要固定成恰好和本机一样,真实场景里速度差很大,模型要在训练里见过快慢两端的分布,评估时才不会懵。
5.5 多机场景下奖励归因不清:绕了不该绕的飞机
现象:三机以上场景里,策略经常对一架远在 60 公里外的飞机做大机动,却对 20 公里外正在逼近的飞机无动于衷。
原因:奖励只给一个总回报,模型不知道当前步的表现究竟是因为哪一架飞机。当多架入侵机同时存在时,shaping 项用的是所有目标距离变化的合量,某一架靠近、另一架远离,信号互相抵消,模型学不到正确归因。
解决:把安全项按配对计算。每一对飞机分别算距离和 shaping 奖励,取所有对里的最小值作为最终安全项。这样只要有一对在恶化,信号就是负的,不会被另一对的优化掩盖。更进一步的做法的代码思路是:
# 多机配对奖励示例 pair_penalties = [] for intr in intruders: d = np.linalg.norm(intr.pos - own.pos) delta = d - prev_d[intr.id] pair_penalties.append(-1.0 if d < min_sep else 0.02 * delta) safe_reward = min(pair_penalties) # 最危险的一对主导 reward = safe_reward + efficiency_penalty + maneuver_penalty这里min(pair_penalties)保证只要有一架飞机离得太近,整体安全奖励就是负的。这个做法会损失部分信息,比如两架飞机都在危险距离内时只反映了最差那架,但对于策略学习来说,"先救最危险的"远比"平均照顾所有飞机"更重要。
6. 落地前必须补齐的三块:场景泛化验证、统计指标与规则安全兜底
训练收敛只是起点,真正判断这个方案能不能投入使用的是最后一公里。第一件事是统计评估:不要只看训练环境的回报曲线,要在另一组随机场景集上跑几百个回合,统计解脱成功率、平均决策步数和最小间距分布。评估脚本通常这样写:
# eval_stats.py def evaluate_policy(model, eval_env, episodes=500): stats = [] for _ in range(episodes): obs = eval_env.reset() done = False min_d = np.inf steps = 0 while not done: action, _ = model.predict(obs, deterministic=True) obs, _, done, info = eval_env.step(action) min_d = min(min_d, info.get("distance", np.inf)) steps += 1 stats.append((done, min_d, steps)) # 统计:解脱成功率、最小间距低于阈值的比例、平均步数注意评估时要用deterministic=True,不要引入探索噪声。评估环境要和训练环境用不同的随机种子,甚至可以故意拉高入侵机速度方差来测鲁棒性。解脱成功率 90% 以上才算及格,平均最小间距至少要比安全间隔大 20%,否则说明策略贴着边界飞,实际空管场景里是没人敢放的。
第二件事是规则兜底。深度强化学习在这个系统里扮演的角色是"候选方案生成器",不是最终决策者。真实工程里,模型输出动作后,后端需要再跑一遍几何检查:如果该动作在下一决策步会让两机距离低于硬性安全线,就自动回退到保守动作或保持原航向,并把这次拦截记录下来,作为后续重新训练的样本。这种"学习策略 + 规则护栏"的架构,既利用了深度强化学习的场景适应能力,又保住了安全红线。
第三件事是我个人习惯:训练脚本里一定留 checkpoint 管理和小规模快速验证入口。改奖励前先另存当前模型,改完先用 20 万步快速验证,确认有效再拉长训练。这个习惯救过我很多次,不然一次失败的奖励调整可能浪费一整晚训练时间。冲突解脱不是靠单次训练一蹴而就的方向,它是反复迭代场景、奖励和验证策略的结果。希望帮到你。
本文还有配套的精品资源,点击获取