SpeechBrainで音声の感情分析をしてみる

SpeechBrainで音声の感情認識ができるようなので試してみました。

# SpeechBrainでの感情認識のサンプル
SpeechBrainには各種タスクを実行するためのレシピが多数用意されています。今回は音声の感情認識を行いたいので以下のレシピを使います。IEMOCAPというデータセットを使って感情認識を行うレシピです。
```
speechbrain/recipes/IEMOCAP/emotion_recognition
```

今回はtrain_with_wav2vec2.pyのほうを使っていますが、train.pyでも同様に動くと思います。

# データセットが入手できない
IEMOCAPは音声と表情のラベルがセットになったデータセットなのですが、こちらのダウンロードには審査があるようで、大学以外の人がダウンロードするのは難しそうです。今回は諦めて他のデータセットを使うことにしました。

# 利用するデータセット
今回はこちらのデータセットを使います。
The Ryerson Audio-Visual Database of Emotional Speech and Song (RAVDESS) 
https://zenodo.org/record/1188976#.Y1493HbP0Q8
Livingstone, Steven R.; Russo, Frank A.  (CC BY-NC-SA 4.0)
ライセンスがCC BY-NC-SA 4.0なので注意してください。

こちらのデータセットは8種類の感情がラベル付けされています。wavのファイル名の'-'で区切られた３番目の数値が感情のラベルです。


今回は以下のフォルダに展開しています。
```
cd speechbrain/recipes/IEMOCAP/dataset_test
curl -O 'https://zenodo.org/record/1188976/files/Audio_Speech_Actors_01-24.zip'
unzip Audio_Speech_Actors_01-24.zip
```

展開するとActor_01, Actor_02 と話者ごとにフォルダ分けされたwavファイルが入っています。

# hparams/train_with_wav2vec2.yamlを修正
データフォルダを修正します。今回はglob.glob()を使うので以下の形式のパスにしています。
out_n_neuronsは感情ラベルの数です。元は４ですがデータセットに合わせて８に変更しました。
```
...
data_folder: ../dataset_test/**/*.wav
...
out_n_neurons: 8
```

# iemocap_prepare.pyを修正
以下の部分を修正します。

```
# 追加
import glob

...

# 音声数の変更
NUMBER_UTT = 1440

...

def prepare_data(
    data_original,
    save_json_train,
    save_json_valid,
    save_json_test,
    split_ratio=[80, 10, 10],
    different_speakers=False,
    test_spk_id=1,
    seed=12,
):
    ...

    #ここはコメントアウト
    #data_original = data_original + "/Session"
```


```
def transform_data(path_loadSession):
    ...

    # 話者の数が変わるので10 -> 24に変更    
    #speaker_dict = {str(i + 1): [] for i in range(10)}
    speaker_dict = {str(i + 1): [] for i in range(24)}
    
    # ここを今回のデータセットに合わせて変更
    '''
    speaker_count = 0
    for k in range(5):
        session = load_session("%s%s" % (path_loadSession, k + 1))
        for idx in range(len(session)):
            if session[idx][2] == "F":
                speaker_dict[str(speaker_count + 1)].append(session[idx])
            else:
                speaker_dict[str(speaker_count + 2)].append(session[idx])
        speaker_count += 2

    return speaker_dict
    '''
    
    # path_loadSessionはtrain.yaml(train_with_wav2vec2.yaml)のdata_folderのパスが来る
    # ../dataset_test/**/*.wav のようにAudio_Speech_Actors_01-24.zipを展開した先をしてする
    wav_file_list = glob.glob(path_loadSession)
    print('path_loadSession:', path_loadSession)
    print('wav_file_list:', wav_file_list[:5])
    
    for wav_file in wav_file_list:
        dirname = os.path.dirname(wav_file)
        dirname = dirname.split('/')[-1]
        
        print('dirname:', dirname)
        
        basename = os.path.basename(wav_file)

        # フォルダ名のActor_10の10を取り出してspeaker名にする
        speaker = str(int(dirname.replace('Actor_', '')))
        # emotionは'-'区切りで３つ目の要素
        # このデータセットでは８種類ある
        emotion = basename.split('-')[2]
        # utteranceはとりあえず空で
        utterance = ''
        speaker_dict[speaker].append([wav_file, emotion, utterance])
        
    print('speaker_dict:', speaker_dict)
    
    return speaker_dict
```

# train実行

以下のコマンドで学習を実行すると途中で止まりました。wavファイルを読み込んだときにShapeの形が変わってしまうことがあるようです。

```
python train_with_wav2vec2.py hparams/train_with_wav2vec2.yaml
```

## 原因は一部のファイルのチャンネル数が２になっていた
こちらの記事を参考にしました。チャンネル数が１より大きい場合はmeanを使って平均を取るとよさそうです。
https://stackoverflow.com/questions/67508634/speechbrain-dataio-prepare-function-with-csv

train_with_wav2vec2.pyを修正します。
``` 
# この２つを追加
import torchaudio
import torch

def dataio_prep(hparams):
    ...
    # Define audio pipeline
    @sb.utils.data_pipeline.takes("wav")
    @sb.utils.data_pipeline.provides("sig")
    def audio_pipeline(wav):
        """Load the signal, and pass it and its length to the corruption class.
        This is done on the CPU in the `collate_fn`."""
        sig = sb.dataio.dataio.read_audio(wav)
        
        # ２チャンネルの場合は平均をとって１チャンネルに変換
        info = torchaudio.info(wav)
        if info.num_channels > 1:
            sig = torch.mean(sig, dim=1)
        #print('sig shape:', sig.shape)            
        
        return sig
```

# 学習結果
最後まで学習が進みました。test lossとerror_rateが小さくなっているので学習は進んだようです。
```
speechbrain.utils.train_logger - Epoch: 30, lr: 3.14e-05, wave2vec_lr: 3.14e-06 - train loss: 5.87e-03 - valid loss: 1.37, valid error_rate: 1.60e-01
speechbrain.utils.checkpoints - Saved an end-of-epoch checkpoint in results/train_with_wav2vec2/1993/save/CKPT+2022-10-29+23-48-26+00
speechbrain.utils.checkpoints - Deleted checkpoint in results/train_with_wav2vec2/1993/save/CKPT+2022-10-29+23-47-33+00
speechbrain.utils.checkpoints - Loading a checkpoint from results/train_with_wav2vec2/1993/save/CKPT+2022-10-29+23-41-19+00
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 36/36 [00:01<00:00, 18.11it/s]
speechbrain.utils.train_logger - Epoch loaded: 22 - test loss: 8.23e-01, test error_rate: 1.25e-01
```