Aizu-Progressive xr Lab blog

会津大学のVR部であるA-PxLの部員が持ち回りで投稿していくブログです。部員がそれぞれVRに関する出来事やVRにちなんだことについて学んだことを書いていきます。

連絡はサークルTwitterのDMへお願いします。
「面白法人カヤックVR分室」としても活動しています。詳細はこちら

70行弱で書ける!音声データから話者を当てる人工知能を作ってみた!

こんにちは。学部3年の柴山です。
今回はPythonで音声データを機械学習させて、話者認識(誰が話しているかを判定する)をする方法を紹介したいと思います。
コード総数70行弱、しかし正答率98.7%コスパ良しな人工知能に興味を持っていただけたのなら、ぜひ最後までお付き合いください。

データの前処理

今回使用した音声データは「12人の話者が日本中の駅名を呟いたもの」です。
音声データは駅名ごとに用意してあり、総数は約4万件、一人当たり3千ちょいあります。
ちなみに日本の駅の総数は9500個ほどらしいので、これでも一部なんですね( ̄◇ ̄;)

以下のことに気をつけていただければ音声データはなんでも平気です。
全てのデータは.wavファイルにします。
ファイル名を全て「<話者の名前>_<番号>」という形式にします。
データは学習用とテスト用に7:3の割合でディレクトリを分けます。
学習用のディレクトリ名は「<話者の名前>」、テスト用のディレクトリ名は「<話者の名前>test」にします。
f:id:aizu-vr:20190425185127p:plain

コード

今回は以下のライブラリを使用します。

import scipy.io.wavfile as wav  # .wavファイルを扱うためのライブラリ
from sklearn.svm import SVC     # SVC(クラス分類をする手法)を使うためのライブラリ
import numpy                    # ndarray(多次元配列)などを扱うためのライブラリ
import librosa                  # 音声信号処理をするためのライブラリ
import os                       # osに依存する機能を利用するためのライブラリ


次にROOT_PATHに音声データの各ディレクトリが入っているディレクトリへの絶対パス、speakersに話者名の配列を代入します。
word_trainingとspeaker_trainingは後々、計算結果を入れたり、データのラベリング(そのデータがどの話者の音声なのかを対応づける)をしたりするために使います。

# ルートディレクトリ
ROOT_PATH = '<音声データのディレクトリが入っているディレクトリへの絶対パス>'

# 話者の名前(各話者のデータのディレクトリ名になっている)
speakers=['SP203', 'SP205', 'SP206', 'SP208', 'SP210', 'SP211',
    'SP502', 'SP605', 'SP619', 'SP622', 'SP704', 'SP708']

word_training=[]    # 学習用のFCCの値を格納する配列
speaker_training=[] # 学習用のラベルを格納する配列

次に、MFCC(メル周波数ケプストラム係数)を求めるための関数を定義します。
MFCCとは音声にどのような特徴があるかを数値化したものです。
この数値によって分類していきます。

# MFCCを求める関数
def getMfcc(filename):
    y, sr = librosa.load(filename)      # 引数で受けとったファイル名でデータを読み込む。
    return librosa.feature.mfcc(y=y, sr=sr) # MFCCの値を返します。

ディレクトリごとにデータをロードして、MFCC求めていきます。

# 各ディレクトリごとにデータをロードし、MFCCを求めていく
for speaker in speakers:
    # どの話者のデータを読み込んでいるかを表示
    print('Reading data of %s...' % speaker)
    # 話者名でディレクトリを作成しているため<ルートパス+話者名>で読み込める。
    path = os.path.join(ROOT_PATH + speaker)    
    # パス、ディレクトリ名、ファイル名に分けることができる便利なメソッド
    for pathname, dirnames, filenames in os.walk(path): 
        for filename in filenames:
            # macの場合は勝手に.DS_Storeやらを作るので、念の為.wavファイルしか読み込まないようにします。
            if filename.endswith('.wav'):
                mfcc=getMfcc(os.path.join(pathname, filename))
                word_training.append(mfcc.T)    # word_trainingにmfccの値を追加
                label=numpy.full((mfcc.shape[1] ,), 
                speakers.index(speaker), dtype=numpy.int)   # labelをspeakersのindexで全て初期化
                speaker_training.append(label)  # speaker_trainingにラベルを追加

