【复现日志】Clustering-Based Speech Emotion Recognition by Incorporating Learned Features and Deep BiLSTM

Clustering-Based Speech Emotion Recognition by Incorporating Learned Features and Deep BiLSTM

复现日志

2021/11/22

看完这篇文章,初步的想法是转载内容:

[En]

After reading the article, the preliminary idea is to reproduce the content:

  1. 数据集的下载和管理(注意speaker dependent和speaker independent数据集处理方式是不同的,需要设置一个超参获取数据信息,并存储到numpy数组当中)

  2. speaker dependent是将所有文件混合后,随机选取80%数据作为测试和20%数据作为验证和测试。

  3. speaker dependent是按照speaker进行五折交叉验证,80%的人用来训练,20%的人用来验证和测试。
    初步考虑将所有文件按照speaker划分文件夹,但是经过观察之后发现对于IEMOCAP数据集来说,session中的男女数据是混杂在一起的,带有”M”和”F”字样,考虑后续获得原始数据后,在训练前转化为语谱图阶段再进行划分,划分后每个session文件夹包含两个speaker,命名为M和F,每个性别文件夹内是两个npy文件,命名为data和label,分别存放聚类后的片段和标签。

  4. 将数据分为片段,窗长为500ms,有25%交叠,每个句子分别处理,存储到numpy数组当中作为处理后的数据,数据的形状大致为(样本数,片段数目,每个片段的数据长度)。

  5. 将上面处理好的数据进行聚类,距离度量使用RBF而不是欧拉距离矩阵,首先确定K值,阈值threshold动态计算[32],具体操作方式看一下该文章,K值确定后,进行K-Means聚类,将每个语段中距离每类最近的一个片段作为key segment,将所有key segment按照speaker存储到npy文件当中备用(上述为特征提取部分,参数一旦改变需要重新运行上面的程序),聚类结果最好做一个可视化,只展示部分数据(考虑4*4窗格)【这里先不加,后面有需要的话,可以添加可视化功能】。
  6. 构建后面的训练模型,CNN与LSTM部分是在一起训练的,可以同时写在一个模型里,但是CNN部分是预训练的,而且是一部分网络,可以在训练之前初始化参数时把预训练参数赋值好,然后LSTM进行空的初始化。这里注意双向LSTM的写法,是两层LSTM,可参考网络资料。LSTM最后一个时间步长的输出加log softmax进行结果预测,这里不要忘记对阈值确定的系数β \beta β进行寻优。
  7. 训练过程参数初步考虑:

  8. epoch = 100

  9. learning rate = 0.001
  10. optimizer = Adam
  11. loss = 对数交叉熵
  12. equipment = RTX 3060 GPU
    五折交叉验证需要根据数据集不同进行调整,同样需要设置if选择超参数决定

  13. 最后的结果需要speaker dependent和speaker independent分别的准确率、召回率、F1分数共2 * 3 * 3 (18个)数据,有必要可以加上WA,还有二者对应的混淆矩阵3 * 2(6个),这里只用IEMOCAP的话,需要SD、SI的三个数据3*2共6个数据和两个混淆矩阵。

数据预处理 2021/11/24

首先采用IEMOCAP数据集作为搭建,后来的数据集还未下载,所以暂时先不考虑数据集接口的更换问题。
IEMOCAP数据集的结构如下所示:

【复现日志】Clustering-Based Speech Emotion Recognition by Incorporating Learned Features and Deep BiLSTM
共有5个session,其中共有10种情感,这里文中只用了4种:anger、hapiness、neutral和sadness,所以将其他没用的数据删掉,保留这四种情感即可。首先要进行的是分帧,即将所有的语音数据读取后分成窗长500ms,25%重叠的(0.25*500=125 overlapping)片段,供后续使用,片段不足500ms的部分进行补零。该数据集采样率为16000Hz,那么每个片段
这里考虑几个函数:
  • 音频读取函数:输入文件路径,输出音频读取的numpy数组,每个数是每个采样点的值;(经过查找可以直接使用scipy中的read函数读取到numpy数组当中)
  • 片段切割函数:输入为数据、采样率、窗长和overlap的百分比,输出每个数据的切割结果,放在一个数组当中;
  • 主函数:按文件夹读取音频,切割后将音频存入对应新的文件夹中,最后得到的numpy文件的文件夹组织要和上面一样。
