Skip to content

机器人学习

机器人学习架起了算法与物理行动之间的桥梁。本章涵盖运动学、动力学、经典控制、模仿学习、虚拟到现实的迁移、操作、运动以及安全性——这些技术赋予机器人移动、抓取、行走以及与现实世界交互的能力。

  • 在前面的章节中,我们学习了如何感知世界(第8章,第11章文件1)以及如何从数据中学习(第6章)。但感知和学习还不够。机器人必须行动:移动手臂去抓杯子、在不平的地形上行走,或者在仓库中导航。这就是机器人学习的用武之地。

  • 核心挑战在于,物理世界是连续的、高维的、充满接触且毫不宽容。图像识别中的分类错误只是一个不正确的标签,而机器人中的控制错误则可能导致机器人损坏或物体掉落。后果截然不同。

机器人运动学

  • 运动学 描述的是不考虑力的运动几何关系。机器人手臂是由关节连接的一系列刚性连杆。每个关节有一个自由度:要么旋转(转动关节),要么滑动(移动关节)。

  • 机器人的位形是所有关节角度(或位移)的集合 \(\mathbf{q} = [q_1, q_2, \ldots, q_n]^T\)。这个向量位于关节空间(或位形空间)中,这是一个 \(n\) 维空间,每个轴对应一个关节。一个6自由度的机器人手臂具有6维的位形空间。

2连杆机器人手臂:通过正向运动学,关节角度 q1 和 q2 决定末端执行器的位置

  • 正向运动学 根据关节角度计算末端执行器(“手”)的位置和姿态。这是一个函数 \(\mathbf{x} = f(\mathbf{q})\),将关节空间映射到任务空间(末端执行器的3D位置和姿态,也称为笛卡尔空间)。

  • 每个关节由一个 \(4 \times 4\) 齐次变换矩阵描述(回顾第2章的仿射变换)。Denavit-Hartenberg (DH) 约定 使用四个参数表示每个关节:连杆长度 \(a\),连杆扭角 \(\alpha\),连杆偏距 \(d\),关节角度 \(\theta\)。关节 \(i\) 的变换为:

\[T_i = \begin{bmatrix} \cos\theta_i & -\sin\theta_i \cos\alpha_i & \sin\theta_i \sin\alpha_i & a_i \cos\theta_i \\ \sin\theta_i & \cos\theta_i \cos\alpha_i & -\cos\theta_i \sin\alpha_i & a_i \sin\theta_i \\ 0 & \sin\alpha_i & \cos\alpha_i & d_i \\ 0 & 0 & 0 & 1 \end{bmatrix}\]
  • 完整的正向运动学是所有关节变换的乘积:\(T_{0 \to n} = T_1 T_2 \cdots T_n\)。这是链式变换的矩阵乘法(第2章):每个关节的变换依次作用,将坐标系从基座旋转平移到末端执行器。

  • 逆向运动学 是相反的问题:给定期望的末端执行器位姿 \(\mathbf{x}^*\),找到关节角度 \(\mathbf{q}\) 使得 \(f(\mathbf{q}) = \mathbf{x}^*\)。这个问题要难得多,因为:

    • 映射是非线性的(涉及正弦和余弦)。
    • 可能存在多解(不同的手臂构型到达同一个点)。
    • 可能无解(目标超出可达范围)。
  • 解析解只存在于特定的机器人几何形状。对于一般机器人,逆向运动学使用雅可比矩阵迭代求解。雅可比矩阵 \(J(\mathbf{q})\) 将关节角度的微小变化与末端执行器位置的微小变化联系起来(回顾第3章的雅可比矩阵):

\[\dot{\mathbf{x}} = J(\mathbf{q}) \dot{\mathbf{q}}\]
  • 要将末端执行器移动一个小量 \(\Delta \mathbf{x}\),我们需要 \(\Delta \mathbf{q} = J^{-1} \Delta \mathbf{x}\)(当 \(J\) 非方阵时使用伪逆 \(J^+ \Delta \mathbf{x}\))。这个过程迭代进行直到末端执行器到达目标,本质上就是牛顿法(第3章)应用于运动学方程。

  • 奇异点附近,雅可比矩阵秩缺失(某些列变得线性相关,正如我们在第2章中所学)。物理上,这意味着机器人失去一个自由度:无论关节运动多快,末端执行器都无法沿某些方向移动。伪逆在奇异点附近会爆炸,因此改用阻尼最小二乘法(增加正则化项 \(\lambda^2 I\)):

