☰
小波多尺度分解与奇异谱分析:GNSS坐标时间序列信号分离实战
2026/9/30 10:20:37 网站建设 项目流程

简介:这份文档面向从事GNSS数据处理、地壳形变监测与地球动力学研究的科研人员和测绘工程技术人员,聚焦GPS站坐标时间序列中非线性运动趋势难以用线性速度完整描述的问题,提出小波多尺度分解与奇异谱分析(SSA)相结合的处理思路。资源包内含1个docx文件,约442KB,完整呈现了从多分辨率分析、时滞矩阵构建、奇异值分解到分组重构的方法原理,并结合全球11个测站20年垂直坐标序列的实例展开验证。读者可从中获取小波低频与高频分量分离、SSA提取周年与半周年周期信号的具体流程,理解两种方法优势互补、削弱噪声并提高建模精度的实现路径,为ITRF框架精度提升与非线性变化改正研究提供可借鉴的分析框架。目前已有186人学习。

1. 小波多尺度分解与奇异谱分析:GNSS坐标时间序列里到底谁在“说话”

做GNSS形变监测的人迟早会撞上一件事:测站坐标时间序列看起来像一团噪声,但里面藏着构造运动、季节性负载、仪器换天线带来的阶跃,还有各种说不清道不明的周期项。直接拿原始序列去做线性拟合,残差大得离谱;直接上滤波器,又说不清哪一段是信号哪一段是噪声。小波多尺度分解和奇异谱分析(SSA)就是用来拆这团乱麻的两把刀——前者按频率把序列切成不同尺度,后者按能量把序列拆成趋势、周期和噪声分量。两者结合,能让你在GNSS站坐标时间序列里把“谁在说话”分清楚。这篇面向做形变分析、地壳运动监测、高精度定位后处理的工程师,从原理到代码到踩坑,把这条链路走通。

2. 小波多尺度分解:把GNSS坐标序列拆成几层才合理

2.1 为什么GNSS坐标序列需要多尺度视角

GNSS站坐标时间序列的功率谱通常不是平坦的。低频段有长期趋势(板块运动、沉降),中频段有周年和半周年周期(水文负载、热膨胀),高频段有与多路径、天线相位中心变化相关的短周期波动。用单一尺度的滤波器去处理,要么把趋势抹掉,要么把高频噪声当信号留下。

小波多尺度分解的核心思路是:选一个母小波,通过伸缩和平移生成一组基函数,把序列投影到不同尺度上。每个尺度对应一个频带,尺度越大频率越低。对GNSS坐标序列来说,常见做法是分解到5到8层,具体层数取决于采样率和你要保留的最低频信号。

这里有一个容易翻车的地方:很多人直接拿MATLAB的wavedec默认参数跑,结果边界效应把序列两端搞得面目全非。GNSS序列两端往往是你最关心的近期形变信息,边界处理不好,后面SSA再一叠加,结论就偏了。

2.2 用PyWavelets做多尺度分解的最小代码

下面这段代码用Python的PyWavelets对一条GNSS站坐标时间序列做多尺度分解,母小波选db4,分解6层,边界用对称延拓。

import numpy as np import pywt import matplotlib.pyplot as plt # 假设 ts 是已经去均值的一维GNSS坐标序列,单位米 # 采样率:日解,即每天一个点 # 这里用模拟数据演示,实际替换为你的序列 np.random.seed(42) n = 365 * 5 # 5年日解 t = np.arange(n) trend = 0.001 * t / 365 # 线性趋势,1mm/年 annual = 0.003 * np.sin(2 * np.pi * t / 365.25) # 周年,3mm semi_annual = 0.001 * np.sin(4 * np.pi * t / 365.25) # 半周年,1mm noise = 0.001 * np.random.randn(n) # 白噪声,1mm ts = trend + annual + semi_annual + noise # 小波分解 wavelet = 'db4' max_level = pywt.dwt_max_level(len(ts), wavelet) print(f"最大分解层数: {max_level}") # 分解6层,边界用对称延拓 coeffs = pywt.wavedec(ts, wavelet, level=6, mode='symmetric') # coeffs[0] 是近似系数(最低频),coeffs[1:] 是各层细节系数 # 重构各层分量 components = [] for i in range(len(coeffs)): c = [np.zeros_like(coeffs[j]) for j in range(len(coeffs))] c[i] = coeffs[i] comp = pywt.waverec(c, wavelet, mode='symmetric') components.append(comp[:len(ts)]) # 截断到原始长度 # 绘制各层分量 fig, axes = plt.subplots(len(components) + 1, 1, figsize=(12, 10), sharex=True) axes[0].plot(ts, 'k', linewidth=0.5) axes[0].set_ylabel('原始序列') for i, comp in enumerate(components): axes[i+1].plot(comp, linewidth=0.8) axes[i+1].set_ylabel(f'分量{i}') plt.tight_layout() plt.show()

