本文はなくてもあとがき
はじめに
M5Stack(ESP32)向けにSimpleVoxというライブラリを開発しました。折角の記念に実装に当たってのこぼれ話のようなものを書いておこうと思います。
SimpleVoxは音声認識、とりわけ孤立単語音声認識(Isolated word recognition)の機能を提供するライブラリです。本ライブラリはVAD(Voice Activity Detection)、音声からのMFCC(Mel-frequency cepstral coefficients)の計算、および2つのMFCC間の距離を計算するためのDTW(Dynamic Time Warping)などの基本的な機能を提供します。
実際のところ、より優れた方法やアルゴリズムは他にも存在しますが、M5Stack(ESP32)のようなリソース成約のあるデバイスでの音声処理ライブラリは充実しているとは言えず、本ライブラリは選択肢の一つになるかと思います。
参考までに以下に紹介のあるESP-SRでは学習済みのモデルを使って200以上のコマンドを識別できるそうです。
Unleashing the Power of OpenAI and ESP-BOX: A Guide to Fusing ChatGPT with Espressif SOCs
本ライブラリでは同様のことを実現するのは難しいですが、自分のためのいくつかのコマンドに対応することは比較的容易です。したがって、一方は精度良く規模の大きな識別が可能だがトレーニングコストが大きい、もう一方は精度はそこそこでいくつかの識別しか出来ないがトレーニングコストは少ないといったトレードオフの関係にあるかと思います。
VAD
VADは音声が存在する区間を検出する技術で、音声区間を予め検出することで、ノイズの除去、リソースの節約などの利点があり、音声セグメントの境界を正確に検出することで、音声認識の精度を向上させることに繋がります。
シンプルな実装としては信号のパワーとZero-crossingを用いた手法があり、元々ライブラリでも同様の手法を採用していたのですが、Espressifが公開していたESP-SRのVADがなかなか良かったことがあり、置き換えた経緯があります。
とはいえ、シンプルな実装もまるでダメってわけではなくて、意外とうまくいくので実装してみると驚くこと請け合いです。難点は摩擦音や破裂音などの検出が難しいところで、例えば「スタックチャン」は「(スタッ)クチャン」と検出されます(それでも、毎回同じように欠落して検出されるので孤立単語音声認識的には問題なく動作したりします)。
以下にわかりやすくまとめられているので気になる方はぜひ目を通されてください。
AI Shift,inc, 信号パワーと零交差数を用いた音声区間検出
MFCC
MFCCについての詳しい解説は既に他で多く語られているのでそちらをご確認ください。
簡単にまとめると、音声を短い区間に分割し、それらにいくつかのステップで変換を行うことで1区間の音声信号が(ライブラリの初期値だと)12次元のMFCCに変換される、ふんわり考えるとデータ圧縮として考えられると思います。
世の中には結構参考プログラムがあるといえばあるんですが、pythonでlibrosaを使っているのが多くて実装に難儀しました。改めてgithubを探してみるとC++でMFCCを実装したものもあるみたいですが・・・。
それにしても大学とか研究室の資料があるので、実装したことある人もいるんじゃないのかなぁ。
DTW
DTW (Dynamic Time Warping) は、音声データや時系列データの比較に使われる手法です。DTWは、時間の伸縮性を考慮して2つのデータ系列の類似度を評価します。音声データは話し方で時間的な変動や発話速度の違いがあるため、単純な距離計算では正確な比較ができません。DTWを使うと、音声データの時間的な変動を考慮しながら、より正確な類似度の評価ができます。
シンプルな実装だとN×Mの行列を予め作ってコサイン距離やDTW距離をメモすることが多いです。ただこれをM5Stackでやると使用可能なRAMを食いつぶすのでそのまま採用することは出来ませんでした。
英語版のWikipediaを見るとHirschberg's alogrithmを使うことでO(min(N, M))になるよ!ってあるんですが、リンク先を見ても「はぁ?何いってんの?」って感じでした。元々のコードを眺めていた直感として、直前のデータだけあれば計算できるから減らせそうだよねとは感じていたんですが、何だかんだウンウン言いながらひねり出して作った感じです。諸々終わってHirschbergを再度見直すと、「うーん、同じことしてるなぁ」ってなったのでちゃんと読み込める力って大事だなぁって思いました。
イメージは下図のような感じで、計算しながら直近の値を更新すれば片方の長さ(N or M)で事足りるということです。閃いたときは唸ったね。
- (0, 0) は初期値として設定し、(0, j) は地続きなので初期値から算出
- (1, 0) は (0, 0) から算出
- (1, 1) は (0, 0), (0, 1), (1, 0)から算出
- 以降、(i, j) は (i-1, j-1), (i-1, j), (i, j-1)から算出
- このとき(i, j)の計算に(i-1, j-2)は必要ないので(i, j-2)の値で更新する
コメント
コメントを投稿