OpenDuckMini 参考运动生成
2023年10月,迪士尼通过展示基于《星球大战》电影的双足情感 BDX 机器人,吸引了许多人的想象力。他们设计的一个有趣方面是机器人平滑而稳健的双足运动。创建具有这种运动的系统是机器人学的核心挑战,值得注意的是,模拟+强化学习的进步一直是当前进展的核心推动者。
OpenDuckMini 是一个受 BDX 机器人启发的开源项目,提供了如何组装此类系统的指导。它最初由 Antoine Pirrone 创建,激励了全世界的爱好者。理解机器人代码可能令人生畏,特别是模仿学习方面。这是一个了不起的项目,我发现通过其内部工作来理解这个机器人如何学习行走是一个有益的挑战。
简而言之,我希望能在这里涵盖一些对机器人模仿学习工作原理的新手 ML 从业者特别有用的内容。在本文中,我们将重点:
- 解析
.urdf机器人格式 - 使用 PlaCo 为 OpenDuckMini 生成参考行走模式
- 将多项式系数拟合到该运动上,以便将其用作模仿学习的训练信号
通过这样做,我们本质上解析了原始 OpenDuckMini 项目中实现行走的关键组件。
在本文的任何位置,完整源代码(代码片段取自其中)可以在我的行走引擎重新实现中找到。或者,也可以在 OpenDuckMini 仓库中导航原始行走引擎。
1、理解运动学习管道
设计具有有意图、自然和富有表现力的运动的机器人是使其在实际应用中可用的关键 [1, 2, 3]。然而,仅通过传统的基于物理的建模方法来实现复杂运动(如双足步态周期)具有挑战性。
这有几个原因:
- 复杂机器人中存在大量关节和执行器 — 设计能够跨越所有这些关节运行的稳健控制系统是一项具有挑战性的工作。
- 对于高度特定的运动类型(如创意应用中的运动), 希望实现特定目标运动的设计人员与实现此目标所需的计算建模之间可能存在翻译障碍。
2、初始设置
在我们开始之前,有一些推荐的设置需要完成:
- 从头开始为我们的自定义行走引擎创建一个新项目目录。
- 从 https://github.com/apirrone/Open_Duck_Mini 克隆仓库,并将所有机器人文件复制到您的目录中。
- 让我们进行健全性检查,并查看一些机器人
.urdf文件。
如前所述,请随时使用我的行走引擎重新实现或原始参考运动生成代码获取更多示例。
3、使用 PlaCo 构建行走引擎
PlaCo 是法国 Rhoban 研究所的运动规划和控制库。我们可以获取已转换为 .urdf 格式的机器人模型,并使用此库创建可作为我们要执行的下游任务(例如模仿学习)参考的轨迹 [4]。
为了说明我们为什么使用 PlaCo 这样的平台,我将尝试回答一些可能出现的问题。理解这些可以帮助进一步建立关于为什么参考运动至关重要的直觉。
为什么我们需要 PlaCo?我们不能直接使用物理模拟器(如 MuJoCo)吗?
OpenDuckMini 仓库也使用 MuJoCo,理解 PlaCo 如何融入这一切可能会令人困惑。毕竟,我们不能只在 MuJoCo 中生成参考运动吗?
这是一个有趣的问题,我向朋友解释的一种方式是这样思考——假设你有一只四足狗(很像 Boston Dynamics 的 Spot),你想训练它在火星和地球上稍微粗糙和平坦的地形上行走。
- 理想情况下,无论我们在什么物理环境中,它的步态都不应发生显著变化。想想你走路的方式——即使在不同的地形甚至经历不同重力的极端情况下,你的运动仍然会遵循某种人类步态的风格。
- 然而,环境物理确实会发生变化——这是我们想要学习和适应的。
因此,它作为一个有用的管道,包含我们希望学习的参考运动(我们通过 PlaCo 获得),然后使用专用的物理模拟器,使我们受到所需物理的约束,同时机器人学习尝试实现参考运动。这是通过模仿学习让机器人学习运动背后的核心思想。
至关重要的是,值得注意的是 PlaCo 不是全面的物理模拟器。相反,它是一个使用逆运动学/动力学(IK/ID)根据我们指定的一组运动任务来计算运动物体轨迹的工具。由于它本质上更简单,我们可以使用它来生成运动,这些运动虽然可能缺乏底层物理真实性,但仍然可以作为一个很好的参考点。
稍后使用 MuJoCo 进行模仿学习的主要好处是,我们可以将其与所需的参考运动进行比较(即使其某些物理特性略有不匹配,也可以更加自由和创造性地定义),然后让模型学习在物理约束环境中复制该运动。
PlaCo 在底层做什么?
在上图中,我提供了 PlaCo 内部工作的简化布局。这主要是为了确保在我们使用它时不太像黑匣子,我强烈建议克隆他们的原始仓库并四处查看。一些核心数学/逻辑可能令人生畏,但是浏览此仓库后,您将对 PlaCo 的模型以及生成行走模式时发生的情况有更深入的理解。
4、检查和加载.urdf 模型
链接中有什么?
让我们花点时间简要检查一下 .urdf 文件。我发现这对于查看定义机器人模型所需的内容非常有帮助。与 MJCF XML 格式(即 MuJoCo 建模)相比,值得注意的是 URDF 模式更容易理解,因为文件中需要提供的物理相关规范似乎更少。
<link name="left_foot">
<inertial>
<origin xyz="0 0 0" rpy="0 0 0" />
<mass value="1e-9" />
<inertia ixx="0" ixy="0" ixz="0" iyy="0" iyz="0" izz="0" />
</inertial>
</link>
<link name="left_foot">
<inertial>
<origin xyz="0 0 0" rpy="0 0 0" />
<mass value="1e-9" />
<inertia ixx="0" ixy="0" ixz="0" iyy="0" iyz="0" izz="0" />
</inertial>
</link>
.urdf 文件中的所有链接都必须具有惯性属性,该属性描述组件的质量及其转动惯量。通过检查其他链接(如下所示),我们还可以更好地理解它们的关联属性:
- 网格: 这些描述与链接关联的3D模型
- 材质名称: 包括要应用于几何体的颜色或视觉纹理
<link name="foot_assembly">
...
<visual>
<origin xyz="0.016060000000000015236 0.22230000000000008087 0.10905000000000000804" rpy="-1.570796326794896558 -1.5415933572433602064e-29 0" />
<geometry>
<mesh filename="package:///foot_top.stl"/>
</geometry>
<material name="foot_top_material">
<color rgba="0.98039215686274505668 0.71372549019607844922 0.0039215686274509803377 1.0"/>
</material>
</visual>
<collision>
<origin xyz="0.016060000000000015236 0.22230000000000008087 0.10905000000000000804" rpy="-1.570796326794896558 -1.5415933572433602064e-29 0" />
<geometry>
<mesh filename="package:///foot_top.stl"/>
</geometry>
</collision>
<inertial>
<origin xyz="0.011071805405810810838 -0.024660828914054331445 0.019062631505101720886" rpy="0 0 0"/>
<mass value="0.075240000000000001323" />
<inertia ixx="1.8748234895611842787e-05" ixy="1.5883913417291485579e-06" ixz="-6.5235393392554439271e-09" iyy="6.7442711017631486138e-05" iyz="5.6739084804828818974e-08" izz="6.0609999941114232999e-05" />
</inertial>
</link>
<link name="foot_assembly">
...
<visual>
<origin xyz="0.016060000000000015236 0.22230000000000008087 0.10905000000000000804" rpy="-1.570796326794896558 -1.5415933572433602064e-29 0" />
<geometry>
<mesh filename="package:///foot_top.stl"/>
</geometry>
<material name="foot_top_material">
<color rgba="0.98039215686274505668 0.71372549019607844922 0.0039215686274509803377 1.0"/>
</material>
</visual>
<collision>
<origin xyz="0.016060000000000015236 0.22230000000000008087 0.10905000000000000804" rpy="-1.570796326794896558 -1.5415933572433602064e-29 0" />
<geometry>
<mesh filename="package:///foot_top.stl"/>
</geometry>
</collision>
<inertial>
<origin xyz="0.011071805405810810838 -0.024660828914054331445 0.019062631505101720886" rpy="0 0 0"/>
<mass value="0.075240000000000001323" />
<inertia ixx="1.8748234895611842787e-05" ixy="1.5883913417291485579e-06" ixz="-6.5235393392554439271e-09" iyy="6.7442711017631486138e-05" iyz="5.6739084804828818974e-08" izz="6.0609999941114232999e-05" />
</inertial>
</link>
关节中有什么?
.urdf 模型的另一个关键组成部分是关节。这些描述了不同链接之间的连接,以及物理属性,例如:
- 速度/位置属性
- 摩擦属性
<joint name="left_knee" type="revolute">
<origin xyz="1.3877787807814456755e-17 -0.078650000000000011569 9.9999999999961231012e-05" rpy="-2.6413756781799972404e-16 -2.7339812777890022088e-26 0" />
<parent link="knee_and_ankle_assembly" />
<child link="knee_and_ankle_assembly_2" />
<axis xyz="0 0 1"/>
<limit effort="1" velocity="20" lower="-1.570796326794896558" upper="1.570796326794896558"/>
<joint_properties friction="0.0"/>
</joint>
<joint name="left_knee" type="revolute">
<origin xyz="1.3877787807814456755e-17 -0.078650000000000011569 9.9999999999961231012e-05" rpy="-2.6413756781799972404e-16 -2.7339812777890022088e-26 0" />
<parent link="knee_and_ankle_assembly" />
<child link="knee_and_ankle_assembly_2" />
<axis xyz="0 0 1"/>
<limit effort="1" velocity="20" lower="-1.570796326794896558" upper="1.570796326794896558"/>
<joint_properties friction="0.0"/>
</joint>
5、创建行走引擎
在本节中,我们现在将开始学习如何使用 PlaCo 为 OpenDuckMini 生成步态周期。在我们开始之前,有一些小要求需要概述:
- 我们希望生成一个理想的步态模式,捕捉我们希望机器人如何移动。
- 我们希望从 IK/ID 计算中排除特定关节,因为它们应在整个参考运动中保持固定。例如,颈部/头部不是我们希望在参考运动中无意义移动的组件。
方便的是,PlaCo 有一个 HumanoidRobot 类,我们可以从 URDF 创建其实例。但是,这里有一个关键点需要记住,传递给 HumanoidRobot 的 urdf 必须包含 left_foot 和 right_foot 关节链接。这可以在 robot.urdf 文件中使用 Ctrl+F 轻松找到。
import placo
import numpy as np
from placo_utils.visualization import robot_viz
class OpenDuckMiniWalkEngine():
def __init__(self, urdf_path: str):
self.urdf_path = urdf_path
self.robot = placo.HumanoidRobot(self.urdf_path)
self.humanoid_params = self.load_default_humanoid_params()
self.viz = None
self.solver = None
self.tasks = None
self.trajectory = None
self.restricted_joints = [
"left_hip_pitch",
"right_hip_pitch",
"left_knee",
"right_knee",
"left_ankle",
"right_ankle",
]
import placo
import numpy as np
from placo_utils.visualization import robot_viz
class OpenDuckMiniWalkEngine():
def __init__(self, urdf_path: str):
self.urdf_path = urdf_path
self.robot = placo.HumanoidRobot(self.urdf_path)
self.humanoid_params = self.load_default_humanoid_params()
self.viz = None
self.solver = None
self.tasks = None
self.trajectory = None
self.restricted_joints = [
"left_hip_pitch",
"right_hip_pitch",
"left_knee",
"right_knee",
"left_ankle",
"right_ankle",
]
6、配置类人机器人参数
PlaCo 中类人机器人的 HumanoidParameters 允许我们指定底层 IK/ID 求解器在生成行走运动时使用的参数。具体来说,这些参数将概述 .urdf 中的关键几何形状以及步态生成的时序周期信息。
与其逐个字段地介绍,不如将它们视为一组共同控制步态的旋钮:机器人如何计时其步伐、如何保持姿势、脚的几何形状以及可以迈出多远的限制。
def load_default_humanoid_params(self):
params = placo.HumanoidParameters()
# --- 时序 ---
params.single_support_duration = 0.1 # 每个单支撑阶段的秒数
params.single_support_timesteps = 8 # 每步的规划分辨率
params.double_support_ratio = (
0.0 # 0 = 步骤之间没有暂停(更简单开始)
)
params.startend_double_support_ratio = 1.0 # 开始/结束时较长的双支撑
params.planned_timesteps = 48 # WPG 向前规划多远
# --- 姿势(来自您的 URDF)---
params.walk_com_height = 0.23 # 匹配测量的 com_world z (~0.23)
params.walk_foot_height = 0.03 # 摆动脚抬高 [m]
params.walk_trunk_pitch = 0.10 # 轻微前倾 [rad] (~6°)
params.walk_foot_rise_ratio = 0.2 # 摆动在峰值高度花费的比例
# --- 脚几何 ---
params.foot_length = 0.08 # 鸭脚的估计值 [m]
params.foot_width = 0.06
params.feet_spacing = 0.09 # 您 0.18 m 站立宽度的一半
params.zmp_margin = 0.01 # 将 ZMP 保持在脚多边形内
params.foot_zmp_target_x = 0.0
params.foot_zmp_target_y = 0.0
# --- 步骤限制(剪辑规划器请求)---
params.walk_max_dx_forward = 0.04 # 最大向前步伐 [m]
params.walk_max_dx_backward = 0.02
params.walk_max_dy = 0.03 # 最大横向步伐 [m]
params.walk_max_dtheta = 0.30 # 每步最大旋转 [rad]
return params
def load_default_humanoid_params(self):
params = placo.HumanoidParameters()
# --- 时序 ---
params.single_support_duration = 0.1 # 每个单支撑阶段的秒数
params.single_support_timesteps = 8 # 每步的规划分辨率
params.double_support_ratio = (
0.0 # 0 = 步骤之间没有暂停(更简单开始)
)
params.startend_double_support_ratio = 1.0 # 开始/结束时较长的双支撑
params.planned_timesteps = 48 # WPG 向前规划多远
# --- 姿势(来自您的 URDF)---
params.walk_com_height = 0.23 # 匹配测量的 com_world z (~0.23)
params.walk_foot_height = 0.03 # 摆动脚抬高 [m]
params.walk_trunk_pitch = 0.10 # 轻微前倾 [rad] (~6°)
params.walk_foot_rise_ratio = 0.2 # 摆动在峰值高度花费的比例
# --- 脚几何 ---
params.foot_length = 0.08 # 鸭脚的估计值 [m]
params.foot_width = 0.06
params.feet_spacing = 0.09 # 您 0.18 m 站立宽度的一半
params.zmp_margin = 0.01 # 将 ZMP 保持在脚多边形内
params.foot_zmp_target_x = 0.0
params.foot_zmp_target_y = 0.0
# --- 步骤限制(剪辑规划器请求)---
params.walk_max_dx_forward = 0.04 # 最大向前步伐 [m]
params.walk_max_dx_backward = 0.02
params.walk_max_dy = 0.03 # 最大横向步伐 [m]
params.walk_max_dtheta = 0.30 # 每步最大旋转 [rad]
return params
7、理解 PlaCo 中的任务、求解器和轨迹
我们现在已配置好机器人及其参数——是时候进行运动规划了!在深入研究一些关键代码片段之前,PlaCo 中有几个核心抽象值得理解:
- 任务: 这些定义了您希望实现的运动的约束。实际上,这些转化为进入底层 IK/ID 求解器的等式和不等式。
- 轨迹: 给定一组任务,轨迹是我们在规划轨迹后获得的内容。
- 求解器: 给定机器人及其参数,求解器将查看轨迹中的当前任务,并构建一个 QP 问题,其解决方案应用于机器人模型。
8、配置 WalkTasks 和求解器
创建类人机器人并配置其参数后,我们就可以生成行走周期了。
PlaCo 有几个关键的抽象层次,有助于生成行走模式:
FootstepsPlanner:给定类人机器人参数和所需的 dx、dy 和 dθ 参数,规划行走的 footsteps 序列。WalkPatternGenerator:给定机器人、其参数以及每个 footsteps 的支持,这将映射到轨迹。
我认为在代码中理解这的工作原理要容易得多,所以让我们在后续块中看看它:
# 在您的 OpenDuckMiniWalkEngine 中,让我们创建以下方法
def configure_walk_solver(self, step_dx: float, step_dy: float, step_dtheta: float, nsteps: int):
self.solver = placo.KinematicsSolver(self.robot)
self.solver.enable_velocity_limits(True)
self.solver.dt = 1e-3
self.tasks = placo.WalkTasks()
self.tasks.initialize_tasks(self.solver, self.robot)
# 第一个任务:保持我们的头部/颈部关节固定
upper_body_restricted_joints = self.solver.add_joints_task()
upper_body_restricted_joints.set_joints({
joint_name: 0.0 for joint_name in self.restricted_joints
})
upper_body_restricted_joints.configure("restricted_upper_body", "soft", 1.0)
# 接下来:设置我们的机器人处于初始站立姿势
self.tasks.reach_initial_pose(
np.eye(4),
self.humanoid_params.feet_spacing,
self.humanoid_params.walk_com_height,
self.humanoid_params.walk_trunk_pitch,
)
planner = placo.FootstepsPlannerRepetitive(self.humanoid_params)
# 最后一个任务:设置行走计划
planner.configure(
step_dx,
step_dy,
step_dtheta,
nsteps
)
T_world_left = placo.flatten_on_floor(self.robot.get_T_world_left())
T_world_right = placo.flatten_on_floor(self.robot.get_T_world_right())
footsteps = planner.plan(
placo.HumanoidRobot_Side.left,
T_world_left,
T_world_right,
)
# 支撑从 footsteps 获得
supports = placo.FootstepsPlanner.make_supports(
footsteps,
0.0,
True,
self.humanoid_params.has_double_support(),
True,
)
# 行走模式生成器用于规划轨迹
walk = placo.WalkPatternGenerator(self.robot, self.humanoid_params)
self.trajectory = walk.plan(supports, self.robot.com_world(), 0.0)
# 在您的 OpenDuckMiniWalkEngine 中,让我们创建以下方法
def configure_walk_solver(self, step_dx: float, step_dy: float, step_dtheta: float, nsteps: int):
self.solver = placo.KinematicsSolver(self.robot)
self.solver.enable_velocity_limits(True)
self.solver.dt = 1e-3
self.tasks = placo.WalkTasks()
self.tasks.initialize_tasks(self.solver, self.robot)
# 第一个任务:保持我们的头部/颈部关节固定
upper_body_restricted_joints = self.solver.add_joints_task()
upper_body_restricted_joints.set_joints({
joint_name: 0.0 for joint_name in self.restricted_joints
})
upper_body_restricted_joints.configure("restricted_upper_body", "soft", 1.0)
# 接下来:设置我们的机器人处于初始站立姿势
self.tasks.reach_initial_pose(
np.eye(4),
self.humanoid_params.feet_spacing,
self.humanoid_params.walk_com_height,
self.humanoid_params.walk_trunk_pitch,
)
planner = placo.FootstepsPlannerRepetitive(self.humanoid_params)
# 最后一个任务:设置行走计划
planner.configure(
step_dx,
step_dy,
step_dtheta,
nsteps
)
T_world_left = placo.flatten_on_floor(self.robot.get_T_world_left())
T_world_right = placo.flatten_on_floor(self.robot.get_T_world_right())
footsteps = planner.plan(
placo.HumanoidRobot_Side.left,
T_world_left,
T_world_right,
)
# 支撑从 footsteps 获得
supports = placo.FootstepsPlanner.make_supports(
footsteps,
0.0,
True,
self.humanoid_params.has_double_support(),
True,
)
# 行走模式生成器用于规划轨迹
walk = placo.WalkPatternGenerator(self.robot, self.humanoid_params)
self.trajectory = walk.plan(supports, self.robot.com_world(), 0.0)
这里有几个额外的注意事项:
- 完整的步态周期相当于
period = (double_support_duration * 2) + (single_support_duration * 2)。 dx、dy和dtheta定义每步位移:每个 footsteps 应相对于前一个支撑脚向前/向后(x)、横向(y)和旋转(theta)移动多远。FootstepsPlannerRepetitive.configure()使用这些值以及步数来布局行走的完整 footsteps 序列——它们实际上是步态的"操纵杆"控制(直走、平移、转弯),受人类参数中设置的walk_max_dx_forward、walk_max_dy和walk_max_dtheta限制。
9、执行轨迹
在上一节中,我们创建了一个函数,使我们能够为机器人配置行走轨迹。这留下了一些待完成的事情:
- 可视化: 验证轨迹并查看其外观是一种很好的方式,可以验证我们是否获得了所需的运动。
- 执行轨迹: 这包括在时间上逐步执行轨迹,在每个时间步更新求解器的任务,并求解生成的关节配置。
- 导出机器人位置和关节数据: 对于下一篇中的模仿学习/MuJoCo,我们希望在轨迹的每个时间步转储机器人的关节角度(和其他相关状态),以便稍后可以将其馈送到多项式拟合步骤中,并在训练期间用作参考信号。
我们可以通过在行走引擎类中引入以下方法来编写一个非常简单的轨迹可视化器:
def view_trajectory(self):
self.viz = robot_viz(self.robot)
t = 0.0
while t < self.trajectory.t_end:
self.tasks.update_tasks_from_trajectory(self.trajectory, t)
self.solver.solve(True)
self.robot.update_kinematics()
if not self.trajectory.support_is_both(t):
self.robot.update_support_side(str(self.trajectory.support_side(t)))
self.robot.ensure_on_floor()
self.viz.display(self.robot.state.q)
def view_trajectory(self):
self.viz = robot_viz(self.robot)
t = 0.0
while t < self.trajectory.t_end:
self.tasks.update_tasks_from_trajectory(self.trajectory, t)
self.solver.solve(True)
self.robot.update_kinematics()
if not self.trajectory.support_is_both(t):
self.robot.update_support_side(str(self.trajectory.support_side(t)))
self.robot.ensure_on_floor()
self.viz.display(self.robot.state.q)
这里有几个需要理解的地方:
- 我们的轨迹涉及机器人在不同时间点经历位置和速度的变化。因此,我们需要通过
self.tasks.update_tasks_from_trajectory更新任务以进行求解。 - 然后我们需要求解新的机器人状态——这是通过
self.solver.solve完成的,我们还传入 True 以使用新的计算结果更新机器人状态。 - 可视化工具(通过
self.viz)在底层使用 MeshCat,并使用基于循环内前几行更新的机器人状态。
10、多项式拟合(用于模仿学习)
我们现在已生成一些参考运动,代表我们希望机器人移动的理想方式。但是,我们需要做更多工作才能使此参考运动在 RL 模拟中可用。
使用我们的行走引擎,我们编写了一些功能,允许我们在所需轨迹的过程中转储机器人的关节角度和关键运动数据。这些相同的关节将存在于 MuJoCo 的 RL 世界中(并被执行),这意味着如果我们有某种机制来比较这些关节,那么就有可能获得奖励信号。
为此,我们可以对每个关节位置使用多项式拟合。本质上,我们要创建的管道可以简化为以下内容:
- 创建轨迹过程中所有关节数据的数据框
- 将其处理为 |q| 元素张量,对于每个维度,存储对应于该维度的给定关节的轨迹数据
- 对于每个维度,运行 N 次多项式的多项式拟合,并提取代表轨迹的系数
- 在下游稍后:然后我们可以使用这些多项式系数来比较在模拟中执行的 MuJoCo 轨迹(记住是在基于物理的环境中)
为了更好地在实践中查看此管道,以下是代码中的外观。首先,我们将从行走引擎转储的每一帧加载到单个数据框中,每个时间步一行:
from pathlib import Path
import json
from dataclasses import dataclass
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
@dataclass
class WalkPatternPolyFitParams:
degree: int
poly_coeffs: dict[str, np.ndarray]
joint_name_to_dim: dict[str, int]
recordings_directory = Path("./recordings/walk_engine")
joint_names = set()
time_based_rows = []
for path in sorted(recordings_directory.glob("dump_*.json")):
with open(path, "r") as f:
data = json.load(f)
joint_angles = {k: np.array(v) for k, v in data["joint_angles"].items()}
joint_names.update(joint_angles.keys())
time_based_rows.append({**joint_angles})
recordings_df = pd.DataFrame(time_based_rows)
from pathlib import Path
import json
from dataclasses import dataclass
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
@dataclass
class WalkPatternPolyFitParams:
degree: int
poly_coeffs: dict[str, np.ndarray]
joint_name_to_dim: dict[str, int]
recordings_directory = Path("./recordings/walk_engine")
joint_names = set()
time_based_rows = []
for path in sorted(recordings_directory.glob("dump_*.json")):
with open(path, "r") as f:
data = json.load(f)
joint_angles = {k: np.array(v) for k, v in data["joint_angles"].items()}
joint_names.update(joint_angles.keys())
time_based_rows.append({**joint_angles})
recordings_df = pd.DataFrame(time_based_rows)
每个 dump_*.json 文件都是从行走引擎逐帧导出的,因此 recordings_df 最终每个关节一列,轨迹中每个时间步一行。
接下来,我们将每个关节的轨迹堆叠到单个 |q| x T 矩阵中——每个关节一行,每个时间步一列——并将时间标准化到 [0, 1] 范围,这样拟合就不会依赖于轨迹运行了多长时间:
joint_name_to_dim = {jname: dim for dim, jname in enumerate(joint_names)}
vector_elements = np.stack([recordings_df[jname].to_numpy() for jname in joint_names])
X = np.linspace(0, 1, vector_elements.shape[1])
joint_name_to_dim = {jname: dim for dim, jname in enumerate(joint_names)}
vector_elements = np.stack([recordings_df[jname].to_numpy() for jname in joint_names])
X = np.linspace(0, 1, vector_elements.shape[1])
有了这个,我们可以独立地将 N 次多项式拟合到每个关节的轨迹,并将系数按升幂顺序存储(c0, c1, c2, ...),以便在下游更容易推理:
degree = 15
poly_coeffs = {}
for dim in range(vector_elements.shape[0]):
coeffs = np.polyfit(X, vector_elements[dim].astype(float), degree)
poly_coeffs[f"dim_{dim}"] = np.flip(coeffs)
params = WalkPatternPolyFitParams(
degree=degree,
poly_coeffs=poly_coeffs,
joint_name_to_dim=joint_name_to_dim,
)
degree = 15
poly_coeffs = {}
for dim in range(vector_elements.shape[0]):
coeffs = np.polyfit(X, vector_elements[dim].astype(float), degree)
poly_coeffs[f"dim_{dim}"] = np.flip(coeffs)
params = WalkPatternPolyFitParams(
degree=degree,
poly_coeffs=poly_coeffs,
joint_name_to_dim=joint_name_to_dim,
)
最后,作为健全性检查,我们可以将每个关节的拟合多项式绘制在同一时间轴上,并与我们期望的平滑步态周期的外观进行目视检查:
for dim_key, coeffs in poly_coeffs.items():
plt.plot(X, np.polyval(np.flip(coeffs), X), label=dim_key)
plt.legend()
plt.show()
for dim_key, coeffs in poly_coeffs.items():
plt.plot(X, np.polyval(np.flip(coeffs), X), label=dim_key)
plt.legend()
plt.show()
由于 poly_coeffs 是按升幂顺序存储的,因此在传递给 np.polyval 之前我们将其翻转回来,np.polyval 期望系数从最高幂到最低幂。
作为最后一步,一旦我们有了这些多项式系数,我们就可以将它们转储到 .pkl 文件中,该文件将作为稍后在 MuJoCo 中执行模仿学习的目标。
11、结束语
在本文中,我们介绍了为模仿学习生成参考运动的基本原理。我们首先查看了 .urdf 文件的结构,以及链接和关节如何共同描述机器人的运动学。然后,我们深入研究了 PlaCo 的内部工作原理,了解它如何使用任务和底层 IK/ID 求解器来产生参考运动。然后,我们将 OpenDuckMini 的 .urdf 模型带入 PlaCo,并使用它生成实际的行走模式,最后将该轨迹缩减为表示步态周期的紧凑多项式系数集。
有了这个参考运动,自然的下一步是让机器人物理实现它。这意味着将这些多项式系数带入物理模拟器(如 MuJoCo),并使用模仿学习来训练策略,以在真实物理约束下复制参考步态——这正是我们将在下一篇文章中介绍的内容。
原文链接: Understanding Reference Motion Generation for Imitation Learning with OpenDuckMini
汇智网翻译整理,转载请标明出处