简介:这份资源为12导联心电图(ECG)数据集,面向从事医疗信号处理、异常检测与深度学习研究的开发者及学生,可用于心律不齐识别、心率变异分析、病理分类等任务。压缩包共45个文件,以37个CSV数据文件为主,另含4个XML配置、1个train_label.csv标签文件及README说明,整体约10.75MB,数据按7:3划分为训练集与测试集,便于直接开展模型训练与泛化能力验证。数据集包含39732条心电图记录,覆盖肢体导联与胸导联的标准采集信息,适合卷积神经网络、循环神经网络等模型的自动特征提取与预测实验。目前已有4081人学习下载,读者可据此完成数据预处理、噪声去除、标准化与时间序列对齐,并围绕异常检测、分类任务与模型调优展开完整实践,是入门与进阶心电智能分析的实用素材。
1. 12导联心电图数据:从“拿到手”到“跑得动”之间隔着什么
12导联心电图数据是生理信号领域最容易被低估的一类数据。很多人第一次拿到它,以为不过是十二列浮点数加一个标签,真正动手才发现采样率不统一、导联顺序各家不同、单位有的写 mV 有的写 μV、长度从 2 秒到 10 分钟都有,标签还常常是诊断文本而不是干净的类别。这个标题背后要解决的核心问题,是把原始 12导联心电图数据整理成模型能直接吃的张量,并且让这套流程可复现、可迁移。它适合两类人:一类是想做心电分类、异常检测、节律识别的算法工程师,另一类是手里有一批心电图记录、想先跑通基线再谈优化的研发同学。下面按“先立住概念、再动手复现、最后讲坑”的顺序展开。
2. 12导联心电图数据的结构:导联、采样率与单位怎么对齐
2.1 12 个导联到底在记录什么
12导联不是 12 个独立传感器,而是 10 个电极通过不同组合算出来的 12 路信号。标准配置里,I、II、III 是双极肢体导联,aVR、aVL、aVF 是加压单极肢体导联,V1 到 V6 是胸前导联。理解这一点很关键,因为肢体导联之间存在线性关系,比如 III = II − I,aVR = −(I + II)/2。这意味着如果你把 12 路当成完全独立的特征送进模型,模型会花容量去学这些恒等式,属于浪费。
常见做法是保留全部 12 路,但在预处理阶段做一次导联一致性检查:随机抽几条记录,验证 III 是否约等于 II 减 I。如果偏差很大,说明数据在采集或导出环节出了问题,比如导联接错、增益不一致。我一般会写一个校验函数,把偏差超过阈值的记录单独标记出来,先不急着丢,人工看一眼再决定。
另一个容易忽略的点是导联顺序。不同设备导出的列顺序可能是 I、II、III、aVR、aVL、aVF、V1…V6,也可能是 V1…V6 在前。顺序错了,模型学到的就是错位的空间关系。落地时一定要在配置里显式写死导联顺序,并在加载时按名字重排,而不是按位置硬取。
2.2 采样率、时长与重采样策略
采样率直接决定你能看到什么。低采样率比如 100 Hz 或 250 Hz,对节律和形态的粗判够用;高采样率比如 500 Hz 或 1000 Hz,对 QRS 宽度、ST 段细微变化更友好。但高采样率意味着数据量和计算量成倍上升。我的经验是:如果任务以节律分类为主,250 Hz 是性价比很高的选择;如果要做精细的波形分割或间期测量,再上 500 Hz。
时长方面,单条记录常见的有 10 秒、30 秒、几分钟甚至 24 小时动态心电。10 秒片段适合做单次分类,长记录更适合做事件检测。把长记录切成固定长度窗口时,窗口长度要和任务匹配:检测早搏,2 到 5 秒窗口通常够;判断整体节律,10 秒更稳。切窗时建议保留一定重叠,避免事件正好落在边界被切碎。
重采样不要直接调库默认参数就完事。降采样前必须先做抗混叠滤波,否则高频噪声会折叠进低频,把波形搞脏。下面这段代码演示一个最小可用的加载与重采样流程,假设数据是每行一条记录、前 12 列是导联、最后一列是标签的 CSV。
import numpy as np from scipy.signal import resample_poly, butter, filtfilt LEAD_ORDER = ["I","II","III","aVR","aVL","aVF", "V1","V2","V3","V4","V5","V6"] def bandpass(x, fs, low=0.5, high=40.0, order=4): # 心电有效成分大致在 0.5~40 Hz,先带通再重采样 nyq = 0.5 * fs b, a = butter(order, [low/nyq, high/nyq], btype="band") return filtfilt(b, a, x, axis=0) def load_and_resample(signal, fs_in, fs_out): # signal: (n_samples, 12) x = bandpass(signal, fs_in) if fs_in != fs_out: # 用有理数重采样,避免直接插值带来的频率失真 from math import gcd g = gcd(int(fs_in), int(fs_out)) up, down = int(fs_out//g), int(fs_in//g) x = resample_poly(x, up, down, axis=0) return x.astype(np.float32)这段代码里,bandpass先做 0.5 到 40 Hz 的带通,把基线漂移和大部分高频噪声压掉;resample_poly用多项式重采样,比简单抽取稳定。参数上,low不要设成 0,否则基线漂移会留在信号里;high一般不超过采样率的一半,250 Hz 采样时 40 Hz 是安全的。如果数据本身已经滤波过,重复滤波可能让波形变钝,这时要先确认数据说明再决定。
2.3 单位与幅值归一化
单位混乱是 12导联心电图数据里最常见的“玄学”问题。有的数据以 mV 存储,数值范围大约在 −2 到 2;有的以 μV 存储,数值范围在 −2000 到 2000。如果不统一,模型第一层卷积的权重尺度会完全对不上。落地时先统计全局幅值的分位数,如果 99 分位在个位数,基本是 mV;如果在几百到几千,基本是 μV,除以 1000 转成 mV。
归一化方式也要统一。按记录做 z-score 会抹掉不同记录之间的幅值差异,对某些任务有利,但会破坏 ST 段抬高这类绝对幅值信息。我的做法是:如果任务依赖形态和间期,用固定缩放比如除以 1 mV 对应的数值;如果任务只关心相对波形,再用逐记录标准化。这个选择要在实验记录里写清楚,否则后面复现时很容易对不上。
3. 把 12导联心电图数据喂进模型:切窗、标签与数据加载
3.1 切窗与标签对齐
切窗的核心是让每个窗口都有明确的标签。对于整条记录一个标签的数据,切窗后所有窗口继承同一个标签,简单但会放大噪声窗口的影响。对于事件级标注的数据,比如某段时间是房颤、某段是正常,就要按时间区间给窗口打标签,并处理跨边界窗口。常见做法是:窗口与标注区间重叠超过 50% 才赋该标签,否则标为忽略,训练时不计算损失。
下面是一个按时间区间打标签的示例,假设标注是[(start_sec, end_sec, label), ...]。
def make_windows(signal, fs, win_sec=10, stride_sec=5, ann=None): win = int(win_sec * fs) stride = int(stride_sec * fs) windows, labels = [], [] for start in range(0, len(signal) - win + 1, stride): seg = signal[start:start+win] if ann is None: labels.append(None) else: t0, t1 = start/fs, (start+win)/fs best, best_ov = "ignore", 0.0 for a0, a1, lab in ann: ov = max(0, min(t1, a1) - max(t0, a0)) if ov > best_ov: best_ov, best = ov, lab # 重叠不足一半就忽略,避免边界噪声标签 labels.append(best if best_ov >= 0.5*win_sec else "ignore") windows.append(seg) return np.stack(windows), np.array(labels)win_sec和stride_sec是最需要调的两个参数。窗口太短,节律信息不足;太长,事件定位变糊。重叠 stride 小于 win 能增加样本量,但要注意同一记录切出的窗口不能同时出现在训练和验证集,否则会数据泄漏。
3.2 数据集划分要按记录而不是按窗口
这是血泪经验里排前三的坑。如果先把所有记录切成窗口再随机划分,同一条记录的不同窗口会同时进训练和验证,验证指标会虚高,上线就翻车。正确做法是按记录或按受试者划分,保证同一条记录只出现在一个集合里。如果数据里有受试者 ID,还要按受试者划分,避免同一受试者的不同记录跨集合。
划分比例上,小数据集常用 6:2:2 或 7:1.5:1.5。类别不平衡时,划分要分层,保证每个集合里各类别比例接近。如果某个类别样本极少,可以考虑在训练集里做重采样或加权损失,但验证集和测试集必须保持原始分布,否则指标没有参考意义。
3.3 用 PyTorch Dataset 封装加载流程
把前面的步骤封装成 Dataset,训练代码就干净了。下面是一个最小实现,重点在按记录划分和按导联顺序重排。
import torch from torch.utils.data import Dataset class ECGDataset(Dataset): def __init__(self, records, fs_out=250, win_sec=10, stride_sec=5): # records: list of dict, 每条含 signal(n,12), fs, ann, record_id self.samples = [] for r in records: x = load_and_resample(r["signal"], r["fs"], fs_out) # 按导联名重排,防止列顺序不一致 idx = [r["lead_names"].index(n) for n in LEAD_ORDER] x = x[:, idx] w, y = make_windows(x, fs_out, win_sec, stride_sec, r.get("ann")) for i in range(len(w)): self.samples.append((w[i], y[i], r["record_id"])) def __len__(self): return len(self.samples) def __getitem__(self, i): x, y, rid = self.samples[i] # 转成 (12, T),卷积网络常用通道在前 return torch.tensor(x.T, dtype=torch.float32), y, rid这里把信号转成(12, T),是因为一维卷积通常按通道维处理。record_id一并返回,方便在训练循环里做按记录分组统计。注意make_windows返回的标签可能是字符串"ignore",在 collate 阶段要转成索引或掩码,训练时跳过。
4. 12导联心电图数据常见坑:从单位到泄漏的排查清单
4.1 现象是 loss 不降,原因是单位没统一
现象:模型训练几个 epoch,loss 几乎不动,输出全偏向多数类。原因:一部分记录是 mV,一部分是 μV,数值尺度差 1000 倍,网络第一层就被大数值主导。解决:加载时统计每条记录的幅值分位数,按阈值判断单位并统一到 mV,再检查全局分布是否合理。
4.2 现象是验证指标高得离谱,原因是窗口级泄漏
现象:验证集准确率 0.98,测试集掉到 0.6。原因:先切窗再随机划分,同一条记录的窗口跨了训练和验证。解决:按记录或受试者划分,切窗在划分之后做,或者切窗时带上 record_id 并在划分时按 id 分组。
4.3 现象是波形看起来“糊”,原因是重采样前没抗混叠
现象:降采样后 QRS 波变宽、细节丢失。原因:直接抽取或插值,没有先做低通滤波。解决:降采样前先带通或低通到目标采样率的一半以下,再用多项式重采样。如果数据已经滤波,确认截止频率后再决定是否重复滤波。
4.4 现象是导联关系对不上,原因是列顺序或导联接错
现象:校验时 III 和 II 减 I 偏差很大。原因:列顺序与假设不一致,或采集时电极接错。解决:加载时按导联名重排,校验线性关系,偏差大的记录单独标记,不要直接混进训练集。
4.5 现象是标签噪声大,原因是诊断文本直接当类别
现象:同一类里波形差异极大,模型学不动。原因:原始标签是自由文本,直接映射成类别会引入歧义。解决:先做标签规范化,把同义诊断合并,模糊或冲突的样本标为忽略,必要时人工抽检一批确认映射规则。
5. 进阶技巧:用导联掩码做自监督预训练与快速验证
当标注数据有限时,12导联心电图数据本身的结构可以拿来做自监督。一个实用技巧是导联掩码:随机遮住若干导联,让模型从剩余导联重建被遮住的信号。这个任务不需要标签,能让模型学到导联之间的空间关系。实现上,把输入的部分导联置零,加一个掩码通道告诉模型哪些位置被遮,输出重建 12 路信号,损失只算被遮住的部分。
def lead_mask(x, mask_ratio=0.3): # x: (B, 12, T) B, C, T = x.shape mask = (torch.rand(B, C, 1, device=x.device) > mask_ratio).float() x_masked = x * mask return x_masked, mask # 训练时:重建被遮导联 x_masked, mask = lead_mask(x) recon = model(x_masked, mask) loss = ((recon - x) ** 2 * (1 - mask)).sum() / ((1 - mask).sum() + 1e-6)mask_ratio一般从 0.2 到 0.4 试起,太高会让任务过难,太低学不到东西。掩码要按导联整路遮,而不是按时间点随机遮,否则模型可能靠相邻时间点插值作弊,学不到导联间关系。预训练完成后,把编码器迁移到下游分类任务,通常比从头训练收敛更快,小样本下更明显。
验证方面,除了常规的准确率和 F1,建议加一个按记录分组的混淆矩阵,看看错误是否集中在某几条记录或某个受试者上。如果某条记录几乎全错,先回去查它的单位、导联顺序和标注,往往能发现数据问题而不是模型问题。另一个习惯是固定一个极小的子集做冒烟测试,每次改预处理都先在这个子集上跑通,确认波形和标签对齐后再上全量。这个习惯帮我省过很多次“训练半天才发现数据加载错了”的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取