拓冰建站拓冰建站
首页 / 资讯中心 / 正文

ECG心电信号分类:Python与MATLAB双轨实现与避坑指南

简介一份以心电图分类为主题的代码与数据组合资源同时提供 Python 和 MATLAB 两套实现适合医学数据分析初学者、生物医学工程学生以及需要快速验证算法的工程师用于解决心电信号去噪、波形识别和病症分类等完整流程问题。解压后共 825 个文件整体约 6.25 MB包含 C、Python、MATLAB 源码以及 dat、hea、atr 格式的心电记录文件还整理了多份说明文档、配置文件和脚本工具目录按数据集、代码和文档划分便于按需查找。资源中特别包含较多 WFDB 工具链相关文件能够完成信号读取、注释解析、格式转换与批处理等操作方便将公开心电数据用于训练和测试。目前已有 444 人浏览学习适合作为课程设计、毕业设计或科研预研的配套资料。仔细阅读项目代码后可以掌握滤波去噪、P-QRS-T 波检测、RR 间期计算和特征构建等关键步骤也能对比支持向量机、随机森林及神经网络在 ECG 分类中的表现并理解 Python 与 MATLAB 在生物医学信号处理上的实现差异具有较好的实践参考价值。1. ECG 分类一份代码包把 Python 和 MATLAB 两条路都铺好了拿到一份带标注的 ECG 心电信号如何把它变成可解释的心脏病分类结论是生物医学数据处理里绕不开的一步。ecg_classification-master.zip 这个代码包给了一条双轨路径Python 侧用 numpy、scipy、sklearn 完成从滤波到分类的完整管线MATLAB 侧借助 Signal Processing Toolbox 与 fitcecoc、fitcknn 实现同样目标。解压后会看到 00README、parsescp、bxb、nst、pschart 等脚本文件熟悉 PhysioNet 工具链的人一眼能认出这些是标准 ECG 标注解析和 beat 比对程序。适合两类人一类是刚接触信号处理的从业者想照着跑通一套可复现的心电分类流程另一类是已经用单一语言、想对比两种实现差异的工程师。如果环境还没搭好按常规的 python 安装教程配好解释器和 scipy、sklearn再备一个 MATLAB 2020 以上版本就能开工。2. ECG 分类的技术底座波形结构、噪声来源与特征设计2.1 P-QRS-T 波形每个间期都是可量化的医学指标ECG 记录的是心脏电生理活动的时序信号一段标准心电图由 P 波、QRS 波群、T 波三大部分组成。P 波对应心房去极化QRS 波群对应心室去极化T 波是心室复极化。分类任务围绕这些波的形态、幅度、间期展开。RR 间期是两次连续 QRS 波峰的距离直接反映心率变异性是心律失常分类里最常用的时域特征QT 间期从 Q 波起点到 T 波终点过长提示长 QT 综合征风险ST 段抬高或压低是心肌缺血的典型标志在心肌梗塞分类里权重极高。包里的 pschart 脚本正是用来可视化这些波段的跑分类前先看图比直接丢给模型靠谱得多。特殊波形也要心里有数。房颤的典型特征是 RR 间期完全不规律正常窦性心律的 RR 间期相对稳定室性早搏则表现为宽大畸形的 QRS 波且后面跟着一个代偿间歇。这些特征用数值计算很容易量化RR 间期的标准差、QRS 波宽度、T 波形态不对称度。常见的做法是把这些统计量拼成一个特征向量再丢给分类器。特征怎么选直接决定分类上限模型只是逼近这个上限所以 2.1 这节是整条管线的地基。2.2 预处理降噪、去基线漂移、统一采样率原始 ECG 信号质量几乎不可能直接满足特征提取要求。体表采集的信号幅度只有毫伏级工频干扰50 Hz、肌电噪声、呼吸引起的基线漂移都会叠加在有用波形上。带通滤波是最基础的降噪手段一般保留 0.5 Hz 到 45 Hz 的频带既能去除基线漂移和高频肌电噪声又不会损伤 QRS 波群的主要能量。滤波器实现上Python 的 scipy.signal 里 butter 配合 filtfilt 做零相位滤波MATLAB 里直接用 designfilt 工具代码都很短。采样率不统一是数据融合阶段最容易翻车的地方。MIT-BIH 心律失常数据库是 360 Hz欧洲 ST-T 数据库是 250 Hz如果直接混合使用RR 间期、QRS 宽度这些依赖时间轴的特征全部失真。我一般会先统一重采样到 360 Hz再进入特征提取环节这样能保证所有样本的时间度量在同一把尺子上。包里的 a2m 脚本涉及 AHA 数据库的读取那份数据的格式和 MIT-BIH 不同建议先在 README 里确认来源再决定重采样目标。2.3 特征提取时域统计、小波分解与形态学特征特征提取是 ECG 分类的核心环节大致分三条路线。第一条是时域统计特征包括 RR 间期均值与标准差、RR 间期差值的均方根RMSSD、QRS 宽度、QT 间期长度这类特征计算成本低、可解释性强适合做心律失常粗分类。第二条是频域特征用傅立叶变换计算功率谱密度提取低频段LF和高频段HF的能量比LF/HF 比值常被用来评估自主神经活性但频域特征对噪声敏感基线未除净时谱能量会偏移。第三条是小波特征对信号做多尺度分解提取各层小波系数的能量和熵db4 小波与 QRS 波形态相似度高能有效捕捉局部突变适合室性早搏和心肌缺血分类。特征选择上有个原则先加时域和形态学特征跑一版基线再叠加小波特征看收益不要一上来就把特征堆到上百维。样本量不大的时候特征维度太高会放大噪声。包里没有直接给出特征清单但 parsescp 脚本负责解析 SCP-ECG 标准格式的标注解析出的波形边界点正好用来计算间期特征这一步是特征提取的入口。下面给出常用的特征参数表供设计特征向量时参考。特征类别具体参数适用场景计算成本时域RR 间期均值、标准差、RMSSD心律失常粗分类低形态学QRS 宽度、QT 间期、ST 段偏移心肌缺血、长 QT 综合征低频域LF/HF 比值、频谱熵自主神经功能评估中小波db4 各层系数能量、小波熵室性早搏、房颤细分类中3. Python 侧实现从读取 .mat 到训练分类器的完整管线3.1 环境准备与数据读取Python 侧依赖四个核心库numpy 做数组运算scipy.signal 提供滤波和峰检测scikit-learn 提供分类模型matplotlib 做结果可视化。数据格式上压缩包里既有 CSV 也有 .mat 格式的样本。.mat 文件用 scipy.io 读取注意 MATLAB 存储变量时可能会包一层结构体load 完先打印 keys 确认变量名不要想当然地取 ecg 字段血泪经验变量名对不上最容易在这里卡住。import scipy.io as sio import numpy as np from scipy.signal import butter, filtfilt mat sio.loadmat(sample_ecg.mat) print(mat.keys()) # 先确认变量名常见的是 ecg / signal / data sig mat[ecg].flatten().astype(np.float64) fs 360 # MIT-BIH 标准采样率 # 带通滤波保留 0.5~45Hz 的心电有效频带 b, a butter(4, [0.5 / (fs / 2), 45 / (fs / 2)], btypeband) filtered filtfilt(b, a, sig)flatten() 这一步很多人会漏。MATLAB 存出来的数组维度是 (n, 1)如果不降维后面所有按一维信号写的算法都会报维度错误或者更隐蔽地产生错误结果。butter 的第二参数是归一化频率分子分母都要除以奈奎斯特频率 fs/2这是 scipy 的硬性要求直接写 0.5 和 45 会得到一条完全没滤波的信号。filtfilt 是零相位滤波正着滤一遍再倒着滤一遍能消除相位偏移代价是延迟会体现在整个序列上但在离线分类场景无所谓。3.2 R 峰检测与特征计算R 峰是 ECG 特征提取的锚点RR 间期、QRS 宽度全都依赖它。scipy 的 find_peaks 配合 height 和 distance 参数能处理质量中等的信号但遇到基线漂移没除净的样本阈值就会失真。更稳的做法是先用一阶差分放大 QRS 波斜率再找局部极大值。这个技巧在处理包里的多份样本时表现很稳定。from scipy.signal import find_peaks # 对滤波后的信号做一阶差分放大 QRS 波群的斜率特征 diff_sig np.diff(filtered) # 差分信号上找峰峰值高度和间距需要根据采样率调整 peaks, props find_peaks(diff_sig, heightnp.std(diff_sig) * 2.5, distanceint(fs * 0.2)) # 计算 RR 间期序列单位秒 rr_intervals np.diff(peaks) / fs rr_mean np.mean(rr_intervals) rr_std np.std(rr_intervals) rmssd np.sqrt(np.mean(np.diff(rr_intervals) ** 2))height 参数用标准差的倍数而不是绝对值是因为不同样本的幅值差异很大绝对值阈值在样本 A 上有效样本 B 上可能完全失效。distance 设为 fs×0.2表示两个 R 峰之间至少间隔 0.2 秒对应心率上限 300 次/分这个约束能过滤掉差分信号里的高频抖动误检。RR 间期计算出来后统计量按数组方式聚合不要用循环逐点累加numpy 的向量化操作在这类计算上快两个数量级。3.3 分类模型训练与评估特征向量拼好后进入分类环节。小样本场景下优先用随机森林或 SVM两者对特征缩放不敏感且能给出特征重要性排序方便回头筛选特征。神经网络需要的数据量大这个包的样本规模未必撑得起。下面的代码用随机森林做基线模型再用交叉验证看稳定性。from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score, train_test_split from sklearn.preprocessing import StandardScaler # X 是特征矩阵每行一个样本y 是标签0 表示正常1 表示异常 X np.array(features) # features: (n_samples, n_features) y np.array(labels) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) clf RandomForestClassifier(n_estimators200, max_depth8, random_state42) clf.fit(X_train_scaled, y_train) scores cross_val_score(clf, X_train_scaled, y_train, cv5) print(f交叉验证准确率: {scores.mean():.3f} (/- {scores.std():.3f}))stratifyy 参数很重要它保证训练集和测试集里正常/异常样本的比例与原始数据一致避免因随机划分导致某一类样本在测试集里过少评估结果虚高或虚低。StandardScaler 的 fit_transform 和 transform 分开调用是防止数据泄漏的关键写法——归一化参数只能在训练集上计算测试集直接套用同样的均值和方差如果测试集参与拟合相当于提前透露了测试集的分布信息交叉验证分数会偏乐观。random_state 固定下来保证每次跑出的划分一致方便调试特征工程的效果。4. MATLAB 侧实现工具箱调用、fitcecoc 与 fitcknn 的选型4.1 信号读取与滤波器设计MATLAB 的优势在于 Signal Processing Toolbox 把滤波、谱分析、峰检测封装成了单行函数写起来比 Python 简洁。但简洁不等于没坑designfilt 的语法和 scipy 差异很大不熟悉的人容易在滤波器阶数和截止频率上犯迷糊。读取 .mat 文件直接用 load注意 MATLAB 和 Python 共用 .mat 格式但默认压缩算法可能不同低版本 MATLAB 打不开高版本存的 -v7.3 文件这是个隐蔽的大坑。% 读取 .mat 文件注意变量名不要被结构体嵌套 data load(sample_ecg.mat); sig data.ecg(:); % 转成行向量统一后续处理维度 fs 360; % 设计带通滤波器注意函数名是 designfilt bpFilt designfilt(bandpassiir, FilterOrder, 4, ... HalfPowerFrequency1, 0.5, HalfPowerFrequency2, 45, ... SampleRate, fs); filtered filtfilt(bpFilt, sig); % 画图确认波形质量先看图再提特征 t (0:length(sig)-1) / fs; plot(t, filtered); xlabel(时间 (s)); ylabel(幅值 (mV));designfilt 的 HalfPowerFrequency 就是 -3dB 截止频率与 scipy 的 butter 参数语义一致但不需要手动除以奈奎斯特频率SampleRate 单独指定这一点对新手更友好。FilterOrder 用 4 是折中方案阶数太低阻带衰减不够工频干扰滤不干净阶数太高滤波器群延迟增大QRS 波的尖锐波形可能被拉钝。filtfilt 同样做零相位滤波MATLAB 和 Python 在这点上是同一套思路。画图这步别省滤波效果好不好裸眼最快。4.2 特征提取与分类器对比MATLAB 的 findpeaks 函数自带多参数调节最小峰高和最小峰间距的默认值在 ECG 场景下通常太宽松需要显式指定。提取完 R 峰位置后间期特征的计算逻辑和 Python 侧完全一样只是语法换成了 MATLAB 风格。分类器选型上fitcecoc 是支持向量机的多分类封装适合三分类以上的心律失常类型fitcknn 是 K 近邻胜在无需训练时间但对特征缩放敏感且预测阶段要遍历全部训练样本。% 在差分信号上检测 R 峰 diffSig diff(filtered); [pks, locs] findpeaks(diffSig, MinPeakHeight, std(diffSig) * 2.5, ... MinPeakDistance, round(fs * 0.2)); rrIntervals diff(locs) / fs; rrMean mean(rrIntervals); rrStd std(rrIntervals); % 构造特征矩阵 featuresMat每行一个样本 % labels 是分类标签向量 % 多分类 SVM svmModel fitcecoc(featuresMat, labels, Learners, svm, ... KFold, 5, Standardize, true); cvError kfoldLoss(svmModel); fprintf(5 折交叉验证错误率: %.3f\n, cvError); % KNN 对比 knnModel fitcknn(featuresMat, labels, NumNeighbors, 5, ... Standardize, true, KFold, 5); knnError kfoldLoss(knnModel); fprintf(KNN 交叉验证错误率: %.3f\n, knnError);fitcecoc 的 Learners, svm 用的是模板 SVM默认核函数是线性核线性核在高维小样本场景下泛化能力强且训练快。Standardize, true 让模型在训练前自动标准化特征等价于 Python 里的 StandardScaler这一步对 KNN 这类距离敏感的算法尤其关键。对比看出fitcecoc 和 fitcknn 都内置交叉验证选项直接传 KFold, 5 就能在训练时自动做五折验证并保存错误率省去了手动写划分循环的麻烦。如果数据包含三类以上的心跳类型fitcecoc 会把多分类问题拆成多个二分类子问题每个子问题用一个 SVM 解决最后通过投票得到预测类别。这种方式比直接训练一个多分类神经网络稳定得多在样本量只有几百条时尤其明显。相对于 Python 侧需要手动拼装分类器链MATLAB 一行函数就封装完了代价是自定义能力受限想更换核函数或调整 SVM 惩罚系数时需要额外构造模板对象。5. 避坑与排查ECG 数据集五个最容易翻车的环节5.1 R 峰检测结果在两种语言里对不上现象同一份 .mat 数据Python 的 find_peaks 检出的 R 峰位置和 MATLAB 的 findpeaks 差了几十个点RR 间期特征完全偏离分类结果一个准确率 90%另一个不到 60%。原因scipy 和 MATLAB 的峰检测器在局部极大值的判定逻辑上不同。scipy 的 find_peaks 要求峰值严格大于相邻点MATLAB 的 findpeaks 在默认行为下会做一定的平滑处理并且对边界处峰值的处理方式不一样。更隐蔽的是两者对 distance 参数的语义相同但对差分信号上细小毛刺的容忍度不同导致误检数量差异很大。解决我后来改成统一使用 R 峰位置的共享文件。先用 MATLAB 的 findpeaks 检测一次把峰位置存成 CSV 或 .mat 文件Python 侧直接读结果不再重复检测。两边特征提取都用同一组 R 峰坐标对比的就是分类算法本身而不是峰检测器的差异。如果一定要各算各的至少把 MinPeakHeight 和 MinPeakDistance 的参数严格对齐。5.2 滤波器阶数过高把 QRS 波拉变形现象滤波后的信号看起来噪声没了但 QRS 波明显变宽原先尖锐的 R 峰变成圆钝的鼓包QRS 宽度特征从 80ms 变成 120ms所有形态学特征整体漂移。原因滤波器阶数太高群延迟曲线在通带边缘剧烈变化导致不同频率成分的延迟不一致波形相位被扭曲。尤其在使用 butter 高阶滤波器时通带内的线性相位特性很差QRS 波群的高频分量和低频分量到达时间不同波峰自然被拉平。解决把滤波器阶数降到 2 到 4 之间优先用 4 阶并配合 filtfilt 双向滤波。如果仍觉得波形被拉伸改用零相位滤波器或者干脆用移动平均平滑替代滤波。检验方法很简单滤波后计算最窄的 QRS 宽度如果小于 60ms 或大于 120ms基本可以断定滤波器出了问题。查看 pschart 脚本生成的波形图QRS 波群形态对比原始信号偏差明显就要调参数。5.3 训练测试数据泄漏导致准确率虚高现象交叉验证准确率 99%但拿到一份新采集的 ECG 数据上测试准确率跌到 70%模型完全不能泛化。原因典型的特征是归一化参数泄漏。标准的做法是在每个交叉验证折内单独计算归一化参数但有偷懒的做法是在整个数据集上先做 StandardScaler 拟合并转换再进行交叉验证归一化的均值和方差已经把测试折的信息透露给了训练折模型看到的是被全局分布校准过的特征评估结果自然虚高。解决使用 Pipeline 把归一化和分类器绑定让交叉验证自动在每个折内独立完成归一化。Python 里是 sklearn 的 Pipeline 类MATLAB 里直接把 Standardize, true 传给 fitcecoc让标准化成为训练流程的一部分。从那以后我每次跑分类都会检查一遍数据流确保从原始信号到特征矩阵的每一步训练和测试都是分开处理的。5.4 采样率混用导致特征时间度量混乱现象把 360 Hz 和 250 Hz 的数据混合训练模型在验证集上表现尚可但上线后对单条 250 Hz 数据预测RR 间期全部偏长分类结果一片混乱。原因RR 间期、QT 间期的计算依赖时间轴精度R 峰位置是样本点数除以采样率才得到秒数。如果采样率标注错误或者混用同一段信号在 360 Hz 下算出 RR 间期 0.8 秒在 250 Hz 下变成 1.1 秒所有时域特征全部失真。解决在特征提取之前统一重采样到 360 Hz。Python 里用 scipy.signal.resample_polyMATLAB 里用 resample 函数。注意 resample 会改变信号长度重采样后必须重新检测 R 峰不能沿用旧位置。数据加载时做一个断言检查采样率字段和实际信号长度是否匹配长度除以采样率应该在合理的时间范围内不匹配直接报错防止脏数据流入后续流程。5.5 加噪测试时模型性能断崖式下跌现象干净数据上分类效果很好但叠加了噪声后性能骤降室性早搏和正常心跳的区分度几乎消失。原因训练集只有干净样本模型学到的是理想条件下的波形特征没有覆盖肌电噪声、电极松动、运动伪影这些真实采集场景。包里的 nst 脚本是噪声压力测试工具原本用来评估检测器在不同信噪比下的表现但没有把加噪样本加进训练集模型根本没见过这些波形形态。解决用 nst 脚本生成不同信噪比的噪声信号按 0dB 到 10dB 五档混入训练集让模型学会在噪声中抓住 QRS 波的核心特征。注意加噪后的信号要重新做滤波否则高频噪声可能被误判成 QRS 波。每档信噪比至少生成 20 条样本不然模型会记住噪声模式而不是学习信号特征。6. 最后一步用混淆矩阵和可视化做结果可信度复盘模型训练完成不代表工作结束分类器的评估指标必须拆开看。准确率在正负样本不平衡时没有参考价值比如数据里 90% 是正常心跳模型全预测正常也能拿 90% 准确率。这时候要看混淆矩阵里的灵敏度和特异度灵敏度高意味着漏诊率低对心电筛查场景更加关键。MATLAB 里用 confusionchart 一行出图Python 里用 sklearn 的 ConfusionMatrixDisplay。from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay, classification_report y_pred clf.predict(X_test_scaled) cm confusion_matrix(y_test, y_pred) disp ConfusionMatrixDisplay(confusion_matrixcm, display_labels[正常, 异常]) disp.plot(cmapBlues) print(classification_report(y_test, y_pred, target_names[正常, 异常]))classification_report 会一次性输出精确率、召回率、F1 值关注召回率那一列代表异常样本被正确检出的比例。筛查场景下召回率 90% 以上才算及格。同时对比训练集和测试集的表现差异如果训练集准确率 99% 而测试集只有 85%说明过拟合已经比较严重需要减少特征维度或降低树深度。我个人的习惯是每跑完一版分类器强制输出三张图原始波形带 R 峰标记、滤波前后的频谱对比、混淆矩阵。前两张图确认信号处理环节没有把波形搞变形第三张图确认模型输出落在合理区间。运行包里的 pschart 脚本对比检测结果和标注文件如果 R 峰位置和标注偏差超过 20ms回到第 5 章排查峰检测参数。这套验证流程能拦截绝大多数隐蔽 bug从那以后我每次 ECG 分类实验都强制走一遍没出过测试集翻车的情况。希望这套管线在你自己的数据上也能跑得顺。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门