离散小波变换结合一维卷积神经网络的心电自动分类实践 简介基于离散小波变换与一维卷积神经网络的心电自动分类Matlab实现面向生物医学工程、电子信息、计算机等专业需要完成课程设计、期末大作业或毕业设计的本硕群体。资源共10个文件以5个.m源码、1个.mat心电数据、1个.py分类脚本及readme/txt/md说明文档为主压缩包仅17KB内容紧凑代码采用参数化编程风格注释明细便于修改采样率、小波层数、网络结构等关键参数。已有101人学习下载。整套方案覆盖MIT-BIH数据下载、信号读取、小波去噪与QRS波群起点/峰值定位以及基于一维CNN的自动分类流程附有可直接运行的案例数据可在Matlab 2014/2019a/2021a中快速复现适合作为算法对比基线或毕业设计核心模块。1. 离散小波变换与一维卷积神经网络为什么这个组合适合心电自动分类心电自动分类里最容易被低估的问题不是分类器而是每次心跳的起点对不齐、基线还在漂。直接把原始波形丢给一维卷积神经网络模型要同时承担去噪、对齐和形态识别三件事卷积核学到的大多是噪声特征换一台采集设备就失效。离散小波变换先把信号按频率拆成近似系数和细节系数去掉工频、肌电和基线漂移后再把干净的 QRS 形态交给一维卷积神经网络。这个组合需要调的参数少、可解释性强在 Matlab 里从去噪到训练评估不到 200 行。下面按可运行的工程顺序展开先定 DWT 参数再设计 1D-CNN然后训练评估最后做整段记录的批量分类。适合做生物医学信号处理、可穿戴心电监测的工程师和研究生也适合刚接触深度学习分类任务、想用 Matlab 跑通心电分类流程的读者。2. 心电数据准备用离散小波变换做去噪与心跳切分的 Matlab 实现2.1 为什么先做 DWT而不是直接让一维卷积神经网络读原始波形心电信号里最常见的干扰是 50/60 Hz 工频、0.5 Hz 以下的基线漂移以及频率更高的肌电噪声。一维卷积神经网络理论上能自动学出滤波器但要达到这个效果需要足够的深度和数据量训练难度和过拟合风险都会上升。离散小波变换的价值在于用固定的滤波器组先把频率内容摊开让网络只需要学时序形态而不是同时学滤波。另一个现实原因是标注通常标在 R 峰上去噪之后再切心跳比在原始波形上切更稳。以采样率 fs360 Hz 的 MIT-BIH 格式为例小波基选 db4分解 8 层。db4 的波形与 QRS 主波有一定相似度能保留形态突变8 层分解后近似系数对应约 1.4 Hz 以下的频率正好把基线漂移隔离到最低频段。细节系数 d1、d2 覆盖 90~180 Hz 和 45~90 Hz绝大多数肌电噪声落在这两层。处理策略是把 d1、d2 置零再重构相当于只保留 1.4~45 Hz 的带通信号。如果只想做分类不一定要去噪得干干净净但所有样本必须经过同样的频带限制否则训练集和测试集的频率响应不一致模型会学偏。小波基分解层数适用采样率保留频带主要作用db48250~500 Hz约 1.4~45 HzQRS 形态保持较好默认首选sym48250~500 Hz约 1.4~45 Hz相位失真略小次选coif56125~250 Hz约 2~30 Hz低频采样率下使用下面是去噪的核心代码% ecg_10s 是 10 秒单导联心电信号fs 为采样率这里按 360 Hz 示例 level 8; wname db4; [C, L] wavedec(ecg_10s, level, wname); d detcoef(C, L, 1:level); % d{1} 是 d1d{8} 是 d8 d{1} zeros(size(d{1})); % 去掉 90~180 Hz d{2} zeros(size(d{2})); % 去掉 45~90 Hz a appcoef(C, L, wname, level); % 第 8 层近似系数 C2 [a; d{8}; d{7}; d{6}; d{5}; d{4}; d{3}; d{2}; d{1}]; ecg_den waverec(C2, L, wname);wavedec 返回的 C 按 [a8, d8, d7, ..., d1] 顺序排列L 记录每一段的长度。重构时必须保持每一段的顺序和长度都与原始 C 一致所以重组时把 d1 放到了最后。detcoef(C, L, 1:level) 返回的元胞顺序是 d1 到 d8恰好和 C 的排列顺序相反这里最容易写错。更精细的做法是对 d1、d2 做软阈值而不是直接置零例如thr median(abs(d{1})) / 0.6745; d{1} wthresh(d{1}, s, thr);但阈值对噪声不平稳的心电数据容易过杀我一般只在 d1 上做阈值d2 以上直接保留这样既抑制了高频毛刺又不会把 T 波末端削平。2.2 R 峰检测与单拍切分让每个样本都是对齐的固定长度切分的常见做法是先用 findpeaks 找 R 峰再以每个 R 峰为中心截取固定长度窗口。窗口太短会丢掉 P 波和 T 波上下文太长会让相邻心跳混进来。0.8 秒窗口在心率 40~180 次/分条件下都能包住完整单拍。为了保证每条记录极性一致先做一次极性判断如果信号的负向峰值大于正向峰值把整段信号取反再找 R 峰。if abs(min(ecg_den)) abs(max(ecg_den)) ecg_den -ecg_den; end [~, locs] findpeaks(ecg_den, ... MinPeakHeight, 0.6 * max(ecg_den), ... MinPeakDistance, round(0.5 * fs), ... MinPeakProminence, 0.3 * max(ecg_den)); seg_len 288; % 0.8 秒 * 360 Hz win_before 100; % R 峰前约 0.28 秒 win_after seg_len - win_before - 1; locs locs(locs win_before locs length(ecg_den) - win_after); segs zeros(length(locs), seg_len); for i 1:length(locs) idx (locs(i) - win_before) : (locs(i) win_after); segs(i, :) ecg_den(idx); endfindpeaks 的 MinPeakHeight 用 0.6 倍最大峰值能滤掉大部分 T 波MinPeakDistance 设置为 0.5 秒对应 120 次/分的心率防止把高耸 T 波误检成 R 峰。MinPeakProminence 设为峰值的 0.3 倍进一步要求峰值相对两侧有明显突出对基线残留不敏感。标签对齐时要注意标注文件里的类别标签对应 R 峰位置而不是窗口中心所以用 locs(i) 取标签即可。2.3 数据划分按记录切分不要按心跳随机切同一个患者的心跳形态高度一致如果训练集和测试集混入同一段记录模型记住的是个体特征而不是心拍类型验证指标会虚高。常见做法是把记录编号作为分组 key整个记录只进入划分后的某一个集合。10 秒记录、每条约 10~15 拍的数据规模下可以按 6:2:2 划分记录而不是按样本数划分。数据划分记录数占比用途训练集60%更新网络权重允许过采样验证集20%观察训练曲线做早停测试集20%最终评估不参与任何调参train_mask ismember(record_id, train_records); val_mask ismember(record_id, val_records); test_mask ismember(record_id, test_records); X_tr segs(train_mask, :); X_va segs(val_mask, :); X_te segs(test_mask, :); y_tr labels(train_mask); y_va labels(val_mask); y_te labels(test_mask);如果记录数很少验证集和测试集各自只有一两条记录这时候要接受指标方差较大不要靠反复重跑来挑测试集那会变成隐式调参。3. 一维卷积神经网络结构设计从序列输入到五分类输出的参数表3.1 输入组织方式单通道去噪段还是多通道 DWT 系数Matlab 的 trainNetwork 里sequenceInputLayer 接受元胞数组每个元胞是一个 numFeatures×numTimeSteps 矩阵。对单导联心电特征数是 1把每拍的行向量放进 cell 即可。如果想把 DWT 的多尺度信息也保留下来可以把近似系数和若干层细节系数重采样到同一长度堆叠成多通道输入。但 waverec 重构后的信号已经包含除了 d1、d2 之外的所有频段再额外送多层系数信息冗余大于收益。我一般直接用去噪重构段作为单通道输入训练更快调参也更直观。XTrain cell(size(X_tr, 1), 1); for i 1:size(X_tr, 1) XTrain{i} X_tr(i, :); % 1 x 288 的行向量 end YTrain categorical(y_tr, {N,S,V,F,Q});这里最容易踩的坑是把行向量传成列向量288×1 的列向量会被解读成 288 个特征、1 个时间步卷积层作用在单个时间点上形态信息完全丢失。组织数据时始终记住「特征是行、时间是列」。3.2 网络结构参数kernel5 起步、池化步长 2 的理由一维卷积心电分类不需要很深。第一层用 5 点卷积核在 360 Hz 下覆盖约 14 ms刚好能捕捉 QRS 的上升沿后面两层用 3 点核继续组合局部形态。每层卷积后接 batchNormalization 加速收敛池化步长 2 把时间分辨率逐步减半让网络对 R 峰位置的微小偏移更不敏感。三层之后时间维从 288 降到 72再用全局平均池化压成 64 维向量而不是在大特征图上展平参数量小、过拟合风险低。层核大小 / 步长输出尺寸设计意图sequenceInput-1×288单导联去噪心跳conv1 BN ReLU5 / 116×288提取 QRS 斜率特征maxpool12 / 216×144抗时间偏移conv2 BN ReLU3 / 132×144组合 P-QRS-T 段maxpool22 / 232×72降低时间分辨率conv3 BN ReLU3 / 164×72高阶形态组合globalAvgPool-64×1去掉时间维fc dropout-128dropout0.5fc softmax-5输出 AAMI 五类对应网络定义代码inputSize 288; numClasses 5; layers [ sequenceInputLayer(1, MinLength, inputSize, Name, input) convolution1dLayer(5, 16, Padding, same, Name, conv1) batchNormalizationLayer(Name, bn1) reluLayer(Name, relu1) maxPooling1dLayer(2, Stride, 2, Name, pool1) convolution1dLayer(3, 32, Padding, same, Name, conv2) batchNormalizationLayer(Name, bn2) reluLayer(Name, relu2) maxPooling1dLayer(2, Stride, 2, Name, pool2) convolution1dLayer(3, 64, Padding, same, Name, conv3) batchNormalizationLayer(Name, bn3) reluLayer(Name, relu3) globalAveragePooling1dLayer(Name, gap) fullyConnectedLayer(128, Name, fc1) dropoutLayer(0.5, Name, dropout) fullyConnectedLayer(numClasses, Name, fc2) softmaxLayer(Name, softmax) classificationLayer(Name, output) ];padding 设为 same 是为了让卷积不改变时间维长度方便和池化的输出尺寸对账。第三个卷积用 64 个滤波器而不是 128是因为 288 点单拍的信息量有限64 已经够用再增加只会加剧过拟合。如果你的 Matlab 版本不支持 globalAveragePooling1dLayer可以把它换成 maxPooling1dLayer(2, Stride, 2)并在展平后把 fc1 的输入维度改成 72×64但 dropout 建议从 0.5 提到 0.6否则 fc1 的参数量会膨胀。3.3 分类头与类别顺序用 categorical 固定五类顺序classificationLayer 要求标签是 categorical类别顺序由 categorical 的第二个参数决定。AAMI 五类建议统一为 N、S、V、F、Q 的大写字符串。如果数据里某个类别在训练集完全缺失categorical 仍会保留它最后的分类层权重随机初始化部分不会更新评估时该类别准确率为 0需要回到数据准备环节检查类别覆盖而不是改网络。label_names {N,S,V,F,Q}; YTrain categorical(y_tr, label_names); YVal categorical(y_va, label_names); YTest categorical(y_te, label_names);4. 训练心电分类模型训练选项、类别不平衡与混淆矩阵评估4.1 训练选项怎么设adam、1e-3 学习率与 best-validation心电训练集通常只有几万样本网络不深adam 加 1e-3 初始学习率是稳妥起点。mini-batch 取 128在 288 点输入下显存压力不大。验证频率设为 30 次迭代一次几千个样本一轮约为几十次迭代能看到曲线变化又不至于太密。OutputNetwork 选 best-validation保存验证准确率最高的权重而不是最后一轮可能已经过拟合的权重。下面代码假设 XValCell 已按 3.1 节的 cell 方式组织好。options trainingOptions(adam, ... InitialLearnRate, 1e-3, ... LearnRateSchedule, piecewise, ... LearnRateDropFactor, 0.3, ... LearnRateDropPeriod, 10, ... MiniBatchSize, 128, ... MaxEpochs, 30, ... Shuffle, every-epoch, ... ValidationData, {XValCell, YVal}, ... ValidationFrequency, 30, ... ValidationPatience, 10, ... OutputNetwork, best-validation, ... Plots, training-progress, ... Verbose, true); net trainNetwork(XTrainCell, YTrain, layers, options);LearnRateDropPeriod 设为 10表示每 10 个 epoch 学习率乘以 0.3后 20 轮从 3e-4 往 9e-5 微调比全程固定学习率更容易收敛到平滑的局部最优。如果验证损失在下降后回升ValidationPatience 会在连续 10 次验证没有改善时自动停止配合 best-validation 一起用能避免最后一轮权重被噪声拖坏。提示trainNetwork 的 classificationLayer 不直接支持类别权重类别不平衡时先用过采样比自定义损失更容易在当前 API 上落地。4.2 类别不平衡对少数类做训练集过采样MIT-BIH 这类数据里N 类占比经常超过 80%V 类次之F 类最少。直接训练会让网络把所有可疑心跳判成多数类。加权交叉熵不直接支持我一般先统计训练集类别数量再把少数类样本复制到接近多数类的数量。过采样只作用于训练集验证集和测试集保持原始分布否则评估结果会失真。counts countcats(YTrain); minor_class categorical({F}); minor_idx find(YTrain minor_class); extra repmat(minor_idx, 4, 1); XTr_bal [XTrainCell; XTrainCell(extra)]; YTr_bal [YTrain; YTrain(extra)];复制倍数取多少以 countcats 观察到的各类数量为参考。F 类占 1% 时复制 4 倍还不够可以到 8 倍但复制太多会让模型对单一患者的重复形态过拟合所以需要在指标和过拟合之间折中。逐类复制后训练样本里少数类的比例会显著提高模型才不会把 N 类的先验概率当作唯一的决策依据。4.3 评估指标准确率之外必须看每类精确率、召回率和 F1心电五分类中准确率会被多数类拉高到 95% 以上但 S、F 类可能基本没学出来。指标要分两个角度预测为该类的样本有多少是对的precision该类真实样本有多少被找回来recall。用 confusionchart 能直接看到错分去向。[YPred, scores] classify(net, XTestCell); cm confusionchart(YTest, YPred, ... RowSummary, row-normalized, ... ColumnSummary, column-normalized); C cm.NormalizedValues; precision diag(C) ./ max(sum(C, 1), eps); recall diag(C) ./ max(sum(C, 2), eps); f1 2 * precision .* recall ./ max(precision recall, eps);每列求和是预测为该类别的数量对应 precision 分母每行求和是该类别真实数量对应 recall 分母。取 max(..., eps) 是防止某一类完全没有预测样本时除以零。下表是结果表格的一种整理格式数字只用于说明结构不代表某个固定数据集的结论类别精确率召回率F1N0.980.990.98S0.870.820.84V0.960.940.95F0.720.650.68Q0.930.950.94如果 S 类的 recall 明显低于 V 类优先检查 S 类的训练样本是不是太少以及去噪时是否把 ST-T 段的低频形态削掉了。心电分类中多数错分发生在形态相近的类别之间只看准确率会漏掉这类问题。4.4 过拟合与伪过拟合先看数据划分再动网络结构训练损失远低于验证损失、验证损失先降后升是最直接的过拟合信号。此时先别急着加正则化回去检查第 2 章的数据划分同一记录的心跳是否混进了训练和验证。另一种常见情况是测试集里有一批噪声主导段与训练集噪声分布完全不同导致验证损失异常升高。这时回到 findpeaks 环节提高 MinPeakProminence把误检的 R 峰样本过滤掉往往比加深网络更有效。确实过拟合时再考虑把第三层卷积的滤波器数从 64 降到 48或把 dropout 从 0.5 提到 0.6。每改一次只动一个变量并固定随机种子否则两组实验之间也会有随机波动。5. 整段心电记录的批量分类多数投票、模型保存与实时采集衔接5.1 把单拍分类封装成记录级预测函数单拍预测会受个别噪声拍影响临床和批量处理更关心整段记录的结论。常见做法是对一条 10 秒记录的所有单拍分别预测再统计众数作为记录标签。function rec_label predict_record(net, ecg_den, fs) seg_len 288; win_before 100; win_after seg_len - win_before - 1; if abs(min(ecg_den)) abs(max(ecg_den)) ecg_den -ecg_den; end [~, locs] findpeaks(ecg_den, ... MinPeakHeight, 0.6 * max(ecg_den), ... MinPeakDistance, round(0.5 * fs), ... MinPeakProminence, 0.3 * max(ecg_den)); locs locs(locs win_before locs length(ecg_den) - win_after); segs cell(length(locs), 1); for i 1:length(locs) idx (locs(i) - win_before) : (locs(i) win_after); segs{i} ecg_den(idx); end y classify(net, segs, MiniBatchSize, 128); cats categories(y); counts countcats(y); [~, best] max(counts); rec_label cats(best); end这个函数把 R 峰检测和训练好的网络串成一条流水线。classify 指定 MiniBatchSize 后整段记录的所有单拍会被分批送入网络避免一次性把所有样本展开占用显存。countcats 统计五类各自的预测数量取最大者作为记录级结论。5.2 模型保存与批量出报告训练完成后把 net 存成 .mat后续批量预测不需要重新训练。save(ecg_dwt_cnn_net.mat, net); files dir(records/*.mat); for k 1:length(files) S load(fullfile(files(k).folder, files(k).name)); rec_label predict_record(net, S.ecg_den, S.fs); fprintf(%s - %s\n, files(k).name, rec_label); end假设每条记录文件里已经包含去噪后的 ecg_den 和采样率 fs。批量脚本最后输出一行记录名加分类结果可以直接重定向到文本报告。换机器加载模型时要求 Deep Learning Toolbox 版本能识别网络里用到的所有层类型跨大版本升级后建议重新验证一遍测试集准确率要部署到没有 Matlab 的环境可以用 exportNetworkToTensorFlow 导出为通用格式。5.3 与实时采集端的衔接落地到可穿戴设备时常见做法是维护一个 0.8 秒的滑动窗口新样本到达后更新窗口再重算 R 峰并预测。不要让每次新样本都跑整段 findpeaks可以每隔 0.2 秒触发一次或只在检测到新的 R 峰时触发。brainflow 这类跨平台生理信号采集库能统一多厂商心电设备的数据格式把每帧数据先做同一套 DWT 去噪和 R 峰检测再喂给训练好的网络省掉大量驱动适配工作。实时场景中更看重单拍预测的延迟而不是整段众数所以训练时同样要按记录划分测试集避免把个体特征当作泛化能力。本文还有配套的精品资源点击获取