機械学習
記事内に商品プロモーションを含む場合があります

音/音声データを画像用DNN(CNN)で学習・推論する方法を解説

tadanori

この記事では、音データ(音声データ)をメルスペクトログラム(Mel Spectrogram)に変換して、画像用のディープニューラルネットワークで学習・推論する手順を解説します。

はじめに

この記事では、音声データをディープニューラルネットワーク(DNN)で学習・推論する方法について解説します。音声データをスペクトログラムに変換すれば、画像として扱うことができ、画像用のモデルが利用できます。

この記事では、感情推定用の音声データセットを利用して、感情推定を行うDNNのモデルを作成し学習してみます。

実際には、モデルはTIMMの既存の画像用のモデルを利用するので新たなモデルを作るわけではありません

音声データを学習させる際の参考になればと思います。

データセットのダウンロード

RAVDESS

ここでは、RAVDESS Emotional speech audioというデータセットを利用します。

RAVDESSは、プロの俳優を用いて記録されたデータセットで、ニュートラル、穏やか、幸せ、悲しみ、怒り、恐怖、驚き、嫌悪の8種類の表現が含まれたデータセットです。

ここでは、このデータセットの音声データを利用します。

ダウンロードはcurlで行うことができます。

curl https://zenodo.org/records/1188976/files/Audio_Speech_Actors_01-24.zip?download=1 -o Audio_Speech_Actors_01-24.zip

ダウンロードが完了したら、dataというフォルダを作成し、unzipで、そこに解凍します。

mkdir data
unzip -q Audio_Speech_Actors_01-24.zip -d data

解凍すると、Actor_xxというフォルダが作成され中に*.wavファイルが格納されます。wavファイルのファイル名は以下の命名規則に沿って付けられているので、これを利用して感情ラベルをつけます。

音声ファイルは03-01-01-01-01-01-01.wavのようなファイル名になっており、それぞれの数字は以下のような意味になります。

  • Modality (01 = full-AV, 02 = video-only, 03 = audio-only).
  • Vocal channel (01 = speech, 02 = song).
  • Emotion (01 = neutral, 02 = calm, 03 = happy, 04 = sad, 05 = angry, 06 = fearful, 07 = disgust, 08 = surprised).
  • Emotional intensity (01 = normal, 02 = strong). NOTE: There is no strong intensity for the ‘neutral’ emotion.
  • Statement (01 = “Kids are talking by the door”, 02 = “Dogs are sitting by the door”).
  • Repetition (01 = 1st repetition, 02 = 2nd repetition).
  • Actor (01 to 24. Odd numbered actors are male, even numbered actors are female).

今回は、感情をラベルとして利用しますので、前から3番目のEmotionをラベルとして抜き出しています(後ろからみると5番目)。

ライブラリのインポート

必要なライブラリをインポートします。

今回はtorchaudiolibrosaを利用します。

import glob
import torch, torchvision, torchaudio
import torch.nn as nn
import torch.nn.functional as F
import librosa
import random
import os
import matplotlib.pyplot as plt
import multiprocessing
import timm
from tqdm.notebook import tqdm

データセットの自作

音声データを読み込むデータセットは以下のようになります。

詳しくは、以下の記事を参照してください

あわせて読みたい
Mel Spectrogram|音声データを画像用のDNNに入力する方法
Mel Spectrogram|音声データを画像用のDNNに入力する方法

なお、y = librosa.util.normalize(y)として音量の正規化を行なっています。

また、ラベルは、label = int(filename.split("-")[-5])-1として取り出しています。これで0から7のラベルが付けられます。

感情ラベルは0から7としています。元は1~8なので注意してください

class AudioDataset(torch.utils.data.Dataset) :
  def __init__(self, path, mode, sr=16000, hop_length=512, sec=2) :
    self.files = glob.glob(os.path.join(path,"*/*"))
    self.mel = torchaudio.transforms.MelSpectrogram(
        sample_rate=sr, hop_length=512, n_fft=1024, f_max=sr/2, power=1) # 1 for magnitude, 2 for power
    self.sr = sr
    self.hop_lenght = hop_length
    self.sec = sec

    # データ拡張用の変換列などを定義しておく

    if mode == "train" :
      self.mode = 1
    elif mode == "eval" :
      self.mode = 2
    else:
      self.mode = 0
  def __len__(self) :
    return len(self.files)
  def __getitem__(self, idx) :
    filename = self.files[idx]
    y, sr = librosa.load(filename, sr=self.sr)

    yt, _ = librosa.effects.trim(y)

    # trimして不足したら先頭yにする
    if len(yt) > self.sr*self.sec : y = yt

    # 正規化する(正規化した方が今回のデータでは良さそう)
    y = librosa.util.normalize(y) 

    if self.mode == 1 or self.mode == 2 :
      start = random.randint(0, len(y)-self.sr*self.sec)
    else: 
      start = 0

    y_sel = y[start:start+self.sr*self.sec]

    label = None
    if self.mode == 1 or self.mode == 2:
      # ここで、ノイズ付加などのデータ拡張を行う
      label = int(filename.split("-")[-5])-1

    mel_spec = self.mel(torch.tensor(y_sel))

    return mel_spec.unsqueeze(0), label

