小波阈值去噪:用多尺度分解把高频噪声和真实跳变分开
去噪的终极难题不是『去掉噪声』,是『别把真实跳变一起磨平』。小波阈值去噪的答案:噪声的能量摊平在所有细节层系数上,真实跳变的能量集中在少数几个大系数上——设一条阈值线,小系数归零、大系数存活,跳变就保住了。合成实验(4 个 ±4~6 元的真实跳变 + std 1.2 的噪声):MAD 从最细层估出 sigma=1.178(真值 1.2),universal threshold 硬阈值把整体 RMSE 从 1.135 压到 0.474;跳变后误差收敛只要 2 天,20 日均线要 5 天,因果均线的跳变后 12 日 RMSE 是小波的 3 倍。但最重要的一节是反面教材:同一个去噪算法,离线全样本版在跳变前一天就开始『预降』(去噪值 99.83 vs 观测 102.99)——它看见了未来。用离线去噪序列做趋势策略总收益 +19.5,换成只用当日以前数据的因果滚动版立刻变成 -16.7。文献里大量『小波去噪+LSTM』的惊人精度,就是死在这一步(中高阶)。
先说结论:去噪的终极难题从来不是”去掉噪声”——任何低通滤波器都能去噪声,难的是”别把真实跳变一起磨平”。小波阈值去噪的核心洞察:白噪声的能量均匀摊在所有小波系数上(每个都很小),而跳变、突变这类局部结构的能量集中在少数几个大系数上。设一条阈值线:小系数全部归零,大系数原样保留——噪声死了,跳变活着。合成实验实测:整体 RMSE 从 1.135 压到 0.474,跳变后误差收敛只要 2 天(20 日均线要 5 天)。但本文最重要的部分是第六节的反面教材:同一个去噪算法,离线全样本使用时会在跳变发生前一天就开始”预降”——它看见了未来。用离线去噪序列做趋势策略总收益 +19.5,改成因果滚动版立刻变成 -16.7。这不是小波的错,是用法的错,而文献里大量”小波去噪 + 机器学习”的惊人精度就是死在这一步。

一、线性滤波器的原罪:平滑和保跳变不可兼得#
移动平均、指数平滑、Butterworth 低通——所有线性滤波器共享同一个数学限制:它们对信号的处理不依赖信号内容。同一组权重同时作用于噪声和跳变,想把噪声磨得越干净(窗口越长/截止频率越低),跳变就被拖得越钝。20 日均线遇到一个 -6 元的单日跳变,需要 20 天才能完全消化,中间这 20 天的输出既不是跳变前的价格也不是跳变后的价格,是两者的错误混合。
对量化研究这是实打实的伤害:财报跳空、政策冲击、指数调仓日的价格跳变是真实信息,把它磨平等于删除信息;不磨平又满屏噪声。线性滤波在这两者之间只能选一个。
小波阈值去噪(Donoho & Johnstone, 1994)跳出了这个两难,靠的是非线性:先把序列分解到多个尺度上,再按系数大小逐个决定去留。处理是内容依赖的——这是它和一切线性滤波的本质区别。
二、多尺度分解:噪声摊平,跳变集中#
离散小波变换(DWT)把 1024 日的序列分解成一层近似系数(低频轮廓)加多层细节系数(不同尺度的局部波动):
import pywt
import numpy as np
# x: 1024 日合成价格 = 慢趋势 + 4 个真实跳变(±4~6 元) + std 1.2 的高斯噪声
coeffs = pywt.wavedec(x, 'sym8', level=5)
# coeffs = [A5, D5, D4, D3, D2, D1]
# 长度分别为 [46, 46, 78, 141, 267, 519]python关键性质有两条。第一,正交小波变换保持白噪声的统计性质:时域里 std 为 σ 的白噪声,变换后每个系数还是 std 为 σ 的白噪声——能量被均匀摊在全部 1051 个细节系数上,每个都不大。第二,局部突变的能量集中:一个单日跳变在每层细节里只影响与小波支撑重叠的少数几个系数,这几个系数会非常大。
同样的总能量,一个摊成薄饼,一个堆成尖塔——中间画一条线,就能把它们分开。

