离散小波变换+WEKA:心电信号分类的特征工程实践

发布时间:2026/9/1 20:00:29
离散小波变换+WEKA:心电信号分类的特征工程实践 简介一套面向生物医学信号处理与机器学习研究者的心律失常分类完整方案基于离散小波变换DWT完成ECG特征提取并通过WEKA与MATLAB监督训练实现自动分类。资源包共539个文件约28.11MB涵盖402个png结果图、29个csv样本数据、18个mat文件、15个m脚本、6个arff数据集以及hea/info/txt等标准ECG文件其中png多为特征与结果可视化m脚本为算法实现arff可直接导入WEKA建模可完整复现从数据预处理到模型评估的流程。已有54人学习适合用于课程设计、科研实验或临床辅助诊断模型搭建。内含从ECG数据读取、DWT特征提取到WEKA分类器训练的MATLAB脚本配合可直接加载的arff文件可快速验证不同监督学习算法同时附有LaTeX论文模板便于撰写实验报告或期刊论文。 说实话现在再提“离散小波变换手工特征”这套心电分类方案有点复古的味道但我在实际项目里验证下来它依旧是性价比很高的一条路。很多人一上手就想着卷积神经网络结果数据量不够、硬件受限折腾半天还不如把特征工程做扎实。这个项目做的事情很直观用MATLAB把MIT-BIH心电信号切成单心拍做离散小波变换特征提取把特征矩阵导出成WEKA能识别的ARFF格式然后用多种监督学习分类器去识别心律失常类型。对于想搞懂信号分类全链路又不想一上来就碰深度学习的朋友这篇文章能把整个过程串起来包括每一步的选型理由和踩坑记录。1. 为什么是“小波变换WEKA”这套组合1.1 心电信号特征提取的难点在哪里心电信号和普通振动信号、语音信号不一样它是微伏到毫伏级别的生理信号很不平稳而且噪声来源多工频干扰、肌电干扰、基线漂移全部叠加在有用波形上。如果你直接用原始波形做机器学习分类器确实也有可能学到东西但泛化性往往很差因为输入维度太高而训练样本有限模型很容易把个体差异当成类别差异。所以问题的核心就变成了怎么把一个心拍表示成一组有区分度、又稳定的特征。只用时域特征比如R波峰值、QRS宽度信号一旦有噪声干扰就会大幅波动只用频域特征把整段信号做FFT得到频谱又会丢掉波形在时间轴上的位置信息。离散小波变换的优势就在于它能同时保留时间和频率两个维度的局部特征把一个心拍拆成几层“轮廓细节”正好适合心电这种形态差异集中在局部波形的信号。1.2 离散小波变换和短时傅里叶的差别短时傅里叶变换需要固定窗长窗长一旦确定时间分辨率和频率分辨率就互相牵制窗太短频率看不清窗太长时间定位又模糊。心电信号里的QRS波是又陡又窄的高频成分P波和T波是宽缓的低频成分两者形态差异很大用固定窗很难同时照顾好两种尺度。小波变换没有这个问题。它用平移和伸缩的方式生成不同尺度的母小波对信号做多分辨率分解。离散小波变换每次把信号拆成近似系数和细节系数近似系数对应低频“轮廓”细节系数对应高频“细节”。你可以把它理解成用不同粗细的筛子筛面粉第一层筛出最粗的颗粒第二层在剩下的里面再筛一遍最后每一层的颗粒大小都有记录。这样分类器拿到的不是一个高维波形而是每个尺度上的统计特征。这里需要说清楚我选的是离散小波变换不是连续小波变换。连续小波变换冗余度高计算量也大在特征提取场景下没有必要离散小波变换配合Mallat快速算法一拍的系数计算很快几千个心拍几秒钟就能跑完非常适合做批量特征工程。1.3 WEKA在监督训练链路中扮演什么角色MATLAB做信号处理和特征工程WEKA做监督训练和模型评估这是我比较推荐的分工方式。有些人会问MATLAB里面不是也有fitctree、fitcsvm这些函数吗为什么还要绕一圈用WEKA我的经验是WEKA在快速对比多组分类器、做特征选择和看混淆矩阵这件事上效率比写脚本高得多。WEKA的图形界面把所有候选算法都摆在那里J48、RandomForest、SMO、IBk、朴素贝叶斯一键运行十折交叉验证结果面板直接给出准确率、混淆矩阵、ROC面积等指标。我可以先把手头特征丢进去全部跑一遍快速判断哪些特征值得保留哪些算法值得深入调参。等模型选型确定之后再回MATLAB做更精细的优化。这种“特征工程在MATLAB模型实验在WEKA”的流程我在好几个项目里验证过效率确实高。2. MATLAB端特征提取流水线从波形到特征向量2.1 数据准备与心拍切割实验数据我选的是MIT-BIH心律失常数据库这是公开心电分类研究里最常用的基准数据集包含48条双通道动态心电记录采样率360Hz。主要使用MLII导联因为大多数心拍类型注释都基于这个导联。R峰位置和心拍类型标注可以从注释文件中读取这一步如果处理不好后面所有特征都会跑偏。我的预处理步骤是先做50Hz陷波滤除工频干扰再用一个0.5~45Hz的带通滤波器把基线漂移和高频肌电噪声压下去。这里有一个经验滤波器的截止频率不要卡得太死尤其不要对QRS波所在的十几Hz到二十几Hz频段做过度衰减否则小波分解后高频细节系数会被“磨平”特征区分度会明显下降。切分心拍时我以每个R峰位置为基准取R峰前0.25秒、R峰后0.45秒也就是采样点前90点、后162点左右总共约252个采样点作为一个心拍。这个窗口能覆盖完整的P-QRS-T形态同时尽量少带进相邻心拍。切割代码大致是这样% 读取心电信号和R峰注释这里用示意函数 [ecg, fs] rdsamp(100); ann rdann(100, atr); before round(0.25 * fs); after round(0.45 * fs); for k 1:length(ann) s ann(k) - before; e ann(k) after - 1; beat ecg(s:e); % 后续对这个beat做小波分解和特征提取 end注释文件解析最容易踩坑的地方是时间单位。MIT-BIH的注释文件中时间索引有时直接给采样点序号有时需要根据采样率换算回秒数两个方式差360倍一旦搞错心拍窗口就完全错位了。2.2 小波基与分解层数怎么定小波基的选择我建议优先试db4、sym4这类紧支撑小波。原因是QRS波的形态约等于一个不对称的窄脉冲sym4和db4的波形与它比较接近分解出来的系数不会产生过多虚假高频分量。我在实验里对比过db4、sym4和sym8最后固定用sym4稳定性好特征区分度也不错。如果你换用Haar小波虽然计算最简单但对QRS这种平滑起落的波形描述太粗糙不太适合心拍特征。分解层数要结合采样率算。MIT-BIH数据是360Hz采样4层分解后各频带大致是D190~180HzD245~90HzD322.5~45HzD411.25~22.5HzA40~11.25Hz心电信号的主要能量集中在0.5~45HzQRS波的谱峰落在10~25Hz正好对应D4和A4的低频部分P波和T波也在更低频段。所以4层分解足够把心拍的主要形态拆开。如果你用的是其他采样率的数据比如250Hz或128Hz层数一定要重新算不能照搬。判定的标准是最后一层近似系数的频带上限尽量低于QRS主能量频段但又不至于把P波信息全丢掉。[ C, L ] wavedec(beat, 4, sym4); cA4 appcoef(C, L, sym4, 4); cD4 detcoef(C, L, 4); cD3 detcoef(C, L, 3); cD2 detcoef(C, L, 2); cD1 detcoef(C, L, 1);2.3 特征构造不是特征越多越好对每个心拍做完4层DWT之后我提取三类特征第一类是能量占比。计算每一层细节系数和最终近似系数的能量占总能量的比例。这相当于给信号做了一张“频带能量分布指纹”。比如室性早搏的QRS波明显增宽高频细节能量分布就会和正常心拍不一样。第二类是系数统计量。取每层细节系数的绝对值均值、标准差和最大值以及近似系数的均值。这些特征能捕捉小波系数的幅度变化对波形形态异常比较敏感。第三类是信息熵。我用Shannon熵计算每层系数的复杂度。心律不齐的时候波形复杂度往往会增大熵值也会发生变化这类特征对房颤、房扑这类节律紊乱尤其有效。% 以D4层为例计算特征 E_ratio_d4 sum(cD4.^2) / sum(C.^2); mean_abs_d4 mean(abs(cD4)); std_d4 std(cD4); entropy_d4 wentropy(cD4, shannon);如果只算一层特征还不够我会把D1到D4以及A4的特征全部拼成一个向量再额外加入当前心拍的RR间期。RR间期对于区分室性早搏尤其重要因为这类心拍后面通常跟着一个代偿间歇。这里要特别注意RR间期是从连续R峰序列中计算出的上下文特征不能通过单个切拍信号硬算。最后每个样本的原始特征维度在16~20维左右这个规模对传统分类器来说非常友好。3. 特征与标签如何“喂”给WEKAARFF与特征选择3.1 特征归一化必须做但要选对方法从MATLAB导出的特征矩阵里能量占比是0~1量级标准差可能是几十上百RR间期可能是0.6~1.2秒量纲差异很大。如果直接用原始数值跑基于距离的分类器比如SMO、IBk量纲大的特征会主导距离计算量纲小但判别力强的特征很容易被淹掉。我的做法是在MATLAB里先做一次z-score标准化然后到WEKA里再确认一下。WEKA的Preprocess界面有Standardize和Normalize两种选项我通常用Standardize因为心电特征偶尔会有离群值标准化后比简单缩放更稳。如果你选的分类器是J48或RandomForest归一化倒不是必须的树模型只看特征分裂点不受量纲影响。但为了后续方便换分类器对比我还是建议一开始就标准化避免中途切换分类器时结果对不上。3.2 从MATLAB导出ARFF文件的两种方式ARFF是WEKA的标准输入格式结构很直白。第一行写RELATION然后声明每个ATTRIBUTE的名称和类型最后用DATA引出数据行。分类标签需要声明为离散型比如{ N, V, S, F, Q }。最简单的方式是MATLAB先把特征矩阵写成CSV文件然后用脚本生成ARFF的头部文本再拼进去。几百行数据用这种方法完全没问题几万行也不慢。我给出一个ARFF模板特征顺序要和MATLAB导出矩阵的列顺序严格对齐RELATION arrhythmia ATTRIBUTE e_d1 NUMERIC ATTRIBUTE e_d2 NUMERIC ATTRIBUTE e_d3 NUMERIC ATTRIBUTE e_d4 NUMERIC ATTRIBUTE e_a4 NUMERIC ATTRIBUTE mean_d4 NUMERIC ATTRIBUTE std_d4 NUMERIC ATTRIBUTE ent_d4 NUMERIC ATTRIBUTE rr_interval NUMERIC ATTRIBUTE class {N,V,S,F,Q} DATA 0.231,0.198,0.312,0.187,0.072,0.154,12.36,...,N如果你不想用手拼文本也可以用MATLAB的Java接口直接加载weka.jar在内存里创建Instances对象设置类属性后写入ARFF文件。这种方式更灵活适合需要自动生成多组特征文件的场景但配置Java路径有点麻烦第一次调试容易卡住。3.3 特征选择CfsSubsetEval与GreedyStepwise的组合特征维度不高不代表没有冗余。DWT不同层之间的能量占比往往存在相关性熵特征和标准差也并非完全独立。冗余特征留在数据里一方面可能降低分类器的泛化能力另一方面会让训练时间变长对RandomForest这种算法影响尤其明显。我的经验是在WEKA的Preprocess界面里加一个特征选择Filter评估器用CfsSubsetEval搜索方式用GreedyStepwise。CfsSubsetEval会评估特征子集整体与类别的相关性同时惩罚彼此高度相关的特征组合GreedyStepwise负责搜索子集空间。这个组合通常能把我的16~20维特征压缩到10~13维分类准确率几乎不掉训练时间缩短一大截。相比之下单独按信息增益给每个特征排序再取前N个容易忽略特征之间的冗余问题。两个信息增益都很高的特征可能高度重合合在一起并不会带来额外收益。所以我在做心电特征选择时优先用CfsSubsetEval而不是单纯看单特征排序。4. 监督训练中的分类器对比与评估陷阱4.1 数据划分按心拍随机划分是按患者划分结果差很大这是我在这个项目里最深刻的一个教训。刚开始实验时我用默认的十折交叉验证随机把所有心拍打乱划分测试准确率高得吓人接近97%。后来仔细一想问题出在一名患者的大量心拍同时出现在训练集和测试集里模型其实记住了患者本身的波形特点而不是学到了心律失常的分类规律。临床场景里模型面对的是新患者不是从同一个人的心电图里再抽一段心拍。所以正确的做法是按患者划分数据用一部分患者记录做训练另一部分患者记录做测试。这样评估出来的泛化性能才靠谱。在这个项目里我会把48条记录按比例分成训练组和测试组比如前38条记录用于训练后10条记录用于测试并且保证测试集里各类别都有一定数量的样本。如果某个患者的心拍数量特别多先用这一个人几千个心拍随机划分会让数据泄漏问题更严重。所以无论是处理MIT-BIH还是其他数据集只要涉及心电数据我都会在实验记录里写清楚划分方式绝不在没有按患者分组的前提下相信准确率。4.2 分类器对比我用过的一组实验结果使用相同的20维特征按患者划分训练集和测试集我在WEKA中对比过几组分类器。下表是一次典型实验的记录准确率只是参考不同特征集和划分方式下会有浮动分类器准确率平均敏感性平均特异性备注J4891.8%0.900.97输出树规则可解释性强RandomForest94.6%0.930.98综合表现最稳IBk(k5)93.2%0.920.98距离度量必须先归一化SMO(RBF核)92.4%0.910.97参数敏感需要调gammaNaiveBayes88.7%0.880.96特征相关性高时偏弱RandomForest在这个特征集上表现最好而且对参数不太敏感。如果只是想快速出一个基线我一般会先用J48跑一遍因为它的树结构能直观看到模型依赖什么特征比如RR间期是不是被反复用到了然后再切到RandomForest调高整体精度。4.3 准确率之外混淆矩阵才是重点心律失常分类不是普通的多分类任务类别分布往往极度不平衡。正常窦性心拍N可能占绝大多数室性早搏V和室上性早搏S样本少很多。一个把所有样本都判成N的模型准确率可能也有80%以上但这种模型没有实际意义。所以我在WEKA结果面板里第一步不是看Overall Accuracy而是看每个类别对应的Precision、Recall和F-measure再看混淆矩阵。重点盯恶性心律失常类别的召回率也就是有多少真阳性被正确找出来了。如果V类召回率低于90%哪怕整体准确率到了95%这个模型在临床上也是不合格的因为漏掉一个室性早搏的代价远高于多报几个正常心拍。ROC曲线我也会顺手看一眼尤其是对少数类。WEKA会生成每个类的ROC面积如果少数类的AUC明显低于多数类说明特征对少数类区分能力不足这时候应该回头检查特征而不是盲目换分类器。5. 复现时容易翻车的细节与我的处理经验5.1 心拍切割没有对齐R峰会毁掉特征DWT的特征对相位很敏感。如果R峰注释在时间轴上偏移几个采样点切出来的心拍虽然看起来差别不大但每层小波系数的能量分配会发生明显变化特征分布就会出现“同类别样本发散、异类别样本重叠”的情况。我有一次跑完实验发现特征图特别乱排查到最后发现是把注释时间的秒数和采样点数弄混了一个换算误差让所有心拍错位了将近一秒钟。如果你也遇到同类样本特征分布特别散先回去检查注释读取和索引对齐不要急着换分类器。5.2 类别不平衡会让准确率好看但模型没用就像前面说的心电数据天然不平衡。处理方式可以在WEKA里用SMOTE过滤器做合成过采样也可以手动对多数类欠采样。我的建议是先统计各类样本数量如果多数类和少数类数量差距超过3倍再考虑采样策略。这里有一个特别隐蔽的坑SMOTE必须在每折训练集内部做不能先对整个数据集做SMOTE再划分交叉验证。否则少数类的合成样本会同时出现在训练集和测试集里模型相当于剽窃了测试集的分布评估结果会虚高。如果你用了我前面说的按患者划分还要额外注意不要让SMOTE生成样本跨越患者边界否则同样存在泄漏风险。5.3 不要把小波系数全部堆给分类器DWT每一层的系数序列其实还是一串高维向量不是特征。有人会直接把每层几十个或上百个系数拼在一起作为特征这样维度会迅速膨胀分类器容易过拟合。正确的做法是用统计量去压缩每一层的信息比如能量、均值、标准差、熵只保留最核心的分布描述。特征工程的关键不是把所有信息都塞给模型而是用一个低维但稳定的向量把不同类别拉得足够开。5.4 实验可复现参数记录比代码本身更重要最后说一个我自己的习惯跑实验时我会把特征列表、小波基、分解层数、归一化方式、分类器名称、参数版本全部记在一个实验表格里。DWT的可调参数太多了sym4和db4的结果会有差异分解4层和5层的特征含义不同RR间期是否加入也会影响结果。如果没有记录几个月后回来看MATLAB脚本根本不知道当初那批ARFF文件是怎么生成的。我给ARFF文件命名时都会带上版本比如arrhythmia_sym4_L4_RR_std.arff这样后面调整参数时能清楚知道每一版模型跑的是什么特征不用重新猜。本文还有配套的精品资源点击获取