\[\Delta \mathbf{q} = J^T(JJ^T + \lambda^2 I)^{-1} \Delta \mathbf{x}\]

动力学与控制

  • 动力学 在图像中加入了力。机器人手臂的运动方程遵循机械臂方程
\[M(\mathbf{q})\ddot{\mathbf{q}} + C(\mathbf{q}, \dot{\mathbf{q}})\dot{\mathbf{q}} + \mathbf{g}(\mathbf{q}) = \boldsymbol{\tau}\]
  • 其中 \(M(\mathbf{q})\) 是质量(惯性)矩阵,\(C(\mathbf{q}, \dot{\mathbf{q}})\) 包含科里奥利力和离心力效应,\(\mathbf{g}(\mathbf{q})\) 是重力向量,\(\boldsymbol{\tau}\) 是关节力矩向量(控制输入)。这是一个二阶微分方程组,每个关节一个方程。

  • 质量矩阵 \(M\) 总是对称正定的(回顾第2章,正定矩阵保证了唯一的最小值,这里它确保系统对施加的力矩有可预测的响应)。

  • PID控制 是机器人领域应用最广泛的控制器。对于每个关节,它根据误差 \(e(t) = q_{\text{desired}}(t) - q_{\text{actual}}(t)\) 计算力矩:

\[\tau(t) = K_p e(t) + K_i \int_0^t e(s) \, ds + K_d \dot{e}(t)\]
  • 三项具有直观的作用:
    • 比例项\(K_p\)):与当前误差成比例地修正。误差越大 → 修正越大。就像一个弹簧将关节拉向目标。
    • 积分项\(K_i\)):累积过去的误差以消除稳态偏差。如果关节持续达不到目标,积分项会累积并提供额外的推力。
    • 微分项\(K_d\)):对误差的变化率做出反应,提供阻尼。它随着误差减小而减慢响应,防止超调和振荡。

PID控制器调参:高 Kp 振荡,高 Kd 迟钝,调好的 PID 能快速到达目标

  • 调参 \(K_p, K_i, K_d\) 是一种平衡:\(K_p\) 太大会引起振荡,\(K_d\) 太大会使系统迟钝,\(K_i\) 太大会导致积分饱和(持续误差下积分项无界增长)。

  • 模型预测控制 会向前看。在每个时间步,它求解一个优化问题:在有限时域内,在动力学模型和约束条件下,找到使成本函数(例如跟踪误差 + 控制努力)最小的未来控制序列。只应用第一个控制,然后在下一个时间步重复该过程。

\[\min_{\mathbf{u}_{0:T}} \sum_{t=0}^{T} \left[ \|\mathbf{x}_t - \mathbf{x}_t^*\|_Q^2 + \|\mathbf{u}_t\|_R^2 \right] \quad \text{subject to} \quad \mathbf{x}_{t+1} = f(\mathbf{x}_t, \mathbf{u}_t)\]
  • 这里 \(\|\mathbf{x}\|_Q^2 = \mathbf{x}^T Q \mathbf{x}\) 是使用正定矩阵 \(Q\) 的加权范数(第2章),可以让你对不同状态的误差施加不同的惩罚。模型预测控制能自然地处理约束(关节限位、力矩限制、避障),因为这些约束已明确包含在优化中。

  • 阻抗控制 调节力与运动之间的关系,而不是跟踪刚性的轨迹。它不命令“移动到位置 \(x\)”,而是命令“表现得像一个以 \(x\) 为中心的弹簧-阻尼系统”:

\[F = K_s(\mathbf{x}^* - \mathbf{x}) + D(\dot{\mathbf{x}}^* - \dot{\mathbf{x}})\]
  • 其中 \(K_s\) 是刚度矩阵,\(D\) 是阻尼矩阵。这使得机器人具有柔顺性:如果接触到障碍物,它会屈服而不是强行穿过。阻抗控制对于接触丰富的任务(如将销钉插入孔中或向人类传递物体)至关重要。

模仿学习

  • 我们不是手动设计控制器,而是可以从演示中学习控制策略。人类执行任务,机器人观察,学习算法提取策略。这就是模仿学习(或通过演示学习)。

  • 行为克隆 是最简单的方法:将演示视为监督学习数据集。给定来自专家的观测-动作对 \(\{(\mathbf{o}_t, \mathbf{a}_t)\}\),训练一个策略 \(\pi_\theta(\mathbf{a} \mid \mathbf{o})\) 从观测中预测专家的动作。这就是标准的监督学习(第6章):最小化损失:

