☰
脉搏信号分析避坑指南:小波变换与分类实战
2026/10/22 4:45:17 网站建设 项目流程

简介:这份由同济大学完成的脉搏识别资料包,面向生物医学工程、信号处理方向的学习者与科研人员,聚焦小波分析在脉搏信号去噪、特征提取与分类识别中的完整实现。包内共8个文件,以4个m脚本为核心,配合2个doc与1个docx说明文档、1个xlsx数据表,压缩包约39KB,体积轻巧却覆盖了从采样频率设定、滤波器设计到参数提取要求的全流程。资源围绕小波变换展开,先对原始脉搏信号做去噪与平滑预处理,再通过多尺度分解提取周期性、振幅变化等关键特征,进而借助分类模型区分正常与异常脉搏,为临床诊断与健康监测提供思路。代码与文档相互印证,读者可据此复现算法、理解小波系数与特征之间的对应关系,并在此基础上尝试替换分类器或调整参数。目前已有237人学习,适合希望把信号处理理论落到实处的入门与进阶读者。

1. 脉搏信号分析为什么总在“小波+分类”这一步翻车

很多人第一次拿到脉搏信号,第一反应是直接上深度学习,结果发现数据量小、噪声大、个体差异离谱,模型训到一半就玄学震荡。脉搏信号本质是准周期生理信号,主频集中在 0.5~5 Hz,但基线漂移、工频干扰、运动伪影全叠在一起,直接做分类基本等于让模型学噪声。小波变换之所以在这个场景里被反复提起,是因为它能把时域和频域同时切开,把“哪一段时间里出现了什么频率成分”这件事讲清楚,而脉搏的生理意义恰恰藏在这种时频结构里。这套“小波 + 分类”的路线,适合手上只有几百到几千条脉搏片段、又想做可解释特征工程的从业者,不追求端到端黑匣子,而是先把信号洗干净、把特征提对,再让分类器做它该做的事。

2. 脉搏信号预处理:先别急着做小波,把基线漂移和工频干扰处理干净

2.1 采样率、滤波频段与去基线的参数怎么定

脉搏信号的有效成分大致在 0.5~10 Hz,但真正决定波形形态的是 0.5~5 Hz 这一段。采样率常见做法是 100~500 Hz,如果原始设备只给 50 Hz,后面做 5 层小波分解时高频细节层会非常薄,分类器拿不到有用的高频信息。我一般会先确认三件事:采样率、信号时长、是否已经做过硬件滤波。

去基线漂移最稳的方式不是直接高通,而是先做中值滤波估计基线再减掉。窗口长度取 0.6~0.8 秒,对应一个脉搏周期的量级,太短会把脉搏主波削掉,太长基线跟不住。工频干扰用 IIR 陷波器,中心频率按实际工频设定,Q 值不要太高,否则会引入振铃。

import numpy as np from scipy.signal import butter, filtfilt, iirnotch, medfilt def preprocess_pulse(sig, fs): # 1) 中值滤波估计基线,窗口约 0.7 秒 win = int(0.7 * fs) if win % 2 == 0: win += 1 baseline = medfilt(sig, kernel_size=win) sig_detrend = sig - baseline # 2) 工频陷波,Q 值取 30,避免过度振铃 f0 = 50.0 # 按实际工频改 Q = 30.0 b_notch, a_notch = iirnotch(f0, Q, fs) sig_notch = filtfilt(b_notch, a_notch, sig_detrend) # 3) 带通 0.5~10 Hz,保留脉搏主频与部分高频细节 low, high = 0.5, 10.0 b_bp, a_bp = butter(4, [low/(fs/2), high/(fs/2)], btype='band') sig_bp = filtfilt(b_bp, a_bp, sig_notch) return sig_bp

这段代码里三个参数最容易被乱设:中值窗口、陷波 Q 值、带通阶数。中值窗口小于 0.5 秒会把主波当基线减掉,表现为波形变平;Q 值大于 50 时陷波器群延迟明显,脉搏上升沿会拖尾;带通阶数超过 6 阶后相位非线性加重,后面做小波时各层系数对不齐。如果预处理后波形仍然上下漂,先看中值窗口是不是被采样率算错了,而不是继续加滤波。

2.2 分段与标注:把长信号切成可分类的样本

