一聞きで誰の声だと見抜いた…りはしなかった

ごちうさを素材に機械学習する流行に乗って,
諸先生方の後を追い,声判別器を作ってみた.
Warning! 精度は全然出てません.
気づいたときにはごちうさAdvent Calendar の枠がなかったし,そもそも25日にも間に合ってなかった.

28日に載せていただけました!

諸先生方
1234224576先生
声優の声分類してみた

piruty先生
音声から特徴抽出して学習するもろもろツールと学習器を実装しました

勝手に先生と呼ばせていただいています.

目標
アニメから音声切り出して,MFCCベクトルに変換
SVMで5人(ココア,チノ,リゼ,千夜,シャロ)+otherの判別器をトレーニング
→Deep Learning にも手をだすことに

声を集める
1期1,7話から,5人の声を切り出します.
これが結構たいへんで,ホントはもっとデータが欲しいのですが...
集めた声部分をつなげて聞いてるだけでも幸せになれますね.

一番少ないチヤだと,2分ちょっとしかないです.
これを,1秒ごとに切って声音声を集めます.
音声切り出しは@TwitKgoさんがやってくれました.

ココア チノ リゼ 千夜 シャロ other
7:19 5:01 4:45 2:02 4:05 2:19

pirtury先生の手法にこのデータを入れてみたところ,SVMのスコアで0.32ほどでした.
トレーニングデータを集めるのはたいへんだから,勉強を兼ねていろいろやってみたいと思います.

特徴量を計算
諸先生方が MFCC が良いというからには MFCC がいいんだよ.
ということで,1秒の音声を MFCC にベクトル化します.

from scikits.audiolab import wavread
from pysptk.sptk import mfcc
def wav2mfcc(wav_path,mfcc_degree=19,power=True):
    data, fs, fmt = wavread(wav_path)
    # MFCC
    mfcc_vec = mfcc(x=data,order=mfcc_degree,fs=fs,power=power)
    return mfcc_vec

MFCCの抽出部分はこんな感じです.
ここで作ったベクトルを,スペース区切りのテキストファイルとして,各行の最後に正解ラベルを付与して書き込みます.
今回は,pysptk を使ってみました.
mfcc を取り出すだけなら scikits.talkbox.features の mfcc が楽なのですが,エネルギーの取り出し方がわからなかったので.
とりあえず,MFCC 19次元+エネルギー1次元の20次元でやってみます.

アルゴリズムに困っても,SVMがあれば大丈夫
じゃなかったです.SVMで精度が出るなら,SVMでいいだろうと思ってでやってみました.
(個人的に,Deep Learning の利点は Feature Engineering をサボれるところだと思っています.特徴量を算出できて,それを単独で使えるならば,SVM や CRF を使う方が楽なことがほとんどだと思います.)

# -*- coding: utf-8; -*-
import numpy as np
from sklearn import svm
from sklearn.grid_search import GridSearchCV
from sklearn.externals import joblib
def grid_search(train_features, train_labels):
	param_grid = [
		{'C': [0.1,1,10,100,500], 'kernel': ['linear']},
		{'C': [0.1,1,10,100,500,1000] ,'gamma' : [0,01,0.001,0.0001],'kernel':['rbf']}
	]
	clf = GridSearchCV(svm.SVC(C=1), param_grid, n_jobs=4)
	clf.fit(train_features, train_labels)
	print clf.best_estimator_
	print clf.best_score_
	return clf

def getBestCLF():
	# 学習データ
	data_training_tmp = np.loadtxt('../doc/train2.txt', delimiter=' ')
	data_training = [np.array(list(x)[:-1]) for x in data_training_tmp]
	label_training = [int(x[-1]) for x in data_training_tmp]
	# ベストなパラメーターでトレーニング
	clf = grid_search(data_training,label_training)
	joblib.dump(clf,'clf.pkl')

if __name__ == '__main__':
	# clf を作る or 読む
	getBestCLF() # コメントにしたりしなかったりで使いわける
	clf = joblib.load('clf.pkl')
	print clf.best_estimator_
	print clf.best_score_

	# 予測
	test_data_path = 'test.txt'
	test_data = np.loadtxt(test_data_path, delimiter=' ')
	label_prediction_tmp = clf.predict(test_data)
	label_prediction = list(label_prediction_tmp)

一応,ざっくりパラメーターサーチはします.
それなりに時間がかかるので,サーチするパラメーターは飛び飛びです.
何度か使ったので,作ったモデルは保存するようにしました.getBestCLF()をコメントにすると,既存モデルを読みます.
で,これのスコアが 0.38
これではチノちゃんに怒られてしまいます.

特徴量を探す旅
ということで,MFCCだけでダメなら,他の特徴量も出してかさ増ししよう!
MFCCにはピッチ情報が含まれない,ならばメルケプストラム分析(mcep)だ.
で,窓関数をいろいろ試しながら作ったのがこれ

import wave
import numpy as np
from pysptk.sptk import mcep
def wav2mcep(wav_path):
	data, fs, fmt = wavread(wav_path)
	# mcep
	wf = wave.open(wav_path,'r')
	x = wf.readframes(wf.getnframes())
	x = frombuffer(x, dtype="int16") / 32768.0
	N = 512
	hammingWindow = np.hamming(N)
	hammingMCEP = mcep( hammingWindow * x[0:N] )
	hanningWindow = np.hanning(N)
	hanningMCEP = mcep( hanningWindow * x[0:N] )
	blackmanWindow = np.blackman(N)
	blackmanMCEP = mcep( blackmanWindow * x[0:N] )
	bartlettWindow = np.bartlett(N)
	bartlettMCEP = mcep( bartlettWindow * x[0:N] )

	mcvec = hammingMCEP#np.r_[hammingMCEP,hanningMCEP] # ここらへんで好きに作る
	return mcvec

