🤖 roboto_origin_03 Wiki
首页 / 推理子模块 / 新增观测源与模型适配指南

本文面向需要扩展机器人策略输入维度的高级开发者,系统阐述如何在推理节点中注册新的观测源、组织观测布局,并确保 ONNX 模型的输入尺寸与运行时组装逻辑严格一致。阅读前建议先理解 观测布局配置与动态解析 中关于 obs_layout 字符串解析的基本机制,以及 ONNX Runtime 推理引擎集成 中的会话初始化流程。本指南不涉及强化学习训练本身,仅聚焦推理侧(C++ 运行时)的对接与适配。

Sources: obs_manager.cpp, inference_node.hpp

观测源架构总览

推理节点内部采用「注册表 + 布局描述 + 运行时组装」的三层架构。所有合法观测源首先在编译期注册于 obs_source_definitions() 静态表中,该表将字符串名称映射到类的成员函数指针;随后 YAML 中的 obs_layoutsextra_obs_layouts 在节点构造时被动态解析为 ObsSourceSpec 数组;最后 inference() 线程在每一推理步调用 update_obs_segments(),通过函数指针分派填充各段数据,再经归一化、截断、帧堆叠后送入 ONNX Runtime。若新增观测源,必须同时改动注册表、实现数据获取逻辑,并确保 YAML 与模型输入维度三者匹配。

Sources: obs_manager.cpp, inference_node.cpp

flowchart LR
    A[YAML obs_layout<br/>name:size 字符串] -->|parse_obs_layout| B[ObsSourceSpec 数组]
    C[obs_source_definitions<br/>静态注册表] -->|名称查找| B
    B -->|update_obs_segments| D[逐段填充<br/>std::vector<float>]
    D -->|flatten + clip| E[单帧观测向量 obs]
    E -->|update_stacked_obs| F[带历史堆叠的<br/>input_buffer]
    F -->|拼接 extra_obs| G[ONNX 输入张量]
    G -->|Session->Run| H[策略输出]

内置观测源速查

下表列出当前已注册的全部观测源、维度、数据来源及缩放配置,可作为新增 source 时的参考模板。

观测源名称 典型维度 数据来源 缩放系数 特殊说明
ang_vel 3 RobotInterface::get_ang_vel() obs_scales_ang_vel_ 机体角速度
gravity_b 3 IMU 四元数解算 obs_scales_gravity_b_ 机体坐标系重力向量;z 分量超过 gravity_z_upper 会触发倒地保护停机
cmd_vel 3 /joy/cmd_vel obs_scales_lin_vel_ / obs_scales_ang_vel_ 线速度 x, y 与偏航角速度
dof_pos joint_num RobotInterface::get_joint_q() obs_scales_dof_pos_ 已减去 joint_default_angle 并做 usd2urdf 重排序;超限即停机
dof_vel joint_num RobotInterface::get_joint_vel() obs_scales_dof_vel_ 关节速度
last_action joint_num 上一帧 ONNX 输出 output_buffer 策略自回归输入
motion_pos joint_num MotionLoader::get_pos() 仅 motion policy 使用
motion_vel joint_num MotionLoader::get_vel() 仅 motion policy 使用
interrupt 1 is_interrupt_ 原子标志 中断动作指示位
perception 可变 /robot_0/elevation_data 等外部 Topic 通常置于 extra_obs_layout,不参与帧堆叠

Sources: obs_manager.cpp, inference_node.hpp

新增观测源四步实践

要在运行时引入新的传感器或外部信息,需完成代码注册、数据获取、资源初始化和配置对接四个环节。以下以假设新增足底力传感器观测源 foot_force(维度为 4)为例,给出最小改动集合。

第一步:注册观测源定义

obs_source_definitions() 的静态初始化列表中追加名称与成员函数指针。名称即为后续 YAML 配置中的关键字,必须全局唯一。

// src/obs_manager.cpp 中修改
static const std::vector<ObsSourceDefinition> definitions{
    // ... 原有条目
    {"foot_force", &InferenceNode::get_foot_force_obs},
};

Sources: obs_manager.cpp

第二步:实现数据获取方法

inference_node.hpp 中声明私有方法,并在 obs_manager.cpp 中实现。实现职责为:将原始传感器数据写入传入的 segment 向量前端,必要时应用缩放或坐标变换。注意 segment 的大小已由 YAML 解析阶段确定,实现中不应改变其长度。

// inference_node.hpp 私有方法区声明
void get_foot_force_obs(std::vector<float>& segment);

// obs_manager.cpp 中实现
void InferenceNode::get_foot_force_obs(std::vector<float>& segment) {
    foot_force_buffer_ = robot_->get_foot_force();  // 假设 RobotInterface 已提供
    for (int i = 0; i < 4; i++) {
        segment[i] = foot_force_buffer_[i] * obs_scales_foot_force_;
    }
}

Sources: obs_manager.cpp, inference_node.hpp

第三步:初始化运行时资源

若新观测源依赖临时缓冲区或 ROS 订阅,需在 initialize_runtime_state() 中分配资源,并在构造函数中建立订阅。has_obs_source("foot_force") 可用于条件判断,避免在未配置该 source 的策略中浪费内存。