分类器吃的是样本,不是整段长信号。常见做法是按固定窗口切分,窗口长度取 2~5 秒,覆盖 2~5 个脉搏周期,步长取窗口的 1/4 到 1/2 做重叠。重叠太少样本不够,重叠太多同一条记录泄漏到训练集和验证集,指标虚高。

标注环节要特别注意:如果标签来自设备自动标注,先人工抽检 5%~10%,看有没有把运动伪影段标成正常。我见过一个模拟项目里,自动标注把抬手动作产生的尖峰全标成“异常”,结果分类器学到的是“有没有尖峰”,而不是脉搏本身的形态差异。

def segment_signal(sig, fs, win_sec=3.0, overlap=0.5): win = int(win_sec * fs) step = int(win * (1 - overlap)) segments = [] for start in range(0, len(sig) - win + 1, step): segments.append(sig[start:start + win]) return np.array(segments)

窗口 3 秒、重叠 0.5 是脉搏分类里比较稳的起点。窗口小于 2 秒时低频成分展不开,小波分解层数受限;窗口大于 5 秒时个体内不同生理状态被混进同一个样本,标签噪声变大。切完之后建议画几张样本波形图肉眼过一遍,这一步比调分类器参数值钱。

3. 小波分解与特征提取:层数、基函数和特征向量怎么选

3.1 小波基与分解层数的选择依据

脉搏信号不是平稳信号,傅里叶变换只能告诉你“整体上有哪些频率”,小波能告诉你“第 2 秒到第 3 秒之间 2 Hz 成分强不强”。选小波基时,常用的是 db4、db6、sym4、coif3 这几类。db 系列消失矩高,适合捕捉脉搏主波的陡峭上升沿;sym 系列相位更接近线性,重构波形失真小;coif 系列在低频段更平滑,适合基线残留较多的信号。

分解层数由采样率和目标频段决定。公式是max_level = floor(log2(fs / f_min)),其中 f_min 是你想保留的最低频率。比如 fs=200 Hz、f_min=0.5 Hz,最大层数约 8 层。但实际不会全用,通常取 5~6 层,因为再往下分解,最低频那几层已经接近基线漂移,信息量低还容易引入边界效应。

小波基消失矩适用场景注意点
db44主波陡峭、噪声中等相位非线性,重构需对齐
db66高频细节丰富边界效应更明显
sym44波形重构要求高计算量略大
coif36基线残留多低频平滑但细节弱

选基函数没有绝对最优,我一般会先用 db4 跑一遍,看各层系数的能量分布是否合理,再换 sym4 对比分类指标。如果换基函数后指标波动超过 5 个百分点,说明特征本身不稳定,问题不在小波基。

3.2 从各层系数里提取可分类的统计特征

小波分解后得到近似系数 A_n 和细节系数 D_1~D_n。直接把这些系数拉平做特征维度太高,而且对时间偏移敏感。常见做法是对每一层系数算统计量:能量、均值、标准差、峰度、偏度、熵。能量反映该频段强弱,峰度和偏度反映波形尖锐程度,熵反映复杂度。

import pywt from scipy.stats import kurtosis, skew def wavelet_features(seg, wavelet='db4', level=5): coeffs = pywt.wavedec(seg, wavelet, level=level) feats = [] for c in coeffs: energy = np.sum(c ** 2) / len(c) feats.extend([ energy, np.mean(np.abs(c)), np.std(c), kurtosis(c), skew(c) ]) return np.array(feats)

这里 level=5 对应 fs=200 Hz 时最低频段约 3.1 Hz 以下,覆盖了脉搏主频。如果 fs=100 Hz,level 取 4 更合适,否则最低层系数长度太短,统计量不稳。kurtosis 和 skew 对异常波形很敏感,但前提是分段里没有明显削顶,否则这两个特征会被饱和值主导。

特征提取完建议做一次标准化,尤其是能量和标准差量纲差异大。标准化参数只能在训练集上拟合,再应用到验证集,否则就是数据泄漏。很多“离线指标很好、上线就崩”的案例,根源就在这一步。

3.3 特征降维与分类器选型:小样本下别上太复杂的模型

脉搏分类的样本量通常不大,几百到几千条。这种规模下,随机森林、SVM、XGBoost 往往比深度网络更稳。特征维度如果是 5 层 × 5 个统计量 = 25 维,再加时域特征可能到 40~60 维,直接上分类器问题不大。如果维度超过样本数的 1/10,考虑用 PCA 降到 10~20 维,或者用基于树模型的特征重要性做筛选。