import os
import numpy as np
import math
from scipy.io.wavfile import read

def process(data, sp_r, win_len=500, overlap=0.25):
    """分割语段函数,输入为数据、采样率、窗长和overlap的百分比,输出每个数据的切割结果,放在一个数组当中"""
    num = len(data)
    win_num = int(sp_r * (win_len / 1000))
    gap = math.floor(win_num * (1 - overlap))
    start = 0
    result = []
    while start < num:
        if start + win_num >= num:
            seg = np.zeros([win_num])
            seg[:len(data[start:])] = data[start:]
        else:
            seg = data[start: start + win_num]
        result.append(seg)
        start += gap
    return np.array(result)

if __name__ == '__main__':
    data_base = 'IEMOCAP'
    sample_rate = 16000
    target_name = 'IEMOCAP-preprocess'

    for dir_path, dir_names, filenames in os.walk(data_base):
        if not os.path.exists(target_name + dir_path[len(data_base):]):
            os.mkdir(target_name + dir_path[len(data_base):])
        for name in filenames:
            path = os.path.join(dir_path, name)
            wave_data = read(path)[1]
            processed = process(wave_data, sample_rate)
            new_dir = target_name + path[len(data_base): - (len(name) + 1)]
            if not os.path.exists(new_dir):
                os.mkdir(new_dir)
            np.save(new_dir + '\\' + name[:-3] + 'npy', processed)

更改数据库只需要更改data_base、sample_rate和target_name三个参数即可。

聚类 2021/11/25

上面经过预处理后,每个语段被分为若干大小相同的片段,我们而后需要对每个语段进行聚类以找到key segements,并将结果的npy文件保存在另外结构同源数据相同的文件夹中。需要注意的是,聚类需要一个参数K,即每个语段聚类类数,这个参数是通过一个阈值动态确定的,在每个语段中是不同的。
其中文献[32]给出了文中所使用方法Shot boundary detection的具体细节,文章连接:镜头边界检测,文中并没有具体说明是怎么计算距离的,所以这里暂时采用欧式距离矩阵计算两个片段间的距离。直接使用scipy中自带的cdist函数计算即可。根据文献[32]中的描述,阈值的设置公式如下,首先计算出所有相邻片段间的距离,对这些距离求均值,前面乘以一个权重作为阈值:

【复现日志】Clustering-Based Speech Emotion Recognition by Incorporating Learned Features and Deep BiLSTM
上面的权重自定义,一般β \beta β取为5-10之间最佳,我考虑将β \beta β作为外部参数,训练时调整参数寻优。
计算欧几里得距离时,我发现距离数值非常大,所以在计算前可以将所有数值按行进行z-score归一化,当然这里的归一化方法不是固定的,对结果的影响未知。但是归一化之后发现出错了,因为计算欧几里得距离时有可能算出inf或者nah,所以这里我去掉归一化了。
然后就是进行K-Means聚类了,可以单独有一个函数进行聚类,直接采用sklearn包的聚类算法,这里需要自定义距离度量,改成文中的RBF函数计算,具体自定义方法详见:sk-learn自定义距离度量
聚类函数: 输入样本和聚类数,输出距离聚类中心最近的k个样本和标签情况(用来进行可视化),当然这里K=1没有意义,函数自己会将其归为一类。自带聚类函数中包含很多信息,有K个聚类中心和聚类结果标签。
K的取值显然是很重要的,有的时候会有6个片段只分为1类的情况,那只能选取一个片段训练,显然丢失了很多信息,这个K值选择的过程如果能优化一下,或者片段长度小一点,会不会好一些?
这里用sk-learn出现了一个警告信息:KMeans is known to have a memory leak on Windows with MKL, when there are less chunks than available threads. You can avoid it by setting the environment variable OMP_NUM_THREADS=1. 不知道是为什么
在这里聚类还是个问题,下面的公式看不懂,不能完成那里的自定义距离,暂时放在这里,用欧几里得距离聚类,为什么不直接通过核函数把数据映射到高维空间呢?取而代之的是使用核函数作为距离度量?
[En]