窓関数をいろいろ試したが,ベーシックな hamming 窓が一番良いらしい.
「SVM は簡単に特徴量を増やせるのがいいところだ」と言われているように,どんどんベクトルをつなげていけばいいだけなのは楽ですね.(その分,どれがどのくらい効くのかを調べるのがたいへんになりますが)

結局, MCEP 単体で0.48, MFCC+MCEP で0.47 でした.
MFCC効いてない(;・∀・)

Deep Learning で頑張る
せっかくなので,Chainerを使ってDeep Learning も試してみます.
コードは一部のみ掲載(そのうち整理してgithubに上げるつもり)

SVMで使ったデータファイルを読み込むようにします.

train_data_path = 'train.txt'
train_data = np.loadtxt(train_data_path,delimiter=' ',dtype=np.float32)
x_train = np.array([x[:-1] for x in train_data])
y_train = np.array([x[-1] for x in train_data],dtype=np.int32)

この時,ベクトルは np.float32型,ラベルは np.int32型であることに注意します.

モデルはよくある3層ネットワークの改造版です.ただの sigmoid 4層ネットワークを使ってみます.
ReLuは収束が早いのでとても楽なのですが,入力ベクトルに負値がたくさんあるので,せめて1層目は sigmoid の方がよさそうです.で,面倒なので全部 sigmoid に.
chainerだとモデルの定義が本当に楽ですね.caffeの層を書くのはもちょっと難しいです.

# 多層パーセプトロンのモデル(パラメータ集合)
model = chainer.FunctionSet(l1=F.Linear(input_dimention, n_units), # 最初は入力次元
                            l2=F.Linear(n_units, n_units),
                            l3=F.Linear(n_units, n_units),
                            l4=F.Linear(n_units, 6)) # 最後はラベル数(5人+other)

epochはそれぞれ,エラー率99%くらいで安定するところで適当に切りました.

ラベルの予測

def predict(x_data):
	x = chainer.Variable(x_data)
	h1 = F.dropout(F.sigmoid(model.l1(x)), train=False)
	h2 = F.dropout(F.sigmoid(model.l2(h1)), train=False)
	h3 = F.dropout(F.sigmoid(model.l3(h2)), train=False)
	y = model.l4(h3)
	label = y.data[0].argmax()
	return label

で,出力ベクトルの中で最も大きいところが正解ラベル(番号)になります.

音声識別攻略未完了(みっしょんいんこんぷりーと)
どうにもデータ量の多いココアがやたらめったら多いし,精度もまだまだです.
短い時間で4人が喋ってる部分(10秒間)の結果を載せてみます.
後ろの音が大きいこともあって,なかなか難しそうです.


Deep Learining の()内はepoch数

SVM DeepLearning
正解  MFCC  MCEP MFCC+MCEP MFCC(300) MCEP(500) MFCC+MCEP(190)
 リゼ  リゼ  ココア   ココア   other   リゼ    ココア
 リゼ  other   other   other   チノ   リゼ    リゼ
リゼ  ココア  ココア   リゼ  ココア   ココア   ココア
 リゼ  ココア   ココア    リゼ  ココア   リゼ    ココア
 ココア  チノ  シャロ   チノ  チノ   ココア    チノ
 ココア/other  other  other    other  チノ   other    ココア
 other  ココア   ココア    ココア  シャロ   ココア    ココア
 千夜  チノ  チノ    チノ  ココア   チヤ    ココア
 チノ(千夜も)  other   リゼ    other   チノ   チノ    ココア
 チノ(ココアも)  ココア   チノ    ココア  シャロ   チノ    チノ

Deep Learning はさすがですね, SVM よりだいぶ向上しました.
1秒って結構長いもので,1秒の中で話者が入れ替わったり,BGMの音が大きかったりで,なかなかやっかいです.
理想と実現性を考慮すると,頑張っても0.5秒ずつくらいでの分割がいいところでしょうか.

機械学習では,用意したデータを適当に分けて,トレーニング用とテスト用に分けるのが普通で,できれば交差検定くらいしてやればさらにいいわけですが,今回はトレーニングデータを減らさないように全部でトレーニングをかけています.サボったとも言えますが.

今回はあまりうまくいってませんが
思いつく改善方法は

・人の声が入っているかどうかは機械学習でない方法で判定したい
・トレーニングデータをもっと食わせる(切り出すのがたいへん)
・Deep Learning の手法として, このタスクなら RNN がいいんじゃないかなぁ(データ作りがたいへん)

ざっくり,こんな感じでしょうか.
ホントはもっとうまいこといったよって書きたかったんだけどなぁ.

字幕化する(おまけ)
結果のテキストと照らしながら動画を見るのはたいへんなので,動画に埋め込みました.
make_zimaku.py

# -*- coding: utf-8; -*-
num = 1
time = 0
for line in open('myway2/2wa.txt'):
	start_min = time / 60
	start_sec = time % 60
	end_sec   = start_sec + 1
	end_min   = start_min
	if end_sec == 60:
		end_sec = 0
		end_min += 1
	out_time = "00:{0}:{1},000 --> 00:{2}:{3},000".format(str(start_min).zfill(2),str(start_sec).zfill(2),str(end_min).zfill(2),str(end_sec).zfill(2))
	zimaku = line.rstrip()

	if not zimaku == "bgm":
		dat = str(num) + '\n' + out_time + '\n' + zimaku + '\n'
		print dat
		num += 1

	time += 1

で, $python make_zimaku.py > zimaku.srt な感じにして字幕ファイルを作り,Ubuntu 上の Avidemux で埋め込みました.

音とか映像で何かしてブログ書くのって,とってもたいへんなんですね.

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です