ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

正则化反演方法详解:基于MATLAB的Tikhonov实现与L曲线调参

正则化反演方法详解:基于MATLAB的Tikhonov实现与L曲线调参 做地球物理反演的朋友应该都有过这种经历正演模型写得很顺一上反演解出来的模型要么乱跳得像噪声要么干脆发散到数值溢出。其实大部分问题都出在方程本身“病态”上——观测数据远远不够约束每一个模型参数直接硬解最小二乘结果必然被噪声放大成灾难。正则化反演就是干这个用的。这篇文章我从一套完整的MATLAB实现讲起覆盖目标函数构建、核矩阵与约束矩阵的生成、正则化参数选择以及实测中经常踩的坑给做重力、磁法、电阻率这类数值反演的朋友一份能直接上手改的参考。1. 正则化反演到底在解决地球物理里的什么问题1.1 反演本质是解一个“坏条件”的线性方程组大多数地球物理反演最终都落到这样一个形式上d G m e其中 d 是观测数据m 是离散化后的模型参数G 是正演核矩阵e 是噪声。看起来就是个最小二乘问题但问题在于 G 的条件数往往大得吓人。我以重力勘探做个例子。深度越大、横向分辨率越低的场源对地表观测值的影响就越平滑反映在矩阵里就是不同列之间高度相关。你用MATLAB的 cond(G) 算一下动辄 10^12 以上这种矩阵意味着观测值微小变化经过反演会被放大 10^12 倍。所以你加上 1% 噪声反演出的密度模型就是完全乱套的。1.2 用一个小例子感受病态为了把问题说清楚我给一个极度简化的场景假设你要反演地下一个界面的起伏分成 20 个柱体每个柱体密度差固定界面深度就是模型参数 m。代码里构造一个最简单的核矩阵然后直接做最小二乘解% 观测点坐标 obs_x linspace(-500, 500, 21); % 模型参数20个柱体的深度 n 20; model_x linspace(-450, 450, n); % 简化核函数越深影响越宽越平 G zeros(length(obs_x), n); for i 1:length(obs_x) for j 1:n r abs(obs_x(i) - model_x(j)); G(i, j) exp(-r / 150); end end核矩阵每一列都是一个以该柱体位置为中心的衰减函数列之间长得像、高度相关矩阵条件数巨大。现在给定一个真实模型 m_true生成观测数据再反演m_true 100 30 * exp(-((model_x - 0).^2) / 2 / 200^2); % 真实界面深度 d G * m_true; m_lsq G \ d; % 直接最小二乘 plot(model_x, m_true, k-, model_x, m_lsq, r--)你会发现即使没有噪声得到的 m_lsq 都可能是正确的但一旦数据稍微带点噪声就全乱了。这个现象不是代码问题而是数学上必然的。2. 把正则化写进目标函数Tikhonov方法的MATLAB实现2.1 目标函数为什么要加罚项既然 G 病态就不能只最小化数据拟合残差需要在目标函数里加入对模型本身的约束这就是正则化的基本思想。最常用的 Tikhonov 形式长这样min || G m - d ||_2^2 alpha^2 || L m ||_2^2其中 alpha 是正则化参数L 是约束矩阵。约束的意思简单说就是在拟合数据的同时要求模型的某些特征尽量小或尽量平滑。一阶差分 L 惩罚相邻参数差逼出平滑模型二阶差分惩罚曲率逼出更自然的渐变界面。alpha 越大模型越平滑但数据拟合越差alpha 太小模型又回到乱跳状态。2.2 从优化问题到MATLAB里的线性方程组上面那个目标函数是一个二次型求梯度等于零可以得到正规方程(G^T G alpha^2 L^T L) m G^T d但直接按这个式子写代码其实不是好习惯。因为 G^T G 会把条件数再平方一次数值上更差。工程上更推荐“增广矩阵”的做法把问题改写成min || A m - b ||_2^2其中A [G; alpha * L]; b [d; zeros(size(L, 1), 1)];这样一写原来带约束的最小二乘问题变成了一个不带约束的最小二乘问题直接调用 MATLAB 的左除运算符即可m_reg A \ b;这一招很实用对比一下两种方式的实际效果直接用正规方程左除alpha 较小时经常出现警告“Matrix is close to singular”而用增广矩阵方式则可以稳定工作到更小的 alpha并且少算一次矩阵乘法代码也更简洁。2.3 标准化不标准化你会死活调不对alpha这是新手最容易被坑的一环。数据 d 的量纲和模型 m 的量纲经常差好几个数量级。比如重力异常单位是 mGal模型深度单位是米alpha 的值域会被数据量级带跑导致 L 曲线法得到的拐点严重失真。标准化的做法是先令数据的 RMS 值等于 1模型参数也除以一个特征值。实际操作时我习惯把 G 按列做归一化让每一列的 L2 范数都是 1对应的模型参数再乘回来。这样处理后 alpha 通常在 0.001 到 10 之间L 曲线拐点稳得多。3. 正演核矩阵与模型约束矩阵的构建3.1 重力界面反演中的核矩阵代码我用重力界面反演来演示核矩阵的物理构建。假设地下介质被离散成若干矩形柱体每个柱体的密度差已知我们反演各柱体的深度或厚度。地面观测点坐标为 x每个柱体中心坐标为 x_j埋深为 z_j核函数用二维薄板公式近似function G gravityKernel(obs_x, model_x, z0, drho, dz) % obs_x : 观测点坐标列向量 % model_x : 柱体中心坐标 % z0 : 柱体顶面深度 % drho : 密度差单位 kg/m^3 % dz : 柱体厚度方向单元尺寸 n_obs length(obs_x); n_mod length(model_x); G zeros(n_obs, n_mod); G_const 6.674e-11 * drho * dz; for i 1:n_obs for j 1:n_mod dx obs_x(i) - model_x(j); r2 dx^2 z0^2; % 垂直重力分量近似z方向影响核 G(i, j) G_const * z0 / (r2 z0^2); end end end写的时候要注意核矩阵的元素往往随着柱体深度增加而急剧衰减表层柱体和深层柱体在数据中的灵敏度差几个数量级。这种“灵敏度不均”会让反演结果偏向浅层。解决办法是在约束矩阵里加入深度加权后面会专门讲到。3.2 一阶和二阶差分约束矩阵的MATLAB生成约束矩阵 L 的行数是约束个数列数必须等于模型参数个数。一阶差分矩阵 L1 的行数是 n-1第 k 行在 k 和 k1 列分别为 -1 和 1惩罚相邻参数的差值function L1 firstOrderDifference(n) L1 zeros(n - 1, n); for k 1:n - 1 L1(k, k) -1; L1(k, k 1) 1; end end二阶差分 L2 的行数是 n-2每行对应 [-1 2 -1]function L2 secondOrderDifference(n) L2 zeros(n - 2, n); for k 1:n - 2 L2(k, k) -1; L2(k, k 1) 2; L2(k, k 2) -1; end end实际用哪个取决于你对模型的先验认识。地下密度界面一般比较平缓用二阶差分效果更好如果你要反演的是阶跃型构造一阶差分更合适。要注意的是约束矩阵不能有零行否则对应参数完全不受约束。3.3 深度加权约束核矩阵天然偏向浅层的破解刚才提到核矩阵对深层参数的灵敏度低如果不做处理反演结果会集中在浅层。我们可以在约束矩阵前面乘以一个对角加权矩阵 W第 j 个参数对应权重可以设置为depth_w 1 ./ (z0 z_ref).^beta;其中 z_ref 是参考深度beta 通常取 0.5 到 1.5。最后的目标函数变成min || G m - d ||^2 alpha^2 || W L m ||^2在 MATLAB 里实现就是A [G; alpha * W * L]; b [d; zeros(size(L, 1), 1)]; m A \ b;用不用深度加权反演结果深度分布会有很大差别。我记得有一次做实际重力剖面反演没加深度加权时结果里 500 米以浅的密度起伏占满了全部方差加了以后深层结构才显示出来。4. 正则化参数怎么选L曲线法在MATLAB里的工程实现4.1 L曲线为什么是L形正则化参数 alpha 是最难调的一个量。alpha 太小模型不稳定数据拟合残差小但模型范数巨大alpha 太大模型过于平滑数据拟合残差大。如果把不同 alpha 下的残差范数和模型范数都画在双对数坐标里通常得到一条像字母 L 的曲线。拐角处对应解的最优折中。4.2 完整可用的lcurve函数我把自己常用的函数贴出来逻辑就是扫一串 alpha算出每一组 (残差范数, 模型范数)再在 L 形拐角处取点function [rho, eta, alpha_opt, m_opt] lcurve_scan(G, d, L, alpha_list) % rho: 数据残差范数 ||G m - d|| % eta: 模型范数 ||L m|| n_alpha length(alpha_list); rho zeros(n_alpha, 1); eta zeros(n_alpha, 1); models cell(n_alpha, 1); for k 1:n_alpha alpha alpha_list(k); A [G; alpha * L]; b [d; zeros(size(L, 1), 1)]; m A \ b; models{k} m; rho(k) norm(G * m - d); eta(k) norm(L * m); end % 在双对数坐标中找曲率最大的点作为最优alpha log_rho log(rho); log_eta log(eta); curvature abs(diff(log_rho, 2) .* diff(log_eta, 2) - diff(log_rho) .* diff(log_eta, 2)); [~, idx] max(curvature); alpha_opt alpha_list(idx 1); % 差分损失了一个位置 m_opt models{idx 1}; end调用方式很简单alpha_list logspace(-3, 2, 50); [rho, eta, alpha_best, m_best] lcurve_scan(G, d, L, alpha_list); figure; loglog(rho, eta, o-); hold on; loglog(rho(idx_best), eta(idx_best), ro, MarkerFaceColor, r);我建议每次都要把 L 曲线画出来看一眼不要只看自动求出的拐点。有时候数据噪声大L 曲线根本没有明显的拐角出现一条几乎平直的斜线这时最优 alpha 取决于你对模型平滑度的主观接受程度。曲线形状本身就是对数据质量最直观的体检。4.3 和GCV、chi2原则的对比除了 L 曲线还有广义交叉验证GCV和拟合差原则。GCV 不需要人为给定噪声水平公式为V(alpha) || G m - d ||^2 / (trace(I - G (G^T G alpha^2 L^T L)^{-1} G^T))^2MATLAB 里可以直接扫 alpha 计算不用解析求导gcv zeros(size(alpha_list)); for k 1:length(alpha_list) alpha alpha_list(k); A [G; alpha * L]; m A \ b; res G * m - d; H G / A; % 正规方程中的影响矩阵 dof length(d) - trace(H); gcv(k) (res * res) / (dof^2); end [~, idx_gcv] min(gcv);实际对比下来我的经验是数据噪声水平已知时拟合差原则最直观噪声水平未知且数据量较大时GCV 比 L 曲线稳定数据量小、模型参数不规律时L 曲线更可靠。三者求出的 alpha 通常在同一数量级如果差出两三个数量级就要警惕核矩阵或约束矩阵写错了。5. 跑完反演后的调试经验与常见的坑5.1 结果完全平坦或完全乱跳先检查alpha而不是算法很多朋友一看到反演模型“太光滑”就怀疑正则化用错了实际上最简单的原因是 alpha 太大。先不要急着改约束矩阵把 alpha 减小一个数量级看看如果模型马上变得乱跳说明问题就是正则化强度没调好。反过来如果 alpha 已经压到很小模型还是乱的那就要检查核矩阵是不是秩亏。5.2 模型上下限约束与投影Tikhonov 正则化本身不保证模型参数在物理允许范围内比如密度差不能为负。工程上常用的做法是“投影法”每次迭代或每次求解后把越界的参数直接拉到最近边界。虽然这破坏了原目标函数的严格最优性但工程上足够用。如果要做得更讲究可以用约束反演把不等式约束变成惩罚项代码会复杂一个量级。5.3 边界效应模型两端疯狂起伏正则化反演结果最常见的一个特征是模型两端容易大幅振荡。原因是边界参数只有一边的观测约束另一边没有邻居约束矩阵在最两端也不充分。几个补救办法加宽模型范围反演后只取中间一段结果对边界参数单独增强约束比如加大 L 中对应行的权重在模型两端加入渐变到先验值的“缓冲单元”。自定义约束有一个很直接的方法在 L 矩阵最后加一行只在边界两个参数上有非零元素L(end 1, 1) 1; L(end, n) -1;这一行的含义是强制第一个和最后一个参数尽量相等能压住两端很多异常。5.4 带噪声数据的稳定性测试做好一个反演流程后一定不要只用无噪声的合成数据验证。把 2%、5%、10% 的高斯噪声加进数据分别运行同一套代码观察模型解的变化。一套合格的反演方案应该是小噪声时结果基本稳定大噪声时结果变模糊但不发散。如果 2% 噪声就让解面目全非说明 alpha 选得还是偏小或约束矩阵不足以压制噪声放大。5.5 实测数据反演前必须做的几件事用真实数据之前我的固定流程是先做三件事第一步把观测数据的异常值直接剔掉否则一个离群点会在反演结果里形成一个大假异常正则化很难平衡这种局部过拟合第二步对数据和核矩阵做标准化第三步用合成模型走一遍完整流程确认核矩阵、约束矩阵和 alpha 选择代码没有 bug。这三步做完实测反演的成功率会明显提升。6. 从一维走向多维和扩展6.1 扩展思路本文所有代码都基于一维剖面反演但正则化框架本身是通用的。二维反演只需要把模型参数按网格展开成一列核矩阵按网格顺序排列约束矩阵改成二维差分算子。MATLAB 里可以用 kron 来生成二维差分约束Lx kron(speye(nz), Dx); Lz kron(Dz, speye(nx));Dx 和 Dz 分别是 x 和 z 方向的一维差分矩阵L [Lx; Lz] 就是完整的二维平滑约束。配合 sparse 矩阵正则化反演的计算开销并没有想象中那么可怕。6.2 换个角度L1正则化处理稀疏构造有些地球物理问题如断层位置识别、矿体边界圈定希望解出的是一个稀疏异常不是平滑模型。这时把 L2 罚项换成 L1 罚项目标函数变为min || G m - d ||^2 alpha || L m ||_1这个不能直接用左除法求解。MATLAB 里可以用迭代重加权最小二乘IRLS逼近每次迭代把 L1 罚项写成加权 L2权重从上一次解算出通常 510 次迭代就能收敛。实际反演下来L1 正则化的边界锐利程度明显好于 L2但需要谨慎选择 alpha噪声干扰下 L1 容易产生孤立的大尖峰假异常。6.3 和贝叶斯反演的连接正则化参数 alpha 在贝叶斯框架下本质上是先验分布的宽度。把 L m 看成模型参数的先验协方差结构alpha^2 的倒数对应先验方差。理解了这一点后你就能用 L 曲线、GCV 之外的方法——比如最大似然法——去估计 alpha。实际编程中和多层贝叶斯反演相结合本质上只是在外层多一个对 alpha 的优化循环。7. 一点个人实操体会正则化反演写起来不难真正难的是判断解靠不靠谱。我自己用这套 MATLAB 流程做过的重力界面反演项目里最大的教训是永远不要轻信一次反演出来的“漂亮模型”一定要做模型分辨率测试比如把单个柱体的深度异常设为输入看反演能不能恢复再加上不同 alpha 的结果对比才能判断哪些特征是数据真实约束的哪些是正则化硬拉出来的。建议新手从一维合成数据开始把核矩阵、约束矩阵、alpha 扫描这三块单独模块化逐块验证之后再去碰实测数据否则错误很难定位。MATLAB 的好处是矩阵操作直接、画图调试方便这套流程用熟了之后换到任何一门的数值反演课题都能很快迁移过去。最后再分享一个我平时觉得很好用的小技巧在 lcurve_scan 函数里不要把 alpha_list 的范围设得太宽比如 logspace(-5, 5, 100) 这种。扫得太宽时 L 曲线两端往往出现数值对称的假象拐点检测容易失真。一般根据数据特征先手动试 3 个数量级找出解从“乱跳”到“过度平滑”的过渡区间再在这一段内加密扫描效果会稳定很多。这个习惯帮我避开了不少调试陷阱。
返回列表