资讯动态

脉搏识别实战:小波分解、特征提取与分类器选型全解析

发布时间:2026/10/13 8:31:03 来源:尧图企业网站定制
简介由同济大学完成的《基于小波分析的脉搏信号处理与识别》项目面向生物医学工程、信号处理方向的初学者与研究者围绕非平稳脉搏信号的分析需求演示了从去噪预处理、小波多尺度分解、特征参数提取到分类识别的一整套流程。压缩包共八个文件大小约三十九KB里面含四个脚本文件、两个Word文档、一个说明文档和一个Excel数据表脚本对应巴特沃斯低通、带通、高通滤波与主分析函数Word文档说明采样频率和参数提取要求Excel表格存放脉搏样本数据。当前已有二百三十七人浏览学习。通过阅读和运行这些代码可以直观掌握小波分析时频局部化特性在脉搏信号处理中的具体应用并借鉴其从去噪、多尺度分解到特征提取和分类建模的完整思路帮助快速搭建脉搏识别实验框架是课程设计或科研入门的紧凑参考。1. 脉搏识别到底在做什么小波拆信号、特征喂分类器把“脉搏识别”拆开看问题其实很朴素给一段几十秒的压力波形怎么让算法说出采样对象当前处于什么状态。小波负责把信号里的基线漂移和工频干扰剥掉再把波形拆成不同频段的能量分量分类器拿到这些特征后做决策。这个链路看起来短实际坑都在细节里——采样率不一致、标签错位、类别不均衡每一个都能让模型的准确率从 95% 掉到 60%。这个方向适合三类人做课程设计或毕业设计的学生手头刚好有一包采集好的脉搏数据做可穿戴健康监测的工程师想把实时脉搏分类塞进 MCU以及刚接触生理信号处理、想弄清小波和分类器怎么配合的入门者。读完后你能得到一套完整可复现的预处理、特征提取、分类评估流程以及我在实际项目中踩过的几条坑。2. 预处理小波分解怎么把基线漂移和工频干扰摘出来2.1 为什么是 db4 而不是 db1小波基的选择逻辑脉搏信号最脏的地方不在高频毛刺而在低频基线漂移。呼吸引起的胸腔压力变化、手部微小晃动都会让整段波形上下浮动浮动的频率通常在 0.2~0.5Hz 之间。如果用传统带通滤波器比如 Butterworth你得先想清楚阶数和截止频率而且滤波后存在相位偏移波形形态会被拉偏脉搏主峰位置也跟着挪动。小波分解的天然优势是可以把信号拆成不同频段的系数丢掉指定频段后直接重构重构过程是零相位的主峰位置不会被移动。小波基的选择是第一步玄学。db1 就是 Haar 小波它的波形是阶跃状的用来分析光滑的脉搏波会额外产生大量不需要的高频细节系数效果很差。db4 有 4 阶消失矩波形比较平滑形态上与脉搏主波接近分解出来的系数更集中是生理信号处理里最常用的起点。也有人用 sym5对称性更好一些但计算量略大在实时场景里意义不大。我一般会先用 db4 跑通全流程后面有空再对比 sym5 的差异不建议一上来就在小波基上纠结太久。2.2 用 Python 跑通六层小波分解的预处理脚本下面这段代码是脉搏预处理的标准骨架基于 PyWavelets 实现。它的思路是用 6 层小波分解把信号拆成 1 个低频概貌和 6 个高频细节把低频概貌对应基线漂移置零把最高两层细节对应工频和肌电也置零再用剩下的系数重构出干净波形。import numpy as np import pywt def preprocess_pulse(raw_sig, fs250): # raw_sig: 一段脉搏原始波形1 维数组 # 1) 6 层小波分解返回 [cA6, cD6, ..., cD1] coeffs pywt.wavedec(raw_sig, db4, level6) cA6, cD6, cD5, cD4, cD3, cD2, cD1 coeffs # 2) cA6 代表 0~约3.9Hz 的基线趋势直接置零 cA6 np.zeros_like(cA6) # 3) cD1、cD2 对应 31.25~125Hz50Hz 工频落在 cD2 里 cD1 np.zeros_like(cD1) cD2 np.zeros_like(cD2) # 4) 用剩余系数重构干净波形 clean pywt.waverec([cA6, cD6, cD5, cD4, cD3, cD2, cD1], db4) # 5) 边界延拓带来的误差掐头去尾 cut 6 * 8 # level * 滤波器长度估算边界效应范围 if len(clean) 2 * cut: clean clean[cut:-cut] return clean参数选择是这套流程里最值得说的地方。level6 在 250Hz 采样率下最低频段 cA6 大约对应 0~3.9Hz呼吸和体动造成的基线漂移正好落在这段所以把它置零可以实现零相位去漂移。cD1 对应 62.5~125HzcD2 对应 31.25~62.5Hz50Hz 工频干扰落在 cD2 里所以 cD2 必须置零cD1 是高频肌电和传感器噪声也一起去掉。cD3~cD6 保留了 3.9~31.25Hz 的主波、重搏波前切迹和大部分有用形态信息全部保留。边界效应是新手最容易忽略的点。pywt.wavedec 默认用 symmetric 延拓重构后波形两端误差明显偏大有时会在开头多出一两个假峰对后续峰值检测极具欺骗性。上面代码里 cut 6 * 8 是经验值level 乘以滤波器长度近似估计边界影响范围直接在预处理出口把两端切掉。如果你后面还要做滑动窗口切片这一步不做每个窗口开头都会带污染。2.3 到底分解多少层以频段核对为准分解层数不是固定的核心判断标准是每层对应的频段是否和你要去除或保留的频率对齐。250Hz 采样率下 6 层比较合适因为 cA6 的截止频率约 3.9Hz和基线漂移频段匹配。如果原始信号采样率是 500Hz同样的 6 层分解cA6 对应的截止频率就变成约 7.8Hz此时基部漂移一般低于 1Hz和有用波形段混在一起直接置零会伤及主波形态。常见的做法是先统一重采样到 250Hz再进小波分解这样调参只需要做一次。重采样建议用 scipy.signal.resample_poly它对波形形态的保持比线性插值好。还有一类翻车是过度置零把 cD3 也顺手清零cD3 在 250Hz 采样率下对应约 15.6~31.25Hz这个频段里包含部分脉搏波上升沿的陡峭成分清掉后波形会变钝主峰幅度下降后续特征提取的幅度特征全部失真。如果确实看到 cD3 有明显噪声尖刺先确认是不是工频谐波不要凭感觉一刀切。3. 特征提取把波形从“看得见”变成“算得清”3.1 小波包能量占比最稳的一类特征普通小波分解只对低频概貌继续递归分解高频细节不再细分。脉搏信号中有诊断价值的形态比如重搏波、切迹、主波宽度恰恰分布在中高频细节里只用普通分解会丢信息。小波包分解WPT对每个节点都做二分能够把高频细节继续铺开再统计各子带的能量占比这是脉搏识别里最常见的特征组合方式。def extract_wpt_features(clean_sig, waveletdb4, maxlevel4): # maxlevel4 得到 2^416 个子带节点 wp pywt.WaveletPacket(clean_sig, wavelet, maxlevelmaxlevel) nodes [node.path for node in wp.get_level(maxlevel)] energies [] for path in nodes: data wp[path].data energies.append(np.sqrt(np.sum(data ** 2))) energies np.array(energies) total np.sum(energies) 1e-12 return energies / total每个节点的 data 是该子带的重构系数用 sqrt(sum(x^2)) 计算能量这里不用平方和是为了降低对信号幅值波动的敏感度。最后除以总和归一化得到 16 维的能量占比特征无论原始波形幅值是被放大了还是缩小了这个特征基本不变。在 300~600 个样本的小数据集上这 16 维特征配合分类器能稳定做出可用的分类结果。我早期试过直接把原始波形喂 CNN发现样本量不够时模型根本学不到稳定模式反而小波包能量占比这种粗粒度特征更鲁棒。如果你觉得 16 维太粗可以把 maxlevel 提到 5得到 32 维特征但要注意特征维度上升会让小样本场景下的分类器更容易过拟合并不总是越多越好。3.2 时域补充脉率、脉率变异性和波型参数频域特征描述的是“波形里各频段有多少能量”但分类任务往往还需要知道“这一拍和下一拍隔了多久、稳不稳定”。这时就需要从预处理后的波形里检测主波峰再计算时域特征。from scipy.signal import find_peaks def extract_time_features(clean_sig, fs250): peaks, _ find_peaks( clean_sig, distanceint(0.4 * fs), prominencenp.std(clean_sig) ) if len(peaks) 2: return None intervals np.diff(peaks) / fs # 单位秒 pulse_rate 60.0 / intervals # 每分钟脉率 return { pulse_rate_mean: np.mean(pulse_rate), pulse_rate_std: np.std(pulse_rate), sdnn: np.std(intervals * 1000), # 主峰间期标准差单位 ms peak_amp_mean: np.mean(clean_sig[peaks]), signal_power: np.mean(clean_sig ** 2), }distance 参数取 0.4 * fs即 200ms对应 300 次/分钟的脉率上限。这是为了防止把重搏波峰误判为主波峰重搏波通常出现在主波之后 200~350ms 的位置距离比这个阈值更远。prominence 用整段波形的标准差做门槛低于该幅度的毛刺峰不会被识别对低幅值噪声有天然过滤作用。时域特征拼接时要注意单位统一。sdnn 用毫秒脉率用次/分钟幅度特征直接用原始单位这些量级差异很大后面必须交给标准化器统一缩放。另外如果一段信号里 find_peaks 找到的主峰少于 2 个这段数据应该直接标记为不可用而不是硬补一个默认值测量硬件接触不良时经常出现这种情况宁缺毋滥。3.3 特征归一化与数据集组织先于分类器的一步特征矩阵的组织方式直接影响训练结果。每一行是一个样本每一列是一个特征所有样本的特征向量拼接成 X标签对齐到 y。常见翻车现场是把所有数据标准化后再划分训练集和测试集这样测试集的统计信息均值和方差已经混进训练过程属于典型的信息泄漏测试指标会虚高。正确做法是先划分数据集再只用训练集的统计量去 fit StandardScaler然后把同样的变换分别应用到训练集和测试集。小波包能量占比本身已经归一化理论上不需要再缩放但拼接上时域特征后整体还是需要统一标准化否则 SVM 的 RBF 核会被量级大的特征主导。我一般会把特征提取封装成特征矩阵构建函数输入是预处理后的信号列表和对应标签列表输出是 X 和 y这样后续切换分类器、调整验证策略都不需要改数据组织代码。别忘了把采样率也作为元数据存下来不同采样率的信号混在一起是脉搏数据预处理里最常见的低级事故。4. 分类器选型与训练从 SVM 到轻量 CNN 的进阶路径4.1 为什么小样本场景先试 SVM 和随机森林脉搏识别项目的数据量通常不大几百段到几千段信号这种规模下深度模型很容易过拟合。SVM 的 RBF 核适合处理非线性边界对高维小样本数据尤其友好随机森林则能天然输出特征重要性方便验证“小波包能量占比是否是有效特征”这个前提。有人会想用循环网络处理时序比如小波分解结果拼接 Elman 网络这在序列建模上更自然但参数多、收敛慢数据量不够时表现反而不如表格型特征加简单分类器。我在实际项目里的基准路径是先跑随机森林拿到特征重要性和一个参考准确率再换 SVM 调 C 和 gamma最后才考虑要不要上 CNN。如果特征向量本身已经有 16 维小波包能量 几个时域特征SVM 和随机森林通常能到不错的效果这时上深度模型是低性价比的。等数据量超过几千段再考虑用原始波形或时频图喂 CNN收获才明显。4.2 一套可直接跑的分类评估模板下面这个模板覆盖了从划分数据到输出混淆矩阵的完整流程所有参数都有明确的调整方向。from sklearn.model_selection import train_test_split from sklearn.svm import SVC from sklearn.ensemble import RandomForestClassifier from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline from sklearn.metrics import classification_report, confusion_matrix # X: n_samples × n_featuresy: 类别标签 X np.array(feature_matrix) y np.array(labels) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) model make_pipeline( StandardScaler(), SVC(kernelrbf, C10, gammascale) ) model.fit(X_train, y_train) y_pred model.predict(X_test) print(classification_report(y_test, y_pred)) print(confusion_matrix(y_test, y_pred))stratifyy 让训练集和测试集的类别比例与原数据一致避免某类样本在测试集里意外缺失。C10 是 SVM 对小样本的一个常用起点C 越大越容易过拟合如果训练集准确率远高于测试集把 C 降到 1 或 0.1 再试。gammascale 会根据特征数量自动调整 RBF 核宽度比手工指定 gamma 更稳妥固定 random_state 保证每次实验可复现。分类评估时不要只看准确率分类报告里的 macro F1 在类别不均衡时才是最诚实的指标。如果想把 XGBoost 加进来对比也完全没问题表格型特征下它是强基准。但小样本场景要注意控制树的深度和叶子节点数量默认参数很容易让树长过头训练集上全对、测试集上崩盘。4.3 数据量够大时轻量 CNN 怎么做当样本量上千段甚至上万段后原始波形直接进 1D-CNN 会更合适避免手工特征工程可能丢掉的信息。最小可用结构是三到四层卷积加全局池化比如 Conv1d(32, 5) 接 ReLU 和 MaxPool(2)再接 Conv1d(64, 5) 接 ReLU最后全局平均池化接全连接层输出类别。训练时加早停和 dropoutdropout 建议从 0.3 起调。更进阶的做法是把 CWT 小波变换的结果做成时频图喂给 2D-CNN这能同时保留时间和频率信息但预处理代价高不太适合 MCU 部署。我个人的建议是先跑通 SVM 路径拿到可靠指标把数据管线验证完再投入 CNN 优化否则一旦特征提取链路有 bug深度模型只会把 bug 也学进去。5. 脉搏识别避坑5 条让准确率“缩水”的常见坑5.1 采样率不一致导致的波形“长短腿”现象同一套预处理代码换一台采集设备后分类准确率掉了 10 个点以上。原因小波分解层数对应的频段依赖采样率。250Hz 下 level6 的 cA6 对应 0~3.9Hz而 100Hz 下同样的分解层数对应频段完全不同置零的系数不再对应基线漂移波形成分被错误破坏。解决把所有信号统一重采样到 250Hz 再进入预处理管线。在代码入口处检查 fs 参数如果不一致直接用 scipy.signal.resample_poly 转换不要靠记忆去适配不同设备。5.2 标签错位信号段与标签错一个窗口现象训练时 loss 降得很快验证集表现也不错但到真实场景里预测结果完全不可用。原因滑动窗口切段时窗口尾部跨过了标签边界。一段窗口前半是状态 A、后半是状态 B标签却是 A模型学到的其实是混合状态的特征。解决要么按事件中心对齐窗口要么在切段后校验窗口内所有采样点属于同一个标签。一个简单可靠的做法是让窗口长度小于最小事件持续时间并在切段循环里加标签一致性检查不一致的窗口直接丢弃。5.3 类别不均衡多数类把少数类“吃掉”现象准确率 99%但少数类召回率只有 20%分类报告里 macro F1 惨不忍睹。原因数据里健康样本是异常样本的 10 倍SVM 学到的最优策略就是总是输出多数类。准确率这个指标在这种场景下完全被多数类主导很容易给人“模型已经挺好”的错觉。解决SVM 里设 class_weightbalanced让少数类在损失函数里获得更高权重。同时把评估指标换成 macro F1 和混淆矩阵并画出每一类的召回率。数据扩充方面可以对少数类波形做小幅平移、加噪声但要小心不要把原始样本复制太多导致过拟合。5.4 过拟合同一受试者前后两段数据“作弊”现象随机划分训练集测试集时准确率 95%换成按受试者划分后只剩 70%。原因同一个人前后的脉搏波形高度相关。随机划分时同一个受试者的前 30 秒数据可能进了训练集、后 30 秒进了测试集模型记住的是“这个人”而不是“这类状态”。解决使用 GroupKFold 或留一受试者交叉验证。分组依据是受试者 ID同一个人的所有样本必须全部待在训练集或测试集不能跨集。这类验证指标才是部署到新用户身上时真正能兑现的准确率。5.5 小波边界效应两端数据点“看起来很凶”现象每段预处理后的波形开头都会多出一两个异常高幅值的假峰峰值检测受到严重干扰。原因pywt.wavedec 默认 symmetric 延拓信号两端的重构误差会被放大切出来的窗口如果刚好贴着信号边缘波形前几个点会明显偏离真实形态。解决预处理出口统一截掉两端各 level * 滤波器长度 个采样点。这个经验值在多数场景下够用。如果边界问题仍然存在切换延拓模式为 periodization但要注意该模式下信号长度必须为 2 的整数倍可能需要先做截断或补零。6. 验证与进阶留一受试者交叉验证和滑动窗口的取舍6.1 留一受试者交叉验证评估模型是否真的“通用”随机划分训练集测试集只能证明模型在这批数据上有效证明不了模型对没见过的人有效。留一受试者交叉验证是把每个受试者轮流留出用其他人的数据训练用留出的这个人测试。这个评估代价高数据有几十个人就要训练几十次但结果才是真实部署时最接近的预期。小样本场景下训练速度足够快SVM 或随机森林几十次训练完全可接受所以这个步骤值得做。6.2 滑动窗口切数据窗口长度与步长怎么配滑动窗口的默认配置建议窗口长度取 2~5 个完整脉搏周期比如正常脉率 72 次/分钟一个周期约 0.83 秒窗口长度就取 2~4 秒。步长取窗口长度的一半这样相邻窗口有重叠可以扩充样本量。但要注意重叠窗口之间高度相关如果让重叠窗口同时出现在训练集和测试集评估指标会虚高所以测试集的窗口之间不要重叠。数据量实在不够时可以先做小幅波形平移和加随机噪声的数据扩增而不是靠无限重叠窗口硬撑样本量。6.3 进阶把模型压到 MCU 上的轻量化思路如果目标设备是 MCU建议训练时优先尝试 SVM 线性核线性核模型只有一组权值向量直接导出成 C 数组即可不需要保留核矩阵内存占有率极低。RBF 核虽然精度高但部署时每个测试样本都要算与所有支持向量的距离资源占用明显。另一种路径是训练随机森林后把树结构转成 C 代码推理时只需要一组 if-else 比较很多开源库可以直接完成转换特征向量仍然沿用小波包能量加时域特征。早期我还犯过一个错误只看准确率就判定模型合格直到有一次项目组让我按状态类别逐一汇报召回率才发现问题。自那以后我的习惯是每次训练完先看混淆矩阵和 macro F1再决定要不要继续调参。这个习惯希望帮到你也省得你拿“准”当唯一标准后面翻车再回头补课。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