给机器人编程加点‘肌肉记忆’:手把手教你用Python实现DMP动态运动基元(附收敛性分析)

在机器人运动规划领域,让机械臂像人类一样流畅地完成动作一直是个挑战。想象一下,当你第一次学习打篮球时,投篮动作可能僵硬不协调,但经过反复练习后,手臂会自然形成"肌肉记忆",无需刻意控制每个关节角度就能完成流畅的投篮。DMP(Dynamic Movement Primitives,动态运动基元)正是将这种生物运动特性数学化的杰出代表。

不同于传统轨迹规划需要精确计算每个时间点的位置,DMP通过模仿学习到的轨迹,结合弹簧-阻尼系统特性,使机器人能够自然地适应环境变化。本文将带你用Python从零实现DMP算法,通过可视化理解其收敛原理,并应用于机械臂轨迹生成。即使没有深厚的数学背景,你也能掌握这套让机器人运动更"智能"的核心技术。

1. DMP核心原理与生物启发

1.1 从人类运动到数学模型

观察婴儿学习抓取物体的过程,会发现几个有趣现象:

  • 适应性:无论物体初始位置如何变化,手臂总能调整轨迹到达目标
  • 鲁棒性:即使中途受到轻微干扰,动作仍能趋向目标
  • 时间缩放:快速或慢速执行时,运动轨迹形状保持相似

DMP正是将这些特性抽象为二阶微分方程系统:

class DMP:
    def __init__(self, n_bfs=100, alpha=48.0, beta=12.0):
        self.alpha = alpha  # 弹簧常数
        self.beta = beta    # 阻尼常数
        self.n_bfs = n_bfs  # 基函数数量

其中关键参数α和β分别对应系统的"刚度"和"阻尼",这类似于肌肉的弹性和粘滞特性。当α/β=4时,系统处于临界阻尼状态,能在快速收敛和避免振荡间取得平衡。

1.2 弹簧-阻尼系统的直观理解

想象用橡皮筋拴住一个小球:

  • 目标位置:橡皮筋另一端固定点
  • 当前位置:小球所在位置
  • 系统行为
    • 距离目标越远,橡皮筋拉力越大(弹簧项α(g-y))
    • 小球运动越快,阻尼力越大(阻尼项-βẏ)

这种物理类比使得DMP的数学表达更易理解:

τ²ÿ = α(β(g-y) - τẏ) + f(s)

其中τ是时间常数,f(s)是非线性强迫项,用于塑造特定轨迹形状。当强迫项f(s)随时间衰减为0后,系统将纯粹由弹簧-阻尼项驱动收敛到目标。

2. Python实现基础DMP框架

2.1 系统动力学实现

我们先构建最简化的DMP系统,忽略强迫项,专注于收敛特性:

import numpy as np
import matplotlib.pyplot as plt

class SimpleDMP:
    def __init__(self, alpha=48.0, beta=12.0, dt=0.01):
        self.alpha = alpha
        self.beta = beta
        self.dt = dt
    
    def simulate(self, y0, g, T):
        """模拟DMP系统运动
        Args:
            y0: 初始位置
            g: 目标位置
            T: 运动时长
        Returns:
            t: 时间序列
            y: 位置序列
            dy: 速度序列
        """
        steps = int(T / self.dt)
        y = np.zeros(steps)
        dy = np.zeros(steps)
        y[0] = y0
        
        for i in range(1, steps):
            ddy = self.alpha*(self.beta*(g - y[i-1]) - dy[i-1])
            dy[i] = dy[i-1] + ddy * self.dt
            y[i] = y[i-1] + dy[i] * self.dt
        
        t = np.arange(0, T, self.dt)
        return t, y, dy

2.2 可视化收敛过程

通过不同初始条件的模拟,我们可以直观观察系统行为:

dmp = SimpleDMP()
fig, ax = plt.subplots(2, 1, figsize=(10, 8))

# 测试不同初始位置
for y0 in [0, 2, -1, 3]:
    t, y, _ = dmp.simulate(y0=y0, g=5.0, T=2.0)
    ax[0].plot(t, y, label=f'y0={y0}')
ax[0].set_title('Position Convergence')
ax[0].legend()

# 测试不同初始速度
for dy0 in [-5, 0, 5, 10]:
    t, y, dy = dmp.simulate(y0=0, g=5.0, T=2.0)
    dy[0] = dy0  # 覆盖初始速度
    for i in range(1, len(t)):
        ddy = dmp.alpha*(dmp.beta*(5.0 - y[i-1]) - dy[i-1])
        dy[i] = dy[i-1] + ddy * dmp.dt
        y[i] = y[i-1] + dy[i] * dmp.dt
    ax[1].plot(t, y, label=f'dy0={dy0}')
ax[1].set_title('Velocity Impact')
plt.tight_layout()
plt.show()

运行这段代码会生成两张图表:

  1. 位置收敛:不同初始位置最终都收敛到目标g=5.0
  2. 速度影响:初始速度不同会导致轨迹超调或欠调,但最终仍会稳定

提示:实际应用中,α和β的选择会影响收敛速度和平滑性。通常保持β=α/4可获得临界阻尼特性。

3. 完整DMP系统实现与轨迹学习

基础DMP只能收敛到目标,要复现复杂轨迹需要引入非线性强迫项。这部分我们将实现完整DMP系统。

3.1 径向基函数与强迫项

强迫项f(s)由径向基函数(RBF)组合构成:

