ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

小波包与冗余小波包:从原理到Python实现与去噪应用

小波包与冗余小波包:从原理到Python实现与去噪应用 简介本资源是一份面向信号处理与图像分析初学者的冗余小波包变换实践资料包聚焦于时频局部化特征提取这一核心需求适用于故障诊断、图像去噪与多尺度特征学习等实际场景。压缩包共5个文件包含3个MATLAB脚本.m用于实现Haar小波的二维分解与重构、1幅JPG格式的分解结果可视化图、1张BMP原始测试图像lena.bmp整体仅110KB轻量易用且结构清晰。已有181人下载学习适合希望快速理解小波包分解原理、观察不同频带能量分布、对比冗余性对时频分辨率影响的工程实践者。资源提供完整可运行代码链路——从原始图像读入、小波包逐层分解、子带系数提取到结果图像生成辅以直观视觉反馈便于调试验证与教学演示。 做了这么多年信号处理我越来越觉得时频分析就像给信号拍X光片傅里叶变换能告诉你“有没有病”但对不准“病灶在哪一段”短时傅里叶变换把时间轴勉强加上去了可窗口一固定低频高频的分辨率永远在打架。直到小波这套工具出现才算找到一把既能调焦又能变频的“显微镜”。这篇要聊的就是围绕一个名为 xiaobobao.zip 的工程代码包展开的主题从小波变换、小波包到冗余小波包再到它们在去噪、图像增强、时频掩码里的实际用法。整篇内容适合刚入门时频分析的研究生、做故障诊断或语音处理的工程师以及任何想搞清楚“小波包到底比小波强在哪”的人。1. 先从时频分析的痛点说起1.1 频谱和时间为什么不能两全学过信号处理的人都知道傅里叶变换它把一个时域信号拆成正弦波的叠加输出一个横轴是频率的频谱。这个过程非常优美但它有个致命缺陷变换结果里完全没有时间信息。换句话说你拿到一个频谱能知道信号包含哪些频率分量却不知道这些分量是同时存在、还是先后出现的。举个我经常用来解释的例子一段语音里先说了“啊”再说了“衣”傅里叶变换把整段一起处理得到的是两个音的频率叠加但无法告诉你“啊”在前面还是“衣”在前面。对于平稳信号这没关系可现实世界的信号——地震波、心电、机械振动、语音——基本都是非平稳的频率成分时刻在变。短时傅里叶变换STFT部分解决了这个问题思路很简单给信号加一个固定长度的窗一段一段地做傅里叶变换把结果拼成一个时间-频率二维平面。但窗口长度一固定分辨率矛盾就来了窗口长频率分辨率高时间分辨率差窗口短时间定位准了低频又分不开。这就是经典的海森堡测不准原理在信号处理里的体现——时间分辨率和频率分辨率的乘积存在下限你不可能两头都占着。1.2 小波变换登场可变窗口如何解决分辨率矛盾小波变换的核心思想就是不再用固定窗口而是用“可伸缩”的波形去匹配信号。低频成分用拉长的小波去匹配时间窗自然变宽频率分辨率高高频成分用压缩的小波去匹配时间窗变窄时间定位准。这种自动调节正好符合人们对信号分析的需求低频往往变化慢、需要看清频率细节高频往往持续时间短、需要找准发生时刻。连续小波变换CWT的表达式是[ C(a,b) \frac{1}{\sqrt{a}} \int x(t) \psi^*\left(\frac{t-b}{a}\right) dt ]其中尺度 a 对应频率的倒数平移量 b 对应时间位置。实际用的时候没人会计算连续的 a 和 b而是用离散二进小波变换DWT通过一组低通滤波器和高通滤波器把信号逐级分解。这就是 Mallat 算法每一层先滤波再隔点采样产生低频近似系数 cA 和高频细节系数 cD下一层只对 cA 继续分解。小波变换因此被称为“数学显微镜”粗尺度看全局细尺度看局部。但用着用着你就会发现一个尴尬的事实——显微镜的倍数没法随意调因为 DWT 每一层只分解低频高频部分一旦被分离出来就再也不管了。2. 小波包变换把高频也拆开来看2.1 小波变换的短板高频一去不回头我最早用 DWT 做轴承故障诊断时踩过一个很直接的坑轴承故障激发的共振频率往往在高频段而 DWT 每层只对低频近似系数继续分解高频细节系数永远只有一个“笼统”的输出相当于把一个班的优等生单独辅导了六轮而另一批学生从头到尾只考了一次试。对语音、振动、超声这类高频信息同样重要的信号这种“偏科”是无法接受的。小波包变换Wavelet Packet Transform就是来解决这个问题的。它把小波变换那条只往低频延伸的线改成了一棵完整二叉树每一层的每个节点不论它是低频近似还是高频细节都要同时做低通和高通滤波再各自下采样。经过 N 层分解你得到的不再是一条“低频链”而是 (2^N) 个等带宽的频带横向上还有时间信息这就是一个标准的时频平面。2.2 小波包的分解逻辑与节点索引用 PyWavelets 中的 WaveletPacket 对象时会看到一套字符索引规则根节点为空字符串下一层低通子节点叫 a高通子节点叫 d再下一层就是在父节点的路径上继续拼接。比如路径 ad 表示先做一次高通分解再做一次低通分解。这种命名方式一开始容易搞混但用熟了会发现它比数字索引直观得多因为每条路径就是一个滤波组合顺序。有个容易犯晕的点是频带顺序。小波包的子带频率不按自然顺序从头排到尾而是存在“之字形”重排。自然顺序下节点列表 [aaa, daa, ada, dda, aad, dad, add, ddd] 对应的中心频率并不是线性递增的需要用频率重排函数处理。如果你直接按节点顺序画时频图带宽排列会乱这是我见过最多人踩的坑之一。2.3 选基问题Shannon熵到底在干嘛小波包分解有个“幸福的烦恼”一棵深度为 J 的满二叉树里可供选择的子空间组合有天文数字那么多。不同组合对应不同的时频划分方式有的适合压缩有的适合去噪有的适合特征提取。怎么选业界常用的是 Coifman 和 Wickerhauser 提出的“最佳基”方法核心就是给每个节点定义一个代价函数比较父节点的代价与两个子节点代价之和决定保留哪些节点、剪掉哪些节点。最常用的代价函数是 Shannon 熵定义是每个归一化系数的能量对数加权之和。直觉上讲熵小的节点意味着系数能量越集中、越有结构比熵大的节点包含的信息更有价值。PyWavelets 内置了 best_tree 方法正是用这个思路在整棵树里搜索代价最小的组合。实际使用时把节点按熵排序、再逐层比较就能得到一个剪枝后的“精品树”既保留了主要的时频成分又大幅减少了系数数量。3. 冗余小波包牺牲冗余度换回平移不变性3.1 为什么下采样是个麻烦事小波包和小波变换一样每一层滤波后都要隔点采样。这个操作本来是为了保持总系数数量基本不变但它带来一个很隐蔽的副作用平移敏感性。你让信号整体平移几个样本点再去做分解得到的系数可能和之前完全不同因为隔点采样等于把信号重新“相位对齐”了一遍相位一变保留的点就变了。这个特性在信号去噪里尤其讨厌。我实测过一段心电信号只是整体移动了三个采样点DWT 系数的分布就明显变了导致固定阈值去噪后的波形出现奇怪的跳变。原因就是下采样破坏了平移不变性——滤波后理论上有信息量的点被丢掉了而丢哪几个点完全取决于信号的起始相位。3.2 冗余小波包的实现思路a-trous冗余小波包Redundant Wavelet Packet的方法很直接不做隔点采样整数平移下系数保持不变。为了弥补每层不采样导致的信息重叠滤波器会在每一层“膨胀”一次——也就是在滤波器系数之间插入零值配合循环卷积实现。这个算法在法语里叫 a-trous意思是“有洞的”因为滤波器像被撑开了一样。用公式表示第 j 层分解时不降采样而是把滤波器膨胀 (2^{j-1}) 倍后再卷积[ cA_{j1}[n] (x * \overline{h}^{(j)})[n], \quad cD_{j1}[n] (x * \overline{g}^{(j)})[n] ]其中 (\overline{h}^{(j)}) 表示把原始低通滤波器每隔一个位置插入 (2^j - 1) 个零。这样每一层输出序列长度和输入完全一致整棵树总系数数量变成 ((J1) \times N)比严格采样的小波包多了大约 J 倍。换来的是严格平移不变性每个采样点在任何分解层里都有对应的系数。3.3 冗余之后的代价与对策代价第一个就是内存和算力。一个 10 万点的信号做 5 层冗余小波包分解系数总量大约是 6 万 × 5 30 万个浮点数如果用完整树还会更多。在嵌入式设备或实时处理场景里这个开销有时很紧张。第二个问题是系数之间高度冗余相邻尺度、相邻节点的系数相关性很强直接拿全部系数做特征容易维度爆炸。对策通常有这么几条一是按实际需求只保留若干感兴趣频带的冗余系数而不是全都要二是先用冗余小波包做特征提取再降维比如 PCA 或自编码器三是做去噪时只在最关键的一两层用冗余分解其他层用普通小波包可以省大量计算。很多商用故障诊断系统就是这么混合着用的。4. Python从零实现小波包和冗余小波包的完整代码4.1 环境准备与库选型动手实现前先把环境搭好。Python 生态里做小波分析首选 PyWaveletspywt它同时支持 DWT、SWT、WaveletPacket 以及 CWTAPI 设计得比较统一。配合 numpy 做数值计算、matplotlib 画图基本就够了。安装命令很简单pip install pywt numpy matplotlib需要注意 pywt 的版本差异尤其是 WaveletPacket 的重构函数在不同版本里参数名略有变化。我建议用 1.4.0 以上版本api 更稳定。4.2 小波包分解与重构代码先用一个仿真信号把完整链路跑通。构造一个 1 秒钟的采样率 1000 Hz 的信号包含 50 Hz 和 120 Hz 两个稳态正弦再加一个 300 Hz 到 100 Hz 的扫频段模拟非平稳特征。import numpy as np import pywt import matplotlib.pyplot as plt fs 1000 t np.arange(0, 1, 1/fs) x (np.sin(2*np.pi*50*t) np.sin(2*np.pi*120*t) np.sin(2*np.pi*(300-200*t)*t) 0.2*np.random.randn(len(t)))接下来创建 WaveletPacket 对象分解 4 层。用 db4 小波边界模式用 symmetric。wp pywt.WaveletPacket(datax, waveletdb4, modesymmetric, maxlevel4)访问某个节点非常直接node wp[ad] # 第1层高频 - 第2层低频 coeffs node.data如果想看所有叶子节点也就是第 4 层全部 (2^416) 个子带leaves wp.get_level(4, natural) for node in leaves: print(node.path, node.data.shape)重构特定频带信号时思路是“只保留你想要的节点其他全部置零再整树重构”wp2 pywt.WaveletPacket(dataNone, waveletdb4, modesymmetric, maxlevel4) for node in leaves: wp2[node.path] np.zeros_like(node.data) wp2[ad] wp[ad].data reconstructed wp2.reconstruct(updateFalse)这里的 updateFalse 表示重构时不做系数的递归更新直接按树的结构合成。这样你就能把任意一个子带的信号单独提取出来做带通滤波效果比 FIR 还干净因为频带边界更陡。4.3 冗余小波包的自实现PyWavelets 没有现成的冗余小波包 API最接近的是 pywt.swt但它只对低频链做冗余分解不是完整的小波包树。想实现冗余小波包需要自己写滤波器膨胀逻辑。核心代码如下def atrous_filter(h, dilation): # 在滤波器系数之间插入 dilation-1 个零 n len(h) h_new np.zeros(n (n-1)*(dilation-1)) h_new[::dilation] h return h_new def rwp_decompose(x, waveletdb4, level4): w pywt.Wavelet(wavelet) h np.array(w.dec_lo) # 低通分解滤波器 g np.array(w.dec_hi) # 高通分解滤波器 N len(x) tree {: x.astype(np.float64)} for j in range(1, level1): dilation 2 ** (j-1) hj atrous_filter(h, dilation) gj atrous_filter(g, dilation) parent_paths [p for p in tree if len(p) j-1] for p in parent_paths: sig tree[p] cA np.convolve(sig, hj, modesame) cD np.convolve(sig, gj, modesame) tree[pa] cA tree[pd] cD return tree这段代码的要点是每深入一层把滤波器膨胀一倍再做 same 模式卷积。用 same 而不是 full 是为了保持长度一致代价是边界处有轻微误差工程上可以接受。做冗余分解后每个节点的长度都和原始信号相同这就是“冗余”的含义——总数据量比原信号多很多但每个样本点都保留了自己在多个尺度上的完整信息。4.4 时频图的绘制与可视化时频图是把小波包分解结果变成一张二维图横轴时间、纵轴频率、颜色表示能量强度。小波包分解天然给出一个多分辨率的时频网格绘制时把各频带系数按频率顺序排列再求能量。def plot_rwp_time_frequency(tree, fs, level4): # 获取某一层的节点列表 nodes [tree[p] for p in sorted(tree.keys()) if len(p) level] # 按频带重排这里用自然顺序实际需按频率排列 num_bands len(nodes) tf_matrix np.array([np.abs(c)**2 for c in nodes]) plt.figure(figsize(12, 5)) plt.imshow(tf_matrix, aspectauto, originlower, extent[0, len(tree[])/fs, 0, fs/2], cmapjet) plt.xlabel(Time [s]) plt.ylabel(Frequency [Hz]) plt.colorbar(labelEnergy) plt.show()画完你会看到低频段频率分辨率高、时间分辨率低高频段时间分辨率高、频率分辨率低这就是小波包时频分析的典型特征。5. 三个最值得上手的应用场景5.1 信号去噪阈值怎么定小波包去噪的基本流程分解 → 对细节系数做阈值处理 → 重构。阈值方式上硬阈值把小于阈值的系数直接置零保留大于阈值的原值软阈值把保留系数的绝对值整体减去阈值。这里有个容易被忽略的问题硬阈值会让重构波形出现突跳软阈值则会压缩幅度导致信号整体略偏小。阈值怎么选工程上最常用的是 Donoho 的 VisuShrink公式是[ \lambda \sigma \sqrt{2 \ln N} ]其中 (\sigma) 是噪声标准差通常用第一层细节系数中位数估计(\sigma \text{median}(|cD_1|) / 0.6745)。我实际对比下来如果噪声比较均匀VisuShrink 出来的结果还不错但如果噪声是色噪声建议用 SureShrink 或 GCV 这类自适应阈值效果会好一个档次。我贴一段完整的小波包去噪代码你改改参数就能用def wp_denoise(x, waveletsym8, level4, modesoft): wp pywt.WaveletPacket(datax, waveletwavelet, modesymmetric, maxlevellevel) # 估计噪声标准差 sigma np.median(np.abs(wp[d].data)) / 0.6745 threshold sigma * np.sqrt(2 * np.log(len(x))) # 遍历所有节点对非根节点做阈值 for node in wp.get_level(level, natural): data node.data if mode soft: data np.sign(data) * np.maximum(np.abs(data)-threshold, 0) else: data data * (np.abs(data) threshold) node.data data return wp.reconstruct(updateFalse)一个经验噪声较强时先用 4~6 层分解比 2~3 层效果好很多因为噪声能量被分散到更多子带每个子带的系数基数更小阈值更容易“压死”噪声。但层数也不能太多否则有效信号也被切得太碎。5.2 图像增强低层小波包也能干图像的活有人说小波分析只能处理一维信号这是误解。二维小波包对图像同样有效思路就是把图像分解成低频近似、水平细节、垂直细节、对角细节再对细节系数做非线性增益。这样既保留图像的主体轮廓低频又能增强纹理边缘高频。我用 2D 小波包做显微图像增强的经验是对高频细节系数的增益函数要设计成“小系数抑制、大系数提升”的形式def gain_shrink(coeff, knee0.1, alpha0.5): # 小于 knee 的细节系数看噪声抑制掉 # 大于 knee 的细节系数增强 norm np.abs(coeff) out np.zeros_like(coeff) mask norm knee out[mask] coeff[mask] * (norm[mask] / knee) ** (alpha - 1) return outalpha 大于 1 是增强小于 1 是压缩。做图像增强时我建议 alpha 取 0.7~1.2 之间慢慢调太大会把噪声一起放大。处理完后把系数重构回图像边缘明显比原图锐利背景噪声也没跟着起来。5.3 时频掩码语音分离里的万能工具时频掩码Time-Frequency Masking是语音增强、语音分离里的常用手段思路是把信号变换到时频域构造一个与系数同尺寸的掩码矩阵每个元素表示该时频单元的“信号占比”然后让掩码乘系数重构信号。小波包系数的优势是它天然具备多分辨率特性低频段的频率分辨率比短时傅里叶变换高所以对低频元音和谐波的区分更准确。掩码的估计方式很多最简单的有理想二值掩码IBM就是比较目标信号与干扰信号在同一个时频单元的能量大小谁大选谁def estimate_ibm(target_spec, noise_spec): return (target_spec noise_spec).astype(np.float64)实际应用中没有目标信号怎么办可以用谱减或似然比估计也可以用神经网络学习输入特征与掩码的映射关系。只需要把 DNN 的输入从小波包系数换成冗余小波包系数处理非平稳噪声时的稳健性通常会更好因为这个表示本身就对时移不敏感模型更容易学到稳定的规律。6. 实际操作中的常见问题与排查实录6.1 边界效应大到怀疑人生做小波包分解时边界处理模式选不对重构信号两端会“翘”。pywt 支持很多模式常见的有 zero、symmetric、reflect、periodization。我首推 periodization它会先把信号对称延拓再处理重构误差在边界也基本可控而且它保证分解前后长度恰好是 2 的幂倍数关系很多算法都依赖这个特性。如果你只是做特征提取边界的轻微误差不影响但要做精确重构或者信号很短边界模式的影响会被放大。想验证边界处理得好不好有一个笨办法把任意信号分解再重构如果 max 误差小于 1e-10说明模式和参数组合没问题。6.2 分解层数不是越多越好很多初学者觉得层数越多越好但实际不是这样。分解层数 J 与最低可分析频率的关系约等于[ f_{\min} \approx \frac{f_s}{2^{J1}} ]如果采样率 1000 Hz、分解 5 层最低子带宽度只有约 15.6 Hz这对很多机械信号已经够细了。但再往下分子带过窄单个频带的样本数不足统计上的稳健性变差容易出现“子带能量稀疏到全是零”的情况。经验法则是让每个最底层子带的样本数不少于 50~100 个太少了就停止分解。6.3 计算时间爆炸怎么办冗余小波包的完整树在深层数下非常耗内存。一个长度 N65536 的信号做 8 层冗余分解系数总量是 9 × 65536 ≈ 59 万个点float64 下接近 5 MB听着不大但要是批量处理几百个文件内存和计算都会吃不消。对策是“按需分解”不要从一开始就建完整二叉树而是先看每个节点的谱能量低于阈值的节点直接丢弃只保留信息量大的节点继续分解。这种贪心剪枝策略在故障诊断中很实用可以把计算量砍掉一半以上。另一个技巧是把信号分块每块 4096 点单独做小波包分解再对相邻块之间做重叠保留既省内存又避免跨块边界突变。6.4 重构波形出现振铃振铃是阈值去噪里最经典的问题表现为重构波形在突变点附近出现额外的振荡。原因一般是两个一是小波选择不当用了频域重叠较强的母小波比如 Morlet 或某些高阶 sym 系二是阈值太激进把真实信号系数和噪声一起压掉了。解决办法是把硬阈值改成软阈值、或者用半软阈值再把阈值调低 20%~30% 试一次。如果振铃只出现在信号的强跳变处可以试试分块处理先检测信号中能量突变的位置在突变点附近用较保守的阈值平稳区域用较积极的阈值。这个思路和语音增强里的“分段信噪比自适应”类似实际操作下来效果比全局阈值好很多。我测过一段带有脉冲干扰的振动信号自适应阈值方案重构出来的波形振铃幅度比全局阈值方案低了将近一半。最后再分享一个我自己的经验小波分析这套工具入门靠公式但真正用得顺不顺心全看参数调优、边界处理和应用场景的匹配。对同一段信号换了小波函数或分解层数结果可能像换了一个算法。在做任何正式分析之前先把信号分解重构一次确认误差降到了可接受范围再开始后续的阈值处理、特征提取和时频掩码能帮你省掉后面大量的排查时间。本文还有配套的精品资源点击获取
返回列表