SAPIルールベースの音声認識と精度
ルールベースの音声認識は、想定していない単語にまで過剰にマッチしてしまうことがあります。
そのため、そのままだと使いものにならないので、何かで重しをつけて上げる必要があります。
その重しの付け方について説明します。
windows SAPI な音声認識だと、 SPPHRASEELEMENT の SREngineConfidence に一致率を示す確率が格納されます。
確率なので、 0〜1の間のdouble型で格納されます。
これを見ることでおおよそのマッチ率を見ることができます。
ISpRecoResult* tempresult; { tempresult = tempevent.RecoResult(); //認識した文字列の取得 CSpDynamicString tempdstrText; hr = tempresult->GetText(SP_GETWHOLEPHRASE, SP_GETWHOLEPHRASE, TRUE, &tempdstrText, NULL); if(FAILED(hr)) return xreturn::windowsError(hr); SPPHRASE *pPhrase; hr = tempresult->GetPhrase(&pPhrase); if ( FAILED(hr) ) return xreturn::windowsError(hr); double confidence = pPhrase->pElements->SREngineConfidence; std::string ret = _W2A(tempdstrText); Log(std::string() + "認識文字列 :" + ret + + " " + num2str(confidence)); if (confidence <= 0.60) { Log(std::string() + "認識文字列棄却"); return ""; } return ret; }
この例だと 0.60つまり、60%以下の精度のものは捨てています。
SREngineConfidence は、ルールごとのエレメントごとになっているので、 複数のルールを結合している場合は、forで回さないといけません。
ただ、これがちょっとクセがあります。
例えば、(コンピュータ)(電気)(つけて) のような 3つのルールを組み合わせている場合を考えます。
ここで先頭の(コンピュータ)というのが音声認識をスタートさせる呼びかけの部分です。
//マッチしたphraseからデータを抽出する. //複雑になるのでクラス内クラスとして独立させる. //phrase を開放までをサポートします. class PhraseTo { private: //名前が付いているルールをすべてピックアップする. class spphraseRuleTracker { public: //ツリー構造になっているのでトップノードから再起的に下りていきます。 std::list<const SPPHRASERULE*> pickupRules; void track(const SPPHRASERULE *rule) { if (!rule) return ; if (rule->pFirstChild) track(rule->pFirstChild); if (rule->pNextSibling) track(rule->pNextSibling); if (!rule->pszName) return ; this->pickupRules.push_back(rule); } } Track; SPPHRASE* Phrase; public: PhraseTo(SPPHRASE* phrase) { assert(phrase != NULL); this->Phrase = phrase; if (this->IsError()) { return ; } //CommandRule の下を読み取る. this->Track.track(this->Phrase->Rule.pFirstChild); } ~PhraseTo() { if (this->Phrase) { CoTaskMemFree(this->Phrase); this->Phrase = NULL; } } //マッチした結果を正規表現キャプチャしたものだけを取得する. std::map<std::string , std::string> GetRegexpCapture() const { _USE_WINDOWS_ENCODING; std::map<std::string , std::string> ret; for(auto it = this->Track.pickupRules.begin(); it != this->Track.pickupRules.end() ; ++it) { const SPPHRASERULE* rule = *it; const int captureNumber = _wtoi(rule->pszName); //キャプチャされた値が入っているところまでスキップ unsigned int count = 0; const SPPHRASEELEMENT * pElem = this->Phrase->pElements; for (; count < rule->ulFirstElement ; ++pElem , count ++) ; //そこから指定された数の文字列を結合したものがキャプチャした結果になる。 std::string str; for (count = 0; count < rule->ulCountOfElements ; ++pElem , count ++) { str = str + _W2A(pElem->pszLexicalForm); } ret[num2str(captureNumber)] = str; } return ret; } std::string GetAllString() const { _USE_WINDOWS_ENCODING; std::string str; const SPPHRASEELEMENT * pElem = this->Phrase->pElements; const int allcount = this->Phrase->Rule.ulCountOfElements; for(int count = 0;count < allcount;++pElem,++count) { str = str + _W2A(pElem->pszLexicalForm); } return str; } double GetYobikakeEngineConfidence() const { //呼びかけの部分の信頼度を取得する. return this->Phrase->pElements->SREngineConfidence; } //平均認識率 double GetSREngineConfidenceAvg() const { double SREngineConfidenceSum = 0; for(auto it = this->Track.pickupRules.begin(); it != this->Track.pickupRules.end() ; ++it) { const SPPHRASERULE* rule = *it; const int captureNumber = _wtoi(rule->pszName); //キャプチャされた値が入っているところまでスキップ unsigned int count = 0; const SPPHRASEELEMENT * pElem = this->Phrase->pElements; for (; count < rule->ulFirstElement ; ++pElem , count ++) ; SREngineConfidenceSum += rule->SREngineConfidence; } return SREngineConfidenceSum / this->Track.pickupRules.size(); } //コールバックする関数IDの取得 unsigned int GetFuncID() const { //最初にNULLじゃないプロパティがでてくるらしい const SPPHRASEPROPERTY * pProp = this->Phrase->pProperties; if (pProp != NULL && pProp->vValue.vt == VT_UINT) { return pProp->vValue.ulVal; } return UINT_MAX; } bool IsError() const { if (!this->Phrase->Rule.pszName) return true; // if (!this->Phrase->Rule.pFirstChild) return true; return false; } };
このクラスの GetYobikakeEngineConfidence() で(コンピュータ)の部分の呼びかけが、
GetSREngineConfidenceAvg() の部分で、それ以降の (電気)(つけて)の部分の平均認識率がとれます。
これを利用して、誤認識をさせるフィルタを作ることができます。
「が」、これだけでは誤認識を防ぐことはできません。
ないちち2では、さらに呼びかけ部分をもう一度音声認識にくぐらせて再検証する仕組みを持っています。
これで大部分の誤認識は防げます。
一人暮らしのご家庭にはこれでも十分に機能します。
「ただし」、人と議論するような、短文が連続する会話シーンでは、これでも不適当です。
これですらも、突破して誤認識を発生させてしまいます。
ココらへんは SAPIのルールベースの限界といえます。
単純な一致率のような確率では、誤認識を防ぐことはできません。
もっとたくさんのデータが必要になってきます。