畳込みニューラルネットワークの基本技術を比較する　ー音でもやってみたー

[前回](https://qiita.com/shinmura0/items/2f34e81e371ded8b4afc)の投稿では、多くの皆様から「いいね」を頂きました。
この場を借りて御礼申し上げます。

前回は画像ファイルでしたが、音ファイルでもやってみたいと思います。

やりたいことは、音ファイルでどの技術が**一番効果があるのか**数値化します。
以下の流れでやっていきます。

（基本のCNN）　→　（3つの技術を追加していく）

基本のCNNに、3つの技術を追加しながら、分類精度の上昇幅を比較します。
コードはkerasで書いています。

画像ファイルと同じ結果になるのか？
乞うご期待！

#データのダウンロード
使うデータは[ESC-50](https://qiita.com/cvusk/items/61cdbce80785eaf28349#%E9%9F%B3%E5%A3%B0%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E5%89%8D%E5%87%A6%E7%90%86)です。
これは、人の音声や自然界の音を集めたデータで、50個のラベルが付与されています。

#データの前処理
音の生データは波になっています。

```py
import librosa
import pandas as pd
import os

# define directories
base_dir = "./"
meta_file = os.path.join(base_dir, "meta/esc50.csv")

# load metadata
meta_data = pd.read_csv(meta_file)
x, fs = load_wave_data(audio_dir, meta_data.loc[5,"filename"])

#show wave
plt.plot(x)
plt.show()
```

![Wave.png](https://qiita-image-store.s3.amazonaws.com/0/264781/2d609063-455c-8230-5d9c-15e20e47e640.png)


ついでに、以下のようにコマンドすると音の再生ができます。

```py
import IPython.display as ipd
ipd.Audio(x, rate=fs)
```

##スペクトログラム
波のデータのままでは、うまく特徴が捉えられないので、周波数帯域に
変換してやります。

```py
import librosa.display

# change wave data to stft
def calculate_sp(x, n_fft=512, hop_length=256):
    stft = librosa.stft(x, n_fft=n_fft, hop_length=hop_length)
    sp = librosa.amplitude_to_db(np.abs(stft))
    return sp

# display wave in spectrogram
def show_sp(sp, fs, hop_length):
    librosa.display.specshow(sp, sr=fs, x_axis="time", y_axis="log", hop_length=hop_length)
    plt.colorbar(format='%+2.0f dB')
    plt.title('Spectrogram')
    plt.show()

sp = calculate_sp(x)
show_sp(sp, fs, hop_length=256)
```

![Spectrogram0.png](https://qiita-image-store.s3.amazonaws.com/0/264781/92884121-e03a-15aa-cf35-354b128ac193.png)


横軸は時間（sec）、縦軸は周波数（Hz）です。
科捜研のドラマとかで出てくるスペクトログラムの完成です。

##メルスペクトログラム
前項で得られたスペクトログラムを、ゴニョニョと処理してメルスペクトログラムを求めます。

　スペクトログラム　→　ゴニョニョ　→　**メルスペクトログラム**　→　(MFCC)

ゴニョニョが気になる方は以下のリンクを参考にしてください。
・[音楽と機械学習 前処理編 MFCC ~ メル周波数ケプストラム係数](https://qiita.com/martin-d28jp-love/items/34161f2facb80edd999f)
・[ケプストラム分析](http://aidiary.hatenablog.com/entry/20120211/1328964624)

メルスペクトログラムへ変換することにより、以下の効果があるそうです。
　・次元が下がる（本稿の場合、513次元　→　128次元）
　・低音域では**敏感**に、高音域では鈍感になる（人間の耳の仕様に近づく）
具体的に可視化してみます。

```py
# change wave data to mel-stft
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

# display wave in heatmap
def show_melsp(melsp, fs):
    librosa.display.specshow(melsp, sr=fs, x_axis="time", y_axis="mel", hop_length=128)
    plt.colorbar(format='%+2.0f dB')
    plt.title('Mel spectrogram')
    plt.show()

# example data
melsp = calculate_melsp(x)
show_melsp(melsp, fs)
```

![Mel Spectrogram.png](https://qiita-image-store.s3.amazonaws.com/0/264781/29f83226-9846-14df-7d28-756141daabe3.png)

横軸は時間（sec）、縦軸はメル周波数（Hz）です。


#基本のCNN
基本のCNNは[@cvusk](https://qiita.com/cvusk/items/61cdbce80785eaf28349#cnn%E3%81%AE%E5%AE%9A%E7%BE%A9)さんのコードを参考にしています。
コードは以下の通りです。

```py
import keras
from keras.models import Model
from keras.layers import Input, Dense, Activation
from keras.layers import Conv2D, GlobalAveragePooling2D, Add

# redefine target data into one hot vector
classes = 50

def cba(inputs, filters, kernel_size, strides):
    x = Conv2D(filters, kernel_size=kernel_size, strides=strides, padding='same')(inputs)
    x = Activation("relu")(x)
    return x

# define CNN
inputs = Input(shape=(x_train.shape[1:]))

x_1 = cba(inputs, filters=32, kernel_size=(1,8), strides=(1,2))
x_1 = cba(x_1, filters=32, kernel_size=(8,1), strides=(2,1))
x_1 = cba(x_1, filters=64, kernel_size=(1,8), strides=(1,2))
x_1 = cba(x_1, filters=64, kernel_size=(8,1), strides=(2,1))

x_2 = cba(inputs, filters=32, kernel_size=(1,16), strides=(1,2))
x_2 = cba(x_2, filters=32, kernel_size=(16,1), strides=(2,1))
x_2 = cba(x_2, filters=64, kernel_size=(1,16), strides=(1,2))
x_2 = cba(x_2, filters=64, kernel_size=(16,1), strides=(2,1))

x_3 = cba(inputs, filters=32, kernel_size=(1,32), strides=(1,2))
x_3 = cba(x_3, filters=32, kernel_size=(32,1), strides=(2,1))
x_3 = cba(x_3, filters=64, kernel_size=(1,32), strides=(1,2))
x_3 = cba(x_3, filters=64, kernel_size=(32,1), strides=(2,1))

x_4 = cba(inputs, filters=32, kernel_size=(1,64), strides=(1,2))
x_4 = cba(x_4, filters=32, kernel_size=(64,1), strides=(2,1))
x_4 = cba(x_4, filters=64, kernel_size=(1,64), strides=(1,2))
x_4 = cba(x_4, filters=64, kernel_size=(64,1), strides=(2,1))

x = Add()([x_1, x_2, x_3, x_4])

x = cba(x, filters=128, kernel_size=(1,16), strides=(1,2))
x = cba(x, filters=128, kernel_size=(16,1), strides=(2,1))

x = GlobalAveragePooling2D()(x)
x = Dense(classes)(x)
x = Activation("softmax")(x)

model = Model(inputs, x)
```

色々なサイズのフィルターを通すことで、短期から長期の特徴を捉えることができるそうです。
早速、以下のように学習させます。入力データはスペクトログラムを使用しています。

```py
# initiate Adam optimizer
opt = keras.optimizers.adam(lr=0.00001, decay=1e-6, amsgrad=True)

# Let's train the model using Adam with amsgrad
model.compile(loss='categorical_crossentropy',
              optimizer=opt,
              metrics=['accuracy'])

hist = model.fit(x_train,y_train,
                 validation_data=(x_test,y_test),
                 epochs=50, 
                 verbose=1,
                 batch_size=20)
```

学習結果↓

![base.png](https://qiita-image-store.s3.amazonaws.com/0/264781/0bd39eef-ff3f-24f5-5b42-be059ba16d9b.png)

Validationの分類精度は**36.2％**となりました。
まだ伸びる余地はありますが、行っても40％くらいという印象です。


#各技術の追加
これより、各技術を追加します。

基本のCNNと同じ条件にするために、畳込み層は同じ構造にして、epochも20で固定します。
その他、全結合層や正則化層などは無制限です。

前回の結果を受け、追加する技術はData AugmentationとBatch Normalizationに絞ります。
そして、技術というよりは、前処理に近いメルスペクトログラムの有無によって精度が
どれくらい変わるのか観察します。

##メルスペクトログラムの効果
基本のCNNの入力は、スペクトログラムを使っていました。
ここでは、入力をメルスペクトログラムに変えてみます。

学習結果↓

![mel.png](https://qiita-image-store.s3.amazonaws.com/0/264781/4f7aeaee-a136-6246-5b8c-84429fb7b1de.png)

Validationの分類精度は28.2％となり、基本のCNNより6.4％**ダウン**しました。
メルスペクトログラムは、音声認識では適しているかもしれませんが、自然界の音は
スペクトログラムの方が良いのかもしれません。

##Batch Normalizationの効果
前項のモデルにBatch Normalizaion(BN)を追加します。

```py
def cba(inputs, filters, kernel_size, strides):
    x = Conv2D(filters, kernel_size=kernel_size, strides=strides, padding='same')(inputs)
    x = BatchNormalization()(x)
    x = Activation("relu")(x)
    return x
```
学習結果↓

![Batch.png](https://qiita-image-store.s3.amazonaws.com/0/264781/39c2b32e-48ed-d2ce-ba5b-04123f6b9977.png)

Validationの分類精度は46.2％で、前項より**17.8％**アップしました。
BNの効果は絶大です。動作は軽い上に、良い仕事をします。

##Data Augmentationの効果
Data Augmentationを追加します。こちらも[@cvusk](https://qiita.com/cvusk/items/61cdbce80785eaf28349#augmentation)さんのコードを参考にしています。
音程を少し変えたり、ノイズを加えたり、時間をずらしたりしています。

```py
# data augmentation: add white noise
def add_white_noise(x, rate=0.002):
    return x + rate*np.random.randn(len(x))

# data augmentation: shift sound in timeframe
def shift_sound(x, rate=2):
    return np.roll(x, int(len(x)//rate))

# data augmentation: stretch sound
def stretch_sound(x, rate=1.1):
    input_length = len(x)
    x = librosa.effects.time_stretch(x, rate)
    if len(x)>input_length:
        return x[:input_length]
    else:
        return np.pad(x, (0, max(0, input_length - len(x))), "constant")

x_w = add_white_noise(x)
x_s = shift_sound(x)
x_st = stretch_sound(x)
```

学習結果↓
![Aug.png](https://qiita-image-store.s3.amazonaws.com/0/264781/3f6f1212-f032-4701-6de0-97fbe5c981e9.png)

Validationの分類精度は56％となり、前項より**10％**アップしました。
まだまだ伸びる余地はあり、エポックを増やせば70％くらいまで行けるかもしれません。

#まとめ
以上の結果をまとめると、以下のようになります。
![result.png](https://qiita-image-store.s3.amazonaws.com/0/264781/1566338b-e671-d5fb-ff0a-1c2029e22123.png)


前回同様、ここだけで判断するのはちょっと乱暴ですが、音ファイルでは
**「Batch Normalization」が一番効果的である**ことが確認されました。

BNとData Augmentationは、画像ファイルでも良い作用をしましたし、これは外せない技術といえます。
一方、メルスペクトログラムやresblockは慎重に取り扱わないと、悪影響を及ぼす可能性があるので注意が必要です。

#感想

今回使用したデータは、1つのファイルサイズが128×1723×1でとんでもなく重く、保存や学習でかなり時間がかかりました。

前回のCIFAR-10（サイズ：32×32×3）は、100エポックの学習で**2時間**かかり、気軽にできるレベルでした。しかし、今回は50エポックの学習で**4時間**かかりました（Colaboratory）。

また、batchsizeは20が限界で、これ以上だとColaboratoryからメモリーエラーで怒られました。
ColaboratoryでESC-50を使う際は、気長に待ちましょう。