简介:一套面向深度学习语音情绪识别方向的完整工程包,主要服务于人工智能相关专业的毕业设计、课程设计开发者,覆盖语音数据特征提取、模型训练、预测评估全流程。压缩包共34个文件,包含17个Python脚本(如train.py、predict.py、特征抽取脚本及工具模块)、4个PyTorch模型权重(.pth)、3组特征缓存与标准化文件(.pkl/.csv),以及配置文件和说明文档,整体大小仅15.94MB,目录结构清晰,便于按CNN/RNN/LSTM、Wav2Vec2等模块独立复用。项目内置Librosa、Torchaudio、OpenSmile多套前端特征方案,并给出对应特征缓存,可直接对比不同声学特征对情绪识别的影响;同时提供完整依赖清单和模型checkpoint,便于快速复现与继续训练。目前已有102人浏览学习,适合想系统掌握语音情绪识别技术路线、快速搭建实验环境的开发者借鉴,也可作为课程设计或毕业设计的工程参照。
1. 从三个特征工程到两份权重:语音情绪识别项目应该怎么拆
拿到这个压缩包时,我第一反应不是看模型,而是看features/和checkpoints/这两个目录。语音情绪识别(Speech Emotion Recognition, SER)这个方向,真正拉开差距的往往不在模型结构,而在特征侧。压缩包里同时给了 Torchaudio、Librosa、Wav2Vec2 三套特征提取方案,以及 LSTM、RNN、CNN 三种后端模型和对应的预训练权重——这意味着它不是单一模型的 demo,而是一套完整的对比实验框架。对于正在做课程设计、毕业设计,或者刚入坑语音方向的工程师来说,这种结构比单点模型有价值得多。你不需要自己从头搭数据管线,直接改配置文件就能跑通训练和推理。本文接下来会按特征提取、模型选型、配置驱动训练、预测验证这条链路,把每一层的关键设计和可复现的细节拆开讲。
2. 三种特征提取方案对比:Torchaudio、Librosa 与 Wav2Vec2 的取舍逻辑
2.1 为什么一份代码里要同时维护三套特征管线
语音情绪识别项目里,特征提取的代码往往比模型本身的代码更容易成为性能瓶颈。原因在于情绪信号既依赖短时的音色特征(比如频率分布),也依赖长时的韵律变化(比如语速和音高起伏),单一特征很难同时覆盖这两类信息。这个项目给出的思路是:不做人工融合,而是把三种特征提取方案并列在extract_feats/下统一管理。
具体来看,Torchaudio.py和Librosa.py属于经典信号处理路线。Librosa 提取的是 MFCC、chroma、mel spectrogram 这类手工设计的声学特征,特征是静态的,计算快,但需要研究者自己决定“哪些声学维度对情绪敏感”。Torchaudio 的优势在于它和 PyTorch 的张量操作无缝衔接,特征提取的每一步(分帧、加窗、滤波)都可以在 GPU 上完成,而且支持可微分的特征计算——这意味着特征提取器本身可以作为神经网络的一部分参与反向传播。
Wav2Vec2 则完全不同。它是一个预训练的语音表征模型,原始音频进去,出来的是一段稠密的上下文表征向量。这类特征的优势是无需手工设计,模型在大量无标注语音上预训练后已经学会了通用的声学规律;劣势是特征维度高、计算开销大,且对中文语音的适配取决于预训练权重本身的语言覆盖范围。把这三者放在同一套代码框架下,本质上是在对比“手工特征 + 轻量模型”和“预训练表征 + 轻量模型”两种技术路线的效果边界。
2.2 三类特征的实际加载逻辑与参数配置
先看features/目录下与三种特征配套的scaler.pkl。标准化器单独存放是有讲究的:训练集上计算均值和方差,然后在验证集和测试集上复用同一套统计量,避免信息泄漏。这是时间序列任务里最常见的错误——如果对整段数据一次性拟合标准化器,验证集的分布信息就间接参与了训练,评估结果会虚高。
对应代码逻辑上,一般会写成面向对象的方式,让每个特征提取器对外暴露load()、extract()和normalize()三个方法,这样在训练脚本里可以做到特征侧对模型侧完全透明:
import torchaudio import torch class TorchaudioFeatureExtractor: def __init__(self, sample_rate=16000, n_mels=128): self.sample_rate = sample_rate self.transform = torchaudio.transforms.MelSpectrogram( sample_rate=sample_rate, n_fft=1024, hop_length=512, n_mels=n_mels ) def extract(self, waveform: torch.Tensor) -> torch.Tensor: # 输入: (batch, time) 输出: (batch, n_mels, time) return torch.log1p(self.transform(waveform))这里把n_fft设为 1024、hop_length设为 512,即帧长约为 64ms,帧移为 32ms。对 16kHz 采样的语音来说,这个分辨率能保留足够的频率细节用于区分愤怒和高兴这类高唤醒度情绪,同时不会让序列长度过长拖慢训练速度。log1p是对幅度谱做对数压缩,模拟人耳对声音强度感知的非线性特性。
2.3 特征与模型的时间维度对齐
一个值得注意的设计点是时间维度的对齐。Librosa 提 MFCC 时默认的 hop length 是 512,而 Torchaudio 的 MelSpectrogram 如果按上面的参数配置,输出的时间帧数是ceil(waveform_len / 512),两者基本能对齐。但 Wav2Vec2 的特征是逐帧输出的,帧率是 50Hz,也就是每 20ms 一帧,和传统特征的 32ms 帧移并不一致。
训练时会遇到的典型问题是:同一个 batch 里,不同样本的原始音频长度不同,经过三种特征提取器后得到的时间帧数也不同。常见的处理策略是按特征帧数做动态 padding,然后配合 mask 让 LSTM 或 RNN 忽略 padding 部分。还有一种做法是直接截断到固定长度,比如统一截取 6 秒,保证特征形状是(batch, feature_dim, fixed_frames)。前一种方案信息利用率更高,适用于数据量小、每条样本都不舍得丢的项目;后一种实现简单,Shape 固定后 CNN 和 RNN 都能直接用。
提示:如果你在训练时发现 Wav2Vec2 特征和其他特征的 batch 维度对不上,先检查两边的帧率换算,而不是急着改模型输入维度。
3. 模型层的工程组织:LSTM、RNN、CNN 如何共享一套训练入口
3.1 模型注册机制与统一接口
models/目录下的组织方式体现了工程上的考量。base.py定义基类,LSTM.py、RNN.py、CNN.py各自继承并实现具体结构,__init__.py里做统一导出。这样做的好处是训练脚本里不需要写 if-else 来分发模型结构,而是通过配置项动态实例化。
通常的做法是在base.py中定义一个工厂方法:
class BaseModel(nn.Module): def __init__(self, config: dict): super().__init__() self.config = config def forward(self, x, lengths=None): raise NotImplementedError def build_model(name: str, config: dict): if name == "LSTM": from models.LSTM import EmotionLSTM return EmotionLSTM(config) elif name == "CNN": from models.CNN import EmotionCNN return EmotionCNN(config) elif name == "RNN": from models.RNN import EmotionRNN return EmotionRNN(config) else: raise ValueError(f"Unknown model: {name}")工厂方法的核心约束是:所有模型对外暴露的 forward 签名必须一致,都接收(x, lengths)。lengths参数传给 LSTM 时用于 pack_padded_sequence,传给 CNN 时可以省略,但不建议在基类里做参数分叉——保持签名统一,上层调用代码才不需要感知模型差异。
RNN 和 LSTM 放在两个文件里而不是合并,看起来冗余,实际上对情绪识别是有意义的。vanilla RNN 在短语音片段上往往表现不差,且参数量只有 LSTM 的四分之一左右,训练速度快,适合做基线实验;LSTM 则更适合捕捉语音中跨 1 秒以上的情绪渐变过程。用配置项切分这两种结构,便于直接对比“记忆能力对情绪识别是否必要”。
3.2 拼音输入还是频谱输入:特征维度与模型结构的匹配
这里有一个在课程设计中经常踩的设计问题:如果特征侧的features/已经输出了 MFCC 这类二维特征(时间帧 × 特征维度),模型侧是用一维卷积(沿时间方向滑动)还是二维卷积(把时间帧和特征维度当图像的两个空间轴)?
项目里CNN.py走的通常是二维卷积路线:把 mel spectrogram 或 MFCC 矩阵当作单通道灰度图,用 Conv2d 提取局部时频模式。愤怒情绪的语音往往在 2000-4000Hz 区间有较高的能量集中,这种频带特征通过二维卷积的垂直卷积核更容易被捕捉。
配置上要注意通道数设计。如果输入是 MFCC 40 维,reshape成(batch, 1, time, 40)后,第一层卷积的 kernel size 建议设为(3, 3),padding 设为(1, 1),保持时频分辨率不变。池化层沿时间轴的 stride 可以大一些,技术上讲,情绪识别不需要帧级别的精度,时间上压缩 4 倍对最终准确率影响很小,但能显著降低后续全连接层的计算量。
3.3 Checkpoints 组织方式与恢复训练逻辑
checkpoints/目录按特征方案分成了librosa/和Wav2Vec2/两个子目录,下面分别放着LSTM_best_model.pth、RNN_best_model.pth、best_model.pth。这种命名规则中,没有显式标注 CNN 的目录里就是 CNN 权重,标注了 LSTM 和 RNN 的则是相应的循环网络权重。
断点续训和权重加载的核心在于只加载模型参数而不加载优化器状态:
def load_weights(model: nn.Module, checkpoint_path: str, device: str): state = torch.load(checkpoint_path, map_location=device) filtered = {k: v for k, v in state.items() if "classifier" not in k} model.load_state_dict(filtered, strict=False) print(f"Loaded {len(filtered)} layers from {checkpoint_path}")用strict=False的好处在于:如果你修改了分类层的输出类别数(比如从 7 类情绪改为 5 类),加载预训练权重时不会因为最后一层维度不匹配而报错。在课程设计中,这种做法尤其常见——原项目的情绪类别不一定和你自己的数据集完全一致。
4. 配置驱动的训练流程:INI 文件如何拆解实验参数
4.1 INI 配置的模块化设计
configuration/目录下放了RNN.ini、CNN.ini、new.ini、demo.ini四个配置文件。这种把模型结构、训练超参数、数据增强开关分散到不同文件里的做法,适合课程设计中的多组对比实验。每一组实验只需要复制一份 INI、修改几个字段,按文件名即可溯源。
一个标准配置文件的骨架大致如下:
[data] feature_type = librosa dataset_root = ./dataset sample_rate = 16000 max_len = 6.0 [model] name = LSTM hidden_size = 128 num_layers = 2 dropout = 0.3 [training] epochs = 60 batch_size = 32 learning_rate = 1e-4 early_stop_patience = 8按照这个配置,训练启动时config.py模块读取 INI 并传给train.py。feature_type指定从features/目录加载哪一列特征对应的 scaler 和特征文件。在强烈依赖数据集划分的语音任务中,seed字段的显式配置也很重要——决定训练集/测试集划分的随机种子不同,输出结果就有 randomness 因素,写论文时很难复现。
4.2 训练前的数据对齐检查
一个容易忽略的点:train.py在进入训练循环前,应当先执行一次数据对齐自检。具体逻辑是——对每条语音样本,加载原始波形后依次送入特征提取器和模型,打印模型输出的 shape,与当前 batch 内其他样本比对。
python train.py --config configuration/CNN.ini --check_mode代码里建议加上--check_mode这个参数,走一遍完整前向但不跑反向传播,能提前定位 80% 以上的维度不匹配问题。常见报错是Expected input batch_size (32) to match target batch_size (16)——这是 padding 时没按 batch 内实际长度对齐导致的。LSTM 训练时如果手动做了 pad_sequence,一定要用pack_padded_sequence把填充区域屏蔽掉。
4.3 训练过程中的关键超参数选择
学习率和 batch size 的组合在这类情感分类任务里比较固定。语音情绪识别的数据集规模都不大(RAVDESS 约 1400 条,CASIA 约 9600 条),设置batch_size=32、learning_rate=1e-4是常见跑顺的起点。优化器选 Adam,但将 weight decay 设置为1e-5防止过拟合。
epochs 的设置则要考虑 early stopping。情绪识别任务的验证集准确率往往在第 20-30 个 epoch 左右达到平台期,后面就是来回震荡。把early_stop_patience设为 8,意味着连续 8 个 epoch 验证集准确率不提升就终止训练,这样既不需要手动盯训练曲线,也能避免在测试集上调参导致的过拟合。压缩包里的best_model.pth保存的应该是验证集准确率最高的那个 checkpoint,而不是最后一个 epoch 的。
注意:配置文件中的路径一律使用相对路径,并且与
README.md的说明保持一致。直接复制压缩包到别的机器上时,相对路径不会因为用户主目录不同而失效。
5. 从 checkpoints 到推理预测:predict.py 的使用方式与跨库验证
5.1 推理管线与特征一致性
训练时用什么特征,预测时就必须用同一套特征管线。这是语音识别领域最容易踩的坑——训练集上做了 log1p、做了均值方差标准化,推理时如果忘了做同一个变换,预测概率分布会完全偏移。predict.py的设计思路应该是:复用一个完整的特征提取流程,而不是把特征提取代码复制到预测脚本里。
def predict_single(wav_path: str, config: dict): # 1. 加载音频 waveform, sr = torchaudio.load(wav_path) if sr != config["sample_rate"]: resampler = torchaudio.transforms.Resample(sr, config["sample_rate"]) waveform = resampler(waveform) # 2. 提取特征(与训练完全一致) extractor = get_feature_extractor(config["feature_type"]) feature = extractor.extract(waveform) # 3. 加载标准化器并归一化 scaler = pickle.load(open(config["scaler_path"], "rb")) feature = scaler.transform(feature) # 4. 模型推理 model = build_model(config["model"]["name"], config["model"]) model.load_state_dict(torch.load(config["checkpoint_path"])) model.eval() with torch.no_grad(): logits = model(feature.unsqueeze(0)) prob = torch.softmax(logits, dim=-1) return prob这段代码的关键在第二步到第三步之间不能插入任何额外的处理步骤。很多人会在推理时顺手加一个静音切除,训练阶段没有同样的预处理,就会导致特征分布不一致。语音情绪识别和语音识别的最大区别就在这里:ASR 任务对静音段不敏感,但情绪识别里静音段的时长和位置本身就携带着节奏信息,两个预处理管线不一致会直接拉低 ACC。
5.2 情绪标签平滑与阈值策略
推理输出的 softmax 概率不能直接当作最终判断依据。实际情况下,情绪识别的类别分布很不均衡——平静和中立情绪容易占多数样本,愤怒和厌恶容易被混淆。一个有用的做法是对输出概率做温度缩放:
temperature = 1.5 scaled_probs = torch.softmax(logits / temperature, dim=-1) label = torch.argmax(scaled_probs).item()温度大于 1 时,softmax 的输出分布变得更平滑,低置信度的类别不会被直接“干掉”。这在跨库测试(比如用 RAVDESS 训练的模型去测 CASIA 的数据)时尤其有用:不同数据集的标注风格差异很大,训练集里“愤怒”可能是大吼,测试集里的“愤怒”只是提高音量,硬性 argmax 容易把这种边界样本判错。温度缩放无法完全消除数据集偏移,但至少能避免概率接近时的一票定音。
5.3 跨库验证脚本的组织
用压缩包里现成的 checkpoint 做跨库验证时,建议把验证目标限定在“同分布数据”和“相近领域数据”两个层次。同分布就是与训练集同源的测试集部分,这个指标代表了模型本身的拟合能力;跨库验证是在另一个数据集上用零样本方式跑一遍推理,观察准确率掉落幅度。语音情绪识别领域一个公开的现象是:跨库准确率往往比同库低 20 到 30 个百分点,这是领域漂移(domain shift)的直接影响,不是模型代码写错了。
写验证脚本时,可以让predict.py接受一个目录参数,自动批处理目录下所有 wav 文件,输出一个 CSV 文件记录每条的预测标签和置信度。随后用一个简单的plot.py绘制混淆矩阵,看哪些情绪对互相打架。愤怒和厌恶、平静和悲伤是默认两个最容易混淆的情绪对,如果混淆矩阵里这两对不是最高值,反而需要检查是不是数据预处理步骤引入了偏差。
6. 动手完善项目时的四个工程细节
围绕这份代码做二次开发时,有几处小改动对项目体验提升很明显。第一个是把三套特征提取器的结果缓存到本地磁盘,格式可以统一用.npy。语音数据集的特征提取是纯 CPU 密集型操作,Wav2Vec2 虽然可以跑 GPU,但每次实验都重新提取一遍会浪费大量时间。在features/下建立一个cache/目录,以(数据集名, 特征类型, 音频文件名)做 key,如果缓存命中就直接加载,不重新走特征提取流程。
第二个细节是样本级别的数据增强。语音情绪识别领域最有效、实现成本最低的增强是加性噪声和音高微调。加载音频后,在送进特征提取器之前,以 0.3 的概率给波形叠加一个低幅度的环境噪声(可以从torchaudio.functional.add_noise实现),再用torchaudio.functional.speed做 0.95 到 1.05 倍速的随机变速。注意变速会改变帧数,所以要与后续 padding 策略配合使用,实际中更稳妥的做法是先变速再统一截断到固定长度。
第三个是类别权重。原始数据集中平静/中立类别的样本量通常是愤怒/恶心的数倍,直接用 CrossEntropyLoss 训练会让模型对少数类情绪几乎不敏感。在train.py中读取配置文件的class_weight字段,计算每个 batch 的损失时按类别频率反比分配权重,这种行为改变往往比微调模型结构更有效。
第四个是我自己会改的一个点:在utils/plot.py中同时输出训练集和验证集的 loss 曲线到一张图上。模型过拟合时,训练 loss 一路下降但验证 loss 在第 15 个 epoch 附近开始抬升,对比两条曲线可以在 early stopping 触发之前就判断出走火趋势。另外有些框架会提示 "UserWarning: Named tensors and all their associated APIs are an experimental feature",不影响运行,直接忽略或者用未命名张量替代操作即可。这部分工程化改造做完,这套基于深度学习的语音情绪识别框架就算是真正长在了自己的项目里。
本文还有配套的精品资源,点击获取