ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

MATLAB非线性最小二乘拟合:lsqcurvefit与lsqnonlin实战详解

MATLAB非线性最小二乘拟合:lsqcurvefit与lsqnonlin实战详解 简介面向需要做曲线拟合、参数估计的科研人员、工程师以及相关课程学生这份源码包提供MATLAB求解非线性最小二乘法拟合问题的整套实现。项目基于MATLAB实现无约束条件下非线性最小二乘求解通过主函数与辅助函数配合演示从模型构建、算法调用到结果输出的完整流程适合新手入门也可供有经验者快速复用。压缩包共3个文件包括2个m脚本和1个docx文档分别承担核心算法实现、配套计算函数以及方法说明与使用步骤整体仅13KB轻量易部署。目前已有4172人浏览学习可直接下载后运行调试。源码已经过校正测试附有说明文档若遇到报错可联系作者指导或更换。借助该资源读者能理解最小二乘拟合法在非线性问题中的编程实现思路掌握MATLAB函数调用与脚本组织方法。1. 非线性最小二乘法拟合MATLAB里为什么不能直接套polyfit手里有一组实验数据理论模型又是明确的非线性函数——酶促反应的Michaelis-Menten方程、电池放电的指数衰减曲线或传感器标定常见的y a·(1−e^(−bx))。用polyfit硬凑高次多项式曲线能穿过所有点系数却没有物理意义外推几步就失真。这正是非线性最小二乘的场景模型形式已知、参数未知要让模型输出与观测值的残差平方和最小。残差对参数非线性没有闭式解必须迭代逼近。MATLAB里的常规武器是优化工具箱的lsqcurvefit和lsqnonlin一个直接吃xdata和ydata一个操作残差向量。下面按原理、选型、可运行代码、调参、验证的顺序把这条路走通。2. 非线性最小二乘的数学原理与MATLAB优化工具箱选型2.1 残差平方和、雅可比矩阵与Levenberg-Marquardt迭代线性最小二乘里y Xβ目标函数S(β) ‖y − Xβ‖²对β求导置零一次矩阵运算就得到正规方程XᵀXβ Xᵀy这就是polyfit能直接出结果的原因。非线性模型y f(x, β)对参数β不线性写不成固定设计矩阵目标函数变成S(β) Σᵢ(yᵢ − f(xᵢ, β))²求导后得到的是一组非线性方程只能走数值迭代。通用的做法是在当前参数βₖ处把f做一阶泰勒展开f(x, β) ≈ f(x, βₖ) Jₖ(β − βₖ)。J是残差对参数的雅可比矩阵维度是n×p数据点数×参数个数展开之后局部变成一个线性最小二乘子问题。Gauss-Newton法的更新方程是(JᵀJ)Δβ −Jᵀr其中r是当前残差向量解出Δβ后更新参数并重复。JᵀJ在参数相关性强的模型里会接近奇异步长一旦失控迭代就发散。Levenberg-Marquardt法在法方程里加了一个阻尼项(JᵀJ λI)Δβ −Jᵀr。λ大时方程退化成梯度下降λ小时回到Gauss-Newton求解器每轮根据实际下降量自动调整λ。MATLAB的lsqcurvefit默认走trust-region-reflective算法它维护一个信赖域半径思路和阻尼项一致且天然支持参数的lb/ub边界约束。理解这层数学的直接收益有两个。一是知道初值为什么重要迭代从β₀出发目标函数存在多个局部极小点时初值决定你收敛到哪个坑里。二是知道怎么读迭代输出exitflag、一阶最优性、残差范数这些信号全是在描述迭代是否在稳定下降、终止条件是否满足。2.2 lsqcurvefit和lsqnonlin拟合入口怎么选两个函数都来自优化工具箱核心求解器相同差别在接口。选错入口不会出错但代码会绕。下面这张表把差别列清楚对比项lsqcurvefitlsqnonlin传入函数fun(beta, xdata) 返回预测值fun(beta) 返回残差向量数据接口自动用ydata算残差残差在函数内自己写典型场景数据拟合、参数估计方程求解、自定义残差边界约束lb/ublb/ub加权写法需改写模型或数据残差直接除以标准差最顺手lsqcurvefit本质上是lsqnonlin的包装它内部把fun(beta, xdata) − ydata当作残差交给同一套求解核心所以两者的拟合结果是数值一致的。下面这段等价关系能直接看出内部结构% lsqcurvefit 调用方式 fun (beta, t) beta(1) * exp(-beta(2) * t) beta(3); beta_cf lsqcurvefit(fun, beta0, t, y_obs, lb, ub); % 等价于 lsqnonlin 的残差写法 resfun (beta) fun(beta, t) - y_obs; beta_nl lsqnonlin(resfun, beta0, lb, ub);选型判断很直接手上是(x, y)观测点、模型形式明确用lsqcurvefit代码量最少残差需要自己组合、或者要拟合的是隐式方程F(x, y, β) 0用lsqnonlin。统计工具箱里的fitnlm是第三个选项它基于迭代加权最小二乘能直接给出p值和系数置信区间适合偏统计推断的场景但模型要走公式对象定义灵活度不如lsqcurvefit工程拟合里我用得不多。2.3 模型可辨识性过拟合和欠拟合的边界拟合质量的上限往往不在算法而在模型结构。数据点数是n、待估参数是p自由度是n − p。p逼近n时模型开始记忆噪声而不是描述规律这是过拟合模型结构本身错了比如用线性函数去拟合一条明显饱和的曲线残差怎么压都压不下去这是欠拟合。两者都不该靠肉眼判断要用残差和参数置信区间说话。可辨识性更容易被忽略。两个参数强相关时数据里没有足够信息把二者分开。比如y a·e^(−bt) c如果观测区间很短、e^(−bt)全程接近1那么a和c同时增大、一增一减都能得到几乎相同的拟合曲线算法给出的唯一解只是数值意义上的。判断可辨识性要放到拟合完成之后看参数置信区间宽度和雅可比矩阵的条件数这就是最后一章要做的事。3. lsqcurvefit拟合源程序最小可运行示例与参数说明3.1 最小可运行示例带噪声的指数衰减模型拟合直接给一版能跑的代码。场景是电池放电曲线拟合模型形式y β₁·e^(−β₂·t) β₃β₁是初始幅值β₂是衰减速率β₃是基线电压。先用模拟数据验证流程% 生成模拟数据指数衰减 常数基线 t (0:0.1:5); y_true 3.2 * exp(-0.7 * t) 0.8; rng(7); % 固定随机种子结果可复现 y_obs y_true 0.15 * randn(size(t)); % 加高斯噪声模拟实测 % 模型函数beta(1)*exp(-beta(2)*t) beta(3) fun (beta, t) beta(1) * exp(-beta(2) * t) beta(3); % 初值、下界、上界 beta0 [2, 0.2, 0.5]; lb [0, 0, 0]; ub [10, 5, 2]; % 调用 lsqcurvefit [beta_est, resnorm, residual, exitflag] ... lsqcurvefit(fun, beta0, t, y_obs, lb, ub); fprintf(beta [%.3f, %.3f, %.3f]\n, beta_est); fprintf(残差平方和 %.4f, exitflag %d\n, resnorm, exitflag);代码里的关键参数逐一说明fun是模型函数句柄第一个入参必须是待估参数向量beta第二个入参是xdata向量返回预测值beta0是初值向量维度必须和beta一一对应xdata和ydata是同尺寸的列向量MATLAB允许传矩阵但列向量是最稳妥的写法lb、ub是参数下界和上界维度与beta0一致某个参数不想约束就填-inf或inf。输出里beta_est是估计参数resnorm是最优残差平方和residual是每个点的残差exitflag是收敛标志大于0表示正常收敛负值对应各种失败原因。如果数据存在.mat文件里先load再取字段和这里构造的t、y_obs没有区别。加了约束的拟合会走trust-region-reflective路径比无约束版本多一次投影到可行域的操作速度略慢但在工程规模下几乎无感。3.2 匿名函数与函数文件两种模型写法的取舍模型短、参数少匿名函数最省事。但匿名函数有个隐蔽的坑它会捕获定义时工作区里的所有自由变量。比如上面的fun里用了beta(1)、beta(2)如果模型里还出现另一个变量k那个k会被固化在函数句柄里之后你改了工作区的k拟合结果却纹丝不动。排查这种问题很费时间。模型要复用、要在多个脚本里调用或者函数体超过两三行写成函数文件function yhat mymodel(beta, xdata) % 指数衰减 基线模型 yhat beta(1) * exp(-beta(2) * xdata) beta(3); end调用时把句柄传给lsqcurvefit即可[beta_est, resnorm] lsqcurvefit(mymodel, beta0, t, y_obs, lb, ub);。函数文件名必须和函数名一致且在工作路径或MATLAB路径下。相比匿名函数函数文件的优势在于可以写注释、做输入检查还能在内部临时变量上打断点调试代价是多一个文件模型短时不划算。3.3 边界约束、缺失值与数量级预处理lb/ub是硬约束适合物理上不允许为负的参数比如速率常数、浓度。注意边界不能写反——lb必须逐项小于ub否则lsqcurvefit直接报错。还有一个容易踩的坑算法收敛到边界值本身不一定错但要警惕最优解贴在边界上是否意味着模型或数据有问题比如初值给歪了参数被边界硬推回可行域结果resnorm明显偏大。缺失值处理是另一个高频问题。lsqcurvefit遇到NaN会直接失败报错信息还看不出来源。稳妥做法是拟合前统一清洗% 剔除 xdata 或 ydata 中的 NaN / Inf valid isfinite(xdata) isfinite(ydata); xdata xdata(valid); ydata ydata(valid);数量级也是实战里常出问题的地方。如果y的量级是10⁶而参数又是10⁻⁵雅可比矩阵各列量级差十几个数量级JᵀJ会病态到无法求逆。我一般会对数据进行归一化把y除以一个特征量比如最大值或均值拟合完成后再把参数换算回去。这个预处理付出的成本极小却能把很多莫名不收敛的问题消灭在源头。4. 拟合调参实战初值估计、加权残差与算法选项4.1 初值估计的三种可靠做法初值决定了非线性最小二乘最后收敛到哪个局部极小点。没有通用解但有三种工程上最高频的做法。第一种是线性化变换。模型y a·e^(−bt)里如果基线c已知两边取对数得到ln(y − c) ln a − bt变成参数ln a和b的线性回归直接用regress或polyfit算出初值。这个方法快且可靠但注意它改变了误差结构——对数变换会放大y接近c处的噪声所以线性化得到的初值只用作迭代起点不作为最终结果。第二种是网格粗扫。参数维度不高时在合理范围内撒一个粗网格每个点算一次残差平方和取最小的点作为初值。三维参数大约几千次计算毫秒级完成best_rss inf; beta0 [2, 0.2, 0.5]; for a 0.5:0.5:5 for b 0.1:0.1:1.5 for c 0:0.2:2 rss sum((y_obs - (a * exp(-b * t) c)).^2); if rss best_rss best_rss rss; beta0 [a, b, c]; end end end end网格扫描的粒度决定了初值质量粒度太粗可能把真正的低谷漏掉。实际用法是网格粗扫一轮定位大致区域再在最优网格点附近缩小范围精扫一轮两轮下来就能给出接近全局最优的起点。第三种是直接从数据里读物理量。对饱和型或衰减型曲线数据的稳态均值就是基线β₃y最大值减去稳态值就是幅度β₁β₂可以从特征时间估算——衰减到1/e所需的时间t_c约等于1/β₂。这个方法依赖经验但往往比前两种更快engineer手里有背景知识时最实用。4.2 异方差数据下的加权最小二乘写法默认的最小二乘假设每个点的噪声方差相同。实际测量里这个假设经常不成立——比如传感器在量程高段的绝对误差更大或者测量值越小时相对误差越显著这就叫异方差。直接拟合的话大噪声区间的点会在目标函数里占过大的权重拉偏参数。正确的做法是给每个残差按标准差缩放。lsqcurvefit没有单独的weights参数常见解法是转用lsqnonlin在残差函数里直接除以标准差% 假设噪声标准差随幅值线性增大体现异方差 sigma_obs 0.05 * abs(y_true) 0.02; % 加权残差除以标准差把每个点缩放成单位方差 model (beta, t) beta(1) * exp(-beta(2) * t) beta(3); resfun (beta) (model(beta, t) - y_obs) ./ sigma_obs; opts_w optimoptions(lsqnonlin, Display, final); beta_w lsqnonlin(resfun, beta0, lb, ub, opts_w);除以sigma_obs等价于在目标函数里给每个点乘以权重1/sigma_obs²大噪声点的贡献被压下去。一个验证加权是否起作用的方法拟合完成后画出残差除以sigma_obs的归一化残差图如果点在零线附近均匀散布且幅度接近1说明噪声模型与数据匹配。4.3 算法选项trust-region-reflective还是levenberg-marquardtlsqcurvefit提供两种算法选项。默认的trust-region-reflective支持lb/ub边界约束也支持稀疏雅可比levenberg-marquardt在无约束问题时收敛通常更快但设置了边界时该选项会被内部处理掉边界或者直接忽略约束行为不如前者明确。选项默认值什么时候调Algorithmtrust-region-reflective无边界且想试更快收敛时换levenberg-marquardtMaxFunctionEvaluations100×参数个数复杂模型报错超过函数评价次数时增大MaxIterations400迭代到上限仍未收敛时增大FunctionTolerance1e-6目标函数下降量小于该值即停止想更精确就调小StepTolerance1e-6步长小于该值即停止和FunctionTolerance配合使用Displayfinal想看每轮迭代过程改成iter实际设置的写法如下。opts optimoptions(lsqcurvefit, ... Algorithm, trust-region-reflective, ... MaxFunctionEvaluations, 5000, ... MaxIterations, 500, ... FunctionTolerance, 1e-10, ... StepTolerance, 1e-10, ... Display, final); [beta_est, resnorm, residual, exitflag, output] ... lsqcurvefit(fun, beta0, t, y_obs, lb, ub, opts);容差设置不是越小越好。FunctionTolerance和StepTolerance调到1e-10级别时迭代会在参数空间里做大量小幅游走计算时间成倍增加但参数的数值改善往往微乎其微。工程上1e-6到1e-8足够只有做基准测试或严格对比时才需要更高精度。4.4 收敛失败时先看这四个信号不收敛时错误提示往往含糊。按下面顺序排查定位最快。第一看exitflag。它小于等于0时output.message里会写终止原因最常见的是函数评价次数超过上限和步长小于步长容差但未满足最优性条件。前者加MaxFunctionEvaluations后者说明卡在平缓区域多半是初值或模型结构问题。第二看output.firstorderopt。这是一阶最优性指标衡量梯度范数数值越小说明越接近稳定点。如果迭代结束后这个值仍然很大说明离真正的最优点还很远初值给偏了。第三看参数是否撞上边界。beta_est里某个值正好等于lb或ub对应项说明模型想让参数越过边界真实最优解在约束之外。这时要重新审视边界设置是否合理而不是简单地把边界放宽。第四看雅可比矩阵的条件数。cond(J)超过1e6量级时参数间的相关性非常强即使exitflag大于0参数估计的方差也很大。此时模型本身的可辨识性有问题该做的是简化模型或缩小参数范围而不是继续压容差。5. 拟合质量验证残差正态性与参数置信区间的判断技巧5.1 残差图先确认模型结构再谈参数参数算出来只是第一步拟合质量要用残差说话。把残差对自变量画出来正常情况下应该围绕零线随机分布没有趋势、没有喇叭形展开。有趋势说明模型结构漏了一项比如指数衰减模型漏掉了线性漂移项喇叭形说明异方差要做加权。再进一步可以做残差的Q-Q图或直接看直方图粗略判断是否符合正态分布因为最小二乘的置信区间推导依赖正态误差假设。% 拟合后的残差图 figure plot(t, residual, o, MarkerSize, 6) hold on yline(0, k--, LineWidth, 1); xlabel(t); ylabel(residual); title(残差随时间分布); % 归一化残差的直方图 std_res residual / std(residual); figure histogram(std_res, 20); xlabel(标准化残差); ylabel(频数);5.2 用nlparci算置信区间判断参数可辨识性残差图验证完结构下一步是参数的置信区间。lsqcurvefit可以直接返回雅可比矩阵再交给nlparci计算每个参数的95%置信区间% jacobian 是拟合终点的雅可比矩阵 [beta_est, resnorm, residual, exitflag, ~, ~, jacobian] ... lsqcurvefit(fun, beta0, t, y_obs, lb, ub, opts); % 计算95%置信区间 ci nlparci(beta_est, residual, jacobian, jacobian); disp([beta_est, ci]);nlparci内部用残差方差估计s² resnorm / (n − p)再结合(JᵀJ)⁻¹构造协方差矩阵区间宽度本质上是参数不确定性的量化。判断规则有三条区间不含零说明该参数对数据有显著解释力区间宽度和参数值同数量级说明数据对参数的约束力正常区间宽度远超参数值本身说明数据里根本没有足够信息固定这个参数——这正是过拟合或参数冗余的信号。一个直接见效的验证技巧把区间最宽的那个参数固定成常数删掉它重新拟合如果剩余参数的置信区间明显收紧说明原模型确实过度参数化。与其增加参数压低resnorm不如删掉冗余参数换回可辨识性。这个判断方式比盯着决定系数R²看小数点后几位可靠得多。本文还有配套的精品资源点击获取
返回列表