利用scipy.optimize.least_squares实现鲁棒最小二乘优化:抑制噪声的实战技巧

张开发
2026/4/6 15:30:52 15 分钟阅读

分享文章

利用scipy.optimize.least_squares实现鲁棒最小二乘优化:抑制噪声的实战技巧
1. 为什么需要鲁棒最小二乘优化做数据拟合的朋友们肯定都遇到过这种情况明明模型设计得很完美算法也选得没问题但就因为数据里混进了几个捣蛋鬼异常值整个拟合结果就歪到姥姥家去了。这种情况在工程实践中特别常见比如传感器采集的数据偶尔会出现跳变或者图像处理时某些像素值明显偏离正常范围。传统的最小二乘法就像个老实人对所有数据点都一视同仁。离群点稍微大点声它就被带跑偏了。这时候就需要鲁棒最小二乘优化来救场了——它能自动降低异常值的影响力让拟合结果更加稳定可靠。scipy.optimize.least_squares这个神器自带了多种抗噪声的武器loss参数我们不用自己从头造轮子只需要选对武器就能轻松应对各种噪声场景。接下来我就带大家实战演练几种常用的抗噪策略。2. 认识least_squares的loss参数2.1 内置损失函数大盘点least_squares提供了6种现成的损失函数通过loss参数就能一键切换linear默认选项就是传统的平方损失对异常值零容忍huber温和派小误差按平方处理大误差按线性处理soft_l1平滑版huber过渡更自然cauchy重尾分布爱好者对异常值极其宽容arctan反正切函数比cauchy更克制一些hard_l1硬核版huber设定阈值后直接截断from scipy.optimize import least_squares # 使用huber损失的典型调用方式 result least_squares(fun, x0, losshuber, f_scale1.0)这里的f_scale参数特别关键它决定了多大误差算异常值。我一般会先用默认值跑一次看看残差的尺度范围再调整f_scale到残差标准差的1.5倍左右。2.2 损失函数的选择策略根据我的实战经验可以按这个流程图来选数据看起来干净 → 直接用linear最快有少量离群点 → huber或soft_l1大量噪声且分布未知 → cauchy明确知道误差阈值 → hard_l1举个实际案例去年做无人机定位时GPS数据偶尔会突然跳变几十米。用默认linear拟合时轨迹会出现明显的尖刺换成huber后这些跳变点被自动降权轨迹平滑得就像专业摄影师拍的延时摄影。3. 实战传感器数据校准3.1 问题描述假设我们有个温度传感器采集了100个温度读数。由于电磁干扰其中有5个读数明显异常偏差超过10℃。现在需要用二次曲线拟合传感器的校准曲线。首先生成模拟数据import numpy as np np.random.seed(42) # 正常数据 x np.linspace(0, 10, 95) y_true 2 0.5 * x - 0.1 * x**2 y_clean y_true np.random.normal(0, 0.2, sizelen(x)) # 异常值 x_outlier np.linspace(2, 8, 5) y_outlier y_true[:5] np.random.uniform(8, 12, size5) # 合并数据 X np.concatenate([x, x_outlier]) Y np.concatenate([y_clean, y_outlier])3.2 不同损失函数对比定义残差函数和初始猜测def residual(params, x, y): a, b, c params return y - (a b*x c*x**2) x0 [1, 1, 0] # 初始猜测参数现在来对比三种情况默认linear损失huber损失cauchy损失# 传统最小二乘 res_linear least_squares(residual, x0, args(X, Y), losslinear) # Huber损失 res_huber least_squares(residual, x0, args(X, Y), losshuber, f_scale0.5) # Cauchy损失 res_cauchy least_squares(residual, x0, args(X, Y), losscauchy)把拟合曲线画出来对比明显能看到linear拟合被异常值拉偏了曲线整体上移huber和cauchy都较好地抵抗了干扰其中cauchy对极端值更宽容在参数估计误差上huber比cauchy更接近真实值(2, 0.5, -0.1)4. 高级技巧与调参经验4.1 f_scale的黄金法则f_scale这个参数相当于告诉算法超过这个尺度的误差可能就是异常值。经过多次实验我总结出几个经验先用linear拟合计算残差的中位数取中位数的1.5-2倍作为f_scale初始值观察拟合结果如果还有明显偏差适当调大f_scale对于cauchy损失f_scale可以设得更小些# 自动计算f_scale的示例 temp_result least_squares(residual, x0, args(X, Y), losslinear) mad np.median(np.abs(temp_result.fun)) # 计算中位数绝对偏差 f_scale 1.4826 * mad # 换算成标准差估计4.2 方法选择的三重奏least_squares还有method参数控制优化算法与loss配合会产生不同效果trf默认适合中小规模问题对边界条件处理得好lmLevenberg-Marquardt适合无约束问题速度快dogbox介于两者之间内存消耗小我的常用组合是数据量1000trf huber1000-10000dogbox soft_l1带约束条件trf hard_l14.3 自定义损失函数如果内置函数都不满足需求还可以自己实现def custom_loss(residual, a1.0, b2.0): r residual / a return b**2 * (np.sqrt(1 (r/b)**2) - 1) res least_squares(residual, x0, losscustom_loss, kwargs{a:0.5, b:1.0})这种自定义方式在处理特殊噪声分布时特别有用比如同时存在高斯噪声和椒盐噪声的场景。5. 工程实践中的避坑指南5.1 初始值选择的艺术鲁棒优化虽然抗噪能力强但对初始值依然敏感。我常用的初始化策略用RANSAC算法先滤除明显异常点对剩余数据用linear拟合得到初始值用这个初始值启动鲁棒优化from sklearn.linear_model import RANSACRegressor # 第一阶段RANSAC去噪 ransac RANSACRegressor() ransac.fit(X.reshape(-1,1), Y) inlier_mask ransac.inlier_mask_ # 第二阶段干净数据拟合 clean_X X[inlier_mask] clean_Y Y[inlier_mask] x0 least_squares(residual, [1,1,0], args(clean_X, clean_Y)).x # 第三阶段鲁棒优化 final_result least_squares(residual, x0, args(X,Y), losshuber)5.2 收敛判据的调整有时候算法会提前收敛到次优解可以调整这些参数xtol参数变化容忍度默认1e-8可放宽到1e-6ftol函数值变化容忍度默认1e-8可放宽到1e-6max_nfev最大函数调用次数默认100*(n1)result least_squares( residual, x0, args(X,Y), losssoft_l1, xtol1e-6, ftol1e-6, max_nfev500 )5.3 结果验证的必做项每次拟合完我必做三件事绘制残差分布图看是否还有明显异常计算拟合优度R²用交叉验证检查过拟合# 残差分析 plt.hist(result.fun, bins30) plt.title(Residual Distribution) # 拟合优度 ss_res np.sum(result.fun**2) ss_tot np.sum((Y - np.mean(Y))**2) r_squared 1 - (ss_res / ss_tot)6. 性能优化技巧当数据量达到10万级以上时这些技巧能显著提升速度使用稀疏雅可比矩阵开启并行计算采用增量式拟合# 稀疏雅可比示例 def jac_sparse(params, x, y): n len(x) Jac lil_matrix((n, 3)) Jac[:, 0] -1 Jac[:, 1] -x Jac[:, 2] -x**2 return Jac result least_squares( residual, x0, jac_sparsityjac_sparse, args(X,Y), losshuber )对于超大规模数据我会先用随机采样跑几次鲁棒优化确定大致参数范围后再用全部数据做精细优化。

更多文章