Ceres损失函数选型指南:从原理到实战的鲁棒优化策略

发布时间:2026/8/23 22:10:11
Ceres损失函数选型指南:从原理到实战的鲁棒优化策略 1. 项目概述为什么Ceres的损失函数选型是个技术活在非线性优化领域Ceres Solver早已是工程师和研究者手中的一把瑞士军刀。无论是做视觉SLAM中的Bundle Adjustment还是做机器人运动学标定甚至是金融模型的参数拟合Ceres都以其稳健性和灵活性著称。但很多朋友包括我早期在内常常会陷入一个误区把注意力全放在构建残差项和雅可比矩阵上而把损失函数LossFunction的配置当作一个简单的“鲁棒核函数”开关默认用个Huber或者Cauchy就完事了。实际上损失函数的选择直接决定了你的优化器在面对数据中的“捣蛋鬼”——也就是异常值Outliers时是英勇“阵亡”还是从容“排雷”。它不是一个可有可无的装饰而是整个优化问题鲁棒性的基石。简单来说Ceres中的损失函数包裹在每一个残差项外面它的核心作用是对残差的平方项即我们通常说的二范数平方 \( r^2 \)进行重新加权。当某个残差特别大很可能对应一个异常观测时一个设计良好的损失函数可以抑制其影响力防止它“带偏”整个优化过程。今天我们就来深挖一下Ceres中LossFunction的选择门道。这不仅仅是选哪个函数的问题更是理解你的数据特性、问题本质并在计算效率与模型鲁棒性之间找到最佳平衡点的过程。无论你是刚接触Ceres的新手还是已经用它跑过不少实验的老兵相信关于损失函数的这些细节和实战经验都能让你对优化问题的构建有新的认识。2. 损失函数的核心原理与Ceres中的角色定位2.1 从最小二乘到鲁棒优化损失函数的本质要理解损失函数我们得从最基础的最小二乘法Least Squares说起。标准的非线性最小二乘问题长这样 \[ \min_x \frac{1}{2}\sum_i \rho_i(\|f_i(x)\|^2) \] 在经典最小二乘中\( \rho \) 就是恒等函数 \( \rho(s) s \)即我们直接最小化所有残差 \( f_i(x) \) 的平方和。这里的隐含假设是所有观测数据都服从高斯噪声且没有异常值。但现实很骨感。传感器会抽风特征匹配会出错总会混进来一些“不听话”的数据点。这些异常值会产生巨大的残差。由于平方项的增长速度极快二次增长这些大残差在目标函数中会占据主导地位。优化算法为了最小化这个被严重扭曲的目标函数会不惜大幅调整其他所有正常参数去“迎合”这些异常值导致最终估计结果严重偏离真实值。损失函数 \( \rho(s) \) 的使命就是改造这个平方项 \( s \|f_i(x)\|^2 \)。它通过一个非线性变换改变大残差对应的目标函数贡献度的增长速度。一个理想的损失函数应该具有这样的性质对于小的残差内点其行为近似于平方函数以保证在高斯噪声下的统计效率对于大的残差外点其增长应该放缓例如变为线性增长甚至饱和从而限制异常值的影响力。在Ceres中当你为一个残差块添加LossFunction时你并不是在替换残差模型而是在修改优化目标中对这个残差项的“看法”和“容忍度”。2.2 Ceres中损失函数的接口与计算机制Ceres定义了一个纯虚基类LossFunction核心是Evaluate方法。这个方法接收残差的平方值 \( s \)输出三个值rho[0]: \( \rho(s) \)即变换后的损失值。rho[1]: \( \rho(s) \)一阶导数。rho[2]: \( \rho(s) \)二阶导数。你可能好奇优化算法通常只需要一阶梯度雅可比矩阵为什么这里需要二阶导数这是因为Ceres在内部使用高斯-牛顿法、列文伯格-马夸尔特法等算法时需要构建近似的海森矩阵\( J^T J \)。当引入损失函数后海森矩阵的近似需要用到 \( \rho(s) \)。具体地加权后的梯度为 \( \rho(s) \cdot J^T f \)而加权后的海森矩阵近似为 \( \rho(s) \cdot J^T J 2 \rho(s) \cdot (J^T f)(f^T J) \)。Ceres的TrivialLoss即标准最小二乘对应 \( \rho(s)1, \rho(s)0 \)就退化回了我们熟悉的 \( J^T J \)。理解这个计算机制很重要因为它解释了为什么损失函数能生效通过 \( \rho(s) \) 缩放梯度大残差对应的梯度被减小从而其在参数更新中的影响力下降。对算法的影响不同的 \( \rho(s) \) 会影响海森矩阵的结构进而影响优化步长的计算。某些损失函数在 \( s \) 很大时 \( \rho(s) \) 为负可能导致海森矩阵不正定影响算法稳定性。Ceres的实现已经妥善处理了这些问题。注意我们通常说的“核函数”Kernel Function在Ceres的语境里就是LossFunction。在统计学和鲁棒估计领域它可能被称为“影响函数”Influence Function或“鲁棒核函数”Robust Kernel。在Ceres的代码和文档中统一使用LossFunction这个术语。3. 内建损失函数全解析与选型指南Ceres提供了一系列开箱即用的损失函数每个都有其独特的性格和适用场景。选择哪一个取决于你对异常值的假设和容忍度。3.1 经典三剑客Huber、Cauchy、Arctan这是最常用的一组它们提供了一个标度参数a用于控制函数从“二次行为”过渡到“线性行为”或“饱和行为”的拐点。1. HuberLoss这是许多人的首选也是我的默认起点。它的行为非常直观当残差小\( s \leq a^2 \)时它是标准的二次函数 \( \rho(s) s \)。当残差大\( s a^2 \)时它变为线性函数 \( \rho(s) 2a\sqrt{s} - a^2 \)。参数a的意义可以直观理解为残差的阈值。当残差的绝对值注意是 \( \sqrt{s} \)大于a时损失函数切换为线性模式。a通常设置为测量噪声标准差的倍数例如在视觉SLAM中重投影误差的像素单位下a1.0对应1像素或a1.345一个统计学上的常用值都是常见选择。优点计算简单具有凸性能保证全局收敛性在满足条件下。它对中等程度的异常值有良好的抑制效果。缺点对于极大的异常值其线性增长意味着它仍然会赋予一定的权重可能不足以完全屏蔽极端外点。适用场景数据质量相对较好异常值比例不高10%且异常值不是特别极端的情况。非常适合作为基线配置。2. CauchyLoss柯西损失来源于柯西分布其函数形式为 \( \rho(s) a^2 \log(1 s / a^2) \)。行为对于小残差近似为二次对于大残差增长极其缓慢近似对数增长。这意味着它对非常大的异常值有很强的抑制能力。参数a的意义同样是一个尺度参数。减小a会使函数更“敏感”更早地进入缓慢增长区增大a则使其更接近二次损失。优点对重尾噪声和极端异常值的鲁棒性非常强。缺点在异常值比例很高时可能会过度抑制数据导致收敛变慢且其非线性更强可能带来更多的局部极小点。适用场景数据中存在显著且可能非常巨大的异常值例如动态物体干扰下的视觉里程计、带有错误匹配的点云配准。3. ArctanLoss反正切损失函数\( \rho(s) a^2 \cdot \arctan(s / a^2) \)。行为与Cauchy类似但对大残差的抑制更加“温和”且具有饱和性。当 \( s \to \infty \) 时\( \rho(s) \to a^2 \cdot \pi/2 \)即损失值有上界。优点饱和特性意味着它对极大异常值的影响力削减最为彻底几乎完全忽略。缺点饱和特性也可能导致在优化后期当参数接近真值时那些被判定为“异常”但可能包含有用信息的数据点完全失去作用。适用场景你非常确信数据中存在完全错误的、需要彻底剔除的观测而不是仅仅降低其权重。3.2 其他内建损失函数速览除了上述三个Ceres还提供了其他选项用于更特殊的场景SoftLOneLoss: 形式为 \( \rho(s) 2 (\sqrt{1s} - 1) \)。它是Huber损失的一种平滑近似处处可微没有显式的阈值a其过渡是平滑的。在Bundle Adjustment的某些实现中如OpenMVG被使用。TolerantLoss: 这是一个双参数损失函数 \( \rho(s) b \log(1 e^{(s-a)/b}) - b\log(1e^{-a/b}) \)。它通过参数a和b可以精确控制函数在原点附近的弯曲程度和线性区域的斜率提供了极大的灵活性适合当你对误差分布有非常具体的先验知识时进行精细调优。TrivialLoss: 即 \( \rho(s) s \)标准最小二乘。用于确认问题本身或数据清洗后无需鲁棒核函数。ScaledLoss: 这是一个包装器可以对任何损失函数进行全局缩放。例如new ScaledLoss(new HuberLoss(1.0), 0.5)会将Huber损失计算出的损失值乘以0.5。这在多传感器融合中当你需要手动调整不同传感器残差的相对权重时非常有用但通常更推荐通过信息矩阵协方差的逆来加权。3.3 实战选型决策树与参数设置经验面对这么多选择如何快速决策我总结了一个简单的决策流程第一步评估数据质量如果数据经过严格预处理异常值极少 → 优先尝试TrivialLoss(无) 或HuberLoss。如果数据来源复杂已知存在匹配错误或噪声较大 → 直接上CauchyLoss或HuberLoss。第二步确定异常值严重程度异常值大但比例不高 →HuberLoss。异常值巨大且可能很多 →CauchyLoss。需要完全剔除某些确信的错误数据 →ArctanLoss。第三步设置尺度参数a这是调参的关键。没有放之四海而皆准的值但有一些经验法则基于测量单位如果残差是像素误差a1.0是个好起点。如果是距离误差米a0.1或a0.01可能更合适。a的单位与残差f(x)的单位一致。统计学方法对于Huber常取a1.345σσ为测量噪声的标准差这能在高斯分布假设下保持95%的渐近效率。试错法这是最常用的。从一个估计值开始如a1.0运行优化观察最终的重投影误差或残差直方图。如果很多残差远大于a说明a设小了异常值影响仍被过度抑制对于Huber或未被充分抑制对于Cauchy/Arctan。如果几乎所有残差都小于a说明a设大了损失函数没怎么起作用。理想情况是大部分内点残差小于a少数外点残差远大于a。可视化将优化后的残差绘制成直方图a的值应该落在内点分布尾部和外点起始区域之间。第四步交叉验证与效果评估用不同的损失函数和参数a运行优化。比较最终的目标函数值、内点残差的均值/中位数、参数估计的合理性如果已知ground truth。在SLAM或SfM中可以直观查看重建的点云或轨迹检查是否还有明显的“拉飞”的点或相机位姿。实操心得在大多数视觉几何问题中我个人的经验是CauchyLoss配合一个稍保守的a值例如a0.5或a1.0对于像素误差往往能提供一个非常稳健的基线性能。它的“重尾”特性对视觉数据中常见的、由错误匹配导致的极端异常值非常有效。HuberLoss则更适用于对收敛速度和全局最优性有更高要求的场景或者数据相对干净时。4. 自定义损失函数当内建函数无法满足需求Ceres的内建函数覆盖了90%的场景但总有那10%的特殊需求。比如你的误差分布有特殊的形状或者你想实现一个文献中提出的新核函数。这时就需要自定义。4.1 实现自定义LossFunction类自定义需要继承ceres::LossFunction并实现三个纯虚函数Evaluate和两个Check方法通常直接返回true。核心是正确计算rho[0],rho[1],rho[2]。举个例子假设我们想实现一个“线性阈值损失”Linear Threshold Loss它的思想是当残差绝对值小于阈值t时用二次函数大于t时损失值保持为一个常数c完全饱和。class LinearThresholdLoss : public ceres::LossFunction { public: explicit LinearThresholdLoss(double threshold, double constant_value) : t_squared_(threshold * threshold), c_(constant_value) {} virtual void Evaluate(double s, double rho[3]) const override { if (s t_squared_) { // 二次区域 rho[0] s; rho[1] 1.0; rho[2] 0.0; } else { // 饱和区域 rho[0] c_; rho[1] 0.0; // 导数为0梯度消失 rho[2] 0.0; } } private: const double t_squared_; const double c_; };关键点解析导数连续性在上面的简单实现中在s t_squared_处一阶导数从1.0跳变到0.0是不连续的。这可能导致优化在阈值附近震荡。一个更工程化的实现会使用一个平滑的过渡函数例如sigmoid来连接两段确保一阶甚至二阶导数的连续性。二阶导数在饱和区我们设置了rho[2] 0.0。这对于Ceres内部计算海森矩阵近似是必要的。当rho[1]为0时对应的残差项对海森矩阵没有贡献这符合“完全忽略该观测”的直觉。Check方法示例中省略了。通常实现为bool Check() const { return true; }用于验证对象状态在自定义中一般直接返回true。4.2 自定义函数的应用场景与调试技巧何时需要自定义特定的噪声模型你的传感器或物理过程有已知的、非标准的误差分布。算法对比实验你需要复现论文中某个特定损失函数的效果。性能优化内建函数的计算在某些极端情况下可能成为瓶颈而你有一个计算更简单的近似版本。特殊逻辑例如你想实现一个随着迭代次数增加阈值a逐渐减小的“自适应”损失函数这通常通过每轮迭代重新构建Problem来实现而非在LossFunction内部。调试自定义损失函数单元测试编写简单的测试程序输入一系列s值打印rho[0],rho[1],rho[2]并手动计算或绘图验证其正确性。特别检查在阈值过渡点附近的数值行为。梯度检查利用Ceres的自动微分与你的自定义函数组合使用有限差分法检查梯度的正确性。虽然Ceres不直接提供该工具但你可以手动实现或利用其他库如Google Test进行数值梯度检验。小规模问题验证先在一个小的、可控的优化问题上比如拟合一条直线并人工添加异常值测试你的自定义损失函数观察其优化行为是否符合预期。注意事项自定义损失函数增加了代码复杂性和出错风险。除非内建函数确实无法满足需求否则应优先使用经过充分测试的内建函数。如果必须自定义务必进行严格的数学推导和数值测试确保其导数计算正确避免引入数值不稳定问题。5. 高级策略与性能调优实战选对了损失函数类型和参数只算成功了一半。在实际的大型优化问题如大规模BA中如何高效、正确地使用它们还需要一些策略。5.1 混合使用不同损失函数一个Problem中的不同残差块可以使用不同的损失函数。这是非常强大的功能。场景示例在视觉惯性SLAM中视觉重投影误差可能使用CauchyLoss来对抗错误匹配而IMU预积分误差的噪声模型更接近高斯且通常更可靠可能使用HuberLoss甚至TrivialLoss。实现方法在调用AddResidualBlock时为每个代价函数传入对应的LossFunction指针即可。好处可以为不同来源、不同置信度的传感器数据施加不同级别的鲁棒性更贴合物理实际。5.2 损失函数与参数块规模的协同考虑Ceres的LossFunction是作用于单个标量残差上的。对于一个残差块输出多维残差例如重投影误差是2维的情况损失函数是分别应用于每个残差分量后再求和。这意味着一个2D重投影误差点如果其在x和y方向上都因为异常值而很大那么它会受到两次抑制。这通常是合理的但需要注意对于某些定义为“距离”的残差例如点到直线的距离、点到平面的距离它本身就是一个标量直接应用损失函数即可。5.3 迭代重加权最小二乘的关联使用鲁棒损失函数的优化过程在数学上等价于迭代重加权最小二乘。每一次高斯-牛顿或LM迭代可以看作是用当前残差计算权重即 \( \rho(s) \)然后求解一个加权最小二乘问题。启示你可以手动实现IRLS。先不用损失函数运行一次优化根据残差分布计算每个观测的权重例如用Huber或Cauchy的权重函数然后以这些权重作为固定值再进行一次或多次加权最小二乘优化。这在某些需要精确控制权重或调试权重计算时有用。Ceres的自动化Ceres将这个过程完全自动化并集成在每一次迭代中效率更高且能处理权重随参数变化而动态变化的情况。5.4 计算开销分析与选择建议不同的损失函数有轻微的计算开销差异但在绝大多数应用中计算残差和雅可比矩阵的成本远高于计算损失函数及其导数。因此性能通常不是选择损失函数的主要考量。不过在极端边缘场景例如嵌入式设备上运行超大规模优化下可以遵循以下原则TrivialLoss(无) 是最快的。HuberLoss和SoftLOneLoss涉及开平方和条件判断稍慢。CauchyLoss和ArctanLoss涉及对数、反正切等超越函数理论上最慢。结论除非在性能测试中明确发现损失函数成为瓶颈否则应基于鲁棒性需求而非微小的计算差异来做选择。6. 常见问题排查与实战避坑指南在实际使用中你会遇到各种奇怪的现象。下面是我踩过的一些坑和对应的解决方案。6.1 优化结果不理想或发散问题现象加了损失函数后优化反而发散或者结果比不用损失函数更差。排查思路检查尺度参数a这是最常见的原因。a值设置过小会导致几乎所有数据都被当作异常值抑制有效信息丢失优化失去方向。尝试将a值增大一个数量级再试。检查损失函数类型对于初值很差的问题使用像ArctanLoss这样饱和性很强的函数可能会过早地“杀死”所有梯度导致优化停滞。初期尝试使用HuberLoss这种更温和的函数。检查残差定义确认你的残差f(x)计算是否正确。一个错误的残差符号或尺度会使损失函数基于错误的值进行判断。在添加损失函数前先用TrivialLoss运行确保优化能收敛到一个合理的结果。可视化中间结果在迭代回调函数中输出当前迭代的损失值、梯度范数以及残差的统计信息如最大值、最小值、中位数。观察损失函数是否在正常下降大残差是否被有效抑制。6.2 如何诊断损失函数是否起作用方法一比较最终残差分布。分别用TrivialLoss和你选择的损失函数运行优化将最终的所有残差值绘制成直方图。如果损失函数有效你应该能看到使用鲁棒核后残差分布的“长尾”大值部分被显著削减整体分布更集中。方法二观察参数更新过程。在LM算法中可以输出mu阻尼因子的变化。如果使用损失函数后优化过程需要更少的迭代次数或mu能更快地下降说明问题条件数改善优化更顺畅。方法三检查特定残差。如果你知道某些数据点很可能是外点记录它们在优化过程中的残差变化。一个有效的损失函数应该能将这些点的残差权重rho[1]降得很低。6.3 与自动微分的配合注意事项当使用自动微分AutoDiffCostFunction时损失函数是在自动微分之外起作用的。自动微分负责计算残差f(x)的雅可比矩阵J。然后Ceres内部会计算rho[1]和rho[2]并用它们来缩放梯度并修正海森矩阵。这意味着自定义损失函数的导数必须手动实现正确自动微分不会帮你求rho的导数。不影响残差计算逻辑你无需为了使用损失函数而修改残差仿函数的实现。6.4 内存与所有权管理这是一个C编程的常见坑。Ceres的Problem::AddResidualBlock方法会接管你传入的CostFunction和LossFunction指针的所有权。这意味着不要手动删除在AddResidualBlock之后切勿再delete这些指针否则会导致程序崩溃。使用堆内存必须使用new关键字在堆上创建这些对象。对于内建损失函数Ceres提供了工厂函数如new HuberLoss(1.0)直接使用即可。对于自定义损失函数同样需要new出来传入。一个良好的实践是在构建完整个Problem后如果后续不再需要添加残差块可以将这些指针保存到std::vector或其他容器中尽管Ceres已接管所有权我们保存指针是为了避免重复创建的逻辑但实际内存管理权已移交但更清晰的做法是让Problem对象在作用域结束时自动清理一切。6.5 尺度参数a的自适应设置策略手动调a很麻烦有没有自适应的方法有但需要一些工程技巧。两阶段法第一阶段使用一个较宽松的损失函数如HuberLosswith a largea或不用损失函数进行初步优化。第二阶段根据第一阶段优化后的残差统计例如计算残差的中位数绝对偏差MAD估算出内点残差的尺度据此设置第二阶段的a值例如a 1.4826 * MAD其中1.4826是针对高斯分布的系数。迭代更新法更复杂在Ceres的迭代回调函数中每次迭代后根据当前残差重新计算所有观测的权重或a值然后在下一次迭代前重建ProblemCeres不支持动态修改已添加的LossFunction参数。这种方法开销大仅适用于特殊研究场景。对于绝大多数工程应用基于对数据尺度的理解进行几次手动调参并确定一个经验值是最高效可靠的方式。例如在视觉里程计中重投影误差超过3-5个像素的点通常值得怀疑那么a可以设置在1.5到2.5之间进行尝试。最后关于损失函数的选择我的个人体会是它更像是一门“艺术”而非纯粹的“科学”。它依赖于你对数据噪声特性的直觉和经验。从HuberLoss开始它是一个安全且有效的起点。如果发现仍有明显的异常值干扰就切换到抑制能力更强的CauchyLoss。永远记得在调整损失函数的同时前端数据处理的质量如特征匹配的筛选、RANSAC同样至关重要。一个好的损失函数是后端的“安全网”但前端的“过滤器”做得越好后端优化就越轻松、越稳定。