\[\mathcal{L}(\theta) = \mathbb{E}_{(\mathbf{o}, \mathbf{a}) \sim \mathcal{D}} \left[ \| \pi_\theta(\mathbf{o}) - \mathbf{a} \|^2 \right]\]

行为克隆中的分布偏移:小误差累积,导致学习到的策略漂移远离专家轨迹

  • 问题在于分布偏移(也称为复合误差问题)。在训练期间,策略看到的是专家的状态。在部署期间,策略自身的小误差把它推到了专家从未访问过的状态。这些不熟悉的状态导致更差的动作,进而导致更不熟悉的状态,误差迅速复合。

  • 想象一下通过观看一个完美驾驶员来学习开车。你从未见过轻微打滑后会发生什么,因为专家从未打滑过。当你第一次稍微偏离车道时,你完全不知道如何纠正。

  • DAgger(数据集聚合)通过迭代解决了这个问题:

    1. 在现有数据上训练策略。
    2. 在环境中运行策略,收集新状态。
    3. 请专家为这些新状态标注正确的动作。
    4. 将新数据添加到数据集中并重新训练。
  • 经过多次迭代,数据集覆盖了学习策略实际访问的状态,而不仅仅是专家的轨迹。策略因为看到并学会了从自身错误中恢复而得到改进。

  • 基于Transformer的动作块 是一种现代方法,策略预测的是未来的一个动作序列(一个“块”),而不是一次一个动作。这通过带有Transformer主干的条件VAE来实现。预测动作块更鲁棒,因为它捕捉了时间相关性:到达运动的平滑性编码在块中,而不是依赖于可能漂移的自回归单步预测。

  • 扩散策略 将扩散模型(第8章)应用于动作生成。它不预测单个动作,而是模拟在观测条件下所有可能动作的完整分布。从噪声开始,迭代去噪以产生动作序列。这自然地处理了多模态问题:当存在多种有效方式完成任务时(从左或从右够到),扩散模型可以表示两种模式,而回归策略会对它们取平均(最终到达中间的某个位置,可能两种方式都不是有效的)。

虚拟到现实的迁移

  • 在现实世界中训练机器人昂贵、缓慢且危险。一个通过试错学习抓取的机器人可能需要数千次尝试,在此过程中会损坏物体和自身。仿真 提供了无限、安全、快速的体验。但仿真器并不完美:物理被近似,视觉效果是合成的,接触被简化。

  • 虚拟到现实的差距 是仿真性能与现实性能之间的差异。一个在仿真中完美工作的策略可能在真实机器人上完全失败,因为它对仿真器特定的细节过拟合了。

通过域随机化进行虚拟到现实迁移:在许多随机化的仿真中训练,使得真实世界只是另一个变体

  • 域随机化 通过在广泛范围的仿真器设置下训练来对抗这一问题。不使用单一仿真,而是使用成千上万个随机化设置:

    • 物理:摩擦系数、质量、阻尼
    • 视觉:光照、纹理、颜色、相机位置
    • 动力学:电机延迟、噪声水平
  • 其思想是:如果策略在所有变体下都能工作,那么真实世界就只是分布内的“另一个变体”。策略学习到对随机化属性不变的特征,这些不变的特征能够迁移。

  • 系统辨识 采取了相反的方法:不是随机化一切,而是仔细测量真实系统的物理参数并调整仿真器以匹配。这能产生更准确的仿真,但也很脆弱(任何未建模的效应都会造成差距)。

  • 在实践中,最好的结果是两者结合:使用系统辨识使仿真器相当接近,然后使用域随机化来覆盖剩余的不确定性。

  • 通过微调进行虚拟到现实迁移 主要在仿真中训练,然后进行少量的现实世界微调。仿真提供了一个良好的初始化,现实世界数据纠正了仿真器特定的偏差。这需要的现实世界数据远少于从头训练。

