跳转到内容

着陆器悬停 ​

本例使用着陆器运动轨迹训练世界模型和离散动作 PPO 策略,使着陆器在目标区域持续悬停。训练后通过 Box2D 仿真评估悬停比例与回报。

1. 任务背景与目标 ​

用离散推力维持空间位置 ​

着陆器受到重力和发动机推力的共同作用。主引擎主要改变竖直运动,两侧喷口影响横向运动及姿态。发动机以离散档位工作,控制器每一步选择关闭、左侧喷射、主引擎或右侧喷射,靠连续的档位切换维持悬停。

目标区域以 (x, y) = (0, 1) 为中心。到达目标位置还不够:如果速度较大,着陆器很快就会冲出区域。因此控制器需要结合位置偏差与速度,提前减速,并在偏离后重新调整推力。

本例使用位置与速度四维观测。姿态和角速度仍影响真实运动,但没有直接提供给策略,所以相同观测下的响应可能存在差异。世界模型需要从轨迹中学习这些控制动作与运动变化的关系,PPO 再学习使悬停时间更长的动作选择。

悬停区域与动作定义 ​

本环境基于 Gym 的 LunarLander(Box2D)改造,任务目标是在目标区域附近持续悬停。 策略每步选择一个引擎档位;世界模型从已有交互记录学习动作对位置、速度的影响, PPO 再在学到的环境中优化悬停奖励。最后固定候选,在独立 Box2D 仿真中检验控制效果。

基于 Gym LunarLander 改造的 Box2D 场景中,着陆器通过引擎喷射在月面上方悬停的环境动图

图:着陆器悬停环境。

观测 obs = [x, y, vx, vy] 是环境缩放后的坐标与速度,不能直接按米、米/秒解读。 examples/landerhover/lander_env.py还包含姿态、角速度与接触状态,但返回给策略的只有前 4 维; 因此这是部分可观测问题,4 维观测并不等于完整物理状态。

原始动作值环境含义
0不启动引擎
1一侧侧喷,方向编码为 -1
2主引擎
3另一侧侧喷,方向编码为 +1

奖励与终止规则 ​

使用位置悬停区域判据区域内 / 区域外奖励
离线策略优化:examples/landerhover/reward.pyabs(x) < 0.2 且 abs(y - 1) < 0.2,取下一步观测+10 / -0.3
独立仿真:examples/landerhover/lander_env.pyabs(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 安装包分发;使用数据提供方授权的版本,并核对下述字段。

后续命令均在案例目录执行。已有有效处理文件时直接复用;仅需转换原料时执行:

bash
cd examples/landerhover
python prepare_data.py

examples/landerhover/prepare_data.py只转换文件:除 index 外转为 float32, index 转为 int64,不采集新轨迹,也不把动作预先变成 one-hot。 直接调用会重写处理文件,脚本没有 --force 选项;原料缺失时会报错,不会自动启动仿真。

环节是否与独立仿真交互
获取已有数据、文件转换、日志分数统计否;获取缺失数据可能需要网络
世界模型训练、模型内 PPO 与验证选模否;策略只在学到的模型中推演
evaluate.py --model是;推进 Box2D 环境
run_all.sh是;包含最终独立仿真测试

3. 建模与配置 ​

下面先展示 examples/landerhover/config.yaml 的配置,再结合本任务逐块解释。训练命令使用同一文件。

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.yamlvenv.revive_ppolicy.ppo完整训练入口,800 + 3000 epoch;显式记录网络、训练和选模设置
examples/landerhover/config.min.yamlvenv.revive_ppolicy.ppo精简任务定义,仍含两个阶段;省略项由默认层补全,适合结合 smoke 检查流程

config.min.yaml 不是“只训练世界模型”的配置,也不是小规模 smoke。 比较默认值时先查看解析结果;要运行本页完整训练,使用完整配置。

离散动作如何接线 ​

text
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网络学习动力学增量,加法用确定性函数;列定义负责类别编码
datatrain_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. 训练与选模 ​

先检查已有数据与完整配置:

bash
revive validate --config config.yaml --train-data data/LanderHover.npz --show-defaults
python evaluate.py --dataset --json

--dataset 按 index 计算日志回报及正奖励步比例,不推进 Box2D。 它使用全部原始轨迹作为行为策略描述,不是独立策略测试集。

bash
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
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 的评分协议。

bash
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 仅为兼容参数,加载自包含部署模型时不用于重建网络。

评估协议 ​

参数默认值
--episodes100
--max-steps400
--seed0

每集最多执行 400 步,可能提前终止。脚本从指定 seed 派生逐集种子,并在 env.seed 存在时调用旧式种子接口;若依赖版本没有该接口,目前不会向 reset 传入 seed。应记录兼容依赖版本,不能保证任意 Gym 版本下同 seed 都可复现。 默认设备为可用 CUDA,否则 CPU;上面的显式 CPU 与一键脚本一致。

输出指标含义与限制
real_return_mean/std先按实际回合奖励求和,再计算跨回合均值/标准差
real_hover_step_ratio所有回合正奖励步数之和 / 所有实际执行步数;不是各回合占比的简单平均
episodes仿真模式取参数;数据模式取 index 的轨迹数量
max_stepsJSON 总会写入参数值;在数据模式中不是轨迹真实长度,也不用于截断数据

人工可读输出还包括回报中位数与最小/最大值;当前未输出逐集长度、终止类型或姿态安全指标。 回报同时受到存活长度与悬停比例影响,不能简单除以 400 冒充实际每步均值。

日志基线是已存轨迹表现,不是把原专家重新放入相同初态的配对对照。 多候选仿真对比应固定种子、回合数、步数上限与设备;若用这批仿真结果继续挑选候选, 它就成为开发验证集,最终结论还应在另一组预先留出的种子上复核。

6. 结果与分析 ​

在 Box2D 悬停环境中评估 100 集,每集最多 400 步,起始 seed 为 0。轨迹可能因触地或越界提前结束,悬停比例按实际执行步数统计。

对象平均回报悬停步占比
数据集控制行为379.7914.58%
PPO 悬停策略301277.21%

策略在目标区域内的停留比例明显高于采集数据中的控制行为。由于区内每步奖励为 +10,区外为 −0.3,增加有效悬停时间会提高回报。

分析时同时检查回报和悬停比例:较高回报可能来自更长的存活时间,也可能来自更稳定的悬停。比较不同候选时,应保持环境、初态种子、回合数和步数上限一致,并观察偏离目标后的恢复能力。

7. 迁移到实际业务 ​

保留类别动作编码、已知状态更新公式与离线/独立测试的分界。 重新定义观测中缺失的动态信息、动作语义、奖励及终止条件,并核查日志对各档位的覆盖。 数据未覆盖的动作组合不能仅靠 Onehot 接线正确就获得可靠预测。 本例的悬停奖励和训练规模不应直接套用到安全关键业务。

阅读任务页约定,区分数据准备、训练验证和独立评估。