word_training=numpy.concatenate(word_training)  # ndarrayを結合
speaker_training=numpy.concatenate(speaker_training)

ここで機械学習のプログラムを書く上でのちょっとしたコツを一つ。
上記のprint('Reading 〜')のところをご覧ください。
この行は本来機械学習をする上では一切必要ありません。ただ文字をコンソールに出力しているだけですから。
ですが、この一行がないと自らのプログラムがきちんと動いているのか疑心暗鬼になります。
余談ですが、上記の4万件のデータだと一回の実行に10時間以上かかりました。
その間何も映らない暗い画面だけ、タスクマネージャーを見るとCPU使用率が100%。
私は一度、数時間実行した挙句、自分のコードが信じられずにCtrlを押しながらCキーに指を当て...。
皆さんには同じ目にあって欲しくありません笑。
なのである程度進捗がわかるような構造を作りましょう。

そして、いよいよ学習部分ですが、なんと3行です。

# カーネル係数を1e-4で学習
clf = SVC(C=1, gamma=1e-4)      # SVCはクラス分類をするためのメソッド
clf.fit(word_training, speaker_training)    # MFCCの値とラベルを組み合わせて学習
print('Learning Done')

python機械学習をするためのライブラリが充実しているのがいいですね。
カーネル係数は長くなる上私も勉強中であまり下手なことは言えないので、
カーネル法という機械学習に用いられるパターン認識の手法に使う値」と思ってください。
この値をいじると正答率に直接影響します。私の場合はトライ&エラーの結果1e-4という値に落ち着きましたが、
データによってはある程度上下すると思います。

つぎに、学習したデータをもとに、〜testディレクトリのデータでテストします。

counts = []     # predictionの中で各値(予測される話者のインデックス)が何回出ているかのカウント
file_list = []  # file名を格納する配列

# 各話者のテストデータが入っている~testというディレクトリごとにMFCCを求めていく
for speaker in speakers:
    path = os.path.join(ROOT_PATH + '%stest' % speaker)
    for pathname, dirnames, filenames in os.walk(path):
        for filename in filenames:
            if filename.endswith('.wav'):
                mfcc = getMfcc(os.path.join(pathname, filename))
                prediction = clf.predict(mfcc.T)    # MFCCの値から予測した結果を代入
                # predictionの中で各値(予測される話者のインデックス)が何回出ているかをカウントして追加
                counts.append(numpy.bincount(prediction))   
                file_list.append(filename)  # 実際のファイル名を追加

最後は推測される話者のインデックスより、speakersから話者の名前を取得し、実際のファイル名がその名前から始まっていれば正解。
違っていれば間違いという判定で正答率を求めます。

total = 0   # データの総数
correct = 0 # 正解の数

# 推測される話者の名前がファイル名の頭と一致したらCorrect
for filename, count in zip(file_list, counts):
    total += 1
    result = speakers[numpy.argmax(count-count.mean(axis=0))]   # 
    if  filename.startswith(result):
        correct += 1

print('score : ' + str(correct / total))

まとめ

こうしてみると随分ライブラリ頼りで、70行弱なんてのは詐欺まがいですが、そこがpythonのいいところと許していただきたいです笑。
自分で想定していた以上の正答率が出たので嬉しいですが、課題としては如何せん実行時間がかかり過ぎかもしれません。
今回使用したMFCCというのは精度は高いですが、計算量が多めな手法だったので、今度は別の手法も試してみたいと思います!
最後まで読んでいただきありがとうございました!

学部3年 柴山 叶

会津大学VR部の部員が持ち回りで投稿していくブログです。特にテーマに縛りを設けずに書いていきます!