用于机器人的世界模型

  • 上述所有的强化学习和模仿学习方法都是无模型的:策略通过直接交互(或演示)来学习行动,而不显式地建模世界的运作方式。另一种选择是基于模型的学习:首先学习环境动力学的模型,然后使用该模型进行规划或生成合成经验。

  • 世界模型 学习转移函数 \(p(s_{t+1} \mid s_t, a_t)\):给定当前状态和动作,预测下一个状态(如第10章所述)。在机器人领域,这意味着预测如果机器人采取特定动作会发生什么:“如果我把这个方块向左推,它会滑动3厘米,它后面的杯子会倒下。”

  • 其吸引力在于样本效率。现实世界中的机器人交互代价高昂。如果机器人能够从适量的真实数据中学习一个世界模型,那么它就可以通过在头脑中展开模型来“想象”数千条轨迹,在不动物理世界的情况下规划和完善其策略。这类似于棋手通过在脑海中模拟走法来提前思考。

  • DreamerV3 是一个通用的基于模型的强化学习智能体。它联合学习三个组件:

    • 表示模型:将观测编码为紧凑的潜在状态。
    • 转移模型(世界模型):根据当前状态和动作预测下一个潜在状态。
    • 奖励模型:根据潜在状态预测奖励。
  • 然后,智能体通过在潜在空间中展开转移模型许多步来“做梦”,在这些想象的轨迹上训练策略,并将策略迁移到真实环境中。关键的创新在于,所有想象都发生在潜在空间(紧凑的学习表示)中,而不是像素空间中,这使得计算上可行。

\[\hat{s}_{t+1} = f_\theta(s_t, a_t), \quad \hat{r}_t = g_\theta(s_t)\]
  • 转移模型 \(f_\theta\) 和奖励模型 \(g_\theta\) 在真实经验上训练,而策略则在想象的展开上训练。这将数据收集与策略优化解耦了。

  • 对于机器人操作,世界模型可以实现心理演练。在尝试抓取之前,机器人可以在其学习到的模型中模拟几种方法,并选择最可能成功的一种。这对于接触丰富的任务尤其有价值,因为现实世界中的试错既慢又有风险。

  • 世界模型也与虚拟到现实迁移自然连接:在真实数据上训练的世界模型实际上是一个学习到的仿真器,它自动捕捉了真实世界的物理,从而完全绕过了虚拟到现实的差距。对于已知场景,该模型可能不如手工构建的仿真器准确,但它能捕捉到手工仿真器常常出错的效应(摩擦、变形、接触动力学)。

  • JEPA(联合嵌入预测架构,在第10章介绍)提供了像素级预测的替代方案。JEPA 不预测确切的未来观测,而是在嵌入空间中进行预测:“下一个状态的潜在表示将接近这个向量。”这避免了完美预测像素的困难(既没必要又计算浪费),并专注于预测对决策重要的未来方面。

  • 世界模型的局限性在于复合预测误差。转移模型中的微小不准确会在长序列展开中累积,导致想象的轨迹偏离现实。缓解方法包括短想象范围、集成模型(使用不确定性来检测预测何时变得不可靠)以及定期用新的真实数据对模型进行修正。

操作

  • 操作 是使用机器人的末端执行器与物体交互的艺术:拾取、放置、推、插入、组装。

  • 抓取 是基础的操作技能。目标是找到一个稳定的抓取姿态:一个能够稳固夹持物体的夹爪位置和方向。

  • 解析抓取规划 使用物理学。如果接触力能够抵抗外部力旋量(力和力矩),则抓取是稳定的。对于平行夹爪,最简单的准则是力封闭条件:接触法线必须张成力的所有方向,使得抓取能够抵抗任何扰动。这涉及到检查抓取力旋量矩阵的秩,是第2章秩概念的直接应用。

  • 数据驱动的抓取 学习从感官输入预测抓取成功与否。给定桌子上物体的深度图像,一个网络为每个候选夹爪姿态预测抓取质量分数。GraspNet 及类似架构使用点云编码器(PointNet风格,第8章)来预测带有置信度分数的6自由度抓取姿态(位置+方向)。

  • 灵巧操作 超越了简单的拾取和放置。多指手具有20+自由度,可以执行诸如掌中转动物体(用手指旋转笔)、使用工具和精细装配等任务。状态空间巨大,接触复杂,这使得它成为机器人领域中最困难的问题之一。

  • 学习灵巧操作通常使用强化学习(第6章)在仿真中进行,并伴有大量的域随机化。OpenAI 用 Shadow 手解魔方的工作,在随机化物理的仿真中训练了 PPO 策略,并成功迁移到了真实机器人手上。

  • 接触丰富的任务 如销钉插入或擦拭表面,要求机器人与环境保持受控的接触。这些任务需要力感知和柔顺控制(阻抗控制),而且很难准确仿真,因为接触物理是出了名的难以建模。