这段代码的逻辑是:wavedec返回一个列表,第一个元素是近似系数(对应最低频),后面依次是各层细节系数(从高频到低频)。重构时每次只保留一层系数、其余置零,就能得到该层对应的时域分量。

参数说明:wavelet='db4'是Daubechies小波,消失矩为4,对多项式趋势有较好的抑制能力,适合GNSS序列里常见的线性趋势。level=6意味着把序列分成6个频带,对于日解5年序列,第6层对应的周期大约在64到128天之间,能覆盖周年和半周年。mode='symmetric'是边界延拓方式,对称延拓在序列两端不会引入剧烈跳变,比零填充温和得多。

如果你用的是小时解或者更高采样率,分解层数要相应增加。一个经验公式是:level = floor(log2(N)) - 2,N是序列长度。但这不是铁律,最终要看各层分量的物理意义是否清晰。

2.3 分解层数和母小波怎么选

选母小波时,GNSS坐标序列分析里常见的选择有db4、db6、sym8和coif5。db系列消失矩越高,对趋势的拟合越光滑,但计算量也越大。sym系列对称性更好,重构时相位失真小。我一般先用db4跑一遍看各层能量分布,如果周年项被拆到了两层里,说明层数不够或者母小波频率分辨率不足,换db6或增加层数再试。

分解层数的判断标准不是“越多越好”。层数太多,低频分量会混入趋势项,高频分量会变成纯噪声。一个实用的做法是:看各层细节系数的标准差,如果某一层之后标准差趋于平稳且接近噪声水平,那一层之后就可以不再细分。

注意:小波分解不是万能的。它对非平稳信号的局部特征敏感,但对周期成分的分离能力不如SSA。所以实际工作中,我通常先用小波把高频噪声和低频趋势剥掉,再对中间频带做SSA,把周期项提纯。

3. 奇异谱分析:从轨迹矩阵到分量重构的完整链路

3.1 SSA的四个步骤和GNSS序列的适配

SSA的核心流程是:嵌入、分解、分组、重构。对一条长度为N的GNSS坐标序列,先选窗口长度L,构造轨迹矩阵X,大小为L×(N-L+1)。然后对X做SVD,得到奇异值和左右奇异向量。奇异值从大到小排列,前几个对应趋势和强周期,后面的对应噪声。分组就是选哪些奇异值对属于同一个分量,重构就是把选中的分量叠加回去。

GNSS序列做SSA时,窗口长度L的选择很关键。L太小,周期分辨率不够,周年和半周年分不开;L太大,计算量暴涨,而且趋势项会占据多个奇异值对。经验上,L取序列长度的1/3到1/2,但要保证L至少能覆盖你最关心的最长周期。比如你要分离周年项,L至少得大于365天。对于5年日解序列,L取730到1095比较合适。

3.2 用Python实现SSA并分离周年项

下面这段代码对去趋势后的GNSS序列做SSA,窗口长度取730,重构前4个分量。

