新闻详情

新闻详情

首页 / 资讯中心 / 详情

基于CNN的心电信号特征提取与融合:心律失常分类实战

发布时间:2026/9/30 18:33:28来源:尧图网络
基于CNN的心电信号特征提取与融合:心律失常分类实战
简介这份PDF文献面向生物医学工程、深度学习与心电信号处理方向的研究者及学生聚焦心律失常的自动分类问题。资源为单篇论文共1个PDF文件压缩包约1.21MB内容涵盖卷积神经网络对心电信号与QRS波群小波分量的特征提取并将网络特征与人工提取的RR间期特征在全连接层融合最终经softmax输出完成心拍分类。文中基于MIT-BIH心律失常数据库MIL L导联数据训练测试给出总体分类准确度98.12%、平均灵敏度87.32%、平均阳性预测值90.37%等量化结果并讨论了单导联、特征提取与融合、计算机辅助诊断等关键环节。读者可借此理解CNN自动学习特征替代人工设计的思路掌握多源特征融合提升泛化能力的建模方法并获取可复现的实验指标与评估框架。目前已有202人学习适合作为心律失常分类课题的入门参考与方案对照。1. 从一段心电信号说起为什么心律失常分类值得用 CNN 做特征提取与融合拿到一段 30 秒的 Holter 心电记录里面可能混着基线漂移、工频干扰、肌电噪声而真正决定诊断的那几个异常搏动往往只占几百毫秒。传统做法是先做 R 波检测、再手工算 RR 间期、QRS 宽度、形态学描述子最后丢给 SVM 或随机森林。这套流程在干净数据上能跑但一旦导联位置偏移、患者个体差异大手工特征就开始集体翻车。卷积神经网络特征提取与融合这套思路本质上是把「特征长什么样」这件事交给网络自己学再把不同尺度、不同导联、不同模态的特征拼在一起做心律失常分类。它适合两类人一类是手里有 MIT-BIH、CPSC、PTB 这类公开心电数据集想跑一个能复现的 baseline另一类是做可穿戴心电设备需要在有限算力下把分类精度再往上顶几个点。这篇笔记就按「信号怎么进网络 → 特征怎么提 → 特征怎么融 → 坑在哪」的顺序把这条链路拆开讲清楚。2. 心电信号进 CNN 之前预处理、分段与标签对齐2.1 为什么原始心电不能直接喂给卷积网络心电信号采样率常见 360 HzMIT-BIH或 500 HzCPSC单条记录长度从几十秒到几十分钟不等。直接把整段信号塞进 CNN会碰到三个问题一是长度不固定全连接层没法接二是不同记录幅值差异大有的导联 mV 级有的被噪声淹没三是心律失常事件是局部现象整段信号里正常搏动占绝大多数直接整段分类会被正常类主导。常见做法是切成固定长度的窗口。窗口长度一般取 2 到 10 秒或者按单个心拍切。按心拍切需要先做 R 波检测常用 Pan-Tompkins 或其变体按固定窗口切则不需要检测但标签对齐要小心。我一般会先做 0.5–40 Hz 带通滤波去基线漂移和高频噪声再做 z-score 归一化让每条记录零均值单位方差。import numpy as np from scipy.signal import butter, filtfilt def bandpass_filter(signal, fs360, low0.5, high40, order4): # 心电有效频带大致在 0.5-40 Hz超出部分多为噪声 nyq 0.5 * fs b, a butter(order, [low / nyq, high / nyq], btypeband) return filtfilt(b, a, signal) def zscore_normalize(signal): # 逐记录归一化避免不同患者幅值差异影响网络 return (signal - np.mean(signal)) / (np.std(signal) 1e-8) def segment_signal(signal, fs360, win_sec5, stride_sec2): # 滑动窗口切分win_sec 窗口长度stride_sec 步长 win int(fs * win_sec) stride int(fs * stride_sec) segments [] for start in range(0, len(signal) - win 1, stride): segments.append(signal[start:start win]) return np.array(segments)这段代码里low0.5和high40是心电处理的常规边界低于 0.5 Hz 的基线漂移会被滤掉高于 40 Hz 的肌电和工频残留也会被压下去。filtfilt做零相位滤波避免滤波本身引入时间偏移。win_sec5和stride_sec2是经验值窗口太短会丢上下文太长则一个窗口里混多个心拍类型标签不好打。如果做的是单心拍分类窗口可以缩到 0.6–1 秒步长等于窗口长度不重叠。2.2 标签对齐AAMI 五类还是原始类别MIT-BIH 的标注是逐拍的符号有 N、L、R、V、A、F、j 等十几种。直接拿原始符号做多分类类别极不平衡而且很多类样本极少。AAMI EC57 标准把它归成五类N正常及其他、S室上性异位、V室性异位、F融合搏动、Q未知搏动。做心律失常分类时我建议先按 AAMI 五类归并再根据任务需要决定是否合并 Q 类。标签对齐的关键是如果按固定窗口切窗口内可能包含多个心拍标签取窗口中心位置对应的心拍标签或者取窗口内多数心拍的标签。如果按心拍切就以 R 波位置为中心前后各取固定长度。两种方式没有绝对优劣按心拍切更干净但依赖 R 波检测精度按窗口切更鲁棒但标签噪声更大。提示MIT-BIH 里 102、104、107、217 这四条记录常被用作测试集因为包含较多室性异位搏动能检验模型对少数类的识别能力。3. 卷积神经网络特征提取从一维卷积到多尺度特征图3.1 一维 CNN 提取心电形态特征的原理心电信号是一维时间序列用一维卷积核在时间轴上滑动每个卷积核学到一个局部波形模式。浅层卷积核感受野小学到的是 QRS 波上升沿、T 波下降沿这类短时形态深层卷积核感受野大学到的是几个心拍之间的节律关系。这跟图像 CNN 从边缘到纹理到物体的层次化特征提取是一个道理只不过心电的「纹理」是时间轴上的波形。一个典型的一维 CNN 特征提取块长这样卷积层 → 批归一化 → 激活函数 → 池化层。卷积核数量逐层翻倍常见 32、64、128、256。卷积核大小第一层取 5 到 11后面逐层减小到 3。池化用最大池化窗口 2 到 5步长等于窗口把时间轴逐步压缩。import torch import torch.nn as nn class CNNFeatureExtractor(nn.Module): def __init__(self, in_channels1): super().__init__() # 第一层大卷积核捕捉 QRS 等短时形态 self.block1 nn.Sequential( nn.Conv1d(in_channels, 32, kernel_size11, padding5), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(kernel_size2, stride2) ) # 第二层中等卷积核捕捉 P 波、T 波细节 self.block2 nn.Sequential( nn.Conv1d(32, 64, kernel_size7, padding3), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(kernel_size2, stride2) ) # 第三层小卷积核捕捉节律变化 self.block3 nn.Sequential( nn.Conv1d(64, 128, kernel_size3, padding1), nn.BatchNorm1d(128), nn.ReLU(), nn.MaxPool1d(kernel_size2, stride2) ) def forward(self, x): x self.block1(x) x self.block2(x) x self.block3(x) return x # 输出形状 [batch, 128, time_steps]kernel_size11在第一层对应约 30 ms 的时间窗360 Hz 采样下刚好覆盖 QRS 波的主要成分。padding取kernel_size // 2是为了保持时间轴长度不变只在池化时压缩。BatchNorm1d放在卷积和激活之间能加速收敛对心电这种幅值波动大的信号尤其有用。三层之后时间轴压缩了 8 倍5 秒窗口从 1800 点变成 225 点特征图通道数从 1 变成 128。3.2 多尺度特征提取不同卷积核并行单一尺度的卷积核只能捕捉特定时间跨度的模式。QRS 波宽度约 80–120 msP 波约 80–100 ms但两者形态差异大用同一个卷积核同时学好两者比较吃力。多尺度做法是并行几个不同 kernel_size 的卷积分支再把输出在通道维拼接。class MultiScaleBlock(nn.Module): def __init__(self, in_channels, out_channels): super().__init__() # 三个分支分别用 3、7、11 的卷积核 self.branch_small nn.Conv1d(in_channels, out_channels // 3, kernel_size3, padding1) self.branch_mid nn.Conv1d(in_channels, out_channels // 3, kernel_size7, padding3) self.branch_large nn.Conv1d(in_channels, out_channels // 3, kernel_size11, padding5) self.bn nn.BatchNorm1d(out_channels) self.relu nn.ReLU() def forward(self, x): s self.branch_small(x) m self.branch_mid(x) l self.branch_large(x) out torch.cat([s, m, l], dim1) # 通道维拼接 return self.relu(self.bn(out))out_channels // 3保证三个分支拼起来后总通道数等于out_channels。kernel_size3对应约 8 ms捕捉陡峭的 QRS 上升沿kernel_size7对应约 20 ms捕捉 P 波和 T 波kernel_size11对应约 30 ms捕捉更宽的异常波形。三个分支并行网络可以同时学到不同时间尺度的特征最后在通道维融合。这种结构在心律失常分类里比单尺度 CNN 通常能涨 1–3 个点的 F1。3.3 特征提取后的全局池化与嵌入向量卷积层输出的是[batch, channels, time_steps]的三维张量要接分类头需要先压成二维。常见做法是全局平均池化或全局最大池化把时间轴压掉得到[batch, channels]的特征向量。全局平均池化对每个通道取时间维均值保留整体激活强度全局最大池化取最大值保留最显著激活。心电分类里我一般两个都算拼接后使用。class GlobalPooling(nn.Module): def __init__(self): super().__init__() self.avg_pool nn.AdaptiveAvgPool1d(1) self.max_pool nn.AdaptiveMaxPool1d(1) def forward(self, x): avg self.avg_pool(x).squeeze(-1) # [batch, channels] max_ self.max_pool(x).squeeze(-1) return torch.cat([avg, max_], dim1) # [batch, 2*channels]AdaptiveAvgPool1d(1)不管输入时间轴多长都输出长度 1这样网络对输入窗口长度不敏感。拼接后特征维度翻倍后续接全连接层做分类。这个嵌入向量就是后续特征融合的输入。4. 特征融合多导联、多尺度、多模态怎么拼4.1 早期融合、中期融合与晚期融合的选型特征融合按发生位置分三种。早期融合在输入层拼比如多导联心电直接按通道维堆叠in_channels从 1 变成 12。中期融合在中间层拼比如多尺度分支在通道维拼接或者多导联各自过 CNN 后在某个中间层合并。晚期融合在决策层拼每个导联或每个模态单独出一个分类概率最后加权平均或投票。选型上多导联心电如果导联间同步采集早期融合最简单in_channels12直接进网络。如果导联间差异大比如一个肢体导联一个胸导联中期融合更稳各自提特征再合并。多模态场景比如心电加心音、心电加血压晚期融合更常见因为不同模态采样率和物理意义差异大强行早期融合反而互相干扰。class EarlyFusionCNN(nn.Module): def __init__(self, n_leads12, n_classes5): super().__init__() # 多导联直接作为输入通道 self.extractor CNNFeatureExtractor(in_channelsn_leads) self.pool GlobalPooling() self.classifier nn.Sequential( nn.Linear(256, 128), nn.ReLU(), nn.Dropout(0.5), nn.Linear(128, n_classes) ) def forward(self, x): # x: [batch, n_leads, time_steps] feat self.extractor(x) feat self.pool(feat) return self.classifier(feat)n_leads12对应标准 12 导联如果只有单导联就设 1。Dropout(0.5)放在全连接层前心电数据集样本量通常不大dropout 能压过拟合。这个结构就是早期融合的典型写法简单直接适合导联同步且数量固定的场景。4.2 中期融合多导联各自提特征再合并中期融合的做法是每个导联走一个共享权重的 CNN 分支得到各自的嵌入向量再拼接或做注意力加权。共享权重能减少参数量也强制网络学导联无关的特征。如果导联间差异确实大也可以不共享权重但参数量会翻倍。class MidFusionCNN(nn.Module): def __init__(self, n_leads12, n_classes5): super().__init__() # 共享权重的特征提取器 self.shared_extractor CNNFeatureExtractor(in_channels1) self.pool GlobalPooling() # 融合后的分类头输入维度 导联数 * 每导联特征维度 self.classifier nn.Sequential( nn.Linear(n_leads * 256, 128), nn.ReLU(), nn.Dropout(0.5), nn.Linear(128, n_classes) ) def forward(self, x): # x: [batch, n_leads, time_steps] batch, n_leads, length x.shape feats [] for i in range(n_leads): lead x[:, i:i1, :] # [batch, 1, time_steps] f self.shared_extractor(lead) f self.pool(f) # [batch, 256] feats.append(f) fused torch.cat(feats, dim1) # [batch, n_leads*256] return self.classifier(fused)循环里每个导联单独过shared_extractor权重共享意味着所有导联用同一套卷积核。torch.cat在特征维拼接融合后的向量维度是n_leads * 256。这种写法比早期融合参数量少因为卷积核只有一套但计算量随导联数线性增长。如果导联数很多比如 12 导联循环会比较慢可以改成把导联维当 batch 维一次性过网络再 reshape 回来。4.3 注意力加权融合让网络自己决定哪个导联重要不同导联对心律失常的敏感度不一样。II 导联看 P 波清楚V1 导联看室性异位有优势。与其等权拼接不如让网络学一组权重对每个导联的特征加权求和。这就是注意力融合的思路。class AttentionFusion(nn.Module): def __init__(self, n_leads12, feat_dim256): super().__init__() # 为每个导联学一个标量权重 self.attention nn.Sequential( nn.Linear(feat_dim, 64), nn.Tanh(), nn.Linear(64, 1) ) def forward(self, lead_feats): # lead_feats: list of [batch, feat_dim]长度 n_leads stacked torch.stack(lead_feats, dim1) # [batch, n_leads, feat_dim] scores self.attention(stacked) # [batch, n_leads, 1] weights torch.softmax(scores, dim1) # 导联维归一化 fused (stacked * weights).sum(dim1) # [batch, feat_dim] return fusedattention是一个两层小网络输入单导联特征输出一个标量分数。softmax在导联维归一化保证权重和为 1。(stacked * weights).sum(dim1)做加权求和得到融合后的单向量。这种融合方式在导联质量参差不齐时特别有用网络会自动压低噪声大的导联权重。代价是多了注意力网络的参数小数据集上要注意过拟合。注意注意力权重可以可视化出来如果发现某个导联权重始终接近 0要么是该导联数据质量差要么是预处理有问题值得回头查。5. 避坑与排查心律失常分类里最容易翻车的五件事5.1 类别极不平衡导致模型只预测正常类现象训练完模型在测试集上 accuracy 很高但 V 类、S 类的 recall 接近 0混淆矩阵里几乎所有样本都被判成 N 类。原因MIT-BIH 里 N 类占比超过 90%V 类可能只有几个百分点。交叉熵损失下模型只要全预测 N 就能拿到很高的 accuracy梯度被多数类主导。解决换损失函数用带类别权重的交叉熵权重取类别频率的倒数或者用 Focal Loss让难分类的少数类样本产生更大梯度。重采样也是办法对少数类过采样或对多数类欠采样但过采样要注意别简单复制可以用 SMOTE 在特征空间插值。我一般先试类别权重不行再上 Focal Loss。5.2 按窗口切分时标签错位现象模型在训练集上表现正常但验证集 F1 波动很大同一个记录不同窗口预测结果矛盾。原因固定窗口切分时窗口中心不一定落在心拍上标签取窗口中心对应心拍但窗口内可能包含多个不同类心拍标签噪声大。解决改成按 R 波位置切分以 R 波为中心前后各取固定长度保证窗口内主要是一个心拍。R 波检测用 Pan-Tompkins检测漏了或多了要人工核对。如果坚持按窗口切标签取窗口内多数心拍的类别并在训练时对标签置信度低的窗口降权。5.3 预处理滤波参数不当把有用信号滤掉现象模型对正常搏动识别很好但对宽 QRS 的室性异位识别差F1 明显低于预期。原因带通滤波高频截止设太低比如设到 20 Hz宽 QRS 的高频成分被滤掉形态特征丢失。解决心电带通滤波高频截止一般设 40 Hz 到 100 Hz。40 Hz 是诊断标准下限如果做形态学分析可以放宽到 100 Hz。但要注意工频干扰在 50 Hz 或 60 Hz如果高频截止超过工频需要额外做陷波滤波。我一般先 0.5–100 Hz 带通再 50 Hz 陷波最后归一化。5.4 训练集和测试集按窗口随机划分导致数据泄漏现象测试集指标高得离谱换一批数据就崩。原因同一个记录切出的窗口被随机分到训练集和测试集相邻窗口高度相似模型相当于在测试集上见过训练样本。解决按记录划分MIT-BIH 常用的是按患者划分训练集和测试集记录不重叠。如果数据来自多个中心按中心划分更严格。划分完检查一下训练集和测试集的类别分布别一个类只在测试集出现。5.5 模型参数量大但数据量小过拟合严重现象训练 loss 一直降验证 loss 先降后升验证集 F1 在几个 epoch 后开始掉。原因CNN 参数量几十万到几百万MIT-BIH 训练集可能只有几万心拍参数比样本还多。解决加 Dropout全连接层前 0.5卷积层后 0.2加 L2 正则权重衰减 1e-4用数据增强比如时间轴随机缩放、加高斯噪声、随机裁剪或者换更小的模型减少卷积核数量和全连接层宽度。早停也是必须的验证集 F1 连续几个 epoch 不涨就停。6. 把融合特征用起来从分类头到可解释性验证分类头本身不复杂融合后的特征向量接一两层全连接加 softmax 就行。但做完分类不是终点还得验证网络学到的特征是不是真的对应心电形态而不是在拟合数据集偏差。我一般做两件事一是把卷积核激活可视化看浅层卷积核是否响应 QRS 波二是用 Grad-CAM 或类似方法看分类决策时网络关注信号哪个时间段。def compute_gradcam(model, input_signal, target_class): # 简化版 Grad-CAM针对一维 CNN model.eval() input_signal.requires_grad_(True) feat model.extractor(input_signal) # 最后一层卷积输出 feat.retain_grad() pooled model.pool(feat) logits model.classifier(pooled) score logits[0, target_class] model.zero_grad() score.backward() # 特征图梯度在时间维取平均作为权重 weights feat.grad.mean(dim2, keepdimTrue) # [batch, channels, 1] cam (weights * feat).sum(dim1) # [batch, time_steps] cam torch.relu(cam) return cam.detach().numpy()feat.retain_grad()保留中间层梯度score.backward()反传后feat.grad就是梯度。weights是每个通道的梯度均值代表该通道对目标类的重要性。(weights * feat).sum(dim1)加权求和得到时间轴上的激活曲线relu只保留正贡献。把这条曲线和原始心电叠在一起如果峰值落在 QRS 波附近说明网络确实在用心电形态做判断如果峰值落在噪声段那模型可能学到了伪相关得回头查预处理和数据集划分。参数上target_class选你要解释的类别比如 V 类。cam的长度和最后一层特征图的时间长度一致要上采样回原始信号长度才能叠加。这个验证步骤花不了多少时间但能帮你判断模型是不是真的可用而不是在测试集上碰巧分数高。我自己踩过的坑是有一次模型 F1 很高Grad-CAM 一看激活全在信号开头那段后来发现那段是记录起始的校准信号所有样本都有类似模式模型其实在偷懒。把那段裁掉重训F1 掉了两个点但模型泛化反而好了。所以别只看指标可解释性验证该做还得做。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

