跳转至

150_机器人系统辨识

博士前数学路线图 · 第三批 · 专题 3.15:机器人系统辨识 (System Identification for Robot Dynamics)

档位:核心档位 3(博士入学)+ 进阶档位 4(博士毕业+) 建议时长:档位 3 约 18–22 h;档位 4 额外 10–15 h 前置:控制理论/50_LQR_LQG与Riccati方程;刚体动力学/20_Lagrange与Hamilton力学;纯数学基础/20_向量空间与线性变换 (线性代数/SVD) 下游:控制理论/120_MPC数值求解与实时实现;控制理论/90_DDP_iLQR原理与实现;刚体动力学/50_动力学解析微分;控制理论·自适应控制(规划中)(辨识结果直接驱动自适应律)


0. 前置自测

在开始本章之前,请确认你能回答以下问题。如果某题卡住,先回到对应专题补课。

编号 自测题 对应前置
Q0.1 写出刚体机器人关节空间运动方程 \(M(q)\ddot{q} + C(q,\dot{q})\dot{q} + g(q) = \tau\) 中每一项的物理含义和矩阵维度 刚体动力学/20_Lagrange与Hamilton力学
Q0.2 给定超定方程组 \(Ax = b\),写出 OLS 解的闭式表达式,说明何时解唯一 纯数学基础/20_向量空间与线性变换
Q0.3 什么是 SVD?对 \(A \in \mathbb{R}^{m \times n}\) 写出 SVD 分解形式,说明奇异值的几何意义 纯数学基础/20_向量空间与线性变换
Q0.4 LQR 闭环需要哪些模型参数?这些参数从哪里来? 控制理论/50_LQR_LQG与Riccati方程
Q0.5 用 Lagrange 方程推导单摆的运动方程 刚体动力学/20_Lagrange与Hamilton力学

如果你对 Q0.1--Q0.3 都很熟悉,可以直接进入正文。


1. 本章目标

学完本章后,你将能够

  1. 对任意串联机器人,将运动方程写成**线性回归形式** \(Y(q,\dot{q},\ddot{q})\pi = \tau\)
  2. 识别**基参数**(base parameters),知道哪些惯性参数可辨识、哪些不可辨识
  3. 设计**激励轨迹**,使辨识精度最大化(Fourier 参数化 + D-最优准则)
  4. 用 OLS/WLS 从实验数据中提取动力学参数,并给出参数的**置信区间**
  5. 理解**子空间辨识**方法,在不知道模型结构时辨识状态空间模型
  6. 实现**在线递归辨识**(RLS),对运行中的机器人实时更新参数
  7. 区分白箱/灰箱/黑箱方法,设计 sim-to-real 辨识流水线
  8. 用 Pinocchio/Drake 完成完整的辨识实验
  9. 理解**频域辨识**方法(Bode 图辨识、多正弦激励),并与时域方法对比
  10. 将辨识结果无缝对接到**自适应控制**(控制理论·自适应控制(规划中))中

2. 为什么需要系统辨识 ⭐

2.1 动机:CAD 参数不靠谱

你刚组装好一台 7-DOF 机械臂,CAD 模型告诉你每个连杆的质量、质心位置和惯性张量。你兴冲冲地把这些参数填入 MPC 控制器,结果——

  • 末端执行器抖动 ±5 cm,无法稳定抓取
  • 抓起 1 kg 负载后轨迹跟踪误差暴涨 300%
  • 低速运动时关节"粘滞",高速时超调

这不是控制器的问题,而是模型的问题。工业实践表明:

参数来源 质量误差 质心误差 惯性矩误差 摩擦参数
CAD 标称值 5–15% 10–30% 20–50% 完全未知
系统辨识后 < 2% < 5% < 10% 已知

为什么 CAD 参数不准? 原因很多:(1) CAD 模型忽略线缆、润滑脂、紧固件等附加质量;(2) 加工公差导致实际几何偏离设计值;(3) 减速器内部惯量通常不在 URDF 中;(4) 摩擦模型(Coulomb + viscous + Stribeck)在 CAD 中完全不存在。

2.2 反面案例:sim-to-real gap

假设你在 MuJoCo 仿真器中用 CAD 参数训练了一个 RL 策略,直接部署到真实机器人上。会发生什么?

  1. 仿真中:策略学会了利用精确的惯性矩来做"甩臂"运动,节省力矩
  2. 真实中:惯性矩偏差 30%,力矩不够 → 末端到不了目标位置 → 碰撞
  3. 更糟的情形:摩擦力在仿真中不存在 → 策略学会了"瞬间反向"以节能 → 真实中被 Coulomb 摩擦卡死

这就是 sim-to-real gap。系统辨识的目标之一,就是**让仿真模型尽可能逼近真实**,从根源上缩小这个 gap。

2.3 历史:线性参数化的发现

系统辨识是一个古老而庞大的领域(Ljung 的经典教材 1987 年出版),但**机器人动力学参数辨识**的现代理论始于 1986 年的一篇关键论文:

Atkeson, An, and Hollerbach, "Estimation of Inertial Parameters of Manipulator Loads and Links," International Journal of Robotics Research, 5(3):101--119, 1986.

这篇论文的核心发现:虽然刚体动力学方程 \(M(q)\ddot{q} + C(q,\dot{q})\dot{q} + g(q) = \tau\)\(q\) 高度非线性,但对惯性参数 \(\pi\)(质量、质心位置、惯性矩)是线性的。这意味着可以写成

\[Y(q, \dot{q}, \ddot{q})\,\pi = \tau\]

其中 \(Y\) 是一个只依赖运动学量的**回归矩阵**(regressor),\(\pi\) 是待辨识的**惯性参数向量**。这个"线性参数化"发现是一切后续工作的基石——因为线性问题可以用最小二乘精确求解。

后续里程碑: - Khalil & Dombre (1986):提出基参数概念 - Gautier (1991):系统地研究了可辨识性和基参数的数值计算 - Swevers et al. (1997):优化激励轨迹设计 - Atkeson, An, Hollerbach (1986) + Khosla & Kanade (1985):奠定了整个方法论框架

2.4 系统辨识与自适应控制的深度联系

为什么要在这里提 控制理论·自适应控制(规划中)? 因为系统辨识和自适应控制是一枚硬币的两面:

  • 辨识:先收集数据,再离线/在线估计参数,然后用估计出的参数设计控制器
  • 自适应控制边控制边辨识——控制律中直接嵌入参数估计,两者同时运行

经典的 Slotine & Li (1987) 自适应控制律为:

\[\tau = Y(q, \dot{q}, \dot{q}_r, \ddot{q}_r)\hat{\pi} - K_D s\]

其中 \(s = \dot{q} - \dot{q}_r\) 是滑模面变量,\(\hat{\pi}\) 是在线参数估计,参数更新律为:

\[\dot{\hat{\pi}} = -\Gamma Y^\top s\]

注意:这里用到了**同一个回归矩阵 \(Y\)**!本章学到的回归矩阵构造方法,在 控制理论·自适应控制(规划中) 中会直接复用。辨识质量直接决定了自适应控制的收敛速度和稳态精度。

⭐ 自测检查点 2.1:解释为什么"对惯性参数线性"这个性质对辨识至关重要。如果动力学方程对参数是非线性的,辨识会变成什么问题?

提示:非线性参数估计是非凸优化,可能有多个局部极值,初始猜测敏感,计算量大。线性参数化把辨识变成凸的最小二乘——全局最优、闭式解、计算量 \(O(n^3)\)

⚠️ 常见陷阱

⚠️ 编程陷阱:URDF 惯性参数格式混淆

错误做法:直接从 URDF 的 <inertia> 标签读取 \(I_{xx}, I_{xy}, \ldots\) 并当作关于连杆原点的惯性使用

现象:辨识出的参数与真实值偏差极大(即使激励轨迹和算法都正确),残差不收敛

根本原因:URDF 的 <inertia> 标签定义的是**关于质心**的惯性张量,而回归矩阵中使用的是**关于连杆坐标系原点**的惯性。两者通过**平行轴定理**关联:\(I_{\text{origin}} = I_{\text{com}} + m(c^\top c \cdot E_3 - c c^\top)\)

正确做法:提取 URDF 惯性时,必须用平行轴定理转换到连杆原点。见 3.5 节的 Pinocchio 代码

💡 概念误区:认为"辨识精度越高,控制越好"

新手想法:花大量时间把参数精度从 3% 提到 1%,一定会显著改善控制性能

实际上:控制器对参数误差的敏感度取决于控制律的鲁棒性。一个好的鲁棒控制器(如 \(H_\infty\)、滑模控制)可以容忍 10--20% 的参数误差。辨识的边际收益递减:从 50% 到 5% 改善巨大,从 5% 到 1% 可能几乎无感

正确认知:辨识精度应该匹配控制需求。MPC 和前馈控制对参数敏感(需要 < 5%),PD+重力补偿对参数不敏感(< 20% 即可)

延伸:这正是自适应控制(控制理论·自适应控制(规划中))的动机——即使参数不精确,自适应律也能在线补偿

🧠 思维陷阱:认为"数据越多辨识越准"

新手想法:采集 1 小时的数据一定比 1 分钟好

实际上:如果轨迹不"激励"某些参数方向,再多数据也辨识不出来。100 万个共线性数据点不如 100 个正交的数据点。关键是**信息矩阵 \(Y^\top Y\) 的条件数**,不是数据量

正确思维:先设计激励轨迹(最大化 \(\det(Y^\top Y)\)),再决定采集时长。通常一个周期(10--30 秒)的 Fourier 激励轨迹就够了

练习

练习 2.1 ⭐:列出你使用过的(或你实验室的)机器人的 URDF 文件。对比 URDF 中的惯性参数和实际测量值(如果有),估算 CAD 参数的误差量级。

练习 2.2 ⭐:在 MuJoCo 或 PyBullet 中加载一个机械臂模型,将所有连杆质量乘以 1.3(模拟 30% 误差),然后用原始参数设计的 PD 控制器跟踪一个正弦轨迹。记录跟踪误差,与无参数误差的情况对比。

练习 2.3 ⭐⭐:解释为什么 Slotine & Li 自适应控制律中,参数更新律 \(\dot{\hat{\pi}} = -\Gamma Y^\top s\) 能保证闭环稳定性。提示:构造 Lyapunov 函数 \(V = \frac{1}{2}s^\top M s + \frac{1}{2}\tilde{\pi}^\top \Gamma^{-1}\tilde{\pi}\),其中 \(\tilde{\pi} = \pi - \hat{\pi}\)


3. 刚体机器人的线性参数化 ⭐⭐

3.1 从运动方程到回归形式

回顾 刚体动力学/20_Lagrange与Hamilton力学中推导的机器人运动方程:

\[M(q)\ddot{q} + C(q,\dot{q})\dot{q} + g(q) = \tau\]

其中: - \(q \in \mathbb{R}^n\):关节角度向量 - \(M(q) \in \mathbb{R}^{n \times n}\):质量矩阵(对称正定) - \(C(q,\dot{q}) \in \mathbb{R}^{n \times n}\):Coriolis/离心矩阵 - \(g(q) \in \mathbb{R}^n\):重力矩向量 - \(\tau \in \mathbb{R}^n\):关节力矩

每个连杆 \(i\) 有 10 个惯性参数(standard inertial parameters):

\[\pi_i = \begin{bmatrix} m_i \\ m_i c_{x,i} \\ m_i c_{y,i} \\ m_i c_{z,i} \\ I_{xx,i} \\ I_{xy,i} \\ I_{xz,i} \\ I_{yy,i} \\ I_{yz,i} \\ I_{zz,i} \end{bmatrix} \in \mathbb{R}^{10}\]

其中 \(m_i\) 是质量,\((c_{x,i}, c_{y,i}, c_{z,i})\) 是质心在连杆坐标系中的位置(注意:我们使用"第一矩" \(m_i c_{x,i}\) 而非 \(c_{x,i}\),正是为了保证线性性),\(I_{xx,i}, \ldots\) 是关于连杆坐标系原点的惯性张量分量。

关键观察:运动方程的每一项(\(M(q)\ddot{q}\)\(C(q,\dot{q})\dot{q}\)\(g(q)\))都可以写成"运动学量乘以惯性参数之和"的形式。具体地:

\[\tau = \sum_{i=1}^{n} Y_i(q, \dot{q}, \ddot{q}) \, \pi_i = Y(q, \dot{q}, \ddot{q}) \, \pi\]

其中 \(Y \in \mathbb{R}^{n \times 10n}\) 是回归矩阵,\(\pi = [\pi_1^\top, \ldots, \pi_n^\top]^\top \in \mathbb{R}^{10n}\) 是全部惯性参数。

为什么对参数线性? 直觉上:动能 \(T = \frac{1}{2}\dot{q}^\top M(q)\dot{q}\),而 \(M(q)\) 的每个分量都是惯性参数的线性函数(参看 刚体动力学/20_Lagrange与Hamilton力学中 \(M(q) = \sum_i J_{v_i}^\top m_i J_{v_i} + J_{\omega_i}^\top R_i I_{c_i} R_i^\top J_{\omega_i}\),其中 \(J_{v_i}, J_{\omega_i}, R_i\) 只依赖 \(q\))。Lagrange 方程中的 \(\frac{d}{dt}\frac{\partial T}{\partial \dot{q}} - \frac{\partial T}{\partial q}\)\(T\) 是线性算子,所以最终结果对惯性参数仍然线性。

更严格的证明:设单个连杆 \(i\) 对总动能的贡献为

\[T_i = \frac{1}{2}m_i v_{c_i}^\top v_{c_i} + \frac{1}{2}\omega_i^\top I_{c_i}\omega_i\]

其中 \(v_{c_i}\) 是质心线速度,\(\omega_i\) 是角速度。展开:

\[v_{c_i} = J_{v_i}(q)\dot{q} + \dot{c}_i \times \omega_i\]

但在连杆坐标系中 \(c_i\) 是常数,所以 \(v_{c_i}\) 最终是 \(q, \dot{q}\)\(c_i\) 的函数。将动能对惯性参数 \(m_i, m_i c_{x,i}, \ldots\) 展开,可以验证每一项都是惯性参数的一次齐次函数。Lagrange 方程中的偏导和时间导数都是线性算子,因此最终的广义力对惯性参数仍然是线性的。

3.2 完整推导:2-DOF 平面机械臂

为了把抽象理论变得具体,我们对一个 2-DOF 平面机械臂**手推回归矩阵**。

模型描述: - 关节 1 和关节 2 都是旋转关节,在竖直平面内运动 - 连杆长度 \(l_1, l_2\),质心到关节的距离 \(l_{c1}, l_{c2}\) - 连杆质量 \(m_1, m_2\),转动惯量 \(I_1, I_2\)(关于质心) - 重力加速度 \(g_0\)

动能

\[T = \frac{1}{2}(I_1 + m_1 l_{c1}^2 + I_2 + m_2 l_1^2 + m_2 l_{c2}^2 + 2m_2 l_1 l_{c2}\cos q_2)\dot{q}_1^2$$ $$+ \frac{1}{2}(I_2 + m_2 l_{c2}^2)\dot{q}_2^2 + (I_2 + m_2 l_{c2}^2 + m_2 l_1 l_{c2}\cos q_2)\dot{q}_1\dot{q}_2\]

势能

\[V = m_1 g_0 l_{c1}\sin q_1 + m_2 g_0 (l_1 \sin q_1 + l_{c2}\sin(q_1+q_2))\]

Lagrange 方程 \(\frac{d}{dt}\frac{\partial L}{\partial \dot{q}_i} - \frac{\partial L}{\partial q_i} = \tau_i\) 给出:

\[\tau_1 = (I_1 + m_1 l_{c1}^2 + I_2 + m_2 l_1^2 + m_2 l_{c2}^2)\ddot{q}_1 + (I_2 + m_2 l_{c2}^2)\ddot{q}_2$$ $$+ 2m_2 l_1 l_{c2}\cos q_2 \cdot \ddot{q}_1 + m_2 l_1 l_{c2}\cos q_2 \cdot \ddot{q}_2$$ $$- 2m_2 l_1 l_{c2}\sin q_2 \cdot \dot{q}_1\dot{q}_2 - m_2 l_1 l_{c2}\sin q_2 \cdot \dot{q}_2^2$$ $$+ (m_1 l_{c1} + m_2 l_1)g_0\cos q_1 + m_2 l_{c2} g_0\cos(q_1+q_2)\]
\[\tau_2 = (I_2 + m_2 l_{c2}^2)\ddot{q}_1 + (I_2 + m_2 l_{c2}^2)\ddot{q}_2$$ $$+ m_2 l_1 l_{c2}\cos q_2 \cdot \ddot{q}_1 + m_2 l_1 l_{c2}\sin q_2 \cdot \dot{q}_1^2 + m_2 l_{c2} g_0\cos(q_1+q_2)\]

现在定义**回归参数**(将原始物理参数重组为与线性参数化一致的形式):

\[\theta_1 = I_1 + m_1 l_{c1}^2 + I_2 + m_2(l_1^2 + l_{c2}^2)$$ $$\theta_2 = m_2 l_1 l_{c2}$$ $$\theta_3 = I_2 + m_2 l_{c2}^2$$ $$\theta_4 = (m_1 l_{c1} + m_2 l_1) g_0$$ $$\theta_5 = m_2 l_{c2} g_0\]

则运动方程变为:

\[\begin{bmatrix} \tau_1 \\ \tau_2 \end{bmatrix} = Y(q,\dot{q},\ddot{q}) \begin{bmatrix} \theta_1 \\ \theta_2 \\ \theta_3 \\ \theta_4 \\ \theta_5 \end{bmatrix}\]

其中回归矩阵为:

\[Y = \begin{bmatrix} \ddot{q}_1 & 2\cos q_2 \cdot \ddot{q}_1 + \cos q_2 \cdot \ddot{q}_2 - 2\sin q_2 \cdot \dot{q}_1\dot{q}_2 - \sin q_2 \cdot \dot{q}_2^2 & \ddot{q}_2 & \cos q_1 & \cos(q_1+q_2) \\ 0 & \cos q_2 \cdot \ddot{q}_1 + \sin q_2 \cdot \dot{q}_1^2 & \ddot{q}_1 + \ddot{q}_2 & 0 & \cos(q_1+q_2) \end{bmatrix}\]

观察:原始参数有 \(m_1, m_2, l_{c1}, l_{c2}, I_1, I_2\) 共 6 个,但在回归形式中只出现 5 个独立组合 \(\theta_1, \ldots, \theta_5\)。这就是**基参数**概念的雏形。

3.3 基参数(Base Parameters)

对于一般的 \(n\)-DOF 机器人,标准惯性参数有 \(10n\) 个。但其中很多参数**不可辨识**(unidentifiable),原因有两类:

(A) 线性依赖:某些参数组合在回归矩阵中**始终以同一线性组合**出现。上面 2-DOF 例子中,\(I_1\)\(m_1 l_{c1}^2\) 永远以 \(I_1 + m_1 l_{c1}^2\) 的形式出现——你无法从力矩数据中将它们分开。

(B) 对力矩无贡献:某些参数完全不影响力矩。例如,第一个连杆(固定于基座)的质量 \(m_1\) 只通过 \(m_1 l_{c1}^2\)\(m_1 l_{c1} g_0\) 影响力矩——纯质量项 \(m_1\) 乘以基座转动时的零力臂,贡献为零。

形式化定义:设完整回归矩阵为 \(Y_{\text{full}} \in \mathbb{R}^{n \times 10n}\)(或经过多个采样点堆叠后的大矩阵)。对 \(Y_{\text{full}}\) 做列空间分析:

\[\operatorname{rank}(Y_{\text{full}}) = p \leq 10n\]

通过 QR 分解(列主元 pivoting)或 SVD,可以找到 \(p\) 个独立的参数组合,称为**基参数** \(\pi_b \in \mathbb{R}^p\),使得

\[Y_{\text{full}} \pi = Y_b \pi_b\]

其中 \(Y_b \in \mathbb{R}^{n \times p}\) 是满秩的。

Gautier (1991) 算法: 1. 在多个随机构型 \((q_k, \dot{q}_k, \ddot{q}_k)\) 处计算 \(Y_k\),堆叠成大矩阵 \(\bar{Y}\) 2. 对 \(\bar{Y}\) 做列主元 QR 分解:\(\bar{Y}P = QR\),其中 \(P\) 是置换矩阵 3. \(R\) 的前 \(p\) 个对角元非零(\(|R_{ii}| > \epsilon\)),对应的列就是基参数的"骨架" 4. 剩余 \(10n - p\) 个参数要么是零,要么可以吸收进基参数

典型数字:7-DOF 机械臂有 \(10 \times 7 = 70\) 个标准参数,但基参数通常只有 40--50 个(加上摩擦参数后约 50--64 个)。

3.4 加入摩擦模型

实际机器人的摩擦力不可忽略。最常用的模型是 Coulomb + viscous 摩擦:

\[\tau_{\text{friction},i} = f_{v,i}\dot{q}_i + f_{c,i}\operatorname{sign}(\dot{q}_i)\]

其中 \(f_{v,i}\) 是粘性摩擦系数,\(f_{c,i}\) 是 Coulomb 摩擦系数。加入摩擦后,回归矩阵扩展为:

\[\underbrace{Y_{\text{dyn}}(q,\dot{q},\ddot{q})}_{n \times 10n}\,\pi + \underbrace{Y_{\text{fric}}(\dot{q})}_{n \times 2n}\,\pi_{\text{fric}} = \tau\]

其中 \(Y_{\text{fric}}\) 是对角矩阵,每行只有 \(\dot{q}_i\)\(\operatorname{sign}(\dot{q}_i)\) 两个非零元。

更精细的 **Stribeck 摩擦模型**为:

\[\tau_{\text{friction},i} = f_{v,i}\dot{q}_i + \left(f_{c,i} + (f_{s,i} - f_{c,i})e^{-(\dot{q}_i/v_{s,i})^2}\right)\operatorname{sign}(\dot{q}_i)\]

但 Stribeck 模型对参数 \(v_{s,i}\) 是非线性的,不能直接放入线性回归。实践中通常先用 Coulomb + viscous 辨识,再单独拟合 Stribeck 参数。

3.5 代码:Pinocchio computeJointTorqueRegressor 完整工作流

Pinocchio 提供了直接计算回归矩阵的 API。下面给出一个**完整的、可运行的工作流**,从加载 URDF 到验证回归矩阵的正确性,再到基参数提取。

Step 1: 先讲为什么要用 Pinocchio 而不是手写回归矩阵

手写回归矩阵(如 3.2 节)对于 2-DOF 还可行,但对于 7-DOF 机械臂(70 个参数、7 行 70 列的矩阵),手推不仅容易出错,而且无法复用。Pinocchio 基于 Featherstone 的递归算法,在 \(O(n)\) 复杂度内计算回归矩阵——速度快、数值稳定、且自动处理任意运动链拓扑。

Step 2: 正确写法——完整的 Pinocchio 工作流

import numpy as np
import pinocchio as pin

# ====== 1. 加载模型 ======
model = pin.buildModelFromUrdf("panda.urdf")
data = model.createData()

n = model.nv          # 关节自由度
nb = model.nbodies - 1  # 连杆数 (排除 universe/world)

print(f"机器人: {model.name}")
print(f"关节自由度: {n}")
print(f"连杆数: {nb}")
print(f"标准惯性参数总数: {10 * nb}")

# ====== 2. 提取标准惯性参数向量 ======
def extract_standard_parameters(model):
    """从 Pinocchio 模型提取标准惯性参数向量 pi

    每个连杆 10 个参数: [m, mc_x, mc_y, mc_z, I_xx, I_xy, I_xz, I_yy, I_yz, I_zz]
    其中惯性矩是关于连杆坐标系原点的 (不是质心!)
    """
    pi = []
    for i in range(1, model.nbodies):  # 跳过 universe (index 0)
        inertia = model.inertias[i]
        m = inertia.mass
        lever = np.array(inertia.lever)  # 质心位置 (连杆坐标系)
        mc = m * lever                    # 第一矩

        # 关于质心的惯性矩阵
        I_com = np.array(inertia.inertia)  # 3x3 对称矩阵

        # 平行轴定理: I_origin = I_com + m*(c'c*I - c*c')
        # 这一步非常容易出错! 见陷阱 3.2
        I_origin = I_com + m * (np.dot(lever, lever) * np.eye(3) 
                                - np.outer(lever, lever))

        pi.extend([m, mc[0], mc[1], mc[2],
                    I_origin[0,0], I_origin[0,1], I_origin[0,2],
                    I_origin[1,1], I_origin[1,2], I_origin[2,2]])

    return np.array(pi)

pi = extract_standard_parameters(model)

# ====== 3. 计算回归矩阵并验证 ======
np.random.seed(0)
q = pin.randomConfiguration(model)
v = np.random.randn(n)
a = np.random.randn(n)

# Pinocchio 的回归矩阵 API
Y = pin.computeJointTorqueRegressor(model, data, q, v, a)
print(f"\n回归矩阵维度: {Y.shape}")  # (n, 10*nb)

# 用 RNEA 计算"地面真值"力矩
tau_rnea = pin.rnea(model, data, q, v, a)

# 验证 Y @ pi == tau_rnea
tau_regressor = Y @ pi
error = np.linalg.norm(tau_rnea - tau_regressor)
print(f"RNEA 力矩:       {tau_rnea}")
print(f"回归矩阵 @ pi:   {tau_regressor}")
print(f"误差范数:          {error:.2e}")  # 应为 ~1e-14 (机器精度)

assert error < 1e-10, f"验证失败! 误差 {error} 过大"

# ====== 4. 基参数提取 ======
def find_base_parameters(model, data, n_samples=1000, tol=1e-6):
    """通过随机采样 + QR 分解找基参数"""
    from scipy.linalg import qr

    # 堆叠多个构型的回归矩阵
    Y_stack = []
    for _ in range(n_samples):
        q_rand = pin.randomConfiguration(model)
        v_rand = np.random.randn(model.nv) * 2.0
        a_rand = np.random.randn(model.nv) * 5.0
        Y_k = pin.computeJointTorqueRegressor(model, data, q_rand, v_rand, a_rand)
        Y_stack.append(Y_k)

    Y_full = np.vstack(Y_stack)
    print(f"\n堆叠回归矩阵维度: {Y_full.shape}")
    print(f"数值秩: {np.linalg.matrix_rank(Y_full, tol=tol)}")

    # 列主元 QR 分解
    Q, R, P = qr(Y_full, pivoting=True)
    diag_R = np.abs(np.diag(R))

    # 确定基参数个数
    p = np.sum(diag_R > tol * diag_R[0])
    print(f"基参数个数: {p}/{Y_full.shape[1]}")

    # 基参数对应的列索引
    base_indices = np.sort(P[:p])

    return base_indices, p

base_idx, p = find_base_parameters(model, data)

# ====== 5. 用基参数重建回归矩阵 ======
Y_base = Y[:, base_idx]
pi_base = pi[base_idx]
tau_base = Y_base @ pi_base
print(f"\n基参数回归重建误差: {np.linalg.norm(tau_rnea - tau_base):.2e}")
print(f"基参数回归矩阵条件数: {np.linalg.cond(Y_base):.1f}")

Step 3: 错误写法及分析

# ❌ 错误 1: 不做平行轴定理转换
def extract_params_WRONG_v1(model):
    pi = []
    for i in range(1, model.nbodies):
        inertia = model.inertias[i]
        m = inertia.mass
        mc = m * np.array(inertia.lever)
        I_com = np.array(inertia.inertia)
        # 错! 直接用质心惯性, 没有转换到原点
        pi.extend([m, mc[0], mc[1], mc[2],
                    I_com[0,0], I_com[0,1], I_com[0,2],
                    I_com[1,1], I_com[1,2], I_com[2,2]])
    return np.array(pi)

# 后果: Y @ pi_wrong != tau_rnea, 误差量级 ~1e-1 到 ~1e+1
# 辨识结果完全错误, 且这个 bug 非常隐蔽

# ❌ 错误 2: 惯性矩阵元素顺序搞错
# URDF 格式: <inertia ixx="..." ixy="..." ixz="..." iyy="..." iyz="..." izz="..."/>
# Pinocchio 内部存储为完整 3x3 矩阵
# 但有些用户直接按 [Ixx, Iyy, Izz, Ixy, Ixz, Iyz] 排列 —— 顺序不对!
# 正确顺序 (Pinocchio convention): [Ixx, Ixy, Ixz, Iyy, Iyz, Izz]

# ❌ 错误 3: 遗漏 universe 连杆
def extract_params_WRONG_v3(model):
    pi = []
    for i in range(model.nbodies):  # 错! 包含了 universe (index 0)
        inertia = model.inertias[i]
        # ... universe 的惯性是零, 会导致回归矩阵维度不匹配
    return np.array(pi)

Step 4: C++ Eigen 实现对比

// C++ 版本的回归矩阵计算 (使用 Pinocchio C++ API)
#include <pinocchio/algorithm/joint-configuration.hpp>
#include <pinocchio/algorithm/regressor.hpp>
#include <pinocchio/algorithm/rnea.hpp>
#include <pinocchio/parsers/urdf.hpp>
#include <Eigen/Dense>
#include <iostream>

int main() {
    // 加载模型
    pinocchio::Model model;
    pinocchio::urdf::buildModel("panda.urdf", model);
    pinocchio::Data data(model);

    const int n = model.nv;
    const int nb = model.nbodies - 1;

    // 随机关节状态
    Eigen::VectorXd q = pinocchio::randomConfiguration(model);
    Eigen::VectorXd v = Eigen::VectorXd::Random(n);
    Eigen::VectorXd a = Eigen::VectorXd::Random(n);

    // ✅ 正确: 计算回归矩阵
    // 返回 n x (10*nb) 的矩阵
    Eigen::MatrixXd Y = pinocchio::computeJointTorqueRegressor(
        model, data, q, v, a
    );

    // RNEA 验证
    Eigen::VectorXd tau_rnea = pinocchio::rnea(model, data, q, v, a);

    // 提取标准惯性参数
    Eigen::VectorXd pi(10 * nb);
    for (int i = 1; i < model.nbodies; ++i) {
        const auto& inertia = model.inertias[i];
        double m = inertia.mass();
        Eigen::Vector3d lever = inertia.lever();
        Eigen::Vector3d mc = m * lever;

        // 平行轴定理
        Eigen::Matrix3d I_com = inertia.inertia().matrix();
        Eigen::Matrix3d I_origin = I_com 
            + m * (lever.dot(lever) * Eigen::Matrix3d::Identity() 
                   - lever * lever.transpose());

        int offset = (i - 1) * 10;
        pi(offset + 0) = m;
        pi(offset + 1) = mc(0);
        pi(offset + 2) = mc(1);
        pi(offset + 3) = mc(2);
        pi(offset + 4) = I_origin(0, 0);
        pi(offset + 5) = I_origin(0, 1);
        pi(offset + 6) = I_origin(0, 2);
        pi(offset + 7) = I_origin(1, 1);
        pi(offset + 8) = I_origin(1, 2);
        pi(offset + 9) = I_origin(2, 2);
    }

    Eigen::VectorXd tau_Y = Y * pi;
    double error = (tau_rnea - tau_Y).norm();

    std::cout << "回归矩阵验证误差: " << error << std::endl;
    // 应输出 ~1e-14

    return 0;
}