最後にunsqueeze(0)をして、高さx幅のデータをデータをch(1)x高さx幅のデータに変換しています。

今回は行っていませんが、データ拡張をすることでロバスト性を向上できます。音声データのデータ拡張についてはこちらを参考にしてください。

Audiomentations|音声データ向けデータ拡張ライブラリを解説
Audiomentations|音声データ向けデータ拡張ライブラリを解説

パラメータ設定など

ハイパーパラメータなどの各種パラメータを設定し、データセット、データローダーを生成しています。

EPOCHS = 20
BATCH_SIZE = 8
NUM_CLASSES = 8
cpus = multiprocessing.cpu_count()

device = "cuda" if torch.cuda.is_available() else "cpu"
print(device)

dataset = AudioDataset('data', mode="train", sec=2)
train_dataloader = torch.utils.data.DataLoader(dataset, batch_size = BATCH_SIZE, shuffle=True, num_workers=cpus)

ここで、とりあえずデータローダーの動作を確認してみます。以下のコードで最初のバッチ分を読み出して、画像に変換された音声データを表示しています。

fig, ax = plt.subplots(2, 4, figsize=(10, 8))
for inputs, labels in train_dataloader:
  for n, x in enumerate(inputs) :
    i = n//4
    j = n%4
    ax[i][j].imshow(x[0])
    ax[i][j].set_title(labels[n])
  break
plt.show()
メルスペクトログラム

モデル作成

モデルを生成します。今回はTIMMのefficientnet_b0を利用しています。

入力が1chなのでチャネルを1に、8分類なので8クラスを設定しています。

また、クラス分類なので損失関数にはCrossEntropyLossを指定しました。

model = timm.create_model("efficientnet_b0", pretrained=True, in_chans=1, num_classes = NUM_CLASSES).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.005)

学習

以下、学習ループです。基本は画像の学習と同じです。

model.train()

losses = []
acc = []

for epoch in range(EPOCHS) :
  train_loss, train_acc = 0.0, 0.0
  with tqdm(train_dataloader) as pbar:
    pbar.set_description(f'[Epoch {epoch + 1}/{EPOCHS}]')
    for inputs, labels in pbar:
      optimizer.zero_grad()
      inputs = inputs.to(device)
      labels = labels.to(device)
      outputs = model(inputs)
      loss = criterion(outputs, labels)
      loss.backward()
      optimizer.step()
      train_loss += loss.detach().item()

      preds = outputs.argmax(axis = 1)
      train_acc += torch.sum(preds == labels).item() / len(labels)

      pbar.set_postfix({'loss': loss.item(), "acc":torch.sum(preds == labels).item() / len(labels)})

  train_loss /= len(train_dataloader)
  train_acc /=  len(train_dataloader)
  losses.append(train_loss)
  acc.append(train_acc)
  print(f"epoch {epoch+1}: loss = {train_loss:.4f} acc={train_acc:.4f}")

今回はエポック数を20にして学習させました。GPUを使って1エポック29秒程度でした。

学習ログ

Lossをプロットしてみます。ロスをみると、まだ下がりそうな雰囲気があります。

plt.plot(losses, 'ro--')
plt.show()
LOSS

精度もプロットしてみました。こちらもまだ上がりそうです。

plt.plot(acc, 'ro--')
plt.show()
ACCURACY

推論

推論を試してみます。本来は学習したデータとは別のデータを用意した方がよいのですが、今回は学習したデータと同じデータを使っています

test_dataset = AudioDataset('data', mode="eval", sec=2)
test_dataloader = torch.utils.data.DataLoader(test_dataset, batch_size = BATCH_SIZE, shuffle=False, num_workers=cpus)

gt = []
pred = []
model.eval()
with tqdm(train_dataloader) as pbar:
  pbar.set_description(f'[Epoch {epoch + 1}/{EPOCHS}]')
  for inputs, labels in pbar:
    inputs = inputs.to(device)
    labels = labels.to(device)
    with torch.no_grad() :
      outputs = model(inputs)
    p = outputs.argmax(axis = 1)
    # print(p, labels)
    pbar.set_postfix({"acc":torch.sum(p == labels).item() / len(labels)})
    pred += p.tolist()
    gt += labels.tolist()

print("acc = ", sum([p == g for p, g in zip(pred, gt)]) / len(pred))

精度は89%になりました。

acc =  0.8909722222222223

混同行列も求めてみます。

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay

cm = confusion_matrix(pred, gt)
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=[i for i in range(8)])
disp.plot()

混同行列は以下のようになりました。どの感情も概ね正解しているようです。

confusion matrix

まとめ

音声データをディープニューラルネットワーク(DNN)で学習させる方法について解説しました。このように、メルスペクトログラムに変換することで、音声データを画像として学習できるようなり、画像用のネットワークを利用できます。

データ拡張などは画像と異なりますが、画像にしてしまえば画像と同じ手法が使えます。ちなみに、音声データだけでなく、時系列のモーションデータなども画像に変換して扱うことができます。


おすすめ書籍

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

記事URLをコピーしました