CNN+メルスペクトグラム使って言語識別してみた

#言語識別とは？
一言でいうと、音声データから言語を特定すること。
例えば、「おはようございます、いい天気ですね」という音声データから、「この音声データは日本語！」とか、
「Buenas Tardes」という音声データから、「この音声データはスペイン語！」って感じです。

使用用途としては、何語を話してるか分からい相手に対して、言語を特定することとか。
今ある自動翻訳機とかって、基本的に予め「英語だよ」「スペイン語だよ」って情報を与えないといけっぽい。
だから、そもそも何語を話してるか分からない相手に対しては翻訳しようがない。（と思う。）

だから、何語を話しているか分からない相手に対しては、
言語識別を使って言語を特定　→　翻訳　
みたいな感じで使われたりする。（と思う。）　

# CNNを使った理由
言語識別には色々な手法があるけれど、今回はCNN使ってみました。
理由は英語の分かり易い記事をたまたま見つけたから。
(http://yerevann.github.io/2015/10/11/spoken-language-identification-with-deep-convolutional-networks/)

この記事はトップコーダで2015年に行われた言語識別のコンテストで10位だったらしいでの、勉強がてらやってみました。

# 使用したデータセット
上の記事では、予め用意された66,176個の10秒のMP3ファイルに対し、176言語分類する問題でした。

でも今回私はVoxForge(http://www.voxforge.org/)
から取得した、英語、フランス語、スペイン語のwav形式の音声ファイルを使用しました。
それぞれ下記のURLからダウンロードできます。
音声データが大量なので、私はwgetコマンドで入手しました。

```
http://www.repository.voxforge1.org/downloads/SpeechCorpus/Trunk/Audio/Main/16kHz_16bit/
http://www.repository.voxforge1.org/downloads/fr/Trunk/Audio/Main/16kHz_16bit/
http://www.repository.voxforge1.org/downloads/es/Trunk/Audio/Main/16kHz_16bit/
```

# 前処理
CNNを使うので、上で手に入れた各言語のwav形式の音声ファイルを画像に変換します。

今回は「メルスペクトグラム」という、横軸が時間、縦軸が周波数、画像の濃淡が強さを示すものを使用しました。
librosaというライブラリを使えば、簡単にwavファイルからメルスペクトグラムを得ることができます。

以下がwavファイルをメルスペクトログラムの画像に変換するコード。
(mp3ファイルでも同じコードで動くと思います)

```
# 入力:音声ファイルのパス　
# 出力:音声データのメルスペクトグラム画像(192×192)

import librosa as lr

def wav_to_img(path, height=192, width=192):
    signal, sr = lr.load(path, res_type='kaiser_fast')
    if signal.shape[0] < sr: # wavファイルが3秒以下の場合
        return False, False
    else:
        signal = signal[:sr*3] # 前半3秒だけ抽出
        hl = signal.shape[0]//(width*1.1)
        spec = lr.feature.melspectrogram(signal, n_mels=height, hop_length=int(hl))
        img = lr.amplitude_to_db(spec)**2
        start = (img.shape[1] - width)  // 2 
        return True, img[:, start:start+width] 
```

先程のデータセットには数秒～数十秒のデータが含まれています。
今回はそのデータの中の3秒以上のデータから、初めの3秒だけ抽出し使用しました。
3秒以下のデータは使用しません。



以下の関数で指定したフォルダ内の音声データを全てメルスペクトグラム画像に変換して保存します。

```
# 指定したフォルダの音声ファイルを全てスペクトログラム画像に変換し、指定したフォルダに保存

import os
import glob
import imageio

def process_audio(in_folder, out_folder):
    os.makedirs(out_folder, exist_ok=True)
    files = glob.glob(in_folder)
    start = len(in_folder)
    files = files[:]
    for file in files:
        bo, img = mp3_to_img(file)
        if bo == True:
            imageio.imwrite(out_folder + '.jpg', img)
```

以下に示すように、第一引数に各言語の音声データがフォルダ、第二引数に出力先のフォルダを選択して実行してください。
全ての言語に対して行い、全ての音声ファイルをメルスペクトグラムに変換してください。

```
# 以下は音声ファイルの保存先のパスと、出力先のパスを指定
process_audio('data/voxforge/english/*wav', 'data/voxforge/english_3s_imgp/')
```

# データの分割

上で取得した、各言語に対するメルスペクトグラムのフォルダを扱いやすいように1つのHDF5ファイルに保存します。
ちょっと不格好ですが、上で保存した各言語に対するメルスペクトグラム画像を以下のパスにHDF5形式で保存します。
保存先のパス：'data/voxforge/3sImg.h5'

```
import dask.array.image
import h5py
dask.array.image.imread('data/voxforge/english_3s_img/*.jpg').to_hdf5('data/voxforge/3sImg.h5', english)
dask.array.image.imread('data/voxforge/french_3s_img/*.jpg').to_hdf5('data/voxforge/3sImg.h5', french)
dask.array.image.imread('data/voxforge/spanish_3s_img/*.jpg').to_hdf5('data/voxforge/3sImg.h5', spanish)
```

トレーニングデータ、バリデーションデータ、テストデータに分割します。

```
import h5py
# データサイズなどは得られたメルスペクトグラム画像などを考慮して自分で決めてください
data_size = 60000
tr_size = 50000
va_size = 5000
te_size = 5000

x_english = h5py.File('data/voxforge/3sImg.h5')['english']
x_french = h5py.File('data/voxforge/3sImg.h5')['french']
x_spanish =  h5py.File('data/voxforge/3sImg.h5')['spanish']

x = np.vstack((x_english[:20000], x_french[:20000], x_spanish[:20000]))

del x_french
del x_english
del x_spanish

x = da.from_array(x, chunks=1000)

# 正解データのための準備
y = np.zeros(data_size)
# 英語、フランス語、スペイン語のラベルをそれぞれ0,1,2
y[0:20000] = 0
y[20000:40000] = 1
y[40000:60000] = 2

# データのシャッフルと分割
import numpy as np

shfl = np.random.permutation(data_size)
training_size = tr_size
validation_size = va_size
test_size = te_size

# トレーニングデータ，評価用，テストサイズでそれぞれ分割のために，ランダムに用意したインデックスshflを割り当て．
train_idx = shfl[:training_size] 
validation_idx = shfl[training_size:training_size+validation_size] 
test_idx = shfl[training_size+validation_size:] 

# 割り当てたindexでトレーニングデータ，評価用，テストサイズを作成
x_train = x[train_idx]
y_train = y[train_idx]
x_vali = x[validation_idx]
y_vali = y[validation_idx]
x_test = x[test_idx]
y_test = y[test_idx]

# 画像の正規化
x_train = x_train/255
x_vali = x_vali/255
x_test = x_test/255

# 学習のための形の変形
x_train = x_train.reshape(tr_size, 192, 192, 1)
x_vali = x_vali.reshape(va_size, 192, 192, 1)
x_test = x_test.reshape(te_size, 192, 192, 1)

# 教師データのワンホットベクトル
y_train = y_train.astype(np.int)
y_vali = y_vali.astype(np.int)
y_test = y_test.astype(np.int)

```

以上の処理で学習データとバリデーションデータ、テストデータに分割できます。


# 使用したCNNの構造
使用したネットワーク構造は次の通りです。
ここは自由に変えてみてもいいかもしれません。
フレームワークはkerasを用いました。

```
import tensorflow as tf
from tensorflow.python import keras
from tensorflow.python.keras import backend as K
from tensorflow.python.keras.models import Model, Sequential, load_model
from tensorflow.python.keras.layers import Conv2D, MaxPooling2D, Dense, Flatten, Dropout, Input, BatchNormalization,  Activation
from tensorflow.python.keras.preprocessing.image import load_img, img_to_array, array_to_img, ImageDataGenerator

i = Input(shape=(192,192,1))
m = Conv2D(16, (7, 7), activation='relu', padding='same', strides=1)(i)
m = MaxPooling2D(pool_size=(3, 3), strides=2, padding='same')(m)
m = BatchNormalization()(m)

m = Conv2D(32, (5, 5), activation='relu', padding='same', strides=1)(m)
m = MaxPooling2D(pool_size=(3, 3), strides=2, padding='same')(m)
m = BatchNormalization()(m)

m = Conv2D(64, (3, 3), activation='relu', padding='same', strides=1)(m)
m = MaxPooling2D()(m)
m = BatchNormalization()(m)

m = Conv2D(128, (3, 3), activation='relu', padding='same', strides=1)(m)
m = MaxPooling2D(pool_size=(3, 3), strides=2, padding='same')(m)
m = BatchNormalization()(m)

m = Conv2D(128, (3, 3), activation='relu', padding='same', strides=1)(m)
m = MaxPooling2D(pool_size=(3, 3), strides=2, padding='same')(m)
m = BatchNormalization()(m)

m = Conv2D(256, (3, 3), activation='relu', padding='same', strides=1)(m)
m = MaxPooling2D(pool_size=(3, 3), strides=2, padding='same')(m)
m = BatchNormalization()(m)

m = Flatten()(m)
m = Activation('relu')(m)
m = BatchNormalization()(m)
m = Dropout(0.5)(m)

m = Dense(512, activation='relu')(m)

m = BatchNormalization()(m)
m = Dropout(0.5)(m)

o = Dense(3, activation='softmax')(m)

model = Model(inputs=i, outputs=o)
model.summary()
```

以下で学習しました。
トレーニングデータ数が少ないのか、モデルが悪いのか、すぐに過学習してしまう傾向にあったので、
エポック数は5くらいで十分です。
ここらへんは考察が全然できてなくて申し訳ありません。

```
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
model.fit(x_train, y_train, batch_size=32, epochs=5, verbose=1, validation_data=(x_vali, y_vali), shuffle = True)
```


# 結果
テストデータに対して行った結果です。

```
model.evaluate(x_test, y_test)
```

[0.2763474455833435, 0.8972]

約90%でちゃんと予測できてる感じですかね。

ただ実際、学習データとテストデータに同じ人間の音声データが多く混じっているので、精度は高く出ていると思います。

より正確に精度を調べるのであれば、
'data/voxforge/english_3s_imgp/'
のシャッフルする前のデータで使用してないデータ、今回の例でいうと、

```
x_english = h5py.File('data/voxforge/3sImg.h5')['english']
x_french = h5py.File('data/voxforge/3sImg.h5')['french']
x_spanish =  h5py.File('data/voxforge/3sImg.h5')['spanish']

x = np.vstack((x_english[20000:], x_french[20000:], x_spanish[20000:]))
```

のデータを使うとより、正確な精度が調べられると思います。
ちなみにその時の、各言語に対する正答率は次の通りでした。

英語:0.8414201183431953
フランス語:0.7460106382978723
スペイン語:0.8948035487959443

フランス語はあんまりうまく識別できてないですね。

# まとめ
今回は勉強がてらCNNを使って音声認識をやってみました。
途中駆け足になって説明が不十分の場所があるかもしれません、申し訳ありません。
基本的な考え方は、冒頭でも紹介しましたが、以下のURLから確認できるので、英語が得意な方はそっちを見たほうがいいかもしれませんね。
(http://yerevann.github.io/2015/10/11/spoken-language-identification-with-deep-convolutional-networks/)

最後まで読んでいただきありがとうございました。