在具身智能推理系统中,策略网络对输入张量的顺序、维度与语义极度敏感。本页面向中级开发者阐述 观测布局(Observation Layout) 的声明式配置语法、运行时动态解析机制,以及观测源注册表的工作原理。掌握这些机制后,你可以在新增策略模型或引入外部感知数据时,仅通过修改 YAML 配置与少量标准化接口完成适配,而无需改动核心推理循环。
核心架构:从 YAML 字符串到推理输入张量
观测布局系统采用三层架构:声明式配置 → 动态解析与校验 → 运行时组装。配置层通过人类可读的逗号分隔字符串描述每个策略所需的观测字段;解析层在节点启动时将字符串转为强类型的 ObsSourceSpec 列表,并校验字段名与维度的合法性;运行时层则在每次推理周期中,通过成员函数指针分派到对应的观测采集接口,将分段缓冲区拍平为 ONNX Runtime 所需的连续输入张量。
flowchart LR
A[YAML 配置<br/>obs_layouts / extra_obs_layouts] -->|字符串解析| B[parse_obs_layout]
B -->|校验字段名与维度| C[ObsSourceSpec 列表]
C -->|初始化分段缓冲区| D[PolicyRuntime]
D -->|推理周期调用| E[update_obs_segments]
E -->|成员函数指针分派| F[各观测源 Getter]
F -->|填充分段| G[flatten_obs_segments]
G -->|可选帧堆叠| H[update_stacked_obs]
H -->|拼接 extra_obs| I[ONNX 输入张量]
整个流程的起点是 load_config(),它从 ROS 2 参数服务器读取策略数组、观测布局数组与帧堆叠数组,并确保它们的长度严格一致。随后 parse_obs_layout() 接管字符串解析与语义校验工作。
Sources: ros_interface.cpp, obs_manager.cpp
配置语法与多策略示例
观测布局在 YAML 中以 逗号分隔的 name:size 对 表示,顺序即最终张量中的排列顺序。系统支持两种布局参数:
| 参数名 | 是否必填 | 长度约束 | 说明 |
|---|---|---|---|
obs_layouts |
是 | 与 model_names 等长 |
主观测布局,可被帧堆叠 |
extra_obs_layouts |
否 | 为空或等长 | 额外观测,不参与帧堆叠,直接拼接在主观测之后 |
以下对比三种典型场景的配置差异:
| 场景 | obs_layouts 示例 |
extra_obs_layouts |
frame_stacks |
特殊说明 |
|---|---|---|---|---|
| 基础 Locomotion | ang_vel:3, gravity_b:3, cmd_vel:3, dof_pos:23, dof_vel:23, last_action:23 |
无 | [10] |
标准行走策略 |
| 带感知编码器 | ang_vel:3, gravity_b:3, cmd_vel:3, dof_pos:23, dof_vel:23, last_action:23 |
perception:187 |
[5] |
地形感知不堆叠,直接追加 |
| 起身辅助 (Getup) | motion_pos:23, motion_vel:23, ang_vel:3, gravity_b:3, dof_pos:23, dof_vel:23, last_action:23 |
无 | [1] |
引入运动参考轨迹作为观测源 |
| 中断策略 | ang_vel:3, gravity_b:3, cmd_vel:3, dof_pos:23, dof_vel:23, last_action:23, interrupt:1 |
无 | [10] |
通过 interrupt:1 向策略暴露中断标志 |
配置文件中 obs_layouts 的字段顺序必须与 ONNX 模型训练时的输入顺序完全一致;否则 setup_model() 会在启动阶段检测到输入维度不匹配并报错终止。
Sources: config/inference.yaml, config/inference_attn_enc.yaml, config/inference_getup.yaml, config/inference_interrupt.yaml
观测源注册表与字段解析
解析阶段的核心数据结构有两层。ObsSourceDefinition 将字段名映射到 InferenceNode 的成员函数指针,构成静态注册表;ObsSourceSpec 则在运行时持有字段名、指向注册表的指针以及用户声明的维度大小。
classDiagram
class ObsSourceDefinition {
+const char* name
+void (InferenceNode::*get)(vector<float>& segment)
}
class ObsSourceSpec {
+string name
+const ObsSourceDefinition* source
+int size
}
class InferenceNode {
+obs_source_definitions() static
+parse_obs_layout(spec, name) vector~ObsSourceSpec~
+update_obs_segments(segments, layout)
}
ObsSourceSpec --> ObsSourceDefinition : 引用
InferenceNode ..> ObsSourceSpec : 生成并持有
parse_obs_layout() 的实现遵循严格的防御式校验逻辑:首先按逗号分割字符串并去除空白;随后要求每一段必须包含且仅包含一个冒号分隔符,左侧为字段名,右侧为无符号整数维度;字段名必须在 obs_source_definitions() 返回的静态列表中存在。任何一项不满足都将抛出 std::runtime_error,并附带易于定位的配置项名称(如 obs_layouts[0])。
当前系统内置了 10 个标准观测源,其语义与实现职责如下:
| 观测源 | 典型维度 | 数据来源 | 特殊行为 |
|---|---|---|---|
ang_vel |
3 | IMU 角速度 | 乘以 obs_scales_ang_vel_ |
gravity_b |
3 | IMU 四元数 | 转换到体坐标系;若 gravity_b.z > gravity_z_upper_ 则判定摔倒并紧急停机 |
cmd_vel |
3 | /cmd_vel 或手柄 |
前两项乘线速度缩放,第三项乘角速度缩放 |
dof_pos |
joint_num |
电机反馈 | 减默认角度后乘位置缩放;同时进行关节限位检查 |
dof_vel |
joint_num |
电机反馈 | 乘速度缩放 |
last_action |
joint_num |
策略上一帧输出 | 取自 ctx->output_buffer |
motion_pos |
joint_num |
MotionLoader |
复制当前帧参考位置 |
motion_vel |
joint_num |
MotionLoader |
复制当前帧参考速度 |
interrupt |
1 | 内部原子标志 | is_interrupt_ 为真时输出 1.0 |
perception |
视配置而定 | ROS Topic | 从 perception_obs_buffer_ 锁保护拷贝 |
Sources: obs_manager.cpp, obs_manager.cpp, obs_manager.cpp
运行时组装:分段采集与帧堆叠
在每次推理周期的 inference() 线程中,系统执行如下观测流水线:
- 分段更新:
update_obs_segments()遍历obs_layout,通过存储在ObsSourceSpec中的成员函数指针,依次调用各观测源的 Getter 函数,将数据写入对应的obs_segments[i]缓冲区。这种设计将布局的“元信息”与数据获取的“副作用”解耦,新增观测源只需在注册表中追加条目并实现对应的 Getter,无需修改组装逻辑。 - 拍平与裁剪:
flatten_obs_segments()按布局顺序将各分段std::copy到一维policy.obs向量中,随后对所有元素执行clip_observations_限幅。 - 帧堆叠:
update_stacked_obs()根据策略配置的frame_stack与stack_order维护时序历史。系统支持两种堆叠顺序:frame_major(帧优先):整帧观测连续存放,即[obs_t0, obs_t1, ..., obs_tN]。首帧时所有历史槽位用当前观测填充;后续周期将缓冲区左移一个obs_num,并在末尾追加最新帧。obs_major(字段优先):每个观测字段的历史连续存放,即[field0_t0..tN, field1_t0..tN, ...]。实现上以obs_layout_sizes为步长,对每个子字段分别做左移与追加。
- 追加额外观测:若策略声明了
extra_obs_layout,则重复步骤 1–2,将结果写入 ONNX 输入缓冲区的frame_stack * obs_num偏移之后。额外观测不经过帧堆叠,适用于地形编码器等一次性高维特征。
最终,setup_model() 在启动时已校验 ONNX 模型的输入尺寸必须恰好等于 obs_num * frame_stack + extra_obs_num,因此运行时的缓冲区拼接不会产生越界或维度错位。
Sources: inference_node.cpp, inference_node.cpp, obs_manager.cpp
多策略场景下的跨策略一致性约束
当配置中包含多个策略(如 model_names 长度为 4)时,load_config() 会强制要求 obs_layouts、frame_stacks、obs_stack_orders 与模型数量等长,而 extra_obs_layouts 与 motion_names 可为空或等长。这一设计保证了每个策略拥有独立的 PolicyRuntime 实例,从而在热切换时各自的布局、堆叠状态与运动参考帧互不影响。
值得注意的是,部分全局资源(如 perception_obs_num_、interrupt_action_)的初始化取决于 所有策略中是否出现过对应观测源。initialize_runtime_state() 通过 has_obs_source() 在全部策略的主布局与额外布局中搜索特定字段名,以决定是否分配对应的缓冲区。这意味着:只要有一个策略使用了 perception,感知订阅就会生效;只要有一个策略使用了 interrupt,中断动作缓冲区就会初始化。
Sources: ros_interface.cpp, inference_node.hpp, obs_manager.cpp
常见配置错误与排查
| 报错信息 | 根因 | 修复方式 |
|---|---|---|
obs_layouts[i] must be explicitly configured |
布局字符串为空或仅含空白 | 检查 YAML 中对应索引的字符串是否缺失 |
entry must use 'name:size' format |
缺少冒号、字段名或尺寸为空 | 确保格式严格为 name:size,无多余空格导致截断 |
field size must be a positive integer |
尺寸含非数字字符 | 检查是否误写为 dof_pos:23.0 或 dof_pos:23 (尾部空格不影响,但中间字符会) |
Unsupported obs source: xxx |
字段名不在注册表中 | 对照内置 10 个观测源检查拼写,或扩展 obs_source_definitions() |
ONNX input size mismatch |
布局计算出的总维度与模型不一致 | 核对训练代码中的观测构造逻辑,确保 obs_num * frame_stack + extra_obs_num 等于模型输入尺寸 |
Sources: obs_manager.cpp, inference_node.cpp
下一步
理解观测布局的声明与解析机制后,你可以继续阅读 观测值组装、归一化与帧堆叠 深入了解各观测源 Getter 的数学处理细节,或参考 新增观测源与模型适配指南 学习如何为自定义传感器扩展新的观测字段。若你的策略需要运动参考轨迹驱动,可前往 动作参考加载与 MotionLoader 了解 motion_pos 与 motion_vel 的数据来源。