import numpy as np def ssa_decompose(ts, L): """ ts: 一维时间序列 L: 窗口长度 返回: 奇异值, 左奇异向量, 右奇异向量, 轨迹矩阵 """ N = len(ts) K = N - L + 1 # 构造轨迹矩阵 X = np.zeros((L, K)) for i in range(K): X[:, i] = ts[i:i+L] # SVD分解 U, s, Vt = np.linalg.svd(X, full_matrices=False) return s, U, Vt, X def ssa_reconstruct(U, s, Vt, indices): """ 重构指定索引的分量 indices: 要保留的奇异值索引列表 """ X_rec = np.zeros_like(U @ np.diag(s) @ Vt) for i in indices: X_rec += s[i] * np.outer(U[:, i], Vt[i, :]) # 对角线平均恢复一维序列 L, K = X_rec.shape N = L + K - 1 ts_rec = np.zeros(N) counts = np.zeros(N) for i in range(L): for j in range(K): ts_rec[i+j] += X_rec[i, j] counts[i+j] += 1 ts_rec /= counts return ts_rec # 用上一节去趋势后的序列 # 先去掉线性趋势 from scipy import signal ts_detrend = signal.detrend(ts) L = 730 # 窗口长度,约2年 s, U, Vt, X = ssa_decompose(ts_detrend, L) # 看奇异值谱 print("前10个奇异值:", s[:10]) print("奇异值占比:", s[:10] / s.sum()) # 重构前4个分量(通常对应周年、半周年等强周期) ts_ssa = ssa_reconstruct(U, s, Vt, [0, 1, 2, 3]) # 对比原始去趋势序列和SSA重构 import matplotlib.pyplot as plt plt.figure(figsize=(12, 4)) plt.plot(ts_detrend, 'k', alpha=0.5, label='去趋势序列') plt.plot(ts_ssa, 'r', linewidth=1.5, label='SSA重构(前4分量)') plt.legend() plt.show()

这段代码里,ssa_decompose构造轨迹矩阵后直接调np.linalg.svd做分解。ssa_reconstruct把选中的奇异值对叠加回轨迹矩阵,再通过对角线平均恢复成一维序列。对角线平均是SSA重构的标准步骤,因为轨迹矩阵的反对角线元素对应原始序列的同一个点。

参数说明:L=730是窗口长度,对于日解序列约等于2年,能覆盖周年和半周年周期。indices=[0,1,2,3]是保留前4个奇异值对,实际工作中要看奇异值谱的拐点——如果前两个奇异值远大于后面的,说明趋势或强周期占主导;如果前4个都比较大且成对出现,通常对应两个周期项。

3.3 分组策略:怎么判断哪些奇异值对属于同一个分量

SSA分组是整个过程里最依赖经验的一步。几个实用判据:

第一,看奇异值对是否成对出现。周期项的左右奇异向量通常成对出现,对应的奇异值接近相等。如果你看到s[2]和s[3]很接近,s[4]和s[5]很接近,那它们很可能分别对应两个不同的周期。

第二,看重构分量的频谱。把每个奇异值对单独重构出来,做FFT,看主峰频率。如果两个分量的主峰频率相同但相位差90度,它们应该合并。

第三,看W-correlation矩阵。计算各分量之间的加权相关系数,如果两个分量高度相关,说明它们应该分到一组。这个在SSA的标准文献里有详细公式,Python里可以自己实现。

我一般会先单独重构前10个分量,画出来看波形,再决定怎么合并。周年项通常占两个奇异值对,半周年占两个,趋势占一到两个。剩下的基本是噪声。

4. 小波和SSA串起来用:GNSS坐标序列的完整处理流程

4.1 先小波后SSA,还是先SSA后小波

这两种顺序我都试过,结论是:先小波去高频噪声,再SSA分周期,效果更稳。原因在于SSA对噪声敏感,如果序列里高频噪声能量太大,SVD的前几个奇异值会被噪声污染,分组时容易把噪声当成信号。小波先去高频,相当于给SSA一个更干净的输入。

但小波去高频时要注意:不要去掉太多。GNSS序列里的高频成分不全是噪声,有些短周期形变信号(比如地震后的瞬态)也在高频段。我一般只去掉最高频那一到两层,保留其余。

4.2 完整流程的代码串联

下面把前面的步骤串起来,形成一条从原始GNSS序列到分量分离的完整链路。

