ルールベースjuliusの誤認識対策にSVMを利用してみよう

前回やったことの続きです。
ルールベースの音声認識をjuliusでやったときに過剰にマッチしまくる問題への対策です。


前回、juliusのクセを観察し、独自のスコアリングをやりました。
多少は誤認識に強くなったのですが、それでも人と人が会話や議論するような短文のやり取りにさらされると、やっぱり誤認識してしまいます。

SVM

もう、これは単純なパラメータの閾値では無理です。
ある閾値がそれを超えたら捨てるなどの単純な話ではないのです。
複数のパラメータが複雑に絡み合った世界です。


それをニンゲンの手で観察し、推論していては時間が膨大にかかってしまいます。
人間でやると大変なことは、機械にやらせましょう。
と、いうわけで、機械学習です。
今回は、機会学習の中からSVMを利用します。


SVMは精度もさることながら、学習速度はやや問題があるものの、判別は高速ですし、何よりライブラリが比較的揃っており、導入しやすいためてす。
ライブラリが充実しているので、ブラ重とよんであげましょう。爆発しろ。


さて、SVMのライブラリの中で、 liblinearを今回は利用します。
liblinearな理由は特にないんですが、なんか流行っているというだけです。ようするにミーハーですw


さて、SVMを利用して、間違った認識と正しい認識を切り分けてみます。

そもそも間違った認識とは何か?

私達が作っている音声認識では、呼びかけキーワード + 命令 といった構文を使います。
前回は、ケーキという単語を使いましたが、今回は コンピュータ という単語を使います。


コンピュータに仕事をやらせたいときは、音声で、「コンピュータ」「命令をして」 と、しゃべって依頼します。
例えば、「コンピュータ、エアコンを付けて」といった感じです。
このコンピュータというワードを呼びかけと読んでいます。(勝手に命名しました。どうだまいったか。)


で、そうなると、このコンピュータといった呼びかけキーワードを正しく認識できればいいわけです。
人と人とが会話しているような、短文の連続を「コンピュータ」という単語と誤認してしまい、音声認識をスタートさせてしまうといったケースが問題なのです。


コンピュータといった単語が正しい認識できれば、ほとんどの誤動作は防げます。

よって、音声認識で誤認識を避ける問題は、認識した単語が本当にコンピュータという単語なのか調べる問題といえます。
間違った呼びかけさえ検出しなければ、暴発は防げます。

julius-plusでは、呼びかけ部分を 別途 __temp__DictationCheck.wavというファイルに出力します。
これは、呼びかけ部分をもう一度、別視点から見るために、音声認識に再度くぐらせているためです。
このファイルを利用して、SVMによる判別を通してみます。



↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓


学習させる

SVMで学習し、モデルを作るプログラムを作りました。
githubにあるのでご自由にお使いください。
https://github.com/rti7743/rtilabs/tree/master/files/asobiba/juliustest/linear


学習するには、学習するデータが必要です。
コンピュータという単語を正しく聞き取れた wav を ok_wavディレクトリに、
間違って聞きとった wav を bad_wav に入れます。

これだけでもモデルは作れますが、モデルの完成度を見るために、未知のテストデータを用意します。
正しくコンピュータといっているwavを test_ok_wav に、
間違っている wav を test_bad_wav に入れていきます。

linear
	bad_wav
		失敗した呼びかけを入れる場所。
		学習データとして利用します。
	ok_wav
		成功した呼びかけを入れる場所。
		学習データとして利用します。
	test_bad_wav
		失敗した呼びかけを入れる場所。
		学習したモデルのテスト用に利用します。
	test_ok_wav
		成功した呼びかけを入れる場所。
		学習したモデルのテスト用に利用します。


今回は、110個ぐらいのデータを用意しました。
これでプログラムを動かす準備は出来ました。


実際に動かしてみましょう。
プログラムを実行すると、 julius による認識ログがざっーとでて素性を記録します。
そのあとに、SVMがモデルを作成し、モデルの検証を行い結果を画面に出力します。
このとき、モデルの保存も行います。



学習したデータを再テスト: Accuracy = 100.000% (110/110)   詳細はlog_train.txt
未知のデータでのテスト  : Accuracy = 85.714% (24/28)   詳細はlog_test.txt

