news 2026/9/16 2:00:53

基于深度学习的心脏病诊断系统:数据集与运行说明

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于深度学习的心脏病诊断系统:数据集与运行说明

简介:基于深度学习的心脏病诊断系统完整项目包,面向医疗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_ms=2000, step_ms=500, fs=360): 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_ms=2000, step_ms=500, fs=360) print(wins.shape) # 期望输出类似 (67, 720)

这段代码里window_msstep_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_weight='balanced', classes=classes, y=y_train) print(weights) # 各类权重,数量少的那一类权重更大

compute_class_weightbalanced模式会自动按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_classes=3): super().__init__() self.conv = nn.Sequential( nn.Conv1d(1, 32, kernel_size=5, stride=1, padding=2), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(32, 64, kernel_size=5, stride=1, padding=2), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(64, 128, kernel_size=3, stride=1, padding=1), 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_sizepadding。心电的 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_classes=3, hidden_dim=64): super().__init__() self.cnn = nn.Sequential( nn.Conv1d(1, 32, kernel_size=7, padding=3), nn.ReLU(), nn.MaxPool1d(4), nn.Conv1d(32, 64, kernel_size=5, padding=2), nn.ReLU(), nn.MaxPool1d(4), ) self.gru = nn.GRU(input_size=64, hidden_size=hidden_dim, batch_first=True, bidirectional=True) 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 开始调,配合学习率同步调整。第二个是学习率,推荐用OneCycleLRCosineAnnealingLR,初始学习率可以设为 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_norm=1.0) optimizer.step() total_loss += loss.item() return total_loss / len(loader) model = ECGCRNN(num_classes=3).to(device) criterion = torch.nn.CrossEntropyLoss(weight=class_weights_tensor) optimizer = AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = OneCycleLR(optimizer, max_lr=1e-3, steps_per_epoch=len(train_loader), epochs=30)

clip_grad_norm_在这里非常重要。心电信号任务和 NLP 类似,RNN 或 GRU 可能因为个别异常样本产生很大的梯度,不裁剪很容易让损失变成 NaN。max_norm=1.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_classes=3) model.load_state_dict(torch.load('model.pt', map_location='cpu')) model.eval() LABELS = ['正常', '室性早搏', '房颤'] @app.route('/predict', methods=['POST']) def predict(): data = request.get_json() # 假设 data 中 signal 是长度为 720 的列表 signal = np.array(data['signal'], dtype=np.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, dim=1).squeeze() result = {LABELS[i]: round(prob[i].item(), 4) for i in range(len(LABELS))} return jsonify(result) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)

接口设计上有两个要点。第一,输入校验不能少,如果用户传入长度不一致的信号,直接爆维度错误不友好。可以提前判断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.md

README.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_splits=5) for train_idx, val_idx in gkf.split(X, groups=record_ids): print(f'train records: {np.unique(record_ids[train_idx]).size}, ' f'val records: {np.unique(record_ids[val_idx]).size}')

用这种分组切分后,模型的性能会明显下降,那是真实水平。还要检查每一折的类别分布,某些稀有类别可能只在特定记录里出现,如果某一折完全没有某个类别,整个模型在那一折会崩溃,需要报告每折的平均指标和标准差。

5.2 对抗验证:排查数据泄漏和分布漂移的实用技巧

对抗验证是一个很值得养成的习惯。做法是训练一个二分类器去区分训练集和测试集样本,如果它能轻松分开,说明训练集和测试集来自不同分布,或者存在泄漏。心电数据里最常见的问题是来自同一台采集设备或同一个医院的数据在测试集里隐藏了设备噪声特征,模型学会了识别设备而不是识别病理。对抗验证只需要把训练集样本贴上 0,测试集贴上 1,然后用一个简单逻辑回归或轻量 CNN 训练,看 AUC 是否接近 1。如果 AUC 大于 0.9,就要警惕你的系统在真实医院环境里会失效。

最后再提一个可落地的技巧:对训练好的模型做一次 Grad-CAM 或时序注意力可视化,把一个窗口的注意力权重画出来,叠加在原始心电信号上。如果模型高权重区域落在 T 波或 ST 段,说明它可能在学真正的心电特征;如果注意力集中在基线漂移或噪音频段,那就要重新检查预处理和模型输入。这个验证用 PyTorch 的 hook 实现并不复杂,关键是能在你给同事演示时多一个解释依据。诊断系统做到模型能跑只是开始,能让别人信服模型学到了什么,才算真正完成。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 1:59:58

树莓派OpenCV人脸识别实战:从环境搭建到门禁联动

简介&#xff1a;基于树莓派、OpenCV与Python搭建的人脸识别完整工程&#xff0c;为嵌入式视觉初学者、树莓派玩家以及计算机视觉开发者&#xff0c;提供一套在低成本设备上从人脸检测、特征提取、模型训练到实时识别的软硬件结合解决方案。资源共442个文件&#xff0c;压缩包约…

作者头像 李华
网站建设 2026/9/16 1:59:03

SSE浏览器端全解析:EventSource重连、解析与中止机制

老读者都知道&#xff0c;我写前端协议相关的系列已经写到第105篇了。这个系列里聊过HTTP/1.1、HTTP/2、WebSocket&#xff0c;也聊过gRPC-Web&#xff0c;今天终于轮到SSE。说实话&#xff0c;我挺早就想单独写一篇浏览器端的SSE&#xff0c;但一直觉得这东西"看起来简单…

作者头像 李华
网站建设 2026/9/16 1:58:55

PyVSR视频超分原理与工程实践:帧间建模、光流对齐与硬件调度

简介&#xff1a;本资源是一套基于Python实现的PyVSR视频超分辨率算法开源工程&#xff0c;面向数字图像处理、计算机视觉方向的学习者与开发者&#xff0c;解决低分辨率视频质量提升的实际问题&#xff0c;适用于视频增强、监控画质优化及边缘设备轻量超分等场景。压缩包共34个…

作者头像 李华
网站建设 2026/9/16 1:57:12

Rhino 3D入门攻略:从NURBS曲面核心逻辑到实战建模

很多人第一次听说Rhino 3D&#xff0c;是在工业设计或者建筑行业的朋友那里。但真正接触后你会发现&#xff0c;这软件根本不是“某个行业的专用工具”&#xff0c;而是一个能把脑子里那些不规则的、流畅的、异形的想法&#xff0c;直接变成可加工数据的建模平台。我最早拿它做…

作者头像 李华
网站建设 2026/9/16 1:57:08

STP生成树协议核心机制详解:从环路成因到RSTP快速收敛

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华