简介:本资源是一套面向人工智能与语音信号处理初学者的MATLAB语音情感识别实践方案,聚焦于从原始语音中提取情感特征并完成分类识别,适用于高校课程设计、科研入门及竞赛备赛场景。压缩包共6个文件(35KB),含4个核心MATLAB函数文件(如mfcc.m、trifbank.m等,实现梅尔滤波器组、MFCC特征提取与帧分割)、1个可视化结果.fig文件及1份说明性rtf文档,完整覆盖预处理、声学特征建模与基础分类流程。已有1207人学习下载,代码结构清晰、模块职责明确,无需额外依赖工具箱即可运行,特别适合理解语音情感识别中MFCC特征生成机制、滤波器组设计原理及MATLAB信号处理典型范式。读者可直接复现实验流程,快速掌握从语音读取、特征计算到情感判别的端到端技术链路。 最近把一套之前一直放在本地没整理的语音情感识别MATLAB源码重新翻了一遍,顺便补齐了注释和运行说明。这套代码名是speech_struggle6k9,一看就是当年随手写的内部版本号,但“语音情感识别(MATLAB源代码)”这个定位倒是一点没变。语音情感识别也叫Speech Emotion Recognition,缩写SER,核心任务是输入一段语音,让程序判断说话人当时的情绪状态,常见标签有平静、高兴、愤怒、悲伤、惊讶、恐惧、厌恶等。对做信号处理或者机器学习相关课程设计、毕业设计的人来说,这套源码提供了一条相当完整的路线:从读取音频开始,到提取特征、训练分类器、评估效果,每个环节都在里面。这篇文章我就拿这套源码做例子,把语音情感识别的实现思路、核心细节和踩坑经验一次讲清楚。
1. 项目整体设计与思路拆解
1.1 情感识别和语音识别到底有什么本质区别
刚接触语音情感识别的人,最容易把它和语音识别搞混。语音识别是把语音转成文字,关注的是“内容”,也就是每个音素、每个词到底说了什么。情感识别关注的是“怎么说”,同样一句“没关系”,平平淡淡说出来和咬牙切齿说出来,文字识别结果可能完全一样,但情感识别要能区分出这是平静还是愤怒。这个差异决定了特征设计的方向不同,语音识别更依赖声学建模和语言模型,情感识别则更看重韵律特征、频谱特征和整段语音的变化规律。
从机器学习角度看,情感识别本质是多分类问题。每个音频文件先被变换成一个特征向量,再把特征向量和对应的情感标签一起交给分类器训练。预测阶段,新音频走同样的特征提取流程,得到特征向量后喂给训练好的模型,输出情感类别。这个链路本身不复杂,但要做到稳定和泛化,中间的细节特别多,尤其体现在特征设计、数据划分和结果评估这几个环节上。
这类技术的应用场景其实很广。呼叫中心可以通过识别客户情绪做质检,智能客服可以根据用户语气调整应答策略,车载系统可以通过检测司机疲劳或愤怒状态给出安全提醒,游戏和虚拟角色也可以用情感识别做更自然的交互反馈。不过这些工程落地场景通常要考虑实时性和跨设备泛化,而目前大多数课程设计、论文实验还是在离线数据上做分类验证,两套目标的评测方式不太一样,但核心方法是一致的。
1.2 为什么选MATLAB而不是Python
经常有人问我:语音情感识别不都用Python吗?说实话,Python生态在语音领域确实更强,librosa做特征提取非常方便,PyTorch也能随意搭深度学习模型。但MATLAB在工程验证和教学场景里依然很有优势,尤其适合快速把算法流程跑通。
MATLAB的优势首先在信号处理工具箱,分帧、加窗、FFT、滤波器组这些底层操作都有现成函数,文档里还带例子,初学者不用一上来就啃DSP教材。其次是可视化方便,波形、频谱、语谱图直接画出来,对理解“特征为什么这样提”特别有帮助。再者,代码量更小,一个语音情感识别系统三百行左右就能搭出来,调试和维护成本都比较低。
当然MATLAB也有缺点,比如部分语音特征函数依赖Audio Toolbox,没装对应工具箱会直接报“未定义函数或变量”。再比如循环处理大量音频时速度一般。但这个项目的数据规模通常只有几百到几千条语音,时间开销完全可接受。如果后续要上深度学习或者做大规模实验,再把特征提取逻辑迁到Python也来得及,因为核心流程是通用的。
1.3 源码模块怎么划分
我把语音情感识别的实现分成四层:数据层、特征层、模型层、评估层。数据层负责读取音频、统一采样率、生成文件清单和标签;特征层负责预加重、分帧、加窗、提取MFCC、基频、能量这些特征;模型层负责训练分类器和预测;评估层负责计算准确率、绘制混淆矩阵、生成报告。
分层设计最大的好处是替换性特别强。比如语音情感特征不是只有MFCC,你完全可以把特征层单独换掉,用谱对比度、Chromagram或者其他特征试一试,模型层不用动。如果觉得SVM效果不好,也可以只换模型层,特征层保持不变。分层清晰以后,做实验对比其实就是换插件的过程,效率会提高很多。这套源码的原始结构虽然不像一个正式框架那么规范,但基本遵循了这个思路,这也是我后来能顺利扩展它的原因。
2. 特征提取:情感信息量化的核心环节
2.1 为什么必须先做预加重、分帧、加窗
语音是一种非平稳信号,直接对整段音频做傅里叶变换,得到的是一个“一锅炖”的频谱,不同时刻的信息混在一起,情感特征根本看不出来。解决办法是把语音切成一段一段,也就是分帧。每一帧长度在25ms到30ms左右,在这个尺度内语音可以近似看成平稳信号。为什么不直接对整个文件处理?因为情绪的表达是随时间变化的,人发怒可能突然拔高音调,平静语音则很平滑,这些局部变化正是情感识别的关键信息。
分帧之前通常先做预加重,公式是 y[n] = x[n] - a * x[n - 1],a取值一般在0.95到0.97。预加重的目的是补偿高频能量。语音信号大部分能量集中在低频,但清音和情感变化的高频细节同样重要,预加重相当于提前把高频分量抬高,避免后续特征计算时高频信息被淹没。
分帧过后要加窗,常用汉明窗。如果直接截一段信号做FFT,相当于给信号加了一个矩形窗,频谱泄漏会很严重,原本一条清晰的谱线会变成一大片拖尾。汉明窗两端趋近于零,能有效抑制这种泄漏。MATLAB里写 win = hamming(frameLen, 'periodic'),其中frameLen是采样点数量,不是毫秒。假设采样率fs=16000Hz,帧长25ms,那frameLen就是 round(fs * 0.025) = 400个采样点,帧移10ms对应160个采样点。单位搞错,后面分帧结果会完全不对,这是新手特别容易踩的坑。
2.2 MFCC、基频、能量、过零率,各管哪一块
现在常用的情感特征大致可以分成三类:韵律特征、频谱特征、音质特征。MFCC是频谱特征的典型代表。MFCC把人对声谱的听觉感知特性融入特征工程,计算流程大致是:对每一帧加窗后的信号做FFT,得到幅度谱;通过梅尔滤波器组把线性频率映射到梅尔刻度;然后取对数能量;再做DCT,保留前12到13个系数。这样每一帧语音就变成一个固定维度的MFCC向量,它是语音情感识别里最常用的基础特征。
梅尔滤波器组是MFCC里比较关键的部分,它的作用是在频域上做非线性尺度压缩。人耳对低频的分辨能力比高频强,梅尔刻度模拟的就是这种感知特性。构造滤波器组时,通常先用公式 mel = 2595 * log10(1 + f / 700) 把频率范围转换成梅尔刻度,再在梅尔刻度上均匀划分26个或40个三角滤波器,最后把线性频率映射回去。做完这步,幅度谱就被压缩成26维左右的能量输出。之后取对数并做DCT,得到每帧的“倒谱系数”。DCT的作用是把滤波器输出解相关,因为相邻梅尔滤波器输出的能量往往高度相关,直接作为特征会让数据冗余,分类器训练时也容易受影响。
基频也就是声带振动频率,俗称音高,是韵律特征里最核心的一个。人在愤怒时基频会整体抬高,悲伤时基频会降低,而且起伏变小。基频提取可以用自相关法:对一帧信号做自相关,峰值位置对应基频周期,取倒数就是基频。工程上要注意,清音段没有明显周期性,提取出来的值没有意义,所以通常需要加上下限过滤,再统计有效值的均值、方差等。
短时能量和短时过零率也是常用辅助特征。短时能量刻画声音的响度变化,愤怒语音的短时能量往往冲得很高,悲伤语音则整体低迷。短时过零率表示信号穿过零轴的次数,清音的过零率较高,浊音较低,也能间接反映语速快慢。这些特征单独拿出来不能代表情感,但组合在一起,就能从多个角度描述“语气”。
2.3 怎么把一段语音变成一个固定长度的特征向量
情感识别最终输入给分类器的,是“一句话的特征向量”和“对应的情感标签”。所以特征提取的最终目标是把一个音频文件变成一个固定长度的行向量,而不是把所有帧的原始特征直接堆在一起。假设每帧提取13维MFCC,整个文件有100帧,最后需要把这100帧的数值汇总,比如计算每个维度的均值和标准差,得到26维统计特征,再拼上基频统计、能量统计等,组成最终的特征向量。
这里有一个常见的认知误区:帧级特征矩阵不能直接丢给SVM这类传统分类器,因为不同音频的帧数不一样,特征维度不固定。如果你用LSTM或者CNN,帧级特征矩阵是可以直接用的,甚至更合适。但在课程设计和快速原型阶段,统计特征向量成本低、效果好,也更容易分析哪些特征在起作用。
拼接特征时我强烈建议定义一个固定的特征顺序表,比如前13维是MFCC均值,第14到26维是MFCC标准差,第27到29维是基频统计,第30到32维是能量统计,第33到35维是过零率统计。这样做的好处是后续做特征筛选、可视化和结果分析时,能直接知道每个维度对应谁,不用靠猜。分类器只关心维度位置,不关心特征语义,如果你顺序乱了,模型照常能跑,但结果会变得很怪且很难排查。
3. 分类器选型与实验方案
3.1 SVM为什么是小样本情感识别的首选
情感识别数据集通常都不大,能有几百到几千条语音就算不错,即便公开数据集也基本是这个量级。在样本量受限的情况下,深度学习模型容易过拟合,训练集准确率可以很高,但换一批说话人立刻露馅。SVM在这种场景下要稳定得多。
SVM通过核函数把低维特征映射到高维空间,寻找一个最大间隔分类面。用MATLAB的fitcsvm就可以实现,推荐用RBF核。RBF核有两个关键参数:BoxConstraint,也就是惩罚系数C;另一个是KernelScale,对应核宽度。C越大,模型对训练集误差的容忍越小,越容易过拟合;C越小,模型越平滑,但可能欠拟合。KernelScale越小,意味着每个支持向量的影响范围越小,模型越复杂;越大,决策边界越平滑。实际操作里,BoxConstraint可以先从0.1、1、10这几个量级试,KernelScale先设成'auto',再根据交叉验证结果微调。
使用SVM之前必须做特征标准化。MFCC、基频、能量不在一个数量级上,比如MFCC均值可能在10左右,能量统计可能到几千,SVM的间隔计算会偏向数值大的特征。标准化做法是计算训练集的均值和标准差,然后对训练集和测试集都使用这组统计量进行变换。这里有个技术细节,必须只用训练集计算均值和方差,不能把测试集也加进来算,否则就发生了信息泄漏,测试评估失去意义。
3.2 KNN、决策树和集成学习,我都试了一遍
除了SVM,我在同一套特征上还试过KNN、决策树、随机森林,这里说下直观对比。
KNN实现非常简单,测试时选择特征空间里最近的K个邻居,让邻居投票决定类别。它几乎不用训练,但KNN对特征尺度非常敏感,必须标准化。K大多取5到15,太小容易受噪声影响,太大会把其他类别样本拉进来误判。另外,KNN预测速度慢,每预测一条都要算所有训练样本的距离,数据量过千以后测试时间会明显变长。
决策树单独用效果通常不好,因为单棵树容易过拟合,训练集准确率很高,测试集下降明显。随机森林通过多棵树投票会改善一些,但情感特征之间有较强相关性,随机森林的特征随机选择机制会打一些折扣,最终准确率就我实测来说略低于SVM。不过随机森林几乎不用调参,拿来当基线模型很合适。几种模型放在同一套特征和数据划分下对比,能更全面判断特征质量,也能在报告里展示选型过程。
下面是我在实际项目里的粗略对比,只代表这一套特征和数据,不同数据集结果会有波动:
| 模型 | 大致准确率 | 训练速度 | 调参难度 | 备注 |
|---|---|---|---|---|
| SVM RBF | 较高 | 较快 | 中等 | 小样本表现稳,推荐做主模型 |
| KNN | 中等 | 无训练开销 | 低 | 预测慢,维度高时效果一般 |
| 决策树 | 偏低 | 快 | 低 | 过拟合明显,适合当可视化参考 |
| 随机森林 | 中上 | 较快 | 低 | 不用调参,适合当基线 |
3.3 数据划分必须按说话人进行
语音情感识别实验中最容易翻车的地方,就是数据划分。很多人直接对文件列表做随机交叉验证,这样同一说话人的多条情感语音很可能同时出现在训练集和测试集里,模型可能记住“这个人的声音长这样”,而不是“这类情感长这样”,准确率会虚高。
我在源码里专门写了一个按说话人划分的函数。先把音频文件名解析出说话人ID,再按说话人ID把数据分成若干折,每一折测试集对应的说话人不出现在训练集里。这样才能测出模型对新说话人的泛化能力,这也更接近实际应用场景。
提示:网上很多语音情感识别Demo声称准确率高达90%以上,一部分是用随机划分做出来的。如果你答辩或写论文也要报这个准确率,被问到“测试集说话人与训练集是否有重叠”时很容易解释不清。改成按说话人划分,准确率可能低几个点,但结论站得住脚。
4. 实操过程:从文件到识别结果
4.1 数据准备与文件组织
我用的数据是开源情感语音库。中文场景下常用CASIA,英文场景可以用RAVDESS或eNTERFACE。不同数据库的标签格式不一样,RAVDESS文件名很长,里面包含了说话人编号、情感类别、强度等字段,解析时得仔细阅读官方说明;CASIA按目录划分情感类别,直接读取目录名就能得到标签,省事很多。
文件命名尽量规范,我推荐“说话人ID_情感标签_句子编号.wav”这种格式,比如02_neutral_01.wav。这样解析标签只需要按下划线拆分文件名。如果文件名不规范,就必须额外维护一份标签表,会很麻烦。数据准备阶段还要统一采样率和声道数,我习惯统一成16kHz单声道WAV。16kHz对语音识别和情感识别都够用,采样率过高会增大计算量,过低则损失高频信息。
4.2 特征提取核心代码框架
下面是我整理后的核心特征提取函数骨架,简化掉了调试打印代码,保留主流程。你在自己项目里可以拿它做基础,再按需加特征。
function feat = extractFeatures(audio, fs) % 预加重,补偿高频分量 audio = filter([1, -0.97], 1, audio); % 分帧参数:帧长25ms,帧移10ms frameLen = round(fs * 0.025); hopLen = round(fs * 0.010); % 分帧并加汉明窗 frames = buffer(audio, frameLen, frameLen - hopLen, 'nodelay'); win = hamming(frameLen, 'periodic'); frames = frames .* win; % 对每一帧计算幅度谱,并经过梅尔滤波器组和DCT得到MFCC nfft = 2^nextpow2(frameLen); mfccAll = zeros(size(frames, 2), 13); for i = 1:size(frames, 2) spec = abs(fft(frames(:, i), nfft)); spec = spec(1:round(nfft / 2) + 1); % melFilter 是自建的梅尔滤波器组矩阵,这里省略具体构造细节 melSpec = melFilter * spec; logMel = log(melSpec + eps); dctCoeff = dct(logMel); mfccAll(i, :) = dctCoeff(1:13); end % 汇总统计:均值 + 标准差 feat = [mean(mfccAll, 1), std(mfccAll, 0, 1)]; end如果机器上装了Audio Toolbox,可以直接调用内置mfcc函数,代码会再短一截。但自己实现一遍梅尔滤波器组对理解原理帮助很大,建议至少尝试一次。调试时可以把每一帧的MFCC数值打出来,看看数值范围是否符合常识,这能帮你尽早发现预加重或窗口写错的问题。
4
本文还有配套的精品资源,点击获取