
简介心电图ECG分类是医疗AI中的基础任务其核心在于从一维时序信号中提取有效的形态与节律特征。传统CNN难以捕捉长程依赖而循环神经网络在处理局部形态时效率较低。视觉Transformer的兴起为时序信号建模提供了新思路尤其是Swin Transformer通过窗口自注意力在局部与全局建模间取得平衡。结合连续小波变换将ECG转换为时频图可充分利用预训练视觉模型同时引入年龄、心率变异性等临床元数据构建多模态融合架构显著提升心律失常五分类性能与可解释性。基于MIT-BIH数据集完整复盘了从数据预处理、模型选型、训练优化到ONNX部署的全链路工程实践并剖析了Grad-CAM热力图与门控系数在临床解释中的应用。该方法适用于动态心电监测、急诊筛查等场景为医疗AI落地提供了可复现的基线方案。1. 项目整体定位与方案选型复盘1.1 这个系统到底要解决什么问题先把这个项目说清楚。简单讲这是一套面向MIT-BIH心电数据集的心律失常智能诊断系统核心任务是把单条心电信号自动分成不同的类型。分类目标不是我拍脑袋定的而是对齐了AAMI标准下的五类心律失常见解也就是正常窦性心律N类、室上性异位搏动S类、室性异位搏动V类、融合搏动F类和不可分类搏动Q类。为什么选这个任务因为心电图分类这件事看起来是标准的多分类问题但实际做下来坑非常多。第一MIT-BIH里各类别数量严重不均衡Normal类样本占了绝大多数V类和S类相对少F类更是稀有物种。第二同一个病人的心电形态高度相似如果划分训练集和测试集时不按患者隔离模型很容易“背题”测试分数虚高得离谱上了临床立马现原形。第三医生看心电图不只是看波形形态病人的年龄、性别、基础心率、心率变异性这些信息都会影响判断但绝大多数公开模型只吃波形等于主动丢掉了一部分诊断依据。这个系统在训练层面已经完成了所以这篇复盘不打算写“从零开始做研究”的教程而是站在项目已经跑通、模型已经收敛、部署也验证过的角度把整个链路再拆一遍。重点写给三类人看想用Transformer类模型处理一维时序信号的同学想给ECG分类模型加临床可解释性的同学以及做完模型之后不知道怎么往产品形态落地的同学。整个过程有明确的取舍依据不是模型堆叠。1.2 为什么选Swin Transformer而不是CNN或RNN模型选型是项目一开始最先定的方向。当时摆在面前的路有清晰的三条纯CNN比如ResNet一维变体、时序模型LSTM/GRU或TCN、视觉TransformerViT/Swin。我最后选了Swin Transformer不是因为它“新”或者“好看”而是因为它同时解决了两个痛点长程依赖建模和计算效率。先说为什么不用纯CNN。CNN靠卷积核做局部感受野层数堆叠也能扩大感受野但毕竟靠的是逐层堆叠对长距离依赖的捕捉相对慢。心电信号里有非常典型的远程依赖问题比如早搏后的代偿间歇、T波与下一个P波之间的关系这些模式跨度可能达到数百乃至上千个采样点。CNN不是不能学而是需要很大的核或很深的网络代价上不太划算。再说LSTM这类时序模型。它们的序列推理是逐步进行的对单条几秒钟的心电信号来说计算速度偏慢的问题在训练阶段还能忍到了部署阶段如果要做实时筛查硬件成本会明显上去。而且LSTM对局部形态的刻画能力不如卷积类模型心电里的QRS波群形态细节非常关键这一点时序模型天然就没那么擅长。Swin Transformer的优势在于骨架本身就是为图像设计的分层结构窗口自注意力机制让它在局部和全局之间取得了不错的平衡。窗口注意力先在局部区域内建模再通过移位窗口跨窗口交互信息正好贴合心电信号“局部形态QRS形态重要全局上下文心律节律也重要”的特点。更重要的是Swin有成熟的开源预训练权重虽然ECG和自然图像有区别但底部几层学到的边缘、纹理类特征可以迁移过来做初始化对训练稳定性的帮助非常大。不过要诚实说明一点直接用Swin处理一维信号是不行的Swin的输入格式是二维图像。所以我在项目里做了模态转换把一维ECG波形用连续小波变换CWT变成二维时频图再用Swin去提取图像特征。关于这个转换的细节后面数据工程的部分会展开。1.3 多模态融合的必要性这是整个项目里我比较坚持的一个设计。当前做ECG分类的主流方案是纯信号建模输入一条波形输出一个类别。这种方式的问题在于它把诊断问题简化成了“只看图形”忽略了医生实际决策过程中的多源信息。我在和心内科方向的朋友聊过之后确认了一个常识临床医生判读心电图时一定会结合病人的年龄、性别、心率、既往病史等背景信息。举个例子同一个宽大畸形的QRS波形发生在年轻人和老年人身上医生的警惕程度完全不一样同样的早搏频率静息心率和运动状态下的临床意义也不一样。这些信息不存在于波形本身你只炼丹不喂这些特征模型就永远学不到这种“背景感知”能力。所以我在这套系统里做了双分支的多模态融合一个分支是Swin Transformer主干吃ECG信号的时频图另一个分支是轻量的表格特征编码器吃临床元数据包括年龄、性别、平均心率、RR间期标准差SDRR、心率变异性相关指标等。两个分支在特征层做融合后面再接分类头。融合方式我试过拼接加全连接、加权求和、还有带注意力门控的融合。最后稳定下来的是“拼接后接一层注意力门控”的方式两个分支分别得到特征向量拼接后通过一个小型全连接层计算出一个门控权重对两个分支的特征做软性重标定再拼回去分类。这样模型自己会学会在什么情况下更信任波形特征、什么情况下更依赖临床背景可解释性分析的时候也能通过门控系数反推模型判断依据。这个细节后面讲解模型结构时再说。2. 数据工程MIT-BIH数据集的完整处理链路2.1 MIT-BIH数据集的真实面貌MIT-BIH心律失常数据库是心电图分类这个领域绕不开的公共基准。它包含48条半小时的双通道动态心电记录采样率360Hz两个通道分别是MLII导联和V5导联。注释体系非常细细分成了15种以上的心律类别做五分类任务时一般都要按照AAMI标准做类别映射。这里必须提醒一句AAMI的标准操作是去掉含起搏器伪迹的4条记录102、104、107、217然后用剩下的44条做实验不然评价结果不太有说服力。不少人第一次接触这个数据集会踩一个坑直接用torchvision那套数据加载思路去处理MIT-BIH结果发现数据格式是WFDB注释文件结构复杂样本又不是现成的“一张图一个标签”形式一下就卡住了。这个问题处理起来并不难用wfdb库读记录和注释然后自己做切片但确实需要一点耐心。在数据准备阶段我做的第一件事就是把每条记录的注释列表完整打印出来看看类别分布到底是什么样。MIT-BIH中N类样本占了绝大多数V类和S类样本量相对少F类只有几百个样本。这个分布如果直接拿来训练模型会严重偏向多数类。后面我用了联合损失加类别重加权的策略来对抗这个问题具体方法在训练章节细讲。2.2 信号预处理与切片策略信号预处理这块我的处理链路是原始信号 → 去除基线漂移 → 去除工频干扰 → 标准化 → 滑窗切片。每一步都可以找到对应的必要性解释。基线漂移是心电信号里最常见的低频干扰主要来源于呼吸运动和电极移动。我用的是中值滤波加高通滤波组合的方式去掉基线。具体来说对每个滑窗先做一次中值滤波得到基线估计然后从原始信号中减去再用一个截止频率约0.5Hz的高通滤波器做二次保障。50Hz的工频干扰我用陷波滤波器处理导联选择上统一用MLII通道因为这条导联的QRS波群形态最明显注释也最全。切片策略我采用的是固定时间窗口滑动切片窗口长度设为8秒。为什么是8秒而不是5秒或10秒理由是8秒的窗口能覆盖约6到10个完整心动周期对判断节律性信息够用再短的话某些间歇性事件可能恰好落在窗口外面再长的话窗口内可能包含多个类别标签标注冲突概率上升。切片前我对每条记录做了边界处理抛弃首尾1秒的数据避免起止段的不完整心搏干扰。切片后的标签怎么定我采用的是窗口主导标签策略统计窗口内每个心搏的原始注释取占比最高的类别作为窗口标签。这个策略有个隐含问题就是窗口边界处可能截断一个P波或者T波但这是滑窗方法的固有代价靠后续的类别均衡和损失加权去缓解。这里有一个关键的细节按患者划分数据集。必须在患者级别划分训练集、验证集和测试集不能把同一条记录的相邻窗口分散到多个集合。否则模型会通过记忆患者特有的波形形态来提高分数跨患者的泛化能力完全没得到验证。我最终按大约7:1:2的比例把44条记录分配到三个集合并确保每个集合里的类别分布大致接近原始分布这一步是保证评估可信度的基石。2.3 一维信号转二维时频图与模态配对Swin吃的是图像所以必须把一维ECG波形转成二维表示。可供选择的方式有短时傅里叶变换STFT、小波变换CWT、以及直接把波形像素化成图像。我选择了CWT原因很直接心电信号的重要特征既有低频ST段、T波也有高频QRS波群的快速起落STFT的固定窗长很难同时在两者之间取得好平衡而小波变换通过不同尺度的母小波天然适配这种多分辨率需求。实际实现时我用的是PyWavelets库里的连续小波变换母小波选择复Morlet小波cmor尺度范围从1到128输出的时频图尺寸统一缩放到224x224。这里有个容易忽略的点做完CWT后取的是小波系数的幅值还是实部我两种都试过幅值表示的时频能量分布对模型更友好因为CWT的实部带有较强的振荡直接当图像喂给网络会引入大量高频噪声。幅值图则平滑很多。把临床元数据变成表格特征分支的输入也要做一个对齐。每条窗口样本从对应的记录中提取一组统计特征和静态特征包括年龄、性别编码为0/1、平均心率、最短RR间期、最长RR间期、SDNN、RMSSD、以及QRS平均宽度。前两个是静态特征后面这些动态特征是从当前窗口里实际计算的统计口径是每个窗口独立计算不是整条记录的全局统计不然输入特征里就包含了未来信息部署时在线推理会完全对不上。最后做模态配对就是把同一个窗口的时频图和对应的表格特征绑成一个训练样本。时频图走Swin分支表格特征走MLP分支两个分支的预测结果在融合层汇合。3. 模型构建与训练细节复盘3.1 模型的整体结构设计整体结构可以描述为“双分支特征提取 注意力门控融合 分类头”。图像分支我用的是Swin-Tiny结构输入是CWT生成的224x224x3时频图。这里有个取巧的做法CWT幅值图原本是单通道灰度图我把它复制成三通道再输入直接用官方预训练权重初始化。Swin-Tiny的最后一层特征会经过全局平均池化得到768维的特征向量。表格分支就是一个三层的MLP输入维度约为8临床元数据维度中间层是128维用ReLU激活加Dropout输出64维特征向量。这里不要搞太复杂的网络表格特征维度低、样本量也不算大网络深度上去了反而容易过拟合。融合模块是项目中调参最多的部分。我最终采用的是这样的流程两个分支的特征768维的视觉特征和64维的表格特征拼接成832维向量经过一个全连接层映射到128维再分别计算出两个标量权重过Sigmoid后就得到了门控系数。视觉特征和表格特征分别乘以各自的系数再加权合并得到最终融合特征向量最后过一层全连接输出五分类logits。这样做的优势非常明确门控系数直接反映了模型在某个样本上依赖哪一侧信息更多。在可解释性分析阶段我统计了不同类别样本上的门控系数分布发现S类和V类样本的平均视觉门控权重明显高于其他类别这说明模型在区分室性和室上性早搏时更多依赖形态细节而F类这类易混淆样本上表格特征权重相对更高。这个发现是有实际临床参考价值的后面章节会细说。3.2 训练配置、损失函数与类别不平衡处理训练配置这部分我直接给出最终稳定使用的参数组合图像分支Swin-Tiny加载ImageNet预训练权重骨干网络学习率设为3e-5融合层和分类头学习率设为3e-4表格分支MLP学习率1e-3优化器AdamWweight_decay0.05批次大小32训练轮数30轮使用CosineAnnealingLR学习率调度前3轮做线性warmup混合精度开启AMP显存占用从接近7GB降到大约4GB损失函数这里值得多说两句。五分类任务直接上交叉熵是完全可行的但面对F类和Q类样本极度稀少的情况交叉熵会让模型对少数类的召回率低得可怜。我用了两个手段组合处理第一类别加权交叉熵。具体权重我是按照“样本数倒数开根号再归一化”来计算的而不是直接用样本数倒数。直接用倒数的后果是权重差异过大训练早期梯度爆炸收敛不稳定。开根号后少数类的权重提升比较温和模型既不会忽略少数类也不至于被少数类带偏。第二标签平滑label smoothing。平滑系数设了0.1。这个手段对心电图分类有额外的价值MIT-BIH的注释本身就带有一定主观性不同医生对某些异位搏动的标注不完全一致标签平滑相当于给模型注入了一点“标注不确定性”的先验能明显缓解过拟合。训练过程中我还加了一个非常有效的trick类别均衡采样器。每个epoch采样时对F类和Q类样本做上采样让每个batch里五类的比例保持在相对接近的状态。这里要注意上采样只作用于训练集验证集和测试集不做任何重采样保持原始分布否则评估结果失真。3.3 评估体系与最终效果评估指标上我坚持用宏观F1macro-F1作为主指标而不是准确率。原因很简单这个数据集本身类别分布严重不均衡准确率会被N类主导S类、F类被完全判错的情况下准确率依然可以很高那就掩盖了模型的真实短板。宏观F1对每个类别的表现一视同仁少数类表现差会直接反映在指标上。另外我额外关注了敏感度recall和阳性预测值precision的搭配。在医疗场景下召回率低意味着漏诊这是比误报更不能接受的。所以我在调参时的原则是在保证宏观F1不下降太多的前提下尽量拉高少数类的召回率。最终验证集和测试集上的结果我这边跑出来的整体水平大约是N类精确率和召回率都在98%以上V类召回率在91%左右S类召回率在84%左右F类约78%左右Q类在90%左右宏观F1在0.90上下一档水平。这个数值得放在“按患者划分真实MIT-BIH44条数据”的设定下看才有意义跑在不同数据划分方案下结果会有波动不必逐位对标。从混淆矩阵上看最容易混淆的类别对是S类和N类、F类和V类。S类室上性异位的P波形态变化微妙和正常窦性容易混F类样本太少且形态上介于N和V之间天然是难点。这些认知后来直接指导了可解释性分析的侧重点。4. 模型可解释性与临床部署落地4.1 可解释性分析Grad-CAM与门控系数的组合拳模型在实验室里跑出指标只是第一步。如果这个系统拿给临床医生用医生问的第一句话大概率是“你凭什么说这个是室性早搏”这正是可解释性要回答的问题。我用了三层可解释性分析从不同粒度回答这个问题。第一层是Grad-CAM热力图。对Swin Transformer而言标准Grad-CAM需要处理多头注意力的梯度聚合问题实现上比CNN要绕一些。我的做法是取最后一层窗口自注意力的梯度对空间维度做平均得到类别权重再把注意力图加权求和最终上采样回224x224叠在时频图上。这样能直观看到模型在区分某个类别时主要关注时频图上的哪些区域。我抽查了V类样本的热力图发现高亮区域基本集中在QRS波群对应的高频能量带上这个观察和心电学直觉是一致的。第二层是门控系数的统计。前面说过融合模块会输出一维视觉门控权重和一维表格门控权重。我把测试集里每个样本的门控系数收集起来按真实类别做分布统计。结果挺有意思N类样本的视觉门控权重分散有些样本甚至更依赖表格特征S类和V类样本则明显更偏视觉门控。这个信息提示我们模型在面对形态差异微小的类别时反而会更依赖形态特征去抠细节符合诊断逻辑。第三层是SHAP值分析用在对表格特征分支的解释上。跑SHAP时直接用训练好的融合模型会比较别扭我简化了一下单独对融合后的分类结果做SHAP把视觉分支的768维特征固定住只计算8个临床元数据特征的贡献值。结果符合临床常识平均心率和SDNN对决策影响最大其次是年龄和QRS宽度性别的影响相对较低。这层分析对医生建立对系统的信任很有帮助。4.2 从模型到部署ONNX导出与服务化训练完成后要把模型做成真正能用的系统第一步就是脱离PyTorch的运行时依赖。我用ONNX对模型做了导出把Swin分支、MLP分支和融合模块都包在一个图里输出层接Softmax。导出时踩了一个很有意思的坑Swin在PyTorch里的forward流程包含一个绝对位置编码的reshape操作ONNX导出时如果不把输入张量的维度信息固定成静态shape导出的图在推理时会报维度不匹配。解决办法是在torch.onnx.export里显式指定输入尺寸并设dynamic_axes为空集合接受固定输入尺寸来换稳定性。推理服务我用FastAPI包了一层简单的HTTP接口接收JSON格式的临床元数据和一段心电数据内部做预处理、CWT变换、模型推理最后返回五行概率、预测类别和门控系数。为了演示部署场景前端做的是一个极简的可视化页面上传一条12秒的心电波形左侧显示原始波形和时频图右侧显示分类结果和Grad-CAM热力图叠合图。这里给一个部署性能参考在CPU上单条8秒样本的预处理加推理总耗时大概在300毫秒左右在普通的入门级GPU上能压到60毫秒以内。如果未来要做批量筛查可以再用TensorRT做一次优化但现阶段FastAPI加ONNX Runtime的方案已经足够应对单机演示和小规模并发。部署这件事我有几点体会。第一系统仓库里必须塞一个模型版本说明文件记录训练数据划分、超参数、最终指标和导出时间不然后面迭代模型新旧模型混在一起会非常痛苦。第二推理接口一定要做输入校验真实环境里传来的心电数据可能是乱序的、带缺失值的、采样率不一致的这些都要在入口处拦截不然模型在脏数据上会输出一些“自信但荒谬”的结论。第三返回结果里必须保留概率向量而不是只给硬标签医生能根据置信度判断这条结果是否值得采信这个信息对做临床辅助判断来说很关键。5. 常见问题排查与实操心得5.1 新手容易踩的五个坑这个项目从头到尾做下来踩过的坑积累了一些整理一下供后来者参考。第一个坑数据划分没有按患者隔离。这个问题在ECG任务里特别隐蔽因为按窗口切分看起来样本很多但如果把同一条记录的相邻窗口同时丢进训练集和测试集模型会通过“记忆记录级特征”取巧跨患者测试时性能掉得厉害。我看过不少开源项目报告89%的准确率复现时用自己的划分方式一试只有70多大概率就是这个原因。解决方式就是对数据记录ID做分层划分并明确写进代码注释。第二个坑CWT参数没有固定比如尺度范围和母小波选择。这个参数的调整会直接影响时频图的形态进而影响整个模型。更麻烦的是训练和推理阶段如果使用不同的小波参数模型输出会完全错乱。务必把CWT参数固化到配置文件中训练和部署共用同一份配置。第三个坑类别权重设置过猛。我最初直接用样本数倒数做类别加权结果F类权重是N类的几十倍模型训练前几轮loss剧烈震荡直到后期才勉强收敛。后来改成“开根号归一化”的方式才稳定。如果你发现训练初期loss爆炸先检查是不是类别权重或学习率设置问题。第四个坑Swin的输入尺寸和patch size不匹配。Swin-Tiny默认patch size是4输入尺寸必须能被32整除。如果你用224x224没问题但如果你想换到384x384或者下游任务自定义尺寸要仔细算一下否则会报形状错误或者出现不可见的特征丢失。第五个坑表格特征标准化时把训练集、验证集、测试集一起fit了。这是数据泄漏中比较隐蔽的一种。正确做法是只计算训练集的均值和方差验证集和测试集复用这套统计量做转换。这个错误很多人会犯因为它不影响训练精度反而会让测试集指标虚高。5.2 一些真正有用的经验总结最后分享几个我觉得特别有价值的实操经验。一个是对“多模态融合模型是什么”这件事的认知。很多人以为多模态融合就是把不同输入拼在一起但做下来我最大的体会是融合方式的本质是信息路由模型需要学习在什么条件下优先使用哪条信息通道。因此做融合不要一上来就堆复杂模块先从基线做起比如简单concat加MLP把指标跑出来再逐步替换成注意力机制。每一步都能看到明确的指标变化才不会在调试时迷失方向。另一个经验是Swin Transformer虽然是从视觉Transformer引入的但它并不是一个“开箱即用”的心电分类器前期的数据形态转换和后期微调策略才是项目成败的重头戏。数据侧CWT参数选择、滑窗策略、类别映射决定了输入信息的质量和分布模型侧冻结底层预训练参数、只微调高层训练稳定性比全参数微调好得多。我实际比较过冻结Swin前两个stage只微调后两个stage和融合层在样本量不大的情况下效果优于全参数微调而且显存占用更低。还有一个体会是关于模型的可解释性不应该在模型训完以后才开始想。我是在设计融合模块的时候就预留了门控权重输出在训练日志里就记录了各样本的门控系数分布。这样训完以后解释材料是自动产出的不需要再回头改模型结构重新训练。如果你打算做临床方向的项目强烈建议在模型设计阶段就把可解释性当成一个输出项来规划而不是亡羊补牢。这套系统目前的形态是“可以演示、可以复现、可以扩展”的完整基线。后续如果继续做下去我可能会在三个方向上扩展一是把输入从单导联扩展到双导联甚至12导联验证多导联信息对S类和F类分类的帮助二是把示例级的可解释性升级为“片段级”也就是定位到具体异常心搏的时间范围这其实是在Grad-CAM基础上再做一层峰值检测三是增加在线增量学习能力让系统可以在新数据上做小规模微调适配不同人群的心电形态差异。不过这些都是后话了先把当前的成果稳定住、把评估流程标准化对于做医疗AI方向的项目来说比急着追新模型更重要。本文还有配套的精品资源点击获取