简介:基于深度学习的语音情感识别系统MATLAB实现,面向人工智能、语音信号处理方向的开发者与研究者,用于解决语音情绪分类中的特征提取与模型构建问题。资源包共9个文件,包含4个m脚本,覆盖BPNN、PNN、LVQ等经典网络实现,另有5个mat数据文件对应愤怒、高兴、中性、悲伤、恐惧等情感样本,压缩包约260KB,轻量易用。已有454人学习下载。通过该资源可掌握情感语音特征的MFCC提取思路,以及多种神经网络在情感识别任务中的训练与评估方法,适合作为课程设计或入门项目的参考实现。 语音情感识别这几年在呼叫中心质检、辅助驾驶疲劳检测、人机交互和心理健康筛查里都变成了刚需,但大多数行业团队卡在“声音里到底哪些维度承载了情绪”这个问题上。做这个课题时,我从最开始就把它当模式识别问题而不是“玄学”来对待:先确定用深度学习是因为传统GMM-UBM在高噪声、自然对话场景下无法同时刻画短时声学和长时韵律特征;再确定选MATLAB是因为它的音频工具箱把读取、分帧、加窗、MFCC提取全部封装成一行命令,复现和调参效率比开源语音框架高很多;最后才回到线上去理解“用卷积+循环网络把局部声学纹理和时序语调变化融合起来”这个常见落地思路。这篇文章围绕“LSTM为主、CNN辅助多模态对比”的实现路径展开,你能直接拿去跑通一个最小系统。
1. 先搞清楚这个标题里的建模对象是什么
语音情感识别(Speech Emotion Recognition, SER)在MATLAB里做出来,和“给音频打标签”完全是两回事——它要处理的是同一句话、不同人说、不同情绪下的声学表现差异。比如“真的吗”这三个字,平静时降调、惊讶时句尾上扬、愤怒时语速和能量都在变化,传统手工特征很难把这三种状态分开。
深度学习模型要学习的是“一段语音中情感相关的因果关系”:不光看某一个瞬间的声道特性,还要看上下文如何变化。这也是为什么几乎所有SER实现都会先抽取MFCC(梅尔频率倒谱系数)、基频F0和短时能量,然后喂给LSTM或CNN这类结构——它们能够同时建模短时频谱纹理和帧间演变规律。
这套系统的完整链路是:采集音频 → 提取声学特征序列 → 设计网络结构 → 训练并验证 → 接上麦克风或音频文件做实时预测。你不需要有语音信号处理背景,但需要能看懂分帧、加窗、特征维度这些概念,接下来的内容会一步步展开。
2. 从数据集到特征矩阵:MATLAB里的声学特征工程
2.1 准备一份能用的情感语音数据集
“情绪识别”这个任务很依赖数据分布的厚度。常见公开数据集像RAVDESS、EMO-DB、CASIA,都提供了标注好的愤怒、快乐、悲伤、惊讶、恐惧、厌恶和中性共7类情绪。不同数据集的采样率(16kHz 或 44.1kHz)、录音设备、说话人数量差异很大,特征归一化策略和训练验证集的切分方式会直接影响模型泛化能力。
我一般会先写一个数据探查脚本,统一所有音频为16kHz单声道,再按说话人而不是按文件切分验证集,避免同一说话人的不同句子同时出现在训练集和验证集里,导致情绪识别变成“说话人识别”。
% 1. 创建数据存储并统一采样率 dataFolder = fullfile('data', 'ravdess'); ads = audioDatastore(dataFolder, 'IncludeSubfolders', true, 'LabelSource', 'foldernames'); % 2. 归一到 16kHz 单声道 adsTmp = transform(ads, @(x)normalizeAudio(x)); ads = adsTmp; % 3. 按说话人划分训练/验证集 [adsTrain, adsVal] = splitEachLabel(ads, 0.8, 'Include', categories(ads.Labels)); % 4. 统计标签分布 tbl = countEachLabel(adsTrain); disp(tbl);这段代码把音频数据统一成一个数据存储对象。audioDatastore不会一次性把所有文件读入内存,它维护一个文件路径队列,对长音频和大量样本非常友好。transform配合normalizeAudio(栗子中为自定义子函数,内部调用resample(x, 16000, fs)和mono)把原始录音强制转换成单声道16kHz。splitEachLabel按标签比例随机划分,但若要让训练验证场景更接近真实使用,建议自己按说话人ID做索引划分,这也是精度忽高忽低最常见的原因。
2.2 MFCC 序列和 delta 系数到底在提取什么
情感在声音里的体现,整体上集中在频谱包络的变化速度与准周期性能量波动上。愤怒声音在高频区有更集中的能量分布,悲伤声音的低频能量比重更高且基频波动更平缓。MFCC 表征的是频谱包络的倒谱表示,它把感知层面的频率非线性映射到梅尔刻度上,所以能够区分“这个音是开元音还是闭元音”类的声学差异。
只靠静态MFCC不够,情感更依赖“变化”。所以要加一阶差分(delta)和二阶差分(delta-delta),再把它们拼接成高维特征向量。经典取值是:13维MFCC + 13维Δ + 13维ΔΔ = 39维。MATLAB的mfcc函数一次性返回前两组系数,也可以手动指定。
% 提取 MFCC 的详细参数配置 windowLength = round(0.03 * fs); % 30 ms 帧长,覆盖足够周期 overlapLength = round(0.015 * fs); % 15 ms 帧移,保证帧间平滑过渡 numCoeffs = 13; % 保留13维系数 feat = mfcc(x, fs, ... 'WindowLength', windowLength, ... 'OverlapLength', overlapLength, ... 'NumCoeffs', numCoeffs, ... 'DeltaWindowLength', 9); % feat 每一行是一帧的39维特征(13静态+13Δ+13ΔΔ)帧长30ms是经验折中:太短(如10ms)会让低频分辨率和韵律信息丢失,太长(如50ms)则让语音内部平稳性假设失效。DeltaWindowLength取9表示用前后各4帧计算回归斜率。情感计算中不建议把delta窗口调得太短,不然愤怒这类急剧变化的基频轮廓会被平滑掉。
2.3 输出数据标准化和序列长度对齐
深度网络对输入范围非常敏感,LSTM尤其如此。如果你不统一训练和验证时的特征均值方差,模型很容易收敛到某个偏离很大的局部极小。这里有两种常用处理方式:离线计算全局均值和标准差,或使用sequenceInputLayer的Normalization选项。我选择离线归一化,因为在自定义训练循环或特征工程阶段会反复看到中间值。
% 把特征集合成一个 cell 数组: features{k} 为 (39 x T) 矩阵 features = cell(numel(adsTrain.Files), 1); for i = 1:numel(adsTrain.Files) [x, fs] = read(adsTrain); feat = computeMFCC(x, fs); % 自己封装的提取函数 features{i} = feat'; end % 全局均值方差归一化 allFeats = cat(2, features{:}); mu = mean(allFeats, 2); sigma = std(allFeats, 0, 2); for i = 1:numel(features) features{i} = (features{i} - mu) ./ (sigma + eps); end特征矩阵形状必须是特征维度 × 时间帧数。MATLAB 的 LSTM 层接受这种“观测值按时间步展开”的形式,每一列对应一帧。cat(2, features{:})把所有帧拼起来统计全局均值,sigma + eps防止悲伤情绪里某些维度方差接近0导致除零错误。这个细节影响巨大——在训练收敛过程中,如果loss迟迟不降,第一步查的就是标准化是否生效。
3. LSTM 为主干的深度学习网络构建与核心参数
3.1 为什么要用 LSTM 而不是纯 CNN 或纯全连接
语音情感是一个时间序列建模问题。全连接网络把整段语音当作一个平铺向量,帧与帧之间的顺序关系完全丢失;CNN通过卷积核对局部时频区域做平移不变特征提取,能捕捉喉咙发声时的短时频谱模式,但感受野有限,对“句末降调”这类需要跨秒级别上下文感知的情绪表达无能为力。LSTM的循环连接使当前帧预测可以携带之前几十帧的状态信息,这种长程记忆对悲伤、平静这类缓变的情绪尤其重要。
一个常见的成熟结构是:LSTM 双层堆叠 → Dropout → 全连接 → Softmax。第一层LSTM输出整个序列用于捕捉局部上下文,第二层只输出最后时刻的隐藏状态用于分类。参数上,隐藏单元数(HiddenSize)影响记忆容量,序列越长、情绪类别越多,越需要更大的容量。注意“最后时刻输出”和“整个序列输出”的语义差异:OutputMode设置为"last"时只取末尾隐藏状态,适合序列级分类任务。
3.2 搭建一个可运行的LSTM情感分类网络
numClasses = 7; inputSize = 39; % MFCC + Δ + ΔΔ 维度 layers = [ sequenceInputLayer(inputSize, 'Normalization', 'zerocenter', 'Name', 'input') lstmLayer(256, 'OutputMode', 'sequence', 'Name', 'lstm1') dropoutLayer(0.3, 'Name', 'dropout1') lstmLayer(256, 'OutputMode', 'last', 'Name', 'lstm2') fullyConnectedLayer(numClasses, 'Name', 'fc') softmaxLayer('Name', 'softmax') classificationLayer('Name', 'output')]; analyzeNetwork(layers);sequenceInputLayer的zerocenter归一化会把训练数据均值减去,推到网络里后输入分布自动零均值。lstmLayer(256, 'OutputMode', 'sequence')保留每个时间步的隐藏状态输出,以便堆叠两个LSTM层;dropoutLayer(0.3)随机置零30%神经元,在情感这类相对小规模数据集上能明显缓解过拟合。fullyConnectedLayer输出7个类的logits,softmaxLayer转换为概率,classificationLayer内部计算交叉熵损失。构建后可以用analyzeNetwork检查每一层的激活输出尺寸是否匹配。
3.3 训练选项和防止过拟合
小数据集的语音情感识别难点在于“学到的不同情绪读音差异过少”。控制过拟合我会用三个策略:早停(validation patience)、低批量配合适度学习率、L2正则和Dropout叠加。
options = trainingOptions('adam', ... 'MaxEpochs', 60, ... 'MiniBatchSize', 32, ... 'InitialLearnRate', 1e-3, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropFactor', 0.5, ... 'LearnRateDropPeriod', 10, ... 'ValidationData', {valFeatures, valLabels}, ... 'ValidationFrequency', 30, ... 'Shuffle', 'every-epoch', ... 'Plots', 'training-progress', ... 'Verbose', true);MiniBatchSize取32是CNN/LSTM在音频任务上常见的安全值。过大(128以上)在LSTM反向传播时会放大内存消耗且更容易陷入尖锐极小值;过小(8以下)梯度噪声太大收敛慢。初始学习率1e-3配合adam比较通用,若损失震荡不降,可以先降到3e-4,观察前几个epoch的曲线形态再调整。ValidationFrequency按迭代次数设置,太频繁会在小数据集上放大验证集的随机波动。
表:LSTM情感识别常用超参推荐
| 参数 | 推荐区间 | 说明 |
|---|---|---|
| 隐藏单元数 | 128~512 | 特征维度和数据量越大,取越高;超过512容易过拟合 |
| 层数 | 1~3 | 2层最常用;3层需更长时间训练 |
| Dropout | 0.2~0.5 | 数据量小于5万样本时建议不低于0.3 |
| MFCC维度 | 13~26 | 加delta共39~78维;低维更鲁棒 |
| 初始学习率 | 1e-3~3e-4 | LSTM训练不稳定时优先调低这个值 |
提示:
ValidationData对应特征的cell数组必须和训练数据采用完全相同的标准化参数。很多初学者在验证阶段单独重新提取特征,导致分布漂移,验证精度可达不到训练精度的80%。
4. 训练、评估与实时识别系统搭建
4.1 训练时的特征缓冲和 mini-batch 设计
LSTM要求一个batch内每条样本的时间帧数相同。实际录音长度不一致,所以要做padding和masking。MATLAB完全不支持masking输入给LSTM层,但dataset在批量加载时可以把短序列补零到该batch最长序列的长度。
% 定义 sequence 数据变换函数,用于训练前长度对齐 function [seq, labels] = padSequence(data, labels) % data 是 1×N 的 cell,每个 cell 是 featureDim × time maxLen = max(cellfun(@(x) size(x, 2), data)); seq = zeros(size(data{1}, 1), maxLen, numel(data), 'single'); for i = 1:numel(data) len = size(data{i}, 2); seq(:, 1:len, i) = data{i}; end end补零操作不会把“无意义帧”当作静音帧特征影响结果吗?确实会,但LSTM的遗忘门可以学习到零输入区域的信息流是否要清零。如果序列长度差异过大(如2秒和10秒混合),建议直接剔除过长样本或拆分,补零长度超过原始长度的一半会严重拖慢训练速度。
4.2 混淆矩阵和分类器评估
训练结束后,我会用验证集生成混淆矩阵,而只看总体准确率会掩盖模型对“恐惧”和“惊讶”混淆严重的事实。这两个情绪在声学剖面有点类似,都包含高基频和高能量,若不细分,交付时用户会发现流畅对话场景下模型总是给出错得离谱的结果。
YPred = classify(net, valFeatures); figure; confusionchart(valLabels, YPred, 'RowSummary', 'row-normalized', ... 'ColumnSummary', 'column-normalized');row-normalized显示每个真实类中被正确分类的比例,column-normalized显示每个预测类中来源于哪个真实类最多。如果某个类别召回率低于70%,优先考虑数据量增强而不是加网络层。情感识别领域的经验是加数据比加参数更有效,因为情绪间的声学边界本身语义模糊,人类专家的一致性也就70%~80%。
4.3 实时识别麦克风音频流
系统要可用,就要能对麦克风输入流式打分。实时语音情感识别和离线批量推理的区别在于:你不可能等用户说完10秒再给结果,需要滑动窗口。
deviceReader = audioDeviceReader('SampleRate', fs, 'SamplesPerFrame', fs*0.5); buffer = zeros(fs*3, 1); % 维护3秒环形缓冲 while true audioIn = deviceReader(); buffer = [buffer(size(audioIn,1)+1:end); audioIn]; % 滚动更新 if sum(abs(buffer)) < 0.01 * fs continue; % 静音段跳过 end feat = computeMFCC(buffer, fs); feat = (feat' - mu) ./ sigma; [label, score] = classify(net, {feat'}); fprintf('当前情绪: %s (%.2f)\n', label, max(score)); end缓冲长度为3秒对应一个情感判断单元。太短(1秒)无法覆盖语句的完整韵律变化;太长(5秒以上)会引入多句话的混合情绪,导致输出概率平均化。SamplesPerFrame按帧取0.5秒是实时性的折中,每次循环读取和计算开销都低于200ms。若是产品级系统,还需要加一个“语音端点检测(VAD)”模块,过滤咳嗽、环境音和背景其他人的语音。
5. 提高泛化能力的三个细节:数据增强、语音分段和模型压缩
当前面系统跑通、准确率在干净语音上达到80%以上后,真正决定这个课题能不能落地的,是它面对真实环境噪声和远场拾音时的表现。下面这三个工程细节能直接改善验证集之外的表现,我建议在做排障时先检查它们。
5.1 用MATLAB的audioDataAugmenter做在线增强音频数据增强和图像平移裁剪不同,语音段不能随意改变语义。可行的方式包括:加背景噪声(信噪比0~20dB)、音高小范围偏移(半音偏移不超过±2)、时间拉伸(速度因子0.9~1.1)。注意不要把“语速改变”和“时间拉伸”混为一谈——后者会改变语音的自然度,过度使用反而破坏F0轨迹中的情感线索。
aug = audioDataAugmenter( ... 'AugmentationMode', 'sequential', ... 'NumAugmentations', 4, ... 'AddNoise', true, ... 'SNR', 10, ... 'PitchShift', true, ... 'PitchShiftRange', [-2 2], ... 'TimeStretch', true, ... 'TimeStretchRange', [0.9 1.1]);'AugmentationMode', 'sequential'依次应用所有增强操作,而不是从多个操作中随机选一个。它们按固定顺序执行:先加噪声、再变调、最后变速。加噪声的SNR取10dB,过低会把模型训练成“只在噪声背景中找情感”;变调范围±2个半音与真实说话人差异一致,过大则音频听起来像卡通人物,模型学到了与情感无关的特征。
5.2 语音分段:超过6秒的音频先切再拼用长音频做情感分类时,模型会对整段语音取统计池化,结果是把开头和结尾的情绪表达平均掉了。处理长语音文件时,我的做法是先按2~4秒切成子段,预测每个子段的情感概率,然后将各子段的softmax概率做平均输出最终结果。
分段不会割裂语义句子的韵律吗?对连续语句会。但实际业务场景里绝大多数对话按停顿拆分后,子段长度就在3~6秒之间;只有像演讲这样的长持续语音才需要有重叠分段(每次滑动1秒),保证“情绪转换点”落在窗口内部。
5.3 模型压缩:去掉第二个LSTM层换取实时性在实时识别里,双LSTM 256单元的推理时延在GPU上可接受,但部署到CPU或树莓派时会超过实时极限。我发现裁剪一层隐藏单元换来的精度损失通常只有1%~3%,但推理速度提升一倍以上。具体操作:删除第二个lstmLayer,把第一个层的OutputMode改成last,保留Dropout。如果想进一步压缩,可以把fullyConnectedLayer的输出维度改为32,再用一层输出层映射到7类,这会让“学习到的情绪语义表示”变成低维嵌入,效果不一定差。
最后再强调一次:语音情感识别没有“最深模型最优”的说法。当你怀疑系统性能时,先把验证集中的错误样本单独抽出来听一遍,区分是标注错误、音频本身情绪模糊,还是模型真实缺陷;再决定走增强、换特征还是调网络结构。把错误类型分类之后再去优化,比盲目增加网络层数有效得多。
本文还有配套的精品资源,点击获取