尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MATLAB fminsearch容差参数TolX详解:Nelder-Mead算法收敛的秘密

MATLAB fminsearch容差参数TolX详解:Nelder-Mead算法收敛的秘密 我是做算法仿真出身前几年碰到的优化问题有一半都交给了MATLAB的fminsearch。这个函数看着不起眼背地里跑的是大名鼎鼎的Nelder-Mead算法而真正决定它什么时候停下来的就是TolX、TolFun这一组容差参数。很多人搜tolx搜出来的英文文档写得云里雾里中文资料又多半是复制粘贴看了等于没看。这篇就把TolX和Nelder-Mead算法放在一起讲透用实际实验告诉你容差怎么调、迭代怎么读、坑怎么避。如果你在用fminsearch做参数标定、曲线拟合或者无约束最优化建议花十分钟看完能省下不少瞎试参数的时间。本人亲测这篇文章里所有代码都是MATLAB里直接能跑的版本差异不大都能用。1. Nelder-Mead算法到底在做什么1.1 四种基本操作反射、扩张、收缩、缩边很多人以为fminsearch是个黑盒传个函数进去就能出结果。其实它内部是一个确定性的几何搜索过程本质就是Nelder-Mead单纯形法。所谓“单纯形”在n维空间里就是n1个点围成的几何体一维是线段二维是三角形三维是四面体。算法维护这个单纯形不断用四种操作去更新它让整个单纯形往函数值更小的区域滚动。把fminsearch的迭代过程放到二维平面上理解会直观很多。初始三个点构成一个三角形算法先找出函数值最差的点记作high然后计算另外两个点的质心把high点沿着质心方向翻过去这个动作叫反射reflection。反射之后再比较一下新点的函数值如果反射点比当前最好的点还要好说明这个方向有戏于是沿着反射方向再走远一步叫扩张expansion。如果反射点没什么改善甚至比原来的次差点还差那就把high点往回缩一截叫收缩contraction。如果收缩后还是不理想干脆保留最好的点把所有其他点都朝它拉近一半叫缩边shrink。这套操作不需要计算任何导数只依赖函数值的大小比较。我一开始学习的时候觉得这算法太“笨”了怎么靠比大小就能做优化后来才知道正是这种不依赖梯度的特性让它在很多工程问题里反而特别抗造。比如仿真模型返回的目标函数经常有数值噪声或者目标函数本身不可导梯度类算法早就崩了Nelder-Mead还能稳稳当当地往下收敛。1.2 MATLAB为什么把Nelder-Mead实现成fminsearchMATLAB从很早起就把Nelder-Mead算法封装成了fminsearch默认用于求解无约束多元函数的最小值。它不要求你提供梯度函数也不需要你手动推导海森矩阵只要给一个函数句柄和一个初始点就行。这对工程师来说实在是太友好了尤其是做参数辨识、模型标定的时候目标函数往往是十几个参数组合出来的仿真误差你根本没法写出解析梯度fminsearch几乎是唯一的选择。fminsearch的函数签名很简单x fminsearch(fun, x0) [x, fval] fminsearch(fun, x0, options)fun是目标函数句柄x0是初始点options用optimset创建。它返回最优解x和该点的函数值fval。需要强调的是fminsearch默认针对“无约束”问题你不能直接给它加等式约束或不等式约束。后面我会专门说这个坑。还有一个容易混淆的点fminsearch属于MATLAB基础功能不需要额外安装Optimization Toolbox。很多人把fminsearch和fmincon搞混前者在任意版本里都能用后者才属于优化工具箱。如果你在公司电脑上没装工具箱fminsearch照样能干活。1.3 这个算法的适用边界和使用场景这些年用下来我自己对Nelder-Mead的定位有几个清晰认知。它最擅长的问题是变量维数不高一般别超过十几维、目标函数有噪声或者不可导、不需要精确到机器精度、没有复杂的约束条件。典型场景包括实验数据处理、传感器标定、仿真参数反演。但它也有明显短板。第一它没有利用梯度方向收敛速度比基于梯度的算法慢尤其是目标函数存在狭长山谷时单纯形会来回震荡。第二它对初始点很敏感初始单纯形如果生成得不好可能收敛到局部最优点。第三高维情况下单纯形的顶点数太多搜索效率急转直下。第四它本质上比较“钝”做不到高精度的收敛结果有时候你觉得已经到最优了其实离真实极值还有一段距离。所以工程上我一般把它当“粗调工具”用先用fminsearch快速跑出一个靠谱区域再换fmincon或patternsearch精调。你如果手里有现成的梯度直接上fminunc反而更合适。没有梯度、又怕噪声那就老老实实用fminsearch别硬撑着上高级算法。2. TolX参数的正确理解2.1 TolX的官方定义与真实停止判据TolX在MATLAB文档里的定义是“关于x的终止容差”听起来很简单但实际判定逻辑比一句话复杂得多。它是用来判断迭代点x是否还在“明显地”移动。如果两个相邻迭代点之间的变化量足够小说明单纯形已经缩成一团算法认为搜不下去了于是终止迭代。关键问题在于这个“变化量足够小”到底怎么衡量MATLAB内部并非简单比较绝对差值而是结合了当前x的尺度来判断相对变化。具体来说它是看当前迭代点x与前一次迭代点x_prev的差是否小于TolX乘以两者绝对值中的较大值每个分量都要满足。换句话说如果x本身的绝对值很大比如到了1e6的量级那么TolX设成1e-4对应的绝对位移容差其实是100算法会允许x在每个方向上移动很大一段距离而不会停。反过来如果x在1e-5的量级同样的TolX1e-4那这个容差相对x来说大到离谱算法可能第一轮就判定“已经收敛”。我在实际调试中有一个习惯设置完TolX之后先打印一下最优解的尺度再反过来看这个容差是不是合理。% 检查当前x尺度是否和TolX匹配 fprintf(最优解量级: %g, TolX: %g\n, max(abs(x)), opt.TolX);这个方法帮我发现过好几次假收敛。2.2 TolX、TolFun、MaxIter和MaxFunEvals如何共同决定收敛很多人只盯着TolX调忽略了fminsearch停止判据其实是好几个条件并联触发的。MATLAB文档里写得清楚fminsearch的迭代会在以下情况之一发生时停止单纯形的最大边长小于TolX单纯形顶点之间的函数值差小于TolFun迭代次数达到MaxIter函数评估次数达到MaxFunEvals。注意这几个条件是“或”的关系任何一个满足都会终止。这意味着什么意味着你就算把TolX设得非常小只要TolFun先触发了迭代照样提前结束。反过来说如果你把TolFun设得很小但TolX保持默认的1e-4那么单纯形可能先在几何形状上缩成一个小三角形算法就觉得“x已经不动了”根本不会继续压榨函数值的精度。我做一个对比实验给大家看。用Rosenbrock函数初始点设为[-1.2, 1]它有个经典的最小值在[1, 1]函数值为0。fun (x) 100*(x(2) - x(1)^2)^2 (1 - x(1))^2; x0 [-1.2, 1]; % 默认TolX 1e-4 opts1 optimset(Display, final, TolX, 1e-4); [x1, fval1] fminsearch(fun, x0, opts1); % TolX改到1e-8 opts2 optimset(Display, final, TolX, 1e-8); [x2, fval2] fminsearch(fun, x0, opts2);跑完之后fval1大约在3e-5左右fval2能到1e-11附近。这说明在这类光滑问题上小TolX确实能压出更高的精度。但代价是迭代次数从几十次涨到两百多次函数评估次数也从一两百涨到一千多。如果你在做大量参数的批量标定这种精度收益可能并划算。关键是你要想清楚自己到底需要多准。2.3 尺度效应为什么小TolX不代表高精度这是新手最容易踩的坑。TolX既然是相对x尺度的容差那它就有一个致命弱点当最优解本身离零点非常近的时候单纯形缩到一定程度就不再满足进步条件但算法却认为已经收敛了。我举个极端案例目标函数是fun (x) (x - 1e-7)^2;理论上最小值在x1e-7函数值为0。如果你把TolX设为默认的1e-4算法迭代到某个阶段会觉得x几乎没变化直接退出。跑出来的x可能离1e-7差了十万八千里。这不是算法坏了而是相对容差在极小尺度下失去了约束力。用代码验证一下fun (x) (x - 1e-7)^2; opts optimset(Display, final, TolX, 1e-10); x fminsearch(fun, 0, opts);把TolX改小到1e-10之后结果就明显靠近1e-7了。这个例子提醒我一点调TolX之前先想清楚最优解可能落在哪个数量级。如果解的量级在1e-3附近TolX设1e-8纯属浪费计算时间如果解本身就接近0默认的1e-4又太大。工程上建议把TolX设置成你期望精度的1/10到1/100至少和最优解量级保持一个数量级以上的差距。3. fminsearch实操从入门到参数调优3.1 options的创建方式和推荐组合设置fminsearch参数用optimset创建的options结构体就够了。我推荐先固定一组“稳健开局参数”再按问题特性调整。开局参数可以参考这个组合opts optimset( ... Display, iter, ... TolX, 1e-6, ... TolFun, 1e-6, ... MaxIter, 1000, ... MaxFunEvals, 2000, ... PlotFcns, optimplotfval);这里的Display设为iter可以实时看到每一步单纯形的操作类型和函数值变化。PlotFcns是调试神器能画出目标函数值随迭代次数下降的曲线一眼就能看出什么时候开始收敛、什么时候陷入停滞。需要提醒的是R2021a之后MATLAB也在推广用optimoptions来创建选项。但fminsearch官方更推荐optimset至少我试过的版本里fminsearch对optimoptions的兼容性并不理想。如果你同时装了优化工具箱用optimoptions创建fmincon的选项没问题但交给fminsearch用就可能报错。这个细节容易让人折腾半天建议直接用optimset。3.2 案例一Rosenbrock函数上的TolX对比实验为了让大家看得直观我把实验完整写一遍。Rosenbrock函数是算法测试的经典基准它有一个狭长的抛物线山谷非常考验搜索算法的方向判断能力。% 目标函数 fun (x) 100*(x(2) - x(1)^2)^2 (1 - x(1))^2; % 初始点 x0 [-1.2, 1]; % 方案1默认容差 opts_default optimset(Display, none); [x_d, fval_d, exitflag_d, output_d] fminsearch(fun, x0, opts_default); % 方案2TolX和TolFun同时收紧 opts_tight optimset(Display, none, TolX, 1e-8, TolFun, 1e-8); [x_t, fval_t, exitflag_t, output_t] fminsearch(fun, x0, opts_tight); % 对比输出 fprintf(默认: x [%.6f, %.6f], fval %.2e, 迭代次数 %d\n, ... x_d(1), x_d(2), fval_d, output_d.iterations); fprintf(收紧: x [%.6f, %.6f], fval %.2e, 迭代次数 %d\n, ... x_t(1), x_t(2), fval_t, output_t.iterations);在我的机器上默认容差得到的结果函数值大约3e-5迭代次数30次左右收紧容差后函数值降到1e-11迭代次数上涨到接近200次。这个对比告诉我们如果你只是需要“大概找到一个不错的位置”默认值够了但如果你在做高精度标定必须把TolX和TolFun一起收紧只调一个是压不出精度的。3.3 案例二带噪声数据拟合的容差选择工程里更常见的问题是曲线拟合比如把实验数据拟合成指数衰减模型% 生成带噪声的观测数据 t (0:0.2:5); rng(1); y_true 2.5 * exp(-0.8 * t); y_obs y_true 0.05 * randn(size(t)); % 目标函数模型参数为[amp, rate] model (p, t) p(1) * exp(p(2) * t); fun (p) sum((model(p, t) - y_obs).^2); % 初始猜测 p0 [1, -1]; % 尝试不同的TolX opts_coarse optimset(TolX, 1e-2, TolFun, 1e-3); opts_fine optimset(TolX, 1e-8, TolFun, 1e-8); [p_coarse, fval_coarse] fminsearch(fun, p0, opts_coarse); [p_fine, fval_fine] fminsearch(fun, p0, opts_fine); fprintf(coarse: amp %.4f, rate %.4f, SSE %.4f\n, p_coarse(1), p_coarse(2), fval_coarse); fprintf(fine: amp %.4f, rate %.4f, SSE %.4f\n, p_fine(1), p_fine(2), fval_fine);有意思的事情来了。因为有噪声目标函数面上存在很多“小毛刺”。TolX设得很小的时候fminsearch会花大量迭代去捕捉这些毛刺得到的参数不一定比粗容差的更好甚至可能过拟合噪声。工程经验是带噪声的拟合问题TolX不宜设置得过小通常1e-3到1e-4之间就很好用。这个案例我强烈建议读者自己跑一遍因为你能在输出结果中直观看到最优参数并没有随着TolX的收紧而持续改善。这和我前面说的“小TolX不等于高精度”是一回事只是这里的原因变成了数据噪声。3.4 用Displayiter读懂迭代过程参数调优不只是改数字你得知道迭代过程长什么样。把Display改成iter之后fminsearch会输出一张表包含每一步的操作类型、函数值、当前点坐标等。opts optimset(Display, iter, TolX, 1e-4, TolFun, 1e-6); fun (x) (x(1)-2)^2 (x(2)3)^2; fminsearch(fun, [0, 0], opts);你会看到类似这样的输出Iteration Func-count min f(x) Procedure 0 1 13 initial simplex 1 3 9.25 reflect 2 5 5.82 reflect ...关注Procedure这一列当连续多次出现reflect时说明单纯形在某个方向上走得很顺如果频繁出现contract或shrink说明搜索方向有反复算法可能在绕弯。我习惯在调参时先用iter模式跑一组确认算法行为正常再把Display改回final省去刷屏。4. 常见问题与避坑实录4.1 只调TolX不调MaxIter的连锁反应这是最常见的“掉坑”姿势。TolX和TolFun调小之后算法需要更多迭代才能达到收敛条件但MaxIter和MaxFunEvals不会自动变大。默认的MaxIter是200倍变量维度对大多数简单问题够用对复杂问题或者初始化很差的场景往往不够。一旦迭代次数超过MaxIterfminsearch会直接退出返回当前单纯形中最好的点exitflag为0。你有两个办法排查第一看output.iterations是不是正好等于MaxIter第二看exitflag0表示达到迭代上限但未收敛。opts optimset(Display, none, TolX, 1e-12, TolFun, 1e-12, MaxIter, 50); [x, ~, exitflag, output] fminsearch(fun, x0, opts); fprintf(exitflag %d, iterations %d\n, exitflag, output.iterations);出现这种情况的时候不要傻乎乎地只把TolX改大应该先增加MaxIter或者MaxFunEvals再重新评估容差是否合理。我的习惯是MaxIter和MaxFunEvals先放宽到1000和2000等确认收敛行为正常后再往回收。4.2 多变量尺度不一致导致的假收敛当目标函数的自变量尺度相差悬殊比如一个参数是1e-4量级另一个是1e4量级TolX这个标量就很难同时满足两个维度的进度要求。单纯形会在尺度大的方向上缩得很慢在小尺度方向上又缩得飞快最后算法可能在“大方向还没动”的状态下提前终止。解决办法是变量归一化。把每个参数都映射到相近的尺度范围内再优化等得到结果后再还原。% 原始参数: a ~ 1e-4, b ~ 1e4 % 归一化用缩放因子把两者都拉到1附近 scale [1e-4, 1e4]; fun_norm (u) original_fun(u .* scale); u0 [1, 1]; u_opt fminsearch(fun_norm, u0, opts); x_opt u_opt .* scale;这个方法很土但极其有效。每次我遇到fminsearch结果不稳定第一反应就是检查自变量量级八成能发现问题。4.3 含约束优化直接套fminsearch的误区fminsearch从头到尾不检查约束。你以为给目标函数里加个惩罚项就能“变相约束”这是个危险的行为。惩罚函数法确实是一种思路但惩罚项的权重要反复调调不好会得到不满足约束的解或者精度很差的解。如果你本身的问题带线性或非线性约束建议直接用fmincon。fmincon也是优化工具箱里的支持边界约束、线性不等式、非线性约束而且内部的算法会显式处理约束可行性。举个我见过的错误例子有人用fminsearch拟合电池充放电参数要求放电倍率不能在1到3之外结果直接对越界的参数乘以一个巨大惩罚最终结果既不在边界内拟合曲线也扭曲。如果用fmincon加上边界约束一次就搞定。4.4 退出信息速查与调试建议fminsearch有个output结构体里面藏着很多有用信息我调试时最常看的就是这个。下面整理了一张速查表按我实际项目的经验做了备注退出信息或标志含义处理建议exitflag 1根据容差正常收敛检查结果是否满足实际需求exitflag 0迭代达到上限或函数评估次数超限增加MaxIter/MaxFunEvals重新查看参数合理性exitflag -1被绘图函数或输出函数终止检查是否误设了OutputFcnExiting: Maximum number of iterations exceeded迭代次数超限配合output.iterations分析是否接近收敛Exiting: Maximum number of function evaluations exceeded函数评估次数超限增加MaxFunEvals或考虑降低TolX/TolFun除了看exitflag我还强烈建议把output.message打印出来看。它虽然只是一行字但会明确告诉你算法是因为哪个条件停下来的这对判断“收敛是不是真的还是因为某个上限被撞到”非常关键。[x, ~, exitflag, output] fminsearch(fun, x0, opts); disp(output.message);如果你发现算法一直卡在某个局部极小点可以多试几个初始点做多起点优化。把初始点选在合理的物理范围内用循环批量跑最后取函数值最小的结果。这种“多起点Nelder-Mead”的组合在工程实践中比单纯调大迭代次数有效得多。我也在不少标定问题里用这个套路十次里有八九次能跳出局部极小。最后说一个个人感悟。TolX这种参数本质上是在“计算时间”和“结果精度”之间做折中。你在文档里看到的默认值是MATLAB针对绝大多数问题妥协出来的结果。真正常用的不是那个值而是你理解它之后结合自己问题的尺度、噪声水平和时间预算做出的合理选择。我在实际使用中一般会先跑一版默认参数看趋势再用Displayiter分析收敛行为最后才决定要不要动TolX和TolFun。这个过程可能只多花几分钟却比盲目地一轮轮试参数要靠谱得多。
返回列表