图里红色虚线就是阈值。最细层 D1 的 519 个系数几乎全军覆没(绝大多数是纯噪声),而每层里刺破红线的孤立大系数,位置恰好对应 200、450、700、880 四个真实跳变日。
三、阈值怎么定:MAD 估 sigma + universal threshold#
阈值定低了噪声漏网,定高了跳变被误杀。Donoho–Johnstone 的经典方案分两步。
第一步,从最细层估计噪声水平。 D1 层几乎全是噪声(真实信号在最细尺度上的成分极少),但可能混着少数跳变产生的大系数——所以不用标准差(会被大系数污染),用对离群值稳健的 MAD:
sigma = np.median(np.abs(coeffs[-1])) / 0.6745
# 估出 1.178,真值 1.2,误差 1.8%python0.6745 是标准正态分布的 MAD 与 σ 的比值。这一步的稳健性是整个流程的基石:即使 D1 里混进了跳变系数,中位数纹丝不动。
第二步,universal threshold:
这个公式的来历很优雅:n 个独立标准正态变量的最大值以高概率不超过 。也就是说,把阈值设在这里,纯噪声系数几乎一个都活不下来——这是”以高概率完全消灭噪声”的最小阈值。
阈值化有软硬两种:
# 硬阈值:|c| < λ 归零,其余原样保留
# 软阈值:|c| < λ 归零,其余向零收缩 λ
denoised_coeffs = [coeffs[0]] + [
pywt.threshold(c, uthresh, mode='hard') for c in coeffs[1:]
]
x_denoised = pywt.waverec(denoised_coeffs, 'sym8')[:n]python软阈值输出更平滑(无伪吉布斯振荡),但对幸存的大系数也收缩了 λ,跳变幅度被系统性低估;硬阈值保幅度但偶有毛刺。实测本例:硬阈值 RMSE 0.474,软阈值 0.523——保跳变的场景硬阈值占优,这和直觉一致。
四、对决移动平均:平滑段打平,跳变段碾压#
拿 20 日居中移动平均做对照(注意:居中均线本身就用了未来 10 天数据,这已经是给均线开挂了):
| 指标 | 小波硬阈值 | 小波软阈值 | 20日居中均线 | 20日因果均线 |
|---|---|---|---|---|
| 整体 RMSE | 0.474 | 0.523 | 0.480 | — |
| 跳变后误差收敛天数 | 2 天 | 2 天 | 5 天 | >10 天 |
| 跳变后 12 日 RMSE | 1.053 | 1.209 | 1.192 | 3.340 |

整体 RMSE 上小波和居中均线几乎打平(0.474 vs 0.480)——平滑段里小波没有优势,这个结论要诚实。差距全部出现在跳变附近:450 日的 -6 元跳变,小波 2 天内误差收敛到 25% 以内,居中均线要 5 天,因果均线(实盘唯一可用的均线)跳变后 12 日 RMSE 是小波的 3.2 倍。
结论很清晰:序列越平滑,小波相对线性滤波的优势越小;跳变越多越大,优势越碾压。 如果你的数据本来就没什么跳变(比如已经日度聚合的宏观指标),用小波纯属炫技。
五、幅度保真的细账#
跳变没被磨平,但幅度保住了吗?硬阈值重构后 450 日跳变幅度约 -5.6 元(真值 -6),损失约 7%——来自跳变能量中低于阈值的那部分细节系数被误杀。软阈值损失更大(约 15%),因为幸存系数还被额外收缩了 λ。
想进一步压低幅度损失,工程上有两个常用升级:SureShrink(逐层用 Stein 无偏风险估计选阈值,通常比 universal 更温和)和平移不变去噪(cycle spinning:对序列做多个循环平移分别去噪再平均,消除跳变位置与小波格点错位造成的伪影)。两者都在 pywt 生态里有现成实现,本文用 universal + 硬阈值是为了让每一步都可手算验证。
六、反面教材:离线去噪就是未来函数#
这一节是全文最值钱的部分。上面所有去噪都是离线全样本的:对完整的 1024 日序列做一次 DWT。问题在于小波是有支撑宽度的基函数——sym8 在第 j 层的支撑约 天。第 t 天的重构值,依赖以 t 为中心前后各几十天的系数。换句话说:离线去噪后的第 t 天数值,包含了 t 之后的信息。
看实测数据有多惊悚。450 日发生 -6 元跳变,跳变前一天(449 日):
| 观测价 | 离线去噪 | 因果去噪 | 真实值 | |
|---|---|---|---|---|
| 449 日(跳变前一天) | 102.99 | 99.83 | 102.29 | 102.28 |
离线去噪在跳变发生之前就把价格降了 3.16 元——它”看见”了明天的暴跌,提前把曲线往下弯。因果版(每天只用当日以前 256 天数据重新分解,只取最后一点)老老实实停在 102.29,和真实值几乎一致。