⭐ 自测检查点 3.1:运行上述代码,验证 tau_rneatau_regressor 的差异在机器精度范围内。如果差异较大,检查惯性参数的提取方式(特别是平行轴定理的应用)。

⚠️ 常见陷阱

⚠️ 编程陷阱:忽略基参数直接辨识全部 \(10n\) 个参数

错误做法:把全部 70 个参数放进最小二乘中辨识

现象\(Y_{\text{full}}\) 列不满秩 → \((Y^\top Y)\) 奇异 → 解不唯一或条件数极大 → 辨识出的参数**物理上荒谬**(如负质量 \(m = -2.3\) kg)

根本原因\(10n\) 个标准参数中有很多线性相关或对力矩无贡献。列秩亏的最小二乘有无穷多解,numpy.linalg.lstsq 返回的是最小范数解,但这个解没有物理意义

正确做法:**必须**先通过 QR 或 SVD 降到基参数。辨识基参数后,如需还原物理参数,可附加先验约束(如 \(m_i > 0\))做约束优化

💡 概念误区:认为"质心位置"是线性参数化中的待辨识量

新手想法:辨识目标是每个连杆的质量 \(m_i\) 和质心位置 \((c_{x,i}, c_{y,i}, c_{z,i})\)

实际上:直接辨识 \(c_{x,i}\) 会破坏线性性(因为动能中出现 \(m_i c_{x,i}^2\) 等二次项)。线性参数化中使用的是**第一矩** \(m_i c_{x,i}\)。从 \(m_i c_{x,i}\)\(m_i\) 可以恢复 \(c_{x,i}\),但前提是 \(m_i\) 也是可辨识的

延伸:对于第一个连杆(旋转关节固定在基座),\(m_1\) 本身不可辨识(只有 \(m_1 l_{c1}^2\)\(m_1 l_{c1}\) 的组合可辨识),所以 \(c_{x,1}\) 也不可恢复——这就是基参数的本质

🧠 思维陷阱:认为"回归矩阵中 \(\ddot{q}\) 必须来自传感器测量"

新手想法:需要加速度传感器才能计算回归矩阵