from sklearn.ensemble import RandomForestClassifier from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline clf = Pipeline([ ('scaler', StandardScaler()), ('rf', RandomForestClassifier( n_estimators=300, max_depth=8, min_samples_leaf=3, random_state=42 )) ])

max_depth 控制在 6~10,min_samples_leaf 不小于 3,是为了防止树把训练集里某几个噪声样本背下来。n_estimators 300 在几百样本量下已经足够,再大边际收益很低。如果分类指标在验证集上比训练集低超过 15 个百分点,先查特征泄漏和分段重叠,而不是继续加树。

4. 避坑与排查:脉搏小波分类里最容易翻车的 5 个地方

4.1 分段重叠导致训练验证泄漏

现象:交叉验证准确率 95% 以上,换一批数据直接掉到 60%。原因:相邻分段来自同一段连续信号,重叠部分同时出现在训练和验证集,模型记住了这段信号的个体特征。解决:按记录划分训练验证集,而不是按分段随机划分。同一记录的所有分段只能出现在一个集合里。

4.2 小波边界效应污染首尾系数

现象:每次分解后首尾各几十个点系数异常大,特征分布被拉偏。原因:小波变换在边界处默认补零或对称延拓,和实际信号不连续。解决:分解前对信号做镜像延拓,或者直接裁掉每段首尾各 0.2 秒再提特征。裁掉比补延拓更省事,代价是样本略短。

4.3 工频陷波 Q 值过高引入振铃

现象:滤波后波形在 QRS 类似位置出现小振荡,分类器把振荡当特征。原因:陷波器 Q 值过高,群延迟大,能量在时域上拖尾。解决:Q 值降到 20~30,或者改用自适应滤波。判断方法是把滤波前后波形叠在一起看,如果主波后面多出明显波动,就是振铃。

4.4 特征标准化在划分数据前完成

现象:验证集指标异常好,上线后完全不可用。原因:标准化时用了全量数据的均值和方差,验证集信息泄漏到训练过程。解决:先划分训练验证集,再在训练集上 fit scaler,验证集只 transform。用 Pipeline 可以避免手动写错顺序。

4.5 标签不平衡时只看准确率

现象:准确率 90%,但少数类召回率不到 30%。原因:正常样本远多于异常样本,模型倾向于全预测正常。解决:看混淆矩阵和每类 F1,必要时用 class_weight='balanced' 或对少数类过采样。过采样只在训练集做,验证集保持原始分布。

5. 把分类结果落回信号:用混淆矩阵反查特征与参数

分类器跑出指标只是第一步,真正有价值的是拿混淆矩阵反查哪一类样本被错分,再回到信号和特征里找原因。我一般会做三件事:导出错分样本的原始波形、看它们在哪个小波层能量异常、对比正确分类样本的特征分布。

import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay def inspect_errors(clf, X_val, y_val, segments_val, fs): y_pred = clf.predict(X_val) cm = confusion_matrix(y_val, y_pred) disp = ConfusionMatrixDisplay(cm) disp.plot() plt.show() # 找出错分样本索引 wrong_idx = np.where(y_pred != y_val)[0] for i in wrong_idx[:5]: plt.figure() plt.plot(np.arange(len(segments_val[i])) / fs, segments_val[i]) plt.title(f"true={y_val[i]}, pred={y_pred[i]}") plt.show()

这段代码的关键不是画图,而是把错分样本和原始波形对齐。如果错分样本集中在某一段记录,说明个体差异没被特征覆盖;如果错分样本波形上有明显伪影,说明预处理没洗干净;如果错分样本在各小波层能量分布和正确样本重叠严重,说明特征区分度不够,需要换基函数或加时域特征。

一个具体技巧:把每层小波系数的能量占比算出来,画成堆叠条形图,按真实标签分组。如果某一类样本在 D3 层能量占比明显偏高,而分类器没学到,说明该特征被标准化压掉了,可以单独对这一维做对数变换再标准化。这个操作在模拟项目里把少数类召回率从 0.42 拉到 0.71,代价只是多了一步特征变换。

我自己的习惯是:任何一次指标提升,都要能在原始信号或特征分布上找到对应解释,找不到解释的提升大概率是泄漏或过拟合。脉搏信号分析没有那么多玄学,翻车往往翻在预处理和划分这两步。希望帮到你。

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

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

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

立即咨询