简介面向航天任务规划与深度强化学习研究者的一份实战工程包聚焦多星对区域目标的观测调度优化问题。资料将深度神经网络的特征表达与强化学习的序贯决策结合在动态卫星环境下训练智能体选择观测卫星、调整轨道策略从而提升区域覆盖效率适合希望将DRL落地到实际调度场景的开发者或相关专业学生参考。压缩包内共492个文件容量约198.33MB包含Python训练与评估脚本(.py)、仿真环境与轨道数据(.npz、.fits、.csv)、模型权重与训练记录(.pth、.torch、.h5)等还附带.ipynb示例与实验图表可支撑从环境模拟、训练调参到结果分析的全流程复现。目前已有193人学习下载适合具备一定Python与强化学习基础、希望深入理解多星调度建模的读者使用。用户可从中获得完整的工程目录结构、环境模拟器实现、DQN等深度强化学习算法代码、经验回放与策略网络设计以及多种卫星数据的预处理与可视化思路为扩展更复杂的星座观测任务提供可直接改写的基线方案。1. 为什么区域目标观测规划会让传统任务调度器集体翻车多星对区域目标观测规划做久了你会发现第一版“按收益排序贪心”的调度器一定会在第 3 天翻车条带之间互相抢机动时间存储和侧摆角消耗在整段弧段里连成一片覆盖率卡在 70% 就不再动。深度强化学习算法把这套调度当成序贯决策每颗卫星只看自己的局部状态靠奖励信号学会“什么时刻该抢、什么时刻该放弃”比手工写规则灵活得多。不管是尝试做原型验证的算法工程师还是做运控系统改造的技术负责人都需要先把场景缩到“几十颗星一块区域”再谈落地。这篇文章就是按“建模→选型→最小实现→排错→部署验证”的顺序把能直接照抄的部分拆开写。2. 把区域目标拆成可学习的 MDP条带划分、可见时间窗与状态动作设计区域目标不是点目标卫星也不是拍照就能覆盖一整块的相机。载荷沿星下点轨迹扫过地面形成的是带状的幅宽所以一切基于强化学习的规划都要先回答一个问题定义里的“观测对象”到底是什么。我一般会先按卫星载荷幅宽把区域切条带再把条带中心点当成可访问目标来计算时间窗。别一上来就做网格栅格化那会让动作空间膨胀到连 PPO 都救不回来。2.1 条带划分为什么按沿轨方向切而不是按经纬度网格切按经纬度网格切看似直观但实际上每颗星的轨迹方向不同同一个网格点在不同轨道上的侧摆角差异会很大。按轨迹方向切条带的好处是单条带只经过一颗星的某一段过境弧段姿态机动代价容易计算覆盖收益也不容易被两次观测重复夸大。条带宽度通常取载荷幅宽本身需要重叠余量时再加 10% 到 15%但这是你唯一要调的几何参数。这里给出一个用 numpy 实现的简化条带划分函数核心思路是把多边形顶点投影到垂直航迹方向再按幅宽分段。import numpy as np def split_area_into_strips(polygon_xy, track_direction_deg, strip_width_km): 把区域多边形拆成沿轨方向的条带。 polygon_xy: N*2 的顶点经纬度数组 track_direction_deg: 星下点轨迹方向角0 表示沿经线方向 strip_width_km: 单条带幅宽按载荷幅宽的 100% 取值 track_rad np.deg2rad(track_direction_deg) along np.array([np.cos(track_rad), np.sin(track_rad)]) perp np.array([-along[1], along[0]]) proj polygon_xy perp lo, hi proj.min(), proj.max() # 1 度纬度约 111 km经度方向要除以 cos(纬度)这里用近似值 span_km (hi - lo) * 111.0 n_strips max(1, int(np.ceil(span_km / strip_width_km))) strips [] for i in range(n_strips): c lo (i 0.5) * (hi - lo) / n_strips idx np.argmin(np.abs(proj - c)) strips.append(polygon_xy[idx]) return np.asarray(strips), n_strips area np.array([[100.5, 36.0], [102.0, 36.2], [102.1, 37.5], [100.8, 37.5]]) strips, n split_area_into_strips(area, track_direction_deg65.0, strip_width_km50.0) print(拆出条带数:, n, 中心点示例:, strips[0])这段代码里track_direction_deg不是随便填的它来自星下点轨迹在地面投影的方向。惯性系里卫星轨道的倾角和升交点赤经确定后地面轨迹方向是周期性变化的仿真时要按过境时刻的瞬时轨迹方向传入。条带宽度如果取一半幅宽重叠率会很高训练时覆盖收益虚高但真实载荷并没有冗余这一说所以默认取幅宽后续在奖励函数里用“新增覆盖面积”去抑制重复观测。2.2 可见时间窗把侧摆角和姿态机动时间一起折进去条带中心有了之后下一步是算出每颗星对每个条带的可见时间窗。工程上最省事的做法是使用 SGP4 轨道预报得到每个时刻的星下点位置再判断星下点与条带中心的地心角是否小于允许上限。允许地心角由最大侧摆角和轨道高度共同决定。无代码也能做预报精度在分钟级即可因为真正的姿态机动时间会吃掉秒级误差。一个常见失误是把“可见”定义成“侧摆角不超限”却忘了机动时间。卫星从当前条带的观测姿态转到目标条带需要以 0.5°/s、1°/s 这样的角速度做过境准备转过 30° 就是要 30 秒。在低轨场景里这段时长足以让目标飞出窄幅宽的条带。所以我会把时间窗两端各收缩一个机动时间得到“可用时间窗”训练时只考虑可用时间窗宁可少一批样本也不要环境里出现“看到但来不及拍”的假可行动作。2.3 状态、动作与奖励让智能体知道该抢哪条带状态设计不要太复杂。单星的局部状态我通常固定为归一化剩余可观测条带数、每条带剩余待覆盖比例、自身存储占用率、当前侧摆角、最近三个可用时间窗的剩余时间。如果需要多星协作再追加一个全局覆盖矩阵的压缩向量尺寸做成固定长度不要用变长数组进网络。动作空间推荐离散型0 表示空闲等待1 到 N 表示选第 N 条带观测。多星环境下动作空间共享某些条带不可见时用动作掩码挡掉。奖励函数采用增量覆盖和新覆盖比例设惩罚项奖励/惩罚项定义权重建议覆盖收益本步新增覆盖面积 / 目标总面积1.0空闲惩罚有可用窗口但选择等待-0.1机动惩罚侧摆角变化绝对值归一化-0.02存储惩罚存储超限或接近峰值-0.5奖励里最重要的不是绝对值而是“增量”二字。如果按总覆盖率给奖励多颗星都会抢同一个高价值条带因为任何人观测都能得到一样的总收益竞争信号会被掩盖。3. 深度强化学习算法选型与网络设计MAPPO 路线、CTDE 框架和训练参数怎么取舍算法选型这件事很多人一上来就上 MADDPG做完一版发现动作分支在离散选择上要反复调 Gumbel-Softmax训练方差大。我的对比结论是多星对区域目标观测规划首选 MAPPO。原因是动作本质是离散选择协作信号是稀疏的覆盖率变化而 MAPPO 作为多智能体 PPO稳定性和超参敏感度都更适合工程落地。3.1 为什么用 MAPPO 而不是 DQN、MADDPG 或独立 PPODQN 适合单智能体、动作空间小且目标固定的场景区域目标规划里每颗星的可用窗口随时间变化目标数量也跨批次变动Q 值更新很容易振荡。MADDPG 适合连续动作比如“把侧摆角调到某个连续角度”但这里动作是选哪条带用连续动作再映射回离散空间模拟的是没有必要的中间层。对比之下 MAPPO 的核心思路是每一个智能体有自己的 Actor 策略共享一个中央 Critic。Centralized TrainingDecentralized Execution训练时可以看全局信息部署时只保留局部 Actor。对卫星场景特别合适因为卫星彼此通信受限推理必须在星上独立完成。3.2 Actor-Critic 网络结构局部观测进 Actor全局观测进 Critic用 PyTorch 搭一个最小框架并不难。Actor 输入是单星局部观测obs_dim输出各条带动作 logitsCritic 输入是所有星的局部观测拼成的全局向量输出一个价值标量。import torch import torch.nn as nn class LocalActor(nn.Module): def __init__(self, obs_dim, n_actions, hidden256): super().__init__() self.net nn.Sequential( nn.Linear(obs_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, n_actions), ) def forward(self, obs, action_maskNone): logits self.net(obs) if action_mask is not None: # 不可见条带直接屏蔽不能让模型去学一个负无穷 logits 的物理含义 logits logits.masked_fill(~action_mask, -1e8) return logits class CentralCritic(nn.Module): def __init__(self, global_dim, hidden256): super().__init__() self.net nn.Sequential( nn.Linear(global_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, 1), ) def forward(self, global_obs): return self.net(global_obs)这里的全局观测只在训练阶段参与部署时整个 Critic 都可以丢掉。训练时有一个数据处理的细节多星环境里每颗星观测窗口不同拼接全局向量前要先按固定顺序排序否则同一批数据里星号顺序一变Critic 的输出就失去意义我一般会把所有星按轨道编号排序后拼接。3.3 PPO 训练参数先看覆盖率再看 lossMAPPO 的训练参数和 PPO 基本一致但多智能体场景要把 entropy coefficient 调大一点防止多颗星的策略过早同时锁死到同一条带。经验值如下参数建议区间说明learning_rate2e-4 ~ 5e-4从 3e-4 起步n_steps2048 ~ 4096过短导致方差大batch_size256 ~ 1024显存允许时取大clip_range0.15 ~ 0.25默认 0.2gae_lambda0.95 ~ 0.99偏 0.98ent_coef0.01 ~ 0.05多星场景别低于 0.01训练时画面很玄学loss 曲线不一定平滑真正要看的是“评价仿真器里的覆盖率”和“动作熵”。动作熵一直掉到接近 0说明策略已经确定化如果覆盖率还停留在低位基本是奖励函数设计问题不是优化器问题。这时候优先检查奖励的稀疏程度而不是去改网络层数。4. 最小可复现实现用 Gym 风格环境跑通单星区域目标观测调度要把问题缩到能在一台机器上跑通不要一上来就模拟完整的六十颗星。我通常先做单星环境把条带、时间窗、机动模型做完整用 MAPPO 跑通覆盖率上涨的曲线再平移到多星。单星环境的意义不只是调试它能把“状态动作定义是否合理”这个问题和“多智能体协作”切成两半。4.1 环境类的最小定义状态转移与奖励计算环境每走一步代表一个离散时间片时间片长度的选择直接影响收敛速度。时间片太短机动还没完成环境就要等到机动结束才能观测空转步骤多时间片太长目标快速飞出窗口。我一般把时间片设为 30 秒和姿态机动时间量级一致。import numpy as np class RegionObsEnv: 单星区域目标观测环境动作 0 表示空闲1..N 表示观测对应条带。 def __init__(self, strips, twindows, max_storage100.0, min_interval2, max_slew_rate1.0): self.strips strips # 条带中心点列表 self.twindows twindows # [(start, end), ...]单位时间片序号 self.max_storage max_storage self.min_interval min_interval # 连续观测必须间隔的步数 self.max_slew_rate max_slew_rate self.n_strips len(strips) self.time_idx 0 self.storage 0.0 self.slew_angle 0.0 self.covered np.zeros(self.n_strips) def step(self, action): reward 0.0 reset_flag False if action 0: reward - 0.1 # 空闲惩罚 else: sat_idx action - 1 start, end self.twindows[sat_idx] if start self.time_idx end: # 模拟机动耗时角度差越大越可能错过窗口 angle_cost abs(self.slew_angle - sat_idx * 10.0) if self.time_idx angle_cost * 0.1 end: gain self.covered[sat_idx] * 0.0 if self.covered[sat_idx] 1.0: gain 1.0 - self.covered[sat_idx] reward gain self.covered[sat_idx] 1.0 self.storage 2.0 self.slew_angle sat_idx * 10.0 if self.storage self.max_storage: reward - 0.5 reset_flag True self.time_idx 1 done reset_flag or self.time_idx 480 return self._build_obs(), reward, done, {} def _build_obs(self): # 局部观测剩余时间窗、覆盖状况、存储、侧摆角 return np.array([ self.time_idx / 480.0, self.covered.mean(), self.storage / self.max_storage, self.slew_angle / 50.0, ], dtypenp.float32)这个环境故意把姿态角和存储做了简化但保留了两个最容易让新人翻车的机制空闲惩罚和机动时间消耗。angle_cost的存在让智能体不能在同一时间片内连续观测两个大角度差条带否则会错过窗口。真实仿真里这种耦合会更复杂但最小实现里只要你能看到覆盖率从 30% 爬到 80%就说明状态动作定义是自洽的。4.2 训练主循环MAPPO 的最小调用方式单星环境可以直接用 PPO 算法训练不必先上多智能体框架。常见做法是借助 stable-baselines3 的 PPO 实现库内部已经处理了 GAE、clip 和 mini-batch你要做的只是把环境包进去。from stable_baselines3 import PPO env RegionObsEnv( stripsstrips, twindowswindows, max_storage100.0, min_interval2, max_slew_rate1.0, ) model PPO( MlpPolicy, env, learning_rate3e-4, n_steps2048, batch_size256, ent_coef0.02, clip_range0.2, gamma0.99, verbose0, ) model.learn(total_timesteps800_000) model.save(./region_obs_policy.zip)n_steps2048表示每轮收集 2048 步环境交互batch_size256是每一轮内更新时的样本子块大小。gamma 0.99 对分钟级时间片来说比较充裕如果时间片加长到 2 分钟建议降到 0.95否则远期奖励折损过快。ent_coef0.02是单星环境比较稳妥的探索量训练到后期如果发现策略反复震荡可以降到 0.01。保存权重时直接用了.zip后缀这是算法库默认的序列化格式。但注意这只存了网络权重没有存环境定义和轨道数据所以同时还要把归一化统计量、轨道预报结果和条带划分参数一起写入另一个文件。否则模型换了条带数量就完全没法推理。4.3 评估指标覆盖率高不等于策略好训练过程中的 reward 曲线只能帮你判断是否收敛不能用来向用户证明方案可用。我会单独写一个评估函数把一整段弧段拆成步进计算四个核心指标覆盖率、失败动作率、总机动角度和存储峰值。其中失败动作率特别值得看它统计的是智能体发起了观测但条带已经不可见的次数。覆盖率可以到 90%但如果失败动作率高说明策略在依赖某种“侥幸命中”的模式真实部署时批次一旦改变就会崩。评估时还会对比贪心基线每步选可见窗口里剩余时间最短的条带这个基线实现简单如果 DRL 策略连它都打不过那问题出在奖励或状态定义上不要怀疑算法本身。5. 多星观测规划落地避坑训练不收敛与策略“微调翻车”的 5 个排错点前面章节讲的都是“怎么做对”这一章讲的是我实际踩过、也看同事反复踩过的坑。多星场景和单星最大的区别在于单个智能体的奖励信号和多智能体之间的协作耦合在一起出问题时定位难度成倍上升。5.1 奖励不收敛几十万步之后覆盖率还是贴地现象训练了 60 万步覆盖率曲线像一条水平直线reward 偶尔跳高但很快又掉回低位。原因多半是奖励稀疏加量纲失衡。覆盖率 0 到 1 的增量和 -0.5 的存储惩罚混在一起时存储惩罚可能把正常覆盖收益全部抵消策略学到“什么都不做最安全”。解决把奖励各项拆开分别归一化。覆盖收益控制在 0~1存储惩罚设成条件触发而不是持续输出空闲惩罚也不要和覆盖收益直接相加。先单星跑通一个无存储、无机动约束的“理想环境”确认覆盖率能涨到上限再逐步加入约束。这叫增量式环境复杂度是排除奖励问题的标准手段。5.2 “假覆盖率”与时间步错位观测收益看着涨真实调度一测就崩现象仿真里覆盖率到达 92%但把同样的策略接到更精细的调度仿真器里覆盖率掉到 60%。原因不是策略没学到而是环境的时间推进方式太粗糙。离散时间片内假设姿态机动瞬时完成导致收益被提前计入真实卫星还在摆姿态。解决把“动作发出”和“观测开始”拆成两个事件。动作发出后先计算机动结束时刻只有机动结束时刻落在可见窗内才允许获得覆盖收益。这个修正会让训练变慢但评估结果才有参考价值。凡是环境里敢对机动时间做简化就能在网上找到一堆“训练很好、实测翻车”的血泪经验。5.3 多颗星集体锁死同一条带全局奖励的“搭便车”效应现象多星训练后覆盖率曲线正常但拆开每颗星的动作统计发现60% 的动作集中在同一条高价值条带其他条带覆盖率靠运气。原因在于奖励用的是全局新增覆盖每颗星从同一份奖励中取梯度高价值动作被反复放大。解决先用“独热覆盖矩阵”做状态输入奖励改为该星本次实际新增覆盖面积而不是全局覆盖率增量。如果还想进一步提升协作性再考虑 QMIX 或带有反事实基线的 MAPPO。工程上别一上来就上复杂价值分解优先把“收益归因”改成“贡献归因”。5.4 换一个区域目标就崩训练数据域太窄现象在固定区域 A 上训练 80 万步测试区域 B 时覆盖率骤降。原因很直接仿真只用了固定的区域多边形、固定的条带数和固定的可见时间窗。神经网络记住了这些固定参数没有学到可泛化的“观测决策”。解决训练过程域随机化。每个 episode 开始前把区域多边形顶点做小幅随机移动条带宽度在 45~60 km 之间随机时间窗长度按轨道预报加随机偏移。这是深度强化学习算法落地规划问题里性价比最高的代码改动比调网络结构更有效。5.5 版本与权重打包复现失败多发生在“zip 包不完整”而非算法现象训练成果存档后一个月后重新加载报invalid zip archive: could not find eocd整个实验记录报废。原因常是权重文件与配置数据分开保存归档时漏了环境版本或轨道数据或者分包传输导致文件截断。解决把实验目录当作交付物看待一条训练记录至少包含策略权重、环境源码、轨道预报输入、条带划分参数、随机种子、评估日志。一起打包成一个 zip并用校验值记录完整性。我不会在训练完成后立刻删中间模型而是保留最后一个 checkpoint 和训练日志因为奖励曲线图往往是定位问题最有用的“后悔药”。6. 把策略接入滚动时窗一个轻量验证仿真器和“查表部署”技巧训练完成后不能直接把模型文件丢给运控系统。多星对区域目标观测规划的现实约束是每天都有新的目标区域上报轨道根数也在持续更新。常见做法是把训练好的策略冻结为一个“候选动作生成器”放进滚动时窗里反复调用每次目标区域更新后重新计算条带与可见时间窗然后让策略输出一张“时间-动作-卫星”的决策表再由运控人员按优先级和硬约束做最后检查。验证这一步不需要重型系统一个轻量回放器就够了。它的输入是拍平的状态序列输出是策略动作序列再回放到细化仿真里对比覆盖率。def plan_with_policy(policy, windows, state_factory): plan [] for t in range(windows.shape[0]): obs state_factory.build(windows, t) available_mask state_factory.visible_mask(t) action, _ policy.predict(obs, maskavailable_mask) plan.append((t, int(action))) return plan这段代码里的policy.predict在部署时只加载 Actor 网络不加载 Critic因此推理内存占用通常在 10 MB 以内满足星载约束。每次生成计划后我会跑一遍回放确认覆盖率不低于上一次计划已规划的观测动作不重叠存储和能量曲线不越界。这一步相当于给策略上了保险。我习惯在归档时把这条命令的输入输出都列在实验目录里哪些是训练过的目标区域哪些是全新的评估区域。换一个区域跑不过先看训练域随机化的范围是否覆盖了新区域而不是直接调模型。这个方向真正值得投入的地方不在模型结构有多新颖而在训练环境和真实运控约束对齐了多少。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?