简介:针对语音情感识别研究与毕业设计需求,这套项目资源融合了长短期记忆网络、卷积神经网络、支持向量机与多层感知机四种模型,基于Keras和Python实现完整训练与预测流程。资源包共57个文件,以Python脚本、模型权重、配置文件、特征数据及备份文件等类型为主,整体约82.2MB。训练产物包含模型权重、训练日志与备份数据,便于直接加载或继续调参。项目按模型、特征提取、配置、检查点等模块划分,分别对应模型定义、Librosa与OpenSMILE特征提取、实验配置与训练产物,目录结构清晰,便于二次开发。目前已有55人学习使用,适合作为毕业设计、课程实践或工程落地的参考基线。通过源码与说明文档,读者可掌握多模型融合的语音情绪分析流程,并根据自有数据集扩展特征提取方法与模型结构。
1. 多模型融合的语音情感识别:一套能直接跑的 Python/Keras 基线系统
语音情感识别(Speech Emotion Recognition)这几年在毕业设计里出现频率很高,但很多拿到手的代码只给你一个训练脚本,数据从哪来、特征怎么对齐、模型怎么复用全是黑匣子。这套资源不太一样:它把多模型融合的语音情感识别系统做到了能直接跑的程度 — Librosa 和 OpenSMILE 两套特征提取管线、LSTM/CNN/SVM/MLP 四个模型、训练与预测脚本、还有训好的 checkpoint 全在一个压缩包里。系统设计得很清晰:features 目录存特征,configs 目录存超参,checkpoints 目录存权重,你拿到手先跑一遍 predict 就能看到效果。适合做毕业设计、课程实践,也适合刚接触 SER 的工程师拿来当地基,在这之上换数据集、加注意力机制都比从零写省事得多。
2. 特征工程:Librosa 与 OpenSMILE 两条路线,理解这步后面才不迷糊
2.1 为什么情感识别不直接用原始波形
很多入门者会问:LSTM 不是能处理时序数据吗,为什么不能直接把 16kHz 的采样点丢进去?技术上可以,但工程上很蠢。一段 3 秒的音频采样点是 48000 个,LSTM 要展开 48000 步,显存和训练时间都扛不住,而且原始波形里说话人个性、录音信道噪声的成分远大于情感信息,模型学到的往往是噪声而不是情绪。语音情感识别的通行做法是先做声学特征提取,把每帧音频压缩成 MFCC、基频、能量这类紧凑表示。
这套资源里特征层做得比较完整。extract_feats 目录下有两个提取脚本:librosa.py 走 Librosa 的特征路线,opensmile.py 走 OpenSMILE 工具的特征路线。features 目录里已经帮你把数据集组合的特征跑好了,3-category、6-category、7-category 是情感类别数,不是特征维度。拿到压缩包后,第一步不是马上跑训练,而是先搞清楚你要用哪条特征线,这决定了后续所有模型配置。
2.2 用 Librosa 提取 MFCC 族特征:一段能直接改的参考实现
Librosa 是 Python 生态里最常用的音频分析库,extract_feats/librosa.py 做的事情,核心就是下面这段逻辑。它读取 wav 文件,计算梅尔频率倒谱系数,再加上一阶、二阶差分,让每一帧的特征从 13 维扩到 39 维。
import librosa import numpy as np SR = 16000 def extract_mfcc_feat(wav_path, n_mfcc=13): # 统一重采样到16k,避免不同数据集的采样率影响特征分布 y, _ = librosa.load(wav_path, sr=SR) mfcc = librosa.feature.mfcc( y=y, sr=SR, n_mfcc=n_mfcc, n_fft=400, # 25ms窗长对应的FFT点数,16k采样率下400点 hop_length=160, # 10ms帧移,相邻帧有重叠 win_length=400 ) # 一阶差分捕捉相邻帧的变化趋势,二阶差分捕捉加速度 delta1 = librosa.feature.delta(mfcc, order=1) delta2 = librosa.feature.delta(mfcc, order=2) feat = np.vstack([mfcc, delta1, delta2]).T # 转成 (帧数, 39) 的序列 return feat这里几个参数是 SER 里的通用配置。n_mfcc 取 13 是 HTK 时代的惯用值;n_fft=400 对应 25ms 窗长,hop_length=160 对应 10ms 帧移,这两个值保证频谱的时间分辨率足够捕捉音节级的情感起伏。返回的形状是 (帧数, 39),后面 CNN1D 把它当二维输入,LSTM 把它当时间步序列。
需要说明的是,这套代码里 librosa_casia 相关特征文件就是这条管线的产物。CASIA 是中文情感语音库,中文比英文更依赖声调变化,MFCC 加差分刚好能吃到这部分信息。如果你想换成自己的数据集,只需要保证录音是 wav、单声道、16k 采样率,然后对每个文件调用这个函数,把返回的序列存成 numpy 数组就行。
2.3 OpenSMILE IS09 特征集:标准化配置比你自己提的特征更省心
OpenSMILE 是开源音频特征提取工具,extract_feats/opensmile.py 调用的是 IS09_emotion 配置,这是 INTERSPEECH 2009 情感挑战赛用的标准特征集,每组音频输出一个固定维度的统计特征向量。
SMILExtract -C config/IS09_emotion.conf -I audio.wav -O opensmile_features.csv这条命令的意思很直白:-C 指定配置文件,-I 指定输入音频,-O 指定输出 CSV。IS09_emotion 配置会先按 10ms 帧移计算 16 个低级描述符,比如 MFCC、基频 F0、发声概率、过零率这些,然后对这 16 个序列各做 24 种统计函数,包括均值、标准差、峰度、偏度、分位数、线性回归系数等,最终拼成一个 384 维的向量。
用 OpenSMILE 的好处是特征定义完全固定,别人用同一份配置提出来的特征可以直接和你对比,这在毕设答辩里是加分项。opensmile_casia_is09、opensmile_savee_ravdess 这两个目录名里的 is09 就表明它们是用这套标准配置生成的。缺点是 384 维里有很多冗余维度,对 SVM 这类模型影响不大,但对深度模型一般建议先标准化再进网络,后面训练环节会看到具体处理方式。
2.4 features 目录怎么读:文件后缀决定了加载方式
features 目录下能看到几种不同格式的特征文件,它们的加载方式完全不一样,我先用一张表把对应关系理清楚。
| 文件/目录 | 格式 | 内容与加载方式 |
|---|---|---|
| train_opensmile_savee_ravdess.csv | CSV 文本 | OpenSMILE IS09 特征,行是样本,列是 384 维特征加一个标签列,pandas 直接读 |
| train_opensmile_casia.csv | CSV 文本 | OpenSMILE 特征,规则同上 |
| train_librosa_casia.p | pickle 序列化 | Librosa 特征序列,每个样本是 (帧数, 39) 的数组,pickle.load 读 |
| train_opensmile_emodb.csv | CSV 文本 | OpenSMILE 特征,7 分类标签 |
| 6-category/opensmile_casia_is09 | 目录 | 按目录组织的 OpenSMILE 特征与标签,训练脚本按目录扫描 |
提示:.p 文件是 pickle 格式,不要用 pandas.read_csv 去读。我一般用 pickle.load 打开后先打印类型,确认是 dict 还是 list,再决定怎么组织成训练集。
数据集的组合也值得注意:SAVEE 和 RAVDESS 都是英文情感语音库,两个合在一起做 3 分类;CASIA 中文库单独做 6 分类;EMODB 德语库单独做 7 分类。这种"不同数据集不同类别数"的设计,本质上是给多模型融合制造了多个独立实验场景。训练时先选一个场景跑通,再横向对比特征和模型的组合效果,整个系统的可玩性就在这。
3. 模型侧:CNN1D、LSTM、SVM、MLP 分别解决什么问题
3.1 models 目录的组织方式:先理解 base.py 这个壳子
打开 models 目录你会发现它不是一堆散落的训练脚本,而是分了三层:base.py 定义公共骨架,ml.py 放 SVM 和 MLP,dnn 子目录里放深度模型实现,dnn.py 是基础网络结构,cnn.py 和 lstm.py 各自实现一维卷积和长短期记忆网络。这种组织方式和实际工程里的模型工厂模式比较接近,加一个模型只需要继承 base 类,实现 train、predict、save、load 四个方法。
这种设计的直接好处是训练脚本 train.py 不需要为每个模型写一遍训练逻辑。它只认 base 类暴露的接口,拿到配置文件里的 model_name 字段后,去模型注册表里找对应的类,实例化后统一调用接口。很多毕设代码是复制粘贴四份训练脚本,改一个模型其他三处跟着改,这一套没有这个问题。对你来说,后续想加注意力机制或者 Transformer 编码器,照 cnn.py 的格式写一个新文件,在注册表里登记就能接入现有流程,不用动 train.py 主逻辑。
3.2 CNN1D:把特征当序列做局部卷积
CNN1D 处理语音情感特征的方式,是把 (帧数, 特征维数) 看成一个单通道的时序图,卷积核沿时间维度滑动,提取局部模式。比如愤怒情绪往往伴随短时能量骤升,这个模式在 MFCC 序列上就是一个短促的局部突变,一维卷积能稳定捕获这类局部特征。
from keras.models import Sequential from keras.layers import Conv1D, MaxPooling1D, GlobalMaxPooling1D, Dense, Dropout def build_cnn1d(input_shape, num_classes): model = Sequential([ Conv1D(filters=64, kernel_size=3, activation='relu', input_shape=input_shape), MaxPooling1D(pool_size=2), Conv1D(filters=128, kernel_size=5, activation='relu'), GlobalMaxPooling1D(), Dense(64, activation='relu'), Dropout(0.5), Dense(num_classes, activation='softmax') ]) model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy']) return model注意这里用 GlobalMaxPooling1D 而不是 Flatten,这是针对可变帧长的处理技巧。SAVEE 和 RAVDESS 的音频长度不统一,Librosa 提出来的帧数就不一样;全局池化把每个卷积核在时间维上的最大值取出来,输出的特征维度就不依赖输入帧长了。如果你复现时发现维度 mismatch,多半是帧长没对齐。dropout 取 0.5 是偏保守的配置,小数据集上能明显压住过拟合。
3.3 LSTM:吃时序上下文,但别喂整段音频
LSTM 在情感识别里的价值是建模长时间上下文:一个句子前半段的平静和后半段的爆发,合在一起才是完整的情感表达。checkpoints 里有 LSTM_OPENSMILE_IS10.h5 和 LSTM_LIBROSA_IS10.h5,说明 LSTM 同时覆盖了两条特征线。
from keras.models import Sequential from keras.layers import LSTM, Dense, Dropout, Masking def build_lstm(input_shape, num_classes): model = Sequential([ Masking(mask_value=0., input_shape=input_shape), LSTM(128, return_sequences=False), Dropout(0.5), Dense(64, activation='relu'), Dense(num_classes, activation='softmax') ]) model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy']) return model这里加了一层 Masking,作用是让 padding 出来的全零帧不参与 LSTM 计算。不同音频帧数不一样,训练时通常按 batch 内最大帧数补齐,补齐部分全是 0,如果不屏蔽,LSTM 会把这一堆 0 也当成有效输入,学出一堆奇怪的状态。常见做法是截断到固定长度比如 200 帧,不够的补零。LSTM 隐状态维度取 128,在 6 分类任务上是一个平衡表达能力和训练速度的选择。
3.4 SVM 与 MLP:小样本场景下深度模型不一定是冠军
这套资源同时保留传统模型,这个设计在情感识别里非常务实。深度学习在小样本、短音频任务上不一定打得过调好核函数的 SVM。SAVEE 和 RAVDESS 合并后的样本量通常也就几百上千条,LSTM 在这种量级下很容易过拟合,而 SVM 配合 RBF 核在 384 维 IS09 特征上往往能拿到非常稳的准确率。
models/ml.py 里 SVM 和 MLP 共用一套数据加载逻辑,区别只是分类器:SVM 用 sklearn 的 SVC 配 RBF 核,概率输出需要设置 probability=True;MLP 用 MLPClassifier,隐藏层取 (128, 64)。checkpoints 里 .m 后缀的文件就是传统模型持久化出来的参数文件,和 .h5 的加载方式完全不同;SCALER_*.m 是跟着训练集拟合出的标准化器,预测环节必须带上。
所谓多模型融合,在这套系统里的落地方式分两层。第一层是特征融合:同一个模型分别吃 Librosa 和 OpenSMILE 特征,得到结构相同但参数不同的模型。第二层是决策融合:预测时把 SVM、MLP、LSTM、CNN1D 各自输出的类别概率按投票或者加权平均合并。我在实际使用时会把深度模型和传统模型的分数直接取平均,一般比单模型高 2 到 4 个百分点,这个涨幅在情感识别这种强主观任务里已经属于明显提升。
4. 训练与预测:从 yaml 配置文件到 checkpoint 的完整闭环
4.1 配置文件:四个 yaml 分别控制什么
configs 目录下有 svm.yaml、mlp.yaml、lstm.yaml、cnn1d.yaml 四个文件,训练脚本只读 yaml,不改代码就能切换模型和特征。这套设计的可复现性好,换数据集、换模型时不用到处找魔法数字。下面以 lstm.yaml 为例:
feat_type: opensmile_is09 data_path: features/6-category/opensmile_casia_is09 model_name: lstm num_classes: 6 epochs: 80 batch_size: 32 lr: 0.001 hidden_units: 128 save_path: checkpoints/LSTM_OPENSMILE_IS10.h5feat_type 决定走 OpenSMILE 还是 Librosa 特征线,data_path 指向对应的特征目录,save_path 是模型保存位置。改配置时有三个坑要注意。第一,num_classes 必须和数据集的类别数一致,比如用 7-category 的 EMODB 特征就填 7。第二,epochs 和 lr 要配合调,拿 CNN 的训练参数直接训 LSTM,大概率训不满就发散,LSTM 我一般把 lr 压到 0.001 以下。第三,save_path 的后缀要符合模型类型,Keras 模型存 .h5,sklearn 模型存 .m,写反了加载时会直接报错。
4.2 训练脚本的完整流程:train.py 按什么顺序跑
train.py 的执行流程可以归纳为五步:读 yaml 配置,加载特征,标准化,划分训练验证集,训练并保存模型。其中标准化这个环节最容易被忽略。IS09 特征的 384 个维度取值范围差异很大,有些维度是基频相关值,量级是几百,有些是概率值,量级在 0 到 1 之间。不标准化直接喂 SVM,RBF 核的距离计算会被大数值维度主导,小数值维度直接失去作用。
pip install -r requirements.txt python train.py --config configs/cnn1d.yamlrequirements.txt 里覆盖了 keras、tensorflow、librosa、scikit-learn、pandas、pyyaml 这些基础依赖。跑之前建议确认一下 Python 版本,Keras 的老接口在 Python 3.10 以上的环境里偶尔会有兼容性报错,我一般用 Python 3.8 或 3.9 的虚拟环境。训练完成后,checkpoints 目录里会出现三类文件:.h5 是 Keras 完整模型,包含结构加权重;.json 是单独序列化的模型结构;.m 是 sklearn 模型的持久化文件。SCALER 文件不要删,预测阶段加载模型之后必须用同一个 scaler 转换特征,否则输入分布不一致,准确率会明显掉。
数据集划分这里,我建议从原始特征里手动留出一部分做验证集。常见的划分比例是 8:2,但如果某个情感类别的样本量很少,直接随机切容易把稀有类别全切到验证集里。更稳的做法是按类别分层抽样,保证每个情感在训练集和验证集中占比一致。这套代码支持你先把特征读出来自己切分,再交给训练流程,毕设里这个细节写进论文会显得你考虑过数据分布问题。
4.3 预测流程:predict.py 的使用与模型匹配问题
predict.py 的使用方式跟训练脚本类似,传一个 wav 路径和配置文件即可。它的内部逻辑是先按配置里的 feat_type 提取特征,再加载对应的 scaler 和模型,最后输出每个情感类别的概率。
python predict.py --wav voice/test_happy.wav --config configs/lstm.yaml这里最常翻车的点是模型和特征类型不匹配。configs/lstm.yaml 里如果 feat_type 写的是 opensmile_is09,但实际拿 LSTM_LIBROSA_IS10.h5 来预测,输入维度一定对不上。我自己踩过一次,症状是模型加载成功,但 predict 时维度报错,查了半天才发现 checkpoint 文件名里的 LIBROSA 和 OPENSMILE 已经标明了特征线,配置文件里必须严格对应。
新加音频文件时,最好也把音频转成和训练集一致的格式,wav、单声道、16k 采样率,这是特征提取管线能正常工作的前提。如果拿一个 44.1k 采样率的 mp3 直接丢进去,先不说特征分布偏移,OpenSMILE 在解析非标准格式时就可能报错。我一般先统一转格式再预测,比在代码里做兼容省事得多。
5. 避坑与常见问题:复现这套代码最容易翻车的五个点
这一章记的是我实际复现时踩过的坑,每一条都是真实报错和排查记录。情感识别项目的坑往往不在模型结构,而在数据与特征管线,因为模型结构大家都用熟了,特征对齐这种细节反而最容易卡住。以下条目按"现象 — 原因 — 解决"的顺序写,你可以直接对照排查。
5.1 训练时 loss 变成 nan
现象:epoch 才跑两三个,loss 就变成 nan,acc 直接归零。
原因:最常见的是学习率偏大,ADAM 在 lr=0.01 时对小数据集很容易冲过头;另一个原因是特征里有 NaN 或无穷值,个别样本时长太短导致某个 MFCC 计算失败。
解决:先检查特征文件里有没有 nan,pandas 读进来的 CSV 用 df.isna().sum().sum() 看一眼。然后把 lr 降到 0.001 或 0.0005,训练前做一次 StandardScaler。这套资源里 SCALER 文件的存在就是这个目的,规范化这一步不能省。
5.2 OpenSMILE 提示找不到配置文件
现象:运行 extract_feats/opensmile.py 报错 unable to open config file。
原因:配置文件用的是相对路径,而命令行工作目录不在项目根目录。Windows 下不同盘符切换时相对路径更容易失效,常见于把代码拷到新机器后直接跑。
解决:直接用绝对路径。我一般把 OpenSMILE 的安装路径和 IS09_emotion.conf 的路径都写死成常量,放在 opensmile.py 开头,避免每次换目录都折腾。如果你不用 OpenSMILE,直接用 features 目录里现成的 CSV 特征,也能绕过这个问题。
5.3 LSTM 训练极慢而且验证准确率不涨
现象:一个 epoch 要好几分钟,跑几十个 epoch 准确率还在 50% 附近晃,损失不降。
原因:直接把超长特征序列丢进 LSTM,时间步太长梯度传播困难。Librosa 特征线一帧是 39 维,一段 3 秒音频能提出 300 帧,如果按整段序列训练,LSTM 展开步数太长。
解决:给序列设一个最大长度。Librosa 特征线按 200 帧截断,不够的补零;OpenSMILE 特征线本身已经是固定长度的统计向量,不需要截断。截断后训练速度和收敛速度都能恢复正常。
5.4 predict 时输入维度对不上
现象:模型加载成功,预测时报 Input 0 is incompatible with layer 之类的维度错误。
原因:训练时用的特征类型和预测时不一致,或者配置文件里 num_classes 和模型输出层维度不一致。
解决:predict 时严格使用训练时的 yaml 配置,别手动改 num_classes。加载 checkpoint 后先用 model.summary() 看一眼输出层维度,确认 num_classes 匹配再预测。另外,如果换了模型文件但没换配置文件,特征线不一致也会触发同样问题。
5.5 7 分类的准确率特别低
现象:EMODB 数据集识别率只有三十几,比随机略高,怎么调参都上不去。
原因:EMODB 的标签是德语情感缩写,比如 W 是 angry,T 是 neutral,直接拿缩写当类别名会让模型混淆;另外愤怒和害怕在声学特征上本来就高度重叠,这是数据集本身的主观标注问题。
解决:建立从缩写到英文情感的映射表,检查类别分布,确认没有把某个样本错误归并。情感识别里类别混淆是常态,愤怒和害怕分不清、高兴和惊讶分不清都很常见,不必追求完美,但标签映射错误属于低级错误,必须先排除。
6. 把系统往前推一步:用混淆矩阵验证、用投票机制融合
6.1 用 plot.py 画出混淆矩阵
utils/plot.py 提供了一个画图入口。验证模型不能只看整体准确率,情感识别里"高兴被识别成中性"和"中性被识别成愤怒"的性质完全不一样。我习惯把测试集的真实标签和预测结果拿出来,生成混淆矩阵,重点看对角线以外哪里最密。
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt cm = confusion_matrix(y_true, y_pred) ConfusionMatrixDisplay(cm).plot() plt.savefig('confusion_matrix.png', dpi=150)如果发现某两个类别互相打架,常见处理是合并语义相近的类别,比如把"惊讶"归入"高兴",把"厌烦"归入"悲伤"。这在情感识别实验里是允许的,因为数据集的标注本身就带主观性,不同数据集的类别定义也不完全一致。
6.2 最简单的融合扩展:多模型概率平均
要验证多模型融合的价值,不用写复杂代码,把 SVM、LSTM、CNN1D 各自预测出的类别概率做一个平均就行。
import numpy as np probs = np.array([svm_prob, lstm_prob, cnn_prob]) # 每个都是 (样本数, 类别数) final = probs.mean(axis=0) y_pred = final.argmax(axis=1)这个方法在多数单模型效果一般的场景下能把准确率稳住,尤其是两个模型在错误样本上不重叠的时候。从那以后我每次拿到新的 SER 项目,都会先把最小路径跑通、画出混淆矩阵、再谈融合,否则叠加的模型越多,排查维度 mismatch 的成本越高。这套资源的好处是训练好的 checkpoint 已经齐备,你只需要把 predict 阶段的输出接上这段平均逻辑,就能直接对比融合前后的准确率。希望帮到你。
本文还有配套的精品资源,点击获取