简介:这份资源面向医学信号处理与机器学习方向的学习者和研究者,提供一套基于离散小波变换(DWT)特征提取、结合WEKA与MATLAB监督训练的心律失常分类完整方案,用于解决ECG信号特征提取与心律异常自动识别问题。压缩包共539个文件,约28.11MB,包含402个png图像、29个csv数据表、18个mat与18个txt文件,以及hea、info等WFDB心电记录文件、m脚本、arff数据集和tex、bib等论文排版素材,覆盖从信号处理到模型训练再到结果呈现的完整链路。资源中提供了多组arff训练与测试集,便于直接导入WEKA开展监督学习实验,MATLAB脚本则用于DWT特征提取与数据预处理。目前已有57人学习下载,适合希望快速复现心律失常分类流程、对比不同特征与分类器效果的中高级读者参考。
1. 从一段心电信号说起:这套 DWT+WEKA 心律失常分类资源到底能干什么
手里有一段 MIT-BIH 格式的心电信号,采样率 360 Hz,长度几十万点,肉眼盯着屏幕看波形,除了能看出心跳快慢,几乎分辨不出室性早搏和正常搏动的差别。这是很多做生理信号处理的同学第一次拿到心律失常数据时的真实处境。这套「基于离散小波变换特征提取的心律失常分类」资源,解决的正是从原始 ECG 波形到可解释分类结果之间的整条链路:用 MATLAB 做离散小波变换(DWT)把一维时序拆成不同频带的细节系数和近似系数,从中提取能区分心律类型的特征向量,再交给 WEKA 做监督训练和分类评估。它适合两类人:一类是刚接触生物医学信号、想跑通「特征工程 + 传统机器学习」完整流程的新手;另一类是已经用过 SVM、随机森林,但没系统做过小波域特征设计、想看看别人怎么组织 WEKA 训练流程的熟手。资源本身不依赖深度学习框架,纯 MATLAB + WEKA 组合,对算力要求低,笔记本就能跑,这也是它至今仍有参考价值的原因。
2. DWT 特征提取:为什么选小波而不是 FFT,以及 MATLAB 里怎么落地
2.1 小波变换在 ECG 上的选型理由
心电信号是非平稳信号,QRS 波群、P 波、T 波在时域上位置和宽度都不一样。FFT 把信号整体变换到频域,丢掉了时间信息,你没法知道某个频率成分出现在哪个心动周期。DWT 的优势在于它用可伸缩、可平移的小波基去匹配信号局部特征,低频处频率分辨率高、高频处时间分辨率高,正好对应 ECG 里「慢变基线」和「陡峭 QRS」并存的特点。
常见做法是选 Daubechies 小波族,db4 或 db6 在 ECG 特征提取里出现频率最高。分解层数一般取 4 到 5 层,因为 MIT-BIH 采样率 360 Hz,5 层分解后近似系数对应的频带大约在 0 到 5.6 Hz,覆盖了主要的心律信息,而细节系数 d3、d4、d5 分别对应 QRS 能量集中区、P/T 波区和基线漂移区。这个频带划分不是玄学,是按采样率和分解层数算出来的:每层分解把当前频带一分为二,第 j 层细节系数对应频带约为 fs/2^(j+1) 到 fs/2^j。
2.2 MATLAB 端 DWT 特征提取的完整代码
下面这段代码是我一般会用的骨架,输入是单导联 ECG 信号向量和对应的 R 波位置索引,输出是每个心拍的特征向量矩阵。它做了三件事:按 R 波切分心拍、对每个心拍做 DWT、从各层系数里算统计量。
% 输入: ecg_signal - 预处理后的ECG信号 (1 x N) % r_peaks - R波位置索引 (1 x M) % 输出: feature_matrix - 特征矩阵 (M x F) % feature_names - 特征名元胞 function [feature_matrix, feature_names] = extract_dwt_features(ecg_signal, r_peaks) wavelet_name = 'db4'; % 小波基,ECG常用db4/db6 level = 5; % 分解层数,360Hz采样下5层足够 window_left = 100; % R波前100点,约0.28秒 window_right = 150; % R波后150点,约0.42秒 num_beats = length(r_peaks); feature_matrix = []; feature_names = {}; for i = 1:num_beats % 按R波位置切分单个心拍,边界不足则跳过 start_idx = r_peaks(i) - window_left; end_idx = r_peaks(i) + window_right; if start_idx < 1 || end_idx > length(ecg_signal) continue; end beat = ecg_signal(start_idx:end_idx); % 对单心拍做DWT分解 [c, l] = wavedec(beat, level, wavelet_name); % 提取各层细节系数和最后一层近似系数 feat_vec = []; for j = 1:level d = detcoef(c, l, j); % 每层算4个统计量:能量、均值绝对值、标准差、峰度 feat_vec = [feat_vec, ... sum(d.^2), ... % 能量 mean(abs(d)), ... % 平均绝对值 std(d), ... % 标准差 kurtosis(d)]; % 峰度 end a = appcoef(c, l, wavelet_name, level); feat_vec = [feat_vec, sum(a.^2), mean(abs(a)), std(a), kurtosis(a)]; feature_matrix = [feature_matrix; feat_vec]; end % 生成特征名,方便WEKA里对应 for j = 1:level feature_names{end+1} = sprintf('d%d_energy', j); feature_names{end+1} = sprintf('d%d_meanabs', j); feature_names{end+1} = sprintf('d%d_std', j); feature_names{end+1} = sprintf('d%d_kurt', j); end feature_names{end+1} = 'a_energy'; feature_names{end+1} = 'a_meanabs'; feature_names{end+1} = 'a_std'; feature_names{end+1} = 'a_kurt'; end逻辑说明:wavedec返回的c是所有层系数的拼接向量,l是各段长度记录,必须成对传给detcoef和appcoef才能正确取出对应层。窗口左右不对称是因为 QRS 波群能量主要集中在 R 波之后,前窗短后窗长能减少不必要的数据量。每层取四个统计量是权衡后的结果:能量反映该频带强度,平均绝对值和标准差反映波动程度,峰度捕捉冲击性成分,对室性早搏这种形态突变比较敏感。
参数说明:wavelet_name换成'db6'会得到更平滑的系数,但计算量略增;level改成 4 会少一层低频细节,适合采样率更低的场景;window_left和window_right需要根据实际 R 波检测精度调整,如果 R 波定位有偏移,窗口要适当放宽。
2.3 特征向量的组织与标签对齐
提取完特征矩阵后,还需要把标签列拼上去。标签来自 MIT-BIH 的注释文件,通常把心拍分成正常搏动、室性早搏、右束支传导阻滞等几类。常见做法是只保留样本量足够的三到四类,避免某类只有几十个样本导致训练不稳定。拼好后存成 CSV 或 ARFF,WEKA 两种都能读,但 ARFF 对属性类型声明更明确,推荐优先用 ARFF。
% 假设 labels 是 M x 1 的类别标签向量,与 feature_matrix 行对应 % 注意:如果前面跳过了边界心拍,labels 也要同步过滤 data_table = array2table(feature_matrix, 'VariableNames', feature_names); data_table.label = labels; writetable(data_table, 'ecg_dwt_features.csv');这里有个容易翻车的点:extract_dwt_features里遇到边界心拍会continue跳过,但外部传入的labels不会自动跟着跳。血泪经验是,要么在函数里同时返回有效索引,要么在调用前就把边界心拍和对应标签一起剔除。我一般会在函数里多返回一个valid_idx,外部用它去索引标签。
3. WEKA 监督训练:ARFF 构造、分类器选择和十折交叉验证
3.1 ARFF 文件构造与属性声明
WEKA 读 CSV 也能自动推断类型,但类别列如果被推断成数值型,分类器会当成回归问题处理,结果完全不对。所以稳妥做法是手动写 ARFF 头,或者用 MATLAB 生成 ARFF。下面是一个 ARFF 头的模板,属性名和前面特征名一一对应。
@RELATION ecg_arrhythmia @ATTRIBUTE d1_energy NUMERIC @ATTRIBUTE d1_meanabs NUMERIC @ATTRIBUTE d1_std NUMERIC @ATTRIBUTE d1_kurt NUMERIC @ATTRIBUTE d2_energy NUMERIC @ATTRIBUTE d2_meanabs NUMERIC @ATTRIBUTE d2_std NUMERIC @ATTRIBUTE d2_kurt NUMERIC @ATTRIBUTE d3_energy NUMERIC @ATTRIBUTE d3_meanabs NUMERIC @ATTRIBUTE d3_std NUMERIC @ATTRIBUTE d3_kurt NUMERIC @ATTRIBUTE d4_energy NUMERIC @ATTRIBUTE d4_meanabs NUMERIC @ATTRIBUTE d4_std NUMERIC @ATTRIBUTE d4_kurt NUMERIC @ATTRIBUTE d5_energy NUMERIC @ATTRIBUTE d5_meanabs NUMERIC @ATTRIBUTE d5_std NUMERIC @ATTRIBUTE d5_kurt NUMERIC @ATTRIBUTE a_energy NUMERIC @ATTRIBUTE a_meanabs NUMERIC @ATTRIBUTE a_std NUMERIC @ATTRIBUTE a_kurt NUMERIC @ATTRIBUTE class {NORMAL,PVC,RBBB} @DATA注意class那一行的大括号里是类别名,必须和你的标签字符串完全一致,大小写敏感。如果标签是数字编码,这里要写成{0,1,2}并在数据行里对应。
3.2 分类器选择:为什么先跑随机森林和 SVM
WEKA 里分类器几十种,但在这个特征维度(24 维左右)和样本量(几千到几万心拍)下,我一般先跑两个:RandomForest 和 SMO。RandomForest 对特征尺度不敏感,不用归一化,能直接给出特征重要性,适合快速看哪些小波系数层贡献大。SMO 是 WEKA 里的 SVM 实现,在这个场景下配合 RBF 核通常能比随机森林高出一两个百分点,但需要对特征做标准化,否则能量类特征(数值可能上千)会压过峰度类特征(数值在个位数)。
命令行方式跑十折交叉验证:
# 随机森林,100棵树,十折交叉验证 java -cp weka.jar weka.classifiers.trees.RandomForest \ -t ecg_arrhythmia.arff -x 10 -I 100 -K 0 -S 1 # SMO,RBF核,十折交叉验证 java -cp weka.jar weka.classifiers.functions.SMO \ -t ecg_arrhythmia.arff -x 10 -C 1.0 -L 0.001 -N 0 -V -1参数说明:-x 10是十折交叉验证,-I 100是随机森林树的数量,-K 0表示每个节点随机选的特征数由算法自动定。SMO 的-C是惩罚系数,-L是容忍度,-N 0表示用 RBF 核。如果数据类别不平衡,加-W参数调整权重,或者先用 WEKA 的SpreadSubsample过滤器做欠采样。
3.3 结果解读:看什么指标,别只盯准确率
WEKA 输出里有一堆数字,新手容易只看Correctly Classified Instances那一行。但在心律失常分类里,PVC(室性早搏)的召回率比整体准确率重要得多,因为漏检一个 PVC 的代价远大于把一个正常搏动误判。所以重点看混淆矩阵和各类的 TP Rate、FP Rate。如果 PVC 的 TP Rate 低于 0.8,说明特征对 PVC 的区分力不够,常见原因是 d3 或 d4 层的统计量没抓住 PVC 的宽 QRS 特征,可以考虑加一层 d2 的系数或者换 db6 小波基重跑。
4. 避坑与排查:从数据泄漏到小波边界效应,五个真实翻车点
4.1 现象:交叉验证准确率 99%,换一批数据掉到 70%
原因:特征提取时用了全局归一化,比如对整个信号算均值和标准差再归一化,然后才切分心拍。这样测试集的信息通过归一化参数泄漏到了训练过程。解决:归一化必须放在切分之后,或者用 WEKA 的Normalize过滤器在交叉验证内部做,WEKA 的过滤器在-x 10模式下会自动对每折训练集单独拟合。
4.2 现象:WEKA 报错「nominal value not declared in header」
原因:ARFF 头里class声明的类别名和数据行里的标签字符串不一致,常见于多了空格、大小写不同、或者标签里有中文。解决:用strtrim清理标签,统一转大写,并在 ARFF 头里逐字核对。如果标签是数字,确认 ARFF 里写的是{0,1,2}而不是numeric。
4.3 现象:某些心拍的特征值全是 NaN
原因:kurtosis函数在输入向量方差为零时返回 NaN,比如某层细节系数全为零(信号在该频带无能量)。解决:在计算统计量前加判断,方差为零时用 0 替代,或者直接跳过该心拍。我一般会在特征提取函数里加一行if std(d) == 0, kurt_val = 0; else kurt_val = kurtosis(d); end。
4.4 现象:DWT 分解后系数长度和预期对不上
原因:wavedec默认使用周期延拓,边界处系数长度会因小波滤波器长度而略有变化。如果手动按固定长度切分系数,会错位。解决:始终用detcoef和appcoef按层取系数,不要直接对c做切片。另外,单心拍长度要足够长,至少大于小波滤波器长度乘以 2 的 level 次方,否则分解会报错或产生大量边界效应。
4.5 现象:随机森林训练很慢,几万心拍跑十几分钟
原因:特征维度虽然不高,但树的数量设太多(比如 500),且没有限制最大深度。解决:先用 100 棵树跑一版看特征重要性,把重要性接近零的特征删掉,再降到 50 棵树重跑。另外 WEKA 默认用所有 CPU 核心,如果机器核少,加-num-slots 1反而更稳。
5. 进阶技巧:用 WEKA 特征选择反哺小波参数调整
跑通基础流程后,真正拉开差距的是「特征选择 → 小波参数调整」这个闭环。WEKA 的AttributeSelection面板里,我一般用CfsSubsetEval配合BestFirst搜索,或者直接用InfoGainAttributeEval排个序。跑完你会看到哪些小波系数层的统计量被选中。如果 d1 和 d2 层几乎没被选上,说明高频细节对分类贡献小,可以考虑把分解层数从 5 降到 4,减少特征维度;如果 a 层(近似系数)的统计量排名很高,说明基线信息重要,预处理时就不要做太激进的高通滤波。
下面这段是 WEKA 命令行做特征选择的示例:
# 用信息增益评估所有特征,按排名输出 java -cp weka.jar weka.attributeSelection.AttributeSelection \ -E "weka.attributeSelection.InfoGainAttributeEval" \ -S "weka.attributeSelection.Ranker -N 10" \ -i ecg_arrhythmia.arff-N 10表示输出前 10 个特征。拿到排名后,回到 MATLAB 里只保留排名靠前的层,重新生成 ARFF,再跑一次十折交叉验证。我自己的习惯是至少做两轮这个循环:第一轮用全部 24 维特征跑基线,第二轮用筛选后的 10 到 12 维跑对比。多数情况下维度降一半,准确率不掉甚至略升,训练时间明显缩短。
还有一个容易被忽略的点:小波基的选择可以用 WEKA 的结果来反向验证。把 db4 和 db6 各跑一遍,看哪个基下选出的特征更集中、PVC 召回率更高。不要凭感觉选,让数据说话。从那以后我每次做小波特征提取,都强制先跑一轮 WEKA 特征选择再定分解层数和小波基,不再拍脑袋。希望帮到你。
本文还有配套的精品资源,点击获取