基于MLA的人类语音情感分类

发布于:2025-03-28 ⋅ 阅读:(36) ⋅ 点赞:(0)
《DeepSeek大模型高性能核心技术与多模态融合开发(人工智能技术丛书)》(王晓华)【摘要 书评 试读】- 京东图书

随着信息技术的不断发展,如何让机器识别人类情绪,这个问题受到了学术界和工业界的广泛关注。目前,情绪识别有两种方式,一种是检测生理信号,如呼吸、心率和体温等;另一种是检测情感行为,如人脸微表情识别、语音情绪识别和姿态识别。语音情绪识别(Speech Emotion Recognition,SER)是一种生物特征属性的识别方法,可通过一段语音的声学特征(与语音内容和语种无关)来识别说话人的情绪状态。语音情绪示例如图6-3所示。

图6-3  语音情绪示例

在上一节中,我们完成了基于文本的评论情感分类模型,并完成了一个具有示范意义的模型训练。本节将继续完成一项语音实战任务,即基于MLA的人类语音情感分类。     

6.2.1  情绪数据的获取与标签的说明

首先是语音情绪数据集的下载,这里我们使用瑞尔森情感语音和歌曲视听数据库RAVDESS。RAVDESS语音数据集部分包含1440个文件:每个演员60次试验×24名演员=1440。RAVDESS包含24名专业演员(12名女性,12名男性),用中性的北美口音说出两个词汇匹配的陈述。言语情绪包括平静、快乐、悲伤、愤怒、恐惧、惊讶和厌恶的表情。每种表情都在两种情绪强度(正常、强烈)和一种额外的中性表情下产生。

读者可以自行下载对应的数据集,这里我们使用Audio_Speech_Actors_01-24.zip这个子数据集做情感分类。下载后的数据集结构如图6-4所示。   

图6-4  左图是Audio文件夹,右图是单个文件夹数据

下面需要讲解一下情绪文件的标签问题。这个数据包含中性、平静、快乐、悲伤、愤怒、恐惧、厌恶、惊讶八种情感。请读者注意一下,本项目只使用里面的Audio_Speech_Actors_01-24.zip数据集,说话的语句只有Kids are talking by the door和Dogs are sitting by the door。

在这个数据集中,每个音频文件都拥有一个独一无二的文件名,例如图6-4所示的“03-01-01-01-01-01-01.wav”。这些文件名由七部分数字标识符构成,并非随意命名。这些数字标识符实际上赋予了文件名特定的标签意义,通过文件名就能够了解音频文件的某些属性或特征:

通过对比,03-01-02-01-01-01-01.wav这个文件对应的信息是:

另外需要注意,在这个数据集中,音频的采样率为22050,这一点可以设定或者采用在上一章介绍的librosa进行读取。

6.2.2  情绪数据集的读取

下面是需要对情绪数据集的读取,在读取数据之前需要注意,数据集中每个文件都存放在不同的文件夹中,而每个文件夹都包含若干个不同的情绪文件。因此,在读取数据时首先需要实现文件夹的读取函数:

import numpy as np
import torch
import os
import librosa as lb
import soundfile

# 这个是列出所有目录下文件夹的函数
def list_folders(path):
    """
    列出指定路径下的所有文件夹名
    """
    folders = []
    for root, dirs, files in os.walk(path):
        for dir in dirs:
            folders.append(os.path.join(root, dir))
    return folders

def list_files(path):
    files = []
    for item in os.listdir(path):
        file = os.path.join(path, item)
        if os.path.isfile(file):
            files.append(file)
    return files

由于这里读取的是音频数据,我们在上一章对音频数据降维的时候完成了基于librosa库包的音频读取和转化,可以把相应的代码用在这个示例中,代码如下所示。