import numpy as np import pywt from scipy import signal import matplotlib.pyplot as plt def gnss_ssa_wavelet_pipeline(ts, wavelet='db4', wav_level=6, ssa_L=730, ssa_keep=4): """ GNSS坐标序列处理流程:小波去噪 + SSA分量分离 ts: 原始序列 wavelet: 母小波 wav_level: 小波分解层数 ssa_L: SSA窗口长度 ssa_keep: SSA保留的奇异值对数 """ # Step 1: 小波分解 coeffs = pywt.wavedec(ts, wavelet, level=wav_level, mode='symmetric') # Step 2: 去掉最高频细节系数(第一层),保留其余 coeffs_denoised = coeffs.copy() coeffs_denoised[1] = np.zeros_like(coeffs[1]) # 第一层细节置零 # Step 3: 重构去噪序列 ts_denoised = pywt.waverec(coeffs_denoised, wavelet, mode='symmetric')[:len(ts)] # Step 4: 去线性趋势 ts_detrend = signal.detrend(ts_denoised) # Step 5: SSA分解 N = len(ts_detrend) K = N - ssa_L + 1 X = np.zeros((ssa_L, K)) for i in range(K): X[:, i] = ts_detrend[i:i+ssa_L] U, s, Vt = np.linalg.svd(X, full_matrices=False) # Step 6: 重构前ssa_keep个分量 X_rec = np.zeros_like(X) for i in range(ssa_keep): X_rec += s[i] * np.outer(U[:, i], Vt[i, :]) # 对角线平均 ts_ssa = np.zeros(N) counts = np.zeros(N) for i in range(ssa_L): for j in range(K): ts_ssa[i+j] += X_rec[i, j] counts[i+j] += 1 ts_ssa /= counts # Step 7: 残差 = 去趋势序列 - SSA重构 residual = ts_detrend - ts_ssa return ts_denoised, ts_detrend, ts_ssa, residual, s # 使用示例 ts_denoised, ts_detrend, ts_ssa, residual, s = gnss_ssa_wavelet_pipeline(ts) # 绘图 fig, axes = plt.subplots(4, 1, figsize=(12, 10), sharex=True) axes[0].plot(ts, 'k', linewidth=0.5) axes[0].set_ylabel('原始序列') axes[1].plot(ts_denoised, 'b', linewidth=0.8) axes[1].set_ylabel('小波去噪后') axes[2].plot(ts_ssa, 'r', linewidth=1.2) axes[2].set_ylabel('SSA重构分量') axes[3].plot(residual, 'gray', linewidth=0.5) axes[3].set_ylabel('残差') plt.tight_layout() plt.show() # 打印奇异值占比 print("前10个奇异值占比:", s[:10] / s.sum())

这段代码把整个流程封装成一个函数,输入原始序列,输出去噪序列、去趋势序列、SSA重构分量和残差。关键参数:wav_level=6对应日解5年序列,ssa_L=730对应2年窗口,ssa_keep=4保留前4个奇异值对。

实际使用时,ssa_keep要根据奇异值谱调整。如果前4个奇异值占比超过80%,说明主要信号集中在前4个分量;如果占比不到60%,可能需要增加保留数量,或者检查序列里是否有未去除的阶跃。

4.3 阶跃和粗差怎么处理

GNSS序列里经常有天线更换、地震导致的阶跃,还有粗差。这些如果不处理,小波分解会在阶跃处产生伪高频,SSA的SVD也会被阶跃污染。

处理阶跃的常见做法是:先检测阶跃位置(可以用STARS或手动看),然后在阶跃处把序列分成两段,分别去均值后再拼接。粗差用3σ或MAD准则剔除,但要注意不要把真实的地震瞬态当成粗差删掉。

提示:如果序列里有明显的阶跃,先做阶跃改正再做小波和SSA。否则后面所有分量都会被阶跃的“振铃”效应干扰,重构出来的周期项幅度会偏大。

5. 避坑与排查:GNSS坐标序列做小波SSA时最容易翻车的5件事

5.1 边界效应导致两端分量失真

现象:小波重构后,序列首尾各几十个点出现剧烈波动,和中间段完全对不上。

原因:小波分解默认用零填充或周期延拓,GNSS序列两端没有“上下文”,延拓出来的值和真实值差异大,重构时边界处产生伪影。

解决:用mode='symmetric'或mode='periodization'。对称延拓在两端镜像反射,不会引入跳变。如果还是不行,可以在序列两端各补一段数据(比如用AR模型预测),分解完再截掉。

5.2 SSA窗口长度选错导致周期分不开

现象:SSA重构后,周年项和半周年项混在一个分量里,或者周年项被拆到了两个分量里。

原因:窗口长度L太短,频率分辨率不够。SSA的频率分辨率大约是1/L,要分开周年(1/365)和半周年(1/182),L至少得大于365天,最好到730天。

解决:L取序列长度的1/3到1/2,且L > 最长周期的2倍。对于日解5年序列,L=730到1095。如果计算量太大,可以先用小波把低频趋势去掉,再对残差做SSA,这样L可以小一些。