实际上:几乎没有机器人安装关节加速度传感器。\(\ddot{q}\) 的获取方式有三种: 1. 数值微分(最简单但噪声大) 2. Fourier 轨迹的解析导数(推荐,4.6 节详述) 3. 积分消除法(Gautier & Poignet 2001,通过分部积分消去 \(\ddot{q}\)

自检:如果你的方案依赖加速度传感器,重新考虑

⚠️ 编程陷阱:Pinocchio 的 computeJointTorqueRegressor 输入顺序

错误做法pin.computeJointTorqueRegressor(model, data, v, q, a) —— 把 vq 的位置搞反了

现象:不会报错(因为 qv 都是 VectorXd),但回归矩阵完全错误

根本原因:C++ / Python 动态类型不会检查语义,只检查类型和维度

正确做法:始终按 (model, data, q, v, a) 的顺序传入。养成用关键字参数的习惯

练习

练习 3.1 ⭐:对 2-DOF 平面机械臂(3.2 节),如果关节 2 的连杆是对称圆柱体(\(l_{c2} = l_2/2\)),写出 \(I_2\) 关于 \(m_2, l_2, r_2\)(半径)的表达式。代入基参数公式,验证基参数个数是否改变。

练习 3.2 ⭐⭐:用 Pinocchio 加载一个 7-DOF 机械臂的 URDF,计算其基参数个数。提示:使用 find_base_parameters 函数。

练习 3.3 ⭐⭐:推导当连杆 \(i\) 是平面关节(prismatic)而非旋转关节时,标准惯性参数中哪些变得不可辨识?提示:平移关节的速度雅可比形式不同。

练习 3.4 ⭐⭐⭐:Khalil & Dombre 的基参数提取使用 QR 分解。解释为什么不能用 SVD 直接做基参数提取。提示:SVD 给出的是正交基,但基参数需要保持物理可解释性(SVD 给出的线性组合可能混合了不同连杆的参数)。


4. 最小二乘辨识 ⭐⭐

4.1 问题建立

假设我们在 \(K\) 个时刻 \(t_1, \ldots, t_K\) 采集了数据 \((q_k, \dot{q}_k, \ddot{q}_k, \tau_k)\)。在每个时刻:

\[Y_k \pi_b = \tau_k + \epsilon_k\]

其中 \(\epsilon_k\) 是测量噪声。堆叠所有时刻:

\[\underbrace{\begin{bmatrix} Y_1 \\ Y_2 \\ \vdots \\ Y_K \end{bmatrix}}_{\bar{Y} \in \mathbb{R}^{nK \times p}} \pi_b = \underbrace{\begin{bmatrix} \tau_1 \\ \tau_2 \\ \vdots \\ \tau_K \end{bmatrix}}_{\bar{\tau} \in \mathbb{R}^{nK}} + \underbrace{\begin{bmatrix} \epsilon_1 \\ \epsilon_2 \\ \vdots \\ \epsilon_K \end{bmatrix}}_{\bar{\epsilon}}\]

\(\bar{Y}\pi_b = \bar{\tau} + \bar{\epsilon}\)。这是一个**超定线性方程组**(\(nK \gg p\)),我们需要从含噪数据中估计 \(\pi_b\)

4.2 OLS(普通最小二乘)的完整推导

目标:最小化残差平方和

\[J(\pi_b) = \|\bar{\tau} - \bar{Y}\pi_b\|^2 = (\bar{\tau} - \bar{Y}\pi_b)^\top(\bar{\tau} - \bar{Y}\pi_b)\]

展开

\[J(\pi_b) = \bar{\tau}^\top\bar{\tau} - 2\pi_b^\top\bar{Y}^\top\bar{\tau} + \pi_b^\top\bar{Y}^\top\bar{Y}\pi_b\]

\(\pi_b\) 求梯度

\[\nabla_{\pi_b} J = -2\bar{Y}^\top\bar{\tau} + 2\bar{Y}^\top\bar{Y}\pi_b\]

令梯度为零,得到**正规方程**(normal equation):

\[\bar{Y}^\top\bar{Y}\,\hat{\pi}_b = \bar{Y}^\top\bar{\tau}\]

\(\bar{Y}\) 列满秩(\(\operatorname{rank}(\bar{Y}) = p\)),则 \(\bar{Y}^\top\bar{Y}\) 正定可逆:

\[\boxed{\hat{\pi}_b = (\bar{Y}^\top\bar{Y})^{-1}\bar{Y}^\top\bar{\tau}}\]

这就是 OLS 估计\((\bar{Y}^\top\bar{Y})^{-1}\bar{Y}^\top\) 称为 \(\bar{Y}\) 的**伪逆**(Moore-Penrose pseudoinverse)\(\bar{Y}^+\)

Hessian 矩阵\(\nabla^2_{\pi_b} J = 2\bar{Y}^\top\bar{Y} \succ 0\)(正定),所以 \(\hat{\pi}_b\) 是唯一的全局最小值。

4.2.1 C++ Eigen 实现:OLS(正确 vs 错误 vs 对比)

Step 1: 为什么要用 Eigen 的 ldlt/colPivHouseholderQr 而不是直接求逆?

正规方程 \((\bar{Y}^\top\bar{Y})\hat{\pi} = \bar{Y}^\top\bar{\tau}\) 的解可以通过求逆 \((\bar{Y}^\top\bar{Y})^{-1}\) 得到,但直接求逆在数值上不稳定——当条件数大时,逆矩阵的元素会有很大的舍入误差。更好的做法是使用矩阵分解求解线性方程组。

Step 2: 正确写法

#include <Eigen/Dense>
#include <iostream>
#include <chrono>

// ✅ 方式 A: 使用 QR 分解直接求解 (最推荐, 数值最稳)
Eigen::VectorXd ols_qr(const Eigen::MatrixXd& Y, const Eigen::VectorXd& tau) {
    // Y: (nK x p), tau: (nK x 1)
    // 直接对 Y 做 QR 分解求解 min ||tau - Y*pi||^2
    return Y.colPivHouseholderQr().solve(tau);
}

// ✅ 方式 B: 使用正规方程 + LDLT 分解 (Y'Y 对称正定时)
Eigen::VectorXd ols_normal_ldlt(const Eigen::MatrixXd& Y, const Eigen::VectorXd& tau) {
    Eigen::MatrixXd YtY = Y.transpose() * Y;
    Eigen::VectorXd Ytau = Y.transpose() * tau;
    return YtY.ldlt().solve(Ytau);
}

// ✅ 方式 C: 使用 SVD (最稳但最慢, 可处理秩亏情况)
Eigen::VectorXd ols_svd(const Eigen::MatrixXd& Y, const Eigen::VectorXd& tau) {
    return Y.bdcSvd(Eigen::ComputeThinU | Eigen::ComputeThinV).solve(tau);
}

Step 3: 错误写法及分析

// ❌ 错误 1: 直接求逆
Eigen::VectorXd ols_WRONG_inverse(const Eigen::MatrixXd& Y, const Eigen::VectorXd& tau) {
    Eigen::MatrixXd YtY = Y.transpose() * Y;
    // 危险! 当 cond(YtY) > 1e12 时, inverse() 的结果充满舍入误差
    return YtY.inverse() * Y.transpose() * tau;
    // 问题: (1) inverse() 是 O(p^3) 且数值不稳; (2) 不处理秩亏
}

// ❌ 错误 2: 未检查条件数就使用正规方程
Eigen::VectorXd ols_WRONG_no_check(const Eigen::MatrixXd& Y, const Eigen::VectorXd& tau) {
    Eigen::MatrixXd YtY = Y.transpose() * Y;
    // 没有检查条件数! 如果 YtY 接近奇异, llt() 会默默给出垃圾结果
    return YtY.llt().solve(Y.transpose() * tau);
    // 更糟: llt() 假设正定, 如果 YtY 不正定 (因为秩亏), 行为未定义
}

// ❌ 错误 3: 用 float 精度
Eigen::VectorXf ols_WRONG_float(const Eigen::MatrixXf& Y, const Eigen::VectorXf& tau) {
    // float 只有 ~7 位有效数字
    // YtY 的条件数可能是 1e8, 求解后只剩 ~0 位有效数字!
    // 机器人辨识必须用 double
    return Y.colPivHouseholderQr().solve(tau);
}

Step 4: 三种方式性能对比

方法 数值稳定性 速度 (\(p=50\), \(nK=7000\)) 能否处理秩亏 推荐场景
QR (colPivHouseholderQr) ~2 ms 是 (自动) 通用首选
正规方程 + LDLT ~0.5 ms 否 (需满秩) 已确认满秩、需要速度
SVD (bdcSvd) 最高 ~8 ms 是 (自动) 调试、秩亏分析
直接求逆 ~1 ms 永远不用

4.3 WLS(加权最小二乘)

如果不同时刻的噪声方差不同(异方差性),OLS 不再最优。设噪声协方差为 \(\operatorname{Cov}(\bar{\epsilon}) = \Sigma\),则 WLS 最小化:

\[J_W(\pi_b) = (\bar{\tau} - \bar{Y}\pi_b)^\top \Sigma^{-1} (\bar{\tau} - \bar{Y}\pi_b)\]

解为:

\[\boxed{\hat{\pi}_b^{\text{WLS}} = (\bar{Y}^\top\Sigma^{-1}\bar{Y})^{-1}\bar{Y}^\top\Sigma^{-1}\bar{\tau}}\]

实践中如何估计 \(\Sigma\)?常用方法: 1. 先用 OLS 得到初步估计 \(\hat{\pi}_b^{(0)}\) 2. 计算残差 \(\hat{\epsilon}_k = \tau_k - Y_k\hat{\pi}_b^{(0)}\) 3. 估计每个关节的噪声方差 \(\hat{\sigma}_j^2 = \frac{1}{K}\sum_k \hat{\epsilon}_{j,k}^2\) 4. 构造 \(\Sigma = \operatorname{diag}(\hat{\sigma}_1^2, \ldots, \hat{\sigma}_n^2) \otimes I_K\) 5. 用 WLS 重新估计 → 迭代(IRLS, Iteratively Reweighted Least Squares)

4.4 Gauss-Markov 定理

定理(Gauss-Markov):在线性回归模型 \(\bar{\tau} = \bar{Y}\pi_b + \bar{\epsilon}\) 中,若噪声满足: 1. \(\mathbb{E}[\bar{\epsilon}] = 0\)(零均值) 2. \(\operatorname{Cov}(\bar{\epsilon}) = \sigma^2 I\)(同方差、不相关)

则 OLS 估计 \(\hat{\pi}_b = (\bar{Y}^\top\bar{Y})^{-1}\bar{Y}^\top\bar{\tau}\) 是**所有线性无偏估计中方差最小的**(BLUE: Best Linear Unbiased Estimator)。

证明:设任意线性无偏估计为 \(\tilde{\pi}_b = A\bar{\tau}\),其中 \(A\bar{Y} = I\)(无偏条件)。则:

\[\operatorname{Cov}(\tilde{\pi}_b) = \sigma^2 AA^\top\]

\(D = A - (\bar{Y}^\top\bar{Y})^{-1}\bar{Y}^\top\),由无偏条件 \(D\bar{Y} = 0\),则:

\[AA^\top = [D + (\bar{Y}^\top\bar{Y})^{-1}\bar{Y}^\top][D + (\bar{Y}^\top\bar{Y})^{-1}\bar{Y}^\top]^\top$$ $$= DD^\top + (\bar{Y}^\top\bar{Y})^{-1} + D\bar{Y}(\bar{Y}^\top\bar{Y})^{-1} + (\bar{Y}^\top\bar{Y})^{-1}\bar{Y}^\top D^\top\]

因为 \(D\bar{Y} = 0\),交叉项为零。所以:

\[\operatorname{Cov}(\tilde{\pi}_b) = \sigma^2(\bar{Y}^\top\bar{Y})^{-1} + \sigma^2 DD^\top \succeq \sigma^2(\bar{Y}^\top\bar{Y})^{-1} = \operatorname{Cov}(\hat{\pi}_b)\]

不等号对所有 \(D\) 成立(\(DD^\top \succeq 0\)),等号当且仅当 \(D = 0\) 时成立。\(\square\)

工程含义:在 Gauss-Markov 条件满足时,OLS 已是最优——不需要更复杂的估计方法。但如果噪声存在异方差性或相关性(在实际机器人中很常见,因为力矩传感器精度在不同关节可能不同),则需要 WLS。

4.5 Cramer-Rao 下界

定理(Cramer-Rao Lower Bound, CRLB):对于任意无偏估计 \(\hat{\pi}_b\),其协方差满足

\[\operatorname{Cov}(\hat{\pi}_b) \succeq \mathcal{I}(\pi_b)^{-1}\]

其中 \(\mathcal{I}(\pi_b)\)Fisher 信息矩阵

\[\mathcal{I}(\pi_b) = \mathbb{E}\left[\left(\frac{\partial \ln p(\bar{\tau}|\pi_b)}{\partial \pi_b}\right)\left(\frac{\partial \ln p(\bar{\tau}|\pi_b)}{\partial \pi_b}\right)^\top\right]\]

对于高斯噪声 \(\bar{\epsilon} \sim \mathcal{N}(0, \sigma^2 I)\),Fisher 信息矩阵为

\[\mathcal{I}(\pi_b) = \frac{1}{\sigma^2}\bar{Y}^\top\bar{Y}\]

从而 CRLB 为 \(\operatorname{Cov}(\hat{\pi}_b) \succeq \sigma^2(\bar{Y}^\top\bar{Y})^{-1}\)。这恰好等于 OLS 的协方差——在高斯噪声下 OLS 达到 CRLB,是渐近有效的(等价于 MLE)

4.6 激励轨迹设计

辨识精度不仅取决于算法,更取决于**实验设计**——你让机器人跑什么轨迹来采集数据。

核心原则\(\hat{\pi}_b\) 的协方差正比于 \((\bar{Y}^\top\bar{Y})^{-1}\),要使辨识精度高,就要**最大化 \(\bar{Y}^\top\bar{Y}\) 的"大小"**。

D-最优性(D-optimality):选择轨迹使

\[\max \det(\bar{Y}^\top\bar{Y})\]

等价于最小化参数估计协方差椭球的体积。

为什么是 \(\det\) 而不是 \(\lambda_{\min}\)\(\text{trace}\) 不同的最优性准则对应不同的设计目标:

准则 目标函数 几何含义 适用场景
D-最优 \(\max \det(\bar{Y}^\top\bar{Y})\) 最小化椭球体积 所有参数同等重要
A-最优 \(\min \text{trace}((\bar{Y}^\top\bar{Y})^{-1})\) 最小化方差之和 关注总体精度
E-最优 \(\max \lambda_{\min}(\bar{Y}^\top\bar{Y})\) 最小化最大方差 关注最差参数

实践中 D-最优 最常用,因为它等价于最大化 Fisher 信息矩阵的行列式,有清晰的统计学解释。

4.6.1 Fourier 参数化的完整数学推导

Swevers et al. (1997) 提出将轨迹参数化为有限阶 Fourier 级数:

\[q_j(t) = q_{0,j} + \sum_{l=1}^{N_h} \left(\frac{a_{l,j}}{l\omega_f}\sin(l\omega_f t) - \frac{b_{l,j}}{l\omega_f}\cos(l\omega_f t)\right)\]

其中 \(\omega_f = 2\pi/T_f\) 是基频,\(T_f\) 是轨迹周期,\(a_{l,j}, b_{l,j}\) 是待优化的 Fourier 系数,\(N_h\) 是最高谐波阶数。

为什么要除以 \(l\omega_f\) 这是为了让速度和加速度的表达式更简洁。对 \(q_j(t)\) 求导:

\[\dot{q}_j(t) = \sum_{l=1}^{N_h} \left(a_{l,j}\cos(l\omega_f t) + b_{l,j}\sin(l\omega_f t)\right)\]
\[\ddot{q}_j(t) = \sum_{l=1}^{N_h} \left(-a_{l,j}\,l\omega_f\sin(l\omega_f t) + b_{l,j}\,l\omega_f\cos(l\omega_f t)\right)\]

观察关键性质: 1. 速度的 Fourier 系数就是 \(a_{l,j}\)\(b_{l,j}\)——这意味着优化速度幅度直接就是优化 Fourier 系数 2. 加速度的幅度正比于 \(l\omega_f\)——高次谐波产生更大的加速度,但也需要更大的力矩 3. 轨迹自动满足 \(q_j(0) = q_j(T_f)\)(周期性),因为 \(\sin(0) = \sin(l\omega_f T_f) = 0\),但 \(\cos\) 项给出 \(q_j(0) = q_{0,j} - \sum_l b_{l,j}/(l\omega_f)\)\(q_j(T_f)\) 相同

约束处理:轨迹必须满足关节极限、速度极限和力矩极限:

\[q_{\min,j} \leq q_j(t) \leq q_{\max,j}, \quad |\dot{q}_j(t)| \leq \dot{q}_{\max,j}, \quad |\tau_j(t)| \leq \tau_{\max,j}\]

前两个约束可以在时域上以有限个采样点检查(将连续约束离散化)。力矩约束需要已知参数的粗略估计(CAD 标称值),是一个软约束。

优化问题的完整形式

设决策变量为 \(\xi = [q_{0,1}, a_{1,1}, b_{1,1}, \ldots, a_{N_h,1}, b_{N_h,1}, q_{0,2}, \ldots]^\top \in \mathbb{R}^{n(2N_h+1)}\)

\[\max_{\xi} \; \log\det(\bar{Y}(\xi)^\top\bar{Y}(\xi))$$ $$\text{s.t.} \quad q_{\min} \leq q(t_k; \xi) \leq q_{\max}, \quad k = 1, \ldots, K$$ $$\quad |\dot{q}(t_k; \xi)| \leq \dot{q}_{\max}, \quad k = 1, \ldots, K\]

使用 \(\log\det\) 而非 \(\det\) 的原因:(1) 避免数值溢出(\(\det\) 可能是 \(10^{100}\) 量级);(2) \(\log\det\) 是凹函数(虽然整体问题仍然非凸,但有更好的数值性质)。

求解方法:这是一个非线性约束优化问题(\(\bar{Y}\) 通过回归矩阵非线性地依赖于 \(\xi\)),常用方法: - SQP(Sequential Quadratic Programming):适合中等规模,scipy.optimize.minimize 支持 - CMA-ES(Covariance Matrix Adaptation Evolution Strategy):无梯度优化,适合高维 - 粒子群优化(PSO):简单易实现,适合快速原型

⚠️ 常见陷阱

⚠️ 编程陷阱:数值微分放大噪声

错误做法\(\ddot{q}_k \approx (\dot{q}_{k+1} - \dot{q}_k)/\Delta t\)(前向差分)

现象\(\ddot{q}\) 的估计值充满高频噪声脉冲,回归矩阵的条件数暴增 10-100 倍,辨识出的参数方差是理论值的 100 倍

根本原因:数值微分是一个不适定问题(ill-posed)——它放大输入信号中比 \(1/\Delta t\) 更高频的噪声分量。如果编码器噪声为 \(\sigma_q\),则 \(\sigma_{\ddot{q}} \approx 2\sigma_q/\Delta t^2\),在 \(\Delta t = 1\) ms, \(\sigma_q = 10^{-4}\) rad 时,\(\sigma_{\ddot{q}} \approx 200\) rad/s\(^2\)——远大于实际加速度

正确做法: - 使用 Fourier 参数化轨迹 → \(\dot{q}, \ddot{q}\) 由解析公式计算 - 或者:先对 \(q(t)\) 做低通滤波(截止频率取轨迹最高频率的 2-3 倍),再做中心差分 - 或者:使用"消除加速度"方法——将回归方程在整个周期上积分,\(\ddot{q}\) 通过分部积分消去(Gautier & Poignet, 2001) - 自检方法:画出 \(\ddot{q}\) 的功率谱,如果噪声底层比信号只低 10 dB,说明数值微分质量不够

⚠️ 编程陷阱:电机侧 vs 关节侧力矩

错误做法:直接将电机电流乘以力矩常数当作关节力矩

现象:辨识出的惯性矩比 CAD 值大 \(N^2\) 倍(\(N\) 是减速比),摩擦系数也偏大

根本原因:工业机器人通常测量的是**电流**(正比于电机侧力矩 \(\tau_m\)),而非关节侧力矩 \(\tau_j\)。两者关系为 \(\tau_j = N \tau_m - I_m N^2 \ddot{q} - f_{m,v} N^2 \dot{q} - f_{m,c} N \text{sign}(\dot{q})\),其中 \(I_m\) 是电机转子惯量

正确做法:在回归模型中加入电机转子惯量项 \(I_m N^2 \ddot{q}\)(反映到关节侧的电机惯量),以及电机侧摩擦反映到关节侧的项

💡 概念误区:认为 D-最优轨迹是唯一的

新手想法:存在一条"最优激励轨迹"

实际上:D-最优问题通常有多个局部最优。不同的初始 Fourier 系数会收敛到不同的轨迹,但它们的 \(\det(\bar{Y}^\top\bar{Y})\) 值相近。实践中选一个条件数低于 100 的轨迹就够了——不需要全局最优

正确做法:多次随机初始化优化,取条件数最小的那个

🧠 思维陷阱:忽略数据预处理

新手想法:直接把原始采集的 \((q, \tau)\) 数据塞进辨识算法

实际上:原始数据通常需要:(1) 去除直流偏置(力矩传感器零漂);(2) 低通滤波(消除高频噪声和混叠);(3) 去除异常点(力矩突变、碰撞);(4) 降采样(减少数据量但不丢信息,通常从 1 kHz 降到 100--200 Hz)

正确思维:数据质量 >> 算法花哨程度。80% 的辨识问题出在数据预处理上

⚠️ 编程陷阱:摩擦模型选择

错误做法 A:只用粘性摩擦(\(f_v \dot{q}\))——低速时残差很大,因为 Coulomb 摩擦占主导

错误做法 B:用 Coulomb + viscous(\(f_v \dot{q} + f_c \operatorname{sign}(\dot{q})\))——在零速附近 \(\operatorname{sign}\) 不连续,引入数值问题

现象:做法 A 的残差在低速段系统性偏大;做法 B 在零速穿越时产生力矩脉冲

正确做法:在零速附近用 \(\tanh(\alpha \dot{q})\) 近似 \(\operatorname{sign}(\dot{q})\)\(\alpha\) 取 50--200。这保持了线性参数化(\(f_c\) 前面的 \(\tanh\) 只依赖 \(\dot{q}\),不依赖参数),同时避免了不连续性

4.7 完整的辨识流程代码

import numpy as np
import pinocchio as pin
from scipy.optimize import minimize

def generate_fourier_trajectory(t, omega_f, coeffs, q0):
    """生成 Fourier 参数化的激励轨迹

    Args:
        t: 时间数组 (K,)
        omega_f: 基频
        coeffs: Fourier 系数 (n_joints, 2*N_h) -- [a1,b1,a2,b2,...]
        q0: 关节偏移 (n_joints,)

    Returns:
        q, qd, qdd: 位置/速度/加速度 (K, n_joints)
    """
    n_joints = len(q0)
    N_h = coeffs.shape[1] // 2
    K = len(t)

    q = np.tile(q0, (K, 1))
    qd = np.zeros((K, n_joints))
    qdd = np.zeros((K, n_joints))

    for j in range(n_joints):
        for l in range(1, N_h + 1):
            a_l = coeffs[j, 2*(l-1)]
            b_l = coeffs[j, 2*(l-1)+1]
            lw = l * omega_f
            q[:, j]   += a_l/(lw) * np.sin(lw*t) - b_l/(lw) * np.cos(lw*t)
            qd[:, j]  += a_l * np.cos(lw*t) + b_l * np.sin(lw*t)
            qdd[:, j] += -a_l*lw * np.sin(lw*t) + b_l*lw * np.cos(lw*t)

    return q, qd, qdd

def build_stacked_regressor(model, data, q_traj, qd_traj, qdd_traj):
    """堆叠所有时刻的回归矩阵

    Returns:
        Y_bar: (n*K, 10*nbodies) 堆叠回归矩阵
    """
    K = q_traj.shape[0]
    n = model.nv
    nb = model.nbodies - 1  # 排除 universe
    Y_bar = np.zeros((n * K, 10 * nb))

    for k in range(K):
        Y_k = pin.computeJointTorqueRegressor(
            model, data, q_traj[k], qd_traj[k], qdd_traj[k]
        )
        Y_bar[k*n:(k+1)*n, :] = Y_k

    return Y_bar

def compute_base_parameters(Y_bar, tol=1e-6):
    """通过 QR 分解找基参数

    Returns:
        col_indices: 基参数对应的列索引
        p: 基参数个数
    """
    from scipy.linalg import qr
    Q, R, P = qr(Y_bar, pivoting=True)

    # 找秩: R 对角线元素 > tol 的个数
    diag_R = np.abs(np.diag(R))
    p = np.sum(diag_R > tol * diag_R[0])

    col_indices = P[:p]
    return col_indices, p

def identify_parameters(Y_bar, tau_bar, col_indices):
    """OLS 辨识基参数

    Returns:
        pi_hat: 辨识得到的基参数
        sigma_pi: 参数标准差
        cond_number: 回归矩阵条件数
    """
    Y_b = Y_bar[:, col_indices]

    # OLS
    pi_hat = np.linalg.lstsq(Y_b, tau_bar, rcond=None)[0]

    # 残差与噪声方差估计
    residual = tau_bar - Y_b @ pi_hat
    n_samples = len(tau_bar)
    p = len(pi_hat)
    sigma2 = np.dot(residual, residual) / (n_samples - p)

    # 参数协方差
    cov_pi = sigma2 * np.linalg.inv(Y_b.T @ Y_b)
    sigma_pi = np.sqrt(np.diag(cov_pi))

    # 条件数
    cond_number = np.linalg.cond(Y_b)

    return pi_hat, sigma_pi, cond_number

# ---- 使用示例 ----
model = pin.buildModelFromUrdf("robot.urdf")
data = model.createData()

# 生成激励轨迹
T_f = 10.0  # 周期
omega_f = 2 * np.pi / T_f
dt = 0.001
t = np.arange(0, T_f, dt)
N_h = 5  # Fourier 阶数
n_joints = model.nv

np.random.seed(42)
coeffs = 0.5 * np.random.randn(n_joints, 2 * N_h)
q0 = np.zeros(n_joints)

q_traj, qd_traj, qdd_traj = generate_fourier_trajectory(t, omega_f, coeffs, q0)

# 构建回归矩阵
Y_bar = build_stacked_regressor(model, data, q_traj, qd_traj, qdd_traj)

# 模拟真实力矩(加噪声)
pi_true = np.random.rand(10 * (model.nbodies - 1))  # 假设的真实参数
tau_bar = Y_bar @ pi_true + 0.1 * np.random.randn(Y_bar.shape[0])

# 找基参数
col_indices, p = compute_base_parameters(Y_bar)
print(f"基参数个数: {p}/{Y_bar.shape[1]}")

# 辨识
pi_hat, sigma_pi, cond = identify_parameters(Y_bar, tau_bar, col_indices)
print(f"条件数: {cond:.1f}")
print(f"参数相对误差: {np.linalg.norm(pi_hat - pi_true[col_indices])/np.linalg.norm(pi_true[col_indices]):.4f}")

⭐ 自测检查点 4.1:在上述代码中,如果 cond_number > 1e6,说明什么问题?应该怎么解决?

:条件数过大说明回归矩阵接近秩亏——某些参数组合几乎线性相关。可能的原因:(1) 激励轨迹不够"丰富",没有充分激励所有参数;(2) 基参数选取有误。解决方案:重新设计激励轨迹(增加 Fourier 阶数、扩大关节运动范围),或使用 D-最优准则优化。

⭐ 自测检查点 4.2:Gauss-Markov 定理的两个条件(零均值、同方差不相关)在机器人辨识中是否成立?如果不成立,应该怎么办?

:通常不完全成立。(1) 零均值:如果模型结构正确(含摩擦),基本成立;(2) 同方差:不同关节的力矩传感器精度不同,且大力矩时噪声可能更大——应使用 WLS。不相关:相邻采样点的噪声可能有时间相关性——应降采样或使用 GLS(广义最小二乘)。

练习

练习 4.1 ⭐:证明 WLS 估计在 \(\operatorname{Cov}(\bar{\epsilon}) = \Sigma\) 的条件下是 BLUE。提示:用变量替换 \(\tilde{Y} = \Sigma^{-1/2}\bar{Y}\), \(\tilde{\tau} = \Sigma^{-1/2}\bar{\tau}\),把 WLS 变成 OLS,然后套用 Gauss-Markov 定理。

练习 4.2 ⭐⭐:实现 D-最优激励轨迹设计。使用 scipy.optimize.minimize 优化 Fourier 系数,目标函数为 \(-\log\det(\bar{Y}^\top\bar{Y})\),约束为关节位置和速度极限。对比优化前后回归矩阵的条件数。

练习 4.3 ⭐⭐:用 C++ Eigen 实现 4.2.1 节中的三种 OLS 求解方法,并在 7-DOF 机械臂的辨识数据上比较运行时间和数值精度。绘制 \(p\) (参数个数) vs 运行时间的曲线。

练习 4.4 ⭐⭐⭐:推导"消除加速度"方法的数学细节。提示:将 \(\int_0^{T_f} Y_k \pi_b \, dt = \int_0^{T_f} \tau_k \, dt\) 展开,对含 \(\ddot{q}\) 的项做分部积分,利用周期性条件消去边界项。


5. 频域辨识方法 ⭐⭐⭐

5.1 动机:为什么要在频域做辨识?

时域方法(OLS/WLS)将所有频率的数据混在一起处理。但在实际机器人中,不同频率范围的数据质量差异很大:

  • 低频 (< 1 Hz):重力项和 Coulomb 摩擦占主导,信噪比高
  • 中频 (1--10 Hz):Coriolis 和惯性项显著,正是辨识的"黄金频段"
  • 高频 (> 10 Hz):传感器噪声、结构柔性振动、量化噪声占主导

频域方法允许我们**选择性地利用特定频率范围的数据**,避开噪声严重的频段。

5.2 从时域到频域:离散 Fourier 变换

对于周期激励信号(周期 \(T_f\),采样频率 \(f_s\)\(N\) 个采样点),信号可以用 DFT 表示:

\[\hat{X}(k) = \frac{1}{N}\sum_{n=0}^{N-1} x(n) e^{-j2\pi kn/N}, \quad k = 0, 1, \ldots, N-1\]

其中 \(k\) 对应频率 \(f_k = k f_s / N\)

时域的线性回归 \(Y(t)\pi = \tau(t)\) 在频域变为:

\[\hat{Y}(f_k)\pi = \hat{\tau}(f_k)\]

关键:每个频率点 \(f_k\) 给出一组独立的方程。我们可以只选取感兴趣的频率点组成方程组。

5.3 多正弦激励(Multisine Excitation)

Fourier 参数化轨迹的频域解读:4.6 节中的 Fourier 轨迹在频域上恰好是一个**多正弦信号**——只在 \(f = l\omega_f/(2\pi)\), \(l = 1, \ldots, N_h\) 处有能量。

多正弦激励的设计

\[u_j(t) = \sum_{l=1}^{N_h} A_{l,j} \cos(2\pi f_l t + \phi_{l,j})\]

其中 \(f_l\) 是选定的激励频率,\(A_{l,j}\) 是幅度,\(\phi_{l,j}\) 是相位。

Schroeder 相位(最小波峰因子):为了在给定频率内容下最小化信号的峰值,使用

\[\phi_{l,j} = \phi_{1,j} - \frac{l(l-1)\pi}{N_h}\]

这给出的信号波峰因子接近 \(\sqrt{2}\)(正弦波的理论下限),使得在力矩限制下可以用更大的激励幅度。

频率选择策略

策略 频率分布 优点 缺点
等间距 \(f_l = l \cdot \Delta f\) 简单 高频采样过多
对数间距 \(f_l = f_{\min} \cdot r^{l-1}\) 每十倍频等密度 低频分辨率低
奇数次谐波 \(f_l = (2l-1)\Delta f\) 可检测非线性 需要更多频率

为什么奇数次谐波能检测非线性? 如果系统是线性的,输入只含奇数次谐波时,输出也只在奇数次谐波处有响应。如果偶数次谐波处出现能量,就说明系统存在非线性——这是诊断模型结构是否正确的有力工具。

5.4 Bode 图辨识

对于**单关节**(或解耦后的单轴),可以用经典的频率响应方法。将单关节运动方程在工作点 \(q_0\) 附近线性化:

\[J_{\text{eff}}\ddot{q} + f_v \dot{q} + K q = \tau\]

其中 \(J_{\text{eff}}\) 是等效惯量(包含电机惯量和连杆惯量),\(f_v\) 是粘性摩擦,\(K\) 是弹性项(柔性关节或重力刚度)。

频率响应函数(FRF):

\[G(j\omega) = \frac{\hat{q}(\omega)}{\hat{\tau}(\omega)} = \frac{1}{-J_{\text{eff}}\omega^2 + jf_v\omega + K}\]

从 Bode 图可以直接读出:

  • 低频增益 \(|G(0)| = 1/K\) → 得到弹性刚度 \(K\)
  • 谐振频率 \(\omega_n = \sqrt{K/J_{\text{eff}}}\) → 结合 \(K\) 得到 \(J_{\text{eff}}\)
  • 谐振峰高度 → 得到阻尼比 \(\zeta = f_v/(2\sqrt{KJ_{\text{eff}}})\) → 得到 \(f_v\)
  • 高频渐近斜率 \(-40\) dB/dec → 确认是二阶系统
import numpy as np
from scipy import signal
import matplotlib.pyplot as plt

def bode_identification(freq_hz, frf_mag, frf_phase):
    """从 Bode 图数据辨识二阶系统参数

    Args:
        freq_hz: 频率数组 [Hz]
        frf_mag: FRF 幅值 [dB]
        frf_phase: FRF 相位 [deg]

    Returns:
        J_eff, f_v, K: 等效惯量, 粘性摩擦, 弹性刚度
    """
    omega = 2 * np.pi * freq_hz

    # 1. 低频增益 -> K
    K = 1.0 / (10 ** (frf_mag[0] / 20))

    # 2. 谐振频率 (幅值最大处)
    idx_peak = np.argmax(frf_mag)
    omega_n = omega[idx_peak]

    # 3. 等效惯量
    J_eff = K / omega_n**2

    # 4. 阻尼比 (半功率带宽法)
    peak_mag = frf_mag[idx_peak]
    half_power = peak_mag - 3.0  # -3 dB

    # 找半功率带宽
    above_half = np.where(frf_mag > half_power)[0]
    if len(above_half) > 1:
        bw = omega[above_half[-1]] - omega[above_half[0]]
        zeta = bw / (2 * omega_n)
    else:
        zeta = 0.1  # 默认值

    # 5. 粘性摩擦
    f_v = 2 * zeta * np.sqrt(K * J_eff)

    return J_eff, f_v, K

# 验证: 生成理论 Bode 图并辨识
J_true, fv_true, K_true = 0.5, 2.0, 100.0
num = [1.0]
den = [J_true, fv_true, K_true]
sys_tf = signal.TransferFunction(num, den)

freq = np.logspace(-1, 2, 500)  # 0.1 到 100 Hz
omega_eval = 2 * np.pi * freq
w, mag, phase = signal.bode(sys_tf, omega_eval)

J_id, fv_id, K_id = bode_identification(freq, mag, phase)
print(f"J_eff: 真实={J_true}, 辨识={J_id:.4f}")
print(f"f_v:   真实={fv_true}, 辨识={fv_id:.4f}")
print(f"K:     真实={K_true}, 辨识={K_id:.4f}")

5.5 频域最小二乘

对于多输入多输出(MIMO)系统,频域辨识可以这样做:

在每个激励频率 \(f_l\) 处,对输入 \(\hat{\tau}(f_l)\) 和输出 \(\hat{q}(f_l)\) 做 DFT,得到频域回归方程:

\[\hat{Y}(f_l) \pi_b = \hat{\tau}(f_l), \quad l = 1, \ldots, N_h\]

堆叠所有频率:

\[\begin{bmatrix} \hat{Y}(f_1) \\ \hat{Y}(f_2) \\ \vdots \\ \hat{Y}(f_{N_h}) \end{bmatrix} \pi_b = \begin{bmatrix} \hat{\tau}(f_1) \\ \hat{\tau}(f_2) \\ \vdots \\ \hat{\tau}(f_{N_h}) \end{bmatrix}\]

注意:DFT 结果是复数。可以将实部和虚部分开,得到实数最小二乘问题:

\[\begin{bmatrix} \text{Re}(\hat{Y}) \\ \text{Im}(\hat{Y}) \end{bmatrix} \pi_b = \begin{bmatrix} \text{Re}(\hat{\tau}) \\ \text{Im}(\hat{\tau}) \end{bmatrix}\]

频域 WLS 的权重可以自然地设为每个频率点的噪声方差的倒数——用多次重复实验估计。

⚠️ 常见陷阱

⚠️ 编程陷阱:DFT 频率分辨率不匹配激励频率

错误做法:激励频率 \(f_l\) 不是 DFT 频率网格 \(k f_s/N\) 的整数倍

现象:频谱泄漏——激励能量分散到多个频率 bin 中,FRF 估计出现偏差

根本原因:DFT 假设信号是周期的,只有当信号周期恰好是采样窗口长度的整数分之一时,频谱才是"干净"的

正确做法:确保激励周期 \(T_f\) 和采样窗口 \(T_{\text{window}} = N/f_s\) 满足 \(T_{\text{window}} = M \cdot T_f\)\(M\) 为正整数)。或者用 Hanning 窗减轻泄漏

