着陆器悬停
本例使用着陆器运动轨迹训练世界模型和离散动作 PPO 策略,使着陆器在目标区域持续悬停。训练后通过 Box2D 仿真评估悬停比例与回报。
1. 任务背景与目标
用离散推力维持空间位置
着陆器受到重力和发动机推力的共同作用。主引擎主要改变竖直运动,两侧喷口影响横向运动及姿态。发动机以离散档位工作,控制器每一步选择关闭、左侧喷射、主引擎或右侧喷射,靠连续的档位切换维持悬停。
目标区域以 (x, y) = (0, 1) 为中心。到达目标位置还不够:如果速度较大,着陆器很快就会冲出区域。因此控制器需要结合位置偏差与速度,提前减速,并在偏离后重新调整推力。
本例使用位置与速度四维观测。姿态和角速度仍影响真实运动,但没有直接提供给策略,所以相同观测下的响应可能存在差异。世界模型需要从轨迹中学习这些控制动作与运动变化的关系,PPO 再学习使悬停时间更长的动作选择。
悬停区域与动作定义
本环境基于 Gym 的 LunarLander(Box2D)改造,任务目标是在目标区域附近持续悬停。 策略每步选择一个引擎档位;世界模型从已有交互记录学习动作对位置、速度的影响, PPO 再在学到的环境中优化悬停奖励。最后固定候选,在独立 Box2D 仿真中检验控制效果。