認識率 8割以上。
教師データについては100%は、まーいいとして、未知のデータに対しても 85%の精度で正しく分類できています。
これは結構いいんじゃなイカ。

素性は?

SVMなので考えられる素性をとりあえず投げ込んでみますw

//julius認識したデータ
void g_output_result(Recog *recog, void *dummy)
{
	//喋った時間の総数
	const float mseclen = (float)recog->mfcclist->param->samplenum * (float)recog->jconf->input.period * (float)recog->jconf->input.frameshift / 10000.0f;
	//仮説の数によるペナルティ
	const int hypothesisPenalty = countHypothesisPenalty(recog);

	//1:成功 2:失敗 
	if (g_OKorBAD)
	{
		*g_TrainFile << "1";
	}
	else
	{
		*g_TrainFile << "2";
	}
	for(const RecogProcess* r = recog->process_list; r ; r=r->next) 
	{
		//ゴミを消します。
		if (! r->live || r->result.status < 0 ) 
		{
			continue;
		}

		// output results for all the obtained sentences
		const auto winfo = r->lm->winfo;
		for(auto n = 0; n < r->result.sentnum; n++) 
		{ // for all sentences
			const auto s = &(r->result.sent[n]);
			const auto seq = s->word;
			const auto seqnum = s->word_num;

			int i_seq;
			// output word sequence like Julius 
			//0 , [1 , 2, 3, 4], 5, と先頭と最後を除いている、開始終端、記号を抜くため
			int i;
			for(i = 0;i<seqnum;i++)
			{
				//1単語 --> 単語の集合が文になるよ
				i_seq = seq[i];

				//開始と終了を飛ばす
				if (	strcmp(winfo->woutput[i_seq]  , "<s>") == 0 
					||  strcmp(winfo->woutput[i_seq]  , "</s>") == 0 
				){
					continue;
				}
				break;
			}
			if (i >= seqnum) 
			{
				continue;
			}
			//dict から plus側のrule を求める
			int dict = atoi(winfo->wname[i_seq]);

			//マッチよみがなを取得する
			std::string yomi = ConvertYomi(winfo,i_seq);

			//cmscoreの数字(このままでは使えない子状態)
			auto cmscore = s->confidence[i];
			//素性1 cmsscore
			*g_TrainFile << " 1:" << cmscore;

			//julius のスコア 尤度らしい。マイナス値。0に近いほど正しいらしい。
			//「が」、へんてこな文章を入れても、スコアが高くなってしまうし、長い文章を入れるとスコアが絶望的に低くなる
			//ので、そのままだと使えない。
			auto score = s->score;

			//素性2 文章スコア
			*g_TrainFile << " 2:" << score;

			auto all_frame = r->result.num_frame;

			//素性3 フレーム数
			*g_TrainFile << " 3:" << all_frame;

			//素性4 仮説の数によるペナルティ
			*g_TrainFile << " 4:" << hypothesisPenalty;

			//素性5 録音時間
			*g_TrainFile << " 5:" << mseclen;

			//多少使えるスコアを計算します。
			//			oneSentence->plus_sentence_score = computePlusScore(oneSentence->nodes,s->score,r->result.sentnum,mseclen);
			auto plus_sentence_score = computePlusScore(cmscore,s->score,hypothesisPenalty,mseclen);
			//素性6 plusスコア
			*g_TrainFile << " 6:" << plus_sentence_score;

			//素性7 サンプル数?
			*g_TrainFile << " 7:" << r->lm->am->mfcc->param->header.samplenum;

			//素性8〜 これがきめてになった。
			int feature = 8;
			for(int vecI = 0 ; vecI < r->lm->am->mfcc->param->header.samplenum ;vecI++ )
			{
				for(int vecN = 0 ; vecN < r->lm->am->mfcc->param->veclen ;vecN++ )
				{
					*g_TrainFile << " " << feature++ << ":" << r->lm->am->mfcc->param->parvec[vecI][vecN];
				}
			}

			*g_TrainFile << std::endl;

			return ;
		}
	}

}

とりあえず、 1〜 7 まではそれっぽい変数の値を入れます。
最後の 8 〜は r->lm->am->mfcc->param->parvec の値を入れています。
だいたい2000素性、多いもので5000素性ぐらいが生まれるようです。