💡 概念误区:认为"时域辨识和频域辨识结果应该相同"

新手想法:同样的数据,时域和频域应该给出相同的参数估计

实际上:只有在所有频率都等权重、且数据无噪声时两者才完全等价。频域方法的优势在于**选择性加权**——可以丢弃噪声严重的高频数据,或给谐振频率附近更高的权重

延伸:频域方法还有一个独特优势——通过观察**频率响应函数的相干性**(coherence),可以判断每个频率点的信噪比,自动决定加权

🧠 思维陷阱:对非线性系统盲目使用频域方法

新手想法:Bode 图辨识对所有系统都适用

实际上:频率响应函数假设系统是线性时不变的(LTI)。机器人动力学是强非线性的——\(M(q)\) 依赖构型,Coriolis 项是 \(\dot{q}\) 的二次。在固定构型 \(q_0\) 附近做小幅扰动时可以近似线性化,但全范围运动时 Bode 图辨识不适用

正确做法:频域方法用于单关节参数辨识(固定其他关节)或机电参数辨识(电机+减速器)。全身动力学参数仍然用时域线性参数化方法

练习

练习 5.1 ⭐:对一个已知的二阶系统(\(J = 0.3\) kg\(\cdot\)m\(^2\), \(f_v = 1.5\) N\(\cdot\)m\(\cdot\)s/rad, \(K = 50\) N\(\cdot\)m/rad),生成多正弦激励信号(\(N_h = 20\), Schroeder 相位),模拟系统响应(加噪声),然后用 Bode 图方法辨识参数。比较辨识值与真实值。

练习 5.2 ⭐⭐:实现频域最小二乘辨识。将 4.7 节的辨识流程代码改为频域版本——对力矩和回归矩阵做 DFT,只取前 \(N_h\) 个谐波频率处的数据做最小二乘。比较时域和频域方法的辨识精度。

练习 5.3 ⭐⭐⭐:对一个 7-DOF 机械臂,设计奇数次谐波多正弦激励。分析偶数次谐波处的响应能量,判断 Coulomb + viscous 摩擦模型是否充分描述了实际摩擦。


6. 子空间辨识 (N4SID / MOESP) ⭐⭐⭐

6.1 动机:不知道模型结构怎么办?

第 3--4 节的方法有一个前提:我们知道机器人的运动方程结构,只是不知道参数。但在很多场景下:

  • 柔性关节机器人的弹性参数未知,且结构复杂
  • 机器人与环境交互(如打磨、擦拭),接触力学未知
  • 你想对一个**黑箱**系统(如液压驱动器)建模

此时,我们需要一种**不假设模型结构**的辨识方法。子空间辨识就是这样的工具。

6.2 从输入输出到状态空间

考虑离散时间线性时不变系统:

\[x_{k+1} = Ax_k + Bu_k + w_k$$ $$y_k = Cx_k + Du_k + v_k\]

其中 \(x_k \in \mathbb{R}^n\) 是状态(未知),\(u_k \in \mathbb{R}^m\) 是输入,\(y_k \in \mathbb{R}^l\) 是输出,\(w_k, v_k\) 是过程噪声和测量噪声。

目标:从输入输出数据 \(\{u_k, y_k\}_{k=1}^{N}\) 直接估计 \((A, B, C, D)\) 和系统阶数 \(n\)

6.3 Hankel 矩阵构造

定义过去和未来的输入/输出 Hankel 矩阵。选取"窗口长度" \(i\)(通常 \(i > n\)),将数据排成块 Hankel 矩阵:

\[U_{0|2i-1} = \begin{bmatrix} u_0 & u_1 & \cdots & u_{j-1} \\ u_1 & u_2 & \cdots & u_j \\ \vdots & & & \vdots \\ u_{2i-1} & u_{2i} & \cdots & u_{2i+j-2} \end{bmatrix}\]

将此矩阵分为"过去"和"未来"两块:

\[U_p = U_{0|i-1}, \quad U_f = U_{i|2i-1}\]

类似地定义 \(Y_p, Y_f\)

6.4 SVD 揭示系统阶数

子空间方法的核心步骤是构造**斜投影**(oblique projection):

\[\mathcal{O}_i = Y_f /_{U_f} \begin{bmatrix} U_p \\ Y_p \end{bmatrix}\]

这个投影的物理含义是:从过去的输入输出中提取状态信息,去除未来输入的直接影响

\(\mathcal{O}_i\) 做 SVD:

\[\mathcal{O}_i = U_s \Sigma_s V_s^\top \approx U_n \Sigma_n V_n^\top\]

其中 \(\Sigma_s = \operatorname{diag}(\sigma_1, \sigma_2, \ldots)\)\(\sigma_1 \geq \sigma_2 \geq \cdots\)

系统阶数判定:观察奇异值的"间隙"(gap)。如果 \(\sigma_n \gg \sigma_{n+1} \approx 0\),则系统阶数为 \(n\)。保留前 \(n\) 个奇异值:

\[\mathcal{O}_i \approx U_n \Sigma_n V_n^\top\]

6.5 从 SVD 到状态空间矩阵

扩展可观性矩阵

\[\Gamma_i = U_n \Sigma_n^{1/2} = \begin{bmatrix} C \\ CA \\ CA^2 \\ \vdots \\ CA^{i-1} \end{bmatrix} \in \mathbb{R}^{li \times n}\]

\(\Gamma_i\) 可以直接提取 \(C\)(取前 \(l\) 行)和 \(A\)(利用 shift 性质 \(\underline{\Gamma}_i A = \overline{\Gamma}_i\),其中 \(\underline{\Gamma}_i\) 去掉最后 \(l\) 行、\(\overline{\Gamma}_i\) 去掉前 \(l\) 行):

\[A = \underline{\Gamma}_i^+ \overline{\Gamma}_i, \quad C = \Gamma_i(1:l, :)\]

状态序列\(\hat{X} = \Sigma_n^{1/2} V_n^\top\)

B, D 矩阵:做最小二乘

\[\begin{bmatrix} \hat{x}_{k+1} \\ y_k \end{bmatrix} = \begin{bmatrix} A & B \\ C & D \end{bmatrix} \begin{bmatrix} \hat{x}_k \\ u_k \end{bmatrix} + \text{residual}\]

6.6 三大变体的统一视角

方法 提出者 核心技术 加权方式 优点
N4SID Van Overschee & De Moor (1994) 斜投影 工具变量 + 加权 数值稳健
MOESP Verhaegen & Dewilde (1992) LQ/RQ 分解 行空间分离 计算高效
CVA Larimore (1990) 典型相关分析 典型相关权重 高斯下渐近有效

Van Overschee & De Moor (1995) 统一定理:三种方法的唯一区别在于对 \(\mathcal{O}_i\) 做 SVD 之前乘的**左右加权矩阵** \(W_1, W_2\)。统一形式:\(W_1 \mathcal{O}_i W_2 = U \Sigma V^\top\)

6.7 与 DMD/Koopman 的联系

