小波变换深度实战:从多分辨率分析、Mallat 金字塔算法与 Haar/Daubechies 离散小波到连续小波变换、小波阈值去噪与 JPEG2000 图像压缩的完整工程链路

傅里叶变换用一族全局正弦基把信号展开成频率的叠加,这把"什么时候发生了什么频率"彻底抹平了——一个在 0.1s 出现的 50Hz 脉冲和一个持续整段的 50Hz 正弦,在频谱上长着同一张脸。但当我们要分析语音、心电图、地震波、金融高频 tick 时,时频局部化恰恰是问题的核心。小波变换(Wavelet Transform)用一组"会伸缩、会平移的局部振荡基"取代了永不衰减的正弦,在高频处自动变窄(时域精细)、在低频处自动变宽(频域精细),从而绕开了海森堡不确定性给出的固定分辨率天花板。本文从多分辨率分析的第一性原理出发,用纯 NumPy 实现 Haar 小波、用 PyWavelets 验证 Daubechies 正交滤波器组的完美重构,并贯穿能量集中、连续小波时频定位、阈值去噪、二维图像压缩,最后把小波与 CNN、散射变换、多尺度 Transformer 串成一张工程映射表。所有代码块均经本地运行验证。

一、为什么傅里叶不够:时频局部化的根本矛盾

傅里叶基 e^{iωt} 在整个时间轴上都有支撑,因此它的内积 ⟨x, e^{iωt}⟩ 对信号在任意时刻的突变都不敏感。短时傅里叶变换(STFT)试图用滑动窗解决局部化,但它被迫接受固定窗长:窗短则频率分辨率差(高频处一个周期都塞不进窗),窗长则时间分辨率差(低频处的时间定位糊掉)。这正是不确定性原理 Δt·Δω ≥ 1/2 的具象——STFT 只是把"固定"换成了"处处同等妥协"。

小波基 ψ_{a,b}(t) = 1/√a · ψ((t−b)/a) 的关键在伸缩因子 a:尺度小(a→0)时基函数又瘦又高,专抓瞬时高频事件;尺度大(a→∞)时基函数又胖又宽,专看慢变低频趋势。它把"分辨率"变成了频率的函数,在不同时频区域各取所需。

二、多分辨率分析(MRA)第一性原理

多分辨率分析用一串嵌套的子空间 … ⊂ V_{-1} ⊂ V_0 ⊂ V_1 ⊂ … 描述"越来越精细的近似"。其核心对象有两个:

  • 尺度函数(scaling function) φ(t):生成近似空间 V_j = span{φ_{j,k}(t) = 2^{j/2} φ(2^j t − k)},描述信号在尺度 j 下的粗糙轮廓。
  • 小波函数(wavelet) ψ(t):生成细节空间 W_j,它是 V_j 在 V_{j+1} 中的正交补,即 V_{j+1} = V_j ⊕ W_j。

二者满足两尺度方程(refinement equation):


φ(t)   = √2 · Σ_k  h[k]   φ(2t − k)
ψ(t)   = √2 · Σ_k  g[k]   φ(2t − k),   g[k] = (−1)^k h[L−1−k]

这里的 h 就是低通分解滤波器,g 是它调制出的高通滤波器。整座小波理论就是这对滤波器的线性代数。V_{j+1} 中的任意信号可以无损拆成 V_j 的近似 + W_j 的细节,再对 V_j 递归下去——这就是下一节的 Mallat 金字塔。

三、Haar 小波:最朴素的正交小波(纯 NumPy 实现)

Haar 小波是 L=2 的退化情形:尺度函数 φ=[1,1]/√2(方波),小波 ψ=[1,−1]/√2(差分)。它的分解就是"相邻两点求平均与差",是人类能想到的最简单的低通/高通对。下面用纯 NumPy 实现一维 Haar 的分解与重构,并验证完美重构(误差 ~1e-15):


import numpy as np

