ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

语音情感识别实战:CNN-GRU与注意力机制融合实现

语音情感识别实战:CNN-GRU与注意力机制融合实现 简介面向语音情感识别这一人工智能细分方向这套项目代码以一维卷积神经网络1D-CNN与门控循环单元GRU为骨干模型结合注意力机制进行特征加权并在经数据增强处理的CASIA中文语音情感数据集上完成训练与效果评估可作为科研人员、算法工程师及高年级本科生开展对比实验或项目实践的参考基线。压缩包内共包含7个Python脚本总大小仅20KB脚本除模型定义外还提供了特征融合和混淆矩阵绘制函数方便用户直接调用或二次修改。具体包含多个独立脚本分别实现注意力增强的GRU、卷积循环网络、双向GRU及一维CNN等变体并附有特征融合与混淆矩阵生成工具便于对比实验。目前已有421人学习/下载代码风格简洁、模块划分清晰适合快速搭建语音情感识别基线系统。通过这份代码读者可以掌握从特征融合、模型训练到结果可视化的完整流程并能基于注意力机制展开改进实验用于课程设计、毕业设计或工业预研。1. 为什么语音情感识别绕不开注意力机制语音情感识别SER在真实场景里最难的不是分类而是“同一个词不同人说出来情绪完全不同”而且CASIA这类中文语音库样本量有限直接拿原始波形丢进网络模型很容易过拟合到说话人音色而不是情感本身。这也是为什么这份代码里把数据增强、特征融合、1D-CNN、GRU和注意力机制串成一条完整链路——它不是单点调参而是把“听什么、怎么听、记住什么”拆开处理。对刚入门的工程师来说这份资源能让你直接看到一套可运行的基线系统长什么样对已经做过图像或NLP任务的人来说值得关注的是音频特征在1D卷积下如何保持时域顺序以及GRU的门控机制如何与注意力权重互相补充。下面按实际工程落地顺序从数据增强一路讲到可视化验证。2. 数据增强与特征提取CASIA的预处理链路2.1 为什么CASIA需要做数据增强CASIA中文情感语音库包含6类情感高兴、悲伤、惊讶、恐惧、愤怒、中性由专业录音人在特定文本下录制干净程度高但环境声几乎没有。直接用原始音频训练有两个问题一是模型会把静音段和录音设备响应当作判别特征换到真实录音场景立刻失效二是样本总量不足以让GRU这类循环结构充分学习时序依赖。常见做法是对原始波形做三类增强加性噪声、时间拉伸、音调变换。加性噪声用numpy生成高斯白噪声或叠加真实环境噪声信噪比控制在10-25dB时间拉伸改变语速但不改变基频等价于模拟不同说话节奏音调变换改变基频不改变时长覆盖不同性别和年龄的发音差异。代码里通常会写一个离线增强脚本增强后数据量放大3-5倍再进入特征提取管线。2.2 从波形到双通道特征MEL谱与MFCC拼接项目里Feature_fusion.py的核心逻辑是把MEL频谱和MFCC拼接成双通道输入。MEL谱保留完整的频域能量分布适合CNN卷积核提取局部频谱纹理MFCC则更接近人耳听觉感知的倒谱特征对情感相关的共振峰变化更敏感。两者串联后通道维变成21D-CNN的输入shape为(batch, 2, time_steps, mel_bins)经维度调整后实际按(batch, time, feature)处理。import librosa import numpy as np def extract_feature_pair(wav_path, sr16000, n_mels128, n_mfcc40): y, _ librosa.load(wav_path, srsr) # 预加重高频段在语音中能量偏低人为抬高以均衡频谱 y np.append(y[0], y[1:] - 0.97 * y[:-1]) mel_spec librosa.feature.melspectrogram(yy, srsr, n_melsn_mels, fmin0, fmaxsr // 2) log_mel librosa.power_to_db(mel_spec, refnp.max) mfcc librosa.feature.mfcc(yy, srsr, n_mfccn_mfcc) # 双通道拼接后统一到相同时间帧数 feat np.stack([log_mel, mfcc], axis0) return feat这里预加重系数0.97是语音处理里的经典取值作用是抑制低频噪声干扰refnp.max把频谱能量归一化到相对分贝刻度避免不同录音音量差异直接传导到网络。时间帧数统一这一步容易被忽略如果batch内样本时长不一GRU虽然能处理变长序列但CNN的输出无法直接对齐所以要么截断到固定帧数要么按batch内最大长度padding。2.3 增强与特征提取的耦合顺序顺序问题容易踩坑。如果先做增强再做特征提取加性噪声会进入MEL谱和MFCC的计算模型被迫学习噪声不变性这本身就是一种正则化手段如果先提特征再加噪声噪声的分布与真实信号特征已经不匹配效果差很多。因此正确管线是原始wav → 增强加噪/变速/变调 → 预加重 → 分帧加窗 → 特征提取 → 标准化。标准化统计量只能从训练集计算验证集和测试集使用同一组均值和方差防止信息泄漏。3. 1D-CNN与GRU骨架从局部频谱到时序依赖3.1 1D-CNN为什么要放在GRU前面语音特征在时间维上是连续帧序列单帧内的局部频段变化比如愤怒时高频能量骤增、悲伤时低频持续可以通过1D卷积快速捕获。1D-CNN沿时间轴滑动卷积核输出的是“局部模式响应序列”这比直接把原始高频特征送进GRU更稳定。卷积核数量从32逐层翻倍到128或256每层后接BatchNorm和ReLU防止深层梯度消失。池化层用MaxPooling沿时间维下采样但不要直接压到1否则GRU就失去意义了。项目里的1D-CNN.py走的是典型的编码器路线每个卷积块由Conv1d BatchNorm ReLU MaxPool1d组成最后一个block保留时间步数在10-20之间。这个参数很关键——时间步太少GRU学不到语速变化太多训练代价大且容易过拟合短语音。import torch.nn as nn class ConvBlock(nn.Module): def __init__(self, in_ch, out_ch, kernel_size5, stride1, pool_size2): super().__init__() self.conv nn.Conv1d(in_ch, out_ch, kernel_size, stride, paddingkernel_size // 2) self.bn nn.BatchNorm1d(out_ch) self.relu nn.ReLU(inplaceTrue) self.pool nn.MaxPool1d(pool_size) def forward(self, x): return self.pool(self.relu(self.bn(self.conv(x))))卷积核大小取5而不是3是因为语音帧之间的上下文相关性跨度比图像像素更大5帧的感知野能覆盖一个音节的核心段。padding设为kernel_size // 2保证时间长度不因卷积而迅速收缩而池化层每层把时间步减半4个block后原始约250帧的输入会压缩到15帧左右正好是GRU适合处理的短语级时序长度。3.2 GRU在这里承担什么角色GRU门控循环单元是LSTM的轻量改进两个门重置门、更新门替代了LSTM的三个门参数量更小在小数据集上不易过拟合。CASIA数据量有限LSTM的复杂门控反而容易记住说话人特定习惯GRU的遗忘机制更平滑。双向GRU会同时看前后文——这在语音情感识别里很重要因为人类判断情绪往往需要听到句尾才能确定整句基调单向结构会丢失后文的修正信息。import torch.nn as nn class BiGRUEncoder(nn.Module): def __init__(self, input_size, hidden_size128, num_layers2, dropout0.3): super().__init__() self.gru nn.GRU(input_size, hidden_size, num_layersnum_layers, batch_firstTrue, bidirectionalTrue, dropoutdropout if num_layers 1 else 0) def forward(self, x): outputs, hidden self.gru(x) # outputs shape: (batch, time, 2 * hidden_size) return outputshidden_size128、num_layers2是相对均衡的初始配置。双向GRU输出维度是2 * hidden_size这个维度后续直接喂给注意力机制需要注意和注意力层的输入维度对齐。dropout只对多层结构中层与层之间的连接生效最后一层输出不做dropout避免训练和推理行为不一致。3.3 CNN与GRU的衔接维度处理1D-CNN输出的shape是(batch, channels, time)GRU需要(batch, time, input_size)中间必须做维度置换。常见错误是把channels直接当作GRU的input_size导致每个时间步输入的是整段频谱的通道响应完全丢失帧级顺序信息。正确做法是先transpose(1, 2)把time放到第二维再确认最后一维是CNN的channels。如果CNN输出时间步为15、channels为128则GRU输入维度是128时间步15hidden_size取128能较好地匹配上下文容量。4. 注意力机制让模型知道该盯住哪几帧4.1 从平均池化到注意力为什么简单的mean不行GRU输出的每个时间步都带有当前帧的语义摘要但并非所有帧对情感判断同等重要。愤怒情绪可能在音量爆发的那一帧起决定性作用而中性情感几乎没有突出的峰值帧。如果用简单平均池化等于把“爆发点”稀释在整句的平铺直叙里判别力自然下降。注意力机制本质是给每个时间步学习一个标量权重用softmax归一化后对GRU输出做加权求和相当于让模型自己决定哪几个时间步更关键。4.2 加性注意力的具体实现项目里Attention_GRU.py采用的属于加性注意力范畴核心是一个可学习的打分函数。相比点积注意力加性注意力在特征维度较高时不需要额外的缩放因子训练更稳定。import torch import torch.nn as nn import torch.nn.functional as F class AdditiveAttention(nn.Module): def __init__(self, hidden_dim, attention_dim64): super().__init__() self.W nn.Linear(hidden_dim, attention_dim, biasFalse) self.u nn.Linear(attention_dim, 1, biasFalse) def forward(self, gru_outputs): # gru_outputs: (batch, time, hidden_dim) scores self.u(torch.tanh(self.W(gru_outputs))).squeeze(-1) weights F.softmax(scores, dim1) context torch.bmm(weights.unsqueeze(1), gru_outputs).squeeze(1) return context, weights打分函数先通过全连接层把hidden_dim映射到attention_dim经过tanh激活后压缩到1维分数。softmax在时间维上做归一化确保权重和为1。加权求和用torch.bmm实现weights.unsqueeze(1)把shape从(batch, time)变成(batch, 1, time)才能与(batch, time, hidden_dim)做批量矩阵乘法。这里有一个容易被忽略的点attention_dim不宜过大64或128即可否则打分网络本身参数太多小数据上会学成一个固定映射而非动态选择。4.3 注意力拼接与最终分类CNN-GRU-Attention.py的组合方式是注意力输出的上下文向量与两个方向最后的隐状态拼接构成最终句子级表示。class SERModel(nn.Module): def __init__(self, input_dim, num_classes6): super().__init__() self.cnn nn.Sequential( ConvBlock(input_dim, 64, pool_size2), ConvBlock(64, 128, pool_size2), ConvBlock(128, 256, pool_size2), ) self.gru BiGRUEncoder(256, hidden_size128) self.attention AdditiveAttention(256) self.classifier nn.Sequential( nn.Linear(256 256, 128), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(128, num_classes) ) def forward(self, x): # x: (batch, 2, time, mel_bins) 先转成适应Conv1d的布局 b, ch, t, f x.shape x x.view(b, ch * t, f).permute(0, 2, 1) # 送入Conv1d cnn_out self.cnn(x) cnn_out cnn_out.transpose(1, 2) # (batch, time, channels) gru_out self.gru(cnn_out) context, weights self.attention(gru_out) # 拼接最终隐状态 final_hidden torch.cat([context, gru_out[:, -1, :]], dim-1) logits self.classifier(final_hidden) return logits, weights拼接两个表示的思路是注意力输出提供“全句焦点加权摘要”最后一个时间步的隐状态保留“句尾即时状态”两者互补——因为在实际对话中句尾往往是情感表达最直白的部分。如果模型在训练时注意力权重集中在头部帧说明GRU可能没有学到后向修正需要检查是否用了双向结构或者学习率是否过大。4.4 消融实验判断注意力到底有没有用收敛对比参考表格训练集固定80%验证集和测试集各10%。模型特征输入准确率CASIA测试集1D-CNN.pyMEL谱72.4%BIGRU.pyMFCC68.9%CGRU.pyMEL谱 MFCC拼接76.8%CNN-GRU-Attention.pyMEL谱 MFCC拼接81.3%这组数据说明CNN前置提取局部频谱模式后交给GRU是有效的而注意力机制在“愤怒惊讶”这对易混类上提升最明显这两个类别的共性在于都存在突发性高能量帧注意力能精准锁定爆发点减少被前后静音段稀释的风险。如果条件有限只跑一个baseline建议直接用CGRU.py做对比基准注意力改进幅度至少在3-5个点才算有效。5. 混淆矩阵分析与特征融合的工程验证5.1 confusion_matrix.py的读取confusion_matrix.py负责把预测结果可视化为6x6矩阵横轴是预测标签纵轴是真实标签。重点关注主对角线占比和两类错误模式一是“悲伤/中性”混淆率偏高这是几乎所有SER系统的通病因为中性语音的低唤醒度与悲伤高度重叠二是“恐惧/惊讶”混淆两者都伴随短时能量突刺常规特征无法有效区分时要回到特征层解决而非继续加深网络。import matplotlib.pyplot as plt import itertools import numpy as np def plot_confusion_matrix(cm, classes, normalizeTrue, save_pathcm.png): if normalize: cm cm.astype(float) / cm.sum(axis1)[:, np.newaxis] fig, ax plt.subplots(figsize(8, 6)) im ax.imshow(cm, interpolationnearest, cmapBlues) ax.set_xticks(range(len(classes))) ax.set_yticks(range(len(classes))) ax.set_xticklabels(classes, rotation45) ax.set_yticklabels(classes) for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])): value f{cm[i, j]:.2f} if normalize else format(cm[i, j], d) ax.text(j, i, value, hacenter, vacenter, colorwhite if cm[i, j] 0.5 else black) plt.tight_layout() plt.savefig(save_path, dpi150)注意normalizeTrue时读矩阵要按行理解——每一行概率和为1而不是整张图总和为1。没有应用归一化时若CASIA各类样本不均衡直接看原始数字可能被样本数量误导。5.2 Feature_fusion.py的两种融合层次单纯把MEL谱和MFCC拼接成双通道算是一类特征级融合。另一种在模型层面做MEL谱走一套1D-CNN分支MFCC走另一套1D-CNN分支各自提取高维语义后在特征维上拼接再进GRU。前一种工程实现简单模型自动决定哪个特征维更重要后一种适合两个特征性质差异过大时比如一个要高频细节、一个要整体包络。Feature_fusion.py走的是双分支后融合路线class DualPathFusion(nn.Module): def __init__(self, mel_dim128, mfcc_dim40): super().__init__() self.mel_conv ConvBlock(1, 64) self.mfcc_conv ConvBlock(1, 32) self.fusion_proj nn.Linear(64 32, 128) def forward(self, mel_feat, mfcc_feat): # mel_feat: (batch, time, mel_dim) - 补通道维 mel_out self.mel_conv(mel_feat.unsqueeze(1)) mfcc_out self.mfcc_conv(mfcc_feat.unsqueeze(1)) # 全局池化后拼接 mel_pool mel_out.mean(dim-1) mfcc_pool mfcc_out.mean(dim-1) fused torch.cat([mel_pool, mfcc_pool], dim-1) return self.fusion_proj(fused)双分支设计的收益在于MEL分支可以用更大卷积核捕获宽频段跃迁而MFCC分支用窄卷积核关注精细的倒谱结构二者不会像通道拼接那样互相干扰。代价是两分支输出时间步可能不一致拼接前需要对齐——常见做法是先各自做全局平均池化或自适应池化到固定长度再进入后续结构。5.3 一个容易忽略的验证细节分说话人划分测试集CASIA包含多名发音人文件命名中通常带有发音人编号。如果划分训练/测试集时随机切分同一发音人的不同句会同时出现在训练和测试中模型学到的是发音人的口音和音色特征导致测试准确率虚高。工程上要按发音人分组划分比如保留1-2位说话人的全部样本做测试其余做训练这样得到的结果才对真实应用有参考意义。具体可以参考文件列表里confusion_matrix.py旁边的数据加载函数改动点在于先把文件路径按说话人ID分组再对组索引做train_test_split(..., stratifygroup_labels)。5.4 把注意力分数用起来而不只是可视化注意力权重不是只用来画图的可以直接用于最终判定后处理。当多个类别得分接近时用注意力权重定位最高分帧的能量变化方向如果最高分帧相对句首的短时能量增长超过预设阈值则偏向“愤怒/惊讶”而非“中性”。这个规则在测试集上能额外带来1-2个点的提升但需要人工设计阈值属于细调阶段最后的一招不建议在一开始就加。本文还有配套的精品资源点击获取
返回列表