动态模式分解(Dynamic Mode Decomposition, DMD)是流体力学社区发展的方法,本质上是子空间辨识的特例:

  • DMD:假设 \(x_{k+1} = Ax_k\)(无输入),从快照矩阵 \(X = [x_1, \ldots, x_{K-1}]\), \(X' = [x_2, \ldots, x_K]\)\(A = X' X^+\)
  • **带输入的 DMD(DMDc)**等价于子空间辨识的一步版本

**Koopman 算子理论**进一步将非线性系统嵌入到无穷维线性空间中:对可观函数 \(\phi(x)\),Koopman 算子 \(\mathcal{K}\) 满足 \(\mathcal{K}\phi = \phi \circ F\)\(F\) 是动力学映射)。有限维近似就变回了子空间辨识的设置——只是"状态"变成了提升空间(lifted space)中的特征。

实践含义:对非线性机器人(如柔性关节、接触),可以选择合适的可观函数(如 \([q, \dot{q}, \sin q, \cos q, q\dot{q}]\)),然后对提升数据做标准的子空间辨识。

# 子空间辨识的 Python 实现骨架
import numpy as np
from scipy.linalg import svd, lstsq

def n4sid(u_data, y_data, i, n_order):
    """N4SID 子空间辨识

    Args:
        u_data: 输入序列 (N, m)
        y_data: 输出序列 (N, l)
        i: 块行数 (> n_order)
        n_order: 期望的系统阶数

    Returns:
        A, B, C, D: 状态空间矩阵
    """
    N = len(u_data)
    m = u_data.shape[1] if u_data.ndim > 1 else 1
    l = y_data.shape[1] if y_data.ndim > 1 else 1
    j = N - 2*i + 1  # 列数

    # 构建 Hankel 矩阵
    def hankel_block(data, start, rows, cols):
        if data.ndim == 1:
            data = data.reshape(-1, 1)
        d = data.shape[1]
        H = np.zeros((rows * d, cols))
        for r in range(rows):
            for c in range(cols):
                H[r*d:(r+1)*d, c] = data[start + r + c]
        return H

    U_p = hankel_block(u_data, 0, i, j)
    U_f = hankel_block(u_data, i, i, j)
    Y_p = hankel_block(y_data, 0, i, j)
    Y_f = hankel_block(y_data, i, i, j)

    # 斜投影 (简化版: 用正交投影近似)
    W_p = np.vstack([U_p, Y_p])

    # 去除 U_f 的影响
    proj_Uf = U_f.T @ np.linalg.pinv(U_f @ U_f.T) @ U_f
    Y_f_perp = Y_f @ (np.eye(j) - proj_Uf.T)
    W_p_perp = W_p @ (np.eye(j) - proj_Uf.T)

    O_i = Y_f_perp @ np.linalg.pinv(W_p_perp) @ W_p

    # SVD
    U_svd, S_svd, Vt_svd = svd(O_i)

    # 截断
    U_n = U_svd[:, :n_order]
    S_n = np.diag(S_svd[:n_order])
    V_n = Vt_svd[:n_order, :]

    # 扩展可观性矩阵
    Gamma = U_n @ np.sqrt(S_n)

    # 提取 C
    C = Gamma[:l, :]

    # 提取 A (shift property)
    Gamma_up = Gamma[:-l, :]    # 去掉最后 l 行
    Gamma_down = Gamma[l:, :]   # 去掉前 l 行
    A = np.linalg.lstsq(Gamma_up, Gamma_down, rcond=None)[0]

    # 状态序列
    X_hat = np.sqrt(S_n) @ V_n

    # 估计 B, D
    # x_{k+1} = A x_k + B u_k
    # y_k = C x_k + D u_k
    n_data = min(X_hat.shape[1] - 1, j - 1)

    LHS = np.vstack([X_hat[:, 1:n_data+1], y_data[i:i+n_data].T])
    RHS = np.vstack([X_hat[:, :n_data], u_data[i:i+n_data].T])

    ABCD = LHS @ np.linalg.pinv(RHS)

    B = ABCD[:n_order, n_order:]
    D = ABCD[n_order:, n_order:]

    return A, B, C, D

# 使用 scipy 的 N4SID (更工业化)
# from sippy import system_identification
# sys_id = system_identification(y_data, u_data, 'N4SID', SS_order=n)

⚠️ 常见陷阱

⚠️ 编程陷阱:Hankel 矩阵窗口长度 \(i\) 选取不当

错误做法\(i\) 选得太小(\(i \leq n\),其中 \(n\) 是系统阶数)

现象:SVD 奇异值没有明显间隙,辨识出的系统矩阵数值不稳定

根本原因\(i\) 必须大于系统阶数 \(n\),否则扩展可观性矩阵 \(\Gamma_i\) 不满秩。但 \(i\) 太大(\(i \gg n\))会导致 Hankel 矩阵列数减少(\(j = N - 2i + 1\)),统计效率下降

正确做法:经验法则 \(i \approx 2n\)\(3n\)。如果 \(n\) 未知,从大的 \(i\) 开始,逐渐减小,观察奇异值间隙

💡 概念误区:认为子空间辨识可以直接用于机器人动力学参数辨识

新手想法:子空间方法不需要模型结构,可以直接辨识机器人

实际上:子空间方法辨识的是线性时不变系统的 \((A, B, C, D)\) 矩阵——机器人是非线性的,需要先在工作点线性化。辨识出的矩阵只在该工作点附近有效。而且 \((A, B, C, D)\) 是一个整体,不容易分解为物理参数(质量、惯性矩等)

正确用途:子空间方法适用于 (1) 柔性关节的弹性模态辨识;(2) 未知黑箱子系统(液压、气动);(3) 控制器设计不需要物理参数时(直接用状态空间做 LQR/MPC)

🧠 思维陷阱:认为系统阶数选择只看奇异值间隙

新手想法:奇异值间隙明显,阶数就确定了

实际上:噪声会模糊间隙。多种方法应该交叉验证:(1) 奇异值间隙;(2) 不同阶数下的模型在验证数据上的拟合度(BIC/AIC 准则);(3) 系统极点的稳定性(随阶数增加,物理极点应不变,虚假极点会变化)

正确做法:画出"稳定图"(stabilization diagram)——横轴是阶数,纵轴是极点位置,稳定极点(多个阶数都出现的)是真实的

⭐ 自测检查点 6.1:子空间辨识的核心步骤是对 Hankel 矩阵做 SVD。解释:(1) 奇异值的"间隙"为什么能指示系统阶数?(2) 如果没有明显间隙,说明什么?

:(1) 在无噪声情况下,扩展可观性矩阵的秩恰好等于系统阶数 \(n\),所以只有前 \(n\) 个奇异值非零。有噪声时,非零奇异值会有小的扰动,但前 \(n\) 个仍然显著大于后面的——这就是"间隙"。(2) 没有明显间隙可能意味着:系统阶数难以确定(需要更多数据),或者系统是非线性的(线性模型不适用),或者信噪比太低。

练习

练习 6.1 ⭐⭐:用 N4SID 辨识一个弹簧-阻尼-质量系统(\(m = 1\) kg, \(c = 0.5\) N\(\cdot\)s/m, \(k = 10\) N/m)。生成仿真数据(白噪声激励),验证辨识出的极点与理论值一致。

练习 6.2 ⭐⭐:比较 N4SID 和 MOESP 在同一组数据上的辨识结果。两者的 \((A, B, C, D)\) 不同,但传递函数应该相同(相似变换)——验证这一点。

练习 6.3 ⭐⭐⭐:对 2-DOF 机械臂(3.2 节),在某个固定构型附近线性化,然后用子空间方法辨识。将辨识出的 \(A\) 矩阵的特征值与理论线性化模型的特征值对比。


7. 递归/在线辨识 ⭐⭐

7.1 动机:为什么需要在线辨识?

离线辨识(batch identification)需要先采集完所有数据再计算。但在很多场景下,参数是**时变的**:

  • 机器人抓起一个未知负载,惯性突然改变
  • 减速器磨损,摩擦系数缓慢漂移
  • 温度变化导致连杆热膨胀

我们需要一种**每收到一个新数据点就更新参数估计**的方法——这就是递归辨识。

7.2 RLS(递归最小二乘)的完整推导

出发点:batch OLS 的正规方程

\[\hat{\pi}_k = \left(\sum_{i=1}^{k} Y_i^\top Y_i\right)^{-1} \left(\sum_{i=1}^{k} Y_i^\top \tau_i\right)\]

定义:\(P_k = \left(\sum_{i=1}^{k} Y_i^\top Y_i\right)^{-1}\)(参数协方差矩阵的比例因子)。

递推关系:当第 \(k+1\) 个数据 \((Y_{k+1}, \tau_{k+1})\) 到来时:

\[P_{k+1}^{-1} = P_k^{-1} + Y_{k+1}^\top Y_{k+1}\]

直接对 \(P_k^{-1}\) 递推需要矩阵求逆——计算量 \(O(p^3)\)。利用 矩阵求逆引理(Woodbury identity):

\[(A + BCD)^{-1} = A^{-1} - A^{-1}B(C^{-1} + DA^{-1}B)^{-1}DA^{-1}\]

\(A = P_k^{-1}\), \(B = Y_{k+1}^\top\), \(C = I\), \(D = Y_{k+1}\),得到:

\[P_{k+1} = P_k - P_k Y_{k+1}^\top (I + Y_{k+1} P_k Y_{k+1}^\top)^{-1} Y_{k+1} P_k\]

定义**增益矩阵**:

\[K_{k+1} = P_k Y_{k+1}^\top (I + Y_{k+1} P_k Y_{k+1}^\top)^{-1}\]

则协方差更新为:

\[P_{k+1} = (I - K_{k+1} Y_{k+1}) P_k\]

参数更新为:

\[\hat{\pi}_{k+1} = \hat{\pi}_k + K_{k+1}(\tau_{k+1} - Y_{k+1}\hat{\pi}_k)\]

完整的 RLS 算法

\[\boxed{ \begin{aligned} & \text{预测误差:} & e_{k+1} &= \tau_{k+1} - Y_{k+1}\hat{\pi}_k \\ & \text{增益:} & K_{k+1} &= P_k Y_{k+1}^\top (I + Y_{k+1} P_k Y_{k+1}^\top)^{-1} \\ & \text{参数更新:} & \hat{\pi}_{k+1} &= \hat{\pi}_k + K_{k+1} e_{k+1} \\ & \text{协方差更新:} & P_{k+1} &= (I - K_{k+1} Y_{k+1}) P_k \end{aligned} }\]

初始化\(\hat{\pi}_0 = 0\)(或 CAD 标称值),\(P_0 = \alpha I\)\(\alpha\) 取较大值,如 \(10^4\))。

计算复杂度:每步 \(O(p^2)\)\(p\) 是参数个数),比 batch OLS 的 \(O(Kp^2 + p^3)\) 好得多(对于在线应用)。

7.3 遗忘因子

标准 RLS 赋予所有历史数据相同权重。对时变参数,旧数据应该"淡忘"。引入**遗忘因子** \(\lambda \in (0, 1]\)

\[J_k(\pi) = \sum_{i=1}^{k} \lambda^{k-i} \|\tau_i - Y_i \pi\|^2\]

修改后的 RLS 更新:

\[K_{k+1} = P_k Y_{k+1}^\top (\lambda I + Y_{k+1} P_k Y_{k+1}^\top)^{-1}$$ $$\hat{\pi}_{k+1} = \hat{\pi}_k + K_{k+1}(\tau_{k+1} - Y_{k+1}\hat{\pi}_k)$$ $$P_{k+1} = \frac{1}{\lambda}(I - K_{k+1} Y_{k+1}) P_k\]

\(\lambda\) 的选择

\(\lambda\) 等效记忆长度 \(\approx 1/(1-\lambda)\) 适用场景
1.0 \(\infty\)(不遗忘) 恒定参数
0.99 100 步 缓慢漂移
0.95 20 步 快速变化
0.90 10 步 突变检测

7.4 RLS 与 Kalman 滤波的等价性

惊人的事实:RLS 可以看作一个特殊的 Kalman 滤波器。

将辨识问题建模为:

\[\text{状态方程:} \quad \pi_{k+1} = \pi_k \quad (\text{恒定参数} \to \text{恒等动力学})$$ $$\text{观测方程:} \quad \tau_k = Y_k \pi_k + \epsilon_k, \quad \epsilon_k \sim \mathcal{N}(0, R_k)\]

这是一个**线性高斯状态估计问题**——Kalman 滤波给出最优解。Kalman 滤波的更新步骤恰好就是 RLS(\(F = I\)\(H_k = Y_k\)\(Q = 0\)):

Kalman 滤波 RLS
状态 \(\hat{x}_k\) 参数 \(\hat{\pi}_k\)
状态协方差 \(P_k\) 参数协方差 \(P_k\)
观测矩阵 \(H_k\) 回归向量 \(Y_k\)
过程噪声 \(Q\) \(0\)(恒定参数)或 \((1/\lambda - 1)P_k\)(遗忘因子)
Kalman 增益 \(K_k\) RLS 增益 \(K_k\)

推论:如果参数本身是随机游走 \(\pi_{k+1} = \pi_k + w_k\)\(w_k\) 代表参数漂移),那么 Kalman 滤波(\(Q \neq 0\))是比带遗忘因子 RLS 更好的选择——它可以**分别调整每个参数方向的遗忘速率**。

7.5 与自适应控制 (控制理论·自适应控制(规划中)) 的无缝对接

RLS 在线辨识是自适应控制的核心组件。以 Certainty Equivalence 自适应控制为例:

  1. 辨识:每一步用 RLS 更新参数估计 \(\hat{\pi}_k\)
  2. 控制:假设 \(\hat{\pi}_k\) 就是真实参数,设计控制律(如 computed torque)
\[\tau_k = Y(q_k, \dot{q}_k, \dot{q}_{r,k}, \ddot{q}_{r,k})\hat{\pi}_k - K_D(q_k - q_{r,k})\]

这就是 间接自适应控制(indirect adaptive control)。与 2.4 节中 Slotine & Li 的**直接自适应控制**的区别:

直接自适应 (Slotine & Li) 间接自适应 (RLS + CE)
参数更新律 梯度下降式 \(\dot{\hat{\pi}} = -\Gamma Y^\top s\) RLS(最优滤波)
稳定性保证 Lyapunov 直接证明 需要 PE 条件(持续激励)
收敛速度 慢(梯度法) 快(二阶方法,等价于 Newton)
适用范围 非线性系统(含 Coriolis) 需要参数分离性(regressor 形式)
实践难度 \(\Gamma\) 调参困难 \(\lambda\), \(P_0\) 调参较直观

关键联系:两种方法都依赖**同一个回归矩阵 \(Y\)**——本章 3--4 节的构造方法是共通的基础。

7.6 应用:在线负载估计

一个经典应用:机器人抓取未知负载后,在线估计负载的质量、质心和惯性

负载的效果等价于修改末端连杆的惯性参数。设负载参数为 \(\pi_{\text{load}} = [m_L, m_L c_{x,L}, m_L c_{y,L}, m_L c_{z,L}, I_{xx,L}, \ldots]^\top\),则:

\[Y_{\text{robot}}\pi_{\text{robot}} + Y_{\text{load}}\pi_{\text{load}} = \tau\]

如果 \(\pi_{\text{robot}}\) 已经离线辨识好,残差 \(\tau - Y_{\text{robot}}\pi_{\text{robot}} = Y_{\text{load}}\pi_{\text{load}}\) 就可以用 RLS 在线估计 \(\pi_{\text{load}}\)

7.7 C++ Eigen 实现:RLS(正确 vs 错误 vs 对比)

Step 1: 为什么 RLS 在实时控制中比 batch OLS 更合适?