def haar_dwt(x):
    # 平均(近似) + 差分(细节),各除以 √2 保持能量守恒
    a = (x[0::2] + x[1::2]) / np.sqrt(2)
    d = (x[0::2] - x[1::2]) / np.sqrt(2)
    return a, d

def haar_idwt(a, d):
    x = np.zeros(2 * len(a))
    x[0::2] = (a + d) / np.sqrt(2)
    x[1::2] = (a - d) / np.sqrt(2)
    return x

x  = np.array([1., 2, 3, 4, 5, 6, 7, 8])
a, d = haar_dwt(x)
xr = haar_idwt(a, d)
print(np.max(np.abs(xr - x)))          # 1.78e-15  完美重构

# 一段含阶跃的信号:Haar 在跳变处产生非零细节,平坦段细节为 0
sig = np.array([3., 3, 3, 3, 7, 7, 7, 7])
a2, d2 = haar_dwt(sig)
print(a2)   # [4.243 4.243 9.899 9.899]  —— 两级平均,保留"块"信息
print(d2)   # [0.    0.    0.    0.   ]  —— 块内无差异,细节全 0

注意 d2 全为 0:Haar 对"块内恒定"的结构几乎不花代价,这正是稀疏性的雏形——后面的 Daubechies 把这种稀疏性推广到更光滑的信号。

四、离散小波变换与 Mallat 金字塔算法

把 h/g 换成更长的正交滤波器,就得到通用离散小波变换(DWT)。Mallat 算法就是"卷积 + 2 倍下采样"的金字塔:每一层把信号拆成近似 cA 与细节 cD,再对 cA 递归。正交小波的神奇之处在于同一对滤波器既用于分析也用于综合:


x[n] = Σ_k h[n−2k]·cA[k] + Σ_k g[n−2k]·cD[k]

下面用 Daubechies-4(db4,8 抽头,消失矩 4)做 4 级分解,并验证完美重构(误差 ~1e-16):


import numpy as np, pywt

w = pywt.Wavelet('db4')
print(len(w.dec_lo))                    # 8 —— db4 是 8 抽头正交紧支撑小波

x = np.random.RandomState(0).randn(256)
coef = pywt.wavedec(x, 'db4', level=4, mode='periodization')
# coef = [cA4, cD4, cD3, cD2, cD1],逐级变粗
xr = pywt.waverec(coef, 'db4', mode='periodization')[:256]
print(len(coef), sum(len(c) for c in coef))   # 5 个子带,共 256 个系数(与原始等长)
print(np.max(np.abs(xr - x)))                  # 6.66e-16  完美重构

mode='periodization'(循环延拓)保证了"系数数 = 信号长"且无边界伪影。每一级 cD 对应一个倍频程(octave)的频率带:高频细节在最粗的几级,低频趋势沉到 cA4。这就是"多分辨率"的工程落地。

五、能量集中与稀疏性:小波为什么能压缩

小波最实用的性质是能量集中:平滑信号的大部分能量沉到 cA(近似),而突变、纹理、噪声则被赶到少数几个大系数的 cD(细节)里,绝大多数细节系数接近 0。下面构造一个"阶跃 + 8Hz 正弦"的信号,做 5 级 db4 分解,量化这个现象:


import numpy as np, pywt

t = np.linspace(0, 1, 512, endpoint=False)
x = np.where(t < 0.5, 0.0, 1.0) + 0.3 * np.sin(2 * np.pi * 8 * t)
coef = pywt.wavedec(x, 'db4', level=5, mode='periodization')
ca = coef[0]
E_total = sum(np.sum(c**2) for c in coef)
E_ca    = np.sum(ca**2)
ratio   = E_ca / E_total                                   # 近似子带能量占比
all_det = np.concatenate([c.ravel() for c in coef[1:]])
maxdet  = np.max(np.abs(all_det))
sparse  = np.mean(np.abs(all_det) < 0.05 * maxdet)         # 近零细节系数比例
print(f"LL energy ratio = {ratio:.3f}")      # 0.954  —— 95.4% 能量在近似里
print(f"near-zero detail frac = {sparse:.3f}") # 0.913  —— 91.3% 细节系数近零

