2. 误差建模方法:三种主流路径

好,咱们进入正题。手眼标定做完,你会发现——残差永远存在。为什么?因为真实世界不是理想模型。机械臂有柔性变形,相机有量化噪声,标定板有加工公差。这时候,误差建模就派上用场了。

我个人习惯把误差建模分成三个流派:最小二乘残差建模、高斯过程误差场、神经网络误差拟合。它们各有各的脾气,也各有各的适用场景。下面我一个个讲。

核心观点:误差建模不是越复杂越好。你的机器人精度、传感器噪声、计算资源,决定了该选哪条路。

2.1 基于最小二乘的残差建模

这是最经典的方法。说白了,就是假设误差服从某种参数化形式,然后用最小二乘法去拟合这些参数。

我在项目中遇到过一台六轴机器人,标定后末端位置误差在 0.5~1.5mm 之间波动。我尝试用多项式模型去拟合这个残差:

# 残差模型:Δx = a0 + a1*q1 + a2*q2 + ... + a6*q6
# 其中 q1~q6 是关节角度

import numpy as np

# 构建设计矩阵
Phi = np.column_stack([
    np.ones_like(q1), q1, q2, q3, q4, q5, q6,
    q1**2, q2**2, q3**2
])

# 最小二乘求解
theta, residuals, rank, s = np.linalg.lstsq(Phi, delta_x, rcond=None)

print(f"拟合残差标准差: {np.std(residuals):.4f} mm")

嗯,这里要注意:特征选择是关键。你选哪些关节、哪些高阶项,直接影响拟合效果。我一般会用交叉验证来筛选特征,避免过拟合。

模型阶数 参数数量 拟合残差 (mm) 泛化误差 (mm)
线性 (1阶) 7 0.42 0.48
二次 (2阶) 16 0.28 0.35
三次 (3阶) 30 0.21 0.33

我的经验:最小二乘残差建模适合误差规律比较平滑的场景。如果残差在某个关节角度附近突然跳变,那说明有未建模的物理效应(比如齿轮间隙),这时候多项式模型就吃力了。

2.2 基于高斯过程的误差场建模

高斯过程(GP)是个好东西。它不假设误差的具体形式,而是通过核函数描述误差之间的相关性。你想想看,如果两个位姿很接近,它们的误差应该也差不多——这就是 GP 的核心思想。

我曾经给一台协作机器人做过 GP 误差场。采集了 200 个标定点,用平方指数核:

from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import RBF, WhiteKernel

kernel = 1.0 * RBF(length_scale=0.1) + WhiteKernel(noise_level=0.01)
gp = GaussianProcessRegressor(kernel=kernel, alpha=0.0)

# 训练数据:位姿 x,y,z + 残差
X_train = np.column_stack([x, y, z])
y_train = delta_x

gp.fit(X_train, y_train)

# 预测新位姿的误差
x_test = np.array([[0.5, 0.3, 0.2]])
delta_pred, sigma_pred = gp.predict(x_test, return_std=True)
print(f"预测误差: {delta_pred[0]:.3f} mm, 不确定度: {sigma_pred[0]:.3f} mm")

避坑指南:我曾经在 500 个数据点上跑 GP,结果训练时间超过 10 分钟。GP 的计算复杂度是 O(n³),数据量超过 1000 就要考虑稀疏近似了。另外,核函数的选择很敏感——选错了,预测结果可能还不如线性模型。

GP 最大的优势是自带不确定度估计。这在安全关键应用中特别有用——你可以知道哪些区域误差预测比较可靠,哪些区域需要保守处理。

2.3 基于神经网络的误差拟合

神经网络这几年很火。说实话,在误差建模这个领域,它确实能处理一些非常复杂的非线性关系。但我要泼点冷水——不是所有问题都需要神经网络。

我见过一个案例:有人用 10 层 MLP 去拟合一个本来可以用二次多项式解决的残差。结果呢?训练慢、容易过拟合、部署还麻烦。杀鸡用牛刀了。

但有些场景确实适合 NN。比如,当误差与多个输入变量存在强耦合时:

import torch
import torch.nn as nn

class ErrorNet(nn.Module):
    def __init__(self, input_dim=6, hidden_dim=64):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, 3)  # 输出 Δx, Δy, Δz
        )

    def forward(self, x):
        return self.net(x)

# 训练循环(简化)
model = ErrorNet()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
loss_fn = nn.MSELoss()

for epoch in range(500):
    pred = model(X_train_tensor)
    loss = loss_fn(pred, y_train_tensor)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

我的建议:先用最小二乘试试,如果残差标准差能到 0.3mm 以下,就别折腾 NN 了。如果残差还有明显的空间结构,再考虑 GP 或 NN。另外,NN 的输入归一化一定要做——我见过有人忘了这步,训练直接发散。

三种方法对比

我把它们放在一起对比一下,方便你选型:

维度 最小二乘 高斯过程 神经网络
模型复杂度 低 中 高
数据需求 几十个点 几百个点 上千个点
训练时间 秒级 分钟级 小时级
不确定度 无 有 需额外方法
可解释性 高 中 低
部署难度 低 中 高
误差建模方法选择流程 原始残差数据 残差是否有明显空间结构? 否 是 最小二乘残差建模 数据量 > 1000? 是 否 神经网络误差拟合 高斯过程误差场 经验法则 残差 < 0.3mm → 最小二乘足够 残差有空间结构 + 数据量适中 → 高斯过程

总结一下:三种方法不是互斥的。我在实际项目中经常组合使用——先用最小二乘去掉趋势项,再用 GP 或 NN 拟合残差中的空间结构。这样既保留了可解释性,又提升了精度。