5.3 奇异值分组把噪声当信号

现象:SSA重构出来的分量看起来像噪声,但奇异值却不小。

原因:GNSS序列里的有色噪声(比如闪烁噪声)在SVD里也会产生较大的奇异值,和周期项的奇异值混在一起。

解决:看W-correlation矩阵,噪声分量之间的相关性低,周期分量之间相关性高。另外,周期分量的左右奇异向量是成对出现的,噪声不是。如果某个奇异值对的重构分量频谱没有明显峰值,大概率是噪声,不要保留。

5.4 小波去噪去掉了真实信号

现象:小波去噪后,序列里原本有的短周期形变信号消失了。

原因:最高频细节系数里不全是噪声,可能包含真实的短周期信号。直接置零太粗暴。

解决:不要直接置零,用软阈值或硬阈值。阈值取sigma * sqrt(2 * log(N)),sigma用最高频系数的MAD估计。这样只压缩噪声,保留较大的系数。

5.5 重构后序列长度对不上

现象:pywt.waverec返回的序列比原始序列长几个点。

原因:小波分解时,wavedec返回的系数长度和原始序列长度不一定相等,重构时waverec会补零或截断。

解决:重构后统一截断到原始长度:ts_rec = ts_rec[:len(ts)]。如果长度差超过几个点,检查分解层数是否过大,导致近似系数太短。

6. 进阶技巧:用SSA的W-correlation矩阵自动分组

前面说的分组靠经验看波形,其实可以更系统化。SSA里有一个W-correlation矩阵,衡量任意两个重构分量之间的加权相关性。如果两个分量的W-correlation接近1,说明它们应该合并;接近0,说明它们独立。

下面这段代码计算W-correlation矩阵,并据此自动分组。

import numpy as np def w_correlation_matrix(ts, L, num_components=20): """ 计算SSA分量的W-correlation矩阵 ts: 去趋势序列 L: 窗口长度 num_components: 计算前多少个分量 """ N = len(ts) K = N - L + 1 X = np.zeros((L, K)) for i in range(K): X[:, i] = ts[i:i+L] U, s, Vt = np.linalg.svd(X, full_matrices=False) # 重构每个分量 components = [] for i in range(num_components): X_i = s[i] * np.outer(U[:, i], Vt[i, :]) ts_i = np.zeros(N) counts = np.zeros(N) for m in range(L): for n in range(K): ts_i[m+n] += X_i[m, n] counts[m+n] += 1 ts_i /= counts components.append(ts_i) # 计算W-correlation w = np.minimum(np.arange(1, N+1), np.arange(N, 0, -1)) w = np.minimum(w, L) w = np.minimum(w, K) W = np.zeros((num_components, num_components)) for i in range(num_components): for j in range(num_components): num = np.sum(w * components[i] * components[j]) den = np.sqrt(np.sum(w * components[i]**2) * np.sum(w * components[j]**2)) W[i, j] = num / den if den > 0 else 0 return W, components # 计算W-correlation矩阵 W, components = w_correlation_matrix(ts_detrend, L=730, num_components=10) # 打印矩阵 print("W-correlation矩阵(前10个分量):") print(np.round(W, 3)) # 根据W-correlation分组:如果W[i,j] > 0.8,认为应该合并 groups = [] visited = set() for i in range(len(W)): if i in visited: continue group = [i] visited.add(i) for j in range(i+1, len(W)): if j not in visited and W[i, j] > 0.8: group.append(j) visited.add(j) groups.append(group) print("自动分组结果:", groups)

这段代码先重构前num_components个分量,然后计算加权相关系数矩阵。权重w是三角形窗,反映每个时间点被轨迹矩阵覆盖的次数。分组时,如果两个分量的W-correlation大于0.8,就归为一组。

参数说明:num_components=20是计算前20个分量,实际看前10个就够了。阈值0.8是经验值,可以调到0.9让分组更严格。分组结果里,每组对应一个物理分量——趋势、周年、半周年或噪声。

这个方法的局限是计算量大,重构每个分量都要做一次对角线平均。如果序列很长,可以只计算前10到15个分量,后面的直接当噪声。

我自己的习惯是:先用W-correlation自动分一遍,再人工看每组重构出来的波形和频谱,确认物理意义。自动分组能省掉大量试错时间,但最终判断还得靠对GNSS序列本身的理解。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询