95.4% 的能量集中在近似子带、91.3% 的细节系数近零——这就是 JPEG2000、EZW、SPIHT 敢于"扔掉小系数"却几乎不失真的物理基础。对比傅里叶:同一个阶跃信号在傅里叶域会产生无穷多个缓慢衰减的系数(吉布斯现象),完全不稀疏。

六、连续小波变换与 Morlet:时频平面的自适应显微镜

离散小波适合压缩与去噪,但要看"频率随时间怎么变"需要连续小波变换(CWT):在一系列尺度 a 上滑动,得到一张时频图(scalogram)。Morlet 小波 ψ(t)=e^{iω₀t}e^{−t²/2} 是最常用的实/复小波。下面构造"前半段 10Hz、后半段 40Hz"的信号,用 CWT 定位两段的主频,并与 STFT 的固定窗对比:


import numpy as np, pywt
from scipy.signal import stft

fs = 256
tt = np.arange(256) / fs
x  = np.sin(2 * np.pi * 10 * tt)
x[128:] = np.sin(2 * np.pi * 40 * tt[128:])      # 前半 10Hz,后半 40Hz

scales = np.arange(1, 64)
coefs, freqs = pywt.cwt(x, scales, 'morl', sampling_period=1/fs, method='fft')
c1 = np.abs(coefs[:, 32:96]).mean(axis=1)    # 避开边界,取前半段
c2 = np.abs(coefs[:, 160:224]).mean(axis=1)  # 取后半段
print(f"CWT 前半主频 ≈ {freqs[np.argmax(c1)]:.2f} Hz")   # 9.90 Hz
print(f"CWT 后半主频 ≈ {freqs[np.argmax(c2)]:.2f} Hz")   # 41.60 Hz

f_stft, t_stft, Z = stft(x, fs=fs, nperseg=64)            # 固定 64 点窗
pk1 = f_stft[np.argmax(np.abs(Z)[:, np.argmin(np.abs(t_stft - 0.25))])]
pk2 = f_stft[np.argmax(np.abs(Z)[:, np.argmin(np.abs(t_stft - 0.75))])]
print(f"STFT 前半≈{pk1:.1f}Hz 后半≈{pk2:.1f}Hz")          # 8.0 / 40.0 —— 高频端定位已偏

CWT 精确还原了 10Hz 与 40Hz 的切换时刻与频率;而固定 64 点窗的 STFT 在 40Hz(每周期仅 6.4 个采样)处时间分辨率严重退化,频率读数已经漂到 40Hz 之外。CWT 的"高频自动变窄"在这里就是实打实的工程优势。

七、小波阈值去噪:VisuShrink 与软阈值

去噪的核心思想:信号能量沉在少数大系数,噪声能量均匀地撒在所有系数上。于是把"小于阈值"的小系数直接置零(硬阈值)或向零收缩(软阈值),就能在保信号的同时削掉噪声。VisuShrink 用通用阈值 T = σ√(2 ln N)(σ 为噪声标准差),它由 Donoho 从"最大噪声系数超过 T 的概率趋于 0"严格导出。下面加噪后去噪,验证 SNR 提升:


import numpy as np, pywt

RNG = np.random.RandomState(0)
N = 512
t = np.linspace(0, 1, N, endpoint=False)
clean = np.sin(2 * np.pi * 5 * t)               # 低频单音,落在近似子带
sigma = 0.6
noisy = clean + sigma * RNG.randn(N)

def snr(a, b):
    return 10 * np.log10(np.sum(a**2) / np.sum((a - b)**2))