把两个版本的去噪序列喂给同一个最简趋势策略(去噪曲线斜率为正做多,次日执行):
- 离线去噪版:总收益 +19.5 —— 它总能在跳变前一天”恰好”调仓,因为去噪曲线提前弯了;
- 因果滚动版:总收益 -16.7 —— 这才是实盘能拿到的成绩。
同一个算法,同一段数据,同一个策略,收益差出 36 个点。文献里”小波去噪 + LSTM 预测股价,精度 98%“的论文汗牛充栋,绝大多数都是对全样本先去噪再切训练/测试集——测试集的”输入”里已经嵌进了测试集未来的信息。这类结果复现不出实盘收益,不是玄学,是算术。
因果版的正确写法:
win = 256
causal = np.full(n, np.nan)
for i in range(win, n):
seg = x[i-win:i] # 只用截至 i 的数据
cf = pywt.wavedec(seg, 'sym8', level=4)
sg = np.median(np.abs(cf[-1])) / 0.6745
th = sg * np.sqrt(2*np.log(win))
cf = [cf[0]] + [pywt.threshold(c, th, 'hard') for c in cf[1:]]
causal[i] = pywt.waverec(cf, 'sym8')[:win][-1] # 只取最后一点python代价也要说清楚:因果版的”最后一点”永远处在小波变换的右边界上,边界延拓(默认对称延拓)会引入额外误差,去噪质量比离线版差(两者差异 std 0.69)。这个质量损失不是实现缺陷,是因果性的物价——任何声称”既因果又达到离线质量”的去噪方法都值得怀疑。
七、真实市场里的额外坑#
噪声不是白的也不是高斯的。 金融收益率有肥尾和波动聚集,MAD 估计的”噪声水平”在高波动期会系统性偏大,阈值跟着变大,把真实的小信号也杀掉。对策:对收益率先做波动率标准化(除以 GARCH 或已实现波动率)再去噪,或逐层用局部窗口估 sigma。
“跳变”和”肥尾噪声”在单日尺度上不可区分。 小波保住的大系数,可能是财报跳空(信息),也可能是单纯的肥尾抽样(噪声)。小波只能按能量大小分类,不能按语义分类。想区分,需要引入外部信息(事件日历、成交量)——这超出了任何纯信号处理方法的能力边界,诚实的用法是把小波输出当”候选跳变检测器”,再用事件数据过滤。
去噪序列不能直接当交易价格。 去噪后的曲线是平滑的估计值,实盘成交发生在带噪声的观测价上。回测里用去噪价格算成交,等于假设你能以”理论真值”成交——又一个隐蔽的乐观偏差。正确用法:信号从去噪序列生成,成交价用原始观测价。
八、Checklist:小波去噪的最小纪律#
- 回测/实盘一律用因果滚动版——离线全样本去噪是未来函数,本文实测同一策略 +19.5 vs -16.7;
- sigma 用 MAD 从最细层估——标准差会被跳变系数污染;
- 保跳变用硬阈值,保平滑用软阈值——本例硬阈值 RMSE 0.474 vs 软 0.523;
- 平滑数据别用小波——没有跳变时它对线性滤波没有优势(0.474 vs 0.480),徒增复杂度;
- 高波动期先做波动率标准化——否则 MAD 高估噪声,阈值误杀信号;
- 信号从去噪序列生成,成交价用原始价——去噪价成交是隐蔽的乐观偏差;
- 看到”小波+ML 精度 98%“的文献先查一件事——去噪是在训练/测试切分之前还是之后做的。之前做的,直接扔。
小波阈值去噪是少数”数学漂亮、工程也真能打”的信号处理技术:噪声摊平、信号集中、一刀切开。但它在量化里的第一大杀伤不是去噪效果,而是被错用成未来函数后制造的幻觉收益。工具越锋利,握法越重要。