3. 执行器与传感器:给机器人装上“肌肉”和“神经”
上一章我们把四足机器人的骨架搭好了,但说实话,那只是一堆不会动的几何体。想让它在 MuJoCo 里跑起来,得给它装上两样东西:执行器(Actuator)——相当于肌肉,负责发力;传感器(Sensor)——相当于神经,负责感知。
我个人习惯把这一步叫做“赋予机器人生命力”。没有执行器,关节就是摆设;没有传感器,强化学习智能体就是个瞎子。今天我们就来搞定这两样。
3.1 执行器(Actuator):给关节装上电机
MuJoCo 支持多种执行器类型,但做四足机器人强化学习,我们几乎只用一种——位置伺服电机(position servo)。说白了,就是给每个关节设定一个目标角度,然后电机自己 PID 算力矩去追那个角度。
为什么不用力矩控制?嗯,这里要注意:强化学习初期策略乱跳,纯力矩控制很容易把机器人甩飞。位置伺服天然带阻尼,训练稳定得多。我在项目中试过纯力矩,结果前 100 万步机器人一直在原地抽搐……后来老老实实换回位置伺服。
3.1.1 电机定义语法
执行器定义在 <actuator> 标签内。每个关节对应一个 <motor> 或 <position> 子标签。我们来看一个标准配置:
<actuator>
<!-- 左前腿髋关节 -->
<position name="FL_hip" joint="FL_hip" kp="100" kv="10" forcerange="-50 50"/>
<!-- 左前腿膝关节 -->
<position name="FL_knee" joint="FL_knee" kp="100" kv="10" forcerange="-50 50"/>
<!-- 右前腿髋关节 -->
<position name="FR_hip" joint="FR_hip" kp="100" kv="10" forcerange="-50 50"/>
<!-- 右前腿膝关节 -->
<position name="FR_knee" joint="FR_knee" kp="100" kv="10" forcerange="-50 50"/>
<!-- 左后腿髋关节 -->
<position name="RL_hip" joint="RL_hip" kp="100" kv="10" forcerange="-50 50"/>
<!-- 左后腿膝关节 -->
<position name="RL_knee" joint="RL_knee" kp="100" kv="10" forcerange="-50 50"/>
<!-- 右后腿髋关节 -->
<position name="RR_hip" joint="RR_hip" kp="100" kv="10" forcerange="-50 50"/>
<!-- 右后腿膝关节 -->
<position name="RR_knee" joint="RR_knee" kp="100" kv="10" forcerange="-50 50"/>
</actuator>
3.1.2 参数详解
| 参数 | 含义 | 推荐值(四足) | 说明 |
|---|---|---|---|
name |
电机名称 | 腿_关节 | 建议统一命名规范,方便后续代码索引 |
joint |
绑定的关节名 | 与 body 定义一致 | 必须与 <joint> 的 name 完全匹配 |
kp |
位置增益(刚度) | 50~200 | 越大响应越快,但容易震荡 |
kv |
速度增益(阻尼) | 5~20 | 抑制震荡,建议 kp/kv ≈ 10 |
forcerange |
最大输出力矩范围 | -50 50 | 限制电机暴力输出,保护仿真稳定 |
3.2 接触传感器(Touch Sensor):让机器人“感觉”到地面
强化学习里,智能体怎么知道脚有没有着地?靠接触传感器。MuJoCo 的接触传感器可以检测两个几何体之间的接触力。我们通常在每个脚掌底部装一个。
你想想看,没有接触传感器,智能体连自己摔倒了都不知道,还怎么学走路?
3.2.1 定义接触传感器
传感器定义在 <sensor> 标签内。接触传感器用 <touch> 子标签:
<sensor>
<touch name="FL_foot_touch" site="FL_foot_site"/>
<touch name="FR_foot_touch" site="FR_foot_site"/>
<touch name="RL_foot_touch" site="RL_foot_site"/>
<touch name="RR_foot_touch" site="RR_foot_site"/>
</sensor>
这里有个关键点:site 属性引用的是 site 而不是 body。所以我们需要先在脚掌位置定义一个 site:
<body name="FL_foot" pos="0 0 -0.3">
<geom type="sphere" size="0.04" rgba="0 0 1 1"/>
<site name="FL_foot_site" pos="0 0 0" size="0.01"/>
</body>
site 可以理解为“虚拟点”,专门给传感器用的。它不参与碰撞,只提供位置和方向信息。
3.2.2 接触传感器的输出
接触传感器返回的是一个 标量值——接触力的大小(单位:牛顿)。在强化学习里,我们通常把它二值化:
- 力 > 0 → 脚着地(接触)
- 力 = 0 → 脚悬空(未接触)
这个信息对步态学习至关重要。比如,我们希望四足机器人在 trot 步态下,对角腿同时着地——没有接触反馈,智能体根本不知道自己的脚在哪。
3.3 关节位置传感器(Joint Pos):读取关节角度
关节位置传感器返回每个关节的 当前角度(单位:弧度)。这是强化学习观测空间(observation space)里最基础的信息之一。
3.3.1 定义关节位置传感器
<sensor>
<jointpos name="FL_hip_pos" joint="FL_hip"/>
<jointpos name="FL_knee_pos" joint="FL_knee"/>
<jointpos name="FR_hip_pos" joint="FR_hip"/>
<jointpos name="FR_knee_pos" joint="FR_knee"/>
<jointpos name="RL_hip_pos" joint="RL_hip"/>
<jointpos name="RL_knee_pos" joint="RL_knee"/>
<jointpos name="RR_hip_pos" joint="RR_hip"/>
<jointpos name="RR_knee_pos" joint="RR_knee"/>
</sensor>
注意:joint 属性引用的是关节名,不是 body 名。这个传感器直接读取 <joint> 的当前 qpos 值。
3.3.2 为什么需要关节位置传感器?
你可能觉得奇怪:执行器不是已经知道目标角度了吗?为什么还要传感器读实际角度?
原因很简单:目标角度 ≠ 实际角度。位置伺服电机只是“努力”去追目标,但由于惯性、外力、力矩限制,实际角度总会有些偏差。强化学习智能体需要知道真实状态,而不是理想状态。
我在项目中做过对比实验:用目标角度代替实际角度作为观测,训练出来的策略在仿真里看着还行,一迁移到实物机器人上就摔——因为实物电机的跟随误差比仿真大得多。所以,老老实实用传感器读真实值。
3.4 完整传感器配置一览
把上面所有传感器整合到一起,我们的 <sensor> 段长这样:
<sensor>
<!-- 接触传感器 -->
<touch name="FL_foot_touch" site="FL_foot_site"/>
<touch name="FR_foot_touch" site="FR_foot_site"/>
<touch name="RL_foot_touch" site="RL_foot_site"/>
<touch name="RR_foot_touch" site="RR_foot_site"/>
<!-- 关节位置传感器 -->
<jointpos name="FL_hip_pos" joint="FL_hip"/>
<jointpos name="FL_knee_pos" joint="FL_knee"/>
<jointpos name="FR_hip_pos" joint="FR_hip"/>
<jointpos name="FR_knee_pos" joint="FR_knee"/>
<jointpos name="RL_hip_pos" joint="RL_hip"/>
<jointpos name="RL_knee_pos" joint="RL_knee"/>
<jointpos name="RR_hip_pos" joint="RR_hip"/>
<jointpos name="RR_knee_pos" joint="RR_knee"/>
</sensor>
一共 4 个接触传感器 + 8 个关节位置传感器 = 12 个传感器。这 12 个值构成了强化学习观测空间的核心部分。
3.5 本章知识结构图
下面这张图帮你理清执行器和传感器的关系:
3.6 验证配置
写完 XML 后,怎么知道对不对?我一般用 MuJoCo 自带的 compile 功能快速验证:
import mujoco
# 加载模型
model = mujoco.MjModel.from_xml_path("quadruped.xml")
# 检查执行器数量
print(f"执行器数量: {model.nu}") # 应该输出 8
# 检查传感器数量
print(f"传感器数量: {model.nsensor}") # 应该输出 12
# 打印所有传感器信息
for i in range(model.nsensor):
print(f"传感器 {i}: {model.sensor(i).name}")
如果输出符合预期,恭喜你——你的四足机器人现在既有肌肉又有神经了。
check_model.py,每次修改 XML 后跑一遍。我习惯把它加到 Git 的 pre-commit hook 里,防止提交坏掉的模型文件。