运动

  • 运动是机器人在世界中移动其身体的过程:行走、奔跑、攀爬、游泳。与操作的关键区别在于,机器人在移动时必须保持平衡,并且与地面的接触点会随时间变化。

  • 足式运动 具有挑战性,因为它本质上是不稳定的。一个双足机器人在迈步时单腿站立就像一个倒立摆。质心必须保持在支撑多边形(与地面接触的脚所构成的凸包)上方,否则机器人会摔倒。

  • 零力矩点 是地面上重力与惯性力产生的净力矩为零的点。如果零力矩点保持在支撑多边形内,机器人就不会翻倒。传统的仿人控制器(如本田的ASIMO)规划轨迹以保持零力矩点处于边界内。

  • 中枢模式发生器 是受生物学启发的基于振荡器的控制器。动物使用脊髓中的神经回路产生有节奏的运动模式(行走、小跑、疾驰),无需大脑持续参与。中枢模式发生器模型使用耦合微分方程:

\[\dot{\phi}_i = \omega_i + \sum_j w_{ij} \sin(\phi_j - \phi_i - \psi_{ij})\]
  • 其中 \(\phi_i\) 是振荡器 \(i\) 的相位,\(\omega_i\) 是固有频率,\(w_{ij}\) 是耦合强度,\(\psi_{ij}\) 是期望的相位偏移。不同的相位关系会产生不同的步态:所有腿同步(奔腾),交替对(小跑),顺序移动(行走)。正弦耦合自然地同步了振荡器,类似于傅里叶级数(第3章)将运动分解为频率分量。

  • 用于运动的强化学习 已成为敏捷四足和双足机器人的主流方法。机器人通过仿真中的试错(第6章)学习一个策略 \(\pi(\mathbf{a} \mid \mathbf{o})\),奖励前进速度、稳定性和能效,惩罚摔倒、关节限位违规和抖动运动。

  • 最近工作的关键见解是,通过强化学习训练的运动策略远比手工设计的控制器鲁棒。它们自然地学会从推搡中恢复,适应地形变化,并处理好工程师未曾预料到的情况。训练通常使用带有域随机化的 PPO(第6章)。

  • 四足机器人 已成为足式机器人的主力。四条腿提供了固有的稳定性(三条腿总能支撑身体,而一条腿在移动)。四足机器人的强化学习策略取得了令人印象深刻的效果:以超过3米/秒的速度奔跑、爬楼梯、在岩石地形上导航以及从踢踹中恢复。

  • 双足运动 更困难,因为双足机器人的支撑多边形更小,质心更高。最近的进展(Tesla Optimus, Figure, Unitree H1)使用在仿真中训练并辅以精心设计的奖励塑形的强化学习。仿人机器人不仅要学习行走,还要学习协调手臂摆动以保持平衡,在不平路面上导航,以及从扰动中恢复。

机器人学习中的安全性

  • 一个为了学习而随机探索的机器人(如在强化学习中)可能会损坏自身、环境或附近的人。安全的机器人学习 约束探索以避免灾难性后果。

  • 约束强化学习 在马尔可夫决策过程中增加了安全约束(第6章)。目标变为:在 \(J_c(\pi) \leq d\) 的条件下最大化奖励,其中 \(J_c\) 是期望的累积成本(例如碰撞事件),\(d\) 是允许的最大成本。像约束策略优化这样的算法将 PPO 扩展到处理这些约束。

  • 安全包络 定义了机器人绝不能逾越的硬边界,无论学习到的策略说什么。一个安全控制器监控机器人的状态,并在即将违反约束时(例如接近关节限位、靠近人类时移动过快、或超过力阈值)接管学习到的策略。这是一种分层架构:学习算法负责性能,安全层负责约束。

  • 风险感知规划 明确地对环境和机器人自身状态估计中的不确定性进行建模。它不是为最可能的结果做规划,而是为置信边界内的最坏情况做规划。这与条件数概念(第2章)相关:良态系统对扰动具有鲁棒性,而风险感知规划寻求在扰动下保持安全的控制策略。

