ルールベース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すごい。