
做测量平差时间久了你就会发现最头疼的问题往往不是法方程解不出来而是观测值的权怎么给才合理。尤其手里同时握着测边、测角、水准甚至GNSS基线数据时不同观测类型之间的精度尺度完全不在一个量级随便给一组起始权就开始平差结果经常是“看起来收敛了仔细一看全是虚的”。我做控制网平差这几年遇到这个问题踩了不少坑直到把赫尔默特方差分量估计这套方法搬到Python里实现才算是真正把多类观测值定权这件事给理顺了。今天就把这套方法的原理、实现和实操经验一次讲透。这篇文章适合三类人看一是做控制网平差、工程测量数据处理的技术人员二是搞GNSS数据处理想融合多源观测值的研究生和工程师三是写平差程序但一直对“如何自动定权”没什么头绪的开发者。赫尔默特方差分量估计本质上就是一种根据验后残差自动调整各类观测权重的迭代方法它不挑平差模型无论是间接平差、条件平差还是秩亏自由网都能嵌进去用。读完后你可以直接把文中的Python实现移植到自己的平差项目里。1. 项目核心思路与整体方案设计1.1 解决的问题到底是什么先说说这个项目要解决的痛点。常规最小二乘平差里我们通常根据经验给不同观测量定权比如测角中误差按3秒、测边中误差按5毫米加5ppm然后取单位权方差为某个基准值算出各自的权。但这个做法的前提是你给的起始中误差足够接近真实精度。问题是实际观测时仪器的标称精度、环境条件、观测员的操作水平都会影响真实精度你拍脑袋给的一组中误差很可能和实际情况差了一倍甚至几倍。当不同类观测值之间的权比严重失衡时平差结果会出现一个典型症状高权观测值对应的残差被压得极小低权观测值的残差被拉得极大最后算出的单位权方差严重偏离1。赫尔默特方差分量估计就是为了解决这个失衡问题而生的它的核心逻辑是先按初始权做一次平差然后从各类观测值的验后残差中反推出每一类观测的方差分量用这个信息修正权比再重新平差如此迭代直至权比稳定。1.2 为什么选赫尔默特而不是其他方差估计方法方差分量估计在统计和测量领域有不止一种路径最大似然估计、最小范数二次无偏估计MINQUE、赫尔默特估计都是常用选择。我在实际项目里最终选择赫尔默特有三个实际考虑。第一是计算代价足够低。赫尔默特估计的每次迭代只比普通最小二乘多出几次矩阵迹运算和若干次矩阵乘法在控制网规模几千个未知数以内的情况下每次迭代耗时就是毫秒级别完全不用担心性能瓶颈。第二是它的物理意义直观。它从每一类观测残差的平方和出发给出的方差分量可以直接理解为“这类观测的实际方差应当在初始假设基础上的放大/缩小倍数”这在现场调整仪器精度指标时非常有用。第三是收敛行为稳定。只要初始权不是离谱得离谱赫尔默特迭代通常在5到10次之内就能使各类方差分量比值趋近于1没有出现过震荡发散的情况。当然赫尔默特估计也有局限它要求各类观测值之间相互独立如果你拿的是同一台全站仪测的边和角虽然本质上不完全独立但工程上按独立处理误差不大如果观测值之间强相关那就需要考虑协因数阵的完整形式走上更复杂的广义方差分量估计路线。这个边界我在项目开发时就有意识地做了明确划分。1.3 总体实现流程拆解整个Python实现可以拆成五个环节观测数据组织、误差方程构建、法方程解算、方差分量求解、迭代收敛控制。数据组织环节要把不同类观测值区分开每类维护自己的设计矩阵和权阵误差方程构建环节要把所有类观测拼接成整体设计矩阵A和观测残差向量l法方程解算用常规的最小二乘正规方程方差分量求解是利用上一轮平差的残差去构建赫尔默特估计方程组解出每个类的方差分量迭代控制则是判断各类方差分量比值是否足够接近1不满足就更新权阵重新来一轮。这五个环节里最容易写错的是方差分量求解那一步因为赫尔默特方程组里涉及迹运算矩阵维度大一点就很容易算错下标。为了规避这个问题我在代码里专门写了一个迹运算工具函数并且用一个小规模例子做过数值验证确保每一步都对应得上理论公式。后面会给出完整代码照着改就能用。2. 赫尔默特方差分量估计的数学原理2.1 核心思想用直觉理解先用最朴素的话解释一下这套方法在干什么。你有一堆观测数据这些数据来自不同的仪器或不同的观测方法你觉得它们精度不一样但又不确定每类到底差多少。赫尔默特的做法是先用你拍脑袋定的权做一次平差然后统计每一类观测的残差平均大小残差大的那类说明它的实际精度没你给的那么好就应该降低它的权残差小的那类则相反。这里的关键是残差并不能直接换算成方差因为平差会消化掉一部分残差。比如你给某类观测定了很低的权模型会尽量迁就其他高权观测导致这类观测的残差看起来巨大但这里面有一部分是“权比不合理”人为造成的不是真实噪声。赫尔默特估计的数学魅力就在于它通过法方程逆矩阵的迹把“模型消化作用”给扣除掉剩下的是真噪声贡献的方差分量。打个比方几个裁判给运动员打分如果某个裁判打分出了名的严格他的分数和其他人系统性偏低直接按平均分会把他个人的偏差混进去。赫尔默特估计相当于根据每个裁判的历史偏差系数给分数做校正把各人的“系统偏差”单独拎出来衡量。这个类比你品品就能理解为什么赫尔默特估计在现代测量平差里地位这么高。2.2 严格的赫尔默特估计方程组从数学上讲假设我们有k类观测值每类观测对应的误差方程为V_i A_i x - l_i其中下标i代表第i类观测。整体平差时未知参数x由所有观测共同确定法方程是N A^T P A这里的P是分块对角权阵各块P_i对应各类观测的权阵。解算得到参数估计x后各类观测的残差平方和V_i^T P_i V_i就构成了赫尔默特方程组的观测端。赫尔默特方差分量估计的核心方程组是S * θ W其中θ是待求的方差分量向量W的第i个元素就是第i类观测的V_i^T P_i V_iS矩阵的元素则是由法方程逆矩阵与各类设计矩阵构成的迹函数。具体来说S_ij tr(N^{-1} A_i^T P_i A_i N^{-1} A_j^T P_j A_j) tr(N^{-1} A_i^T P_i A_i) * δ_ij / n_i 形式的修正项不同教材在S矩阵的表达式上略有差别区别在于是否把已知的单位权方差因子代入、以及n_i与自由度之间的修正但本质上都是在解一个“从二次型残差反推原始方差”的问题。2.3 简化矩阵法公式在实际编码时大部分实现不会直接用上面的严密形式因为S矩阵的元素算起来非常繁琐法方程逆矩阵的维度高时连续做两次N^{-1}乘各类法矩阵的迹运算数值误差也会累积得比较快。更常用的是简化矩阵法其推导思路是在各类观测之间相关性较弱的前提下忽略交叉项同时假设各类观测的方差分量相差不大。简化后的每个方差分量公式长这样σ_i^2 (V_i^T P_i V_i) / (n_i - tr(N^{-1} N_i))其中n_i是第i类观测的个数N_i是第i类观测对法方程的贡献即N_i A_i^T P_i A_i这个公式非常好用它把一个复杂的耦合方程组变成了逐类独立求解的形式。分母里的n_i - tr(N^{-1} N_i)可以理解为“第i类观测的有效多余观测数”或者“第i类观测对自由度消化的贡献”。直观想一下如果某一类观测几乎没什么多余观测即它被模型解得死死的那么n_i - tr(N^{-1} N_i)接近0分母很小算出的方差分量会非常大这恰好说明这类观测提供的信息很少它的精度估计不可靠应该降低它的权重。反过来如果这类观测有很多冗余分母大方差分量能稳定估计其权重就能得到合理维持和修正。2.4 迭代更新策略求得各类方差分量σ_i^2之后下一步是更新权阵。设定单位权方差为σ_0^2通常取第一类的方差分量或所有类的加权平均值那么第i类观测新的权阵为P_i_new (σ_0^2 / σ_i^2) * P_i_old换句话说如果某一类观测的实际方差分量是初始假设的两倍那么它的权应该整体缩小到原来的一半。更新后重新平差计算新的残差和新的方差分量如此循环直到满足收敛条件。收敛条件常用两种方式判断一是看各类方差分量的最大值与最小值之比是否接近1比如比值小于1.1就算收敛另一种是看连续两轮迭代之间各类权阵元素的变化量是否足够小。我在项目中用的是第一种因为它更直观而且不容易被个别异常观测带偏。实际测试下来初始权差个两三倍的情况下迭代6到10次就能稳定下来。3. Python实现核心代码与关键细节3.1 依赖选择与整体代码结构这个项目我只用了三个库numpy负责矩阵运算scipy.linalg用于求解对称正定线性方程组比numpy的solve更稳健再配一个copy模块处理迭代过程中的矩阵快照。没有引入特殊依赖Python 3.8以上版本直接跑就行。代码组织上我定义了一个HelmertVCE类类的初始化接收各类观测的设计矩阵、观测向量和初始权阵。平差和方差分量估计的所有逻辑都封装在estimate方法里每次迭代调一次内部方法_iterate返回当前迭代的参数解和各类方差分量。这样拆分的好处是便于调试想看某一步的内部值时可以直接调用_iterate观察中间输出。3.2 核心函数实现直接放代码。下面的实现假设所有观测已经按类别分组每一类的设计矩阵、观测向量和权阵都以列表形式传入。import numpy as np from scipy.linalg import solve from copy import deepcopy class HelmertVCE: def __init__(self, A_list, l_list, P_list): A_list: 每类观测的设计矩阵列表元素形状为(n_i, t) l_list: 每类观测的观测向量列表元素形状为(n_i, 1) P_list: 每类观测的权阵列表元素形状为(n_i, n_i) self.A_list [np.asarray(A, dtypefloat) for A in A_list] self.l_list [np.asarray(l, dtypefloat).reshape(-1, 1) for l in l_list] self.P_list [np.asarray(P, dtypefloat) for P in P_list] self.k len(self.A_list) self.n sum(A.shape[0] for A in self.A_list) self.t self.A_list[0].shape[1] def _build_system(self): 拼接整体设计矩阵、观测向量与分块权阵 A np.vstack(self.A_list) l np.vstack(self.l_list) P np.zeros((self.n, self.n)) start 0 for i in range(self.k): ni self.A_list[i].shape[0] P[start:startni, start:startni] self.P_list[i] start ni return A, l, P def _iterate(self): 执行一次整体平差并计算赫尔默特方差分量 A, l, P self._build_system() N A.T P A U A.T P l x solve(N, U) # 解参数 V_list [] start 0 for i in range(self.k): ni self.A_list[i].shape[0] Ai self.A_list[i] li self.l_list[i] Vi Ai x - li # 残差 V_list.append(Vi) start ni # 法方程逆矩阵 N_inv np.linalg.inv(N) sigma2_list [] for i in range(self.k): Ai self.A_list[i] Pi self.P_list[i] ni Ai.shape[0] Ni Ai.T Pi Ai Vi V_list[i] vtpv float(Vi.T Pi Vi) # 简化矩阵法公式的分母 denom ni - np.trace(N_inv Ni) sigma2_i vtpv / denom sigma2_list.append(sigma2_i) return x, V_list, sigma2_list, N_inv def estimate(self, max_iter30, tol1.1): 迭代执行赫尔默特方差分量估计 history [] for it in range(max_iter): x, V_list, sigma2_list, N_inv self._iterate() history.append({ iter: it, x: x.copy(), sigma2: sigma2_list.copy(), vtpv: [float(V.T P V) for V, P in zip(V_list, self.P_list)] }) # 使用第一类方差分量作为单位权方差参考 sigma0 sigma2_list[0] ratio_max max(sigma2_list) / min(sigma2_list) print(f第{it1}次迭代: sigma2{[round(s,4) for s in sigma2_list]}, 极差比值{ratio_max:.4f}) if ratio_max tol: print(收敛完成) # 更新权阵 for i in range(self.k): self.P_list[i] self.P_list[i] * (sigma0 / sigma2_list[i]) return self.P_list, x, history # 更新权阵 for i in range(self.k): self.P_list[i] self.P_list[i] * (sigma0 / sigma2_list[i]) print(达到最大迭代次数未收敛) return self.P_list, x, history说几个实现上的细节。解算法方程时我用scipy.linalg.solve而不是numpy.linalg.solve因为在法方程病态时scipy能给出更稳定的结果。计算V_i^T P_i V_i时我特意用float()把结果从矩阵类型转成float防止后续除法和比较时出现数组维度上的奇怪问题。还有一个关键细节是每轮迭代都要基于更新后的权阵重新构建P不是只更新self.P_list就够了因为_build_system里读取的正是self.P_list所以直接覆盖更新列表内容就行。3.3 收敛控制的实现角度收敛判据我选了sigma2极差比值小于1.1这个阈值是经验值。太严的话迭代次数会明显增加但实际平差解算结果并没有本质变化太松又可能导致各类观测的权还没有充分调整好就停止后续精度评定会偏乐观。如果你对精度要求高可以把阈值设成1.05代价是多迭代两三轮实测差别不算大。还有一个值得注意的策略每次都保持第一类的方差分量作为单位权基准其他类按比例缩放。为什么不每轮都用某个全局平均方差做基准因为全局平均会受异常类影响如果某类观测数很少、方差分量估得很不稳定用它做基准会让其他正常类的权也跟着抖动延长甚至破坏收敛。固定基准类是测绘平差里的常规做法你默认用第一类做基准没问题。3.4 完整可运行的示例代码为了让你拿到代码就能跑我再给一个完整的模拟例子。假设一个模拟的边角网有三类观测角度、边长和GPS基线向量各有若干观测量真实中误差分别假设为2秒、5毫米和10毫米。初始权阵故意给错给成角度2秒、边长3毫米、基线5毫米看看赫尔默特能不能校正回来。# 模拟数据构造 np.random.seed(2024) t 4 # 4个未知参数 n1, n2, n3 8, 6, 5 # 各类观测数量 # 随机设计矩阵 A1 np.random.randn(n1, t) A2 np.random.randn(n2, t) A3 np.random.randn(n3, t) # 真实参数 x_true np.array([10.0, -5.0, 2.0, 7.0]) # 观测值生成 noise_sigma np.array([2.0, 5.0, 10.0]) # 真实中误差 l1 A1 x_true np.random.randn(n1, 1) * noise_sigma[0] / 206265.0 l2 A2 x_true np.random.randn(n2, 1) * noise_sigma[1] / 1000.0 l3 A3 x_true np.random.randn(n3, 1) * noise_sigma[2] / 1000.0 # 初始权阵故意给错的精度 P1 np.eye(n1) * (1.0 / ((2.0 / 206265.0) ** 2)) P2 np.eye(n2) * (1.0 / ((3.0 / 1000.0) ** 2)) P3 np.eye(n3) * (1.0 / ((5.0 / 1000.0) ** 2)) vce HelmertVCE([A1, A2, A3], [l1, l2, l3], [P1, P2, P3]) P_final, x_final, history vce.estimate(max_iter20, tol1.1) print(最终参数解:, x_final.ravel()) print(真值:, x_true) print(参数误差:, (x_final.ravel() - x_true))跑这个例子你会清楚地看到迭代过程中各类方差分量的变化趋势最终修正后的权比会明显偏向真实精度。4. 手把手实操一个混合边角网的方差分量估计4.1 案例场景与数据组织假设现在做一个模拟的矿区控制网复测项目控制网里同时有精密测距仪测的边长、全站仪测的水平角还有少量水准联测的高差。三类观测精度差异大边长标称精度1mm1ppm测角标称精度2秒水准每公里中误差0.5mm。传统做法直接按标称精度定权但实际观测时边长受大气折光影响明显测角受对中误差影响大水准是最稳定的。这个真实精度差异如果不知道权比就定不准。在代码层面我习惯把数据先做成三个列表A_list、l_list、P_list每个列表的元素数量等于观测类别数。设计矩阵的行数等于该类观测个数列数等于未知参数个数。权阵通常用对角阵对角线元素按测段长度或观测方向数倒数赋值。这一步最费时间的是数据清洗和设计矩阵组装千万别图省事在这上面偷懒后面所有迭代都建立在A矩阵的正确性上。4.2 迭代过程中的数值表现跑起来之后你会看到类似这样的输出第1次迭代: sigma2[1.0000, 2.3741, 0.8212], 极差比值2.8910 第2次迭代: sigma2[1.0000, 1.8305, 0.9302], 极差比值1.8305 第3次迭代: sigma2[1.0000, 1.4117, 0.9831], 极差比值1.4117 第4次迭代: sigma2[1.0000, 1.2053, 1.0023], 极差比值1.2053 第5次迭代: sigma2[1.0000, 1.0922, 1.0134], 极差比值1.0922 第6次迭代: sigma2[1.0000, 1.0431, 1.0190], 极差比值1.0431 收敛完成注意看初始迭代时长边的方差分量明显偏大2.37倍说明标称精度确实比实际好按标称定权等于给了边长观测过高的权重。经过几轮迭代边长方差分量维持在1.04左右说明修正后的权更加合理。最终参数解和真值的偏差也比等权平差时小了不少尤其在边长观测对应的未知参数上改善幅度能到两三成。4.3 与固定权平差的结果对比对比才是说服力的关键。我在案例里同时跑了普通最小二乘固定初始权和赫尔默特迭代后的最小二乘对比结果用一句话概括就是参数解差异不大但方差分量和精度评定指标差异明显。固定权平差算出的单位权中误差虚高而赫尔默特修正后的各类观测验后方差更接近真实中误差。这个现象在理论上很好解释固定权平差时如果某类观测的权被给大了平差会强制缩小这类观测的残差但同时放大其他类的残差。单位权中误差是把所有残差综合起来算的于是被某一类带偏了。赫尔默特估计通过方差分量把每类观测的实际噪声水平单独估出来本质上是把“残差平方和”中真噪声贡献拉出来了所以单位权中误差的估值更健康。我做对比时习惯把点位坐标差、方位角差、边长差值全部列一张表然后针对每个未知参数计算赫尔默特结果相对于等权结果的精度提升比例。如果某类观测被严重错权参数精度提升可以达到30%以上如果初始权已经比较合理提升不大但也不会变差属于稳赚不赔的操作。4.4 收敛慢或者不收敛怎么处理实操中赫尔默特迭代偶尔会出现不收敛的情况常见原因有三类。第一类是某类观测数量太少比如只有两个多余观测分母n_i - tr(N^{-1}N_i)非常小算出的方差分量奇大直接冲垮整个权阵更新。这时要对该类观测设一个方差分量上限比如不允许超过其他类的中位数的10倍或者直接固定住该类权不参与更新。第二类是观测中存在粗差粗差会极大拉伸残差让方差分量估计失真这种情况下要先做粗差探测把超限残差剔除再跑赫尔默特否则迭代很快发散。第三类是初始权给得极端离谱比如某一类权比其他类大了一万倍法方程几乎被那一类主导其他类的方差分量就变成噪声除以零自由度完全无意义。处理方法是收敛失败时回退到上一轮权阵把更新幅度限制在50%以内再重新迭代。我在项目里就踩过第一类坑。某次水准观测段数少高频次迭代时水准类方差分量被估成天价连带着把其他类的权也拉偏了。后来我加了一个保护逻辑_iterate里如果denom小于某个阈值直接跳过该类的方差更新保持原有权阵。这样处理之后收敛稳定多了。5. 常见问题与避坑指南5.1 高频问题速查表现象原因解决方案迭代发散方差分量越来越大某类观测冗余度过低denom接近0对该类设方差上限或固定权重不更新极差比值收敛但参数解抖动存在粗差污染某类残差先用粗差探测剔除异常观测再平差迭代次数超过20次初始权与真实精度差太多或S矩阵病态做一次残差统计用残差中位数重新推算初始权不同类方差分量数量级差异巨大各类观测量纲不同直接比较绝对值无意义统一用比例形式比较或先做量纲归一化结果比普通最小二乘还差某类观测数极少方差分量估计不可靠增加该类观测数量或采用加权平均的简化估计5.2 实操心得初始权该怎么给虽然赫尔默特能自动修正权比但初始权的合理程度直接影响收敛速度和稳定性。我自己的经验是初始权可以不需要多准但方向不能错。比如测角和测边如果你把测角的权给得远高于测边迭代初期测角的方差分量会很小、测边的会很大然后相互调整这个没问题但如果两类初始权方向反了就可能需要多迭代好几轮。做实际项目时我往往会先跑一次普通最小二乘平差把各类观测的残差中误差算出来用它做初始权的基准。这样赫尔默特迭代通常只用三四轮就能收敛比从标称精度出发快得多。5.3 数值精度与矩阵病态的注意事项赫尔默特估计涉及法方程逆矩阵的迹运算当控制网非常大、法方程条件数很糟糕时N^{-1}的计算误差对denom的影响会很明显。这时候有几种处理手段一是用scipy.linalg.pinvh代替np.linalg.inv做伪逆代价是每次迭代慢一些但数值更稳二是对N做Cholesky分解通过分解因子直接计算迹能避开显式求逆三是引入岭修正给N的对角元素加一个很小的正则项防止病态导致的数值爆炸。我测试过一个千级控制网在病态测试网里用np.linalg.inv计算时方差分量出现了轻微震荡换成伪逆后立刻稳定了。所以如果你跑的是大型网或病态网建议直接上伪逆不要在求逆效率上过于纠结这部分的耗时占比很小。5.4 扩展方向从两类到多类、从无相关到相关不同类观测值之间独立的基本假设在很多场景下是成立的但如果你的数据里有重复基线、重复测站这类强相关观测理论上应该考虑协因素阵的非对角块。扩展赫尔默特估计到相关观测并不复杂在S矩阵的元素计算中引入协方差传播律把V_i^T P_i V_i换成V^T P R V的形式其中R是相关性矩阵。这个扩展虽然写起来麻烦一些但能得到更严格的方差分量估计。另一个很实用的扩展是结合已知的先验方差进行约束。控制网平差里可能有历史控制点的坐标协方差把这些先验信息作为伪观测加入赫尔默特框架可以防止某些类别的方差分量被过度拉伸。我在项目里试过把先验约束作为第k1类观测参与迭代效果很不错你可以试试。6. 最后分享一点自己的体会赫尔默特方差分量估计这个方向我做下来最深的感触是它的数学门槛不算高真正难的是把它嵌入到真实的平差流程中处理各种边角情况。Python实现本身不过两三百行但那些数据清洗、权阵保护、病态处理的细节才是决定这套代码能不能用于生产的关键。我有一个一直坚持的习惯每次跑赫尔默特迭代都把方差分量的迭代轨迹画出来看一眼。这个轨迹比任何收敛判据都更能说明问题——如果某类的方差分量在震荡说明数据里可能有粗差或相关性没处理好如果是平滑下降说明初始权和真实值之间存在系统性偏差迭代在正常纠偏。这种可视化检查的成本很低但能帮你躲过不少暗坑。另外还想强调一点方差分量估计不是魔法。它的作用是让你从数据里估计出更合理的权比而不是掩盖观测质量问题。如果你的观测数据本身含粗差、系统性误差或者严重的相关性指望赫尔默特迭代来“修正”是不现实的。保证观测质量做好粗差探测再上赫尔默特得出的平差结果才是真正可信的。如果你正准备把赫尔默特方差分量估计写进自己的平差程序建议先拿模拟数据验证S矩阵和方差分量计算的正确定性再过渡到实测数据。模拟数据下你手上有真实方差能立刻发现实现错误直接上实测数据的话出了问题不一定能分辨是理论问题还是数据问题。这个开发顺序能让你少走很多弯路。