Clustering here is still a problem, the following formula can not understand, can not complete the custom distance there, temporarily put here, using Euclidean distance clustering, and why not directly map the data to the high-dimensional space through the kernel function? Instead, the kernel function is used as the distance measure?

pyclustering包中其实是有聚类结果可视化的,如果后期有需要会加入可视化功能。

from scipy.spatial.distance import euclidean
import os
import numpy as np

from sklearn.cluster import KMeans

def clustering(x, k):
    """聚类函数,输入样本和聚类数,输出距离聚类中心最近的k个样本"""
    clf = KMeans(k)
    r = clf.fit(x)
    clustered = []
    for c in r.cluster_centers_:
        item = x[0]
        min_dist = np.Inf
        for i in x:
            if euclidean(c, i) < min_dist:
                item = i
                min_dist = euclidean(c, i)
        clustered.append(item)
    return clustered, r.labels_

if __name__ == '__main__':
    origin_name = 'IEMOCAP-preprocess'
    target_name = 'IEMOCAP-clustered'
    thresh_weight = 1.5
    for dir_path, dir_names, filenames in os.walk(origin_name):
        if not os.path.exists(target_name + dir_path[len(origin_name):]):
            os.mkdir(target_name + dir_path[len(origin_name):])
        for name in filenames:
            K = 1
            path = os.path.join(dir_path, name)
            data = np.load(path)

            dists = []

            for seg in range(data.shape[0] - 1):
                dist = euclidean(data[seg], data[seg + 1])
                dists.append(dist)

            T = thresh_weight * np.mean(dists)

            for dis in dists:
                if dis > T:
                    K += 1

            result, labels = clustering(data, K)
            new_dir = target_name + path[len(origin_name): - (len(name) + 1)]
            if not os.path.exists(new_dir):
                os.mkdir(new_dir)
            np.save(new_dir + '\\' + name[:-3] + 'npy', result)

生成语谱图 2021/11/26

下一步需要将上面聚类后的结果转换为语谱图放入CNN-BiLSTM网络进行训练,这里单独写一个函数将特征提取结果保存在和源文件结构相同的文件夹当中,过程很简单,即读取对应文件夹及文件后进行处理保存。
这里很奇怪,一个语段分成segments之后,最后生成的语谱图形状是(片段数,时间,频率)一个三维图,直接输入到网络当中吗?显然片段数是不固定的,但是CNN的输入通道大小是确定的,这是怎么回事?只有一种可能,就是取数据的时候取得是(batch-size,片段数,时间,频率)这样大小,但是将前两维度合并,作为二维图输入网络

import os
import numpy as np
import scipy.signal as signal

"""运行顺序3:获取关键片段的频谱图作为特征并存储到文件中,结构同源"""

if __name__ == '__main__':
    origin_name = 'IEMOCAP-clustered'
    target_name = 'IEMOCAP-feature'
    for dir_path, dir_names, filenames in os.walk(origin_name):
        if not os.path.exists(target_name + dir_path[len(origin_name):]):
            os.mkdir(target_name + dir_path[len(origin_name):])
        for name in filenames:
            path = os.path.join(dir_path, name)
            data = np.load(path)
            result = []
            for i in range(len(data)):
                _, _, z = signal.stft(data[i])
                result.append(z)
            result = np.array(result)
            new_dir = target_name + path[len(origin_name): - (len(name) + 1)]
            if not os.path.exists(new_dir):
                os.mkdir(new_dir)
            np.save(new_dir + '\\' + name[:-3] + 'npy', result)

重构特征 2021/11/26

上面提取好特征后,结构和原来的文件夹是一样的,不利于训练,我们需要的是每个speaker在一个文件夹中,因为训练时需要五折交叉验证。每个speaker文件夹包含两个文件夹:feature和label,训练时直接读取feature和label中的数据。

【复现日志】Clustering-Based Speech Emotion Recognition by Incorporating Learned Features and Deep BiLSTM
import numpy as np
import os

"""运行顺序4:重构文件夹形态,按照每一个speaker特征和标签进行划分,由于数据库本身特性,可能只适用于IEMOCAP,请勿改变任何文件夹名称"""

