LoginSignup
18
12

More than 3 years have passed since last update.

深層学習による音声処理:音声から声優さんが誰かを判別させよう

Last updated at Posted at 2020-09-02

まえがき

先日、「現場で使える!TensorFlow開発入門」という本を買った。
とてもわかり易い本で、画像分類からGANまで広くに渡って解説していた。
ちょっとスクリプトが古く、TensorFlow 1.x で書いているため、現在のTensorFlow 2.x だと動かないところも多いが、
Kerasに関しては、ほぼ互換性が保たれているため、4章以降はほぼ問題なく動かせる。

さて、実際に本を読んで勉強したのだから、何らかのアウトプットをしたいと考えた。
だけど、ただ、同じことをやるのは面白くない。そこで、今回は個人的に今興味を持っている音声解析を、Kerasを利用してやってみようと思う。

自分としては、become-yukarinのように、声を変換するようなものを作りたかったのだが、最初にやるには少し難しすぎるので、今回は入門として、
音声から声優さんが誰かを判別させよう
というテーマに決めた。

上記の本は、画像解析がほとんどだったので、音声を画像に変換して、学習する、という流れで実装していくことにした。
なお、筆者は今まで音声解析について学んだことがなかったため、おかしな点があると思う。その時は、コメントお願いします。

環境

  • Windows 10
  • python 3.7.5
  • TensorFlow 2.3.0
  • jupyter

アウトライン

上で書いたように、今回は画像(=二次元データ)に変換するアプローチを取っていく。
そこで、以下のような流れで実装していこう。

  1. データを集める
  2. 音声をMFCCに変換
    • ここで、画像データに変換することができる。
  3. 画像データを整形・調整
  4. モデルを構築して学習

実装

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');

wavdata.png

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.png

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();

mfcc_skip.png

さらに、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');

これによって、グラフは次のようになった。
wav_cut.png

もう一度、mfccsに変換すると、次のようになる。

mfccs = librosa.feature.mfcc(x, sr=fs)
mfccs = mfccs[1:]
librosa.display.specshow(mfccs, sr=fs, x_axis='time')
plt.colorbar();

mfcc_skip_cut.png

これで画像データにすることができた。

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)

これで、下の画像のように、データができるはずだ。

directory.png

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%以上ということで、うまく言ったと思う。

今後は、アニメやラジオの声を利用して、小倉唯とかの声を聞き分けられるようにしたい。

18
12
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
12