STM32嵌入式C++开发环境搭建:CubeMX、Keil、CubeProgrammer与VS Code工具链详解 2026/9/30 21:36:29

STM32嵌入式C++开发环境搭建:CubeMX、Keil、CubeProgrammer与VS Code工具链详解

1. 四个软件到底在干嘛:先把工具链的账算清楚很多人第一次接触STM32的C开发,跟着教程一路点“下一步”,装完Keil、STM32CubeMX、STM32CubeProgrammer,再顺手装个VS Code,回头一看桌面四个图标,脑子里只剩一…

阅读更多 →
未来预测:用 TaoToken 统一 Key 打通 AI Agent Harness Engineering,SaaS 菜单交互会被取代吗? 2026/9/30 21:35:42

未来预测:用 TaoToken 统一 Key 打通 AI Agent Harness Engineering,SaaS 菜单交互会被取代吗?

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

阅读更多 →
MIPI LP RX调试实战:从电气设计到FPGA实现的关键要点 2026/9/30 21:34:50

MIPI LP RX调试实战:从电气设计到FPGA实现的关键要点

1. 先搞清楚LP RX在整个MIPI体系里是什么角色MIPI LP RX这几个词,第一次看到的人大概率是懵的。LP是Low Power,RX是接收端,合起来是“低功耗模式接收器”。光从字面看不出多大名堂,但在实际调试MIPI屏、MIPI摄像头的时候&#xff…

阅读更多 →
freemodel 免费送5美元的gpt-5.5 模型的token 想多了:Codex auth.json 改到 TaoToken 的实测记录 2026/9/30 21:33:39

freemodel 免费送5美元的gpt-5.5 模型的token 想多了:Codex auth.json 改到 TaoToken 的实测记录

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

阅读更多 →
快速部署OpenClaw:轻量应用服务器接入千帆大模型与APIKey配置指南 2026/9/30 21:33:33

快速部署OpenClaw:轻量应用服务器接入千帆大模型与APIKey配置指南

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

阅读更多 →
报错[openclaw-cn] 启动CLI失败: Error: spawn EINVAL —— 用 TaoToken 统一 Key 通道排查 QQbot 环境配置 2026/9/30 21:33:26

报错[openclaw-cn] 启动CLI失败: Error: spawn EINVAL —— 用 TaoToken 统一 Key 通道排查 QQbot 环境配置

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

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