if __name__ == '__main__':
    dic = {'ang':0, 'hap':1, 'neu':2, 'sad':3}
    origin_path = 'IEMOCAP-feature'
    target_path = 'IEMOCAP-result'
    if not os.path.exists(target_path):
        os.mkdir(target_path)
    speaker = 0

    for i in range(1, 6):
        cur_path = os.path.join(origin_path, 'Session{}'.format(i))

        features_M = []
        labels_M = []
        features_F = []
        labels_F = []

        for key in dic.keys():
            c_path = os.path.join(cur_path, key)
            for dir_path, dir_names, filenames in os.walk(c_path):
                for name in filenames:
                    data = np.load(os.path.join(dir_path, name))
                    if name[-8] == 'F':
                        for d in data:
                            features_F.append(d)
                            labels_F.append(dic[key])
                    else:
                        for d in data:
                            features_M.append(d)
                            labels_M.append(dic[key])

        speaker += 1
        save_path = os.path.join(target_path, 'speaker{}'.format(speaker))
        if not os.path.exists(save_path):
            os.mkdir(save_path)
        np.save(os.path.join(save_path, 'features.npy'), np.array(features_M))
        np.save(os.path.join(save_path, 'labels.npy'), np.array(labels_M))

        speaker += 1
        save_path = os.path.join(target_path, 'speaker{}'.format(speaker))
        if not os.path.exists(save_path):
            os.mkdir(save_path)
        np.save(os.path.join(save_path, 'features.npy'), np.array(features_F))
        np.save(os.path.join(save_path, 'labels.npy'), np.array(labels_F))

网络搭建与训练数据生成 2021/11/29

【复现日志】Clustering-Based Speech Emotion Recognition by Incorporating Learned Features and Deep BiLSTM
由上面的结构图可以看出,实际上网络只有两个部分,预训练的Resnet101和双向LSTM,而且经过查找资料发现ResNet的结构没有改变,可以直接调用预训练模型,最后一层的输出需要改变一下,改成softmax分类,类别数是数据库的情绪种类数。这里没有新建类直接调用的pytorch自带的模型进行训练,因为这两个模型没有自定义的成分。
这里比较坑的一点是文章好像没有给lstm的隐层数,我采用256作为隐藏层大小。
import torch
import torch.nn as nn
from torch.autograd import Variable

class LSTM_Model(nn.Module):
    """定义LSTM的模型部分,需要获取最后一步输出作为结果"""
    def __init__(self, input_size, output_size, hidden_size, use_cuda=True):
        super(LSTM_Model, self).__init__()
        self.input_size = input_size
        self.output_size = output_size
        self.hidden_size = hidden_size
        self.use_cuda = use_cuda

        self.bi_lstm = nn.LSTM(input_size, hidden_size, num_layers=1, batch_first=True, bidirectional=True)
        self.out_layer = nn.Linear(hidden_size * 2, output_size)
        self.softmax = nn.LogSoftmax()

    def forward(self, input, hidden):
        x, hidden = self.bi_lstm(input, hidden)
        x = self.out_layer(x[:, -1, :])
        x = self.softmax(x)
        return x, hidden

    def init_hidden(self, batch_size):
        """初始化LSTM隐藏状态,每个batch调用一次"""
        h0 = Variable(torch.zeros(2 * 1, batch_size, self.hidden_size))
        c0 = Variable(torch.zeros(2 * 1, batch_size, self.hidden_size))
        if self.use_cuda:
            h0.cuda()
            c0.cuda()
        return h0, c0

训练采用五折交叉验证,由于不同数据库中speaker的数目是不同的,需要自动设置训练和验证、测试集的大小。这里需要能够训练模块,分别是speaker independent和speaker dependent。
主循环是5个turns,为五轮交叉验证,其中每轮需要划分出训练数据、验证数据和测试数据,五轮结束后以每轮测试结果平均值作为最终准确率、平均召回率和F1分数。
每轮中每5个epoch(总epoch暂时设置为100)做一次验证集上验证并保留验证集loss最低模型作为最优模型,训练结束后使用最优模型在测试集上测试并获得准确率,每轮需要输出的内容有:

  • 验证EPOCH、验证集上的LOSS、UA\WA\召回率\F1分数(必要时加上最优模型存储提示)
  • 最优模型对应的EPOCH(完成)
  • 测试集平均UA、WA、召回率、F1分数
  • 每折训练集loss曲线和测试集混淆矩阵(最好不要在训练过程中显示,直接存储在文件中)

