单导联心电信号心律失常分类:CNN特征融合方案与复现避坑指南

发布时间:2026/9/30 10:03:58
单导联心电信号心律失常分类:CNN特征融合方案与复现避坑指南 简介这份PDF文献面向生物医学工程、深度学习与心电信号处理方向的研究者及学生聚焦心律失常的自动分类问题提供了一套基于卷积神经网络特征提取与融合的完整方法参考。资源包内仅含1个PDF文件大小约1.21MB内容为正式期刊论文涵盖引言、方法、实验与结论等完整章节便于系统研读与引用。文中构建卷积神经网络对心电信号及QRS波群小波分量进行特征提取并将网络特征与人工提取的RR间期特征在全连接层融合输出层采用softmax完成心拍分类实验基于MIT-BIH数据库MIL L导联数据训练测试总体准确度达98.12%平均灵敏度87.32%平均阳性预测值90.37%。目前已有202人学习适合希望了解CNN在单导联心律失常识别中特征融合思路、复现实验流程或撰写相关论文的读者参考借鉴。1. 单导联心电信号做心律失常分类一份能跑通的 CNN 特征融合方案手里只有单导联心电数据却要做多类别心律失常分类这是很多可穿戴心电项目绕不开的坎。多导联数据在公开库里好找但落到实际硬件上能拿到的往往就是一路信号。这份来自天津医科大学生物医学工程与技术学院的研究给了一条挺务实的路子用卷积神经网络分别对原始心拍和 QRS 波群的小波分量做特征提取再把网络自动学到的特征和人工算出来的 RR 间期拼在一起送进全连接层融合最后用 softmax 分四类。数据用的是 MIT-BIH 心律失常数据库里的 MLII 导联报告总体准确度 98.12%平均灵敏度 87.32%平均阳性预测值 90.37%。它最值得看的地方不是那个准确率数字而是「不依赖复杂预处理、单导联就能跑、还带抗噪测试」这套组合拳——对做嵌入式心电筛查或者想复现基线的人来说省掉了不少玄学调参的功夫。2. 数据准备与心拍分割从 MIT-BIH 原始记录到 250 点定长样本2.1 为什么选 MLII 导联和 DS1/DS2 划分MIT-BIH 心律失常数据库包含 48 条记录每条 30 分钟采样率 360 Hz标注心拍约 11 万个。AAMI 标准把心拍分成 N正常、S室上性异位、V心室异位、F融合、Q未知五类其中 Q 类是因为信噪比太低没法准确标注的研究中直接排除只做 N、S、V、F 四分类。导联选择上MLII 也就是修正后的 II 导联是诊断心律失常最常用的单导联之一波形里 QRS 波群清晰适合做形态学特征提取。数据划分遵循的是 inter-patient 模式这点很关键。很多复现翻车就翻在这里如果按心拍随机划分训练测试集同一个患者的心拍会同时出现在两边准确率能虚高到 99% 以上但换个人就崩。研究去掉了 4 条使用心脏起搏器的记录102、104、107、217然后把剩下的分成 DS1 和 DS2 两个不含相同患者信息的数据集。DS1 包含 101、106、108、109、112、114、115、116、118、119、122、124、201、203、205、207、208、209、215、220、223、230 共 22 条记录作为全局训练集DS2 包含 100、103、105、111、113、117、121、123、200、202、210、212、213、214、219、221、222、228、231、232、233、234 共 22 条记录。DS2 的前 5 分钟作为个体训练集用来对全局模型做微调引入患者特异性剩下的数据作为测试集。这个做法符合 AAMI 标准也是这份研究能报出可信指标的前提。2.2 用 R 峰标注做心拍分割的代码实现心拍分割直接用了数据库里标注的 R 峰位置。为了覆盖一个完整的心拍周期取 R 峰前 100 个点、R 峰后 150 个点总共 250 个点作为一个心拍样本。360 Hz 采样率下250 个点大约对应 0.69 秒足够把 P 波、QRS 波群和 T 波都包进去。import numpy as np import wfdb def segment_beats(record_name, data_dir./mitdb): 从 MIT-BIH 记录中按 R 峰标注分割心拍 record_name: 记录编号字符串如 101 返回: beats 形状 (n_beats, 250), labels 形状 (n_beats,) record wfdb.rdrecord(f{data_dir}/{record_name}) annotation wfdb.rdann(f{data_dir}/{record_name}, atr) # MLII 导联通常是第 0 通道 signal record.p_signal[:, 0] r_peaks annotation.sample symbols annotation.symbol # AAMI 四分类映射 aami_map { N: N, L: N, R: N, e: N, j: N, A: S, a: S, J: S, S: S, V: V, E: V, F: F } beats, labels [], [] for r, sym in zip(r_peaks, symbols): if sym not in aami_map: continue # 边界检查避免越界 if r - 100 0 or r 150 len(signal): continue beat signal[r-100:r150] beats.append(beat) labels.append(aami_map[sym]) return np.array(beats), np.array(labels)这段代码有几个参数需要留意。r-100和r150是原文给定的窗口不要随意改成对称窗口因为 QRS 波群之后的信息对区分 V 类和 F 类有帮助后窗取长一点是有道理的。aami_map里把 L、R、e、j 都归到 N把 A、a、J、S 归到 S这是 AAMI 标准的常规做法不同文献在细节上可能有出入复现时以自己引用的标准为准。边界检查那一步不能省记录开头和结尾附近的 R 峰如果强行切窗会索引越界直接跳过是最省事的处理。2.3 RR 间期特征的计算RR 间期是两个相邻 R 峰之间的时间间隔对区分心律失常类型很有效。研究用了前 RR 间期和后 RR 间期两个值来表征时域信息计算公式是rrpre rcur - rpre和rrpost rpost - rcur其中 rcur 是当前 R 峰位置rpre 是前一个相邻 R 峰位置rpost 是后一个相邻 R 峰位置。def compute_rr_features(r_peaks, index): 计算第 index 个心拍的前后 RR 间期 返回: (rr_pre, rr_post)单位是采样点数 if index 0 or index len(r_peaks) - 1: return None # 首尾心拍没有完整的前后 RR rr_pre r_peaks[index] - r_peaks[index-1] rr_post r_peaks[index1] - r_peaks[index] return np.array([rr_pre, rr_post], dtypenp.float32)注意这里返回的是采样点数不是秒。360 Hz 下一个采样点约 2.78 毫秒。要不要转成秒对网络影响不大因为全连接层会自己学缩放但保持量纲一致很重要——如果一部分样本用秒、一部分用点数训练会出问题。首尾心拍没有完整的前后 RR直接丢弃这也是为什么最终样本数会比标注心拍数少一点。3. 网络结构拆解双路 CNN 加特征融合层的具体配置3.1 心拍波形特征提取路径网络分四个模块输入、特征提取、特征融合、分类。输入有三部分尺寸 250×1 的心拍信号、4 个尺寸 44×1 的小波特征、尺寸 2×1 的 RR 间期特征。心拍波形这条路径上先做卷积核 9×1 的一维卷积卷积核数量 32步长 1激活函数 ReLU。然后批量归一化再做最大池化池化步长 4用来减少特征数、降低计算量。接着再做一次 9×1 卷积卷积核数量 16步长 1同样批量归一化加 ReLU 激活。为了防止过拟合加了一个 Dropout 层丢弃率 20%。最后 Flatten 展平得到尺寸 45×16 的特征向量。这里有个细节值得说第一次卷积用 32 个核第二次反而降到 16 个核。常规 CNN 设计里通道数通常是递增的但这里反着来。原因在于输入是一维心电信号250 个点本身信息密度不高第一层多给点核是为了充分捕捉不同形态的局部波形第二层减核是为了压缩、抽象配合后面的全连接融合避免参数量膨胀。复现时如果发现过拟合严重可以把第二层核数再降或者加大 Dropout但别轻易把第一层核数砍到 16 以下否则 QRS 波群的形态差异可能学不出来。3.2 小波分量特征提取路径QRS 波群包含的信息最多研究以 R 峰为中心选取 44 个点表示 QRS 波群用 db4 小波做离散小波变换分解成四个尺度D2、D3、D4、A4然后重建每个尺度的小波分量。这四个分量分别送进卷积层卷积核大小 9×1卷积核数量 4步长 1ReLU 激活然后平均池化池化参数 2最后 Flatten每个分量得到 4×4 的特征向量四个分量合起来是 22 个 4×4 的特征向量。import pywt def extract_wavelet_features(signal, r_peak, level4, waveletdb4): 提取 QRS 波群的四个小波分量 signal: 完整心电信号 r_peak: 当前 R 峰位置 返回: 四个分量的列表每个长度 44 # 以 R 峰为中心取 44 个点 if r_peak - 22 0 or r_peak 22 len(signal): return None qrs signal[r_peak-22:r_peak22] # db4 小波分解 coeffs pywt.wavedec(qrs, wavelet, levellevel) # coeffs 结构: [A4, D4, D3, D2, D1] # 取 A4, D4, D3, D2 四个分量并重建到原长度 components [] for i in range(1, level1): # 构造只保留第 i 层细节的系数 c [np.zeros_like(coeffs[j]) for j in range(len(coeffs))] c[i] coeffs[i] rec pywt.waverec(c, wavelet) # 重建后长度可能略有偏差裁剪或填充到 44 rec rec[:44] if len(rec) 44 else np.pad(rec, (0, 44-len(rec))) components.append(rec) # A4 分量单独处理 c [np.zeros_like(coeffs[j]) for j in range(len(coeffs))] c[0] coeffs[0] rec_a4 pywt.waverec(c, wavelet) rec_a4 rec_a4[:44] if len(rec_a4) 44 else np.pad(rec_a4, (0, 44-len(rec_a4))) components.append(rec_a4) return np.array(components) # 形状 (4, 44)小波分解的层数和基函数选择会影响结果。原文用的是 db4、四层分解取 D2、D3、D4、A4。pywt.wavedec返回的系数顺序是[A4, D4, D3, D2, D1]索引 0 是近似分量1 到 4 是细节分量。重建时每次只保留一个分量、其余置零再waverec回去这样得到的就是该尺度下的时域波形。重建后长度可能和原始 44 点有出入裁剪或补零到 44 是常规操作不影响后续卷积。3.3 融合层与分类输出三路特征——心拍波形特征 45×16、小波特征 22 个 4×4、RR 间期特征 2×1——全部拼接后送进全连接层。全连接层有 20 个神经元ReLU 激活最后输出层 4 个神经元对应 N、S、V、F 四类softmax 分类。训练用交叉熵损失函数Adam 优化器更新权重和偏置。停止条件是 20 次迭代内损失值不再下降保存准确率最高的模型。这个早停策略比较宽松20 次 patience 在几百个 epoch 的训练里能容忍一定的波动但如果数据集小、batch 也小损失曲线抖动大可能会过早触发。实际复现时建议同时监控验证集准确率损失不降但准确率还在涨的情况并不少见。模块层类型核数量核尺寸步长激活Dropout心电信号Conv1D329×11ReLU-心电信号MaxPool--4--心电信号Conv1D169×11ReLU-心电信号Dropout----20%小波分量Conv1D49×11ReLU-小波分量AveragePool--2--融合Concatenate-----融合Fully connected20--ReLU-分类Output4--softmax-4. 训练、评估与抗噪测试指标怎么算、噪声怎么加4.1 评价指标的计算方式研究选了准确度Acc、灵敏度Se和阳性预测值P三个指标。公式是Acc (TPTN)/(TPTNFPFN)Se TP/(TPFN)P TP/(TPFP)。TP 是真阳性FP 是假阳性TN 是真阴性FN 是假阴性。多分类场景下每个类别单独算 Se 和 P然后取平均得到平均灵敏度和平均阳性预测值。from sklearn.metrics import confusion_matrix def evaluate_metrics(y_true, y_pred, classes[N, S, V, F]): 计算每个类别的灵敏度、阳性预测值和总体准确度 cm confusion_matrix(y_true, y_pred, labelsclasses) total cm.sum() acc np.trace(cm) / total se_list, p_list [], [] for i, cls in enumerate(classes): tp cm[i, i] fn cm[i, :].sum() - tp fp cm[:, i].sum() - tp se tp / (tp fn) if (tp fn) 0 else 0 p tp / (tp fp) if (tp fp) 0 else 0 se_list.append(se) p_list.append(p) print(f{cls}: Se{se:.4f}, P{p:.4f}) print(fAcc{acc:.4f}, Mean Se{np.mean(se_list):.4f}, Mean P{np.mean(p_list):.4f}) return acc, np.mean(se_list), np.mean(p_list)混淆矩阵的labels参数要显式指定类别顺序否则 sklearn 会按字母序排N、S、V、F 碰巧是字母序但换成别的类别名就可能对不上。每个类别的 Se 和 P 单独算完再取平均不要用总体 TP 除以总体样本去算平均那是微平均和宏平均结果不一样原文报的是宏平均。4.2 特征融合前后的对比结果研究做了消融实验对比了四种输入组合。只用 CNN 对心拍做特征提取时F 类灵敏度只有 37.15%是明显的短板。加上小波分量后V 类灵敏度提升到 91.03%。心拍加 RR 间期特征时S 类灵敏度最好。三路特征全融合后总体准确度达到 98.12%各类心拍的灵敏度和阳性预测值都上来了。输入组合N SeS SeV SeF SeMean SeAcc心拍99.5770.2683.5837.1572.6496.67心拍小波99.6369.1191.0357.9979.4497.60心拍RR98.7583.9486.4178.3086.8597.21心拍小波RR99.5174.3095.2780.2187.3298.12这张表透露的信息比准确率本身更有用。F 类心拍是融合心拍形态介于正常和室性异位之间单靠波形特征很难区分所以只用心拍时 F 类灵敏度惨不忍睹。RR 间期特征对 F 类的提升最明显从 37.15% 拉到 78.30%说明节律信息对融合心拍的判别至关重要。小波特征则主要帮 V 类因为 QRS 波群的细节形态在心室异位时变化显著。三路融合不是简单堆叠而是各补各的短板。4.3 抗噪测试怎么做为了评估噪声影响研究向原始心电数据叠加了四种不同信噪比的高斯白噪声10 dB、20 dB、30 dB、40 dB另外还用 db8 小波对原始信号做去噪然后分别测试。def add_gaussian_noise(signal, snr_db): 向信号添加指定信噪比的高斯白噪声 signal_power np.mean(signal ** 2) noise_power signal_power / (10 ** (snr_db / 10)) noise np.random.normal(0, np.sqrt(noise_power), len(signal)) return signal noise信噪比计算用的是信号功率和噪声功率的比值10 ** (snr_db / 10)是把 dB 转成线性比例。加噪时要注意对整个记录加还是对单个心拍加原文是对原始心电数据加也就是在分割之前加这样更接近真实场景。如果对已经分割好的心拍逐个加噪边界处会出现不连续网络可能学到这种伪影。测试结果是10 dB 噪声下心拍波形严重失真但总体准确度仍有 95.69%。20 dB 及以上时准确度没有显著变化和原始信号差不多。去噪后的信号分类准确度最高达到 98.24%。这个抗噪能力有两个来源一是批量归一化层能消除一部分噪声影响二是研究没有对输入 CNN 的心电数据做去噪、归一化等预处理最大程度保留了信号完整性同时在包含噪声的情况下训练网络训练后的模型能在含噪信号中提取有效特征。5. 避坑与排查复现时最容易翻车的五个地方5.1 按心拍随机划分数据集导致指标虚高现象训练集准确率 99% 以上测试集也接近 99%但换个数据集或者用真实设备采集的数据一测就崩到七八十。原因同一个患者的心拍被随机分到了训练集和测试集网络记住了这个患者的波形特征而不是学到通用的心律失常判别模式。MIT-BIH 里不同患者的波形差异很大同患者内部的心拍相似度极高。解决严格按记录编号划分用 DS1 做训练、DS2 做测试确保两个集合没有重叠患者。如果自己的数据没有患者编号至少按记录段划分不要按心拍随机打乱。5.2 小波分量重建后长度对不上现象pywt.waverec重建出来的数组长度和原始 44 点不一致有时多几个点有时少几个点直接送进网络报维度错误。原因离散小波变换和重建过程中信号长度不是 2 的整数次幂时会有边界延拓重建长度和原始长度存在偏差。解决重建后统一裁剪或补零到 44 点。裁剪取前 44 个点补零在末尾补。这个操作对特征影响很小因为偏差通常只有一两个点而且集中在边界。5.3 RR 间期特征量纲不统一现象训练损失震荡不收敛或者收敛后准确率明显低于预期。原因RR 间期用采样点数表示时数值在几百而心拍信号经过归一化后在 0 到 1 之间小波分量也在小范围内全连接层融合时 RR 特征的量级压倒其他特征。解决对 RR 间期做标准化减去均值除以标准差或者直接除以采样率转成秒。原文没有明确说做了归一化但从训练稳定性考虑这一步建议加上。如果坚持用原始点数至少确保训练集和测试集用同一个缩放系数。5.4 Dropout 加在错误的位置现象加了 Dropout 后训练准确率上不去或者验证集表现反而变差。原因Dropout 加在了卷积层之后但池化之前或者加在了融合层之后。卷积层后的 Dropout 会随机丢弃特征图的通道对一维心电信号来说通道数本来就不多16 或 32丢 20% 可能把关键波形特征丢掉。解决按原文配置Dropout 加在心拍路径第二次卷积和激活之后、Flatten 之前。融合层和全连接层之后是否加 Dropout原文没提建议先不加过拟合明显时再考虑在全连接层后加一个小比例的。5.5 早停条件设置不当导致欠训练现象模型训练到一半就停了验证集准确率还在缓慢上升但损失已经 20 个 epoch 没降。原因早停只监控损失但损失和准确率的变化不完全同步。心电信号分类中损失可能因为个别难样本的预测概率微调而波动但整体准确率还在改善。解决同时监控验证集准确率和损失两个指标都满足 patience 条件才停。或者把 patience 从 20 加大到 30 到 40给模型更多时间。保存模型时按验证集准确率最高来存不要按损失最低存。6. 从论文到落地把模型塞进可穿戴设备的几个实操技巧论文报的是离线指标真要做成可穿戴设备上的筛查工具还有几件事得自己补。第一是模型压缩。原网络参数量不算大但全连接层 20 个神经元加上三路输入的拼接在单片机上跑仍然吃力。常见做法是把训练好的模型做量化权重从 float32 降到 int8精度损失通常在 1% 以内推理速度能翻倍。第二是 QRS 波群检测的集成。论文用的是数据库标注的 R 峰位置实际设备上没有标注得自己检测。Pan-Tompkins 算法是经典选择计算量小适合嵌入式。检测到的 R 峰位置直接喂给分割和 RR 间期计算模块整条链路就通了。第三是患者特异性微调的策略。论文里 DS2 前 5 分钟做个体训练集这个思路在设备上可以变成用户佩戴后先采集 5 分钟数据让医生或者半自动标注一部分对全局模型做几个 epoch 的微调然后再切换到实时分类。微调时学习率要调小通常是全局训练的十分之一否则会把全局学到的通用特征冲掉。验证模型有没有真正学到东西我一般会做两件事。一是看混淆矩阵F 类和 N 类之间的误判如果特别多说明 RR 间期特征没起作用回去检查 RR 计算是不是有 bug。二是拿一段完全没参与训练的记录手动加不同信噪比的噪声画准确率随信噪比变化的曲线。如果 10 dB 时准确率掉到 90% 以下说明抗噪能力没达标可能需要把训练数据也做加噪增强。有个习惯我每次复现心电分类项目都会走一遍先把数据划分打印出来确认训练集和测试集的记录编号没有交集再把每个类别的样本数统计一遍看看有没有类别严重不平衡最后跑一个只用心拍、不用任何融合特征的基线如果这个基线准确率就超过 97%那大概率是数据泄露了得回头查划分逻辑。这套流程帮我省过好几次后悔药。希望帮到你。本文还有配套的精品资源点击获取