// inference_node.cpp 中 initialize_runtime_state() 追加
if (has_obs_source("foot_force")) {
    foot_force_buffer_.assign(4, 0.0f);
} else {
    foot_force_buffer_.clear();
}

// 构造函数中追加(若需外部 Topic)
foot_force_subscription_ = this->create_subscription<std_msgs::msg::Float32MultiArray>(
    "/foot_force", data_qos,
    std::bind(&InferenceNode::subs_foot_force_callback, this, std::placeholders::_1));

Sources: inference_node.cpp, inference_node.hpp

第四步:配置 YAML 并校验 ONNX 维度

在 YAML 的 obs_layoutsextra_obs_layouts 中以 foot_force:4 形式引用。随后重新训练或导出 ONNX 模型,使其输入尺寸满足以下公式:

input_size = (sum(obs_layout 各段尺寸) * frame_stack) + sum(extra_obs_layout 各段尺寸)

节点初始化时 setup_model() 会读取 ONNX 元数据并做严格校验;若尺寸不匹配将抛出 ONNX input size mismatch 异常并终止启动。

Sources: inference_node.cpp, ros_interface.cpp

帧堆叠顺序与模型输入排布

frame_stack > 1 时,运行时通过 update_stacked_obs() 将多帧历史拼接为单个输入向量。系统支持两种堆叠顺序,必须与训练时的 PyTorch 张量构造逻辑保持一致,否则策略行为会出现系统性偏移。

FrameMajor(默认):连续存储每一帧的完整观测,即 [frame0_obs, frame1_obs, ..., frameN_obs]。适用于大多数 MLP 策略,实现上采用整片内存滑动窗口。

ObsMajor:按观测字段分组存储历史,即 [field0_frame0..frameN, field1_frame0..frameN, ...]。适用于需要对特定字段做时序卷积或注意力处理的网络结构。obs_major 字段顺序与 YAML 中 obs_layout 的声明顺序一致。

Sources: inference_node.cpp, inference_node.hpp

主观测与 Extra 观测的差异

obs_layoutextra_obs_layout 在运行时具有本质区别:obs_layout 参与帧堆叠,因此其历史维度随 frame_stack 线性膨胀;extra_obs_layout 在堆叠后直接拼接到输入缓冲区尾部,不参与历史滑动。典型用法是将高维感知特征(如高程图 perception:187)置于 extra_obs_layout,避免时序重复带来的巨大输入膨胀。例如 inference_attn_enc.yamlobs_layout 仅包含本体感知(堆叠 5 帧),而 perceptionextra_obs_layouts: ["perception:187"] 形式作为当前帧附加特征输入编码器。

Sources: inference_node.cpp, config/inference_attn_enc.yaml

模型适配检查清单

在将新模型部署到推理节点前,请逐条核对以下事项:

检查项 验证方法 常见失败现象
观测源名称拼写与注册表一致 对比 obs_source_definitions() 与 YAML Unsupported obs source 启动异常
各段尺寸与 ONNX 输入形状乘积一致 手动计算 obs_num * frame_stack + extra_obs_num ONNX input size mismatch
帧堆叠顺序与训练脚本一致 检查 YAML obs_stack_orders 策略输出抖动或漂移
extra_obs_layout 中的字段不参与堆叠 确认模型输入尾部仅含单帧特征 高维感知策略输出异常
motion policy 的关节数与 joint_num 一致 MotionLoader::get_num_joints() Motion joint count mismatch
缩放系数与训练时归一化参数一致 对比 YAML obs_scales_* 与训练日志 全局性性能退化

Sources: inference_node.cpp, obs_manager.cpp

常见错误与排查

1. Unsupported obs source: xxx 原因:YAML 中使用了未在 obs_source_definitions() 注册的名称。解决办法:检查拼写,或按四步流程补全注册与实现。

2. ONNX input size mismatch 原因:YAML 配置的 frame_stack 或观测段尺寸与模型导出时不一致。特别注意修改 frame_stack 后必须重新导出 ONNX,因为输入张量的总元素数已改变。

3. 新观测源数据始终为零 原因:仅注册了定义和 getter,但遗漏了 initialize_runtime_state() 中的缓冲区分配,或 ROS 订阅未创建。可通过 has_obs_source() 分支进行条件初始化以避免此问题。

4. 多策略切换时维度冲突 原因:model_names 数组中的各策略若使用不同的 obs_layout,必须保证 obs_layoutsframe_stacksobs_stack_orders 等数组长度与 model_names 严格一致,且每份 ONNX 模型各自匹配对应索引的布局。运行时 active_policy_idx_ 切换后,推理线程会自动使用当前策略的专属 obs_segmentsinput_buffer,但模型文件本身必须预先生成正确。

Sources: ros_interface.cpp, inference_node.hpp

下一步

完成新增观测源与模型适配后,若需将新策略纳入多策略启动体系,请参考 多策略 YAML 配置详解 了解 model_namesmotion_names 与索引对齐规则;若涉及高维感知或注意力编码器网络,建议进一步阅读 观测值组装、归一化与帧堆叠 以深入理解运行时数据流。对于需要在中断或外部覆盖场景下使用新增观测源的情况,请参阅 中断动作与外部覆盖机制