简介:一份面向心电图(ECG)分类的完整代码资料包,使用Python与MATLAB双语言实现,适合生物医学工程、数据分析与机器学习方向的学习者对照实践,也可作为课程设计或科研入门的参考模板。包内共825个文件,压缩后大小约6.25MB,文件类型涵盖py脚本、m脚本、c与h源码、hea/atr/dat等心电数据与标注文件,以及WFDB相关工具、readme说明、结果图表等,结构完整,便于按模块检索与复现。内容覆盖心电信号预处理(噪声去除、基线漂移校正)、波形特征提取(RR间期、QT间期、P-QRS-T段参数)以及基于SVM、随机森林等机器学习模型的分类与评估,能够帮助读者跑通从原始数据到分类结果的完整链路,同时对比Python与MATLAB在信号处理和建模上的不同写法。该资源已有445人学习下载,适合希望快速上手心电分析、理解生物医学信号处理流程的读者。
1. 一个 ECG 分类压缩包,为什么值得从 00README 开始拆
第一次解压 ecg_classification-master.zip 时,我本准备看一堆 .py 和 .m 脚本,结果先看到 00README 和一排带数字尾巴的文件:wave.1、bxb.1、nst.1、a2m.1、parsescp.1 等。这是 WFDB 波形数据库工具链的 Unix 手册页,说明这份资源给的是从读取、滤波、R 波定位、特征提取到分类评估的一整条 ECG 分类闭环。
它用 Python 和 MATLAB 各实现一遍完整流程,特别适合被 MIT-BIH 标注格式、时间单位、评估口径卡住的从业者——无论你是想入门 ECG 分类,还是想把灵敏度和阳性预测率写到能让评审挑不出毛病。下面按预处理、训练、避坑、验收四步拆。
2. ECG 分类的原理与选型:预处理为什么比模型更决定上限
2.1 信号先验:P 波、QRS 波群和 T 波决定滤波与切片参数
ECG 信号不是白噪声,它有强先验结构。一个正常心拍由 P 波(心房去极化)、QRS 波群(心室去极化)和 T 波(复极化)组成,各波段有相对固定的时序和形态。心律失常分类正是靠这些波形的时序混乱(比如 RR 间期不齐)和形态变异(比如 QRS 变宽)来判别。MIT-BIH 数据集是比较常见的基准:48 条 30 分钟双导联记录(MLII 和 V5),采样率 360 Hz,幅度是 ADC 单位、实测电压要除以 200 得到 mV。下游代码里几乎每个魔法数字都能从这三个先验推出来。
滤波参数就是这样选出来的:带通下限 0.5 Hz 是为了切掉呼吸和电极移动引起的基线漂移,上限 45 Hz 是为了压掉高频肌电干扰,这两个值基本覆盖了 P-QRS-T 的主要能量带。R 波检测里的最小间隔 0.3 秒对应心室不应期,防止把 T 波当成 R 波。切片长度取 R 峰前 0.25 秒、后 0.4 秒,是为了把前一个 T 波结尾和新心拍的 P 波都装进去。新手最常翻车的地方就是跳过这一步,直接拿原始信号进分类器——基线漂移会让 ST 段特征面目全非,模型再好也是白搭。
2.2 Python 与 MATLAB 的选型:为什么这个包要写两遍
很多读者是冲 matlab_ecg 分类下载的,打开后发现还有整套 Python 实现,第一反应是冗余。实际上两条路线各管一段。
| 维度 | Python 路线 | MATLAB 路线 |
|---|---|---|
| 主要库 | numpy / scipy.signal / scikit-learn | Signal Processing Toolbox / Statistics Toolbox |
| 滤波与特征 | butter、filtfilt、welch 自己拼 | 工具箱封装好,拼装少、出图快 |
| 多分类器 | sklearn 的 SVC、RandomForest | fitcecoc、fitcknn 一条命令 |
| 批量实验 | 脚本改参数方便,适合跑批 | 也能跑,但启动和授权开销大 |
| 环境成本 | python 安装 + pip 装库 | 需要 MATLAB 授权和对应工具箱 |
我一般这样分工:探索期用 MATLAB 快速验证滤波参数和特征组合,方案定下来后用 Python 重写一遍做批量跑批和最终可视化。这套包两条腿都给了,等于把「验证 → 落地」两个阶段的工作都覆盖了。要注意的是,想靠 matlab 在线网页版这一类免安装途径跑 fitcecoc 基本不现实,本地装好授权和工具箱再动手。
2.3 包里的 .1 工具链:为什么评估环节离不开命令行
zip 里那排带 .1 后缀的文件不是乱码,是 WFDB 命令的手册页。它们对应的工具各有分工:wave 是交互式波形浏览器,plt 是批量绘图,pschart 输出 ST 段测量图表,a2m 把注释文件转成 MATLAB 格式,nst 给信号加标定噪声做鲁棒性测试,parsescp 解析 SCP-ECG 标准文件,bxb 和 epicmp 则是把两套注释逐拍对比并输出灵敏度、阳性预测率的评分工具。
这套工具链存在的意义,是让评估不依赖你个人写的脚本。网上那些 dqn、ppo 的 MATLAB 强化学习代码走的是另一条路线,而这套 ECG 分类走的是经典信号处理加机器学习:预处理 → 特征提取 → SVM/ECOC → 交叉验证。在样本量只有几万个心拍、每拍几十个特征的场景下,这条路线的可复现性和解释性比黑匣子深度学习强得多,而且纯 CPU 就能跑通。读代码时先看它提了哪些特征,再看分类器,顺序别搞反。
3. Python 侧预处理与特征提取实录:先把 MIT-BIH 变成特征矩阵
3.1 两种数据入口:原始 .dat/.hea 和已导出的 CSV/MAT
无论你是按 ecg python 关键词找来的,还是冲 matlab 版本来的,打开脚本之前都要先确认样例数据是哪一种形态。常见做法是两种都兼容:一种是标准 MIT-BIH 的 .dat/.hea 原始记录,另一种是已经导出好的 CSV 或 MAT 特征表。前者用 wfdb 库读,后者用 pandas 或 numpy 直接读。
# 方式一:wfdb 库直接读 MIT-BIH 原始记录 import wfdb record = wfdb.rdrecord('100', sampto=7200, channels=[0]) # 前 20 秒,避免整条读入内存 sig = record.p_signal[:, 0] # MLII 导联 fs = record.fs # 360 Hz ann = wfdb.rdann('100', 'atr', sampto=7200) # 专家参考注释 r_peak_samples = ann.sample # R 波所在样本点sampto=7200 是“读到第 7200 个样本”的意思,360 Hz 下正好 20 秒;调试阶段永远只读一小段,半小时的整条记录有 64 万样本,全读进来不仅慢还占内存。rdann 的 sample 字段是样本序号不是秒,转秒要除以 fs,这个单位问题后面避坑章还会再踩一次。
# 方式二:包里的 CSV 特征矩阵 import pandas as pd df = pd.read_csv('features.csv') X = df.iloc[:, :-1].values y = df.iloc[:, -1].valuesCSV 是包作者导出的特征矩阵,列一般是特征,最后一列是标签。拿到后先用 df.info() 和 df.isna().sum() 检查有没有空值和混入表头的脏行,这一步在 ECG 数据里特别重要——MIT-BIH 原始注释经常有边界情况,导出时容易产生 NaN。
3.2 预处理三件套:带通、零相位、基线校正
import numpy as np from scipy.signal import butter, filtfilt from scipy.ndimage import median_filter def preprocess_ecg(sig, fs=360.0, low=0.5, high=45.0, order=4): # 带通:0.5 Hz 切基线漂移,45 Hz 切肌电,尽量保留 P-QRS-T 能量 b, a = butter(order, [low / (0.5 * fs), high / (0.5 * fs)], btype='band') filtered = filtfilt(b, a, sig) # 零相位,R 波位置不被平移 # 基线再校正:0.5 秒中值窗口跟住缓慢基线,减掉后 ST 段不再倾斜 baseline = median_filter(filtered, size=int(fs / 2)) return filtered - baseline几个参数值得抠。filtfilt 是零相位滤波,正向反向各滤一次,输出没有相位延迟;如果换成 lfilter,R 波位置会被整体平移,后面所有特征跟着错位。order=4 是 Butterworth 的阶数,阶数越高过渡带越陡,但数值稳定性变差,一般 4 到 6 阶够用。中值滤波窗口取 fs/2 即 0.5 秒,这个尺度能跟住基线慢变化,又不会吃掉 QRS 本身的形态。
注意 butter 的 fs 写法是 scipy 新版的风格,老代码里常见 nyq 写法;如果你用新版本 scipy 跑老脚本,会看到 FutureWarning 甚至报错,环境问题集中放到避坑章处理。
3.3 R 波定位与心拍切片
from scipy.signal import find_peaks def loc_rpeaks(filtered, fs=360.0): d = np.abs(np.diff(filtered)) # 差分增强 QRS 陡峭边沿 peaks, _ = find_peaks(d, height=0.3 * np.max(d), distance=int(0.3 * fs)) return peaks / fs # 转成秒这里用差分而非原始幅度,是因为 QRS 的斜率远大于 P 波和 T 波,差分后 R 波顶点是明显极大值。height 取最大值的 0.3 倍相当于一个自适应阈值,避免不同记录间幅度差异导致漏检;distance=0.3*fs 是 300 毫秒不应期,专门防 T 波被误检。工程上更完整的是 Pan-Tompkins 那套积分窗口,但先跑通这个简化版再看代码里的 R 波路径,思路更清楚。
拿到 R 波位置后切片,每个心拍取 R 峰前 0.25 秒、后 0.4 秒,共 234 个样本(360×0.65),按拍对齐后就能提特征。切片时注意边缘拍要丢掉,首尾不足一个窗口的片段会引入半截波形。
注意:切片后把每一拍的标签和 R 波样本序号一起保存成一个 numpy 数组,后面 MATLAB 侧训练和 bxb 评估都要用同一套索引,索引对不上是最常见的串行错误。
3.4 特征表:哪些特征对心律失常真的有区分度
| 特征 | 计算方式 | 主要判别对象 |
|---|---|---|
| RR 均值/标准差 | diff(r_peak) 取均值、标准差 | 房颤时 RR 无规律,rr_std 明显升高 |
| QRS 宽度 | Q 起点到 S 终点 | 室性早搏 QRS 宽大畸形,常大于 120 ms |
| QT 间期 | R 峰到 T 波终点 | 长 QT 综合征、药物反应 |
| T 波幅度/斜率 | T 峰相对基线 | 心肌缺血时 T 波低平或倒置 |
| 频域 LF/HF | welch 功率谱比值 | 自主神经张力相关,常作为辅助特征 |
RR 间期相关特征在心律失常分类里几乎是区分度最高的一组,尤其是房颤这种“绝对不齐”的节律。QRS 宽度和 T 波形态则更偏室性事件。注意特征有两种切片粒度:心拍级(每拍一行)和片段级(每 10 秒一段),包里的 Python 脚本通常两种都算,分类任务如果是分拍识别就用心拍级,如果是判断一段记录有没有事件就用片段级,别混着用。
4. MATLAB 侧训练闭环:从特征矩阵到 fitcecoc 交叉验证
很多人搜 matlab_ecg 分类,搜到的代码要么是单分类器小 demo,要么是读不懂的旧版脚本。这套包的 MATLAB 部分结构比较规矩:读特征 → 标准化 → fitcecoc → 十折交叉验证 → 混淆矩阵。我把它重写成一份可以直接抄的模板。
4.1 数据进 MATLAB:CSV/MAT 导入与标签编码
T = readtable('features.csv'); % 特征矩阵,最后一列是标签 X = table2array(T(:, 1:end-1)); Y_raw = string(T.Label); % 注意 Label 可能是分类变量 % 按 AAMI 标准把五类心拍编码成 1~5 Y = zeros(size(Y_raw)); Y(Y_raw == "N") = 1; Y(Y_raw == "S") = 2; Y(Y_raw == "V") = 3; Y(Y_raw == "F") = 4; Y(Y_raw == "Q") = 5;readtable 读 CSV 时,Label 列经常被识别成 categorical 或 cell,先转成 string 再比较,能省掉一堆类型报错。标签编码这事看起来小,但编码顺序不一致会让混淆矩阵的类别对应关系错位,后面配 bxb 评估时更乱。
4.2 标准化与 fitcecoc 参数:为什么不能直接丢原始特征
Xz = zscore(X); % SVM 对量纲敏感,不标准化的话幅度特征会碾压 RR 间期特征 tmpl = templateSVM('KernelFunction', 'rbf', 'BoxConstraint', 1, 'KernelScale', 'auto'); mdl = fitcecoc(Xz, Y, 'Learners', tmpl);zscore 这一步是血泪经验。RR 间期是几百毫秒的量级,而 QRS 幅度是毫伏量级,不标准化的话 SVM 的核距离基本被幅度特征主导,模型等于只看了形态没看节律。templateSVM 里 KernelScale 用 auto 让 MATLAB 自己按数据尺度估计核宽,省得人去瞎调;BoxConstraint 默认 1 通常够用,过大会过拟合。fitcecoc 是纠错输出编码的多分类框架,内部自动把五分类拆成若干二分类,比手写 one-vs-rest 干净。
提示:不同 MATLAB 版本的 fitcecoc 默认学习器不一定相同,代码里不显式写 templateSVM,换台机器跑出来结果就可能对不上。把 Learners 参数写死,别依赖默认值。装 MATLAB 时也先确认 Statistics and Machine Learning Toolbox 在,缺工具箱的话 fitcecoc 会直接报错。
4.3 十折交叉验证与混淆矩阵:别只盯准确率
rng(42); % 固定随机种子,实验可复现 cvp = cvpartition(Y, 'KFold', 10); % 分类问题默认按类别比例分层 cval = crossval(mdl, 'CVPartition', cvp); loss = kfoldLoss(cval); [pred, ~] = kfoldPredict(cval); C = confusionmat(Y, pred); confusionchart(C, {'N','S','V','F','Q'}); per_class_sens = diag(C) ./ sum(C, 2); % 每类召回率 per_class_ppv = diag(C) ./ sum(C, 1)'; % 每类精确率cvpartition 对分类任务默认做分层切分,每折里五个类别的比例和总体一致,这比 random split 靠谱。kfoldLoss 给出的是误分率,我一般直接看 kfoldPredict 出来的逐拍预测,手工算 per-class 的 Sens 和 PPV——这两个指标在 ECG 论文里比全局准确率重要得多。因为 MIT-BIH 里正常心拍占绝大多数,模型全猜 N 也能有 80% 以上准确率,所以一定要看混淆矩阵的对角线和非对角线的分布方向。
提示:如果结果要写进论文,把十折里每一折的混淆矩阵累加后再算指标,而不是把十折指标做平均——后者会人为缩小方差,审稿人常抓这一点。
5. 常见问题与避坑清单:标注对齐、时间单位、加噪与样本不均衡
5.1 坑一:bxb 对比前没对齐时间窗口,Sens 和 PPV 集体失真
现象:用 bxb 对比参考注释和自己的检测注释,出来的匹配拍数明显偏少,灵敏度掉到 80 以下,而且怎么调参数都救不回来。
原因:bxb 按记录内的绝对时间对齐两个注释文件。如果你在预处理时裁剪了信号片段、但注释文件没同步裁剪,两个注释序列覆盖的根本不是同一段时间,对比结果自然错乱。
解决:裁剪数据时同步裁剪注释。用 wfdb 读取时 rdann 也要加 sampto 参数,或者用 bxb 的 -f/-t 参数显式指定起止时间,保证两个注释序列落在同一窗口内。我一般会在跑 bxb 前先打印两个注释文件的起止样本点做一次校准,确认时间窗一致再放行。
5.2 坑二:a2m 转出来的注释时间单位是样本点,不是毫秒
现象:用 a2m 把 WFDB 注释转进 MATLAB 后,把时间字段直接乘 1000 当毫秒填进特征表,QT 间期全部偏大得离谱。
原因:WFDB 注释内部时间戳以样本点为单位存储,a2m 导出时保留的是样本序号,而毫秒需要先除以采样率 360 得到秒,再乘 1000。
解决:拿到时间字段先除以 fs 再转单位;更省事的做法是直接在 Python 里用 rdann 的 sample/fs 换算,别再倒一手 a2m。这个单位问题在包的手册页里其实写明了,但很少有人读 man page 读到这一行。
5.3 坑三:nst 加噪的 SNR 是 RMS 信噪比,不是峰值信噪比
现象:用 nst 加 5 dB 噪声测试鲁棒性,肉眼看上去波形都快被噪声埋了,怀疑工具写错了。
原因:nst 的信噪比定义是 20*log10(rms_signal / rms_noise),基于均方根幅度,不是峰值信噪比。ECG 的峰值(R 波)远大于 RMS,所以同样的 dB 数,RMS 口径下的噪声能量感官上更猛。
解决:按 RMS 口径理解参数。先算信号段的 RMS,反推要加的噪声 RMS;nst 提供 baseline wander、muscle artifact、electrode motion 三种噪声源,按 dB 指定信噪比,跑完看输出报告里的实际 SNR,不要凭肉眼判断噪声大小。
5.4 坑四:样本不均衡让准确率虚高,多数类掩盖少数类错误
现象:测试集准确率 97%,打开混淆矩阵发现 V 类和 F 类几乎全错,少数类样本全被吞掉了。
原因:MIT-BIH 里正常心拍占绝对多数,分类器只要全猜 N 就能拿到高分,准确率对少数类完全没有约束力。
解决:指标改成逐类 Sens、PPV 和 F1,并给出宏平均;训练侧可以对少数类做重采样,或者给 fitcecoc 的损失加权。我看这类 ECG 分类代码时有个习惯:先看混淆矩阵右下角,再看左上角,最后才看总体准确率。
5.5 坑五:环境问题——python 版本、scipy API 和 MATLAB 工具箱缺一不可
现象:python 脚本在新环境里跑,butter 报错或者告警刷屏;MATLAB 脚本在别的机器上跑,结果和原 README 对不上。
原因:scipy 在较新版本里改了 butter 等函数的 fs 参数写法,老代码用 nyq 写法会报 FutureWarning,极端情况直接抛异常;MATLAB 侧则是 fitcecoc 默认学习器随版本变过,不锁定参数就没法复现。
解决:python 环境按 requirements.txt 锁版本,或者统一改成新版 fs 写法,在 vscode 里配 python 环境时直接用 conda 建一个干净环境;MATLAB 侧把 templateSVM 等参数写死在脚本里。包的 00README 里如果写了环境要求,先照做再谈改代码。
6. 用 bxb 收口评估:灵敏度和阳性预测率的最后一公里
6.1 一条命令把算法输出和专家标注对齐
# 参考注释用 atr(MIT-BIH 专家标注),算法输出用 qrs,逐拍对比 bxb -r 100 -a atr qrsbxb 的输出表里,TOT 行是总拍数,MISS 和误报行是错配的明细,最后两行直接给出灵敏度(Sens)和阳性预测率(PPV)。这两个数就是论文里最常引用的 ECG 检测指标:Sens 是专家标了而你也检出来的比例,PPV 是你检出来且专家也确认的比例。跑批时把每条记录的 Sens、PPV 汇总成一张表,最后一行写平均值,这是我验收任何检测器的固定动作。
6.2 双确认习惯:命令行评分和 sklearn 指标对照着看
| 指标 | 命令行工具 | Python 对照 |
|---|---|---|
| 灵敏度 Sens | bxb 输出行 | classification_report 里的 recall(逐类) |
| 阳性预测率 PPV | bxb 输出行 | classification_report 里的 precision |
| 综合指标 | bxb 无直接 F1 | sklearn 的 f1-score 宏平均 |
我做对照时的做法是把同一批预测结果分别喂给 bxb 和 sklearn,两边数字差超过 1 个百分点就停下来查对齐问题——大多数情况下都是注释时间窗口没对齐,而不是算法本身变差了。从那以后,我每次在报告里写 ECG 分类准确率之前,都会强制走一遍「注释对齐 → 单位换算 → 类别映射 → 双工具交叉验证」这条流水线,把这四个最容易造假的地方过掉,再谈模型好坏。这套流程已经被我写进团队的验收清单,也希望能帮到你。
本文还有配套的精品资源,点击获取