Batch OLS 需要存储所有历史回归矩阵(内存 \(O(Kp)\),计算 \(O(Kp^2)\))。在 1 kHz 控制循环中,每秒产生 1000 行数据,一分钟就是 60000 行——存储和计算都不可行。RLS 只维护 \(p \times p\) 的协方差矩阵和 \(p \times 1\) 的参数向量,每步 \(O(p^2)\),内存恒定。

Step 2: 正确写法

// ✅ 正确: 工业级 RLS 实现
#include <Eigen/Dense>

class RecursiveLeastSquares {
public:
    RecursiveLeastSquares(int n_params, double alpha = 1e4, double lambda = 0.99)
        : n_(n_params), lambda_(lambda)
    {
        // ✅ 显式初始化所有成员
        theta_ = Eigen::VectorXd::Zero(n_);
        P_ = alpha * Eigen::MatrixXd::Identity(n_, n_);
    }

    void update(const Eigen::MatrixXd& Y, const Eigen::VectorXd& tau) {
        // 预测误差
        Eigen::VectorXd e = tau - Y * theta_;

        // 增益矩阵
        Eigen::MatrixXd S = lambda_ * Eigen::MatrixXd::Identity(Y.rows(), Y.rows())
                            + Y * P_ * Y.transpose();
        Eigen::MatrixXd K = P_ * Y.transpose() * S.inverse();

        // 参数更新
        theta_ += K * e;

        // ✅ 协方差更新 (Joseph 形式, 数值更稳定)
        Eigen::MatrixXd I_KY = Eigen::MatrixXd::Identity(n_, n_) - K * Y;
        P_ = (1.0 / lambda_) * (I_KY * P_ * I_KY.transpose()
              + K * Eigen::MatrixXd::Identity(Y.rows(), Y.rows()) * K.transpose());

        // ✅ 强制对称性 (防止数值漂移)
        P_ = 0.5 * (P_ + P_.transpose());
    }

    const Eigen::VectorXd& getParams() const { return theta_; }
    const Eigen::MatrixXd& getCovariance() const { return P_; }

    Eigen::VectorXd getStdDev() const {
        return P_.diagonal().cwiseSqrt();
    }

    // ✅ 置信区间检查
    bool isConverged(double threshold = 0.01) const {
        Eigen::VectorXd rel_std = getStdDev().array() / theta_.array().abs().max(1e-10);
        return (rel_std.array() < threshold).all();
    }

private:
    int n_;
    double lambda_;
    Eigen::VectorXd theta_;
    Eigen::MatrixXd P_;
};

Step 3: 错误写法及分析

// ❌ 错误 1: 使用简单形式的协方差更新
void update_WRONG_simple(const Eigen::MatrixXd& Y, const Eigen::VectorXd& tau) {
    Eigen::VectorXd e = tau - Y * theta_;
    Eigen::MatrixXd K = P_ * Y.transpose() * 
        (lambda_ * Eigen::MatrixXd::Identity(Y.rows(), Y.rows()) + Y * P_ * Y.transpose()).inverse();
    theta_ += K * e;
    // ❌ 简单形式: P = (I - KY)P / lambda
    // 由于浮点舍入, P 会逐渐变得不对称, 然后不正定, 最终算法崩溃
    P_ = (1.0 / lambda_) * (Eigen::MatrixXd::Identity(n_, n_) - K * Y) * P_;
}

// ❌ 错误 2: 遗忘因子导致协方差爆炸, 无保护
void update_WRONG_no_covariance_bound(const Eigen::MatrixXd& Y, const Eigen::VectorXd& tau) {
    // 当 Y ≈ 0 (无激励) 时, P = P / lambda 指数增长!
    // 没有上界保护, P 的元素可能到 1e100, 下一次激励时参数跳变
    Eigen::VectorXd e = tau - Y * theta_;
    Eigen::MatrixXd K = P_ * Y.transpose() * 
        (lambda_ * Eigen::MatrixXd::Identity(Y.rows(), Y.rows()) + Y * P_ * Y.transpose()).inverse();
    theta_ += K * e;
    Eigen::MatrixXd I_KY = Eigen::MatrixXd::Identity(n_, n_) - K * Y;
    P_ = (1.0 / lambda_) * (I_KY * P_ * I_KY.transpose()
          + K * Eigen::MatrixXd::Identity(Y.rows(), Y.rows()) * K.transpose());
    // 缺少: P_ = P_.cwiseMin(max_cov); 或 trace-bounded 修正
}

// ❌ 错误 3: 初始化 P0 太小
RecursiveLeastSquares rls_wrong(10, 0.01, 0.99);
// P0 = 0.01 * I -> 增益 K 很小 -> 参数更新极慢 -> 可能需要 10000 步才收敛
// 正确: P0 应为 1e3 ~ 1e5 * I (表示"对初始参数不确定")

Step 4: 不同实现策略对比

策略 数值稳定性 每步计算量 适用场景
Joseph 形式 + 对称化 \(O(p^2 + mp)\) 通用推荐
简单形式 \(O(p^2)\) 仅用于短期运行
Square-root RLS 最高 \(O(p^2)\) 高精度需求
UD 分解 RLS \(O(p^2)\) 嵌入式系统
// 使用示例: 7-DOF 机械臂在线负载估计
/*
// 已知: 机器人基参数 pi_robot (离线辨识)
// 目标: 在线估计负载参数 pi_load (10 个: m, mc_x, mc_y, mc_z, Ixx,...,Izz)

RecursiveLeastSquares rls(10, 1e4, 0.995);

// 控制循环 (1 kHz)
for (int k = 0; k < N_steps; ++k) {
    // 读取传感器
    Eigen::VectorXd q = readEncoders();
    Eigen::VectorXd v = readVelocities();
    Eigen::VectorXd a = computeAcceleration(v);  // 滤波 + 微分
    Eigen::VectorXd tau = readTorqueSensors();

    // 计算已知机器人部分的力矩
    Eigen::VectorXd tau_robot = pinocchio::rnea(model, data, q, v, a);

    // 残差 = 负载贡献
    Eigen::VectorXd tau_load = tau - tau_robot;

    // 负载回归矩阵 (只对末端连杆)
    Eigen::MatrixXd Y_load = computeEndEffectorRegressor(model, data, q, v, a);

    // RLS 更新
    rls.update(Y_load, tau_load);

    // 读取估计结果
    double load_mass = rls.getParams()(0);
    double load_mass_std = rls.getStdDev()(0);

    if (k % 100 == 0) {
        std::cout << "Step " << k 
                  << " | Load mass: " << load_mass 
                  << " +/- " << 2*load_mass_std << " kg" << std::endl;
    }
}
*/

7.8 实际案例:四足机器人在线质量估计

四足机器人在搬运货物时需要实时估计负载质量以调整步态。这是 RLS 的一个重要应用场景。

问题设置:四足机器人站立状态下,总的垂直支撑力等于总重力:

\[\sum_{i=1}^{4} F_{z,i} = (m_{\text{robot}} + m_{\text{load}}) g\]

其中 \(F_{z,i}\) 是第 \(i\) 条腿的垂直地面反作用力(可从关节力矩和运动学计算得到)。

更一般地,在运动中:

\[Y_{\text{robot}}(q, \dot{q}, \ddot{q})\pi_{\text{robot}} + Y_{\text{load}}(q, \dot{q}, \ddot{q})\pi_{\text{load}} = \tau\]

简化模型(只估计负载质量 \(m_L\) 和质心位置 \(\mathbf{c}_L\)):

\[\pi_{\text{load}} = [m_L, m_L c_{x,L}, m_L c_{y,L}, m_L c_{z,L}]^\top\]

这是 4 个参数,RLS 可以在 50--200 步内收敛(在 1 kHz 控制循环中,仅需 50--200 ms)。

⚠️ 常见陷阱

⚠️ 编程陷阱:遗忘因子导致协方差爆炸

错误做法\(\lambda = 0.95\) 但机器人长时间保持静止(无激励)

现象\(P\) 的 trace 从初始的 \(10^4\) 增长到 \(10^{15}\),下一次运动时参数估计"跳"到荒谬的值

根本原因\(\lambda < 1\)\(Y_{k+1} \approx 0\) 时,\(P_{k+1} = P_k / \lambda\)——协方差指数增长。增益 \(K\) 随之暴增,导致参数跳变

正确做法: - 使用**可变遗忘因子**:\(\lambda_k = 1 - (1-\lambda_0)\|Y_k P_k Y_k^\top\|_F / \text{threshold}\) - 协方差上限截断:if \(\text{trace}(P) > P_{\max}\) then \(P \leftarrow P_{\max}/\text{trace}(P) \cdot P\) - 改用 directional forgetting(只在受激励的方向上遗忘)

💡 概念误区:认为 RLS 的初始参数 \(\hat{\pi}_0\) 不重要

新手想法:反正 RLS 会收敛,初始值随便设

实际上:如果 \(\hat{\pi}_0\) 偏离真实值太远,前几步的控制力矩可能很大(因为 computed torque 控制律直接使用 \(\hat{\pi}_k\)),导致机器人抖动甚至不稳定。应该用 CAD 标称值初始化

正确做法\(\hat{\pi}_0\) = CAD 标称值(或离线辨识值),\(P_0\) 的对角元设为参数不确定度的平方

🧠 思维陷阱:认为"持续激励条件"(PE) 自动满足

新手想法:机器人在运动中就自然满足 PE 条件

实际上:PE 要求 \(\sum_{i=k-T}^{k} Y_i^\top Y_i \succ \alpha I\)(某个窗口内的信息矩阵正定)。如果机器人只做重复运动(如工业流水线上的 pick-and-place),某些参数方向可能始终不被激励——RLS 在这些方向上不收敛

正确做法:(1) 检查 \(P_k\) 的最大特征值——如果某个方向长期不减小,说明该方向缺乏激励;(2) 定期注入激励信号(dither signal)——在期望轨迹上叠加小幅随机扰动

连接 控制理论·自适应控制(规划中):PE 条件是自适应控制收敛性证明的核心假设。本章的 RLS 和 控制理论·自适应控制(规划中) 的自适应律都需要它

⭐ 自测检查点 7.1:RLS 的协方差更新中,为什么使用 Joseph 形式 \((I - KY)P(I-KY)^\top + KK^\top\) 而非简单形式 \((I-KY)P\)

:简单形式虽然数学上等价,但由于浮点舍入误差的累积,\(P\) 可能逐渐变得不对称或不正定——一旦不正定,整个算法就崩溃了。Joseph 形式**自动保持 \(P\) 的对称正定性**,代价是多做一次矩阵乘法。

练习

练习 7.1 ⭐:实现 RLS 并在 2-DOF 机械臂上测试。在 \(t = 5\) s 时突然改变连杆 2 的质量(模拟抓取负载),观察参数估计的收敛速度。比较 \(\lambda = 0.99, 0.995, 0.999\) 的效果。

练习 7.2 ⭐⭐:实现可变遗忘因子 RLS。在机器人静止阶段自动切换到 \(\lambda = 1\)(不遗忘),运动阶段恢复 \(\lambda < 1\)。验证这是否解决了协方差爆炸问题。

练习 7.3 ⭐⭐:用 Kalman 滤波实现在线辨识,设过程噪声 \(Q = \text{diag}(\sigma_q^2)\)(每个参数独立漂移)。比较 Kalman 滤波和 RLS(遗忘因子)在参数突变和缓慢漂移两种场景下的表现。

练习 7.4 ⭐⭐⭐:在四足机器人仿真中(PyBullet 或 MuJoCo),实现在线负载估计。机器人行走时在背上放置不同质量的负载(0.5, 1.0, 2.0 kg),用 RLS 实时估计负载质量。绘制估计值随时间的收敛曲线。


8. 子空间辨识与数据驱动方法 ⭐⭐⭐

8.1 动机:当你不知道模型结构

前面所有方法都假设你**已经知道**系统的结构(刚体动力学方程),只是不知道参数值。但现实中很多情况下:

  • 柔性机器人的柔性建模非常复杂,你不确定用几阶模态
  • 液压驱动系统的非线性摩擦模型未知
  • 机器人与环境交互时,环境动力学完全未知

这时需要**黑箱辨识**:直接从 I/O 数据辨识状态空间模型 \((A, B, C, D)\),不假设任何物理结构。

8.2 子空间辨识 (N4SID/MOESP) 核心思想

核心思想:利用 Hankel 矩阵的列空间结构,从 I/O 数据直接提取系统的可观测子空间。

输入输出 Hankel 矩阵: $\(\mathcal{H} = \begin{bmatrix} Y_p \\ Y_f \\ U_p \\ U_f \end{bmatrix}\)$

其中 \(Y_p, Y_f\) 是"过去"和"未来"输出的 Hankel 块,\(U_p, U_f\) 类似。

关键定理(Van Overschee-De Moor 1996):对 LTI 系统,\(Y_f\)\(U_f\) 消去后的列空间的左奇异向量包含可观测矩阵 \(\mathcal{O} = [C; CA; CA^2; \ldots]\),奇异值揭示系统阶数 \(n\)

算法步骤: 1. 构建 Hankel 矩阵 2. 做 oblique/orthogonal 投影消去输入影响 3. SVD 确定系统阶数 \(n\)(奇异值 gap) 4. 从左奇异向量提取 \(\mathcal{O}\),进而得到 \((A, C)\) 5. 用最小二乘从状态序列估计 \((B, D)\)

8.3 与 DeePC 的深层联系

回顾 §3.13.D(鲁棒 MPC 专题):DeePC 利用 Willems 基本引理,用 Hankel 矩阵的列空间**直接**预测未来输出,跳过了显式辨识。

方法 步骤 优势 劣势
子空间辨识 + MPC 数据 → 辨识 \((A,B,C,D)\) → 设计 MPC 可解释、可验证 两步误差累积
DeePC 数据 → 直接预测控制 一步到位、简单 不可解释、线性限制

等价性定理:对确定性 LTI 系统,DeePC 与"子空间辨识 + 基于辨识模型的 MPC"给出**完全相同**的控制输入。两者的区别只在于噪声处理方式不同。

8.4 Koopman 算子辨识(2020-2025 前沿)

动机:子空间辨识只适用于线性系统。对非线性系统,能否也用数据驱动方法直接辨识一个"线性"模型?

Koopman 算子(1931 年提出,2010 年后复兴):对非线性系统 \(x_{k+1} = f(x_k)\),存在无穷维线性算子 \(\mathcal{K}\) 作用于**观测函数空间**:

\[\mathcal{K} g = g \circ f, \quad (\mathcal{K}g)(x) = g(f(x))\]

有限维近似:选择字典函数 \(\psi(x) = [\psi_1(x), \ldots, \psi_L(x)]^\top\)(如多项式、RBF、Fourier),学习有限维 Koopman 矩阵 \(K \in \mathbb{R}^{L \times L}\)

\[\psi(x_{k+1}) \approx K \psi(x_k) + B_K u_k\]

Extended DMD (EDMD):最简单的学习方法 $\(K = \Psi_+ \Psi^+, \quad \Psi = [\psi(x_1), \ldots, \psi(x_N)]\)$

其中 \(\Psi_+ = [\psi(x_2), \ldots, \psi(x_{N+1})]\)

与机器人辨识的联系: - 四足机器人的步态切换:不同步态阶段用不同 Koopman 模型 - 柔性机器人:Koopman 自动提取柔性模态 - 接触动力学:接触-非接触切换对 Koopman 是挑战(需要 switching Koopman)

Brunton 2022 综述(SIAM Review)的核心观点:Koopman 辨识是**非线性系统辨识的未来范式之一**——它把"非线性"转化为"高维线性",使得所有线性控制工具(LQR、Kalman、MPC)都可以直接用。代价是维度 \(L\) 可能很大(100-1000 维)。

8.5 与自适应控制的联系