编程任务(使用CoLab或notebook)

  1. 为一个简单的2连杆平面机器人手臂实现正向运动学。计算并可视化不同关节角度下的末端执行器位置。

    import jax.numpy as jnp
    import matplotlib.pyplot as plt
    
    def forward_kinematics(q1, q2, l1=1.0, l2=0.8):
        """计算2连杆手臂的关节和末端执行器位置。"""
        x1 = l1 * jnp.cos(q1)
        y1 = l1 * jnp.sin(q1)
        x2 = x1 + l2 * jnp.cos(q1 + q2)
        y2 = y1 + l2 * jnp.sin(q1 + q2)
        return jnp.array([0, x1, x2]), jnp.array([0, y1, y2])
    
    fig, ax = plt.subplots(figsize=(6, 6))
    configs = [(0.5, 0.3), (1.0, -0.5), (1.5, 1.0), (2.0, -1.5)]
    colors = ["#e74c3c", "#3498db", "#27ae60", "#9b59b6"]
    
    for (q1, q2), c in zip(configs, colors):
        xs, ys = forward_kinematics(q1, q2)
        ax.plot(xs, ys, "o-", color=c, linewidth=2, markersize=6,
                label=f"q=({q1:.1f}, {q2:.1f})")
    
    ax.set_xlim(-2, 2); ax.set_ylim(-2, 2)
    ax.set_aspect("equal"); ax.grid(True); ax.legend()
    ax.set_title("2连杆机器人手臂:正向运动学")
    plt.show()
    

  2. 使用雅可比伪逆实现逆向运动学。从随机位形开始,迭代地将末端执行器移动到目标位置。

    import jax
    import jax.numpy as jnp
    import matplotlib.pyplot as plt
    
    l1, l2 = 1.0, 0.8
    
    def end_effector(q):
        x = l1 * jnp.cos(q[0]) + l2 * jnp.cos(q[0] + q[1])
        y = l1 * jnp.sin(q[0]) + l2 * jnp.sin(q[0] + q[1])
        return jnp.array([x, y])
    
    jacobian_fn = jax.jacobian(end_effector)
    
    target = jnp.array([0.5, 1.2])
    q = jnp.array([0.1, 0.1])
    trajectory = [end_effector(q)]
    
    for _ in range(50):
        pos = end_effector(q)
        error = target - pos
        if jnp.linalg.norm(error) < 1e-4:
            break
        J = jacobian_fn(q)
        # 阻尼伪逆以处理近奇异点
        dq = J.T @ jnp.linalg.solve(J @ J.T + 0.01 * jnp.eye(2), error)
        q = q + dq
        trajectory.append(end_effector(q))
    
    traj = jnp.stack(trajectory)
    plt.plot(traj[:, 0], traj[:, 1], "b.-", label="末端执行器路径")
    plt.plot(*target, "r*", markersize=15, label="目标")
    plt.gca().set_aspect("equal"); plt.grid(True); plt.legend()
    plt.title(f"IK在 {len(trajectory)-1} 步后收敛")
    plt.show()
    

  3. 模拟一个简单的PID控制器跟踪期望的关节轨迹。观察调参的效果。

    import jax.numpy as jnp
    import matplotlib.pyplot as plt
    
    # 期望轨迹:平滑正弦运动
    dt = 0.01
    t = jnp.arange(0, 5, dt)
    q_desired = jnp.sin(2 * t)
    
    # 模拟二阶动力学:m * q_ddot + b * q_dot = tau
    m, b_damp = 1.0, 0.5
    
    for Kp, Kd, Ki, label in [(10, 5, 0, "仅PD"), (10, 5, 2, "PID"), (50, 10, 2, "激进PID")]:
        q, q_dot, integral = 0.0, 0.0, 0.0
        qs = []
        for i in range(len(t)):
            error = q_desired[i] - q
            integral += error * dt
            d_error = -q_dot  # 误差的导数(期望速度此处简化)
            tau = Kp * error + Kd * d_error + Ki * integral
            q_ddot = (tau - b_damp * q_dot) / m
            q_dot += q_ddot * dt
            q += q_dot * dt
            qs.append(float(q))
    
        plt.plot(t, qs, label=label)
    
    plt.plot(t, q_desired, "k--", label="期望", linewidth=2)
    plt.xlabel("时间 (秒)"); plt.ylabel("关节角度")
    plt.legend(); plt.title("PID控制器跟踪")
    plt.show()