这里为了方便,我将SI和SD的训练过程数据存储在文件里,格式为”IEMOCAP-SI-train//fold1″以此类推,共5个文件夹,每一个文件夹里是训练、验证和测试数据,生成代码如下:

import torch
import numpy as np
import os

if __name__ == '__main__':
    """运行顺序5:Speaker Independent 训练数据生成,将五折测试、训练数据存储在文件中,节省训练时间"""
    root_path = 'IEMOCAP-result'
    target_path = 'IEMOCAP-SI-train'
    if not os.path.exists(target_path):
        os.mkdir(target_path)
    for t in range(5):
        start = t * 2
        test_ind = start
        valid_ind = start + 1

        test_x = np.load(os.path.join(root_path, 'speaker{}'.format(test_ind + 1), 'features.npy'))
        test_x = np.expand_dims(test_x, 1)
        test_y = np.load(os.path.join(root_path, 'speaker{}'.format(test_ind + 1), 'labels.npy'))

        valid_x = np.load(os.path.join(root_path, 'speaker{}'.format(valid_ind + 1), 'features.npy'))
        valid_x = np.expand_dims(valid_x, 1)
        valid_y = np.load(os.path.join(root_path, 'speaker{}'.format(valid_ind + 1), 'labels.npy'))

        train_x = []
        train_y = []
        for person in range(10):
            if person != test_ind or person != valid_ind:
                train_x.extend([i for i in np.load(os.path.join(root_path, 'speaker{}'.format(person + 1),
                                                                         'features.npy'))])
                train_y.extend([i for i in np.load(os.path.join(root_path, 'speaker{}'.format(person + 1),
                                                                'labels.npy'))])
        train_x = np.expand_dims(train_x, 1)

        new_path = os.path.join(target_path, 'fold{}'.format(t))
        if not os.path.exists(new_path):
            os.mkdir(new_path)
        np.save(os.path.join(new_path, 'train_x.npy'), np.array(train_x))
        np.save(os.path.join(new_path, 'train_y.npy'), np.array(train_y))
        np.save(os.path.join(new_path, 'test_x.npy'), test_x)
        np.save(os.path.join(new_path, 'test_y.npy'), test_y)
        np.save(os.path.join(new_path, 'valid_x.npy'), valid_x)
        np.save(os.path.join(new_path, 'valid_y.npy'), valid_y)

【复现日志】Clustering-Based Speech Emotion Recognition by Incorporating Learned Features and Deep BiLSTM
下一步是训练过程,可以直接从上面的文件夹调用数据,从而节省了加载数据的时间。
[En]

The next step is the training process, where the data can be called directly from the folder above, saving time for loading data.

训练与结果展示 2021/11/30

这里会遇到一个问题,就是预训练模型的输入是[64, 3, 7, 7]的,而我们的数据是[batch-size, 1, 257, 63]的,需要修改预训练模型第一层卷积的结构使得能够塞入数据得到正确的结果。
在检查过程中我遇到了一个严重的问题,loss不下降,经过检查数据和loss定义的部分都是正确的,那么问题可能出在模型上,我对文章的理解可能出现了错误,LSTM的输入在程序里是[batch, 1000, 1],也就是时间步长是1,每个时间步的特征长度是1,学不到东西。为了验证是不是模型出了问题,在训练步中我把第2和3维度进行交换,发现loss可以下降了,可以确定是lstm输入的部分出现了问题,但是时间步长为1的LSTM基本相当于全连接层,所以合理猜测,下面图应该是这个意思:

【复现日志】Clustering-Based Speech Emotion Recognition by Incorporating Learned Features and Deep BiLSTM
每个语音对应几个聚类后的segment,这些segement转换为spectrogram,那么训练数据的形状应该为(batch, seg_num, 257, 63),但是这样每个语段的segment大小就不一样了,没法输入到CNN,因为CNN要求的通道大小要一致。要么就是,一个一个塞到CNN中,得到结果之后再合在一起放入LSTM……

【我卡住了】……

Original: https://blog.csdn.net/cherreggy/article/details/121477363
Author: 你的宣妹
Title: 【复现日志】Clustering-Based Speech Emotion Recognition by Incorporating Learned Features and Deep BiLSTM

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/497898/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球