ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ECG心电信号分类实战:从.mat读取到随机森林评估全流程

ECG心电信号分类实战:从.mat读取到随机森林评估全流程 简介面向医学数据分析与生物医学信号处理学习者这份资源提供了一套基于Python和MATLAB的心电图ECG分类实现覆盖数据预处理如去噪、滤波、基线漂移校正、特征提取如RR间隔、QT间期等、分类模型构建与评估的完整流程适合需要动手实践心律失常、心肌梗塞等分类任务的初学者和研究人员。包内共825个文件其中.py和.m脚本分别对应Python和MATLAB的实现流程用于完成信号处理与模型训练.dat/.hea/.atr等文件为ECG原始信号及标注数据提供实验输入其余为WFDB工具、说明文档等辅助内容整体压缩后约6.25MB目录结构清晰便于按模块查阅。已有445人学习下载。资源既可用于理解ECG信号P波、QRS波群、T波形态分析与SVM、随机森林等机器学习方法的结合应用也可作为二次开发的基础工程帮助读者快速上手生物医学信号处理项目。1. 拿到 ecg_classification-master.zip 先别急着解压这个 ECG 分类包到底要解决什么看到 ecg_classification-master.zip 这个名字核心信息其实已经够清楚了这是一个 ECG 分类项目压缩包里大概率同时提供 Python 和 Matlab 两条实现路径数据文件多半是 .mat 格式。很多人拿到压缩包的第一反应是解压后直接找 main.py 或 run_demo.m但真正能落地的 ECG 分类流程恰恰是先把 .mat 里的数据结构读懂、把滤波和心拍分段做对再谈模型选型。这不是一个纯算法竞赛题而是一个“信号处理 分类模型”的工程任务。适合正在做心电方向毕设的学生也适合医疗算法岗的入门者目标都是同一件事把原始心电信号变成能判断异常类别的结果并且这个结果要可复现、可解释、能迁移到没有见过的新数据上。2. 从 .mat 到特征矩阵ECG 信号的读取、滤波与分段2.1 用 Python 读 .mat 心电数据scipy.io 与字段检查先解决数据入口。这类项目里的 .mat 文件通常是 Matlab 保存的结构体或矩阵里面可能同时包含原始信号、采样率、心拍标签或者患者编号。用 Python 读取时第一件事不是直接跑模型而是把顶层字段打印出来逐个检查。常见做法是用 scipy.io.loadmat它会把 .mat 文件解析成一个字典Matlab 写入的版本信息会放在以双下划线开头的键里剩下的才是我们真正要用的数据字段。import scipy.io as sio import numpy as np # 读取 ecg_classification 数据包中的 .mat 文件 data sio.loadmat(sample_ecg.mat) # 打印顶层字段确认变量名和维度 for key, value in data.items(): if not key.startswith(__): print(key, value.shape)这段代码的作用是“看一眼箱子里有什么”。loadmat 返回的字典里像 ecg、fs、label 这类字段才是信号和标签而header、version、globals是 Matlab 自动附加的元数据直接过滤掉。打印出来之后通常会看到 ecg 是 (1, N) 或 (N, 1) 的矩阵fs 是标量label 可能是 (M,) 的类别编号数组。确认字段名之后再取出信号并整理成一维数组。ecg data[ecg].squeeze() # 去掉 Matlab 矩阵中的单例维度 fs int(data[fs].squeeze()) # 采样率常见取值 250 Hz 或 360 Hz labels data[label].squeeze() # 每条样本对应的类别标签 print(采样率:, fs, 信号长度:, ecg.shape, 标签数:, labels.shape)这里的 .squeeze() 是个容易忽略但很重要的动作。Matlab 保存的一维数组在 Python 里往往是 (1, N) 的二维矩阵不压缩的话后面所有按一维信号写的滤波和 R 峰检测都会报维度错误。fs 转成 int 是因为 scipy 的滤波器函数要求采样率是数值类型如果是 numpy 数组部分接口会警觉地拒绝计算。采样率是后续一切时间窗换算的基础所以在读取阶段就固定成整数后面不会反复踩坑。如果 .mat 文件是 Matlab R2018a 之后默认保存的 v7.3 格式scipy.io.loadmat 会直接抛异常提示需要 HDF 读取器。这时候不要硬刚 scipy换成 h5py 读取即可具体做法我在第 5 章避坑部分展开。这里先记住一个原则任何 .mat 文件读进来第一步永远是打印字段和维度而不是直接画图或跑分类。2.2 滤波与 R 峰检测为什么第一步不是分类而是去噪原始心电信号直接送进分类器效果通常很一般原因不是分类器不行而是信号里混着三类噪声呼吸和电极移动造成的基线漂移、50/60Hz 工频干扰、肌肉收缩产生的肌电干扰。基线漂移主要集中在 0.5Hz 以下肌电干扰主要在高频段所以一个 0.5Hz 到 45Hz 的带通滤波器就能把大部分噪声挡在门外。这里建议用零相位滤波也就是 filtfilt 而不是 lfilter因为普通滤波会产生相位偏移QRS 波群的位置会被移动后面 R 峰检测就会误差累积。from scipy.signal import butter, filtfilt def bandpass(ecg, fs, low0.5, high45.0, order4): nyq fs / 2.0 b, a butter(order, [low / nyq, high / nyq], btypeband) return filtfilt(b, a, ecg, padlen150) # 对原始心电信号做带通滤波 ecg_f bandpass(ecg, fs, low0.5, high45.0)参数说明butter 的 order 取 4 是实践里比较稳的选择阶数太低滤波不干净阶数太高会引入数值不稳定low 和 high 分别对应带通的上下截止频率心电信号的有效能量基本集中在这个区间。filtfilt 的 padlen 参数控制边缘填充长度默认值偶尔会在信号首尾造成明显的振铃手动设成 150 个采样点能缓解一部分边缘效应。滤波之后信号首尾各一小段会失真后面的心拍分段要主动跳过这些位置。滤波之后做 R 峰检测。经典做法是 Pan-Tompkins 算法通过差分、平方和滑动积分定位 QRS 波群但在工程实现上先用一个带通滤波加自适应阈值再用 find_peaks 找局部极大值已经能在大多数公开心电数据上取得可接受的效果。关键是两个参数height 定幅度阈值distance 规定两个 R 峰之间的最小采样点数。from scipy.signal import find_peaks # 幅度归一化便于统一阈值 ecg_norm ecg_f / np.max(np.abs(ecg_f)) # 设置幅度阈值和最小 R-R 间隔 thr 0.6 min_rr int(0.25 * fs) # 250ms正常人最快心率约 240bpm peaks, props find_peaks(ecg_norm, heightthr, distancemin_rr) print(检测到 R 峰数量:, len(peaks))distance 参数是我最建议调的一个。如果不设置find_peaks 会把 T 波甚至噪声尖刺当成 R 峰因为 T 波幅度在某些导联上并不低。0.25 秒的最小间隔对应 240bpm 的心率上限正常心电数据不会超过这个值所以这个参数足够安全。height 用 0.6 是在归一化信号上的经验值如果数据噪声较大可以降到 0.4 再看检测数量是否合理。检测到 R 峰之后下一步就是以每个 R 峰为中心截取心拍片段。这是 ECG 分类里“一个样本一个心拍”的标准做法后面特征提取和分类模型都基于这些长度固定的片段。window int(0.5 * fs) # 每个心拍取 R 峰前后各 0.25 秒 half window // 2 segments, seg_labels [], [] for idx in peaks: if idx - half 0 or idx half len(ecg_f): continue seg ecg_f[idx - half: idx half] segments.append(seg) # 如果标签是以心拍为单位这里直接取对应标签 seg_labels.append(labels[idx]) # 根据实际标签结构调整 print(得到心拍片段数:, len(segments), 每个片段长度:, window)窗口长度取 0.5 秒是因为一个完整心拍从 P 波到 T 波结束大约持续 0.4 到 0.6 秒0.5 秒能覆盖主要形态同时不把相邻心拍卷进来。如果原始标签是按记录或者按时间段给的seg_labels 的取法需要对应调整这也是后面标签错位问题的源头之一。到这里数据就从一条连续心电信号变成了一个形状为 (N, window) 的样本矩阵下一步就是特征工程。3. Python 与 Matlab 双路实现特征工程与分类模型怎么选3.1 特征工程优先于模型哪些特征真正区分异常心拍心电图分类任务中模型的选择空间其实不大至少在第一版里随机森林和线性 SVM 足够跑通流程。真正决定精度上限的是特征工程。心拍特征大致可以分成三组时域特征描述心率变异性和节律规律形态学特征描述 QRS、ST 段、T 波的形状变化频域特征描述自主神经调节的频段能量分布。以下是我常用的特征清单按类别列出来特征组具体特征对应病理含义时域RR 间期均值、RR 间期标准差、RMSSD心率变异性下降常见于心衰、糖尿病神经病变形态学QRS 宽度、T 波幅度、ST 段偏移QRS 增宽提示束支传导阻滞ST 段偏移提示心肌缺血频域低频功率 LF、高频功率 HF、LF/HF反映自主神经张力某些心律失常中明显异常在代码层面我不建议直接把整段心拍信号摊平成一维向量喂给模型那样既没有利用医学先验又容易过拟合。更稳的做法是对每个心拍片段手动计算十几到几十个统计量。以下是一个简化版的特征提取函数def extract_features(seg, fs): feats [] # 时域 feats.append(np.mean(seg)) # 均值反映基线水平 feats.append(np.std(seg)) # 标准差反映整体波动幅度 feats.append(np.sum(np.diff(seg) ** 2)) # 一阶差分平方和和 RMSSD 同理 # 频域 f np.fft.rfft(seg) freqs np.fft.rfftfreq(len(seg), d1.0 / fs) power np.abs(f) ** 2 # LF 0.04~0.15Hz, HF 0.15~0.40Hz lf power[(freqs 0.04) (freqs 0.15)].sum() hf power[(freqs 0.15) (freqs 0.40)].sum() feats.append(lf) # 低频功率 feats.append(hf) # 高频功率 feats.append(lf / (hf 1e-8)) # LF/HF 比值 return np.array(feats)这个函数里有个细节值得注意lf/hf 的频段划分沿用了心率变异性分析里的经典定义但 ECG 分类任务中如果数据采样率较低比如 250Hz0.04Hz 到 0.4Hz 对应的频点数量其实很少特征会不稳定。遇到这种情况我一般会把频段放宽到 0.5Hz 到 2Hz 和 2Hz 到 10Hz按实际信号诊断效果来调而不是死守教科书定义。另一个细节是 hf 加了一个 1e-8 的小常数避免分子分母出现 0 导致溢出这是数值稳定性上很常见的小动作。特征提取完成后把每个心拍的特征拼接成一个大矩阵。这里我强烈建议顺手把特征名列表也保存下来后面做 Matlab 对照或者模型解释时要用。不少人在这里省了一步结果三个月后回来看代码完全不知道第 7 列特征是什么含义。3.2 先跑随机森林基线再决定要不要上深度学习模型选型上我的习惯是先用随机森林跑一个基线。原因有三它对特征尺度不敏感不用做标准化它能直接输出特征重要性帮我们判断哪些特征值得保留它在小样本心电数据上不容易过拟合。深度学习模型不是不能用而是应该放在随机森林之后作为第二版对比方案。from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split X np.array(features) # 形状 (N, n_features) y np.array(seg_labels) X_tr, X_te, y_tr, y_te train_test_split( X, y, test_size0.2, random_state42 ) clf RandomForestClassifier( n_estimators200, max_depth12, class_weightbalanced, random_state42 ) clf.fit(X_tr, y_tr) print(训练集准确率:, clf.score(X_tr, y_tr)) print(测试集准确率:, clf.score(X_te, y_te))参数说明n_estimators 取 200 是收敛速度和精度的折中超过 200 之后精度提升非常有限max_depth 限制为 12 是为了控制单棵树的复杂度防止在特征数不多的情况下过拟合训练集class_weightbalanced 是应对类别不均衡的关键参数正常心拍数量通常远多于异常心拍不加这个参数模型会倾向于把所有样本都判成多数类random_state 固定下来保证同一份数据每次跑出来的结果一致这是复现实验的基本要求。如果同一个压缩包里还提供了 Matlab 版本的实现我一般会在 Python 这边先确认特征矩阵没问题再用 Matlab 的 fitcensemble 复现一遍随机森林。这样做的价值是双重的一方面验证特征工程没有实现错误另一方面也方便团队里更熟悉 Matlab 的同事接手维护。% 在 Matlab 里用同一份特征矩阵做随机森林对比 Mdl fitcensemble(X_train, y_train, ... Method, Bag, ... NumLearningCycles, 200); y_pred predict(Mdl, X_test); acc sum(y_pred y_test) / numel(y_test); fprintf(Matlab 随机森林测试集准确率: %.4f\n, acc);Matlab 的 fitcensemble 需要 Statistics and Machine Learning Toolbox如果手头版本没有这个函数退而求其次可以用 fitctree 先跑一棵决策树找感觉或者用 fitcecoc 搭配 SVM 做多分类。其实关键点不在哪个工具箱而在于把 Python 和 Matlab 两侧的 X_train、y_train 完全对齐否则对比结论没有意义。我通常是让 Python 侧把特征矩阵导出成 CSVMatlab 侧用 readtable 读进来这样两侧用的数据保证一致。4. ECG 分类结果评估混淆矩阵、类别不均衡与交叉验证4.1 评估指标怎么定准确率在 ECG 分类里会骗人很多 ECG 分类项目的第一版评估都会犯同一个错误只看准确率。心电数据里正常心拍往往占 80% 甚至 90% 以上模型只要把全部样本判为正常准确率就能到 90% 上下但这在临床上毫无意义因为我们要找的恰恰是那 10% 的异常心拍。所以我评估模型时必看三样东西混淆矩阵、F1、召回率。from sklearn.metrics import confusion_matrix, classification_report y_pred clf.predict(X_te) print(混淆矩阵:) print(confusion_matrix(y_te, y_pred)) print() print(classification_report(y_te, y_pred))假设标签是 0 正常、1 房性早搏、2 室性早搏classification_report 会按类别分别输出精确率、召回率、F1。诊断类任务里我更关注每个类别的召回率因为漏掉一个异常心拍的代价通常比把正常心拍误判为异常更大。混淆矩阵的行是真实类别列是预测类别看矩阵时重点看对角线以外的数字集中在哪里——如果第 1 类和第 2 类大量互窜说明特征里缺少能区分它们的关键形态如果多数异常都被分到第 0 类说明类别权重或者阈值没有调好。这里还有一个容易被新手忽略的点在多分类 ECG 任务里直接用 macro avg 的 F1 作为总指标比准确率可靠得多。macro avg 把所有类别的 F1 平等对待不会因为正常类样本多就把数值拉高。如果项目目标是二分类比如正常 vs 异常那我会同时报告敏感性和特异性敏感性对应异常心拍的召回率特异性对应正常心拍的召回率这两个指标加在一起才算完整描述了模型的实际表现。4.2 交叉验证按患者分数据泄漏是 ECG 分类最大的翻车点随机切分训练集和测试集在 ECG 分类里是一个危险的默认行为。同一个患者的心拍之间高度相似如果同一个患者的某些心拍进了训练集另一些进了测试集模型相当于提前见过了“考试答案”测试精度会虚高。这种现象在论文里叫数据泄漏工程师口语里叫“假精度”。真正的评估方式是按患者分组做交叉验证确保测试集里的患者从未出现在训练集中。from sklearn.model_selection import GroupKFold # groups 数组和 X 的行一一对应记录每个心拍属于哪个患者 groups patient_ids # 需要从数据集中提前解析出来 gkf GroupKFold(n_splits5) fold_f1 [] for train_idx, test_idx in gkf.split(X, y, groups): clf RandomForestClassifier( n_estimators200, max_depth12, class_weightbalanced, random_state42 ) clf.fit(X[train_idx], y[train_idx]) fold_f1.append(f1_score(y[test_idx], clf.predict(X[test_idx]), averagemacro)) print(按患者分组的 5 折 F1:, np.mean(fold_f1))GroupKFold 是 sklearn 里的专门方案split 的第二个参数传 y第三个参数传 groups。模型训练和预测的代码和普通交叉验证完全一样只是划分索引的方式变了。关键点在 groups 数组的构建上patient_ids 必须和每一条样本对应而不是和每个 .mat 文件对应。如果数据包里一个 .mat 文件包含多个患者的数据需要先按文件内的记录边界把 patient_ids 拆出来这一步做错了整个交叉验证就白做了。按患者分组之后测试精度通常会比随机切分下降几个百分点这是正常的也是好事。它说明你看到的数字更接近真实临床场景来了一个新患者模型要能处理没见过的人。如果这个数字掉得特别多比如从 95% 掉到 60%那说明模型其实没有学到普遍的特征而是死记了患者个体差异这就是典型的过拟合迹象。我通常在论文或者汇报里都会明确标注“按患者分组”还是“随机切分”因为这两个数字的含义完全不同不标清楚很容易被同行质疑。5. ECG 分类常见坑与排查从数据泄漏到标签错位下面这几个坑基本是 ECG 分类项目里最容易翻车的地方每一条都是我实际踩过或者帮别人排查过的问题按“现象 → 原因 → 解决”的顺序写出来。5.1 v7.3 格式的 .mat 文件让 scipy.io 直接抛异常现象代码运行到 sio.loadmat(sample_ecg.mat) 时抛出 NotImplementedError提示 “Please use HDF reader for matlab v7.3 files”。原因Matlab 高版本在保存大数据时默认使用 v7.3 格式这种格式本质上是 HDF5scipy.io 不兼容。如果 .mat 文件是结构体加上多组心电信号很容易触发这个问题。解决改用 h5py 读取注意维度顺序。import h5py with h5py.File(sample_ecg.mat, r) as f: ecg np.array(f[ecg]).T # HDF5 读出来的维度常常是反的 fs int(np.array(f[fs]).squeeze())这里的 .T 是血泪经验。Matlab 按列主序存储矩阵h5py 读出来的数组形状经常和原始信号转置了如果不主动检查 shape后续滤波和 R 峰检测全部会跑偏。每次读取后先打印 shape再决定要不要转置。5.2 T 波被误检成 R 峰现象R 峰检测数量明显偏多心拍分段后很多片段其实是 T 波或者噪声特征矩阵和标签对不上。原因带通滤波不能完全消除 T 波的低频成分find_peaks 的 height 阈值设得太低或者没有设置最小间距。解决在 find_peaks 里加 distance0.25*fs同时在分段前画一张叠加图看一眼检测位置。R 峰应该在每个心拍的最高处如果发现每隔一个波峰就多检一个把 height 调高或者把 distance 从 0.25 秒改成 0.3 秒。5.3 手动 shuffle 导致标签和样本错位现象训练集精度特别高但测试集精度突然崩掉或者训练过程看起来一切正常最后混淆矩阵完全乱掉。原因自己写 shuffle 时只重排了信号数组没有同步重排标签数组导致样本和标签失去对应关系。这种错误很隐蔽因为不会报错只会让结果变得不可解释。解决不要手动 shuffle直接使用 sklearn 的 train_test_split 或者 GroupKFold它们会同时处理 X 和 y。如果确实需要自定义重排先给所有样本生成一个统一的索引数组用同一个索引去取 X 和 y。5.4 类别不均衡导致“高准确率、低召回”现象测试准确率接近 95%但异常类别的召回率只有 30%报告里 anomaly 类的 F1 惨不忍睹。原因正常心拍占绝大多数模型学到了“全部判为正常成本最低”的偷懒策略。准确率被多数类掩盖不能反映真实性能。解决在随机森林里加 class_weightbalanced让少数类获得更高权重如果还是不理想用 SMOTE 对少数类过采样但要注意先划分训练集和测试集再做重采样避免少数类样本同时出现在两边造成数据泄漏。5.5 Python 和 Matlab 的特征顺序不一致现象同样的特征矩阵Python 训练和 Matlab 训练两边精度差异很大而且无法互相复用模型。原因手动拼接特征时Python 侧列顺序和 Matlab 侧不一致导致相同位置的列含义完全不同。这类问题在跨语言协作时非常常见。解决统一用 CSV 作为中间格式特征矩阵和特征名列一起导出。import pandas as pd pd.DataFrame(X, columnsfeature_names).to_csv(features.csv, indexFalse)然后在 Matlab 里用 readtable(features.csv) 读入第一行就是列名。导出前检查 feature_names 的长度必须等于 X 的列数这能挡住大多数手滑。6. 两个让我少返工的收尾动作配置固定与结果导出6.1 把采样率、窗长、特征清单写进配置每次实验跑完环境变量、滤波器参数、窗口长度这些值都散落在代码里改一次跑一次时间一长根本记不住哪组参数对应哪个结果。我的习惯是实验开始前先写一份配置把读的哪个 .mat、采样率多少、窗长多少、特征名列表是什么、随机种子是什么全部放到一个 JSON 文件里。这样每一步都能追溯复现实验的成本降到最低。import json run_config { data_file: sample_ecg.mat, fs: fs, filter: [0.5, 45.0], window_s: 0.5, random_state: 42, feature_names: feature_names } with open(run_config.json, w, encodingutf-8) as fp: json.dump(run_config, fp, indent2, ensure_asciiFalse)6.2 训练完立刻导出模型与中间产物模型训练完成后不要只在内存里跑一次就关掉。把模型、特征矩阵、预测结果、评估报告全部导出到本地下次对比新方案时直接拿来当基线。Python 侧可以用 joblib 或 pickle 保存模型Matlab 侧用 save 函数保存 .mat 文件。这一步看起来简单但能省掉大量“同样的实验再跑一遍”的时间。我之前的习惯是每完成一个版本就导出一份带时间戳的目录里面放着配置、模型、评估图和特征 CSV三个月后同事问起来也能直接说清楚当时做了什么。这个习惯帮我避免了很多次返工。希望帮到你。本文还有配套的精品资源点击获取
返回列表