简介:面向多星对区域目标观测规划这一典型航天调度场景,提供了一套基于深度强化学习的完整可运行方案,适合有编程基础的航天任务规划研究者、深度学习算法学习者以及多智能体调度方向开发者。资料包含环境模拟器、深度Q网络、经验回放缓冲区、策略网络与价值网络等核心模块,并配有训练、评估及超参数调整脚本,可帮助读者从零理解算法的建模和迭代过程。压缩包共492个文件,以npz数据文件、gz压缩数据、fits卫星观测数据、py源码、ipynb示例、csv结果表格等为主要类型,整体大小约198.33MB;其中源码和ipynb便于逐行阅读算法实现,fits/csv/npz可直接用于实验分析,目录结构清晰,便于按模块检索学习。目前已有192人学习下载,适合用于复现星群调度实验、扩展多星观测策略,并为多目标优化问题提供工程化参考。
1. 多星观测调度为什么需要深度强化学习
解压 Multi-Satellite-Scheduling-master.zip 后,里面不是密密麻麻的轨道六根数,而是一组以Fermi.csv、Integral.csv、Swift.csv和GraceDB.csv为骨架的时间线数据。这类数据常见于多颗空间望远镜对伽马射线暴、引力波事件等区域目标的协同观测记录。真正要解决的问题是:当多个高优先级目标在随机时刻出现,星上约束各不相同,怎么动态决定哪颗星看哪个区域、看多久、什么时候机动。
传统做法是拉网格加整数规划,但地面调度员在几分钟内面对数百个候选观测窗口时,整数规划重算一次常常跑不完;贪心策略又容易卡在局部最优,导致后续高优先级目标没有可用的卫星窗口。深度强化学习在这里的价值是离线训练一个策略网络,在线推理时只要把当前卫星位置、目标优先级、剩余观测时间拼成状态向量,前向一次网络就能输出动作。这个项目把多星调度建模成马尔可夫决策过程,用 DQN 学调度策略,而不是靠人工编码规则。适合已经会用 Python 处理数据、想看看强化学习在航天调度场景里怎么落地的工程师。
2. 观测规划问题建模:从 CSV 数据到马尔可夫决策过程
2.1 为什么先要把调度写成 MDP
多星协同观测不是单步选星,而是一个随时间推进的序列决策:上一秒选择观测目标 A,下一秒卫星可能进入地影,需要重新决策。强化学习里的马尔可夫决策过程正好描述这种过程。MDP 用四元组表示,状态 S 是当前环境快照,动作 A 是调度指令,转移概率 P 表示执行动作后状态如何变化,奖励 R 告诉我们这一步做得好不好。
难点在于 P 并不是显式的概率表,而是由环境模拟器隐式给出。每走一步,卫星位置前推一个时间步,目标覆盖情况变化,这些构成了下一个状态。DQN 在这个框架下学习的是 Q 函数,即某个状态 s 下采取动作 a 后累积奖励的期望。只要状态设计能把调度员关心的信息压缩进去,Q 函数就能间接反映"先看哪个目标收益更高"这种非线性的权衡。
2.2 CSV 文件在项目里承担什么角色
拿到Fermi.csv、Integral.csv、Swift.csv时,我一开始以为里面是轨道参数,打开才发现是望远镜的历史观测事件记录,包括事件编号、时间戳、目标赤经赤纬、曝光时间和流量相关字段。Fermi2021.csv、Integral2021.csv、Swift2021.csv以及合并后的Fermi+Integral+Swift2021.csv则是按年份切分的数据集,用于划分训练和测试时间窗口。
GraceDB.csv来自引力波事件数据库,里面每一行代表一个候选事件,带有事件时间、天空定位概率分布。这类事件有两个特点:持续时间短,位置不确定性大。正因如此,多星协同观测才显得必要,仅靠一颗卫星难以在短时间内覆盖概率云图。
在建模时,我用这些 CSV 做两件事。第一,从事件表中提取目标列表,每个目标带优先级和截止时间;第二,用历史事件的到达间隔拟合目标到达率,在训练时生成随机事件序列。原始 CSV 里的定位误差和流量信息可以折算成观测收益权重,流量越高的目标优先级越高。
2.3 状态、动作、奖励的工程化定义
状态向量我一般拼接四类信息:卫星侧的状态(当前位置、剩余可用推进剂、当前姿态指向)、目标侧的状态(每个目标剩余可见窗口、优先级、已覆盖面积占比)、时间信息(当前时刻在规划周期内的归一化位置),以及卫星与目标之间的可见性掩码。动作空间则离散化处理,假设有 N 颗卫星、M 个目标,动作就是两两组合,外加一个"空闲等待"动作。需要说明的是,把连续观测起止时间离散成多个时间片后,动作空间大小是 N*M+1,这个量级对 DQN 是友好的。如果区域数量超过 20,我一般会把动作改成"每颗星选择观测目标 ID"的多头动作结构,否则 Q 值网络输出维度会爆炸。
奖励函数是调度效果的关键。我的做法是让每步奖励同时包含收益项和惩罚项:
def reward(state, action, new_cover): # new_cover: 本时间片新增覆盖面积(归一化后) # action: (sat_id, target_id) 或 None 表示空闲 r = 0.0 r += new_cover * state.target_priority[target_id] # 每颗星连续机动需要时间,动作切换会消耗能源 if action is not None and state.sat_action[sat_id] != target_id: r -= state.maneuver_cost[sat_id][target_id] # 对超时未完成的高优先级目标扣分,避免策略一直拖延 for t in state.overdue_targets(): r -= state.target_priority[t] * 2.0 return r这里new_cover由环境模拟器的观测几何决定;target_priority根据 CSV 中事件流量归一化到 1 到 10 区间。机动代价除了燃料,还包括切换期间 30 秒到 60 秒的观测时间损失,所以动作切换扣分能约束策略不要频繁变卦。超时惩罚加倍,是因为调度员最不能接受的是重要目标完全错过。
3. 环境模拟器与观测收益计算:Fermi/Integral/Swift 数据如何参与训练
3.1 统一时间基准与数据加载
三个望远镜的事件时间戳格式并不完全一致,Fermi 习惯用 MET 秒,Swift 用 ISO 时间字符串。如果直接拼接会导致时间轴错位。我写了一个加载器,把所有 CSV 转换成统一的时间轴(Unix 时间戳),并提取目标相关的列。
import pandas as pd from datetime import datetime def load_event_csv(path, time_col, ra_col, dec_col, prio_expr=None): df = pd.read_csv(path) # 不同数据源时间格式不同,统一转成 UTC 秒 if df[time_col].dtype == object: df["ts"] = pd.to_datetime(df[time_col]).astype("int64") // 10**9 else: df["ts"] = df[time_col].astype("int64") # 从流量/显著性字段映射到优先级,贴近真实调度需求 if prio_expr is not None: df["priority"] = df.eval(prio_expr).clip(1, 10) else: df["priority"] = 5 return df[["ts", ra_col, dec_col, "priority"]].rename( columns={ra_col: "ra", dec_col: "dec"} )加载后把Fermi2021.csv、Integral2021.csv、Swift2021.csv合并成一份目标列表,时间范围作为训练窗口;GraceDB.csv作为应急事件单独加载,因为引力波事件需要更短响应时间。这里prio_expr支持传入字符串表达式,比如"8 / (1 + log(flux))",把高流量目标的优先级提高。
3.2 覆盖收益模型与卫星可见性约束
每个目标有观测收益的上限,同一颗星对同一目标重复观测边际收益递减。常见做法是把目标区域网格化为像素点,卫星每观测一次就标记该区域被覆盖的像素;新增覆盖像素数除以总像素数就是new_cover。但全网格计算太慢,实际项目里我用高斯点扩散函数近似:卫星指向目标中心时,对周围角度内的区域产生覆盖,覆盖权重随离轴角下降。
可见性约束也不能忽略。卫星对某目标可见的条件是目标在地球地平线以上,且太阳高度角不在地影范围内。简单判断方式是卫星到目标中心连线与地球相交,以及卫星位置是否在地球阴影锥内。环境模拟器中每一步都检查这些条件,并把不可见的(sat_id, target_id)动作掩码置为负无穷,防止 DQN 输出非法动作。下面的伪代码展示了环境 step 的核心逻辑:
def step(self, action): # action: (sat_id, target_id) 或 None if action is not None: sat, tgt = action if not self.is_visible(sat, tgt): return self.state, -10.0, False # 非法动作惩罚 # 更新覆盖记录,new_cover 由卫星指向和目标扩展源计算 new_cover = self.cover_model(sat, tgt) self.covered_map[tgt] += new_cover self.state.covered_ratio[tgt] = self.covered_map[tgt] / self.total_area[tgt] # 推进所有卫星轨道一个时间步(简化圆轨道外推) self.state.sat_positions = self.orbit_propagator(self.state.sat_positions, self.dt) self.state.current_time += self.dt done = self.state.current_time >= self.horizon_end reward = self.reward_fn(self.state, action, new_cover) return self.state, reward, doneorbit_propagator里我没有引入高精度 SGP4,而是用 J2 摄动下的开普勒轨道外推,因为调度训练需要快速跑几千个 episode,数值精度足够捕捉可见窗口的分钟级变化即可。若做最终验证,再接入标准 TLE 或轨道预报库。
3.3 奖励归一化与训练稳定性
不同优先级目标混在一起,奖励数值可能相差几个数量级。我一般把优先级做 softmax 归一化,使单步奖励范围落在 [-1, 1] 之间。同时把覆盖面积也做归一化,避免累计奖励被少数大区域目标主导。训练初期如果发现 Q 值持续不下降,先检查奖励是不是存在未加符号的None分支导致异常。另一类常见问题是超时惩罚过大,策略会宁可空闲也不观测低优先级目标,这样总覆盖率反而不如贪心。把超时惩罚从 2 倍优先级降到 0.5 倍之后,训练曲线明显好转。
4. DQN 实现与训练:经验回放、目标网络与超参数设置
4.1 为什么选择 DQN 而不是策略梯度
这个项目的动作空间是离散的,且可以枚举,非常适合 DQN。PPO 这类策略梯度算法在连续动作空间上有优势,但在这里需要额外处理动作合法性掩码,反而多一层复杂度。DQN 配合目标网络和经验回放,只需要前向网络拿到 Q 值,再用argmax输出动作;训练时调整网络参数让预测 Q 逼近目标 Q。
若想把效果再提一档,我会用 Double DQN 替代原版 DQN,把动作选择与价值评估分离,减小过估计偏差。改进后代码改动不大,只是目标 Q 计算时先用 online net 选动作,再用 target net 计算该动作的 Q 值。
4.2 状态特征与网络结构
状态向量由三部分拼接:卫星部分包括各星的位置向量、剩余观测时长和当前指向;目标部分包括每个目标的覆盖完成率、剩余可见窗口时长和优先级;全局部分包括归一化当前时刻。假设 3 颗星、8 个目标,状态维度大约是 36 + 84 + 1 = 51。网络结构我采用两层 256 宽度的全连接网络,输出为动作数,网络代码:
import torch.nn as nn class DQN(nn.Module): def __init__(self, state_dim, n_actions, hidden_dim=256): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, n_actions) ) def forward(self, x): # 输入 state tensor,输出每个动作的 Q 值 return self.net(x)输入状态里没有包含所有历史事件,因为调度决策主要依赖当前局面;如果希望网络记住目标出现的长期统计规律,可以在状态里加入目标到达密度的滑动平均,但会增加训练难度。我自己尝试时发现,简单的拼接当前覆盖比例就足够让 DQN 收敛到接近启发式上限的水平。
4.3 经验回放与训练循环
训练时先在缓冲区里存放(state, action, reward, next_state, done)五元组。采样时随机抽取一个批次,计算目标 Q 值,再用均方误差做梯度下降。目标网络每固定步数同步一次参数,防止自举导致 Q 值发散。下面是训练主循环的核心片段:
def train_dqn(env, agent, episodes=3000, sync_interval=500): replay_buffer = deque(maxlen=200000) for ep in range(episodes): state = env.reset() total_reward = 0.0 done = False while not done: epsilon = max(0.05, 1.0 - ep / 500.0) # 线性退火 action = agent.select_action(state, epsilon) next_state, reward, done = env.step(action) replay_buffer.append((state, action, reward, next_state, done)) if len(replay_buffer) >= 2000: batch = random.sample(replay_buffer, 64) loss = agent.update(batch, gamma=0.99) state = next_state total_reward += reward if ep % sync_interval == 0: agent.target_net.load_state_dict(agent.online_net.state_dict()) if ep % 200 == 0: print(f"episode {ep}, reward {total_reward:.2f}, epsilon {epsilon:.2f}")参数设置上,我常用批量大小 64,学习率 2.5e-4,折扣因子 0.99,经验回放容量 20 万条。动作选择采用 epsilon 贪心,从 1.0 线性退火到 0.05。环境单次 episode 的步数由时间窗长度决定,一般模拟 30 分钟观测窗口,时间步长 30 秒,一个 episode 约 60 步。经验回放容量越大,训练越稳定,但内存占用也高,环境状态维度超过 100 时建议降到 10 万容量。
4.4 超参数调整的对照表
训练过程中影响最大的超参数是折扣因子、目标网络同步间隔和非法动作惩罚。我整理了一张常用的调参起点表:
| 参数 | 推荐值 | 主要影响 |
|---|---|---|
| 折扣因子 gamma | 0.99 | 值过小会短视,只追求当前收益;过大导致训练波动 |
| 目标网络同步步数 | 500 | 越大越稳定,但策略更新滞后 |
| batch size | 64 | 过小梯度噪声大,过大训练慢 |
| 学习率 | 2.5e-4 | 高于 1e-3 容易 Q 值发散 |
| epsilon 退火终点 | 0.05 | 保留持续探索能力 |
| 非法动作惩罚 | -10 | 用于快速屏蔽不可见动作,不宜过大 |
如果 Q 损失在训练中期反弹,优先降低学习率或增大批量。如果总覆盖率一直上不去,说明奖励设计里机动代价权重太高,策略选择了保守空闲,应该降低机动代价系数。
5. 调度效果验证与常见踩坑
5.1 用时间轴切分验证策略泛化性
训练数据来自Fermi2021.csv、Integral2021.csv、Swift2021.csv,验证时我会把2021数据里末尾 20% 的时间段单独剥离,不参与训练。再看 DQN 策略与基线策略的总覆盖率。基线策略常用两种:一是优先观测优先级最高的目标(贪心),二是按最早截止时间排序(EDF)。通过比较,可以判断 DQN 到底学到的只是记忆事件表,还是学到可迁移的调度模式。
在做对比时,不能只看平均覆盖率,还要看高优先级目标按时完成率。因为 DQN 可能会牺牲普通目标来保高优先级,导致平均覆盖率下降,但工程上这是可接受的。
5.2 排查 Q 值波动与覆盖率异常
训练时最常见的现象是 Q 值在某一轮突然飙高。通常是奖励函数里有重复累加,比如没有在覆盖完成后将目标标记为 done,导致同一目标被反复计数。另一个典型问题是环境 step 中next_state返回了未深拷贝的对象,导致经验回放缓冲区里多条记录指向同一个可变列表,后期采样时状态被覆盖,训练完全失效。每次把状态存入缓冲区前,用copy.deepcopy或torch.tensor(state).clone()处理一下,能省很多排查时间。
如果训练后部署到随机生成的新目标序列上覆盖率很低,多半是训练时目标优先级分布与测试分布不一致。我一般会在训练时对 CSV 中的事件按流量重采样,并在每个 episode 里以 10% 概率随机插入一个高优先级事件,模拟应急目标。
5.3 利用真实事件做回放预热
最后一个值得尝试的技巧是,把Fermi+Integral+Swift2021.csv里的真实观测序列拆成多个短窗口,每个窗口内的事件顺序固定,但卫星初始位置随机偏移。用这些序列填充经验回放缓冲区当作预训练数据,让 DQN 在随机探索前先过一遍类似实际调度的轨迹。这样可以把训练收敛时间压缩到原来的六成左右,特别适合目标数量少但事件到达密集的场景。实现上只需要在训练主循环前跑几轮只执行随机动作的环境,把产生的转移堆进缓冲区,然后正常训练即可。
部署验证时,我会把 DQN 输出的调度序列逐条对照 CSV 原始时间戳检查:卫星对某个事件的观测时间是否落在了事件有效窗口内,同一时间点有没有两颗星指向同一个目标造成观测重叠。这些检查脚本用 pandas 就能写,发现问题再回头微调动作掩码或奖励权重,比盲目堆网络层数有效得多。
本文还有配套的精品资源,点击获取