
简介基于深度学习的语音情感识别系统采用MATLAB实现定位清晰面向语音信号处理与机器学习初学者帮助快速搭建并验证语音情感分类算法。资源共九个文件压缩包仅260KB其中五个mat文件分别存储愤怒、恐惧、高兴、中性、悲伤五类情感样本数据四个m文件为算法源码涵盖PNN概率神经网络、BP神经网络及LVQ学习向量量化等典型模型也便于对比传统浅层模型与深度学习思路的差异。已有四百五十五人学习下载。通过这套资料读者可以结合源码理解语音预处理、MFCC特征提取、样本组织、模型训练与预测评估的完整链路也可以替换自己的情感语音数据复用于课程设计或毕业设计。对于想尝试在MATLAB中实现语音情感识别、但缺少数据与参考代码的开发者这组小型但结构完整的文件提供了直观的入门样例是学习算法落地和快速验证思路的实用工具。1. 语音情感识别落到深度学习为什么我最后用MATLAB把它跑通做客服质检或者心理声学分析的人手里最不缺的就是一段段录音最缺的是能从录音里自动读出情绪状态的手段。基于深度学习的语音情感识别系统要做的事就是把“生气、开心、悲伤、中性”这些标签从语音波形里学出来而不是靠人工听或者老式声学规则硬凑。这个方向在MATLAB里做有一个先天优势前端信号处理重采样、分帧、加窗、Mel滤波器组和后端深度学习训练在同一套环境里闭环不用来回导数据。如果你手里已经有标注好的情绪语音数据想快速出一版可评估的基线同时看清参数怎么调、坑在哪里这篇文章就是照着做的路径。新手能跟步骤走熟手直接看第5章的排查清单。2. 特征工程的前置工作MFCC序列与说话人差异处理2.1 为什么MFCC是语音情感识别的默认特征MFCC的物理含义是模拟人耳对频率的非线性感知语音按帧切段每帧做傅里叶变换能量映射到Mel刻度上取对数再做离散余弦变换得到一串系数。情感识别里韵律、音调、语速的升降变化更多体现在系数随时间的轨迹上所以这里把它当作“序列特征”而不是“整体向量”使用。常见误区是只用静态13阶或20阶MFCC直接喂分类器。我的做法是把一阶差分delta和二阶差分delta-delta拼进去组成60维特征向量20阶静态MFCC、20阶delta、20阶delta-delta。原因很简单“越说越激动、音调突然上扬”这种情绪转折主要信息就在差分系数里静态系数只能体现瞬时谱形状体现不了变化趋势。2.2 用MATLAB批量提取MFCC先建一个可复用的特征库下面这段脚本把数据目录里的wav全部读取统一重采样到16kHz再逐条提取60维序列特征。建议单独保存成特征文件后续训练和测试都从这份文件加载不要每次都重新提一遍特征。% 假设数据目录下按说话人分子文件夹存放音频 ads audioDatastore(pwd, FileExtensions, .wav, ... IncludeSubfolders, true); features {}; labels {}; while hasdata(ads) [audio, info] read(ads); fs info.SampleRate; % 统一到16kHz很多语料是44.1k或22k训练测试必须一致 if fs ~ 16000 audio resample(audio, 16000, fs); fs 16000; end % 帧长25ms帧移10ms40个梅尔滤波器输出20阶静态系数 mfccs mfcc(audio, fs, ... WindowLength, round(fs * 0.025), ... OverlapLength, round(fs * 0.015), ... NumCoeffs, 20, NumFilters, 40); [nFrames, ~] size(mfccs); if nFrames 20 continue; % 太短的语音后续会让LSTM报错直接跳过 end % 一阶差分和二阶差分注意对齐长度 deltas mfccs(3:end, :) - mfccs(1:end-2, :); deltas2 deltas(3:end, :) - deltas(1:end-2, :); feat [mfccs(5:end, :), deltas(3:end, :), deltas2]; features{end1} feat; % 标签取文件名下划线前的部分例如“生气_001.wav” [~, name] fileparts(info.FileName); labels{end1} extractBefore(name, _); end % 转成categorical并保存 labels categorical(labels); save(emotionFeat.mat, features, labels, -v7.3);参数说明WindowLength和OverlapLength的单位是采样点所以要先乘fs。25ms窗长、10ms帧移是语音特征提取的常规配置窗太短频率分辨率差太长则时间模糊。做情感识别不比语音识别苛刻20ms到30ms都在安全范围。NumFilters40是中等规模增大到80能提升高频细节但训练更慢基线阶段40即可。差分对齐那一行容易写错最容易出的问题是维度不等导致后面训练直接报错我的建议是每次提完特征打印一下size(feat)确认。循环里直接跳过过短语音有一个副作用labels和features始终同步因为短样本两个数组都没加这个处理是安全的。2.3 说话人归一化让模型不学“谁在说话”情感语料通常是多人录制说话人音量和麦克风距离各不相同导致MFCC整体幅值有明显差异。如果不做处理模型很可能会学“音量大小”而不是“情感类别”。我一般按说话人做归一化同一个人的所有样本一起算均值和方差再对该人的每一帧特征做标准化。% spkID 是从文件路径里切出来的说话人编号例如 spk01 [groups, spkList] findgroups(spkID); for g 1:numel(spkList) idx (spkID spkList(g)); tmp cat(1, features{idx}); mu mean(tmp, 1); sd std(tmp, 0, 1) 1e-6; % 加极小值防止除零 for k find(idx) features{k} (features{k} - mu) ./ sd; end end注意不要把全部数据混在一起算全局均值和方差那样等于把测试集的信息提前泄漏给训练过程。按说话人归一化之后每个样本的数值范围统一网络收敛会明显更快后续调学习率也更省心。3. 网络结构选型与搭建时序依赖是情感识别的关键3.1 为什么LSTM比全连接结构更适合语音情感MFCC序列长度取决于录音长度一秒语音大约100帧三秒就有300帧。全连接网络接受固定维度输入强行把长序列压成固定长度会丢失情感转折的时间信息。LSTM的循环单元按时间步展开能把“越说越快、音调持续上扬”这种跨多帧的模式记住。因此在搭建基线时第一选择是双向LSTM因为情感表达经常是前文铺垫、后面爆发双向结构能看到整句话的上下文。3.2 在MATLAB中搭建最小可跑网络双向LSTM加分类层numFeatures 60; % 与特征维度保持一致 numClasses numel(categories(trainLabels)); layers [ sequenceInputLayer(numFeatures) bilstmLayer(128, OutputMode, last, ... InputWeightsInitializer, he) dropoutLayer(0.3) fullyConnectedLayer(numClasses) softmaxLayer classificationLayer ];OutputMode必须设成last表示只把最后一个时间步的输出传给全连接层。如果设成sequence网络会输出每个时间步的预测结果那是做帧级标注才用的设置做语音级分类时会造成维度不匹配。InputWeightsInitializer用he初始化对后续ReLU类激活更稳定默认初始化也不是不能跑只是收敛速度差一些。这段网络能直接跑通的最小规模是多少我的建议是先用bilstmLayer(64)试一轮确认流程通了再调大隐藏单元数。不要一上来就是256单元小数据集上半小时内就会看到严重过拟合。3.3 增加一维卷积层让局部模式先被提取如果基线准确率卡在70%左右有一种常见增强思路在LSTM之前加一层一维卷积让网络先看相邻几帧的局部时频模式再接LSTM做长程建模。这个结构对语速突变、音节重音比较敏感在小语料上比直接加深LSTM更稳。layers [ sequenceInputLayer(numFeatures) convolution1dLayer(5, 32, Padding, same) reluLayer maxPooling1dLayer(3, Stride, 2) bilstmLayer(64, OutputMode, last) dropoutLayer(0.3) fullyConnectedLayer(numClasses) softmaxLayer classificationLayer ];convolution1dLayer(5, 32)的含义是卷积核长度5个时间步输出通道32个。maxPooling1dLayer(3, Stride, 2)会把时间步下采样一半300帧变成150帧LSTM的计算量直接减半。这里有个取舍池化会丢失一些时序细节所以池化窗口不宜过大3步池化、步长2已经是上限。如果你对每帧的细节很敏感宁可不用池化把LSTM隐藏单元数降到32。3.4 网络参数的边界和初值隐藏单元数小语料几百条用64到128超过256很容易过拟合。大语料几千条可以到256。Dropout放在LSTM输出之后0.2到0.5之间。如果验证损失开始回弹先往0.5调不要急着加数据。学习率Adam优化器配1e-3是安全起点损失震荡就往1e-4降。梯度裁剪LSTM训练容易梯度爆炸GradientThreshold我通常设1这个值比默认的10要激进一点但情感识别这种小模型上效果不错。4. 把训练跑起来数据划分、训练选项与评估指标4.1 说话人无关的数据划分划分数据的第一个原则同一说话人的所有样本只能出现在训练、验证、测试三个集合中的一个。不然模型只要学会“这个人嗓音特征”就能在测试集上拿高分换个人录音立刻现原形。allSpk unique(spkID); rng(2024); % 固定随机种子结果可复现 perm randperm(numel(allSpk)); nTrain floor(numel(allSpk) * 0.7); nVal floor(numel(allSpk) * 0.1); trainSpk allSpk(perm(1:nTrain)); valSpk allSpk(perm(nTrain1:nTrainnVal)); testSpk allSpk(perm(nTrainnVal1:end)); trainIdx ismember(spkID, trainSpk); valIdx ismember(spkID, valSpk); testIdx ismember(spkID, testSpk);比例上70%训练、10%验证、20%测试比较常规。如果样本总量少例如只有30个说话人可以把验证和测试合并用“留一个说话人”交叉验证每次留一个人做测试其余人训练轮流做完全部人后取平均结果。这个指标比单次划分更有参考价值。4.2 训练选项配置与早停策略MATLAB的trainingOptions没有内置直接可配的早停参数实际项目中我一般用OutputFcn写一个简单的早停回调让训练在验证损失连续变差之前停下来省下大量等待时间。options trainingOptions(adam, ... MaxEpochs, 80, ... MiniBatchSize, 32, ... InitialLearnRate, 1e-3, ... GradientThreshold, 1, ... Shuffle, every-epoch, ... ValidationData, {valFeatures, valLabels}, ... ValidationFrequency, 20, ... OutputFcn, (info)myEarlyStop(info, 1.2), ... Plots, training-progress, ... Verbose, false); net trainNetwork(trainFeatures, trainLabels, layers, options);早停回调的定义function stop myEarlyStop(info, threshold) stop false; if strcmp(info.State, iteration) ~isempty(info.ValidationLoss) if info.ValidationLoss threshold stop true; end end end注意ValidationFrequency的单位是迭代次数不是epoch。上面设20表示每20次迭代在验证集上算一次损失。早停的阈值要结合第一次训练打印出的验证损失曲线来定不要一上来乱设。我的经验是先跑一轮完整的观察验证损失在哪个区间波动再把阈值设到比最优值略高的位置。训练选项里另外一个值得改的是Shuffle默认是once对情感这种小数据量任务every-epoch会让每个epoch内样本顺序都不同能多消除一点随机性给训练带来的波动。4.3 评估指标混淆矩阵与宏平均F1语音情感识别数据集通常类别分布不均衡“中性”往往占了一半以上。这时候单纯看准确率会骗人模型全部输出“中性”也能拿到50%以上。业界更常用宏平均F1也就是对每个类别分别算F1再取平均。MATLAB里用混淆矩阵手算并不麻烦。YTest classify(net, testFeatures); figure; confusionchart(testLabels, YTest, RowSummary, row-normalized); C confusionmat(testLabels, YTest); nClass size(C, 1); precision zeros(nClass, 1); recall zeros(nClass, 1); for i 1:nClass precision(i) C(i, i) / sum(C(:, i)); recall(i) C(i, i) / sum(C(i, :)); end F1 2 * (precision .* recall) ./ (precision recall eps); macroF1 mean(F1);confusionchart的RowSummary会显示每一行的召回率占比适合快速看“悲伤被分错到哪一类”。宏F1的取值范围被eps保护不会有除零尴尬。我把宏F1当作主指标准确率只作参考。如果两个模型准确率都是72%宏F1更高的那个对少数类情绪的实际可用性通常更好。提示测试集合上不要来回挑选模型参数然后反复评估这样测试集在实践意义上变成了第二个验证集。我习惯把验证集调参最后只在测试集上测一次测完的记录当作最终上报数字。5. 训练中常见的坑与排查5.1 数据泄漏同一个说话人进了训练又进了测试现象测试准确率冲到95%以上模型部署到新录音上准确率只有70%甚至接近随机水平。原因划分数据时把全部文件打乱再切分同一个说话人完全相同的嗓音同时出现在训练和测试中网络学的是“这个人的声音”而不是“这种情绪”。解决严格按说话人ID分组划分代码见4.1。判断有没有泄漏的办法也很简单把划分脚本打印出的测试说话人列表和训练说话人列表做交集如果非空就是泄漏了。5.2 类别不平衡少数类情绪完全学不出来现象测试集里“悲伤”有100条模型预测“悲伤”的输出是0条全部分到了“中性”或“平静”。原因多数类比例太高交叉熵损失被多数类主导少数类贡献的梯度微乎其微。解决先看类别分布再做数据重采样。最常见做法是过采样少数类每次epoch对少数类样本重复采样弥补样本数量差距。cats categories(labels); maxCount max(countcats(labels)); for c 1:numel(cats) idx find(labels cats(c)); if numel(idx) maxCount % 从已有的少数类样本中有放回抽取补足到最大类数量 addIdx idx(randi(numel(idx), maxCount - numel(idx), 1)); features [features; features(addIdx)]; labels [labels; labels(addIdx)]; end end这段操作要在数据划分之后做只放大训练集验证集和测试集保持原始分布否则验证指标又会失去意义。如果过采样后模型反而在少数类上过拟合明显可以把补足倍数从“补到最大类”降为“补到最大类的一半”用降低学习率配合。5.3 验证损失先降后升训练损失逼近0现象训练到第20个epoch左右验证准确率开始快速下滑训练准确率却一路走到98%。原因模型容量已经超过数据信息量开始背诵训练样本的噪声。解决先调大dropout到0.5减小LSTM隐藏单元数到64再不行就减小序列长度。小数据集上不要急着换复杂网络结构CNN加LSTM这种组合在数据量不够时反而恶化。如果手头只有几百条样本任何结构都救不回来优先找数据更充足的语料。5.4 训练慢到让人失去耐心现象纯CPU跑LSTM一个epoch要20分钟80个epoch根本等不完。原因LSTM按时间步展开计算序列越长计算步骤越多。录音三秒60维特征约300帧每个batch都是300步的循环展开CPU自然吃不消。解决固定采样率降到16kHz录音超过3秒就截取中间三秒降低隐藏单元数到64打开Plots,training-progress观察曲线验证损失连续三个验证点不下降直接手动中断不要傻等MaxEpochs跑完。5.5 特征提取脚本在训练和部署阶段不一致现象训练时准确率正常部署到新录音上预测结果一片混乱。原因训练阶段和预测阶段各写了一份特征提取脚本一份用了默认帧长另一份自己指定了参数两者MFCC维度不同归一化统计量也不一致。解决把特征提取和说话人归一化封装成一个函数训练和预测共用同一入口。function feat extractFeat(audio, fs, mu, sd) if fs ~ 16000 audio resample(audio, 16000, fs); fs 16000; end mfccs mfcc(audio, fs, WindowLength, 400, ... OverlapLength, 160, NumCoeffs, 20, NumFilters, 40); deltas mfccs(3:end,:) - mfccs(1:end-2,:); deltas2 deltas(3:end,:) - deltas(1:end-2,:); feat [mfccs(5:end,:), deltas(3:end,:), deltas2]; feat (feat - mu) ./ sd; end帧长400个采样点对应16kHz下的25ms帧移160对应10ms。这段函数里顺带完成了重采样和归一化部署时只需要读入音频文件调用函数然后喂给分类网络。6. 让模型在真实场景更稳数据增强、迁移学习与单条部署6.1 数据增强三招真实录音哪有那么干净环境噪声、麦克风距离、说话语速都跟训练集不一样。用audioDataAugmenter可以在训练前自动生成增强样本比较有效的三个操作是加高斯噪声、音调平移和速度变化。aug audioDataAugmenter( ... Probability, 0.8, ... AddGaussianNoise, true, SNR, 20, ... PitchShift, true, ... SpeedChangeInputRate, 0.95);SNR值设20dB左右太小的信噪比会把语音本身淹没。训练集每个epoch都做实时增强相当于让网络看到更多变体比离线生成一份增强数据再存盘的做法节省磁盘空间也更不容易引入重复样本。6.2 从训练脚本到单条语音预测训练完成后把网络和归一化参数一起保存写一个接收音频文件路径的预测函数让它自动完成读取、重采样、提特征和分类。function emotion predictEmotion(wavFile, net, mu, sd) [audio, fs] audioread(wavFile); audio [audio; zeros(round(fs*0.3), size(audio,2))]; % 尾部补300ms防止短样本 feat extractFeat(audio, fs, mu, sd); emotion string(classify(net, {feat})); end部署后要做的第一件事是拿训练集里没见过的几个说话人的录音做验收统计每个类别的准确率分别是多少。如果某个类别准确率明显偏低回到类别重采样那一步不要直接上线。我自己第一次做这个方向时也栽在说话人相关划分上看着准确率98%沾沾自喜一换成说话人无关重测只剩71%。后来所有实验都把说话人分组验证设成默认流程特征提取也封装成唯一入口踩坑率一下子降下来。希望帮到你。本文还有配套的精品资源点击获取