class CompleteDMP(SimpleDMP):
    def __init__(self, n_bfs=100, alpha=48.0, beta=12.0, dt=0.01):
        super().__init__(alpha, beta, dt)
        self.n_bfs = n_bfs
        self.centers = np.exp(-np.linspace(0, 1, n_bfs)*5)
        self.widths = np.ones(n_bfs) * (n_bfs**1.5) / self.centers
        self.weights = np.zeros(n_bfs)
        
    def psi(self, x):
        return np.exp(-self.widths*(x - self.centers)**2)
    
    def forcing_term(self, x):
        return np.dot(self.psi(x), self.weights) / (self.psi(x).sum() + 1e-10)

3.2 轨迹学习算法

从示范轨迹中学习权重参数的伪代码:

1. 记录示范轨迹y_demo和对应时间序列t
2. 计算轨迹的一阶(dy)和二阶导数(ddy)
3. 解算强迫项目标值:
   f_target = τ²ddy - α(β(g-y) - τdy)
4. 使用线性回归拟合RBF权重w

Python实现如下:

def learn_from_demo(self, y_demo, t_demo):
    # 计算导数
    dy_demo = np.gradient(y_demo, t_demo)
    ddy_demo = np.gradient(dy_demo, t_demo)
    
    # 计算目标强迫项
    g = y_demo[-1]
    tau = t_demo[-1]
    f_target = (tau**2 * ddy_demo - 
                self.alpha*(self.beta*(g - y_demo) - tau*dy_demo))
    
    # 构造回归矩阵
    s = np.exp(-self.alpha/4 * t_demo/tau)  # 相位变量
    Phi = np.array([self.psi(si) for si in s])
    Phi /= (Phi.sum(axis=1, keepdims=True) + 1e-10)
    
    # 加权线性回归
    self.weights = np.linalg.lstsq(Phi, f_target, rcond=None)[0]

3.3 轨迹生成与泛化

学习完成后,DMP可以生成新目标位置的轨迹:

def generate_trajectory(self, y0, g, T, tau_scale=1.0):
    tau = T * tau_scale
    steps = int(T / self.dt)
    y = np.zeros(steps)
    dy = np.zeros(steps)
    y[0] = y0
    
    s = 1.0  # 初始相位
    for i in range(1, steps):
        # 相位系统
        ds = (-self.alpha/4 * s) * self.dt / tau_scale
        s += ds
        
        # 强迫项
        f = self.forcing_term(s)
        
        # 变换系统
        ddy = (self.alpha*(self.beta*(g - y[i-1]) - tau*dy[i-1]) + f) / tau**2
        dy[i] = dy[i-1] + ddy * self.dt
        y[i] = y[i-1] + dy[i] * self.dt
    
    t = np.arange(0, T, self.dt)
    return t, y

4. 收敛性分析与参数调优

4.1 稳定性证明的工程视角

抛开复杂数学推导,DMP的稳定性可以从能量角度理解:

  1. 能量函数:E = ½α(g-y)² + ½ẏ²
  2. 能量变化率:dE/dt = -βẏ² ≤ 0
  3. 结论:系统能量单调递减,最终稳定在最小值点y=g, ẏ=0

这种Lyapunov稳定性分析比精确解更直观,也解释了参数选择原则:

参数 物理意义 影响 推荐值
α 弹簧刚度 收敛速度 25-50
β 阻尼系数 平滑性 α/4
τ 时间尺度 运动速度 轨迹时长

4.2 数值实验验证

通过改变参数观察系统响应:

params = [
    {'α': 25, 'β': 6.25, 'label': 'Under-damped'},
    {'α': 25, 'β': 12.5, 'label': 'Critical-damped'},
    {'α': 25, 'β': 25, 'label': 'Over-damped'}
]

plt.figure(figsize=(10, 6))
for p in params:
    dmp = SimpleDMP(alpha=p['α'], beta=p['β'])
    t, y, _ = dmp.simulate(y0=0, g=1.0, T=2.0)
    plt.plot(t, y, label=f"{p['label']} (α={p['α']}, β={p['β']})")
plt.title('DMP Response under Different Damping Conditions')
plt.legend()
plt.grid(True)
plt.show()

实验结果将展示:

  • 欠阻尼:振荡收敛
  • 临界阻尼:最快无振荡收敛
  • 过阻尼:缓慢无振荡收敛

4.3 实际应用中的调优技巧

在机器人实际部署时,还需考虑:

  1. 轨迹形状保持

    • 增加基函数数量(n_bfs)可提高轨迹复现精度
    • 但过多会导致过拟合,通常50-200足够
  2. 时间缩放

    # 快速执行(2倍速)
    t_fast, y_fast = dmp.generate_trajectory(y0, g, T/2, tau_scale=0.5)
    
    # 慢速执行(0.5倍速) 
    t_slow, y_slow = dmp.generate_trajectory(y0, g, T*2, tau_scale=2)
    
  3. 目标位置变化

    # 中途改变目标
    t, y = np.zeros(steps), np.zeros(steps)
    for i in range(1, steps):
        if i == steps//2:  # 中途时刻
            g_new = g + 1.0  # 新目标
        ddy = alpha*(beta*(g_new - y[i-1]) - dy[i-1])
        # ... 继续积分
    

注意:虽然DMP对目标变化具有鲁棒性,但过大突变仍可能导致不连续加速度。实际应用中可对目标位置变化率进行滤波。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