恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
基于MATLAB的心音分类器实战:从特征提取到模型训练全流程
首页
资讯中心
/
基于MATLAB的心音分类器实战:从特征提取到模型训练全流程
基于MATLAB的心音分类器实战:从特征提取到模型训练全流程
发布时间:2026/10/4 16:39:29
前阵子学院学生科技协会的周报约稿我想着与其讲一堆机器学习理论不如直接带大家动手跑通一个小项目。正好手边有公开的心音录音数据于是就用MATLAB搭了一个端到端的心音分类器读取音频、做预处理、提取特征、训练分类模型、评估效果一条线走完。这个项目特别适合刚接触机器学习的同学因为它的技术栈覆盖了特征提取和分类这两条主线而且能直接看到模型从“听不懂”到“能区分正常心音和异常心音”的整个过程。今天就把完整流程、代码逻辑、参数选择依据和踩过的坑都整理出来照着做就能复现。1. 心音分类到底在做什么项目定位与整体思路1.1 为什么拿心音当机器学习入门题目心音分类本质上是个二分类问题给一段心音录音判断它是正常还是异常。这个任务看起来跟经典的猫狗分类差不多但有一个很大的差别——输入不是图像而是一维的音频信号。这就意味着你不能直接把它塞进卷积神经网络里完事而是要先把一维信号转换成有意义的数值特征再交给分类器。这个“从信号到特征再到分类”的路径恰恰是机器学习项目里最考验功力的部分。很多同学学机器学习时直接跳到模型训练把数据一导入就开始调参最后发现效果不好也不知道问题出在哪。心音分类逼着你先理解信号本身的物理含义正常心音有清晰的第一心音S1和第二心音S2异常心音往往夹带着杂音或节律紊乱这些差异能不能被数值特征捕捉到直接决定了分类器的上限。另外心音数据在公开数据集上很容易拿到PhysioNet 2016心音数据集就是最常用的一个里面有数千条带正常/异常标签的录音。对咱们学生党来说数据门槛低、任务明确、医学背景知识不需要很深是非常友好的练手素材。1.2 从原始音频到分类结果需要走五步整个项目可以拆成五个阶段数据准备、信号预处理、特征提取、模型训练、效果评估。前两步是让原始音频变成干净统一的输入第三步是把一段声音压缩成一行数字特征第四步是用这些特征训练一个分类器最后一步是判断这个分类器到底靠不靠谱。这五个阶段并不是互相独立的。比如你在特征提取阶段选择了哪些特征会直接决定后面用什么模型效果更好而评估阶段如果发现模型过拟合又得回头检查预处理和特征提取有没有问题。所以我在做这个项目时反复提醒自己不要一上来就追求高大上的模型先把流程跑通再逐步优化。我用的整体技术路线是把每段录音统一重采样到2000 Hz做带通滤波去掉噪声然后提取时域、频域和小波分解三类特征拼成一个特征向量最后对比SVM、决策树、KNN和随机森林这几种常见分类器的表现。1.3 环境准备其实只需要两个工具箱开始写代码之前先把环境说清楚。我用的是MATLAB R2021a实际测试下来R2018a之后的版本都能跑通核心依赖是两个工具箱Signal Processing Toolbox负责滤波和频谱分析Statistics and Machine Learning Toolbox负责分类器和评估函数。如果是想提取MFCC这类语音特征还需要Audio Toolbox但这不是必需的后面我会讲替代方案。对了MATLAB建议通过学校正版授权平台或者官网试用版安装安装的时候千万别图省事只装默认组件记得把Signal Processing Toolbox和Statistics and Machine Learning Toolbox勾上不然运行到一半报错找不到函数很影响心情。2. 数据预处理不要让脏数据毁掉你的特征2.1 先把音频对齐采样率、声道和时长心音数据集里的录音来源很杂采样率不统一有的是1000 Hz有的是2000 Hz甚至还有4410 Hz的。如果你的特征提取代码对每段音频都用了同样的处理流程但采样率不一致提取出来的频谱特征就没有可比性。所以第一步是把所有音频统一重采样到一个固定的采样率。我选择2000 Hz原因有两点第一心音的主要能量集中在20到500 Hz之间根据奈奎斯特采样定理4000 Hz以上的采样率就足够保留这段频带的信息了2000 Hz虽然会砍掉一部分超高频成分但对心音分类任务来说损失很小第二数据集中有很多原始采样率就是2000 Hz的样本统一到这个值能减少重采样带来的信号畸变。声道问题也容易踩坑。有的录音是双声道读出来是个两列矩阵如果直接丢进特征提取函数里就会出错。我习惯在读取后立刻判断size(x, 2)的值如果是2就取第一列。另外不同录音的时长差异很大从几秒到几十秒都有需要在预处理时统一截取固定长度我选了5秒太短会丢失完整的心动周期太长又没必要。function x load_and_preprocess(filename, fs_target, duration_sec) [x, fs] audioread(filename); % 双声道转单声道 if size(x, 2) 1 x mean(x, 2); end % 统一采样率 if fs ~ fs_target x resample(x, fs_target, fs); end % 截取固定长度 n_samples fs_target * duration_sec; if length(x) n_samples x x(1:n_samples); else x [x; zeros(n_samples - length(x), 1)]; end end2.2 滤波和归一化滤掉什么、保留什么心音信号虽然主要能量在500 Hz以下但实际录音里混着不少干扰呼吸声、环境噪音、仪器工频干扰还有皮肤摩擦产生的低频漂移。这些干扰如果不处理会被特征提取算法当成有效信息学进去最后模型学到的是“噪音模式”而不是“心音模式”。我用的是带通滤波通带设在25到400 Hz之间。高通部分选择25 Hz是为了滤掉基线漂移和低频呼吸声低通部分选择400 Hz是为了保留心音中有诊断价值的频率成分同时去掉高频噪声。这里有一个经验滤波器的阶数别选太高否则会产生振铃效应在信号边缘出现虚假波动。用MATLAB的bandpass函数时默认参数一般就够用了如果你的版本太老没有这个函数可以用designfilt自己设计一个IIR带通滤波器替代。滤波之后的归一化也很关键。不同人录心音时音量不一样有的录音响度大有的响度小如果直接用原始幅值提取特征音量差异会盖过真正的生理差异。我会把每段信号除以它的最大绝对值把所有样本的幅值范围压缩到[-1, 1]之间。注意归一化一定要在滤波之后做否则滤波器的增益会改变信号幅值分布先滤波再归一化才能保证每段信号处于同一尺度。2.3 数据划分训练集、验证集和测试集要分层数据划分看起来简单其实有很多细节。心音数据集通常类别不平衡异常样本和正常样本数量差距可能达到两倍以上。如果你直接随机划分很容易出现训练集里正常样本特别多、测试集里异常样本特别少的情况导致评估指标虚高。正确的做法是用分层抽样保证训练集和测试集里正常/异常的比例与原始数据集一致。MATLAB的cvpartition函数支持Stratify选项一句话就能搞定。cv cvpartition(Y, HoldOut, 0.3, Stratify, true);还有一点同一个患者可能贡献了多条录音这些录音应该放在同一个集合里不能一条进训练集、一条进测试集否则模型在训练时已经“见过”这个患者的特征测试结果会偏乐观。PhysioNet 2016数据集正好提供了患者编号这一步能做就做。我自己在初版代码里偷懒没做患者级别划分结果测试准确率高达95%后来按患者划分后掉到了82%这才是真实水平。3. 特征提取从“听”到“算”的关键一跃3.1 时域特征直接从波形里挖信息特征提取是整个项目里最核心也最需要理解的一步。我们先从时域特征开始因为它最直观——波形长什么样特征就怎么描述。我提取的第一组特征是均方根值、方差、峰值和过零率。均方根值反映整段信号的总体能量异常心音常常伴随杂音能量分布会比正常心音更分散方差衡量波形的离散程度峰值能捕捉到异常的大幅波动。过零率统计信号在单位时间内穿越零轴的次数正常心音的波动有比较规律的节律而过零率过高可能意味着信号里出现了高频杂音。包络特征也很重要。心音的包络可以理解为“音量随时间变化的轮廓”医生听诊时听到的“咚咚”声强弱变化在包络上就表现为多个峰。用希尔伯特变换可以提取包络然后计算包络的标准差、峰值和均值之比。异常心音的杂音通常让包络变得毛糙这些指标能很好地反映出来。function feat extract_time_features(x, fs) feat zeros(1, 6); feat(1) rms(x); % 均方根值 feat(2) var(x); % 方差 feat(3) sum(diff(sign(x)) ~ 0) / length(x); % 过零率 feat(4) max(abs(x)); % 峰值 env abs(hilbert(x)); % 希尔伯特包络 feat(5) std(env); % 包络标准差 feat(6) max(env) / mean(env); % 包络峰值均值比 end3.2 频域特征把能量分布摊开看听诊时医生常说“这个杂音出现在收缩期”或者“高频成分太多”这其实就是频域的概念。正常心音的能量集中在低频段而某些异常心音会在特定频段出现额外的能量聚集。我计算频域特征用的是Welch功率谱密度估计MATLAB的pwelch函数它把信号分段加窗做FFT再平均比直接对整个信号做FFT平滑得多。基于功率谱密度我提取了频谱质心和频谱展度前者描述能量集中在哪个频率附近后者描述能量在频率轴上的分散程度。还额外计算了两个子带的能量占比25到100 Hz低频段和100到400 Hz高频段。选择这两个子带有明确的生理意义——第一心音、第二心音的主频在50到100 Hz左右而很多瓣膜狭窄或关闭不全产生的杂音频率更高。如果高频段能量占比异常升高很可能意味着存在杂音。function feat extract_spectral_features(x, fs) [pxx, f] pwelch(x, hann(2048), 1024, 4096, fs); % 频谱质心 feat(1) sum(f .* pxx) / sum(pxx); % 频谱展度 feat(2) sqrt(sum((f - feat(1)).^2 .* pxx) / sum(pxx)); % 子带能量占比 idx_low (f 25) (f 100); idx_high (f 100) (f 400); feat(3) sum(pxx(idx_low)) / sum(pxx); feat(4) sum(pxx(idx_high)) / sum(pxx); end3.3 小波分解在不同尺度上看信号细节时域特征和频域特征虽然有用但它们一个丢失了频率信息一个丢失了时间信息。心音信号往往是“短时突变”的比如异常杂音可能只出现在心动周期的某个阶段这种局部特征在全局统计量里容易被平均掉。小波分解能在不同频率尺度上同时保留时间信息适合捕捉这类局部变化。我用db4小波对信号做6层分解分解后得到6个细节系数序列和一个近似系数序列。每个尺度对应不同的频率范围细节系数的能量分布反映了信号在不同频带上的活动强度。正常心音和异常心音在一些特定尺度上的能量差异会很明显比如高频细节系数的能量总和。function feat extract_wavelet_features(x) [C, L] wavedec(x, 6, db4); details_energy zeros(1, 6); total_energy sum(C.^2); for k 1:6 d detcoef(C, L, k); details_energy(k) sum(d.^2) / total_energy; end feat details_energy; end有一类特征我要单独说一下梅尔频率倒谱系数MFCC。它是语音识别里的经典特征因为心音本质上也是声学信号很多论文证明MFCC对心音分类有效。如果你的MATLAB装了Audio Toolbox一行mfcc函数就能提取。没有也没关系用上面的时域、频域、小波特征组合已经能得到不错的分类效果。我实际对比过加不加MFCC对最终准确率的影响大概在2到3个百分点不是决定性的。3.4 特征矩阵长什么样把上述三类特征拼接起来每段录音变成16维的特征向量5个时域特征实际我代码里写了6个、4个频域特征、6个小波能量特征最后加一个原始信号总能量。所有样本的特征向量按行堆叠就得到一个N乘16的特征矩阵X对应的标签向量Y是N乘1的categorical类型。到这里信号已经彻底变成了表格数据。接下来分类器拿到的不再是“声音”而是一行行数字模型要做的就是从这组数字里找出正常和异常之间的分界线。4. 分类器选型与训练让模型代替耳朵做判断4.1 候选模型的选择逻辑特征提取完成后分类器的选择就相对程式化了。我在这里对比了四种常见模型SVM、决策树、KNN和随机森林。整个特征提取和分类的流程来看这个项目最适合用来做模型对比实验。选SVM是因为它在小样本高维数据上表现稳定RBF核能拟合非线性边界是机器学习入门必学的模型。选决策树是因为它可解释性最强能直接可视化出决策路径——比如“频谱质心大于某个阈值且小波高频能量占比小于多少时判断为异常”。选KNN是因为它最直观不需要训练过程但也最依赖特征尺度。选随机森林是因为它属于集成学习用多棵决策树投票通常能压过单模型。不建议一上来就上深度学习。心音数据集的样本量通常在几千这个量级训练一个神经网络容易过拟合而且调参成本高、解释性差对入门项目来说性价比很低。4.2 训练流程与关键参数先做一个系统的特征归一化。SVM和KNN都依赖样本之间的距离如果特征量纲不统一比如频谱质心是几百、过零率是零点几距离计算就会完全被量纲大的特征主导。我用z-score归一化让每个特征维度均值为0、标准差为1。这里有一个容易犯的错误归一化参数只能用训练集的数据来估计然后把同样的均值和标准差应用到测试集上。如果你先对全部数据做归一化再划分训练集和测试集测试集的信息就泄露到了训练过程中评估结果会偏乐观。% 只用训练集拟合归一化参数 mu mean(XTrain); sd std(XTrain); XTrain_norm (XTrain - mu) ./ sd; XTest_norm (XTest - mu) ./ sd;四种模型的训练代码都不复杂。SVM的关键参数是核函数和KernelScaleRBF核下我会让KernelScale设为auto让MATLAB自动估计合适的尺度决策树可以限制最大分裂数防止过拟合随机森林用fitcensemble的Bag方法树的数量设50棵左右就够用了太多会明显增加训练时间但对准确率提升不大。% SVM svmModel fitcsvm(XTrain_norm, YTrain, ... KernelFunction, rbf, ... KernelScale, auto); % 随机森林 rfModel fitcensemble(XTrain_norm, YTrain, ... Method, Bag, ... NumLearningCycles, 50);4.3 交叉验证与随机种子让结果可复现训练模型时有两个小细节值得注意。一是设置随机种子fitcensemble内部有随机过程不设种子的话每次跑的准确率都不一样设了rng(2024)才能保证结果可复现方便后面调参对比。二是用小规模交叉验证来快速筛选参数而不是每次都用一大半数据做训练。我在调参阶段用5折交叉验证每折验证一次取平均准确率作为模型打分的标准。交叉验证的另一个好处是能看到模型的稳定性。如果5折的准确率分别是0.80、0.82、0.79、0.84、0.81这个模型就相对靠谱如果出现0.90和0.70这样的大幅波动说明模型对数据划分非常敏感背后很可能是特征选择有问题或者数据里有异常样本。5. 评估与调优准确率不是唯一指标5.1 混淆矩阵与灵敏度/特异度训练完模型后最忌讳的就是只看一个整体准确率。我见过太多同学跑到这一步打印出Accuracy0.85就以为完事了实际上这个数字可能掩盖了严重的类别不平衡问题。正确做法是看混淆矩阵。MATLAB的confusionchart函数可以直接可视化横轴是真实标签、纵轴是预测标签。从混淆矩阵能算出两个更细致的指标灵敏度即异常样本中被正确识别出来的比例特异度即正常样本中被正确识别出来的比例。在医疗场景里灵敏度尤其重要——漏掉一个异常心音比把正常心音误判为异常后果严重得多。pred predict(svmModel, XTest_norm); confusionchart(YTest, pred);我做出来的SVM模型准确率在85%左右但细看混淆矩阵发现灵敏度只有72%也就是说有接近三成的异常心音被漏掉了。如果只看准确率这个模型似乎还行但放到实际场景里漏掉三成病人是完全不可接受的。这就是为什么需要细分指标。5.2 用ROC和AUC防止被阈值骗了SVM默认以0.5作为决策阈值但实际应用中这个阈值不一定最优。ROC曲线能展示分类器在所有可能阈值下的性能表现横轴是假阳性率纵轴是真阳性率。曲线越靠近左上角说明模型在所有阈值下都表现良好。曲线下面积AUC是一个更稳定的综合评价指标。AUC0.5说明模型毫无区分能力等于随机猜AUC0.9以上说明区分能力很强。我用perfcurve函数绘制ROC曲线发现SVM的AUC在0.87左右随机森林能达到0.91说明在“重排序”这个层面上随机森林更可靠。5.3 过拟合的排查思路我初版代码在训练集上的准确率高达97%但测试集只有80%明显的过拟合。排查下来有三个原因特征维度相对样本量偏多、个别样本极度异常、模型超参数没有约束。解决办法是先做特征选择用relieff函数或者简单的单变量筛选去掉那些对分类贡献不大的特征我最后从16维降到了11维测试集精度反而小幅回升。然后是清洗数据检查有没有因为录音质量问题产生的坏样本比如全是静音、全是爆音、长度截取失败导致的补零特征。最后是给模型加正则化约束比如决策树限制最大深度SVM增大惩罚系数C等。6. 踩坑清单与进阶方向6.1 我实际踩过的几个坑整理一下整个过程中最值得注意的问题给后来人当参考问题表现解决办法患者级别数据泄露准确率虚高约10%按患者编号划分数据同一患者不进两个集合归一化参数用了全量数据测试集指标偏乐观只用训练集计算均值和标准差音频长度不统一特征矩阵对齐失败或补零过多统一截取5秒不足的做静音补零小波分解没归一化不同录音总能量差异主导特征用细节能量占比替代绝对能量类别不平衡准确率高但灵敏度低看混淆矩阵而非准确率必要时用类别权重6.2 还能往哪走三个值得做的扩展做完这个基础版本后有三个很有价值的扩展方向。方向一是做心音分割先把每段录音切分成S1、收缩期、S2、舒张期四个部分再分别提取特征这样能捕捉到杂音出现在哪个时相的诊断信息比全局统计特征精细得多。方向二是换更强的特征比如样本熵、近似熵这类非线性动力学特征对心音这种混沌性较强的信号往往有奇效。方向三是引入更现代的分类器在保持特征工程不变的前提下尝试XGBoost或LightGBM这两个梯度提升框架在表格数据上通常比随机森林更有优势。我自己在做这个项目的过程中最大的体会是特征工程和模型调参的时间投入比应该至少在6比4甚至7比3。很多初学者喜欢把大量时间花在调模型的超参数上但心音分类这种任务决定上限的永远是特征能不能把正常和异常的本质差异表达出来。把信号理解透了特征提取做好了哪怕用一个最简单的SVM也能获得不错的效果。最后再分享一个小技巧在MATLAB里做实验时把所有特征提取和模型训练的代码封装成独立的函数然后用脚本串起来跑这样想换模型、换特征都很方便不用来回改一坨代码。