PyTorchを使って音×Deep Learningに入門してみた

# はじめに

機械学習やらDeepLearningで音をどうやって扱えばいいのか全く知らなかったので、いろいろ勉強してみました。それらの内容を簡単に書き留めておこうと思います。
いつも通りモチベーションはまずは実装して動かしてみる、としてますので、細かな理論的背景には触れません。そのへんはご了承ください。


音 $\times$ DeepLearningはいろんな方がいろんなベクトルで詳しく解説してくださっているので、勉強する上で非常に助かりました。以下、特に以下の記事を見ながら勉強させていただきました。

- [深層学習を使って楽曲のアーティスト分類をやってみた！] (https://blog.brainpad.co.jp/entry/2018/04/17/143000)
- [ディープラーニングで音声分類](https://qiita.com/cvusk/items/61cdbce80785eaf28349)  
- [音楽と機械学習 前処理編 MFCC ~ メル周波数ケプストラム係数](https://qiita.com/martin-d28jp-love/items/34161f2facb80edd999f)  
- [音声分類を色々なモデルや特徴量でやってみた](https://qiita.com/kshina76/items/5686923dee2889beba7c)
- [機械学習のための音声の特徴量ざっくりメモ (Librosa ,numpy)](https://qiita.com/yutalfa/items/dbd172138db60d461a56)
- [音声特徴量（ケプストラム、メル周波数ケプストラム係数）の理解に役に立つ、声の生成の仕組み](https://qiita.com/Anharu/items/46987e85b68c5d7bab6a)
- [【参加録:🐤🐸】Rainforest Connection Species Audio Detection](https://www.nogawanogawa.com/entry/rfcx)
- [【まとめ】ディープラーニングによる環境音の認識](https://qiita.com/shinmura0/items/6befb83f7cde7b091905)
- [畳込みニューラルネットワークの基本技術を比較する　ー音でもやってみたー](https://qiita.com/shinmura0/items/858214154f889c05e4f4)
- [Audio Classification using Librosa and Pytorch](https://medium.com/@hasithsura/audio-classification-d37a82d6715)  
- [Getting to Know the Mel Spectrogram](https://towardsdatascience.com/getting-to-know-the-mel-spectrogram-31bca3e2d9d0)  

# 扱うデータ
みなさんも使ってる環境音データセットを使います。

- https://github.com/karolpiczak/ESC-50

動物の鳴き声やら雨の音やら車のエンジン音などさまざまな音源が50のカテゴリでまとめられたデータセットです。
音を機械学習で扱おうとする際に実験的によく使われるデータセットだと思われます。
Google Colab上で動かしたいので、とりあえずこのGitリポジトリをGoogle Drive上などにcloneしておきます。

Gitリポジトリの`meta/esc50.csv`というファイルにデータセットのメタ情報（ファイル名やカテゴリーなどが紐づいた情報）が格納されているので、そちらでどんなデータセットなのか確認します。データ件数は2000件のようです。

```python
# Google Driveをcolabにマウント
from google.colab import drive
drive.mount('/content/drive')

import pandas as pd

# Google Drive上に格納した環境音データセットのディレクトリを指定しておく
drive_dir = "drive/My Drive/Colab Notebooks/ESC-50-master/"

# メタファイル（ファイル名にカテゴリーなどが紐づいた情報）がcsvファイルとして格納されているので、DataFrameで持っておく
meta_df = pd.read_csv(drive_dir + "meta/esc50.csv")

# メタファイルを確認する
display(meta_df.head())

# 全カテゴリーを取得して確認する
categories = meta_df['category'].unique()
print("データセットの数", meta_df.shape[0])
print(len(categories), categories[:10]) # いくつかカテゴリーを表示してみる
# データセットの数 2000
# 50 ['dog' 'chirping_birds' 'vacuum_cleaner' 'thunderstorm' 'door_wood_knock'
 'can_opening' 'crow' 'clapping' 'fireworks' 'chainsaw']
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240999/4e9fa57a-d127-931c-d19f-55baf79e332c.png)


後ほど使うライブラリも含めて諸々インポートしておきます。

```python
import os
import pandas as pd
from tqdm import tqdm
import librosa
import librosa.display # インポートしないでlibrosa.display(〜〜)で実行しようとするとエラーになりました
import matplotlib.pyplot as plt
import IPython.display as ipd
import seaborn as sns
import numpy as np
from sklearn.model_selection import train_test_split
import torch
from torch.utils.data import Dataset, DataLoader
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
```


# librosaで音源データを読み込んで見る

音源データを読み込み、波形を確認したり、モデルに投入する各種特徴量（メルスペクトログラムやMFCCなど）を簡単に取得できるライブラリに`librosa`というものがあります。めちゃ便利。
これを使って、環境音の波形や特徴量を取得して可視化してみます。
メルスペクトログラムやMFCCについては参考文献をご参照ください。

```python

# とりあえず1つの音源ファイルを指定してlibrosaを使ってみる
#librosa.loadで音源の波形データ（第1戻り値）とサンプルレート（第2戻り値）を取得できます。
waveform, sample_rate = librosa.load(drive_dir + "audio/1-100032-A-0.wav") # 犬の鳴き声の音源データを指定

# メルスペクトログラムの取得
# librosa.feature.melspectrogramに上で取得した波形データとサンプルレートを渡せば一発でメルスペクトログラムを取得できます。
feature_melspec = librosa.feature.melspectrogram(y=waveform, sr=sample_rate)

# MFCCの取得
# librosa.feature.mfccでOK
feature_mfcc = librosa.feature.mfcc(y=waveform, sr=sample_rate)

# 波形データ、メルスペクトログラム、MFCCはnumpyで取得されます。
print("波形データのデータタイプ", type(waveform))
print("メルスペクトログラムのデータタイプ", type(feature_melspec))
print("MFCCのデータタイプ", type(feature_mfcc))
print("サンプルレート", sample_rate)
print("波形データの形状", waveform.shape)
print("メルスペクトログラムの形状", feature_melspec.shape)
print("MFCCの形状", feature_mfcc.shape)

# ランダムに3つほど音源データをピックアップして、波形、メルスペクトログラム、MFCCをそれぞれ可視化してみます。
# ついでにnotebook上で音源の再生ができるようにもします。
for row in meta_df.sample(frac=1)[['filename', 'category']][:3].iterrows():
    filename = row[1][0] # wavファイル名
    category = row[1][1] # そのファイルのカテゴリ

    # 波形データとサンプルレートを取得
    waveform, sample_rate = librosa.load(drive_dir + "audio/" + filename)

    # メルスペクトログラムを取得
    feature_melspec = librosa.feature.melspectrogram(y=waveform, sr=sample_rate)

    # MFCCを取得
    feature_mfcc = librosa.feature.mfcc(y=waveform, sr=sample_rate)

    # 可視化してみる
    print("category : " + category)
    plt.figure(figsize=(20, 5))

    # librosa.display.waveplotで波形データを可視化できます
    plt.subplot(1,3,1)
    plt.title("wave form")
    librosa.display.waveplot(waveform, sr=sample_rate, color='blue')

    # librosa.display.specshowでメルスペクトログラム、MFCCを可視化できます
    plt.subplot(1,3,2)
    plt.title("mel spectrogram")
    librosa.display.specshow(feature_melspec, sr=sample_rate, x_axis='time', y_axis='hz')
    plt.colorbar()

    plt.subplot(1,3,3)
    plt.title("MFCC")
    librosa.display.specshow(feature_mfcc, sr=sample_rate, x_axis='time')
    plt.colorbar()

    plt.tight_layout()
    plt.show()
    print()

    # 音源の再生はlibrosaで取得できた波形データとサンプルレートをIPython.display.Audioに以下のようにして渡します。
    display(ipd.Audio(waveform, rate=sample_rate))
```

category : drinking_sipping

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240999/3d0e4700-9b21-cf11-b30c-795de9583413.png)

category : snoring

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240999/a71ecb2b-1c5b-8720-d72a-90f1527b1bca.png)

category : pouring_water

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240999/c88e0584-e72d-ec4b-0815-8e3f3d0d07f1.png)


notebookにこんな感じで音源データを再生できるようになっているかと思います。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240999/235c9353-6e6e-f664-320c-651a7df1d82f.png" width=50%>

上で見るようにメルスペクトログラムやMFCCはある種チャネル数1の画像データとみなすことができると思います。
そして、それらを画像とみなした時、隣接するピクセルは密接な関係性があるはずであり、離れたピクセル同士は関係性が薄そうとみなせそう、つまり、CNNが使えそう、ということになるようです。

## メルスペクトログラムのデシベルスケール変換について



今回は特徴量としてメルスペクトログラムを用います。上で求めたメルスペクトログラムではうまくCNNで分類できませんでした。理由としては上のメルスペクトログラムの画像の黒いところは０付近の値が入ってて、こんなに0付近ばかりだとうまく畳み込めないんだと思います。濃淡の値のスケールがバラバラな感じになってしまっています。

そもそもメルスペクトログラムの画像の見方ですが、横軸は時間、縦軸はHz（周波数）になってて、色の濃淡は特定の時間、Hzにおける振幅（つまり音圧）を表しているのですが、人間が知覚する音圧はデシベルスケール（常用対数の10倍）のようで、上で求めたメルスペクトログラムの色の濃淡は人間的にはスケーリングが広すぎるので、デシベルスケールに変換（凝縮）することで、より人間の感覚に近いものになるのかと。

濃淡をデシベルスケールにすればメルスペクトログラムを画像と見立てた際も、より濃淡がはっきりした画像になり、CNNでうまく特徴量変換できそうな気がします。

`librosa`でデシベルスケールに変換するには、上で取得したメルスペクトログラムに`librosa.power_to_db`を施せばOKです。

参考記事をいくつか拝見してても、基本`librosa.feature.melspectrogram`でメルスペクトログラムを抽出したあと、`librosa.power_to_db`でデシベルスケールに変換したものをメルスペクトログラムと紹介されているのはこういうことなのかと。スペクトログラムの縦軸（Hz）を人間の知覚に合わせてメルスケール（log）で変換して、さらに音圧もデシベルスケール（log）に変換、と対数変換を2回行っているイメージですかね。（この辺の理解に苦戦しました。間違っていたらご指摘ください。）


ってことで、メルスペクトログラムの音圧をデシベルスケールに変換したものの可視化結果は以下のようになります。よりCNNが機能しそうな画像に確かになってますね。

```python
for row in meta_df.sample(frac=1)[['filename', 'category']][:3].iterrows():
    filename = row[1][0] # wavファイル名
    category = row[1][1] # そのファイルのカテゴリ

    waveform, sample_rate = librosa.load(drive_dir + "audio/" + filename)

    # メルスペクトログラムを求める
    feature_melspec = librosa.feature.melspectrogram(y=waveform, sr=sample_rate)

    print("category : " + category)
    plt.figure(figsize=(15,5))

    # librosa.feature.melspectrogramをそのまま可視化した場合
    plt.subplot(1,2,1)
    plt.title("mel spectrogram")
    librosa.display.specshow(feature_melspec, sr=sample_rate, x_axis='time', y_axis='hz')
    plt.colorbar()

    # デシベルスケールに変換した場合
    plt.subplot(1,2,2)
    plt.title("db scale mel spectrogram")
    feature_melspec_db = librosa.power_to_db(feature_melspec, ref=np.max)
    librosa.display.specshow(feature_melspec_db, sr=sample_rate, x_axis='time', y_axis='hz')
    plt.colorbar(format='%+2.0f dB')

    plt.tight_layout()
    plt.show()
```

category : crickets

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240999/7eab9f45-5b90-7bf4-1d07-10f4d8a453a3.png)

category : hen

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240999/5db6ced4-99a7-6dec-b6b6-df12810c1dd6.png)

category : sneezing

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240999/3135339f-35c7-a921-65ac-8c0ff36bb4b8.png)

# CNNで分類してみる

[こちらの記事](https://medium.com/@hasithsura/audio-classification-d37a82d6715)をとくに参考にしながら、CNNで環境音の分類に挑戦してみます。

特徴量は音圧をデシベルスケールに変換したメルスペクトログラムを使用します。

[こちらの記事](https://qiita.com/cvusk/items/61cdbce80785eaf28349)  のように自前で畳み込みを用意しても良いのですが、初学者にはややハードルが高いので、無難に学習済みモデルとしてResNetを使うこととします。

## DataLoaderを作成

```python
# メルスペクトログラムを画像に変換する
class ESC50Data(Dataset):
    def __init__(self, base_path, df, in_col, out_col):
        self.df = df
        self.data = [] # 音源データをメルスペクトログラム（画像）に変換して格納する用
        self.labels = [] # 各データのカテゴリー情報を格納する
        self.category2id={}
        self.id2category={}
        self.categories = list(sorted(df[out_col].unique())) # 正解ラベル格納用（５０ラベル）
        # ラベルをIDに変換する辞書を作成
        for i, category in enumerate(self.categories):
            self.category2id[category] = i
            self.id2category[i] = category

        # メタ情報ファイルからファイル名を取得し、wavデータを1件ずつ画像に変換していく
        for row in tqdm(range(len(df))):
            row = df.iloc[row]
            file_path = os.path.join(base_path, row[in_col])
            waveform, sr = librosa.load(file_path)

            # メルスペクトログラムを取得してデシベルスケールに変換
            feature_melspec = librosa.feature.melspectrogram(y=waveform, sr=sr)
            feature_melspec_db = librosa.power_to_db(feature_melspec, ref=np.max)

            self.data.append(feature_melspec_db)
            self.labels.append(self.category2id[row['category']])

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        return self.data[idx], self.labels[idx]

# メタ情報ファイルを学習データとテストデータに分ける
train_df, test_df = train_test_split(meta_df, train_size=0.8)

# メタ情報ファイルから音源データをDataLoaderに変換する
# こちら処理にそこそこ時間がかかります。
train_data = ESC50Data(drive_dir + "audio/", train_df, 'filename', 'category')
test_data = ESC50Data(drive_dir + "audio/", test_df, 'filename', 'category')
train_loader = DataLoader(train_data, batch_size=32, shuffle=True)
test_loader = DataLoader(test_data, batch_size=32, shuffle=True)

# colabのセッション切れなどで上の処理を何度もしなくていいように念の為DataLoaderをバイナリで保存しとく用
#import pickle
#with open(drive_dir + "melspec_train_loader", 'wb') as w:
#    pickle.dump(train_loader, w)
#with open(drive_dir + "melspec_test_loader", 'wb') as w:
#    pickle.dump(test_loader, w)
```


## ResNetの読み込みとネットワークの一部を変更する
ResNetは色付きの画像（チャネル数3）のものに対して学習されているので、一番最初の畳み込み層のチャネル数を1に変えてあげる必要があります。あとは最後の出力をカテゴリ数に合わせるために1000から50に変換しました。


```python
from torchvision.models import resnet34

# 学習済みのResNetをダウンロード
resnet_model = resnet34(pretrained=True)

# ResNetの構造がわからない場合はResNetの構造を出力して確認しましょう。
# print(resnet_model)
# 最初の畳み込みのチャネル3をチャネル1に変更する
resnet_model.conv1 = nn.Conv2d(1, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)

# 最後の層の次元を今回のカテゴリ数に変更する
resnet_model.fc = nn.Linear(512,50)

# GPU使います。
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
resnet_model = resnet_model.to(device)

loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(resnet_model.parameters(), lr=2e-4)
```

## 学習と精度確認
参考記事と同様にepoch数は50で学習してみます。すごく単純ですが、以下のように実装してみました。
参考記事と同様にだいたい80%ちょいくらいのaccuracyになりました。このデータにおける人間の精度が81.3%らしいので、とりあえずは良い結果なのではないでしょうか。

```python

losses = []
for epoch in range(50):

    # 学習
    train_losses = 0
    
    for data in train_loader:
        optimizer.zero_grad()
        x, y = data
        x = x.to(device, dtype=torch.float32)
        y = y.to(device)
        x = x.unsqueeze(1) # チャネル数1を挿入
        out = resnet_model(x)
        loss = loss_function(out, y)
        loss.backward()
        optimizer.step()
        train_losses += loss.item()
    
    # 検証
    test_losses = 0
    actual_list, predict_list = [], []

    for data in test_loader:
        with torch.no_grad():
            x, y = data
            x = x.to(device, dtype=torch.float32)
            y = y.to(device)
            x = x.unsqueeze(1)
            out = resnet_model(x)
            loss = loss_function(out, y)
            _, y_pred = torch.max(out, 1)
            test_losses += loss.item()

            actual_list.append(y.cpu().numpy())
            predict_list.append(y_pred.cpu().numpy())

    actual_list = np.concatenate(actual_list)
    predict_list = np.concatenate(predict_list)
    accuracy = np.mean(actual_list == predict_list)

    # epoch毎の精度確認
    print("epoch", epoch, "\t train_loss", train_losses, "\t test_loss", test_losses, "\t accuracy", accuracy)
# epoch 0 	 train_loss 126.61780571937561 	 test_loss 21.01327872276306 	 accuracy 0.575
# epoch 1 	 train_loss 51.59553635120392 	 test_loss 16.93894588947296 	 accuracy 0.6475
# epoch 2 	 train_loss 27.610755309462547 	 test_loss 13.882235825061798 	 accuracy 0.71
# epoch 3 	 train_loss 16.399369724094868 	 test_loss 14.29767906665802 	 accuracy 0.7175
# 〜省略〜
# epoch 45 	 train_loss 0.028462730842875317 	 test_loss 11.345554560422897 	 accuracy 0.81
# epoch 46 	 train_loss 0.029990295552124735 	 test_loss 10.940108239650726 	 accuracy 0.8025
# epoch 47 	 train_loss 0.026600683166179806 	 test_loss 9.909851685166359 	 accuracy 0.83
# epoch 48 	 train_loss 0.027014318606234156 	 test_loss 8.986240908503532 	 accuracy 0.835
# epoch 49 	 train_loss 0.019624351887614466 	 test_loss 9.874004453420639 	 accuracy 0.82
```

# おわりに
だいぶ単純ではありましたが、とりあえず音をDeep Learningで分類することができました。音声解析系の発展系としては[こちら](https://www.nogawanogawa.com/entry/rfcx)や[こちら](https://qiita.com/shinmura0/items/6befb83f7cde7b091905)の記事で紹介されているようにPANNsなるモデルもあるようです。
音声認識系で次勉強するのならこの辺かな？

おわり