
简介基于深度学习的心脏病诊断系统完整项目包面向医疗AI研究者、数据科学学习者以及需要完成课程设计/毕业设计的高校学生。项目基于TensorFlow、PyTorch等框架对Framingham Heart Study等公开数据集进行缺失值处理、异常值处理与特征工程实现心电图、心脏影像等生物医学数据的自动识别与分类可用于心脏病早期辅助诊断研究。压缩包共18个文件体积仅731KB包含5个CSV数据集克利夫兰、匈牙利、瑞士等中心处理后的数据、3个Python脚本数据预处理、模型训练与主程序、3个HTML/HTM前端展示页面以及训练好的随机森林模型、标准化器、项目介绍PPT和README运行说明。通过这份资料读者可以快速复现从数据清洗、特征工程、模型训练、交叉验证调优到Web界面展示的完整流程还能替换或扩展数据集迁移到其他疾病诊断场景。已有225人浏览学习是入门医疗人工智能、理解经典分类任务及模型部署细节的不错样例。1. 基于深度学习的心脏病诊断系统到底在解决什么问题心血管疾病长期占据全球致死原因前列而心电图ECG是筛查和诊断心律失常、心肌缺血最基础的手段。一个现实矛盾是门诊和体检产生的心电数据量巨大医生逐段判读既耗时又容易因疲劳产生漏判。深度学习正好踩在“数据量大、模式可学习、判别任务明确”这三个条件上。基于深度学习的心脏病诊断系统本质上就是一个“原始心电信号进、类别标签出”的分类或检测管道常见的做法是用卷积网络或循环网络提取波形特征把心跳分成正常、房颤、早搏等类别。标题里同时点出“内含数据集和运行说明”说明交付的是一套能直接复现的工程而不是论文里贴出来的结构图。这篇文章没有某个现成项目可以照着抄我会顺着一个一线工程师拿到这个标题后自己搭建这套系统时最常用、最稳的路径来讲从数据集选型、信号预处理、模型选型、训练调参到最终打包成带运行说明的可执行工程。新手可以按步骤跑通有经验的读者可以重点关注标签不平衡、数据泄漏和序列建模边界。2. 搭建心脏病诊断系统前先把数据集和预处理搞明白2.1 常见心电图数据集有哪些怎么选标题里“内含数据集”意味着发布者已经把数据打包好了但你自己要训练一个可用系统时还是要先明确数据来源。心电信号最常用的是 MIT-BIH 心律失常数据库它包含 48 条 30 分钟的双通道动态心电图记录采样率 360Hz标注分为正常搏动 N、左束支传导阻滞 L、右束支传导阻滞 R、室性早搏 V、房性早搏 A 等类别。这个数据集足够经典但年代久远信号质量和标注风格与现代医院设备有差异。其他常见选择包括 PTB 诊断性心电图数据库它更偏向心肌梗死和健康对照分类每条记录是 12 导联采样率 1000Hz以及 CinC Challenge 提出的房颤分类数据。对于大规模预训练可以关注 MIMIC-III 中提取的 ECG 波形子集不过那需要单独申请权限。选数据集时要看两点一是导联数和你真实落地场景是否一致二是标注体系里有没有“其他”类。很多诊断系统的准确率虚高就是因为把难以判别的样本都丢进“正常”类这在临床上是不可接受的。数据集导联数采样率典型任务使用限制MIT-BIH2360Hz心律失常分类公开需按记录切分PTB121000Hz心梗检测公开含健康对照CinC Challenge单导125Hz房颤检测已提取特征MIMIC-III Waveform多导125Hz 起重症监护心电需完成 CITI 认证我一般会建议优先用 MIT-BIH 做模型预研因为它类别均衡性相对可控而且国内外论文都基于它比较方便看自己的结果有没有跑偏。如果目标是心肌缺血类诊断就换 PTB。不要一上来就追求数据集最大心电图分类任务里数据质量远比数量重要导联脱落、基线漂移、工频干扰这些噪声会让模型学到完全错误的相关性。2.2 把原始心电信号切成可训练样本的关键步骤拿到原始记录之后最核心的预处理包括三件事重采样、去噪、切窗。其次还有归一化。重采样是为了统一输入尺寸不同数据集的采样率不一样模型第一层的输入维度必须固定。去噪常用带通滤波器保留 0.5–50Hz 频段因为心电主要在低频范围过高的工频干扰和肌电噪声反而会干扰学习。切窗方式决定了模型看到多长时间的“上下文”。对于单心跳分类可以基于 R 波位置截取每个心跳前后各 100ms也就是约 36 个样本点在一侧对于节律分类比如房颤就需要 5 秒甚至更长的窗口。下面是一段最小可用的切窗与归一化代码假设输入信号是signal采样率是fs我们按固定长度滑窗import numpy as np def sliding_window(signal, window_ms2000, step_ms500, fs360): window_len int(fs * window_ms / 1000) step_len int(fs * step_ms / 1000) windows [] start 0 while start window_len len(signal): seg signal[start:start window_len] windows.append(seg) start step_len return np.array(windows) # 示例把一段 36000 个点的信号切成 2 秒窗口步长 0.5 秒 raw_signal np.random.randn(36000) wins sliding_window(raw_signal, window_ms2000, step_ms500, fs360) print(wins.shape) # 期望输出类似 (67, 720)这段代码里window_ms和step_ms是最关键的两个参数。窗口越长模型能看到的波形周期越完整对室速、房颤这类持续节律异常更友好但会稀释单个心跳的局部特征。步长决定了数据增强的程度重叠越多样本数越多但相邻样本关联性也越强容易让训练集和验证集之间出现隐式重叠导致验证指标虚高。因此切窗之后必须按“记录级”划分数据不要让同一条记录出现在训练集和验证集两个集合里。归一化有很多种做法最常见的是 z-score也就是减均值除以标准差。注意不要对整段记录一次性计算均值和标准差而是对每个窗口独立计算或者用整个训练集的统计量固定下来。原因是测试阶段你不可能预知整条信号的长度标准化参数必须能从训练集里提前确定并保存。另外医生阅图时看的是波形形态而非绝对幅度所以幅度归一化不会损失诊断信息。2.3 标签不平衡是心脏病诊断里的第一个坑心电数据里“正常”样本往往占 90% 以上房颤或各类早搏比例很低直接训练会得到“把所有样本都判成正常”就能拿到很高准确率的模型但这对诊断系统毫无价值。处理标签不平衡常见做法有三种重采样、损失函数加权、合成样本。重采样又分过采样和欠采样过采样对少数类做复制或插值欠采样丢弃大量多数类数据但在心电这种时间序列上欠采样会丢失上下文容易让模型对少数类的记忆不稳定。我推荐先试损失函数加权因为它改动最小、不改变样本分布。PyTorch 里可以给CrossEntropyLoss传入weight参数权重通常取各类别样本数的倒数再归一化。下面是一个可复用的计算方式from sklearn.utils.class_weight import compute_class_weight y_train np.array([0, 1, 0, 0, 2, 1, 0]) # 0正常, 1早搏, 2房颤 classes np.array([0, 1, 2]) weights compute_class_weight(class_weightbalanced, classesclasses, yy_train) print(weights) # 各类权重数量少的那一类权重更大compute_class_weight的balanced模式会自动按n_samples / (n_classes * np.bincount(y))计算权重。拿到权重后传入损失函数再配合一个早停策略就能见效。更进阶的做法是使用 Focal Loss它除了考虑类别频率还能让模型更关注难分样本公式里需要调节gamma参数通常设为 2。注意如果数据集里存在大量标注错误加权会让模型在噪声少数类上更容易过拟合所以要先清洗标签再做不平衡处理。3. 基于深度学习的心脏病诊断模型从 CNN 到序列模型的选择3.1 一维卷积网络如何自动提取心电特征心电信号是一维时间序列所以卷积操作也要用一维卷积也就是nn.Conv1d。它的核心是把输入通道从原始导联数开始逐步通过多个卷积核学习局部形态特征比如 QRS 波的宽度、T 波的形态、ST 段的偏移。相比手工特征工程CNN 不需要你手动计算 RR 间期或斜率但它需要足够深的层和足够的样本才能拟合。下面是一个最小可运行的 CNN 分类器输入是单导联窗口长度为 720 个采样点import torch import torch.nn as nn class ECGCNN(nn.Module): def __init__(self, num_classes3): super().__init__() self.conv nn.Sequential( nn.Conv1d(1, 32, kernel_size5, stride1, padding2), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(32, 64, kernel_size5, stride1, padding2), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(64, 128, kernel_size3, stride1, padding1), nn.BatchNorm1d(128), nn.ReLU(), nn.AdaptiveAvgPool1d(1), ) self.fc nn.Linear(128, num_classes) def forward(self, x): # x.shape: (batch, 1, window_len) z self.conv(x) z z.view(z.size(0), -1) return self.fc(z)这段代码里有两个容易踩坑的参数kernel_size和padding。心电的 P 波、QRS 波、T 波都有不同持续时间比如 QRS 波通常持续 80–120ms在 360Hz 采样率下大约是 30–43 个点所以第一层 kernel_size 设为 5 或 7 能捕捉短时形态但如果你想识别 ST 段抬升这类持续几几百毫秒的特征就要加 dilation 或者增大 kernel_size。BatchNorm1d在输入样本数较少时要注意因为每个 batch 的均值方差估计不稳定如果 batch size 小于 16可以考虑去掉 BN或者使用 Layer Normalization。AdaptiveAvgPool1d(1)把序列长度压缩为 1这比直接展平后接全连接层更稳定因为它对输入窗口长度不敏感模型可以接受不同长度的输入。3.2 加 LSTM 还是用纯序列模型得看数据量CNN 感受野有限想覆盖整个 10 秒窗口需要很多层卷积这会增加参数和训练难度。LSTM 天然适合建模时间步之间的依赖关系但它有两个缺点训练慢、容易过拟合。一般在心电诊断系统里常见架构是 CNN 做前端特征提取LSTM 或 GRU 建模时序依赖最后接全连接层输出分类。这样的好处是 CNN 降维后 LSTM 处理的是高维语义特征而不是原始波形计算开销可控。下面给出一段结合了卷积和 GRU 的模型骨架import torch.nn as nn class ECGCRNN(nn.Module): def __init__(self, num_classes3, hidden_dim64): super().__init__() self.cnn nn.Sequential( nn.Conv1d(1, 32, kernel_size7, padding3), nn.ReLU(), nn.MaxPool1d(4), nn.Conv1d(32, 64, kernel_size5, padding2), nn.ReLU(), nn.MaxPool1d(4), ) self.gru nn.GRU(input_size64, hidden_sizehidden_dim, batch_firstTrue, bidirectionalTrue) self.fc nn.Linear(hidden_dim * 2, num_classes) def forward(self, x): # x: (batch, 1, time) z self.cnn(x) # (batch, 64, time/16) z z.transpose(1, 2) # (batch, time/16, 64) out, _ self.gru(z) # 双方向 GRU out out[:, -1, :] # 取最后时间步 return self.fc(out)这里MaxPool1d(4)会把时间长度每层缩小 4 倍两层后长度减少 16 倍。如果输入是 10 秒 360Hz 信号也就是 3600 个点经过两层池化后变成 225 个时间步交给 GRU 处理是很快的。如果你用纯 Transformer 替代 LSTM好处是能捕捉长距离依赖但心电数据量通常只有几万条Transformer 需要更多数据才能训好而且训练时间成倍增长。我的判断是单人开发或小团队做诊断系统CNN GRU 是性价比最高的组合当然你也可以用纯 CNN 作为基线再决定要不要加起来序列模型。3.3 训练脚本里必须调好的几个超参数训练心电模型和学习普通图像分类差别不大但有几个参数会直接影响诊断效果。第一个是 batch size信号分类任务里由于样本是长序列显存消耗比图像大。如果 batch size 太大会导致每个 batch 的类别分布不均衡加剧标签问题如果太小BN 不稳定。一般从 32 或 64 开始调配合学习率同步调整。第二个是学习率推荐用OneCycleLR或CosineAnnealingLR初始学习率可以设为 1e-3配合 AdamW 优化器权重衰减设 1e-4。下面是一段包含早停和模型保存的训练循环骨架import torch from torch.optim import AdamW from torch.optim.lr_scheduler import OneCycleLR def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss 0.0 for x, y in loader: x, y x.to(device), y.to(device) optimizer.zero_grad() logits model(x) loss criterion(logits, y) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() return total_loss / len(loader) model ECGCRNN(num_classes3).to(device) criterion torch.nn.CrossEntropyLoss(weightclass_weights_tensor) optimizer AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler OneCycleLR(optimizer, max_lr1e-3, steps_per_epochlen(train_loader), epochs30)clip_grad_norm_在这里非常重要。心电信号任务和 NLP 类似RNN 或 GRU 可能因为个别异常样本产生很大的梯度不裁剪很容易让损失变成 NaN。max_norm1.0是常见取值如果模型不稳定可以适当调到 0.5。早停一般监控验证集的 loss连续 5–10 个 epoch 不下降就停止。另一个容易被忽略的点是scheduler.step()的时机OneCycleLR 需要对每个 batch 调用而不是每个 epoch否则学习率曲线会错乱。4. 把训练好的模型封装成诊断系统并写清运行说明4.1 从 PyTorch 权重到推理服务的最小实现训练完成之后系统要真正能被“运行说明”指挥着跑起来。最小可用的推理服务常见做法是用 Flask 接收心电数据返回各类别概率。这比把整个训练脚本暴露给用户更安全也更容易部署。下面是一个简化到只保留核心逻辑的推理接口import torch import numpy as np from flask import Flask, request, jsonify app Flask(__name__) model ECGCRNN(num_classes3) model.load_state_dict(torch.load(model.pt, map_locationcpu)) model.eval() LABELS [正常, 室性早搏, 房颤] app.route(/predict, methods[POST]) def predict(): data request.get_json() # 假设 data 中 signal 是长度为 720 的列表 signal np.array(data[signal], dtypenp.float32) signal (signal - np.mean(signal)) / (np.std(signal) 1e-8) tensor torch.tensor(signal).unsqueeze(0).unsqueeze(0) # (1,1,720) with torch.no_grad(): logits model(tensor) prob torch.softmax(logits, dim1).squeeze() result {LABELS[i]: round(prob[i].item(), 4) for i in range(len(LABELS))} return jsonify(result) if __name__ __main__: app.run(host0.0.0.0, port5000)接口设计上有两个要点。第一输入校验不能少如果用户传入长度不一致的信号直接爆维度错误不友好。可以提前判断len(signal)是否等于输入长度不等就返回 400。第二标准化参数一定要和训练时保持一致。上面代码里用了当前请求的均值和标准差这是有偏差的正确做法是在训练时保存训练集的全局均值和标准差然后在推理时直接用。否则每个请求的标准化尺度不同同一段波形的输出会不稳定。4.2 运行说明.zip 里到底应该装什么标题明确写了“内含数据集和运行说明”作为一个交付工程zip 里通常需要包含以下几类内容数据文件或下载脚本、数据预处理模块、模型定义与权重、训练脚本、推理脚本、依赖清单、README 运行文档。README 不是写一段“这是一个深度学习项目”就完了而是要写清楚环境版本、每一步命令、每步大概会输出什么。下面是一个合理的目录结构heart_disease_system/ ├── data/ │ ├── raw/ # 原始数据集 │ ├── processed/ # 切窗后的样本 │ └── split/ # 按记录划分的 train/val/test 索引 ├── src/ │ ├── preprocess.py │ ├── model.py │ ├── train.py │ ├── evaluate.py │ └── serve.py ├── weights/ │ └── model.pt ├── requirements.txt └── README.mdREADME.md里需要明确写出 Python 版本比如 3.10 和 torch 2.x 的组合不要只写pip install -r requirements.txt要说明为什么不建议用 Python 3.7 或更高因为某些 CUDA 版本向下不兼容。另外如果数据集较大不建议放进 zip更常见的是在 README 里写一个 download 脚本注明来源和版权方便使用者自己去下载。这样做能避免让压缩包变成几百 MB。真正的“运行说明”一定要包含每个脚本的预期运行时间和硬件要求否则用户跑了一小时不确定是否卡死了。4.3 验证指标别只看准确率灵敏度与特异度才是关键诊断系统在临床语境里假阴性比假阳性危险得多。准确率在类别不平衡时会欺骗你比如 95% 正常样本全预测正常就有 95% 准确率。所以验证部分必须计算混淆矩阵、灵敏度召回率、特异度、F1 得分如果你做的是多类诊断还需要给出每一类的指标而不是只看宏观平均。下面是一段直接可用的评估代码from sklearn.metrics import classification_report, confusion_matrix import numpy as np val_preds [0, 1, 0, 0, 2, 1, 0] val_labels [0, 1, 0, 0, 2, 2, 0] print(confusion_matrix(val_labels, val_preds)) print(classification_report(val_labels, val_preds, target_names[正常, 早搏, 房颤]))上面只是示意真实评估时要用模型对验证集整体做一次预测而不是 batch 对齐。另一个容易被忽略的点是心电信号相邻窗口高度重叠如果你用滑窗方式制造样本同一个原始记录切出来的窗口会在时间上高度相关这会严重高估模型表现。最佳做法是验证集只用每段记录的后半部分或每隔几步取一个窗口也就是做时间解耦的验证。最后把torch.load出来的模型在 CPU 上测试推理时间如果单条信号推理时间超过 100ms在 Web 服务里并发十个人就会排队需要考虑 TensorRT 或 ONNX 导出。ONNX 导出一般只需一次torch.onnx.export但要注意动态输入长度心电模型通常输入长度固定反而简单。5. 现场实测前用一套基线流程快速验证模型没有白学5.1 用跨受试者 K 折代替随机 K 折很多人做心电分类时直接用train_test_split这会把同一条心电图记录的相邻窗口拆到训练集和验证集验证集指标会异常高。正确做法是让同一个受试者或同一条记录的所有窗口都只出现在同一折里。常见做法是按数据里的record_id分组使用GroupKFold。下面是一个用 sklearn 实现的切分方法from sklearn.model_selection import GroupKFold X np.arange(len(record_ids)) # 样本索引 gkf GroupKFold(n_splits5) for train_idx, val_idx in gkf.split(X, groupsrecord_ids): print(ftrain records: {np.unique(record_ids[train_idx]).size}, fval records: {np.unique(record_ids[val_idx]).size})用这种分组切分后模型的性能会明显下降那是真实水平。还要检查每一折的类别分布某些稀有类别可能只在特定记录里出现如果某一折完全没有某个类别整个模型在那一折会崩溃需要报告每折的平均指标和标准差。5.2 对抗验证排查数据泄漏和分布漂移的实用技巧对抗验证是一个很值得养成的习惯。做法是训练一个二分类器去区分训练集和测试集样本如果它能轻松分开说明训练集和测试集来自不同分布或者存在泄漏。心电数据里最常见的问题是来自同一台采集设备或同一个医院的数据在测试集里隐藏了设备噪声特征模型学会了识别设备而不是识别病理。对抗验证只需要把训练集样本贴上 0测试集贴上 1然后用一个简单逻辑回归或轻量 CNN 训练看 AUC 是否接近 1。如果 AUC 大于 0.9就要警惕你的系统在真实医院环境里会失效。最后再提一个可落地的技巧对训练好的模型做一次 Grad-CAM 或时序注意力可视化把一个窗口的注意力权重画出来叠加在原始心电信号上。如果模型高权重区域落在 T 波或 ST 段说明它可能在学真正的心电特征如果注意力集中在基线漂移或噪音频段那就要重新检查预处理和模型输入。这个验证用 PyTorch 的 hook 实现并不复杂关键是能在你给同事演示时多一个解释依据。诊断系统做到模型能跑只是开始能让别人信服模型学到了什么才算真正完成。本文还有配套的精品资源点击获取