
去年有个做视觉定位的项目前端的特征匹配和位姿计算都还好说到了后端做全局优化的时候优化变量里既有相机位姿又有地图点坐标目标函数是非线性的数据量大还带各种约束一开始我拿通用优化库去硬怼结果要么收敛极慢要么直接跑飞。后来静下心把问题重新梳理了一遍把噪声分布假设成高斯把信息矩阵加进去把鲁棒核函数嵌到残差项里最后整个问题就变成一个加权最小二乘的结构说白了就是典型的凸优化问题。想明白这件事之后才真正感觉到“参数估计”和“凸优化”这两个词不是数学课本上两个孤立的概念它们一个描述“我们想从数据里得到什么”一个描述“我们怎么可靠地拿到它”。这篇文章我打算围绕这两件事展开目标是让你看完之后能建立一条完整的主线什么时候问题可以建构成凸的什么样的估计目标对应什么样的目标函数以及在真正的工程代码里怎么做才不会把问题解飞。适合正在学机器学习、SLAM、自动控制、信号处理或者单纯被一堆优化理论糊了一脸却不知道怎么落地的人也适合那些已经把开源库跑通但一换数据就出问题的人。1. 内容整体设计与思路拆解1.1 为什么把参数估计和凸优化绑在一起讲参数估计这件事本质上是从观测数据里反推系统内在参数。最经典的框架是概率视角下的最大似然估计再从最大似然往前迈一步加上参数本身的先验就变成最大后验估计。而凸优化解决的是另一个问题给定一个目标函数怎么高效、可靠地找到它的最小值。这两者之所以能绑在一起讲是因为几乎所有经典参数估计问题在满足“噪声服从指数族分布”这一大前提下最终都会落到一个凸优化问题上。比如最小二乘是高斯噪声假设下的最大似然解L1正则化是拉普拉斯先验下的最大后验解逻辑回归是伯努利噪声假设下的最大似然解。换句话说统计建模决定了目标函数的长相而凸优化决定了这个函数能不能被稳定求解。把它们分开学的毛病在于只学参数估计的人会把大量精力花在推导无偏性、一致性上真到计算时发现梯度算起来容易但迭代不收敛只学凸优化的人能把KKT条件、对偶理论背得很熟却不知道该怎么把实际问题写成标准形式。两条腿缺一条都走不远。1.2 全文的主线逻辑这篇文章我按一条“从问题到解法”的主线来组织第一部分讲清楚参数估计的核心框架包括最大似然估计和最大后验估计以及它们怎么自然地导出目标函数。第二部分讲凸优化的判断标准和标准形式重点是怎么把一个实际问题翻译成凸优化问题。第三部分讲常见的求解算法和工程实操包括梯度下降、近端梯度、拟牛顿法以及怎么用现成的求解器。第四部分集中整理实际项目中遇到的典型坑并给出排查思路。这样走下来你面对一个新问题时脑子里会有个清晰的三步曲先确定噪声模型和先验写出目标函数再检查目标函数是不是凸的如果不是就做变量替换、松弛或改写最后选合适的求解策略并设定合理的停止条件和初值。2. 参数估计的核心框架目标函数怎么长出来的2.1 最大似然估计从概率世界到代价函数最大似然估计的核心思想不复杂给定一堆观测数据找一组参数使得在这组参数下观测到这些数据的概率最大。这句话听起来像是废话但落地时有个关键操作——你得先写出“在参数theta下观测到数据”的概率分布长什么样。假设观测误差服从零均值高斯分布方差为sigma²那么单次观测的似然函数可以写成高斯密度函数。多个独立观测的联合似然就是各密度函数相乘。但相乘在数值上容易下溢所以我们通常取负对数把乘法变成加法于是得到了一个平方和形式的目标函数。负对数似然 sum((观测值 - 预测值)²) / (2 * sigma²) 常数项这就是最小二乘的来历。你看这里没有任何人“拍脑袋”说我们要用平方误差它是从高斯噪声假设里自然推导出来的。这也是为什么我强调“先建模、再优化”——目标函数不是随便选的它是你的噪声假设和先验假设的直接体现。2.2 最大后验估计把先验变成正则化最大似然只考虑数据不考虑参数本身的分布。但很多实际场景里我们对参数是有些先验认识的。比如在SLAM里我们知道相邻两帧之间的位姿变化不会特别离谱在机器学习里我们知道权重参数不应该太大。这时候就该用最大后验估计。最大后验估计的目标函数是“观测数据的负对数似然”加上“参数先验的负对数”。如果先验是高斯分布这一项就等价于L2正则化如果先验是拉普拉斯分布这一项就等价于L1正则化。这个视角特别有用因为它可以解释机器学习里很多让人困惑的做法。比如为什么Lasso能产生稀疏解因为拉普拉斯先验在零点的密度特别高峰值是尖的所以后验分布的最大值更容易落在某个参数恰好为零的位置。从凸优化的角度看L1范数在零点不可导这种不可导性恰恰是稀疏性的来源。2.3 鲁棒估计当噪声不是高斯时该怎么办高斯假设虽然好算但真实数据里经常有野值。相机标定时的误匹配、传感器偶尔的跳变、人工标注里的错误这些都是野值。如果继续用最小二乘野值会以平方级的速度主导目标函数把整个估计结果拉偏。鲁棒核函数就是干这个用的。Huber核、Cauchy核、Tukey核它们的基本思路都一样对小误差保持平方惩罚对超大误差降低惩罚的增长速度让野值不会支配整个优化。从参数估计的角度看这相当于换了一个重尾的噪声分布假设。但要注意一旦换了鲁棒核目标函数可能就不再是凸的了。Huber核在凸性上表现还好但Cauchy核可能会导致局部极小值。这是个很实际的问题后续会在“常见问题”部分展开讲。3. 凸优化什么样的优化问题才算“好”3.1 凸集、凸函数和凸问题的判断很多做工程的朋友一听到凸优化就以为要记一堆数学定义但其实判断标准特别朴素。一个函数是凸函数通俗地讲就是函数图像上任意两点连线这条线都落在函数图像的上方。二维情况下就是曲线是“碗状”的不会出现多峰。一组约束是凸集通俗地讲就是集合里任意两点连线线段仍然全部落在集合内部。不会出现凹进去的形状。带约束的凸优化问题的标准形式长这样最小化目标函数 f(x) 满足约束 g_i(x) 0 等式约束 h_j(x) 0其中f和所有g_i必须是凸函数所有h_j必须是仿射函数直线/平面。只要满足这个结构问题就有非常好的性质局部最优解就是全局最优解。3.2 从目标函数反推问题的凸性看一个目标函数是否凸有几个快速判别的实用技巧二次型 x^T A x当A是半正定矩阵时是凸函数。范数都是凸函数。凸函数的非负加权和仍然是凸函数。凸函数与仿射映射的复合仍然是凸函数。如果函数的海森矩阵二阶导数矩阵在定义域内是半正定的函数就是凸的。我做一个实际判断时通常先看目标函数能不能拆成“几个凸函数的和”再看有没有非凸的成分。比如最小二乘的平方项展开后是二次型只要数据矩阵是满秩的或者加了足够强的正则项海森矩阵就是正定的问题就是凸的。3.3 非凸问题怎么凑成凸问题现实世界很多问题不是天然凸的。SLAM里的位姿优化旋转矩阵构成的流形是非凸的带有离群值剔除的估计问题组合优化部分也是非凸的。但实践中我们有很多操作手段变量替换比如把旋转矩阵参数化改写为旋转向量在某些局部范围内可以减少非凸性。松弛比如把整数变量松弛成连续变量把L0范数松弛成L1范数。剔除先用鲁棒方法求一个初步解剔除野值后再用精确模型重新估计。有一说一松弛和替换不是万能的有时会偏离原问题太远导致结果不可用。我的经验是尽量保留问题的主要结构在次要部分做松弛并且始终记得在找完松弛解之后回到原问题里做一次校验确认这个解在原问题里也说得通。4. 实操路线从建模到求解的完整流程4.1 建模先写目标函数再谈优化不管用什么求解器第一步都是把问题写成目标函数和约束的形式。这一步做得好后面全顺做得差后面全是坑。有一次我需要做一个传感器标定的项目目标是根据一组已知距离和角度测量值估计传感器的安装位置。我最初的建模直接用了原始测量方程结果目标函数里含有反正切函数在某些区域导数变化剧烈。后来我把测量方程改写了一下先用极坐标和直角坐标之间的转换关系把问题变成带等式约束的优化问题再用罚函数法把约束处理掉目标函数立刻变成了一个漂亮的凸二次函数求解速度和稳定性都上来了。4.2 求解器的选择与参数设置现在工具链很成熟没有必要每次都自己写优化算法。我平时用到的几种方式CVXPY适合中小规模、需要快速建模验证的问题特别是带约束的凸优化问题。它自带DCPDisciplined Convex Programming检查能帮你确认问题是不是凸的。SciPy optimize适合中等规模、不需要严格凸性的问题但对初值和参数缩放比较敏感。Ceres Solver / g2o适合SLAM、视觉重建这类大规模非线性问题但需要你自己提供残差和雅可比。PyTorch / TensorFlow 的自动求导适合目标是深度学习组件、需要对复合函数求梯度的情况但收敛性不能保证。求解参数上我最常调的是最大迭代次数、梯度阈值、参数变化阈值。实操中最大迭代次数设个几千就够但梯度阈值不要设得太小——设成1e-8反而会导致迭代很长时间有时候1e-4或者1e-5就够了省下的时间够你多跑好几个实验。4.3 一段可直接运行的最小示例为了让你更直观地理解整个过程我给一个最小二乘估计的小例子。假设我们有N个采样点目标是拟合一条直线噪声为高斯分布。这个问题的最大似然估计就是最小二乘我们用CVXPY来求解import cvxpy as cp import numpy as np # 模拟数据 np.random.seed(42) x_data np.linspace(0, 10, 50) true_k 2.5 true_b 1.0 y_data true_k * x_data true_b np.random.normal(0, 1.0, sizex_data.shape) # 定义优化变量 k cp.Variable() b cp.Variable() # 目标函数最小化残差平方和 objective cp.Minimize(cp.sum_squares(k * x_data b - y_data)) # 求解 problem cp.Problem(objective) problem.solve() print(f估计的斜率 k {k.value:.3f}, 截距 b {b.value:.3f})跑完你会发现只要数据量够估计出来的k和b会非常接近真实值2.5和1.0。这个例子小但内部机制和大型SLAM优化是一样的建模成最小二乘、调用求解器、得到参数估计值。如果想让结果更接近实际工程可以加上权重矩阵也就是测量噪声信息矩阵的逆。这时候目标函数变成加权最小二乘代码改动很小但含义完全不同——你是在告诉优化器哪些测量值更可信。# 假设每次测量的方差已知 sigma np.abs(0.5 np.random.rand(x_data.shape[0])) weights 1.0 / (sigma ** 2) objective cp.Minimize(cp.sum(cp.multiply(weights, cp.square(k * x_data b - y_data))))4.4 算法调试中的关键指标在算法跑起来之后不能只看最终结果准不准还要盯几个过程指标目标函数值曲线应该平滑下降如果出现震荡或上升说明步长太大或者目标函数有问题。梯度范数应该单调下降如果卡在一个值上不动可能到了不可导点或者数值精度不够。参数变化量如果参数变化很小但梯度还很大说明问题可能是病态的。约束违背量带约束问题要专门看约束的违背程度而不是只看目标函数。这些指标所占的权重不一样但至少要监控目标函数值和梯度范数。不监控这两个你无法判断求解是否正常全凭运气。5. 常见问题与排查技巧实录5.1 收敛极慢迭代半天还在原地踏步这是最常见的——目标函数在下降但下降速度像蜗牛。通常原因有这几个步长太小、初值离最优点太远、条件数太大。步长太小好解决加大步长或换用自适应步长的方法比如Adam、Adagrad。初值离最优点太远可以通过归一化特征、中心化数据来缓解。条件数太大是真麻烦特征之间的尺度差异悬殊比如一个特征量级是0.001另一个是100000海森矩阵的条件数就会巨大梯度下降在长轴和短轴之间来回震荡。解决方法是做特征缩放或者直接换用二阶方法比如L-BFGS。5.2 迭代发散目标函数值越跑越大如果目标函数不是单调下降反而爆炸先别急着调算法看一下是不是目标函数本身写错了。我有一次就是代码里把残差矩阵的维度搞错了导致计算出来的“目标函数”根本不是原来建模的目标函数发散得莫名其妙。排除代码错误后最可能的原因是步长过大导致迭代跳过了“碗”的边缘直接掉到外面。梯度下降里这一步的判断标准是Armijo条件或者Wolfe条件简单说就是“每一步下降的量要足够大且梯度变化要足够平滑”。这些条件不满足就减小步长。还有一种可能是目标函数非凸初始点正好落在一个“尖峰”附近梯度方向指向的是另一个更差的山谷。这种情况就要换初值、加正则化或者用信任域方法而非线搜索方法。5.3 约束处理不好解始终不满足约束带约束的问题最容易出问题。CVXPY这类工具会有DCP检查但如果用的是通用优化器约束靠罚函数的时候罚系数选不好就会出问题。罚系数太小约束会被狠狠违背罚系数太大目标函数会被惩罚项主导原问题反而没解好。实操中我的策略是两步走先用小罚系数算出一个大致区域再逐步增大罚系数重新优化让解逐渐“落入”可行域内。如果有等式约束更推荐用拉格朗日法或者增广拉格朗日法。虽然实现上稍微复杂但比单纯罚函数法更稳健。5.4 野值导致结果整体偏移这个前面提到过一旦处理不当会让整个估计偏离真实值。在参数估计里解决野值的经典思路有两层第一层是数据预处理。拿视觉特征匹配来说先用RANSAC做粗剔除把明显不匹配的特征对删掉再拿剩下的内点去做精确估计。第二层是在目标函数里嵌鲁棒核。Huber核在工业界用得最多实现简单而且对凸性的破坏相对小。我个人的经验是两层都要做先粗筛再精估计。单靠鲁棒核误差特别大的野值还是会影响结果单靠粗筛阈值设得不好又会把好点误杀。5.5 数值问题矩阵病态病态矩阵的特征是参数稍微变一点目标函数变化极大或者反过来参数大变但目标函数几乎不变。这在最小二乘里特别常见尤其是信息矩阵接近奇异的时候。最简单的处理方式是加一个小的正则项到矩阵对角线上也就是L2正则。这个操作在线性代数里叫Tikhonov正则化在统计里叫岭回归。实际操作中给信息矩阵加一个lambda * Ilambda取个很小的数比如1e-6就能显著提升数值稳定性对最终结果的影响几乎可以忽略。6. 一些个人的实操体会网上关于参数估计和凸优化的教材很多Boyd那本《Convex Optimization》确实是经典但我真心不建议非数学背景的人从头啃。我自己的学习路径是反着来的先在实际项目里踩坑再回去补理论。比如先遇到一个拟合问题发现直接用最小二乘解不稳定再去学岭回归、病态矩阵的概念先遇到一个SLAM问题发现加上鲁棒核效果特别好再回头去读Huber核的数学性质。带着问题学理论比纯啃书效率高得多。另外一点体会是做参数估计时永远不要直接跳到优化算法那一层。先把数据分布想清楚把噪声模型和先验写下来再让目标函数从这些假设里“长出来”。绝大多数工程问题的求解困难追根溯源都是目标函数建得不对或者不匹配。最后如果只是想快速上手建议花一个下午把CVXPY里几个经典例子跑一遍包括最小二乘、带约束的线性规划、带有L1正则的稀疏估计。跑完这些你对“建模-求解-验证”这条链路就会有肌肉记忆了。后面再遇到具体问题无非是在这个框架里填细节而已。