ARTICLE DETAIL

资讯详情

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

基于机器学习的心电信号分类:从数据预处理到模型部署的完整流程

基于机器学习的心电信号分类:从数据预处理到模型部署的完整流程 简介这是一份面向生物医学工程、机器学习方向本科生与研究生的毕业论文参考资料聚焦心电信号自动分类这一交叉研究课题适合正在开展毕业设计或希望系统梳理该领域知识脉络的读者。压缩包内仅含1个PDF文件大小约8.34MB即完整论文正文涵盖绪论、心电基础知识及去噪、特征提取与分类、实验结果分析、结论等章节。论文从心电信号产生机理与P波、QRS波、T波等组成讲起依次展开小波阈值去噪、时频域特征提取、迁移学习分类以及支持向量机、随机森林、卷积神经网络等算法应用并结合PTB与MIT-BIH数据库说明实验设计。目前已有294人学习读者可借此获得一份结构完整、知识点覆盖全面的论文范本用于选题参考、方法对比与写作框架借鉴。1. 心电信号分类这篇毕业论文到底在解决什么真问题心电信号分类不是拿个数据集跑个准确率就完事。真正做过的人都知道心电图里那些 P 波、QRS 波群、T 波形态差异极小不同人的正常心拍长得都不一样同一个人的不同时段也有漂移。你要做的是让模型在噪声、基线漂移、个体差异三重干扰下把一段心电片段正确归到预设类别里——正常、房颤、室性早搏、房室传导阻滞或者更细的几十类心律失常。这篇毕业论文的标题里“基于机器学习”是方法限定“心电信号分类”是任务定义。它要回答的核心问题是在 MIT-BIH、PTB-XL、CPSC 2018 这类公开心电数据集上用传统机器学习或深度学习方案能不能做到临床可接受的分类精度以及整个流程怎么搭、参数怎么调、坑在哪。适合正在做毕业论文选题的本科生和研究生也适合刚进入心电算法方向的工程师。热搜词里“机器学习项目”“机器学习应用流程”“python机器学习入门”这些恰好对应了这篇论文的落地路径——从数据预处理到特征工程再到模型训练每一步都有可复现的操作。2. 心电信号从原始波形到可训练样本预处理与数据集选择2.1 为什么原始心电信号不能直接喂给模型拿到的心电信号通常是 360Hz 或 500Hz 采样的时间序列长度从几秒到几十秒不等。直接送进模型有三个致命问题第一基线漂移让整段信号的均值缓慢变化模型会学到“漂移方向”这种无关特征第二工频干扰和肌电噪声在 50Hz 附近叠加频谱上和 QRS 波群的高频分量重叠第三不同记录的长度不一致没法组成固定维度的输入矩阵。常见做法是先做带通滤波保留 0.5Hz 到 45Hz 的成分。0.5Hz 以下砍掉基线漂移45Hz 以上砍掉工频和大部分肌电噪声。然后做 R 波检测以每个 R 波为基准向前取 0.25 秒、向后取 0.4 秒切出固定长度的单心拍片段。这样每个样本就是 0.65 秒 × 采样率的定长向量。如果采样率是 360Hz一个心拍就是 234 个点。import numpy as np from scipy.signal import butter, filtfilt, find_peaks def bandpass_filter(signal, fs, low0.5, high45, order4): 对心电信号做带通滤波去除基线漂移和高频噪声 nyq fs / 2.0 b, a butter(order, [low / nyq, high / nyq], btypeband) return filtfilt(b, a, signal) def segment_beats(signal, fs, pre0.25, post0.4): 基于 R 波检测切分单心拍 # 先做差分平方增强 QRS 波群 diff np.diff(signal) squared diff ** 2 # 滑动窗口积分窗口约 0.15 秒 window int(0.15 * fs) integrated np.convolve(squared, np.ones(window) / window, modesame) # 自适应阈值检测 R 波 threshold np.mean(integrated) 2 * np.std(integrated) peaks, _ find_peaks(integrated, heightthreshold, distanceint(0.25 * fs)) # 以 R 波为基准切分 pre_samples int(pre * fs) post_samples int(post * fs) beats [] for p in peaks: if p - pre_samples 0 and p post_samples len(signal): beats.append(signal[p - pre_samples:p post_samples]) return np.array(beats)这段代码里butter的阶数选 4 是经验值阶数太高会引入相位失真阶数太低过渡带太宽。filtfilt做零相位滤波避免 R 波位置偏移。R 波检测用差分平方加滑动窗口积分这是 Pan-Tompkins 算法的简化版对大部分正常心拍够用。distance参数设为 0.25 秒因为生理上两次心拍间隔不会短于这个值。切分时向前 0.25 秒覆盖 P 波向后 0.4 秒覆盖 T 波这个窗口在文献里被反复验证过。2.2 数据集怎么选MIT-BIH、PTB-XL 和 CPSC 2018 的取舍选数据集直接决定论文的对比基线是否合理。MIT-BIH 是经典中的经典48 条记录360Hz 采样标注了 15 类心拍。它的优点是标注质量高、文献多、容易复现缺点是样本量小只有约 11 万个心拍而且部分记录有起搏器伪影。PTB-XL 更大21837 条 12 导联记录500Hz 采样71 种标注。它的优势是数据量大、导联多适合做多导联融合劣势是类别极不平衡某些罕见病只有几十条。CPSC 2018 是竞赛数据集6877 条记录500Hz9 类心律失常专门为分类任务设计评价指标明确。我一般会建议如果论文重点是方法创新用 MIT-BIH 做快速验证因为跑一轮只要几分钟如果重点是模型泛化能力用 PTB-XL 做跨患者划分如果要做端到端对比CPSC 2018 的官方划分最省事。注意不管用哪个数据集都必须按患者划分训练集和测试集不能随机打乱心拍。同一个患者的不同心拍同时出现在训练和测试里准确率会虚高十几个百分点这是血泪教训。from sklearn.model_selection import GroupShuffleSplit def patient_wise_split(beats, labels, patient_ids, test_size0.2): 按患者划分避免同一患者的心拍同时出现在训练和测试集 gss GroupShuffleSplit(n_splits1, test_sizetest_size, random_state42) train_idx, test_idx next(gss.split(beats, labels, groupspatient_ids)) return beats[train_idx], beats[test_idx], labels[train_idx], labels[test_idx]GroupShuffleSplit的groups参数传患者 ID保证同一患者的样本只出现在一边。random_state固定住否则每次跑出来的划分不一样论文里的数字没法复现。3. 特征工程与模型选型传统机器学习还是深度学习3.1 手工特征怎么提时域、频域和形态学三路并进传统机器学习方案的核心是特征工程。心电信号的特征大致分三路时域特征、频域特征和形态学特征。时域特征包括 RR 间期均值、标准差、相邻 RR 间期差值均方根这些反映心率变异性。频域特征对 RR 间期序列做功率谱密度估计提取低频功率、高频功率和低频高频比。形态学特征直接描述波形比如 QRS 波群宽度、R 波幅值、T 波幅值、P 波是否存在。import numpy as np from scipy.stats import skew, kurtosis def extract_time_features(beats, fs): 提取时域和形态学特征 features [] for beat in beats: # 时域统计量 mean_val np.mean(beat) std_val np.std(beat) skewness skew(beat) kurt kurtosis(beat) # 形态学R 波峰值和位置 r_peak np.max(beat) r_pos np.argmax(beat) / fs # QRS 宽度以 R 峰为中心幅值降到 20% 的宽度 threshold 0.2 * r_peak above np.where(beat threshold)[0] qrs_width (above[-1] - above[0]) / fs if len(above) 1 else 0 features.append([mean_val, std_val, skewness, kurt, r_peak, r_pos, qrs_width]) return np.array(features)skew和kurtosis描述波形对称性和尖峰程度房颤时 RR 间期绝对不规则这两个统计量会明显偏离正常。qrs_width用 20% 阈值法估算宽 QRS 波提示室性早搏或束支传导阻滞。这些特征加起来大概 20 到 30 维配合随机森林或 SVM 就能跑出不错的结果。3.2 一维 CNN 和 LSTM 的取舍什么时候该上深度学习如果手工特征做到头了准确率卡在 95% 上不去可以考虑一维 CNN。心电信号是典型的一维时序一维卷积核在时间轴上滑动自动学习局部形态特征。相比手工特征CNN 的优势是能捕捉到人眼难以描述的细微模式比如 QRS 波群的微小切迹。import torch import torch.nn as nn class ECG1DCNN(nn.Module): def __init__(self, num_classes5, input_len234): super().__init__() self.conv_block1 nn.Sequential( nn.Conv1d(1, 32, kernel_size5, padding2), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(2) ) self.conv_block2 nn.Sequential( nn.Conv1d(32, 64, kernel_size5, padding2), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(2) ) self.conv_block3 nn.Sequential( nn.Conv1d(64, 128, kernel_size3, padding1), nn.BatchNorm1d(128), nn.ReLU(), nn.AdaptiveAvgPool1d(1) ) self.fc nn.Linear(128, num_classes) def forward(self, x): # x shape: (batch, 1, input_len) x self.conv_block1(x) x self.conv_block2(x) x self.conv_block3(x) x x.squeeze(-1) return self.fc(x)三层卷积通道数从 32 到 64 到 128卷积核 5 和 3 交替。BatchNorm加速收敛AdaptiveAvgPool1d(1)把时间维度压成 1避免全连接层参数爆炸。输入长度 234 对应 360Hz 采样下 0.65 秒的片段。这个模型参数量不到 10 万在 MIT-BIH 上训练 50 个 epoch 就能收敛。如果要做长时依赖比如捕捉房颤的 RR 间期不规则性可以在 CNN 后面接一层 LSTM但注意 LSTM 训练慢小数据集上容易过拟合。注意一维 CNN 的输入需要归一化。我一般按每个心拍做 z-score 标准化即减去均值除以标准差。不要用全局均值和标准差因为不同患者的基线水平差异很大。4. 训练、评估与调参让论文里的数字站得住脚4.1 类别不平衡怎么处理重采样、加权和 focal loss心电数据集的类别不平衡是常态。MIT-BIH 里正常心拍占 90%房颤和室早加起来不到 10%。如果直接训练模型会把所有样本预测成正常准确率也有 90%但敏感度为零。常见做法有三种对少数类过采样、对多数类欠采样、在损失函数里给少数类更高权重。from torch.utils.data import WeightedRandomSampler import numpy as np def make_weighted_sampler(labels): 根据类别频率构造加权采样器 class_counts np.bincount(labels) class_weights 1.0 / class_counts sample_weights class_weights[labels] sampler WeightedRandomSampler( weightstorch.DoubleTensor(sample_weights), num_sampleslen(labels), replacementTrue ) return samplerWeightedRandomSampler让少数类样本在每个 batch 里出现的概率更高。replacementTrue表示有放回采样保证每个 epoch 看到的样本数不变。如果不想改采样策略可以在 CrossEntropyLoss 里传weight参数把少数类的权重设成多数类的 5 到 10 倍。Focal loss 是另一种选择通过调制因子降低易分类样本的损失贡献但参数调起来更玄学毕业论文里用加权交叉熵就够。4.2 评估指标不能只看准确率敏感度、特异度和 F1 的取舍论文里如果只报准确率审稿人大概率会质疑。心电分类的标准指标是敏感度、特异度、阳性预测值和 F1 分数。敏感度衡量漏诊率特异度衡量误诊率F1 是精确率和召回率的调和平均。对于房颤检测敏感度比特异度更重要因为漏掉一个房颤患者可能导致中风。对于室性早搏特异度更关键因为误报会引起不必要的焦虑。from sklearn.metrics import classification_report, confusion_matrix def evaluate_model(y_true, y_pred, class_names): 输出每个类别的敏感度、特异度和 F1 print(classification_report(y_true, y_pred, target_namesclass_names, digits4)) cm confusion_matrix(y_true, y_pred) print(Confusion Matrix:) print(cm) # 计算每个类别的敏感度和特异度 for i, name in enumerate(class_names): tp cm[i, i] fn cm[i, :].sum() - tp fp cm[:, i].sum() - tp tn cm.sum() - tp - fn - fp sensitivity tp / (tp fn) if (tp fn) 0 else 0 specificity tn / (tn fp) if (tn fp) 0 else 0 print(f{name}: Sensitivity{sensitivity:.4f}, Specificity{specificity:.4f})classification_report直接给出每个类别的精确率、召回率和 F1。混淆矩阵看哪些类别容易混比如室上速和窦性心动过速在形态上接近混淆矩阵里这两类的非对角线元素会偏高。敏感度和特异度手动算一遍写论文时直接引用。4.3 超参数怎么调学习率、batch size 和卷积核数量的组合深度学习调参没有银弹但心电分类有几个经验区间。学习率从 1e-3 开始用余弦退火降到 1e-5。batch size 设 64 或 128太小梯度噪声大太大泛化差。卷积核数量从 32 起步每层翻倍但不要超过 256否则小数据集上过拟合严重。Dropout 加在卷积层后面概率 0.3 到 0.5。from torch.optim import Adam from torch.optim.lr_scheduler import CosineAnnealingLR model ECG1DCNN(num_classes5) optimizer Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max50, eta_min1e-5) criterion nn.CrossEntropyLoss(weightclass_weights)weight_decay设 1e-4 做 L2 正则防止权重过大。CosineAnnealingLR的T_max等于总 epoch 数eta_min是学习率下限。class_weights从训练集类别频率算出来传给损失函数。这套配置在 MIT-BIH 上跑 50 个 epoch验证集 F1 能到 0.98 左右。5. 避坑与排查心电分类论文里最容易翻车的五个地方5.1 数据泄漏同一患者的心拍同时出现在训练和测试集现象验证集准确率 99%测试集准确率 99%但换一个数据集或者换一批患者准确率掉到 70%。原因随机划分心拍时同一个患者的不同心拍被分到了训练和测试两边。模型记住了这个患者的波形特征而不是学到通用的分类模式。解决用GroupShuffleSplit按患者 ID 划分确保同一患者的样本只出现在一边。如果数据集本身没有患者 ID用记录编号代替。5.2 滤波参数不当QRS 波群被削平或基线漂移没去干净现象滤波后 QRS 波群幅值明显变小或者波形整体上下浮动。原因带通滤波的低频截止设得太高比如 1Hz会削掉 QRS 波群的低频成分高频截止设得太低比如 20Hz会削掉 QRS 波群的尖峰。解决低频截止 0.5Hz高频截止 45Hz阶数 4。滤波后画几段波形对比肉眼确认 QRS 波群形态没变形。5.3 类别不平衡导致模型只预测多数类现象训练 loss 一直在降但验证集上少数类的 F1 是 0。原因多数类样本太多模型学到“全部预测成正常”就能拿到低 loss。解决用加权采样或加权损失函数。检查方法是打印每个 batch 的类别分布如果某个类别的样本数长期为 0说明采样器没生效。5.4 过拟合训练集 F1 1.0验证集 F1 0.85现象训练集指标远高于验证集且差距随 epoch 增大。原因模型参数量相对于数据量太大或者训练轮数太多。解决加 Dropout、加 L2 正则、减少卷积核数量、早停。早停的 patience 设 10 到 15 个 epoch验证集 loss 连续不降就停。5.5 评估指标选错用准确率掩盖了敏感度不足现象论文里准确率 98%但审稿人问“房颤的敏感度是多少”答不上来。原因准确率在不平衡数据集上有欺骗性。解决报每个类别的敏感度、特异度和 F1画混淆矩阵。如果某个类别的敏感度低于 0.9在论文里要专门讨论原因和改进方向。6. 从论文到落地把心电分类模型部署成可调用的推理服务论文写完了模型训练好了下一步是让它能跑起来。我一般会把 PyTorch 模型导出成 ONNX 格式然后用 FastAPI 包一层 HTTP 接口。这样前端或者移动端可以直接发一段心电信号过来返回分类结果。import torch import onnx import onnxruntime as ort import numpy as np from fastapi import FastAPI, UploadFile import io # 导出 ONNX model ECG1DCNN(num_classes5) model.load_state_dict(torch.load(ecg_cnn.pth, map_locationcpu)) model.eval() dummy_input torch.randn(1, 1, 234) torch.onnx.export(model, dummy_input, ecg_cnn.onnx, input_names[signal], output_names[logits], dynamic_axes{signal: {0: batch}}) # 推理服务 app FastAPI() session ort.InferenceSession(ecg_cnn.onnx) app.post(/predict) async def predict(file: UploadFile): content await file.read() signal np.frombuffer(content, dtypenp.float32).reshape(1, 1, -1) # 做同样的预处理滤波、切分、归一化 # 这里省略预处理步骤假设输入已经是切分好的单心拍 logits session.run(None, {signal: signal})[0] pred int(np.argmax(logits, axis1)[0]) prob float(np.max(torch.softmax(torch.tensor(logits), dim1).numpy())) return {class: pred, confidence: prob}torch.onnx.export把模型转成 ONNXdynamic_axes让 batch 维度可变。onnxruntime做推理比 PyTorch 原生快 2 到 3 倍而且不依赖 PyTorch 环境。FastAPI 的UploadFile接收二进制信号数据预处理步骤要和训练时完全一致否则推理结果会偏。置信度低于 0.7 的样本建议转人工复核这在临床场景里是后悔药。验证方法很简单拿测试集里的样本先走一遍训练时的预处理再发到接口对比接口返回的类别和真实标签。如果一致率低于 95%检查预处理是否对齐。我习惯在预处理函数里加断言确保输入长度和训练时一致。最后说一个我踩过的坑ONNX 导出时如果模型里有AdaptiveAvgPool1d某些版本的 ONNX 算子集不支持需要把 opset 设成 11 以上。另外推理服务的输入归一化参数要从训练集统计出来存成 JSON 文件服务启动时加载不要硬编码。这个方案值不值得做如果你只是写毕业论文训练完模型跑出指标就够了如果你想让论文有落地价值或者后续想做成演示系统花半天时间搭个推理接口答辩时演示效果会好很多。希望帮到你。本文还有配套的精品资源点击获取
返回列表