观测值(Observation)是策略网络感知外部世界与自身状态的唯一输入。本章聚焦推理节点如何将来自 IMU、关节编码器、手柄指令、外部感知等多源异构数据,经过归一化缩放、分段组装、数值裁剪与帧堆叠,最终拼接为符合 ONNX 模型输入尺寸要求的连续张量。整个流程在推理线程的每一个周期内执行,直接决定策略的时序一致性与控制稳定性。
观测管线总览
单周期内的观测处理遵循一条严格的流水线:多源采集 → 分段填充 → 展平归一 → 全局裁剪 → 时序堆叠 → 额外字段拼接 → 张量绑定。所有步骤均在 InferenceNode::inference() 的循环体内串行完成,无动态内存分配,以保证硬实时约束下的确定性延迟。
flowchart LR
A[IMU/关节/手柄/感知] --> B[观测源 Getter]
B --> C[分段缓冲区<br/>obs_segments]
C --> D[展平为一维<br/>obs vector]
D --> E[clip_observations<br/>全局裁剪]
E --> F[update_stacked_obs<br/>帧堆叠]
F --> G[extra_obs_segments<br/>额外字段拼接]
G --> H[ONNX 输入张量<br/>input_buffer]
Sources: inference_node.cpp
观测源注册与布局解析
系统通过静态注册表维护十种内置观测源。ObsSourceDefinition 结构将字符串名称映射到对应的成员函数指针,实现运行时动态分派。
| 观测源名称 | 尺寸典型值 | 数据来源 | 说明 |
|---|---|---|---|
ang_vel |
3 | IMU | 机体角速度(经外参旋转矩阵变换) |
gravity_b |
3 | IMU | 世界重力在机体坐标系下的投影 |
cmd_vel |
3 | 手柄 / ROS Twist | 线速度 x、y 与角速度 yaw |
dof_pos |
23 | 关节编码器 | 相对于默认姿态的关节位置偏差 |
dof_vel |
23 | 关节编码器 | 关节角速度 |
last_action |
23 | 上一帧策略输出 | 上一周期网络输出的动作值 |
interrupt |
1 | 中断标志 | 是否处于中断覆盖状态 |
perception |
N | 外部感知节点 | 如高程图、地形编码等 |
motion_pos |
23 | MotionLoader | 参考动作帧的关节位置 |
motion_vel |
23 | MotionLoader | 参考动作帧的关节速度 |
Sources: obs_manager.cpp
配置语法与解析
YAML 中的 obs_layouts 与 extra_obs_layouts 采用逗号分隔的 name:size 语法。parse_obs_layout 首先按逗号切分字符串,再逐段校验名称是否存在于注册表、尺寸是否为正整数,最终生成 ObsSourceSpec 向量。其中 obs_layout 定义参与帧堆叠的主观测向量,extra_obs_layout 定义不堆叠、直接拼接在时序数据之后的额外观测。两者在 load_config 阶段分别解析,并累加得到 obs_num 与 extra_obs_num。
Sources: obs_manager.cpp, ros_interface.cpp
实时采集与归一化
每种观测源在对应的 Getter 中完成硬件读取、坐标变换与数值缩放,将原始物理量转换为无量纲的模型输入。以下按数据源分类说明。
IMU 观测:角速度与重力投影
get_ang_vel_obs 通过 robot_->get_ang_vel() 获取经过外参旋转矩阵变换后的三维角速度,再乘以 obs_scales_ang_vel_ 进行缩放。
get_gravity_b_obs 则利用当前四元数将世界坐标系下的重力向量 $(0,0,-1)$ 旋转至机体坐标系。该过程不仅完成归一化前的坐标变换,还内含安全监控:当机体重力投影的 $z$ 分量大于 gravity_z_upper_(默认 -0.5)时,系统判定机器人已摔倒,立即触发 rclcpp::shutdown() 终止运行。
Sources: obs_manager.cpp
关节观测:位置偏差与速度
get_dof_pos_obs 读取关节位置后,先通过 usd2urdf_ 映射表完成 USD 到 URDF 的关节索引重排,再减去 joint_default_angle_ 得到相对于默认姿态的偏差,最后乘以 obs_scales_dof_pos_。同函数内还执行硬限位检查:若任一关节超出 joint_limits_ 范围,立即 fatal 并停机。
get_dof_vel_obs 仅执行索引重排与 obs_scales_dof_vel_ 缩放,无时序差分计算,因为原始速度已由底层电机驱动提供。
Sources: obs_manager.cpp
指令与历史动作
get_cmd_vel_obs 读取受 clip_cmd_ 约束的手柄或 /cmd_vel 指令,前两项(线速度)使用 obs_scales_lin_vel_,第三项(角速度)使用 obs_scales_ang_vel_。get_last_action_obs 则直接将上一帧策略网络的原始输出(尚未经过 action_scale 与默认偏置还原)回传,构成闭环记忆。
Sources: obs_manager.cpp, obs_manager.cpp
外部与离散观测
get_interrupt_obs 将原子布尔标志转换为 0.0 或 1.0 的标量;get_perception_obs 在互斥锁保护下拷贝外部感知话题数据。两者均不经额外缩放,依赖上游节点在发布前完成归一化。motion_pos 与 motion_vel 仅在配置包含动作参考时生效,直接从 MotionLoader 的当前帧拷贝。
Sources: obs_manager.cpp, obs_manager.cpp
分段组装与全局裁剪
完成各段观测填充后,系统执行两次内存操作将离散分段聚合为连续输入。首先,update_obs_segments 遍历 obs_layout,通过成员函数指针动态调用各 Getter,将结果写入 policy.obs_segments[i]。随后,flatten_obs_segments 按顺序将各段 std::copy 至一维 policy.obs 向量,实现内存布局的连续化。
展平后的主观测向量还需经过全局数值裁剪:std::clamp(val, -clip_observations_, clip_observations_) 对每个元素执行饱和限幅。该步骤是防止传感器野值或瞬时冲击导致策略网络进入异常激活区的最后防线。
Sources: obs_manager.cpp, inference_node.cpp
帧堆叠机制
深度强化学习策略通常需要时序上下文以估计隐藏状态或速度。系统通过 update_stacked_obs 在推理线程内维护一个滑动历史窗口,支持两种内存布局,以兼容不同训练框架导出的 ONNX 模型。
FrameMajor:帧优先布局
FrameMajor 模式下,输入缓冲区被视作 frame_stack 个完整观测帧的顺序排列。对于非首帧,缓冲区整体左移一帧(std::move),并将最新观测拷贝至尾部;首帧则简单重复填充 frame_stack 次当前观测,避免冷启动时的时序 discontinuity。
假设单帧观测维度为 $N$,堆叠数为 $K$,则缓冲区布局为: $$[o_{t-K+1}^{(0..N-1)},; o_{t-K+2}^{(0..N-1)},; \dots,; o_{t}^{(0..N-1)}]$$
Sources: inference_node.cpp
ObsMajor:观测优先布局
ObsMajor 模式按字段分块堆叠。对于 obs_layout 中每个尺寸为 $F$ 的字段,该字段的 $K$ 个历史值在内存中连续存放,随后才是下一个字段的堆叠。其布局为:
$$[\underbrace{f_1^{(t-K+1..t)}}{K \cdot F_1},; \underbrace{f_2^{(t-K+1..t)}}{K \cdot F_2},; \dots]$$
实现上,update_stacked_obs 依据 policy.obs_layout_sizes 逐字段执行左移与拷贝,因此字段间无内存重叠,支持变长字段的混合布局。
Sources: inference_node.cpp
堆叠模式对比
| 维度 | FrameMajor | ObsMajor |
|---|---|---|
| 内存布局 | 整帧连续,帧间相邻 | 字段内连续,字段间相邻 |
| 适用场景 | 模型以整帧为时序单元(如 Transformer、RNN) | 模型按特征通道处理时序(如 1D-CNN 逐通道卷积) |
| 配置关键字 | frame_major |
obs_major |
| 首帧填充 | 整帧复制 $K$ 次 | 每字段独立复制 $K$ 次 |
graph TD
subgraph FrameMajor ["FrameMajor(帧优先)"]
F1[帧 t-2] --> F2[帧 t-1]
F2 --> F3[帧 t]
end
subgraph ObsMajor ["ObsMajor(观测优先)"]
O1[ang_vel 历史] --> O2[gravity_b 历史]
O2 --> O3[dof_pos 历史]
end
额外观测与不参与堆叠的字段
并非所有输入都需要时序上下文。extra_obs_layout 专门用于承载当前时刻即可完整表达的观测,例如地形感知编码或中断标志。在推理循环中,系统先完成主观测的帧堆叠,再调用 update_obs_segments 与 flatten_obs_segments 将 extra_obs_segments 拷贝至 input_buffer 的起始偏移量 frame_stack * obs_num 之后。
因此,ONNX 模型的实际输入维度满足: $$\text{input_size} = \underbrace{\text{obs_num} \times \text{frame_stack}}{\text{时序堆叠}} + \underbrace{\text{extra_obs_num}}{\text{瞬时额外}}$$
setup_model 在初始化阶段严格校验该尺寸是否与 ONNX 张量形状一致,不匹配则立即抛出运行时错误,防止在实时循环中出现隐蔽的内存越界。
Sources: inference_node.cpp, inference_node.cpp
运行时状态重置
当策略切换、推理暂停或接收到重置服务时,reset_policy_runtime 将主观测、额外观测、输入输出缓冲区全部置零,并将 is_first_frame 标记恢复为 true。这确保下一周期重新进入帧堆叠时会执行复制填充而非滑动追加,避免跨策略的历史帧污染。
Sources: inference_node.cpp
延伸阅读与下一步
- 若需理解观测布局配置如何在 YAML 中声明与多策略适配,请参阅 观测布局配置与动态解析。
- 若需了解动作参考数据(motion_pos / motion_vel)的加载与插值机制,请参阅 动作参考加载与 MotionLoader。
- 若需深入 ONNX Runtime 的会话创建与张量绑定细节,请参阅 ONNX Runtime 推理引擎集成。