做地球物理反演的朋友应该都有过这种经历:正演模型写得很顺,一上反演,解出来的模型要么乱跳得像噪声,要么干脆发散到数值溢出。其实大部分问题都出在方程本身“病态”上——观测数据远远不够约束每一个模型参数,直接硬解最小二乘,结果必然被噪声放大成灾难。正则化反演就是干这个用的。这篇文章我从一套完整的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 和 k+1 列分别为 -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,权重从上一次解算出,通常 5~10 次迭代就能收敛。实际反演下来,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 个数量级,找出解从“乱跳”到“过度平滑”的过渡区间,再在这一段内加密扫描,效果会稳定很多。这个习惯帮我避开了不少调试陷阱。