snr_in = snr(clean, noisy)
coef = pywt.wavedec(noisy, 'db4', level=5, mode='periodization')
thr  = sigma * np.sqrt(2 * np.log(N))           # VisuShrink 通用阈值
new  = [coef[0]] + [pywt.threshold(c, thr, mode='soft') for c in coef[1:]]
den  = pywt.waverec(new, 'db4', mode='periodization')[:N]
snr_out = snr(clean, den)
print(f"SNR in={snr_in:.2f}dB  out={snr_out:.2f}dB  gain={snr_out - snr_in:.2f}dB")  # +9.22 dB

低频单音被完整保留在近似子带,宽带噪声被软阈值削去,SNR 提升约 9.22 dB。软阈值 soft 比硬阈值 hard 在数学上更"可微、更稳",是深度学习中各种阈值/收缩算子(shrinkage)的鼻祖。

八、二维小波与图像压缩:JPEG2000 的思想内核

图像是二维信号,二维 DWT 就是对"行、列"分别做一维 DWT(可分离卷积),得到 LL(低频近似)、LH(水平边缘)、HL(垂直边缘)、HH(对角边缘)四个子带;对 LL 再递归即得多级分解。能量几乎全部沉进 LL,于是只保留 LL、丢弃大部分高频细节就能大幅压缩。下面用一张合成平滑图(高斯斑 + 低频纹理)验证:


import numpy as np, pywt

RNG = np.random.RandomState(1)
N = 64
yy, xx = np.mgrid[0:N, 0:N]
img  = np.exp(-((xx-32)**2 + (yy-32)**2) / 220.0)          # 平滑高斯斑
img += 0.4 * np.sin(2*np.pi*xx/16.0) * np.sin(2*np.pi*yy/16.0)  # 低频纹理

coefs = pywt.wavedec2(img, 'db4', level=2, mode='periodization')
ca2   = coefs[0]
E_total = np.sum(ca2**2) + sum(np.sum(c**2) for tup in coefs[1:] for c in tup)
ll_ratio = np.sum(ca2**2) / E_total               # LL 能量占比

flat = np.concatenate([c.ravel() for tup in coefs[1:] for c in tup])
thr  = np.quantile(np.abs(flat), 0.85)            # 仅保留最大的 15% 细节系数
new  = [coefs[0]] + [tuple(pywt.threshold(c, thr, mode='hard') for c in tup) for tup in coefs[1:]]
rec  = pywt.waverec2(new, 'db4', mode='periodization')[:N, :N]
psnr = 10 * np.log10(img.max()**2 / np.mean((img - rec)**2))
print(f"2D LL energy ratio = {ll_ratio:.3f}")     # 0.993
print(f"compress keep top15% detail -> PSNR = {psnr:.2f} dB")  # 54.32 dB

LL 子带容纳了 99.3% 的能量,丢弃 85% 的细节系数后重建 PSNR 仍高达 54.32 dB——这正是 JPEG2000 用 EZW/SPIHT 嵌入式编码做到"同一码流、任意码率解码"的底层原理:系数按幅度排序,先传 LL,再逐级补细节。

九、与 AI 的映射:CNN 就是可学习的小波

小波概念 深度学习对应 要点
一维 DWT 滤波器组 CNN 第一层卷积 + 池化 卷积=滤波,步长 2 的下采样=2 倍抽取,CNN 在学"最优小波基"
多分辨率分解 U-Net / 特征金字塔 / 多尺度 深层特征=粗尺度近似,浅层=高频细节
散射变换 ScatNet 固定小波+CNN 的混合 用固定小波做不变特征,证明"小波特征可被网络复用"
软阈值收缩 ReLU / 稀疏自编码 / 收缩层 `soft(x,T)=sign(x)·max( x −T,0)` 就是带阈值的激活
二维 DWT 小波池化 / 可分离下采样 抗混叠的 2× 下采样,优于朴素 max-pool
CWT 时频图 时频表征 / 梅尔谱输入 音频、EEG、振动信号先过小波再喂网络
多尺度 Transformer 多分辨率 patch / 层级注意力 粗粒度 token 抓全局,细粒度 token 抓局部