图:着陆器悬停环境。
观测 obs = [x, y, vx, vy] 是环境缩放后的坐标与速度,不能直接按米、米/秒解读。 examples/landerhover/lander_env.py还包含姿态、角速度与接触状态,但返回给策略的只有前 4 维; 因此这是部分可观测问题,4 维观测并不等于完整物理状态。
| 原始动作值 | 环境含义 |
|---|---|
0 | 不启动引擎 |
1 | 一侧侧喷,方向编码为 -1 |
2 | 主引擎 |
3 | 另一侧侧喷,方向编码为 +1 |
奖励与终止规则
| 使用位置 | 悬停区域判据 | 区域内 / 区域外奖励 |
|---|---|---|
离线策略优化:examples/landerhover/reward.py | abs(x) < 0.2 且 abs(y - 1) < 0.2,取下一步观测 | +10 / -0.3 |
独立仿真:examples/landerhover/lander_env.py | abs(x) <= 0.2 且 abs(y - 1) <= 0.2 | +10 / -0.3 |
| 日志基线 | 读取 NPZ 已存的 rew,不重算上述判据 | 按记录求和 |
两份实现对边界是否包含有细微差别,评估时按各自边界条件统计。 环境保留坠毁、越界、休眠和内部步数限制等终止条件;返回奖励最终被悬停奖励覆盖, 没有额外返回标准 LunarLander 的着陆加分或坠毁扣分。高悬停占比也不等于安全着陆保证。
2. 数据与准备
位置和速度需要成对记录,动作保留为实际引擎编号。由于轨迹可能因越界或触地提前结束,必须保存 done 与 index,避免把两次独立飞行拼成一次状态转移。数据中应覆盖四个动作以及目标区内外的运动状态。
处理后的 data/LanderHover.npz 来自原 REVIVE 示例,包含 100 条轨迹、31612 步。 轨迹可提前终止,不是 100 条各 400 步。
| 字段 | 形状 | 含义 |
|---|---|---|
obs | [31612, 4] | 当前缩放位置和速度 |
action | [31612, 1] | 原始类别值 0、1、2、3;转换后以 float32 存储 |
next_obs | [31612, 4] | 下一步观测 |
rew | [31612, 1] | 采集时记录的奖励 |
done | [31612, 1] | 采集时记录的终止标志 |
index | [100] | 每条轨迹的右开结束下标,int64 |
准备本任务的 LanderHover.npz 原料文件,放入 data/raw/。原始业务数据单独提供,不随 SDK 安装包分发;使用数据提供方授权的版本,并核对下述字段。
后续命令均在案例目录执行。已有有效处理文件时直接复用;仅需转换原料时执行:
cd examples/landerhover
python prepare_data.pyexamples/landerhover/prepare_data.py只转换文件:除 index 外转为 float32, index 转为 int64,不采集新轨迹,也不把动作预先变成 one-hot。 直接调用会重写处理文件,脚本没有 --force 选项;原料缺失时会报错,不会自动启动仿真。
| 环节 | 是否与独立仿真交互 |
|---|---|
| 获取已有数据、文件转换、日志分数统计 | 否;获取缺失数据可能需要网络 |
| 世界模型训练、模型内 PPO 与验证选模 | 否;策略只在学到的模型中推演 |
evaluate.py --model | 是;推进 Box2D 环境 |
run_all.sh | 是;包含最终独立仿真测试 |
3. 建模与配置
下面先展示 examples/landerhover/config.yaml 的配置,再结合本任务逐块解释。训练命令使用同一文件。
name: landerhover
version: '2.0'
graph:
nodes:
action:
inputs: [obs]
network:
backbone: mlp
hidden_dims: [256, 256]
activation: leakyrelu
delta_obs:
inputs: [obs, action]
network:
backbone: mlp
hidden_dims: [256, 256]
activation: leakyrelu
output_dist: normal
next_obs:
inputs: [obs, delta_obs]
function: builtin.delta_add
transitions: auto
columns:
- {name: obs_0, node: obs, type: continuous}
- {name: obs_1, node: obs, type: continuous}
- {name: obs_2, node: obs, type: continuous}
- {name: obs_3, node: obs, type: continuous}
- name: action
node: action
type: category
values: [0, 1, 2, 3]
data: {train_ratio: 0.5, batch_size: 1024, split_mode: outside_traj}
training:
device: auto
stages:
- name: venv
algorithm: venv.revive_p
hyperparameters:
epochs: 800
bc:
optimizer: {lr: 4.0e-05, weight_decay: 1.0e-06}
grad_clip: 50
loss: nll
adversarial:
start_epoch: 0
enabled: true
ood: {d_lr: 0.0006}
rollout: {horizon: 50, batch_size: 1024}
validation:
rollout: {enabled: true, interval: 20, horizon: 50, num_trajectories: 5}
selection: {metric: val/rollout/mae, mode: min}
- name: policy
algorithm: policy.ppo
inherit_from: venv
hyperparameters:
policy_nodes: [action]
epochs: 3000
rollout_horizon: 100
optimizer: {lr: 4.0e-05}
grad_clip: 50
epsilon: 0.2
gamma: 0.99
gae_lambda: 0.95
num_rollout_trajs: 256
value_hidden_dims: [256, 256]
reward: {path: reward.py, function: get_reward}
validation:
rollout:
enabled: true
interval: 1
force_final: true
horizon: 100
metrics: [reward_mean]
selection: {metric: val/rollout/reward_mean, mode: max}
output:
save_freq: 50
tensorboard: true
onnx: {required: true}配置入口
| 配置 | 世界模型阶段 | 策略阶段 | 用途与运行规模 |
|---|---|---|---|
examples/landerhover/config.yaml | venv.revive_p | policy.ppo | 完整训练入口,800 + 3000 epoch;显式记录网络、训练和选模设置 |
examples/landerhover/config.min.yaml | venv.revive_p | policy.ppo | 精简任务定义,仍含两个阶段;省略项由默认层补全,适合结合 smoke 检查流程 |
config.min.yaml 不是“只训练世界模型”的配置,也不是小规模 smoke。 比较默认值时先查看解析结果;要运行本页完整训练,使用完整配置。
离散动作如何接线
obs(4) ──→ action(类别分布,processed 维度 4)
obs(4), action(one-hot 4) ──→ delta_obs(动力学网络)
obs, delta_obs ──→ next_obs(builtin.delta_add)
下一时刻:obs ← next_obs数据中的动作只有一列;graph.columns 声明 type: category, values: [0, 1, 2, 3],归一化模块将其编码为 4 维 one-hot。 action 节点未显式指定分布,由类别列推断为 Onehot。 接收 one-hot 动作的是 delta_obs 网络,不是 next_obs 函数节点。
评估取动作分布的 mode,再通过 norm_module.deprocess(..., "action") 还原原始类别值,不应绕过类别映射把任意索引当成引擎编号。 数据取值、列定义、导出模型的映射与环境编码必须一致;不要只改 values 顺序复用旧权重。 有序离散的 type: discrete 与无序类别不是同一种建模语义。
配置逐块讲解
| 配置块 | 当前设置 | 解读 |
|---|---|---|
graph | 策略和增量网络均为 MLP [256, 256]、leakyrelu;增量分布 normal;transitions: auto | 网络学习动力学增量,加法用确定性函数;列定义负责类别编码 |
data | train_ratio: 0.5、batch_size: 1024、split_mode: outside_traj | 整条轨迹划分训练/验证,避免同一轨迹片段跨集合;不能据此宣称任意初态泛化 |
training.stages[venv] | 800 epoch;BC 的 NLL、学习率 4e-5、梯度裁剪 50;对抗训练从第 0 轮启用,推演 50 步 | 先学习单步转移,再优化多步预测 |
training.stages[venv].validation | 每 20 epoch 验证 5 条、每条 50 步;按 val/rollout/mae 最小选模 | 选择验证轨迹上的预测模型,不是在 Box2D 中选模型 |
training.stages[policy] | PPO 3000 epoch;继承 venv;推演 100 步、256 条;学习率 4e-5、gamma 0.99、GAE 0.95 | 在已选世界模型中优化分段常数悬停奖励 |
training.stages[policy].validation | 每 epoch 验证、末轮强制验证,100 步;按 val/rollout/reward_mean 最大选模 | 只代表模型内收益排序,需要独立仿真复核 |
output | 每 50 epoch 保存;TensorBoard;onnx.required: true | 保留训练与导出检查;导出成功不等于控制性能验收 |
4. 训练与选模
先检查已有数据与完整配置:
revive validate --config config.yaml --train-data data/LanderHover.npz --show-defaults
python evaluate.py --dataset --json--dataset 按 index 计算日志回报及正奖励步比例,不推进 Box2D。 它使用全部原始轨迹作为行为策略描述,不是独立策略测试集。
revive train --config config.yaml --train-data data/LanderHover.npz \
--run-id hover_full --log-dir logs --seed 42两个阶段顺序执行;inherit_from: venv 使用验证选出的世界模型输出文件继续训练策略。 查看 logs/hover_full/report.md、config.resolved.yaml 和阶段验证曲线, 最终部署入口为 models/env.pt、models/policy.pt。 首次运行可用新的 run ID 加 --profile smoke 检查流程。 每次训练使用独立 run ID。
离线阶段先确认损失与预测有限、类别映射正确、验证曲线无明显恶化,再记录模型内排序。 本配置未显式学习终止信号,不应把模型内 100 步收益直接当成存在提前终止的 400 步仿真回报。 若训练多个随机种子,保存各自的配置和选模指标;固定候选后再开展下一节测试。 本例没有独立 PID/FFPID 搜参入口,也不需要取得日志采集时的规则控制器。
一键运行与输出文件
若允许完整训练后进行独立仿真,可用一键入口替代上面的分步命令:
bash run_all.sh hover_oneclick config.yaml默认 run ID 为 revive、配置为 config.yaml、训练种子为 42; 脚本读取 SEED 环境变量。已有处理文件默认复用,FORCE_PREPARE 非空时强制重做文件转换。 脚本依次统计日志、训练、仿真评估,并再次运行仿真输出 logs/<run_id>/flagship.json。 最后两步是相同参数的两次执行,不是把一次结果转换为两种格式,也不是两个独立种子的验证。
5. 模型使用与评估
该阶段才调用 examples/landerhover/lander_env.py 的 Box2D 环境。 需要兼容的 Gym 与 Box2D(如源码环境中的 box2d-py),直接实例化自带环境, 不是标准 Gym LunarLander 的评分协议。
python evaluate.py --model logs/hover_full/models/policy.pt \
--episodes 100 --max-steps 400 --seed 0 --device cpu --json--model 与 --dataset 必须且只能给一个。即使评估模型,当前脚本也会先读取日志基线, 因此仍需 data/LanderHover.npz,或通过 --data 指定文件。 --config 仅为兼容参数,加载自包含部署模型时不用于重建网络。
评估协议
| 参数 | 默认值 |
|---|---|
--episodes | 100 |
--max-steps | 400 |
--seed | 0 |
每集最多执行 400 步,可能提前终止。脚本从指定 seed 派生逐集种子,并在 env.seed 存在时调用旧式种子接口;若依赖版本没有该接口,目前不会向 reset 传入 seed。应记录兼容依赖版本,不能保证任意 Gym 版本下同 seed 都可复现。 默认设备为可用 CUDA,否则 CPU;上面的显式 CPU 与一键脚本一致。
| 输出指标 | 含义与限制 |
|---|---|
real_return_mean/std | 先按实际回合奖励求和,再计算跨回合均值/标准差 |
real_hover_step_ratio | 所有回合正奖励步数之和 / 所有实际执行步数;不是各回合占比的简单平均 |
episodes | 仿真模式取参数;数据模式取 index 的轨迹数量 |
max_steps | JSON 总会写入参数值;在数据模式中不是轨迹真实长度,也不用于截断数据 |
人工可读输出还包括回报中位数与最小/最大值;当前未输出逐集长度、终止类型或姿态安全指标。 回报同时受到存活长度与悬停比例影响,不能简单除以 400 冒充实际每步均值。
日志基线是已存轨迹表现,不是把原专家重新放入相同初态的配对对照。 多候选仿真对比应固定种子、回合数、步数上限与设备;若用这批仿真结果继续挑选候选, 它就成为开发验证集,最终结论还应在另一组预先留出的种子上复核。
6. 结果与分析
在 Box2D 悬停环境中评估 100 集,每集最多 400 步,起始 seed 为 0。轨迹可能因触地或越界提前结束,悬停比例按实际执行步数统计。
| 对象 | 平均回报 | 悬停步占比 |
|---|---|---|
| 数据集控制行为 | 379.79 | 14.58% |
| PPO 悬停策略 | 3012 | 77.21% |
策略在目标区域内的停留比例明显高于采集数据中的控制行为。由于区内每步奖励为 +10,区外为 −0.3,增加有效悬停时间会提高回报。
分析时同时检查回报和悬停比例:较高回报可能来自更长的存活时间,也可能来自更稳定的悬停。比较不同候选时,应保持环境、初态种子、回合数和步数上限一致,并观察偏离目标后的恢复能力。
7. 迁移到实际业务
保留类别动作编码、已知状态更新公式与离线/独立测试的分界。 重新定义观测中缺失的动态信息、动作语义、奖励及终止条件,并核查日志对各档位的覆盖。 数据未覆盖的动作组合不能仅靠 Onehot 接线正确就获得可靠预测。 本例的悬停奖励和训练规模不应直接套用到安全关键业务。
阅读任务页约定,区分数据准备、训练验证和独立评估。