MATLAB+CNN心音分类全流程:预处理、Mel频谱图与模型部署
简介这套基于MATLAB的CNN心音信号分类项目实战资料面向人工智能与生物医学信号处理学习者针对先心病筛查中心脏听诊的辅助诊断需求给出了从信号分析到深度分类的完整实现。压缩包共202个文件其中100个wav为心音样本、100个jpg为预处理/特征可视化图、1个m文件为卷积神经网络相关源码另有1个cfg配置文件整体约15.23MB便于下载和快速查看目录结构。方案重点围绕心音去噪展开可处理皮肤摩擦、环境背景噪声、呼吸扰动等干扰并提取梅尔频率倒谱系数MFCC将一维时间域信号构造成二维特征样本送入CNN分类流程清晰且具备工程可读性。已有1101人学习适合希望参考实际预处理方法、特征组织方式与CNN训练思路的读者。1. 心音分类项目启动为什么最终锁定MATLABCNN我最初接触心音信号分类这个方向是因为去体检时听医生用听诊器做了半天检查又建议我去做心脏彩超。排队等候的时候我就在想医生能从心音里听出那么多门道这个过程能不能用算法来模拟后来查了不少资料发现心音分类在生物医学信号处理领域已经是个相当热门的方向但网上的教程绝大多数都是基于Python写的。作为一个日常工作重度依赖MATLAB的工程师我一直在琢磨MATLAB能不能也把这个流程跑通实话实说MATLAB在信号处理领域有天然优势Signal Processing Toolbox和Deep Learning Toolbox之间的无缝配合让我在处理心音这类弱信号时省去了大量跨语言传递数据的麻烦。CNN方面MATLAB的深度学习工具箱虽然不像TensorFlow那样被频繁讨论但封装程度高、文档清晰对于不搞研究只求落地的人来说其实非常趁手。这篇文章的目标读者很明确有一定MATLAB基础、但没怎么接触过深度学习的人以及想在生物医学信号上尝试分类算法的朋友。我会完整拆解从原始心音数据到最终分类模型的全部流程包括数据预处理、Mel频谱图提取、CNN网络结构设计、训练参数调整和结果评估还会把我在实际踩坑中总结的经验一并交代清楚。2. 心音数据预处理从原始波形到干净的信号片段2.1 心音信号的特点与数据来源先说说心音到底是什么。人的一次心跳周期内会产生两个主要心音成分第一心音S1房室瓣关闭产生和第二心音S2半月瓣关闭产生。正常心音在时域上有明确的节奏感而心脏发生病变时心音的时域波形、频率分布、S1与S2的相对强度和间距都会出现异常。常见的心音异常包括收缩期杂音、舒张期杂音、S1减弱、S2分裂等这些异常特征正是分类算法需要捕捉的核心信息。心音信号本身有非常明显的脏特性。采集过程中混入的噪声包括呼吸声、环境噪音、传感器与皮肤摩擦声、工频干扰幅度甚至可能超过有效信号。所以做任何分类工作之前降噪和分段是绕不开的两步。我使用的数据来自PhysioNet 2016 CinC Heart Sound Challenge公开数据集包含超过3000条心音录音采样率为2000Hz每条录音被标注为正常或异常。拿到数据后第一步不是急着训练而是把目录结构整理好按标签分开放置dataRoot E:\heart_sound_data; normalDir fullfile(dataRoot, normal); abnormalDir fullfile(dataRoot, abnormal);然后用MATLAB的audioDatastore对象统一读取这个对象会自动处理文件名和标签的关联后面的训练集、验证集划分也直接基于它操作ads audioDatastore(dataRoot, ... IncludeSubfolders, true, ... LabelSource, foldernames);2.2 带通滤波与零相位滤波的关键细节原始信号的降噪处理是整个预处理链路中最先要做的事。心音的频率主要集中在20Hz200Hz之间再往上虽然有些高频成分但到400Hz左右就衰减得很厉害了。我用一个四阶Butterworth带通滤波器通带设为20Hz400Hz这个范围既能保留S1、S2的主能量区又能滤掉呼吸低频漂移通常低于10Hz和传感器高频毛刺。fs 2000; % 采样率 [b, a] butter(4, [20 400] / (fs/2), bandpass); filteredSig filtfilt(b, a, rawSig);这里有一个绝大多数教程不会强调但影响很大的细节滤波时用的是filtfilt而不是filter。filtfilt是零相位滤波它先正向滤波一遍再把信号倒过来反向滤波一遍两次滤波的相位偏移相互抵消。对于心音信号S1和S2出现的时间位置在诊断上很有价值如果直接用普通filter产生相位失真S1/S2的时刻会发生偏移后续分析就会受影响。2.3 滑窗分段固定输入尺寸的必要折中CNN要求固定尺寸输入而每条心音录音的时长从几秒到几十秒长短不一。标准做法是把滤完波的长信号切分成固定长度的短片段。我选择每个片段5秒滑动步长2.5秒即相邻片段有一半的重叠。为什么是5秒一个正常心动周期大约0.8秒5秒窗口至少包含5个完整心动周期。CNN要捕捉的是心音节奏和杂音模式的统计规律窗口太短会丢失足够的周期信息窗口太长又会让片段内引入心率变化导致的非平稳性。重叠2.5秒则是为了数据扩充一条20秒的录音能切成7个片段训练样本量直接翻好几倍。segmentLen 5 * fs; hopLen round(2.5 * fs);切完段之后做归一化。心音信号的幅值受采集设备增益、传感器贴合紧密度影响很大不同样本之间的绝对幅值没有可比性。我用z-score标准化将每个片段的均值归零、方差归1seg (seg - mean(seg)) / std(seg);这一步必须在分段之后做不能在整条录音上先做完再切。原因在于不同分段里的噪声水平不一致整条归一化会让静音段和杂音段的幅值比例失真影响后续特征提取。3. Mel频谱图提取把一维心音变成CNN认识的图像3.1 为什么不能直接把一维信号丢进CNN很多初学者会直接把一维时序信号输入一维卷积网络。我最初也这么试过结果验证集准确率只能到78%左右。这个效果不算差但远低于换成二维输入之后的表现。原因在于心音的诊断特征主要体现在频率成分的相对变化上而不是时域的绝对幅值上。S1和S2在心音频谱中有明显的能量聚集区收缩期杂音则表现为特定频带内的额外能量分布。一维CNN虽然在理论上也能学习到时域特征但它很难从原始波形中高效提取出哪个频带能量异常这类抽象信息。Mel频谱图则完美地解决了这个问题。它把一维时间序列变换成时间-频率的二维矩阵频率轴经过Mel刻度变换时间轴上每一帧对应一段短时傅里叶变换的结果。二维CNN在图像分类中早已证明了强大的特征提取能力把心音变成图像之后这些能力就能直接迁移过来。Mel刻度本身也是一种模拟人耳听觉特性的非线性频率刻度在低频区域分辨率更高这恰好匹配心音能量集中的特点。3.2 MATLAB里一行代码搞定频谱图MATLAB的audioToolbox提供了melSpectrogram函数从一维信号到Mel频谱图只需要一次调用[s, f, t] melSpectrogram(seg, fs, ... WindowLength, 0.05*fs, ... % 50ms窗长 OverlapLength, 0.025*fs, ... % 25ms重叠 FFTLength, 1024, ... NumBands, 64);这里参数的选择是有讲究的。窗长50ms在2000Hz采样率下对应100个采样点频率分辨率约为20Hz对于区分心音频带内的成分够了。重叠25ms是为了让帧间变化平滑避免频谱图在时间方向出现不自然的块状感。FFT点数1024是补零到1024点达到约1.95Hz的频率分辨率。NumBands64表示把频谱分成64个Mel频带这个维度取值需要在频率分辨率和计算量之间取舍64是我实验下来比较平衡的值。提取出来的是线性幅值谱直接送入CNN并不是最优选择。心音中的杂音能量可能远低于主心音线性幅值下这些微弱但有诊断意义的成分会在卷积操作中被淹没。解决办法是做对数压缩logMel log1p(s); % log(1s)避免log(0)3.3 统一图片尺寸的关键一步CNN要求所有输入样本尺寸一致。由于不同片段的时长在滑窗分段后其实是一致的都是5秒在固定窗参数下生成的频谱图时间帧数理论上相同。但在实际处理中因为极少数录音文件存在截断或者采样率轻微偏差时间帧数可能会有1到2帧的差异。为了避免之后训练时报尺寸不匹配的错误我统一用imresize把所有频谱图缩放到64×128targetSize [64 128]; logMelResized imresize(logMel, targetSize);这里通过双线性插值改变的是时间方向的分辨率。这个操作在信息论意义上会有轻微损失但对CNN训练来说是必要的妥协而且实验结果表明只要原始频谱图的时间分辨率不是太差这种缩放几乎不影响最终分类精度。注意imresize默认用双线性插值对于频谱图来说这是合适的选择。不要用nearest最近邻插值那会让频谱图出现明显的锯齿状伪影。4. CNN网络结构设计小而精的模型更契合生物信号4.1 网络架构的分层设计思路网络结构是整个方案的发动机。我用的是轻量级CNN输入64×128×1的Mel频谱图输出正常/异常两类概率。layers [ imageInputLayer([64 128 1], Name, input) convolution2dLayer(3, 16, Padding, same, Name, conv1) batchNormalizationLayer(Name, bn1) reluLayer(Name, relu1) maxPooling2dLayer(2, Stride, 2, Name, pool1) convolution2dLayer(3, 32, Padding, same, Name, conv2) batchNormalizationLayer(Name, bn2) reluLayer(Name, relu2) maxPooling2dLayer(2, Stride, 2, Name, pool2) convolution2dLayer(3, 64, Padding, same, Name, conv3) batchNormalizationLayer(Name, bn3) reluLayer(Name, relu3) maxPooling2dLayer(2, Stride, 2, Name, pool3) fullyConnectedLayer(128, Name, fc1) reluLayer(Name, relu4) dropoutLayer(0.5, Name, dropout) fullyConnectedLayer(2, Name, fc2) softmaxLayer(Name, softmax) classificationLayer(Name, output) ];每个设计决策背后都有实际考量。卷积核大小选3×3。小卷积核叠加已经在图像分类领域被反复验证感受野可以通过堆叠扩大但参数数量和计算量远小于大卷积核。对心音这种中等规模的数据集控制参数量是抑制过拟合的第一道防线。每个卷积层后接batchNormalizationLayer这是让训练稳定收敛的关键。BN层把每层的输入分布拉回标准正态解决了内部协变量偏移问题。我在不加BN的版本里试过训练loss曲线震荡得非常厉害加了BN之后整个曲线平滑了很多。对于心音这种类内差异大、类间差异小的信号BN的稳定作用比在自然图像任务里更加明显。三个卷积块的特征通道数按16→32→64翻倍增长。这是一种经典的设计范式浅层用较少的通道学习基础频谱特征深层需要更丰富的组合特征所以增加通道数。整个模型参数量大约50万在普通CPU上也能在合理时间内完成训练。全连接层之间加入dropout0.5。心音数据虽然录音数量不少但分段后样本之间存在很高的相关性相邻片段来自同一录音模型很容易记住而非泛化。Dropout随机丢弃一半神经元连接是最简单有效的正则化手段。4.2 为什么不迁移学习我在设计阶段认真考虑过使用预训练模型如GoogLeNet、ResNet做迁移学习但实验结果表明这条路不太适合心音分类。第一预训练模型的浅层卷积核是在ImageNet的百万级自然图像上训练出来的它们擅长检测自然图像的边缘、纹理、颜色渐变而心音的Mel频谱图是窄带信号图谱两者的低级特征分布差异太大迁移过来的浅层特征基本无用武之地。第二预训练模型的输入尺寸通常要求224×224我的频谱图只有64×128强行缩放会严重破坏频率轴的分辨率结构。第三预训练模型参数量动辄上千万对中等规模的心音数据集来说不叠加大量正则化几乎必然过拟合。从零训练一个小型CNN反而是更工程化的选择验证集准确率能达到92%以上完全够用。4.3 数据增强频谱图特有的增强方式5. 训练配置与数据划分的坑与心得5.1 训练集/验证集/测试集划分的一个致命细节这大概是整个项目里最容易出错、也最影响结果可信度的一步。我的数据集划分策略是先按录音文件分组再做分段和特征提取。也就是说先把3000多条录音按7:1:2比例分成训练、验证、测试三组然后再把训练组的录音切分成片段生成频谱图。听起来很简单对不对但实际操作中很多教程是这么写的先把所有录音都切分成片段把所有片段混在一起然后随机抽70%做训练集、20%做测试集。如果你这么做了同一个录音的多个片段会同时出现在训练集和测试集里模型等于提前见过了测试数据的来源病人测试集准确率会虚高好几个百分点。这种情况叫数据泄漏。我的做法adsSplit splitEachLabel(ads, 0.7, 0.1, 0.2); adsTrain adsSplit{1}; adsVal adsSplit{2}; adsTest adsSplit{3};之后才在adsTrain、adsVal、adsTest上分别做分段、提取频谱图。这样三组数据在录音级别上完全互斥评估结果才真实可信。5.2 训练参数设置与训练曲线的判读训练选项配置直接决定模型能否收敛options trainingOptions(adam, ... InitialLearnRate, 1e-3, ... MaxEpochs, 20, ... MiniBatchSize, 32, ... ValidationData, {valData, valLabels}, ... ValidationFrequency, 30, ... Shuffle, every-epoch, ... Plots, training-progress, ... Verbose, true);优化器选adam而不是sgdm。心音数据的噪声比较大、特征分布不规律ADAM的自适应学习率机制能更快收敛不需要手动做学习率退火。学习率1e-3是ADAM在中小型CNN上的标准起点实践证明这个参数很稳。MiniBatchSize设为32这主要受限于GPU显存也受训练稳定性影响。Batch太大梯度估计更准但内存不够Batch太小梯度噪声过大训练曲线会抖动。32是均衡点。MaxEpochs设为20。我在实验中观察到这个网络在第12到15个epoch之间验证集准确率就趋于饱和20个epoch足够了。训练更久不仅浪费时间还有过拟合风险。ValidationFrequency30的意思是每30次迭代计算一次验证集准确率。在训练结束后MATLAB会自动记录验证准确率最高的模型快照而不是最后的模型。这一点很关键——如果训练后期发生过拟合你拿到的依然是泛化能力最好的那个版本。训练时打开Plots看曲线我遇到过训练loss一开始就不下降的情况大多数原因是学习率太大或者数据预处理时忘了归一化。如果训练集准确率很高但验证集准确率低说明过拟合优先调整dropout比例和增强强度而不是盲目加深网络。5.3 频谱图数据增强时间平移与频率掩码图像分类里常用的随机裁剪、旋转、翻转这些增强手段不能直接套用在频谱图上。频谱图的时间轴和频率轴有明确的物理意义旋转90度就把时间变成频率语义完全破坏。随机裁剪也会切断完整的S1-S2周期结构。我采用的增强策略借鉴了语音识别里的SpecAugment思路只保留两种操作时间平移在时间方向随机平移若干帧模拟心音采集过程中的微小时间偏移。频率掩码随机屏蔽一两个Mel频带迫使CNN不依赖单一频带特征增强泛化能力。在MATLAB里实现SpecAugment的思路是写自定义变换函数配合transform操作应用到训练数据上adsTrainAug transform(adsTrainFeatures, (x) augmentSpec(x));其中augmentSpec内部实现随机频率掩码。我实验对比过只加频率掩码的版本比不加增强的版本验证集准确率提升了约1.5个百分点。经验数据增强不是越狠越好。我试过同时加频率掩码和时间掩码屏蔽整段时间结果训练准确率明显下降——因为时间掩码把部分心动周期整段抹掉了引入了大量虚假信息。最终只保留频率掩码效果最好。6. 模型评估与部署准确率不是唯一指标6.1 混淆矩阵与ROC曲线训练完成之后很多人看一眼准确率就收工了。但在医疗信号分类场景里这是远远不够的。在诊断场景中假阴性漏掉异常心音的代价远高于假阳性误报异常。漏掉一个真正的异常可能延误病情误报一次最多让病人多做一次检查。因此模型评估必须看混淆矩阵和ROC曲线。MATLAB里一行代码就能生成混淆矩阵[labelsPred, scoresPred] classify(net, testData); confMat confusionchart(testLabels, labelsPred);我的模型在测试集上得到的结果是总体准确率约92.4%异常类别敏感性93.1%正常类别特异性91.7%AUC0.96。AUC0.96说明模型的排序能力很好异常样本的预测概率通常明显高于正常样本。这对实际部署很重要。实际操作中我们不会把阈值固定在0.5而是结合诊断成本选择阈值。如果更担心漏诊就把阈值调低比如预测概率超过0.3就判为异常如果更在意误诊率就把阈值调高。MATLAB的perfcurve函数可以直接画出敏感性和特异性随阈值变化的曲线帮助定位最合适的阈值。6.2 模型保存与加载推理训练结束保存模型到mat文件save(heart_sound_cnn.mat, net);在实际部署时对新采集的心音信号做预测需要走一遍和训练时完全一样的预处理流程滤波→分段→归一化→Mel频谱图提取→尺寸缩放然后把处理后的数据传入classifyload(heart_sound_cnn.mat, net); [labelsPred, scoresPred] classify(net, logMelResized);整个过程在普通PC上处理一条5秒录音大约0.1秒完全满足辅助诊断的实时性要求。如果想要更友好的交互方式可以用MATLAB的App Designer做一个带界面的小工具左边导入音频中间显示滤波前后的波形和Mel频谱图右边显示分类结果和置信度再编译成独立exe发布目标机器只需要安装MATLAB Runtime。6.3 边界情况与常见误区部署阶段有几个反复出现的坑。第一个是输入尺寸不匹配。模型训练时输入是64×128×1预测时如果输入了128×128的频谱图classify会直接报错。解决方案是把预处理函数封装成一个公共接口所有数据训练、测试、部署都走同一个函数。第二个是类别概率的使用。classify返回的是标签scoresPred里才是softmax概率。如果要调整阈值必须用scoresPred。我见过不少人用默认的0.5分类边界套classify的输出这在类别不平衡时尤其吃亏。第三个是最容易被忽视的测试集污染。如果你反复用同一份测试集评估模型、微调参数、再评估那么测试集的评估结果就不再能真实反映模型在新数据上的表现。本质上测试集已经被你用过了。如果确实需要反复调试应该从训练集中再切出一块开发验证集来指导调参测试集只在最终评估时跑一次。7. 项目复盘预处理比网络结构更能决定成败7.1 三组对比实验结果我把整个过程中做过的对比实验整理一下结果很说明问题输入方案网络结构验证集准确率原始一维波形一维CNN约78%线性幅值频谱图二维CNN约85%对数压缩Mel频谱图二维CNN92%以上同样的网络结构只是输入端数据形态不同准确率差了超过14个百分点。这个结果印证了我始终坚持的判断在信号分类任务中特征提取环节的质量决定分类效果的上限分类器只是尽可能逼近这个上限。Mel频谱图成功的原因在于它同时保留了频率结构和对数动态范围频率轴经过Mel刻度变换后更匹配心音的能量分布特点对数压缩又让微弱杂音在特征图中不再被淹没。7.2 MATLAB做这个项目的独特优势用MATLAB做心音分类和Python生态相比有几个实实在在的便利audioDatastore和melSpectrogram的组合封装度极高从原始音频到谱图特征不到20行代码trainNetwork屏蔽了反向传播和优化器的全部底层细节训练过程的可视化做得非常直观。这些集成化能力让信号处理背景的工程师专注在理解数据和调参而不是折腾环境依赖。MATLAB的坑也在复盘时值得一提。一是内存管理如果一次性把所有训练数据都生成并写入内存16GB内存的机器会接近极限。解决办法是把特征分批生成、分批保存或者用tall数组延迟计算。二是melSpectrogram要求R2019a以上版本如果版本过老需要升级或更换替代方案。三是如果GPU训练时使用了自定义层要避免在层内使用arrayfun之类的函数这可能会引发GPU兼容性问题。7.3 后续扩展方向这个项目停留在二分类只是起步。比较有工程价值的扩展方向有两个。一是多分类。正常、主动脉瓣狭窄、二尖瓣反流、室间隔缺损等不同病变在心音中的表现形式差异很大做成多分类模型需要更大的数据集和更深的网络结构但诊断信息量会提升很多。二是从离线分类走向流式处理。用滑动窗口实时分析持续输入的心音流这对可穿戴健康监测设备更有意义。可以考虑把训练好的模型用MATLAB Coder生成C代码移植到嵌入式平台或者在边缘设备上运行轻量化版本。如果你手头有心音采集设备也可以自己采集数据扩充训练集。采集时保持环境安静、传感器贴放位置稳定很重要否则引入的额外噪声会拉低模型表现。最后分享一个实际操作中的心得心音分类这类生理信号任务和自然图像分类最大的不同在于数据质量差一点、预处理糙一点模型的精度天花板就会肉眼可见地往下掉。但只要你肯在滤波、分段、特征提取这些看不见的功夫上花时间CNN带来的效果提升是非常明显的。我把这套流程跑通之后最大的收获不只是那个92%准确率的模型而是对整个信号预处理深度学习协同工作的方式有了更深的理解。如果这篇文章能帮你少走几步弯路那就很值得了。本文还有配套的精品资源点击获取