news 2026/9/16 1:05:52

离散小波变换结合一维卷积神经网络的心电自动分类实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
离散小波变换结合一维卷积神经网络的心电自动分类实践

简介:基于离散小波变换与一维卷积神经网络的心电自动分类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 峰上,去噪之后再切心跳,比在原始波形上切更稳。

以采样率 fs=360 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} 是 d1,d{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); end

findpeaks 的 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 网络结构参数:kernel=5 起步、池化步长 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-128dropout=0.5
fc + 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) 是防止某一类完全没有预测样本时除以零。下表是结果表格的一种整理格式,数字只用于说明结构,不代表某个固定数据集的结论:

类别精确率召回率F1
N0.980.990.98
S0.870.820.84
V0.960.940.95
F0.720.650.68
Q0.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 峰检测,再喂给训练好的网络,省掉大量驱动适配工作。实时场景中更看重单拍预测的延迟而不是整段众数,所以训练时同样要按记录划分测试集,避免把个体特征当作泛化能力。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 1:03:21

集成学习完全指南:从Bagging到XGBoost、LightGBM与CatBoost

做竞赛、搞建模或者调模型调到头秃的朋友&#xff0c;一定绕不开集成学习。我在实际项目里试过单一模型死磕到极致&#xff0c;最后评分纹丝不动&#xff0c;反而是一顿 bagging、boosting 组合拳下去&#xff0c;线上指标直接涨了一截。这篇总结想把我对集成学习从原理到代码的…

作者头像 李华
网站建设 2026/9/16 1:02:18

管住Cursor的7条铁律:让AI编程不再失控

说实话&#xff0c;我第一次用 Cursor 的时候是有点上头的。AI 补全快得离谱&#xff0c;Tab 一按就是半屏代码&#xff0c;聊几句就能把一个模块生成出来&#xff0c;整个人感觉像换了台法拉利。但用了大概两周之后&#xff0c;我被迫面对一个现实&#xff1a;我的项目开始失控…

作者头像 李华
网站建设 2026/9/16 1:01:10

Web安全测试核心漏洞解析与实战技巧

1. 小迪安全第四节课程核心内容回顾2024年小迪安全课程的第四节主要围绕Web应用安全测试中的关键漏洞展开深度解析。作为一线安全工程师&#xff0c;我认为这节课的价值在于将OWASP Top 10中的抽象概念转化为可实操的检测方法。以下是课程的核心知识框架&#xff1a;SQL注入漏洞…

作者头像 李华
网站建设 2026/9/16 0:58:21

零碳高校智慧能源AI大模型数字化平台规划设计方案:数据驱动、AI赋能、业务闭环

以AI大模型、物联网、大数据、数字孪生等技术为支撑&#xff0c;面向高校构建“零碳/低碳智慧能源”数字化平台&#xff0c;实现能源生产、传输、分配、消费全流程的智能监测、预测、调度、优化与碳资产管理。 该方案以“零碳高校”为目标&#xff0c;以AI大模型为核心引擎&am…

作者头像 李华