この膨大な素性の中から、SVMにより規則性を見つけ出させます。


最初は 1〜7までの素性でやっていたのですが、そのときは、70%ぐらいの認識率でした。
それが、 r->lm->am->mfcc->param->parvec を入れたことで 8割以上の認識率となりました。

r->lm->am->mfcc->param->parvecを入れない場合

学習したデータを再テスト: Accuracy = 78.899% (86/109)   詳細はlog_train.txt
未知のデータでのテスト  : Accuracy = 71.429% (20/28)   詳細はlog_test.txt


r->lm->am->mfcc->param->parvecを入れた場合

学習したデータを再テスト: Accuracy = 100.000% (110/110)   詳細はlog_train.txt
未知のデータでのテスト  : Accuracy = 85.714% (24/28)   詳細はlog_test.txt

素性の見つけ方ですが、これっぽいものを手で入れたあとは、デバッガで recog変数 を dump して、変数をdiffして変化している奴を見付け出して決めました。深い意味はありませんw
こんないい加減なことをやっているのに、8割以上取れる認識モデルが作れるSVMさんは素敵ですね。
俺達にできないことを平然とやってくれる。そこがしびれる憧れる。

学習結果の読み方

プログラムによりいくつかのファイルがカレントに出力されます。

ファイル名 役割
__svm_model.dat SVM学習結果を格納したモデルです。これを利用して判別を行います。
train.txt 教師データ。学習させるデータを記録したファイルです。liblinearのフォーマットです。
log_train.txt 作ったモデルで教師データを検証した結果です。先頭に正しく分類されればOK 、違えばBADが入ります。
log_test.txt 未知のデータで検証した結果です。先頭に正しく分類されればOK 、違えばBADが入ります。

r->lm->am->mfcc->param->parvecを入れると素性が爆発して非常に読みづらくなりますが、そこはご愛嬌ということで。
ここで作った __svm_model.dat モデルを julius に組み込んでみましょう。

SVMを利用するjulius

julius-plus にSVMを搭載させてみました。
githubにあるのでご自由にお使いください。
https://github.com/rti7743/rtilabs/tree/master/files/asobiba/juliustest/julius-4.2.1_with_svm

このプログラムは windows/linuxのクロスプラットフォームで動作します。

windows
VS2010 で julius-4.2.1\msvc\julius-plus.sln を開いてください。


linux
sudo apt-get install flex g++ 'libboost*-dev' libboost-thread-dev binutils-dev  libboost-system-dev libasound2-dev

./configure --with-mictype=alsa

make

cd julius-plus
make

./julius-plus



学習データを再構築した場合は、次のファイルに上書きしてください
windowsの人
julius-4.2.1_with_svm\msvc\julius-plus\__svm_model.dat としてコピってください。


linuxの人
julius-4.2.1_with_svm/julius-plus/__svm_model.dat としてコピってください。


1日程度動かしてみたのですが、スコアを使っていた時よりも感度が上がったように思います。
今まで、単純な閾値で切っていたのがSVMになったことで、賢い伐採が行われているようです。

今のところ、誤認識はありませんでした。
ただ、幸運なだけなのかしもれませんが・・・引き続きテストを続けたいと思いますw

課題

今のところ、3つほど課題があります。

1つは、オンライン学習ができないこと。
今回 liblinearを利用したため、動的に学習するオンライン学習は利用できません。
たとえば、間違った認識をしたとき、ユーザがそれはちゃうねんといった場合、それをつど学習することができません。
SVMでもオンライン学習する手法はあるそーなんですが、SVMを実装するパワーがないのでぐぬぬっているところです。


2つ目は、なぜ r->lm->am->mfcc->param->parvec を入れたら精度が上がったのかよくわからないというところ。
素性が最大5000素性となっているのでこれは人間の目で見るのは不可能です。
いったい何が決め手になったのはよくわかりません。


3つ目が、r->lm->am->mfcc->param->parvecを入れたことにより、私の声に特化してしまったのではないか?ということ。
他の人の声でどう反応するのか?これはまだよくわかっていませんw
一人暮らしの辛いところですねwwww orz

まとめ

SVMすごい。