共计 1405 个字符,预计需要花费 4 分钟才能阅读完成。
为什么损失函数如此重要?
在 SLAM 系统中,我们曾遇到一个典型问题:当存在 5% 的 GPS 离群点时,使用默认平方误差(L2 范数)的 Bundle Adjustment 结果完全偏离真实轨迹。这是因为:

$$ \rho(r) = r^2 $$
对离群点的平方惩罚会过度放大误差,导致优化被少数坏点 ” 绑架 ”。改用 Huber 损失后,轨迹误差立即降低 82%。
核心损失函数原理剖析
1. Huber 损失:温和的离群点抵抗者
数学定义:
$$
\rho(r) = \begin{cases}
\frac{1}{2}r^2 & |r| \leq \delta \
\delta(|r| – \frac{1}{2}\delta) & |r| > \delta
\end{cases}
$$
- δ 是阈值参数(建议取测量误差标准差 1.345 倍)
- 核心思想:小误差保持二次增长,大误差转为线性增长
2. Cauchy 损失:重尾分布专家
$$
\rho(r) = c^2 \log(1 + (r/c)^2)
$$
- 参数 c 控制函数弯曲程度
- 适合处理脉冲型噪声(如传感器突发干扰)
代码实战演示
C++ 自动微分实现
struct HuberLoss {
template <typename T>
bool operator()(const T* residual, T* rho) const {T delta_sq = T(delta_ * delta_);
T abs_r = ceres::abs(*residual);
if (abs_r <= delta_) {*rho = residual[0] * residual[0];
} else {*rho = delta_sq * (T(2.0) * abs_r - delta_);
}
return true;
}
explicit HuberLoss(double delta) : delta_(delta) {}
private:
const double delta_;
};
Python 绑定示例
class CauchyLoss:
def __init__(self, c):
self.c = c
def __call__(self, residual):
scaled_r = residual / self.c
return self.c**2 * np.log1p(scaled_r**2)
性能对比实验
| 损失函数 | 迭代次数 | 最终误差 (m) | 离群点影响 |
|---|---|---|---|
| L2 | 152 | 3.27 | 极高 |
| Huber(δ=1) | 89 | 0.58 | 中等 |
| Cauchy(c=2) | 112 | 0.42 | 低 |
测试环境:Intel i7-11800H, Ceres 2.1.0, Ubuntu 20.04
生产环境建议
- 线程安全黄金法则
- 所有自定义损失函数必须是无状态的
-
避免在 operator() 中使用静态变量
-
数值稳定性检查表
- 验证损失函数在 |r|→∞时的有界性
- 确保一阶导数在零点连续
-
测试浮点溢出边界(特别是指数类函数)
-
协同调优技巧
- 搭配 SPARSE_NORMAL_CHOLESKY 求解器效果最佳
- 信任域策略中 initial_trust_region_radius 建议设为 δ 的 2 - 3 倍
- 对 Cauchy 损失适当调低 function_tolerance(建议 1e-8)
开放性问题
当噪声呈现混合分布特性时(如视觉 SLAM 中同时存在高斯噪声和运动模糊导致的脉冲噪声),现有的固定参数损失函数可能捉襟见肘。一种可能的解决方案是:
$$ w_i = \frac{1}{1 + (r_i/\sigma)^2} $$
通过实时估计残差分布来自适应调整权重。这引出一个更深入的问题——如何在不显著增加计算开销的前提下,实现噪声类型的在线识别?
正文完
