做信号处理这几年,凡是涉及故障诊断、生理信号分析、语音识别这类项目,最绕不开的一个环节就是特征提取。传感器采集回来的原始信号本质上是电压随时间变化的波形,但模型不会直接理解波形,它需要一组能概括信号内在规律的数字。换过很多方案之后,我发现小波变换做特征提取是性价比很高的一条路:它对非平稳信号天然友好,时频局部化能力强,通常几行代码就能拿到一组结构清晰、解释性强的特征。这篇文章就把我在项目里用DWT、CWT做特征提取的经验完整梳理出来,包括什么场景该用它、小波基和分解层数怎么定、特征怎么从系数里挖出来、怎么接分类器,以及那些常规文档里不会写出来的坑。适合刚接触信号处理、或者已经在故障诊断和生理信号分析这条路上踩过坑的朋友,建议搭配代码边跑边看,一遍不够就两遍。
1. 方案设计:为什么非平稳信号要选小波变换
1.1 傅里叶和短时傅里叶的局限在哪里
设备故障诊断是理解小波价值的最佳场景。设备正常运转时信号比较平稳,一旦出现轴承剥落、齿轮断齿,波形上就会冒出一个个冲击。用快速傅里叶变换(FFT)处理这种信号,把整段序列变成频谱,可以知道“有没有某个频率成分”,但完全不知道“这个冲击发生在哪个时间点”。这不是单纯的信息丢失,而是会直接误导分类结果:假设两种完全不同的故障在同一个频带上激起了相近的能量,FFT看过去几乎没有区别,特征自然分不开。
有人会提议用短时傅里叶变换(STFT)解决时间定位问题。STFT确实是把信号切成片段,再对每个片段做FFT,得到时频矩阵。但STFT的窗口一旦固定,就陷入了分辨率矛盾:窗太窄,时间分辨高,频率分辨差;窗太宽,频率分辨上来了,时间定位又糊了。现实中的非平稳信号,低频成分往往持续很久,高频成分可能只持续几个毫秒,一个固定宽度的窗口根本不可能同时兼顾两种尺度。这个问题不是靠换窗函数就能解决的。正是这个矛盾,让小波变换成了非平稳信号分析的主要工具。
1.2 小波“多分辨率”的本质是什么
小波变换的核心特点是多分辨率分析。用最直白的方式理解:小波变换就是把原始信号和一系列“被缩放、被平移过的小波波形”做内积,得到一组系数。系数越大,说明信号在该时刻、该尺度下的局部波形和小波越像。这有点像模板匹配,不过匹配的是一族模板——同一个小波基函数,拉长一点去匹配低频成分,压扁一点去匹配高频成分。因此小波系数天然具备两个维度:尺度维(对应频率)和时间维(对应位置)。
具体到离散小波变换(DWT),它实现起来更像一组滤波器组:信号先经过低通滤波器得到近似系数,再经过高通滤波器得到细节系数,每一层都把频带对半拆分。低频的近似系数继续进入下一层,高频的细节系数直接留存。这个过程相当于给信号配了一组“焦距随频率变化”的显微镜,低频用宽窗看全局,高频用窄窗看细节。特征提取时,我不会直接使用全部系数,而是对系数做统计和能量聚合,转成一维特征向量,这样既压缩了维度,又保留了对分类最有用的信息。
1.3 哪些项目真正需要小波特征
不是所有信号都值得上小波变换。如果信号本身是稳态的,比如工频正弦、平稳高斯噪声,直接用FFT提取频谱幅值特征就够了,小波反而增加计算量和调参复杂度。小波的优势集中在非平稳、带突变的信号上,典型场景包括:
- 旋转机械振动信号的轴承、齿轮故障诊断
- 脑电、肌电、心电等生理信号的节律分析和事件检测
- 电力系统的瞬态扰动、谐波和电压暂降分析
- 语音端点检测、说话人识别、地震信号分类
- 图像纹理分析、指纹掌纹识别、医学图像分割
判断标准很简单:如果信号里存在瞬态冲击、扫频变化或者事件起始点,FFT太“整体”,STFT太“呆板”,这时候小波就是最合适的选择。如果信号本身就是一段平稳周期波,就别折腾了,跑个FFT出幅值谱,又快又稳。
2. 参数定得好,特征就成功一半
2.1 小波基按信号形态选:各家族画像
小波基的选择没有绝对最优,但绝不是随便挑一个就行。我做过不少对比实验,一个直观的经验是:小波波形与目标信号的局部形态越相似,分解后的系数越稀疏,能量越集中在少数层,特征越好分。反过来,基函数选得不对,能量会摊平在各层,分类器很难捕捉到差异。
常用小波基的画像大致如下:
- Haar(db1):结构最简单,阶跃状。适合检测突变点,但光滑性差,对连续平滑信号的近似能力弱。
- Daubechies(dbN):正交、紧支撑,db4、db6在机械故障诊断中非常常见,对冲击型信号比较友好。
- Symlets(symN):近似对称,能轻微克服相位失真,适合脑电、心电这类生物信号。
- Coiflets(coifN):对称性更好,消失矩更高,做图像纹理特征时效果不错。
- Morlet:复值小波,不满足正交性,主要用在连续小波变换里生成时频图,对振荡型信号特别敏感。
实操上不要拍脑袋选一种就冲到底。我会在一个小数据集上做快速实验:分别用db4、db6、sym8、coif4做相同的分解,提取同一组特征,然后比较分类准确率或者直接看每层能量分布。通常这一步能在半天时间内帮我把备选方案排掉一大半。重要的是,这个选择过程要记录到项目文档里,因为调基函数这件事非常容易过几个月就忘。
2.2 分解层数和采样率的倒推计算法
分解层数决定了特征提取的频带分辨率。DWT每一层都把当前频带对半拆分,细节系数对应高频半区,近似系数继续进入下一层低频半区。假设采样率是Fs,奈奎斯特频率是Fs/2,则第一层细节D1对应[Fs/4, Fs/2),第二层细节D2对应[Fs/8, Fs/4),依次类推;最后一层近似系数A_N对应[0, Fs/(2^(N+1)))区间。
我一般用“倒推法”定层数:先确认关心的故障频率或生理节律落在哪个频带,再反推需要分解到第几层。举个例子,某振动信号采样率1000Hz,轴承故障特征频率大约30Hz,我只需要分解到5层,因为D5频带是[31.25, 62.5),覆盖了30Hz附近区域。继续往深层分解,反而会把能量摊薄,特征维度变多,却对分类没有实质帮助。如果是脑电alpha节律(8到13Hz),采样率256Hz时,奈奎斯特频率是128Hz,D3频带是[16, 32),D4频带是[8, 16),取第4层细节就能覆盖alpha频段,分解到5层足够。
还有一个容易被忽略的点:DWT只对低频部分继续分解。如果你关心的特征落在中高频的某个窄带里,比如语音共振峰,用DWT一层层拆是拆不到目标频带的。这时候应该改用小波包分解(WPD),它对低频和高频都同时拆分,能输出等宽的子带。我之前在一个语音特征项目里,一开始用DWT,共振峰特征怎么都不稳定,换成小波包之后分类准确率直接提升了4到5个点。这个经验值得记住。
2.3 信号长度、边界延拓与平移敏感度
分解层数每增加1,近似系数点数大约减半。我做特征提取时,习惯先用len(signal) >= 2^level这个条件做兜底,否则最后一层的近似系数只剩几个点,统计量非常不可靠。比如信号只有128个点,强行分解到7层,最后一层可能只剩1个系数,均值、标准差都没有意义。
边界效应是另一个大坑。小波滤波器的卷积过程需要借助信号边缘外的数值,补法有零填充、对称延拓、周期延拓、常值延拓等。PyWavelets默认用对称延拓,多数工程场景下表现不错,但振动冲击这类信号如果起始和结束不在同一水平,边界处的几个系数会被扭曲。我的做法是:截取数据片段时多留一些余量,特征统计时把两端若干系数裁掉再计算。还有一个更隐蔽的问题:DWT对信号平移非常敏感,同一段波形整体右移几个采样点,各层系数相位都会变化。如果直接把整段系数展平当特征,分类器会学到跟故障无关的“位置”信息。解决方案我在第5部分细讲,这里先记住一个原则:优先用小波系数的统计量做特征,而不是直接用原始系数。
3. 特征提取的完整链路与设计细节
3.1 预处理先过三关:去均值、去趋势、滤干扰
这一步常规文档很少细讲,但恰恰决定了特征的天花板。原始信号进小波变换之前,至少需要处理三件事:去均值、去趋势、滤掉无关频段。
去均值是为了避免直流分量跑到近似系数里,让能量占比失真。很多传感器在模拟输入阶段会叠加一个直流偏置,如果不减掉,近似系数的能量会被偏置主导,细节系数反而显得无足轻重。去趋势是去掉缓慢漂移的基线,比如设备升温导致的热漂移、心电信号中的呼吸基线漂移。这些低频漂移如果不处理,会污染最深层近似系数的能量。滤掉无关频段,比如50Hz工频干扰,是为了防止它占据高频细节的能量占比。这三个动作都很简单,但少了一个,后面的特征都会不稳定。
信号分段也是预处理的一部分。如果是滚动分类,比如每隔2秒分析一次振动,要注意段与段之间的重叠率。我一般用50%重叠分段,既保证特征平滑,又不会制造太多重复样本导致验证结果虚高。
3.2 三层特征体系:能量、统计量、熵
拿到coeffs = pywt.wavedec(signal, wavelet, level=N)之后,先别急着写特征代码。我建议先看一眼每层系数的形状,确认分解层数没崩,再开始特征工程。特征设计的基本盘可以总结成三大类。
第一类是能量特征。各层系数的平方和占总能量的比例,物理含义非常直观。设备出现故障时,原本集中在某个频带的能量会发生迁移。比如正常状态下主要能量在A5,轴承故障后D3明显变高,这个迁移趋势就是分类器的核心线索。能量占比还有一个优点:天然归一化到0到1之间,跨样本可比性强。
第二类是系数统计特征。对每一层系数计算均值、标准差、绝对均值、峰值、峭度、偏度。这相当于把系数序列压缩成若干标量。需要特别强调峭度,它对冲击信号极其敏感,早期轴承故障的微弱冲击往往先表现在峭度升高上,所以峭度几乎是机械故障诊断的必选特征。
第三类是信息熵特征。小波能量熵刻画信号在尺度分布上的“混乱程度”。熵的原理很简洁:如果能量全部集中在一个系数层,熵值低;如果均匀分散在各层,熵值高。故障程度越重,能量分布通常越分散,熵值就越大。我在很多项目里发现,熵特征的区分度往往比单一能量特征更好,尤其是同类故障在不同严重程度下的分级场景。
3.3 特征向量拼接和标准化的正确姿势
拼特征向量时,排列顺序会影响别人阅读代码的体验,但对模型本身影响不大。我习惯的排列是:近似层系数统计量放前面,然后是各层细节的统计量,最后放能量占比和熵类特征。这样当特征数量变化时,前面部分的索引相对稳定,调试起来方便。
标准化是必须做的一步。小波能量占比天然在0到1之间,但系数标准差可能跨几个数量级,比如振动信号的细节系数标准差可能是0.1,而近似系数标准差是500。如果不处理直接喂给SVM,小数特征会被大数特征完全淹没。标准化要在训练集上估计均值和方差,绝不能把验证集或测试集的数据也一起算进去。这一点属于数据泄漏范畴,第5部分我会专门展开。
3.4 什么时候改用CWT时频图加CNN
如果不想手工拼特征,或者信号模式太复杂、统计量表达不清,可以考虑连续小波变换(CWT)直接生成时频图,再交给卷积神经网络做端到端分类。这在图像模型非常成熟的今天是一条很自然的路。把一维振动信号通过CWT转成二维时频图,喂给一个小型CNN分类轴承故障,效果通常不输手工特征方案。
但这需要付出代价:CWT计算量远大于DWT,时频图的尺寸、颜色映射、尺度范围都会影响CNN效果,可解释性也随之变差。我的习惯是:需求量小、硬件成本敏感、需要向客户解释特征含义时,优先用手工特征加DWT;如果数据量大、计算资源充足、追求端到端省事,再上CWT加CNN。不要一上来就两头都要,兼顾成本太高。
4. 实操:代码从分解到分类
4.1 环境依赖和PyWavelets的版本坑
Python环境下做小波特征提取,绕不开PyWavelets。安装命令是pip install PyWavelets,导入名是pywt。配合numpy做统计计算,scipy做信号预处理,再加一个sklearn接分类器。需要留意的是,PyWavelets的不同版本API有些差异,老版本pywt.cwt不支持sampling_period参数,新版本才支持,安装时尽量锁定稳定版本。如果不小心环境里有多个Python环境,建议用虚拟环境隔离,避免版本冲突导致代码跑着跑着报参数错误。
4.2 一维振动信号特征提取核心代码
下面这段代码是我平时做轴承振动特征时改改就能用的版本。核心函数完成DWT分解之后,从每层系数里提取能量占比、标准差、绝对均值三个视角的特征。
import numpy as np import pywt def wavelet_feature_extract(signal, wavelet='db4', level=5): coeffs = pywt.wavedec(signal, wavelet, level=level) features = [] names = [] energies = [np.sum(np.square(c)) for c in coeffs] total_energy = sum(energies) if total_energy == 0: total_energy = 1e-12 for i, c in enumerate(coeffs): features.append(energies[i] / total_energy) names.append(f'energy_ratio_{i}') for i, c in enumerate(coeffs): features.append(np.std(c)) names.append(f'std_{i}') for i, c in enumerate(coeffs): features.append(np.mean(np.abs(c))) names.append(f'mad_{i}') return np.array(features), names调用时直接传入信号片段:
# 假设signal是采样率1000Hz的振动信号,截取1024个点 signal = your_loaded_signal[:1024] feat, feat_names = wavelet_feature_extract(signal, 'db4', 5) print(feat.shape) # (18,)为什么特征数是18?因为5层分解共产生6个系数数组(A5加D5到D1),每个数组对应3个特征:能量占比、标准差、绝对均值,所以是6乘3等于18个特征。如果是4层分解,就是5乘3等于15个。想清楚这个逻辑,后续扩展特征时不会懵。我在项目中有时还会额外加入峭度特征,代码逻辑完全一样,只是往features列表里继续append而已。
4.3 二维图像小波特征提取代码
二维场景下,函数换成pywt.wavedec2,分解结果是近似系数加三个方向的细节系数:水平方向cH、垂直方向cV、对角方向cD。图像纹理识别、医学图像分割里会经常用到:
def wavelet_image_feature(image, wavelet='db2', level=2): coeffs = pywt.wavedec2(image, wavelet, level=level) cA = coeffs[0] features = [np.mean(cA), np.std(cA)] total_energy = np.sum(image ** 2) for idx, detail in enumerate(coeffs[1:]): cH, cV, cD = detail features.extend([ np.sum(cH ** 2) / total_energy, np.mean(np.abs(cV)), np.sum(cD ** 2) / total_energy ]) return np.array(features)每个细节层的水平、垂直、对角分量反映了图像在不同方向上的高频能量分布。指纹纹理图像里,脊线方向的突变会在对角细节系数上留下痕迹,这种几何定向特征用普通像素统计是提取不出来的,小波的优势在这里体现得很明显。
4.4 把特征集接上SVM分类器
特征提取只是前半程,接分类器才是完整链路。用sklearn跑SVM分类很短小:
from sklearn.svm import SVC from sklearn.model_selection import cross_val_score from sklearn.preprocessing import StandardScaler # X是特征矩阵,每行一个样本;y是标签 scaler = StandardScaler() X_scaled = scaler.fit_transform(X) clf = SVC(kernel='rbf', C=10, gamma='scale') scores = cross_val_score(clf, X_scaled, y, cv=5, scoring='accuracy') print('交叉验证准确率:', scores.mean())标准化顺序这里再强调一次:必须先对训练数据做fit_transform,之后用同一个scaler对验证集和测试集做transform。如果直接对全量数据先归一化再做划分,测试集的信息会偷偷泄漏到训练过程中,得到的准确率是虚高的。这个错误在公开数据集的社区源码里经常看到,但工程上代价非常大。
4.5 生成CWT时频图给CNN
需要给CNN喂时频图时,用下面的方式生成:
import matplotlib.pyplot as plt scales = np.arange(1, 128) coeffs_mat, freqs = pywt.cwt(signal, scales, 'morl', sampling_period=1/1000) plt.imshow(np.abs(coeffs_mat), aspect='auto', cmap='jet') plt.savefig('cwt_image.png', dpi=150, bbox_inches='tight') plt.close()scales的范围和间距决定了时频图的频带分辨率和计算量。从1到128基本覆盖了中低频到高频;如果只关心低频段,可以缩到np.arange(1, 32),图会更聚焦,训练也更快。保存图片时建议统一尺寸,因为后续CNN输入层通常要求固定分辨率,尺寸不一致就要重新resize,浪费算力。
5. 高频踩坑与排查完全记录
5.1 小波基不对,能量摊平怎么排查
我在一个项目里做某类振动信号分类,一开始选了Haar小波,分类准确率只有七十出头,换成db6之后直接跳到85%。原因不难理解:Haar小波本身是阶跃状,对连续光滑的振动信号近似能力弱,分解后的系数不够稀疏,能量分散在多层。排查方法也很直观:打印各层能量占比,如果能量几乎被摊平在各层,多半是小波基跟信号形态不匹配。这时候不要急着加特征,先换小波基做一次消融实验,看能量分布是否开始集中。
5.2 分解层数贪多引发的连锁故障
层数设太深,最深层近似系数只剩几个点,统计特征完全不可靠;高频细节能量被拆得稀碎,特征向量暴涨。我见过有人把512点信号分解到8层,特征维度堆到200多,最后还得靠PCA硬压回去,纯属绕远路。记住频带倒推公式,分解只要覆盖到目标频带就行,不必贪多。如果真的需要高频率分辨率,又不想牺牲时间分辨率,就换小波包分解,而不是无限加深DWT层数。
5.3 CWT慢到跑不动怎么办
PyWavelets的CWT实现是纯Python循环,数据量大了以后非常慢。我拿几十万点的长信号直接跑CWT,等了十几分钟还没结束。解决办法有两个方向:一是先降采样,把采样率降到刚好覆盖目标频带,再跑CWT;二是改用FFT加速的CWT实现,或者把长信号分段处理,每段单独生成时频图。总的来看,CWT适合短片段分析,不适合直接铺在长时间序列上,这一点要在项目设计阶段就想清楚。
5.4 数据泄漏:两个最隐蔽的来源
归一化泄漏是最常见的一种。先对全量数据做标准化,再划分训练集和测试集,会让测试集的信息通过均值方差进入训练过程,交叉验证分数往往高得离谱。第二个隐蔽来源是分段重叠率过高。如果相邻样本大量重叠,训练集和验证集中会出现几乎相同的样本,模型相当于已经见过了验证集的一部分数据。我一般用50%重叠做特征平滑,但最终评估模型效果时,会单独用一段完全不重叠的测试集重新算一次,数字才敢写进报告。
5.5 先想清楚物理意义再堆特征
特征工程做到最后容易陷入一个误区:能加的特征全部加上,靠模型自己挑。这是一个性价比极低的路线。我在一个项目里把特征加到80多维,分类效果不仅没变好,训练时间还翻了三倍。后来把特征按物理来源分组,每组用随机森林的特征重要度排了一遍,删掉一半,准确率反而稳住了。小波特征最大的优势之一是每一个特征都有明确物理含义,能量占比就是能量迁移,峭度就是冲击强度。利用这些含义去筛选特征,比盲目堆维度高效得多。
最后再分享一个小经验
做小波特征提取这些年,我最大的体会是:小波变换本身并不难,难的是每一步都要想清楚“为什么”。为什么选这个基函数、为什么拆这么多层、为什么提取的是能量而不是别的统计量,这些决策背后其实是信号本身的物理特性。把这些问题想透了,小波方法就是一台特别好用的显微镜;想不透,它就只是变成了又一个调参黑盒。如果你读完准备动手复现,建议先拿一段自己手头的真实信号跑一遍,用18维特征做一次简单分类,然后回头看每层能量分布,再去调整基函数和层数。这个过程走一遍之后,你对小波变换的理解会比看十篇教程都管用。