简介:这份基于深度学习的语音情感识别系统MATLAB实现,面向语音信号处理与深度学习入门开发者,可用于完成从语音预处理、MFCC特征提取到模型训练与情感分类的完整实验流程。压缩包共9个文件,其中5个.mat情感数据集涵盖愤怒、高兴、中性、悲伤、恐惧五类样本,4个.m算法脚本提供PNN、BPNN、LVQ等可对比运行的模型实现,数据与代码分离便于复用,整体仅260KB,轻量易用。已有455人学习/下载,适合课程设计、毕业设计或算法验证场景,尤其适合需要快速搭建语音情感识别原型的开发者。借助源码中的模块化代码与注释,读者不仅能快速复现各类情感识别实验,还能理解深度学习模型与经典神经网络在特征建模上的差异,并利用MATLAB深度学习工具箱向CNN、LSTM等结构平滑扩展,从而完成参数调优、准确率对比评估与系统GUI化部署。
1. 语音情感识别:不用大模型,MATLAB也能从录音里译出五种情绪
先讲个场景。某公司要做客服录音质检,人工听一遍录音再打标签,几千条样本听下来,标注员的情绪先崩溃了。后来他们把问题换成机器做——语音情感识别,就是从一段语音里判断说话人当下是愤怒、开心、悲伤、恐惧还是平静。这类任务并不一定都要上GPU集群和千亿参数大模型,数据量不大时,一套MATLAB脚本加三个经典神经网络模型就能跑出能用的结果。这篇要拆的正是这样一个工程:源码包里自带五种情感样本,对应愤怒、快乐、中性、悲伤和恐惧,训练脚本则覆盖了PNN、BPNN和LVQ三套算法,主脚本把数据、训练和评估串成一条完整流程。适合想快速落地一个情感识别Demo、又不想被深度学习框架细节缠住的从业者。
2. 五种情感样本与特征提取:从.mat语音文件到MFCC特征矩阵
这套工程的数据和脚本组织得很直白。压缩包里五个.mat文件对应五类情感样本,三个.m文件对应三套训练脚本,外加一个主脚本负责调度。数据是语音情感识别最容易卡住的第一步,所以先把数据格式和特征工程讲透。
2.1 数据文件与脚本文件的对应关系
先看文件清单,每个文件都对应明确的职责。
| 文件 | 类型 | 内容/职责 |
|---|---|---|
| W_anger.mat | 样本数据 | 愤怒情感的语音特征样本集 |
| F_happiness.mat | 样本数据 | 快乐情感的语音特征样本集 |
| N_neutral.mat | 样本数据 | 中性情感的语音特征样本集 |
| T_sadness.mat | 样本数据 | 悲伤情感的语音特征样本集 |
| A_fear.mat | 样本数据 | 恐惧情感的语音特征样本集 |
| pnn.m | 训练脚本 | 概率神经网络训练函数 |
| bpnn.m | 训练脚本 | BP神经网络训练函数 |
| lvq.m | 训练脚本 | 学习矢量量化网络训练函数 |
| C12_2_y.m | 主脚本 | 数据加载、特征组装、训练调度与对比评估 |
负载前先确认数据存储格式。常见做法是每个.mat文件里存一个变量,变量名可能是data、feat之类,也可能直接存原始波形。我一般会先用whos探查一下文件里的变量名和维度,而不是盲目load:
% 探查W_anger.mat结构 info = whos('-file', 'W_anger.mat'); disp(info.name); disp(info.size); % 加载并查看矩阵规模 S = load('W_anger.mat'); fn = fieldnames(S); feat = S.(fn{1}); fprintf('样本数=%d, 特征维数=%d\n', size(feat,1), size(feat,2));这段代码第1行用whos('-file')直接读文件头,不需要把数据载入工作区,适合快速确认变量结构。第2步的load才是真正把数据读进来,fieldnames动态取第一个字段名,避免不同.mat文件变量名不一致导致脚本报错。样本数和特征维数打印出来后,基本就能判断这份数据是原始波形还是已经提好的特征。如果特征维数在13到40之间,多半是MFCC或类似声学特征;如果维度上千,则可能是频谱序列。
2.2 原始语音的预处理:重采样、去噪、分帧与加窗
不是所有.mat文件里都存好了特征,很多标着情感的数据集实际存的是原始波形。这时预处理就是整个系统稳定性的地基,预处理做不好,后面换什么模型都救不回来。
% 读取原始音频并做基础预处理 [audio, fs] = audioread('sample.wav'); % 统一采样率到16kHz,避免不同来源录音采样率不一致 targetFs = 16000; if fs ~= targetFs audio = resample(audio, targetFs, fs); fs = targetFs; end % 简单去直流偏置,消除录音设备带来的常数偏移 audio = audio - mean(audio); % 分帧:25ms一帧,帧移10ms frameLen = round(0.025 * fs); frameShift = round(0.010 * fs); frames = buffer(audio, frameLen, frameLen - frameShift, 'nodelay'); % buffer的第三参是overlap,实际重叠为 frameLen - frameShift这段代码把原始波形处理成帧矩阵。resample解决采样率不统一的问题,很多开源录音有8kHz也有44.1kHz,不统一会导致特征维度对不上。frameLen用秒数乘采样率计算,而不是写死一个数字,这样以后换采样率不用改代码。buffer是信号处理最常用的分帧函数,它的第三个参数容易写错,写的不是帧移而是重叠长度,所以是frameLen - frameShift,很多人在这一步把维度算错,后面特征矩阵就全乱了。
2.3 MFCC特征提取与特征矩阵组装
分帧加窗之后,下一步是把每一帧变换成MFCC系数。梅尔频率倒谱系数是目前语音情感识别里最常用的手工特征,它把频谱按人耳感知的非线性刻度重新映射,对音调和音色信息保留得比较好。MATLAB的Audio Toolbox里直接提供了mfcc函数,但要注意版本差异,旧版本或没有该工具箱的机器需要自己写滤波器组。
% 为每一帧计算13维MFCC numCoeffs = 13; [coeffs, ~] = mfcc(audio, fs, 'WindowLength', frameLen, ... 'OverlapLength', frameLen - frameShift, 'NumCoeffs', numCoeffs); % coeffs: 帧数 x 13 的矩阵 % 对该段语音做统计池化,把变长帧序列压成固定长度向量 featMean = mean(coeffs, 1); featStd = std(coeffs, 0, 1); featDelta = mean(diff(coeffs), 1); featVec = [featMean, featStd, featDelta]; % featVec: 1 x 39 的定长特征这步是整个特征工程的关键。一段语音的帧数随录音时长变化,直接拼接会导致不同样本特征维度不一致,模型根本没法训练。我一般会把帧级别的MFCC序列做三个统计量:均值描述整体音色、标准差描述语调波动、差分均值描述情感起伏的斜率。三项拼起来得到一个39维的定长向量。这样做的好处是丢掉了时序细节,但大幅降低了特征维度,对PNN和LVQ这类小模型反而更友好。如果后续要用LSTM建模完整时序,才需要保留完整帧序列而不是池化。
3. 训练三套网络:PNN、BPNN、LVQ的MATLAB实现与参数边界
特征准备好之后,进入模型训练环节。这套工程里提供了三种网络脚本,很多人误以为三个都要跑一遍然后挑个最好的,实际情况是三者的适用场景有明显差异。下面把每个模型的原理、MATLAB调用方式和参数边界说清楚。
3.1 PNN概率神经网络:训练成本最低的基线模型
PNN的核心是径向基函数加一层竞争输出。它在训练阶段并不需要反向传播迭代,而是直接把训练样本作为径向基中心存下来,预测时计算新样本到每个中心的距离,距离最近的那个类别获胜。所以PNN的特点是训练极快、永远不会陷入局部最优,但代价是模型体积和样本量线性相关,几千条样本就要存几千个中心。
% PNN训练:P_train为特征矩阵(特征维数 x 样本数),Tc为类别索引 T_pnn = ind2vec(Tc); % 将类别索引转为one-hot向量 spread = 0.5; % 径向基宽度,关键超参 net_pnn = newpnn(P_train, T_pnn, spread); Y_pnn = sim(net_pnn, P_test); [~, pred_pnn] = max(Y_pnn, [], 1);这里spread的参数含义是径向基函数的平滑宽度。spread越小,决策边界越尖锐,容易过拟合训练样本;spread越大,边界越平滑,但太大时所有类别都被模糊成一团。常见做法是在归一化后的特征空间里从0.1到2.0做网格扫描,用小验证集选值。注意ind2vec做类别编码时,类别索引必须是1到5的正整数,不能出现0或跳号,否则编译出来的目标矩阵行数不对。
3.2 BPNN反向传播网络:可调参数最多的主力模型
BPNN是三个模型里最像"正经深度学习"的一个,多层感知机加反向传播,是资源包里的主力模型。它需要设定的参数最多,也因此最容易翻车。MATLAB老脚本里常用newff,新版脚本用feedforwardnet替代,两者核心结构相同,只是新函数封装了更多默认行为。
% BPNN训练:隐藏层两层,神经元数15和10 net_bp = newff(P_train, T_pnn, [15 10], {'logsig', 'logsig'}, 'trainscg'); net_bp.trainParam.epochs = 1000; net_bp.trainParam.goal = 0.01; net_bp.trainParam.lr = 0.01; % 学习率,震荡时优先调小 [net_bp, tr] = train(net_bp, P_train, T_pnn); Y_bp = sim(net_bp, P_test); [~, pred_bp] = max(Y_bp, [], 1);隐藏层神经元数量没有标准答案,我习惯从样本量的十分之一起步:500条训练样本就先试10到15个神经元,欠拟合就加,过拟合就减。trainscg是标量共轭梯度法,比普通traingd收敛稳定得多,对小数据集尤其友好。如果训练误差曲线一直在锯齿状震荡,优先检查lr而不是加深网络,学习率从0.001起步比较稳妥。注意newff的输出层维度必须和T_pnn的行数一致,所以这里直接复用了前面ind2vec的结果。
3.3 LVQ学习矢量量化:带标签的原型聚类
LVQ的定位介于PNN和BPNN之间。它把每个类别用若干原型向量表示,训练过程中原型向量不断往样本靠拢,预测时看新样本离哪个原型最近。和PNN的区别是,LVQ的原型数量远小于训练样本数,而且原型是有类别标签的,本质上是带监督的原型聚类。
% LVQ训练:第一参数是特征矩阵,第二参数是原型数,第三参数是各类比例 protoCount = 12; % 12个原型,均匀分配到5类不现实,故用比例 classRatio = [0.2 0.2 0.2 0.2 0.2]; % 各类别占比,必须和为1 lr = 0.01; net_lvq = newlvq(P_train, protoCount, classRatio, lr); net_lvq = train(net_lvq, P_train, T_pnn); Y_lvq = sim(net_lvq, P_test); [~, pred_lvq] = max(Y_lvq, [], 1);classRatio是LVQ最特殊的参数,它表示原型向量在各类别之间的分配比例,所有元素加起来必须等于1。如果原始数据里五类样本量不均衡,这个比例应该按实际样本占比设置,而不是机械地平均分配。原型总数protoCount设得太少,类别边界拟合不够;设得太多,模型又退化成PNN那样的存储型分类器。常见做法是从每类2到3个原型起步,也就是总数10到15之间,再根据验证集表现增减。
4. 训练与预测的五个常见坑:从特征维度到spread玄学
这套工程跑起来不难,但真正把三套模型都调出可用效果,至少会踩四五个坑。下面按我自己和周围同事的真实经历,把高频问题按"现象→原因→解决"拆开讲,每条都对应代码层面的具体改动。
4.1 特征矩阵维度不齐,拼接时报错
现象:训练前想把五类样本横向拼接成一个大矩阵,用cat(2, feat1, feat2)时报"维度不一致"。
原因:不同.mat文件里存储的语音长度不一样,帧数不同,池化前的MFCC矩阵行数不同。如果直接拼接帧级特征,必然报错。
解决:统一做统计池化,把每段语音压成39维向量再拼接。或者在分帧阶段截断/补齐到固定帧数。前者信息有损但简单可靠,适合小模型;后者保留完整时序,适合后续接LSTM。
4.2 训练准确率高、验证集惨不忍睹
现象:训练集上三套模型准确率都超过95%,验证集直接掉到60%以下。
原因:数据划分时按帧或按短片段随机打乱,同一段录音的相邻帧被分到了训练和验证两侧,出现数据泄漏。模型实际记住了录音本身的特征,而不是情感类别。
解决:按文件粒度划分数据集。对每一类情感,先把样本文件整体打乱,再按比例切出训练集和验证集,绝不把同一条语音的不同片段拆到两侧。
4.3 PNN的spread参数像玄学
现象:spread默认设为1.0,某一类情感的识别全错,改成0.2或0.8效果差异巨大。
原因:spread的取值必须匹配特征空间的欧氏距离尺度。MFCC系数没有归一化时,特征值分布范围大,1.0可能太大导致所有类别混在一起;归一化后距离尺度完全不同,原来的值又可能太小导致过拟合。
解决:特征先做mapminmax归一化到0到1区间,再用验证集对spread做网格扫描,备选值从0.1、0.2、0.5、1.0、1.5、2.0里挑,不要只试一个默认值。
4.4 BP训练震荡,损失不降反升
现象:训练误差曲线像锯齿,前200轮还在下降,后面突然反弹,最终准确率勉强过半。
原因:最常见是学习率设得太大,权重更新跨过了最优区域。其次是特征没有归一化,某些维度数值量级大,在反向传播中主导了梯度更新。
解决:先把特征归一化,再把学习率降到0.001到0.01区间。隐藏层激活函数优先用logsig或tansig,除非有特殊理由,否则不要在这么小的数据量上尝试ReLU。
4.5 LVQ某一类情感全错
现象:五类里四类识别正常,唯独某一类全部被分到相邻类别。
原因:一是该类样本量太少,原型分配不足;二是classRatio按平均分配,实际样本占比失衡时,少数类没有足够的原型覆盖。
解决:先统计各类样本数,按实际比例重设classRatio,并且让protoCount足够大,保证少数类至少分到2个原型。如果仍然全错,检查类别索引和ind2vec的编码顺序是否一致,这类问题多数出在标签错位上。
5. 模型部署:打包、批量识别与最简单的GUI交互
模型训练好之后,真正能用的状态是把模型保存下来,然后对新的录音做批量预测。这一步不用做得很重,但三件事必须落地:模型持久化、批量预测脚本、和最小可用的交互界面。
5.1 模型保存与复用
MATLAB里训练好的网络结构全部封装在net对象里,直接用save存成.mat文件即可。我习惯把三套模型和对应的归一化参数打包存成一个文件,避免预测时还要重新算归一化边界。
% 保存模型与归一化参数,避免预测时重新计算 save('emotion_model.mat', 'net_pnn', 'net_bp', 'net_lvq', ... 'ps_input', 'ps_output'); % ps_input/ps_output是mapminmax返回的归一化结构体加载的时候用load把整个文件读进来,不要单独保存某几个变量然后遗漏归一化参数。实际预测时先对特征做同样的mapminmax('apply', feat, ps_input),再喂给网络,顺序反了预测结果基本全错。
5.2 批量预测文件夹内的新录音
批量识别是实际应用里最常用的形态,把一批wav文件丢进文件夹,脚本输出每段语音的情感标签。
% 批量预测:遍历文件夹内所有wav files = dir(fullfile('new_audio', '*.wav')); results = table(); for i = 1:length(files) [audio, fs] = audioread(fullfile(files(i).folder, files(i).name)); if fs ~= targetFs audio = resample(audio, targetFs, fs); end coeffs = mfcc(audio, targetFs, 'WindowLength', frameLen, ... 'OverlapLength', frameLen - frameShift, 'NumCoeffs', numCoeffs); feat = [mean(coeffs), std(coeffs), mean(diff(coeffs))]; featNorm = mapminmax('apply', feat, ps_input); [~, label] = max(sim(net_bp, featNorm')); results = [results; {files(i).name, label}]; end disp(results);这段代码里有两个细节值得注意。一是循环内没有对音频长度做判断,过短的录音计算diff时会得到空矩阵,跑起来会报错,稳妥做法是先加一个最短时长判断;二是mapminmax('apply', ...)必须使用训练时保存的ps_input结构体,不能用测试集重新计算归一化边界,否则数据分布不一致。
5.3 最简单的GUI交互
MATLAB里做GUI不需要App Designer,用uicontrol就可以搭一个够用的录音识别界面。一个录音按钮、一个文本标签,录音结束后立刻分析并显示情感结果。
% 最简GUI核心框架 fig = figure('Position', [100 100 300 200]); btn = uicontrol('Style', 'pushbutton', 'String', '录音并识别', ... 'Position', [80 80 140 40], 'Callback', @recordAndPredict); function recordAndPredict(~, ~) rec = audiorecorder(targetFs, 16, 1); recordblocking(rec, 3); % 录音3秒 audio = getaudiodata(rec); % ... 特征提取 + 归一化 + sim预测 ... set(labelObj, 'String', ['情感: ', emotionName{pred}]); end这段代码演示了GUI最核心的逻辑:按钮回调函数里完成录音、特征提取和预测。recordblocking会阻塞执行直到录音结束,适合简单的录制场景;如果要做实时流式识别,需要改用定时器配合audiorecorder实现边录边分析。GUI不是这套工程的重点,但把它接上,整个系统就从脚本变成了一个别人也能上手用的工具。
6. 进阶调优:三模型交叉验证与情感类别扩展
训练脚本跑通只是起点,真正决定这套系统能不能用于实际场景的,是模型对比选型和数据扩展能力。
三套模型各自的短板很明确:PNN训练快但模型体量大,预测时要和全部训练样本算距离,样本上万后速度明显下降;BPNN参数量大,需要调的超参数多,但泛化边界最平滑;LVQ模型最紧凑,可解释性最好,代价是对类别分布敏感。要选型,不能只靠一次随机划分的准确率。常见做法是跑一遍五折交叉验证,对三个模型分别统计平均准确率和方差,方差大的模型即使平均分高也不适合上生产。由于这套工程整体是脚本式结构,这一步改造成本很低,可以用cvpartition直接实现。
扩展情感类别时,第一步不是训练而是数据校验。新加入的样本要与现有特征的提取方式完全一致,包括采样率、分帧参数、MFCC维度。很多人加了一类新数据后整体准确率下降,原因不是模型不行,而是新数据的特征分布和旧数据不在同一空间。每添加一类样本,都要重新归一化并重新扫描spread和原型数,而不是沿用旧参数。
我自己在这类项目上吃过一次亏,当时赶着出结果,直接用默认spread训练了PNN就交付给同事用,换了一组新录音后识别率直接对半砍。从那以后我每次换数据集都强制走一遍流程:先探查数据维度、再做按文件粒度的划分、最后网格扫描关键超参。这一套流程下来,至少不会再出现交付后数据一变就翻车的局面。希望帮到你。
本文还有配套的精品资源,点击获取