深層学習で簡単な話者識別をしてみる

#概要
　本記事では簡単な話者識別をしていきます．音声データは[日本声優統計学会](https://voice-statistics.github.io/)様からお借りしました．1秒間の音声ファイルをサンプリングして学習推論します．
（注意）モデルとしてgMLPを使っていますが，後日Conv1dのResNetで学習した方が学習速度・精度共に性能が良かったです．

#準備
　3人の声優さんの音声データをダウンロードします．通常/喜び/怒りの感情の音声データが公開されていますが，今回は感情を扱わないので声優さんごとに全て同じフォルダにまとめてしまいました．
(./wav_data/seiyu0/\*.wavや./wav_data/seiyu1/\*.wavのような構造)
　`python = "3.6.8"`
　`pytorch = "1.6.0"`
　`pip install librosa`
　`git clone https://github.com/jaketae/g-mlp.git`

#コード
　音声の前処理は[ディープラーニングで音声分類](https://qiita.com/cvusk/items/61cdbce80785eaf28349)より引用しています．引用部分は以下のホワイトノイズを加える処理と，メルスペクトログラムを取得する処理です．

```python:
def add_white_noise(x, rate=0.002):
    return x + rate*np.random.randn(len(x))

def calculate_melsp(x, n_fft=1024, hop_length=128):
    stft = np.abs(librosa.stft(x, n_fft=n_fft, hop_length=hop_length))**2
    log_stft = librosa.power_to_db(stft)
    melsp = librosa.feature.melspectrogram(S=log_stft,n_mels=128)
    return melsp
```
　音声開始時や終了時の無音領域カットの関数を用意します．


```python:
def cut_silence(x,eps=0.01):
    x_abs = np.abs(x)
    ind = np.where(x_abs>x_abs.max()*eps)[0]
    return x[ind[0]:ind[-1]]
```

- 声優さんの数だけあるディレクトリのパスを参照して，wavファイルから1分間のサンプリングと前処理を行い一つ一つnpy形式で保存していきます．
- 保存先に声優さんの数だけ数字を振ったディレクトリを作成しておきます．（今回は三人のため「0」「1」「2」のディレクトリを作っておきました）

```python:
import random
import librosa
import numpy as np

dirs = glob.glob("./wav_data/*")
for i,dir in enumerate(dirs):
    wavs = glob.glob(dir+"/*.wav")
    number = 0
    for wav in tqdm(wavs):
        x,fs = load_wave(wav)
        x = cut_silence(x)
        x = add_white_noise(x)
        sampling_len = fs #サンプリングの長さ(とりあえず1秒)
        sampling_num = int(len(x)/sampling_len)*3 #1ファイルから何個サンプリングするか(1秒データ->3個)

        if len(x)>sampling_len:
            for j in range(sampling_num):
                sample_start = random.choice(range(len(x)-sampling_len))
                sample_end = sample_start+sampling_len
                sample_x = x[sample_start:sample_end]
                sample_x_melsp = calculate_melsp(sample_x)
                np.save("./wav_dataset/"+str(i)+"/"+str(number), sample_x_melsp)
                number += 1
```
　学習関係のインポート

```python:
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader,Dataset
from sklearn.metrics import accuracy_score
```
　モデル([gMLP](https://arxiv.org/abs/2105.08050))を用意する．(npy保存したのが(128,345)の形だったため「次元，系列長」と見なして適当にハイパーパラメータを設定)

```python:
from g_mlp import gMLP
class gMLPForWavModeling(gMLP):
    def __init__(self, d_model=128, d_ffn=128, seq_len=345, num_layers=6, class_num=3):
        super().__init__(d_model, d_ffn, seq_len, num_layers)
        self.output = nn.Linear(d_model, class_num)

    def forward(self, x):
        out = self.model(x)
        out = self.output(out.mean(1))
        return out
```
　データセットを用意する．(戻り値1つめは系列長，次元の順番になるように転置している)

```python:
class VoiceDataset(Dataset):
    def __init__(self):
        class_num = 3
        class_paths = {}
        for i in range(class_num):
            data = glob.glob("./wav_dataset/"+str(i)+"/*.npy")
            class_paths[i]=data
        
        id = 0
        self.id_class = {}
        self.id_path = {}
        for i in class_paths:
            for path in class_paths[i]:
                self.id_class[id]=i
                self.id_path[id]=path
                id+=1

    def __getitem__(self, idx):
        return torch.tensor(np.load(self.id_path[idx]).T).float(),self.id_class[idx]

    def __len__(self):
        return len(self.id_class)
```
　データセットを分割しておきます．

```python:
dataset = VoiceDataset()

length = len(dataset)
train_length = int(length*0.9)
val_length = length - train_length

train,val = torch.utils.data.random_split(dataset,[train_length,val_length])
trainloader = DataLoader(train,batch_size=128,shuffle=True)
valloader = DataLoader(val,batch_size=64,shuffle=False)
```

　学習ループを書きます．

```python:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = gMLPForWavModeling().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.00001)

epoch = 20
for i in range(epoch):
    train_loss = 0
    for batch in trainloader:
        data,label = batch
        data = data.float().to(device)
        label = label.long().to(device)
        
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output,label)
        running_loss = loss.item()
        loss.backward()
        optimizer.step()

        train_loss += running_loss
    print("train_loss",train_loss/len(trainloader))

    val_loss = 0
    labels = []
    preds = []
    for batch in valloader:
        data,label = batch
        data = data.float().to(device)
        label = label.long().to(device)
        
        with torch.no_grad():
            output = model(data)
        labels += label.cpu().tolist()
        preds += output.argmax(-1).cpu().tolist()

        loss = criterion(output,label)
        running_loss = loss.item()

        val_loss += running_loss
    print("val_loss",val_loss/len(valloader))
    print("val_acc",accuracy_score(labels,preds))
```
#結果
　Val-Accuracy: 98.4% (gMLP)　->　99.5% (ResNet1D)

#備考
　ファイル構成のメモ
　　./wav_data/seiyu0/\*.wav
　　./wav_data/seiyu1/\*.wav
　　./wav_data/seiyu2/\*.wav
　　./wav_dataset/0/\*.npy
　　./wav_dataset/1/\*.npy
　　./wav_dataset/2/\*.npy
　　./g-mlp/\_\_init\_\_.py
　など

　ResNet1Dはhttps://github.com/fanzhenya/ResNet1D-VariableLengthPooling-For-TimeSeries.git を参考にさせて頂きました．(本記事での実装は省略)

#まとめ
- gMLPを使って簡単な話者識別をしました．
- 1秒音声をサンプリングしてから評価データをスプリットして作成していますが，本来は元音声の時点でスプリットした方が正確な精度が測れると思います．
- 備忘録程度の記事です．

#最後に
　誤っている部分等ございましたら，コメント等で優しく指摘して頂けると嬉しいです．（気付かなかったら申し訳ありません）