まえがき
先日、「現場で使える!TensorFlow開発入門」という本を買った。
とてもわかり易い本で、画像分類からGANまで広くに渡って解説していた。
ちょっとスクリプトが古く、TensorFlow 1.x で書いているため、現在のTensorFlow 2.x だと動かないところも多いが、
Kerasに関しては、ほぼ互換性が保たれているため、4章以降はほぼ問題なく動かせる。
さて、実際に本を読んで勉強したのだから、何らかのアウトプットをしたいと考えた。
だけど、ただ、同じことをやるのは面白くない。そこで、今回は個人的に今興味を持っている音声解析を、Kerasを利用してやってみようと思う。
自分としては、become-yukarinのように、声を変換するようなものを作りたかったのだが、最初にやるには少し難しすぎるので、今回は入門として、
『音声から声優さんが誰かを判別させよう』
というテーマに決めた。
上記の本は、画像解析がほとんどだったので、音声を画像に変換して、学習する、という流れで実装していくことにした。
なお、筆者は今まで音声解析について学んだことがなかったため、おかしな点があると思う。その時は、コメントお願いします。
環境
- Windows 10
- python 3.7.5
- TensorFlow 2.3.0
- jupyter
アウトライン
上で書いたように、今回は画像(=二次元データ)に変換するアプローチを取っていく。
そこで、以下のような流れで実装していこう。
- データを集める
- 音声をMFCCに変換
- ここで、画像データに変換することができる。
- 画像データを整形・調整
- モデルを構築して学習
実装
1. データを集める
今回は、日本声優統計学会というサイトのデータセットを利用する。
土谷麻貴さん、上村彩子さん、藤東知夏さんの三人の声優(アナウンサー)が「通常」「喜び」「怒り」という3つの感情で100個のセリフを無音室で録音したデータセットである。
100セリフ×3人×3感情で、計900ものデータがおいてある。
(Wikipediaを読み上げているため、意味のないセリフで喜び・怒りの感情を表現しているのを聞くと、声優さんってすごいって気持ちになる)
今回は、この三人を見分けられるモデルを作ってみよう。
データは*.tar.gz
だが、解凍ソフトを利用すればwindowsでも解凍できる。
2. 音声をMFCCに変換
では、次にデータをMFCC(メル周波数ケプストラム係数)というものに変換しよう。
MFCCについて詳しく説明すると、長くなるので、「声の特徴量」とだけ説明しておく。
音声データ(wav)をMFCCという数値に変換するのは、librosa
というものを利用する。
pip install librosa
でインストールできる。
2.1. データの読み込み
まず、データを読み込ませよう。
例としてfujitou_normal_001.wav
を読み込ませる。
from matplotlib import pyplot as plt
import librosa
import librosa.display
import os
# データのパスを書く
WAV_DATA_PATH = os.path.join("Dataset","fujitou_normal","fujitou_normal_001.wav")
x, fs = librosa.load(WAV_DATA_PATH, sr=44100)
librosa.display.waveplot(x, sr=fs, color='blue');
librosa.load
の返り値のxがデータ(形式はnumpy
)で、fs
はサンプリングレートである。
これでWavデータの取り込みができた。
2.2. MFCCへの変換
このデータを、MFCCに変換してみよう。
mfccs = librosa.feature.mfcc(x, sr=fs)
librosa.display.specshow(mfccs, sr=fs, x_axis='time')
plt.colorbar();
MFCCをは横軸が時間、縦軸が20次元の値となっている。
print(mfccs.shape) # -> (20, 630)
2.3. データの整形
1次元目のデータ(グラフの一番下)は除こう。
これも詳しくは説明しないが、グラフを見てもらえれば分かる通り、1次元目のデータのせいでデータ範囲が大きくなってしまっており、1次元より上の値の特徴量がつかみにくくなっている。
mfccs = mfccs[1:]
librosa.display.specshow(mfccs, sr=fs, x_axis='time')
plt.colorbar();
さらに、wavの方のデータを見てもらえれば分かる通り、7.5秒以降はほぼ無音だ。
この辺も邪魔なのでカットしてしまおう。
import numpy as np
def cut_silence(wavdata, eps=0.01):
st = 0
gl = len(wavdata)
data = np.abs(wavdata)
threshold = np.max(data) * eps
for i,a in enumerate(data):
if a > threshold:
st = i - 1
break
for i,a in reversed(list(enumerate(data))):
if a > threshold:
gl = i
break
return wavdata[st:gl]
今回は、「0秒からデータの最大値×0.01以上の値になるまで」と「最後からデータの最大値×0.01以上の値になるまで」の値をカットした。
x = cut_silence(x)
librosa.display.waveplot(x, sr=fs, color='blue');
もう一度、mfccsに変換すると、次のようになる。
mfccs = librosa.feature.mfcc(x, sr=fs)
mfccs = mfccs[1:]
librosa.display.specshow(mfccs, sr=fs, x_axis='time')
plt.colorbar();
これで画像データにすることができた。
2.4. 全データを変換して保存
MFCCは意外と時間がかかる処理のため、librosa.feature.mfcc
を利用したあとは、numpyデータ(*.npy)として保存してたほうがあとあと楽だと思う。
すべてのデータを変換して、numpyデータに保存しよう。
以下のようなディレクトリ構造を仮定している。
.
|-Dataset
| |-fujitou_angry
| | |-fujitou_angry_001.wav
| | |-fujitou_angry_002.wav
| | |-fujitou_angry_003.wav
| | |-...
| |
| |-fujitou_happy
| | |-fujitou_happy_001.wav
| | |-...
| |
| |-...
|
|-ImageData
| |-fujitou_angry
| | |-fujitou_angry_001.npy
| | |-fujitou_angry_002.npy
| | |-fujitou_angry_003.npy
| | |-...
| |
| |-fujitou_happy
| | |-fujitou_happy_001.npy
| | |-...
| |
| |-...
|
まず、os.listdir
を利用してすべてのデータのパスを取得する。
import os
import random
DATASET_DIR="Dataset"
wavdatas = []
dirlist = os.listdir(DATASET_DIR)
for d in dirlist:
d = os.path.join(DATASET_DIR, d)
datalist = os.listdir(d)
y = [d[d.find("\\")+1:d.find("_")], d[d.find("_") + 1:]] # ファイル名から正解データの決定
datalist = [[os.path.join(d,x), y] for x in datalist]
wavdatas.extend(datalist)
次に、numpyデータを配置するディレクトリを作る。
IMAGE_DATA = "ImageData"
dirlist = os.listdir(DATASET_DIR)
for d in dirlist:
os.makedirs(os.path.join(IMAGE_DATA, d), exist_ok=True)
そして、全部のデータを変換してnp.save
しよう。
def get_mfcc(datadir):
x, fs = librosa.load(datadir, sr=44100)
x = cut_silence(x)
mfccs = librosa.feature.mfcc(x, sr=fs)
mfccs = mfccs[1:]
return mfccs, x, fs
nn = len(wavdatas)
for i, data in enumerate(wavdatas):
path_list = data[0].split("\\")
path_list[0] = IMAGE_DATA
path_list[2] = path_list[2].replace(".wav", ".npy")
image_path = "\\".join(path_list)
mfcc,x,fs = get_mfcc(data[0])
if i%10 == 0:
print(i, "/", nn)
np.save(image_path, mfcc)
これで、下の画像のように、データができるはずだ。
3. 画像データを整形・調整
これで、データを二次元データにすることができた。
次に、二次元データを扱いやすいように整形していこう。
3.1. データの読み込み
まず、上で保存したnumpyデータを読み込もう。
IMAGE_DATA = "ImageData"
numpy_datas = []
dirlist = os.listdir(IMAGE_DATA)
for d in dirlist:
d = os.path.join(IMAGE_DATA, d)
datalist = os.listdir(d)
datalist = [[np.load(os.path.join(d,x)), os.path.join(d,x)] for x in datalist]
numpy_datas.extend(datalist)
3.2. 正規化
まず、上のデータは-200~100くらいの値の範囲になっている。
これを0~1の範囲に正規化しよう。
# データ全体の最大値と最小値を取得
data = numpy_datas[0][0]
maximum = np.max(data)
minimum = np.min(data)
for i, data in enumerate(numpy_datas):
M = np.max(data[0])
m = np.min(data[0])
if maximum < M:
maximum = M
if minimum > m:
minimum = m
# 0~1の範囲に
normalize = lambda x: (x - minimum)/(maximum - minimum)
for i, data in enumerate(numpy_datas):
numpy_datas[i][0] = normalize(data[0])
3.3. データの大きさを揃える
2節で作ったデータは$19\times T$の大きさとなっている。ここで、$T$は時間(正確には、秒×サンプリングレート)である。
単純にニューラルネットワークに突っ込むためには、データの大きさをすべてそろえたほうがわかりやすい。
from PIL import Image
import numpy as np
img_datas = []
for i,data in enumerate(numpy_datas):
imgdata = Image.fromarray(data[0])
imgdata = imgdata.resize((512,19))
numpy_datas[i][0] = np.array(imgdata)
ここではすべて、512×19のデータに変換した。
3.4. 画像データの保存
2節と同じように、データを保存する。
まず、ディレクトリを作る。
NORMALIZE_DATA = "NormalizeData"
dirlist = os.listdir(DATASET_DIR)
for d in dirlist:
os.makedirs(os.path.join(NORMALIZE_DATA, d), exist_ok=True)
そして、保存。
for i, data in enumerate(numpy_datas):
path_list = data[1].split("\\")
path_list[0] = NORMALIZE_DATA
image_path = "\\".join(path_list)
np.save(image_path, data[0])
4. モデルを構築して学習
それでは、ディープラーニングで学習させてみよう
4.1. 訓練データとテストデータの分割
900個のデータを訓練データとテストデータ、検証データに分割しよう。
import numpy as np
import random, os
NORMALIZE_DATA="NormalizeData"
N_TRAIN = 0.8
N_TEST = 0.1
N_VALID = 0.1
train_data = []
test_data = []
valid_data = []
dirlist = os.listdir(NORMALIZE_DATA)
for d in dirlist:
d = os.path.join(NORMALIZE_DATA, d)
datalist = os.listdir(d)
y = [d[d.find("\\")+1:d.find("_")], d[d.find("_") + 1:]] # ファイル名から正解データの決定
datalist = [[np.load(os.path.join(d,x)), y, os.path.join(d,x)] for x in datalist]
random.shuffle(datalist)
train_data.extend(datalist[:int(len(datalist)*N_TRAIN)])
test_data.extend(datalist[int(len(datalist)*N_TRAIN): int(len(datalist)*N_TRAIN) + int(len(datalist)*N_TEST)])
valid_data.extend(datalist[int(len(datalist)*N_TRAIN) + int(len(datalist)*N_TEST): ])
random.shuffle(train_data)
random.shuffle(test_data)
random.shuffle(valid_data)
全データを訓練データ:テストデータ:検証データ=0.8:0.1:0.1になるようにした。
900個のデータを利用しているため、720:90:90 となっている。
また、うまく分散させるために2回シャッフルしている。
一つのディレクトリ内のデータを取得後、一回シャッフル、最後に結合後にもう一度シャッフルしている。
- 入力データ:
train_datas[i][0]
- 正解データ:
train_datas[i][1]
- 声優名:
train_datas[i][1][0]
- 感情:
train_datas[i][1][1]
- 声優名:
というようなデータ構造になっている。
4.2. 入力と正解データを学習用に変換
tensorflowのkerasで学習する前に、入力と正解データを使いやすいように変換しておこう。
今回は、声優名を分類するため、train_datas[i][1][0]
が正解データである。
# numpyに変換
input_data_train = np.array([train_data[i][0] for i in range(len(train_data))])
input_data_test = np.array([test_data[i][0] for i in range(len(test_data))])
input_data_valid = np.array([valid_data[i][0] for i in range(len(valid_data))])
# 正解データをリストに
label_data_train = [train_data[i][1][0] for i in range(len(train_data))]
label_data_test = [test_data[i][1][0] for i in range(len(test_data))]
label_data_valid = [valid_data[i][1][0] for i in range(len(valid_data))]
正解データは1-hotに変更しておこう。
from tensorflow.keras.utils import to_categorical
label_dict={"tsuchiya": 0, "fujitou":1, "uemura":2}
label_no_data_train = np.array([label_dict[label] for label in label_data_train])
label_no_data_test = np.array([label_dict[label] for label in label_data_test])
label_no_data_valid = np.array([label_dict[label] for label in label_data_valid])
label_no_data_train = to_categorical(label_no_data_train, 3)
label_no_data_test = to_categorical(label_no_data_test, 3)
label_no_data_valid = to_categorical(label_no_data_valid, 3)
4.3. モデルの構築・学習
それでは、ニューラルネットワークのモデルを構築しよう。
鉄則のようなものは知らないので、とりあえず畳み込み演算と正規化を繰り返すことにした。
活性化関数はreluを利用している。
from tensorflow.keras.layers import Input, Conv2D, Conv2DTranspose,\
BatchNormalization, Dense, Activation,\
Flatten, Reshape, Dropout
from tensorflow.keras.models import Model
inputs = Input((19,512))
x = Reshape((19,512,1), input_shape=(19,512))(inputs)
x = Conv2D(12, (1,4), strides=(1,2), padding="same")(x)
x = BatchNormalization()(x)
x = Activation("relu")(x)
x = Conv2D(12, (1,4), strides=(1,2), padding="same")(x)
x = BatchNormalization()(x)
x = Activation("relu")(x)
x = Conv2D(12, (2,2), strides=(1,1), padding="same")(x)
x = BatchNormalization()(x)
x = Activation("relu")(x)
x = Conv2D(12, (2,2), strides=(1,1), padding="same")(x)
x = BatchNormalization()(x)
x = Activation("relu")(x)
x = Flatten()(x)
x = Dense(3)(x)
output = Activation("softmax")(x)
model = Model(inputs=inputs, outputs=output)
model.summary()
model.summary()の結果は次のとおりだ。
Model: "functional_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) [(None, 19, 512)] 0
_________________________________________________________________
reshape (Reshape) (None, 19, 512, 1) 0
_________________________________________________________________
conv2d (Conv2D) (None, 19, 256, 12) 60
_________________________________________________________________
batch_normalization (BatchNo (None, 19, 256, 12) 48
_________________________________________________________________
activation (Activation) (None, 19, 256, 12) 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 19, 128, 12) 588
_________________________________________________________________
batch_normalization_1 (Batch (None, 19, 128, 12) 48
_________________________________________________________________
activation_1 (Activation) (None, 19, 128, 12) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 19, 128, 12) 588
_________________________________________________________________
batch_normalization_2 (Batch (None, 19, 128, 12) 48
_________________________________________________________________
activation_2 (Activation) (None, 19, 128, 12) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 19, 128, 12) 588
_________________________________________________________________
batch_normalization_3 (Batch (None, 19, 128, 12) 48
_________________________________________________________________
activation_3 (Activation) (None, 19, 128, 12) 0
_________________________________________________________________
flatten (Flatten) (None, 29184) 0
_________________________________________________________________
dense (Dense) (None, 3) 87555
_________________________________________________________________
activation_4 (Activation) (None, 3) 0
=================================================================
Total params: 89,571
Trainable params: 89,475
Non-trainable params: 96
_________________________________________________________________
学習させる。
model.compile(
optimizer="adam",
loss="categorical_crossentropy"
)
model.fit(
input_data_train,
label_no_data_train,
batch_size=30,
epochs=50,
validation_data=(input_data_valid, label_no_data_valid)
)
4.4. テストデータで精度予測
最後に、最初に作ったテストデータで予測してみよう。
out = model.predict(input_data_test)
predict = np.argmax(out, axis=1)
answer =np.argmax(label_no_data_test, axis=1)
print("correct:", np.sum(predict == answer), "/", len(predict))
print("rate:", np.sum(predict == answer)/len(predict) * 100, "%")
correct: 90 / 90
rate: 100.0 %
なんと、全問正解できた。素晴らしい。
何回か試してみたが、大体90%以上の精度が出ている。
もうちょっとモデルをチューニングすれば、確実に100%になるようにできると思う。
4.5. 感情の分類
せっかくだから、声優名だけでなく、感情も分類できるか試してみよう。
再掲
- 入力データ:
train_datas[i][0]
- 正解データ:
train_datas[i][1]
- 声優名:
train_datas[i][1][0]
- 感情:
train_datas[i][1][1]
から、train_datas[i][1][1]
が正解データとしてみよう。
結果は、
correct: 88 / 90
rate: 97.77777777777777 %
と、同じような結果になった。
まとめ
今回は、声から声優を分類するということで、音声を画像に変換して、畳込み層をつかったニューラルネットワークを使った。
結果は90%以上ということで、うまく言ったと思う。
今後は、アニメやラジオの声を利用して、小倉唯とかの声を聞き分けられるようにしたい。