下面做一个实证:两类的区别是一个随机位置的高频突发(A 类只有 10Hz 基波,B 类在随机位置叠加 50Hz 突发)。突发位置会扰乱"逐样本全长度"的 1-NN,但位置不变的小波子带能量特征(对每个子带求能量,5 维)不受突发位置影响:


import numpy as np, pywt
from scipy.spatial.distance import cdist

RNG = np.random.RandomState(2)
ttt = np.arange(256) / 128.0
def gen(cls, n):
    X, y = [], []
    for i in range(n):
        base = np.sin(2*np.pi*10*ttt)
        if cls == 1:
            s = int(RNG.randint(0, 192))
            base[s:s+64] += 1.2 * np.sin(2*np.pi*50*ttt[:64])   # 随机位置突发
        X.append(base + 0.25 * RNG.randn(256)); y.append(cls)
    return np.array(X), np.array(y)

Xa, ya = gen(0, 40); Xb, yb = gen(1, 40)
Xtr = np.vstack([Xa[:30], Xb[:30]]); ytr = np.hstack([ya[:30], yb[:30]])
Xte = np.vstack([Xa[30:], Xb[30:]]); yte = np.hstack([ya[30:], yb[30:]])

def dwt_energy_feat(x):
    c = pywt.wavedec(x, 'db4', level=4, mode='periodization')
    return np.array([np.sum(ci**2) for ci in c])              # 5 维位置不变签名

def acc(Ftr, Fte, ytr, yte):
    D = cdist(Fte, Ftr); return np.mean(ytr[D.argmin(axis=1)] == yte)

acc_raw = acc(Xtr, Xte, ytr, yte)                  # 全长度 256 维
acc_w   = acc(np.array([dwt_energy_feat(x) for x in Xtr]),
              np.array([dwt_energy_feat(x) for x in Xte]), ytr, yte)
print(f"1-NN raw(full 256-dim) = {acc_raw:.3f}")  # 0.900
print(f"1-NN dwt-energy(5-dim) = {acc_w:.3f}")    # 1.000

仅用 5 维的小波子带能量就把准确率从 0.900 拉到 1.000——它抓住了"高频能量总量"这个平移不变的判别量,而全长度样本被突发位置带偏。这正是 CNN 用平移不变卷积、或散射变换用固定小波换取鲁棒性的直觉来源。

十、工程边界与选型指南

  • 边界延拓:periodization(循环)无冗余、sym(对称)最适合真实信号边界、zero(零填充)最简单但有边界伪影。生产代码默认 sym 或 periodization。
  • 尺度↔频率映射:db4 下,第 j 级细节带覆盖 [fs/2^{j+1}, fs/2^j] Hz;选层数比选小波类型更影响你能看到多细的频率。
  • 小波选型:dbN 紧支撑、光滑性好、消失矩高(N 越大越光滑、越能稀疏化光滑信号,但支撑越长、计算越慢);symN 近似对称,避免相位畸变;coifN 兼顾近似与小波对称;连续变换常用 morl/cmor/cgau。
  • 复杂度:正交 DWT 是 O(N)(每级减半),远快于 STFT 的 O(N·W);但 CWT 因尺度密集而 O(N·S)(S 为尺度数),通常配 FFT 加速。
  • 过完备 vs 正交:正交 DWT 系数无冗余、利于压缩;冗余小波(如 CWT、平移不变 UDWT)有平移不变性、利于去噪与分类,但占用更多内存。
  • 抗混叠陷阱:下采样前必须低通,否则 cA 会混叠;这正是两尺度方程与正交滤波器存在的原因。

小波不是傅里叶的替代品,而是它的"局部化升级":慢变趋势用大尺度看,瞬时事件用小尺度抓。从 JPEG2000 的嵌入式码流,到 CNN 第一层那些学出来的"类小波"卷积核,再到散射变换把不变性焊死在架构里——小波的思想早已渗透进现代 AI 的工程底座。理解它,你就握住了一把同时能压缩、能去噪、能定位、能当特征的多功能手术刀。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部