#注意采样率的变更
def audio_features(wav_file_path, mfcc = True, chroma = False, mel = False,sample_rate = 22050):
    audio,sample_rate =  lb.load(wav_file_path,sr=sample_rate)
    if len(audio.shape) != 1:
        return None
    result = np.array([])
    if mfcc:
        mfccs = np.mean(lb.feature.mfcc(y=audio, sr=sample_rate, n_mfcc=40).T, axis=0)
        result = np.hstack((result, mfccs))
    if chroma:
        stft = np.abs(lb.stft(audio))
        chroma = np.mean(lb.feature.chroma_stft(S=stft, sr=sample_rate).T, axis=0)
        result = np.hstack((result, chroma))
    if mel:
        mel = np.mean(lb.feature.melspectrogram(y=audio, sr=sample_rate, n_mels=40, fmin=0, fmax=sample_rate//2).T, axis=0)
        result = np.hstack((result, mel))
    # print("file_title: {}, result.shape: {}".format(file_title, result.shape))
    return result

这里需要注意,由于读取的是不同数据集,而采样率会随着数据集的不同而变化,因此这里的采样率为22050。

下面展示了完整的数据读取代码。为了便于理解,我们对每种情绪进行了文字定义,并将这些定义与相应的情绪序号进行了关联。需要注意,在前面的讲解中,情绪序号是排在文件名中第三个位置的数据。因此,我们可以通过对文件名进行文本分割,提取出情绪序号,并根据序号与情绪的对应关系,读取并理解相应情绪的标签,代码如下:

ravdess_label_dict = {"01": "neutral", "02": "calm", "03": "happy", "04": "sad", "05": "angry", "06": "fear", "07": "disgust", "08": "surprise"}

folders = list_folders("./dataset")
label_dataset = []
train_dataset =  []
for folder in  folders:
    files = list_files(folder)
    for _file in files:

        label = _file.split("\\")[-1].replace(".wav","").split("-")[2]
        ravdess_label = ravdess_label_dict[label]
        label_num = int(label) - 1  #这里减1是由于初始位置是1,而一般列表的初始位置是0

        result = audio_features(_file)
        train_dataset.append(result)
        label_dataset.append(label_num)

train_dataset = torch.tensor(train_dataset,dtype=torch.float)
label_dataset = torch.tensor(label_dataset,dtype=torch.long)

print(train_dataset.shape)
print(label_dataset.shape)

最终的打印结果是将训练数据和label数据转化为torch的向量,如下所示。

torch.Size([1440,40])
torch.Size([1440])

这里我们仅仅使用了MFCC的特征作为音频特征,对于其他特征,读者可以自行尝试。特别需要注意,这个示例中MFCC的维度是40,这与后续模型的输入维度相同;如果修改了输入特征长度后,后续的模型维度也要修改。

6.2.3  语音情感分类模型的设计和训练

接下来,我们需要完成情感分类模型的设计与训练。首先是模型部分,根据我们在上一节获取到的输入数据与标签信息,完成模型的设计,代码如下所示。

import torch
import torch.nn as nn
import torch.optim as optim

from moudle import attention_moudle,feedforward_layer
class EncoderBlock(torch.nn.Module):
    def __init__(self,d_model = 128,attention_head_num = 4,hidden_dropout = 0.1):
        super().__init__()
        self.hidden_dropout = hidden_dropout
        assert d_model%attention_head_num == 0,print("d_model % n_head must be Zero")
        self.d_model = d_model
        self.attention_head_num = attention_head_num
        self.per_head_num = d_model//attention_head_num

        self.input_layernorm = torch.nn.RMSNorm(d_model)
        self.mha = attention_moudle.MultiHeadAttention_MLA(d_model,attention_head_num)

        self.post_attention_layernorm = torch.nn.RMSNorm(d_model)
        self.mlp = feedforward_layer.Swiglu(hidden_size=d_model)

    def forward(self,x, past_length = 0):
        residual = x

        embedding = self.input_layernorm(x)

        embedding = self.mha(embedding,past_length=past_length)
        embedding = torch.nn.functional.dropout(embedding,p = self.hidden_dropout)
        layernorm_output = self.post_attention_layernorm(embedding + residual)
        mlp_output = self.mlp(layernorm_output)

        output = torch.nn.functional.dropout(mlp_output, p=self.hidden_dropout)
        output = residual + output

        return output

# 定义模型
class DNN(torch.nn.Module):
    def __init__(self, input_size = 40, hidden_size = 128, output_size = 8):
        super(DNN, self).__init__()
        self.hidden = torch.nn.Linear(input_size, hidden_size)
        self.encoder_block = EncoderBlock(d_model = hidden_size, attention_head_num = 4)
        self.relu = torch.nn.ReLU()
        self.output = torch.nn.Linear(hidden_size, output_size)

    def forward(self, x):
        x = torch.unsqueeze(x, 1)
        x = self.hidden(x)
        x = self.encoder_block(x, past_length=0)
        x = torch.nn.Flatten()(x)
        x = self.relu(x)
        x = self.output(x)
        return x

上面这段代码定义了一个深度神经网络(DNN)模型,它包含一个线性层、一个编码块(EncoderBlock)、一个ReLU激活函数层和一个输出层。编码块内部使用了多头注意力机制(MultiHeadAttention_MLA)和前馈网络(Swiglu),并在注意力和前馈网络后应用了残差连接和层归一化(RMSNorm)。模型的前向传播首先将输入数据通过隐藏层,然后通过编码块,接着应用ReLU激活函数,最后通过输出层得到预测结果。请读者运行代码验证结果。

本文节选自《DeepSeek大模型高性能核心技术与多模态融合开发》,获出版社和作者授权发布。