子空间/Koopman 辨识给出的线性模型可以直接用于: - Model Reference Adaptive Control (MRAC):用辨识模型作为参考模型 - L1 Adaptive Control:辨识提供的名义模型 + 自适应补偿未建模动力学 - Indirect Adaptive MPC:每隔 \(T_{\text{id}}\) 步重新辨识,更新 MPC 模型

跨领域类比:子空间辨识之于系统辨识,就像 PCA 之于统计学——都是"从高维数据中提取低维结构"的方法。子空间辨识的"系统阶数"对应 PCA 的"主成分个数",两者都通过"奇异值 gap"确定。

⚠️ 常见陷阱

💡 概念误区:认为 Koopman 辨识"把非线性系统变成了线性系统"

新手想法:"学到了 Koopman 矩阵 \(K\) 后,我的系统就是线性的了,所有线性工具都能用!"

实际上:Koopman **不改变**系统本身——它只是在更高维空间中找了一个线性表示。(1) 近似是有误差的(有限维截断);(2) 提升空间的状态 \(\psi(x)\) 需要通过非线性变换从物理状态 \(x\) 获取;(3) 控制设计在提升空间中是线性的,但映射回物理空间可能有约束处理问题

正确认知:Koopman 是一种**近似**工具,适合"弱非线性"或"可以用适当字典捕捉"的系统。强非线性、混杂动力学仍需要专门方法

🧠 思维陷阱:认为"数据驱动方法不需要物理知识"

新手想法:"DeePC/Koopman 都是 data-driven,所以不需要了解系统物理"

实际上:数据驱动方法的成功**高度依赖**数据质量和激励设计,而好的激励设计需要物理直觉。例如:辨识腿足机器人需要在不同步态、不同速度下采集数据;辨识柔性臂需要激励低频模态。没有物理知识,你连"什么算好的激励"都不知道

正确认知:数据驱动方法是"物理知识 + 数据"的组合,不是"无需物理"的黑魔法

练习

练习 8.1 ⭐⭐:用 Python sklearnPCA 对一组机器人 I/O 数据做奇异值分析,确定系统阶数。比较 SVD gap 方法与 AIC/BIC 准则给出的阶数。

练习 8.2 ⭐⭐⭐:对一个简单的非线性系统(如 van der Pol 振荡器),实现 EDMD。选择多项式字典 \(\psi(x) = [1, x_1, x_2, x_1^2, x_1 x_2, x_2^2, \ldots]\)(阶 ≤ 3),验证 Koopman 近似在多大范围内有效。

练习 8.3 ⭐⭐⭐(跨章综合):结合本章 §4(激励轨迹设计)和 §3.13.D(DeePC),设计一个完整的数据驱动控制流程:(1) 设计最优激励轨迹;(2) 用激励数据构建 Hankel 矩阵;(3) 实现 DeePC 控制器;(4) 在仿真中验证跟踪性能。


本章常见误解汇总

误解 正确理解
辨识出的惯性参数就是物体的真实物理参数 辨识得到的是基参数(base parameters)——多个物理参数的线性组合;由于参数耦合(如连杆质量和相邻关节的电机惯量),单个物理参数通常不可辨识
随便给一条运动轨迹就能做辨识 激励轨迹必须满足持续激励条件(回归矩阵 \(\Phi\) 列满秩),否则参数不可辨识;D-最优设计通过最大化 \(\det(\Phi^\top \Phi)\) 来优化激励信号
辨识精度只取决于数据量 关节速度和加速度的数值微分引入巨大噪声,低通滤波器的截止频率选择和采样率对精度的影响远大于数据量
子空间辨识(N4SID)可以精确辨识非线性系统 子空间辨识假设系统是线性时不变(LTI),对非线性系统只能在工作点附近给出局部线性模型;Koopman/EDMD 方法通过升维将非线性"提升"为高维线性,但有效范围仍有限
在线辨识(RLS)可以无限期运行而不退化 不带遗忘因子的 RLS 随时间推移协方差矩阵趋零("估计器冻结"),失去对新数据的响应能力;必须使用遗忘因子或协方差重置机制
辨识出的模型可以直接用于控制而无需验证 辨识模型可能在训练数据范围外失效(外推不可靠),且可能不满足物理一致性(如负质量、不对称惯量张量);部署前需交叉验证和物理约束检查
频域辨识和时域辨识给出相同的结果 两者在理想条件下渐近等价,但在有限数据/有色噪声下结果可能不同;频域方法对周期噪声更鲁棒,时域方法对瞬态响应建模更直接

本章小结

方法 核心思想 适用场景 数学工具 与控制的联系
OLS/WLS 线性回归 离线辨识已知结构 最小二乘 computed torque
基参数 找可辨识组合 减少参数维度 QR 分解/SVD 所有模型控制
激励轨迹 D-最优设计 最大化信息 优化/Fourier 提高辨识精度
频域辨识 Bode 图拟合 单入单出 FFT/传递函数 经典控制
RLS 递归更新 在线/实时 Kalman 滤波 自适应控制
子空间辨识 黑箱状态空间 未知结构系统 SVD/投影 MPC
Koopman 非线性→高维线性 弱非线性 DMD/EDMD 线性 MPC
DeePC 数据直接预测 无模型控制 Hankel/引理 数据驱动 MPC

故障排查手册

症状 可能原因 排查步骤 相关节
OLS 残差很大但不收敛 激励不足或模型结构错误 1.检查 \(Y^\top Y\) 条件数;2.增加 Fourier 阶数;3.检查是否遗漏摩擦项 §4, §3.4
辨识参数物理不合理(负质量) 噪声/数值问题 1.加物理约束(NNLS);2.增加数据量;3.检查加速度滤波 §5.3
RLS 参数突然跳变 协方差爆炸或突然激励 1.检查 \(\text{trace}(P)\);2.加协方差上限;3.改用可变遗忘因子 §7.6
子空间辨识阶数不确定 SVD gap 不明显 1.增加数据长度;2.降低噪声(均值滤波);3.用 AIC/BIC 交叉验证 §8.2
辨识后仿真轨迹发散 辨识模型不稳定 1.检查 \(A\) 特征值;2.加稳定性约束;3.用频域方法交叉验证 §6

累积项目:本章新增模块

在"从零构建机械臂控制器"累积项目中,本章新增: 1. 离线辨识模块:Fourier 激励 + OLS/WLS + 基参数提取 2. 在线辨识模块:RLS 负载估计 + 可变遗忘因子 3. 验证模块:用辨识参数做 computed torque 控制,与 CAD 参数对比

项目代码位于 projects/manipulator_control/sysid/


9. 频域辨识方法 ⭐⭐

9.1 动机:为什么需要频域视角

时域辨识(OLS/WLS/RLS)是机器人辨识的主流方法,但频域方法在某些场景下有独特优势:

场景 1:柔性机器人。柔性关节/连杆有多个谐振频率,在频域中表现为 Bode 图的峰值。时域方法需要猜测模态数量,频域方法可以直接从 Bode 图"看到"有几个模态。

场景 2:执行器建模。电机+减速器+传感器的组合传递函数在频域中清晰可见——低频增益、带宽、谐振、高频衰减一目了然。

场景 3:控制器设计验证。闭环带宽、相位裕度等经典控制指标是频域概念。如果辨识目的是设计频域控制器(如 \(H_\infty\)、loop shaping),直接在频域辨识更自然。

9.2 多正弦激励与 DFT

多正弦信号: $\(u(t) = \sum_{k=1}^{K} A_k \sin(2\pi f_k t + \phi_k)\)$

选择频率 \(\{f_1, \ldots, f_K\}\) 对数等间距覆盖感兴趣的频带(如 0.1 Hz - 100 Hz),幅值 \(A_k\) 按 Schroeder 相位设计使信号峰值因子最小。

频率响应估计(非参数化): $\(G(j\omega_k) = \frac{Y(j\omega_k)}{U(j\omega_k)}\)$

其中 \(Y, U\) 是输出和输入的 DFT。多周期平均可显著降低噪声影响。

9.3 与时域方法的对比

维度 时域 (OLS/WLS) 频域 (Bode 辨识)
模型结构 需要已知(如刚体动力学) 可以非参数化
噪声处理 需要加速度估计(放大噪声) 频率选择性强(抗噪)
非线性处理 直接处理(线性参数化) 需要小信号线性化
多输入多输出 需要解耦 自然支持 MIMO
在线应用 RLS 直接支持 需要完整周期

工程建议:对刚体机器人用时域方法(§3-7),对柔性/执行器子系统用频域方法。两者互补而非替代。

练习

练习 9.1 ⭐⭐:对一个单关节机器人(电机 + 谐波减速器),设计多正弦激励信号,用 FFT 估计从力矩输入到关节角输出的频率响应。识别谐振频率。

练习 9.2 ⭐⭐⭐:用辨识到的频率响应设计一个 notch filter 抑制谐振,与没有 notch filter 的 PD 控制对比性能。


延伸阅读

资料 内容 难度
Atkeson et al. 1986 IJRR 奠基论文 ⭐⭐
Khalil & Dombre 2002 Ch.13 教科书处理 ⭐⭐
Swevers et al. 2007 IEEE CSM 激励设计综述 ⭐⭐⭐
Ljung 1999 教材 系统辨识经典 ⭐⭐⭐⭐
Brunton et al. 2022 SIAM Review Koopman/DMD 综述 ⭐⭐⭐
Coulson et al. 2019 ECC DeePC 原始论文 ⭐⭐⭐
Pinocchio 文档 computeJointTorqueRegressor ⭐⭐
Drake 教程 InverseDynamicsController ⭐⭐

中文资源: - 知乎"机器人动力学参数辨识"系列(含 Pinocchio 代码) - B 站"机械臂系统辨识实验"(Fourier 激励 + OLS 全流程) - CSDN"Pinocchio computeJointTorqueRegressor 使用教程" - GitHub robot-identification 中文注释项目


跨章综合练习

综合练习 1(辨识 + MPC + 自适应) ⭐⭐⭐

设计一个完整的"辨识-控制-自适应"流程: 1. 用 Fourier 激励 + WLS 离线辨识 7-DOF 机械臂的基参数(本章 §3-5) 2. 用辨识参数设计 computed torque 控制器 + NMPC 跟踪控制器(3.11-3.12) 3. 在运行中用 RLS 在线更新参数(本章 §7),当参数变化超过阈值时自动更新控制器

讨论:在线辨识和 Tube MPC(3.13)如何互补?什么时候该用在线辨识更新模型,什么时候该用 Tube 容忍模型误差?

综合练习 2(辨识 + sim-to-real) ⭐⭐⭐

对一个在 MuJoCo 中仿真的四足机器人: 1. 在仿真中辨识动力学参数 2. 故意引入参数偏差(模拟 sim-to-real gap) 3. 比较三种策略的跟踪性能:(a) 直接使用仿真参数;(b) 在真实数据上重新辨识;(c) 使用 domain randomization 训练的 RL 策略 4. 讨论:辨识在 sim-to-real 中的角色是什么?

本质洞察专栏

本质洞察:系统辨识的本质不是"拟合数据",而是**从有限观测中提取物理系统的内禀结构**。一个好的辨识结果应该能预测**从未见过的运动模式**下的系统行为——这要求辨识出的参数具有物理意义(质量、惯性、摩擦),而不仅仅是最小化训练集上的拟合残差。这就是为什么物理一致性约束(§6)不是可选的"加分项",而是辨识质量的根本保证。

本质洞察:激励轨迹设计(§4)的核心矛盾是**信息量 vs 安全性**。信息论告诉我们,参数辨识精度正比于 Fisher 信息矩阵 \(\mathcal{I} = Y^\top \Sigma^{-1} Y\)(回归矩阵的加权外积)——要使 \(\mathcal{I}\) 的最小特征值尽量大,轨迹必须"充分激励"所有方向。但激励意味着大加速度、大速度变化,这在真实机器人上受到关节限位、力矩饱和、安全约束的严格限制。Fourier 参数化 + 条件数优化是这对矛盾的工程最优折衷。

本质洞察:基参数(Base Parameters)的概念揭示了一个深刻的数学事实:并非所有物理参数都能从输入-输出数据中辨识出来\(10n\) 个标准惯性参数通过动力学方程映射到力矩空间时,存在退化方向——某些参数组合对可观测的关节力矩没有任何影响。基参数正是消除这些退化后的最小独立参数集。这与可观测性理论(Kalman rank condition)有着深刻的对偶关系:状态估计中有"不可观测状态",参数辨识中有"不可辨识参数"。


补充练习题

练习 S.1 ⭐(辨识精度评估):对已辨识的参数 \(\hat{\pi}\),定义以下验证指标:(a) 相对力矩预测误差 \(e_{\text{rel}} = \|\tau_{\text{pred}} - \tau_{\text{meas}}\| / \|\tau_{\text{meas}}\|\);(b) 参数不确定性椭球 \(\text{cov}(\hat\pi) = \sigma^2 (Y^\top Y)^{-1}\)。在新的(未用于辨识的)验证轨迹上计算这两个指标。讨论:\(e_{\text{rel}} < 5\%\) 对 MPC 来说够用吗?

练习 S.2 ⭐⭐(条件数与激励质量):对 2-DOF 平面机械臂,生成三种激励轨迹:(a) 单频正弦;(b) 多频 Fourier;(c) 随机关节抖动。分别计算回归矩阵的条件数 \(\kappa(Y)\)。用 Monte Carlo 加入不同噪声水平 \(\sigma \in \{0.01, 0.1, 1.0\}\) Nm,比较三种轨迹的参数估计方差。验证 \(\text{var}(\hat\pi) \propto \kappa(Y)^2 \sigma^2\)

练习 S.3 ⭐⭐(物理一致性约束):对辨识得到的参数 \(\hat\pi\),检验以下物理约束是否满足:(a) 所有连杆质量 \(m_i > 0\);(b) 惯性张量 \(I_i\) 正定(即三角不等式 \(I_{xx} + I_{yy} \ge I_{zz}\) 等);(c) 摩擦系数 \(f_{c,i} \ge 0\)。如果不满足,用 SDP(半正定规划)重新辨识,比较有无物理一致性约束时的预测精度差异。

练习 S.4 ⭐⭐⭐(在线辨识 vs 离线辨识):实现 RLS(递推最小二乘)在线辨识器。在仿真中,让机器人执行正常任务(如码垛),中途改变负载质量(模拟抓起物体)。(a) 画出参数估计随时间的变化曲线;(b) 测量参数收敛到新值所需的时间;(c) 讨论:遗忘因子 \(\lambda\) 越小收敛越快但方差越大——对于 MPC 来说,最佳的 \(\lambda\) 是多少?

练习 S.5 ⭐⭐⭐(跨章综合 — 辨识误差对控制性能的影响):回顾 §3.13(Tube MPC)。设辨识参数有 20% 的相对误差 \(\|\Delta\pi\|/\|\pi\| = 0.2\),将其转化为动力学模型的加法扰动界 \(\|w\| \le \delta\)。(a) 估计 \(\delta\)(提示:\(w \approx Y\Delta\pi\));(b) 用 \(\delta\) 设计 Tube MPC 的扰动集 \(\mathcal{W}\);(c) 比较"辨识精度提升到 5% + 窄 Tube"与"保持 20% 误差 + 宽 Tube"两种策略的控制性能(跟踪精度 vs 计算量)。


术语对照表

英文术语 中文 首次出现节
Base Parameters 基参数 §3
Regressor Matrix 回归矩阵 §3
Fourier Excitation Trajectory Fourier 激励轨迹 §4
Condition Number 条件数 §4
Weighted Least Squares (WLS) 加权最小二乘 §5
Recursive Least Squares (RLS) 递推最小二乘 §7
Persistent Excitation (PE) 持续激励 §4
Physical Consistency 物理一致性 §6
Semidefinite Constraint 半正定约束 §6
Cross-Validation 交叉验证 §5