音声 AI エージェント — 聞いて、調べて、話すパイプライン
書き起こし — ストリーミング・トランスデューサと Whisper、そして WER が隠すもの
一言でいうと
ストリーミングASRは、音が入ってくる間に部分結果を直し続け、話が終わると短い仕上げのあとに確定結果を出します。オフラインASR(Whisper)は、発話を丸ごと受け取らないと始められません。精度はWER、つまり(置換 + 削除 + 挿入) ÷ 正解の単語数で測りますが、この数字は評価データがモデルの学習データとどれだけ似ているかによって大きく揺れます。
なぜ必要なのか
音声アシスタントの応答は、「話が終わった」という判定のあとに始まります(モジュール2)。その瞬間に認識結果がすでにほとんど出ていれば、すぐ次の段階へ進めますが、そこから認識を始めると、発話の長さに比例して待つことになります。この差が、ストリーミングを使う理由です。同時に、部分結果は「聞いている」というサインになり、検索を先に始める根拠にもなります。
ところが、モデルを選ぶときに最初に注目する数字であるWERは、思ったより多くのことを隠します。このイメージに入れるモデルを選んでいる途中で、実際に経験したことがあります。最初に選んだ20Mのストリーミングモデルは、公開評価では問題なかったのに、このコースのクエリを入れるとストリームの最初の1–2秒を丸ごと失いました。「How late can I cancel without paying the fee?」が「LE WITH THAT PAIN THE FEET」と出力されたのです。同じファイルを2回つなげると、2回目は正確でした。平均のWERでは見えない失敗で、モデルを変えた理由です(labs/voice/Dockerfileに記録)。
どう動くのか
トランスデューサー(RNN-T)。このイメージのストリーミングモデルは、icefallのzipformerトランスデューサーです(LibriSpeechで学習、Apache-2.0)。エンコーダーが音の断片を受け取って表現を作り、ジョイナーが「この時点で何を出力するか(または何も出力しないか)」を決めます。エンコーダーは、決まった大きさの断片と限られた左側の文脈だけを見るように学習されているため(モデル名のchunk-16・left-128)、未来を待たずに断片ごとに結果を出します。そのため部分結果が出て、最後の断片は、後ろに短い無音(このコースでは0.66秒)を入れて仕上げさせます。この仕上げは数十msで終わります。このイメージで8つの発話の「最後の音から確定まで」の中央値は44 msでした(MacのDocker、CPU 2個)。
Whisper。OpenAIのWhisper(Radford et al., 2022)は、68万時間のウェブデータで学習したエンコーダー・デコーダーモデルです。音を30秒の窓のログメルスペクトログラムに変換してエンコーダーに入れ、デコーダーが文字を1トークンずつ書きます。発話全体を見たあとに書くので、句読点や大文字小文字まで付いた結果が出ますが、話が終わらないと始められません。同じ条件で、発話1つに734 ms(中央値)かかりました。最も小さいtiny.en(3,900万パラメーター)のint8版を入れました。
部分結果は揺れます。ストリーミングモデルは、後ろの文脈を見て前の単語を直すこともあります。画面に部分結果を表示すると、ユーザーは文字が変わるのを目にします。そのため、よく「安定した前半」だけを太字で見せたり、検索のような元に戻しにくい処理は、確定結果でのみ行ったりします。
WERと正規化。WERは、単語単位の編集距離です。比較の前に必ず正規化します。大文字小文字、句読点、アポストロフィです。正規化せずに「Sunday?」と「SUNDAY」を比べると、モデルではなく表記ルールを採点することになります。コーパスWERは、ファイルごとのWERの平均ではなく、エラー数の合計 ÷ 単語数の合計です。短い発話1つの100%のエラーが、平均を台無しにしないようにするためです。
WERはデータに左右されます。このイメージで測った3つの数字を並べると、はっきりします。LibriSpeech test-cleanの8つの発話で、ストリーミングzipformerのWERは0.95%、Whisper tiny.enは6.7%でした。zipformerがまさにそのコーパスで学習したからです。ところが、合成音声で作った電話クエリ12個(モジュール9)では、同じzipformerが20%近く間違えました(「GUYS」→「GUISE」、「refill」→「ORIFYL」)。評価データが学習データに似ているほど、数字は良く見えます。モデルを選ぶときは、自分たちのサービスに似たデータで測る必要があります。
サンプルレートは正直に。sherpa-onnxは、16 kHzでない音を受け取ると、自分で変換して入力します。8 kHzの電話録音を8000だと伝えると、このラボの素材でWERは0%でしたが、16000だと偽ると、音が2倍速くなって100%が間違いでした。ノイズは違います。同じ8つの発話にホワイトノイズを混ぜると、SNR 20 dBで0%、10 dBで2.9%、0 dBで24.8%に上がりました。
現場での姿
2パス認識(two-pass)。ストリーミングモデルで部分結果をすぐに見せ、話が終わったら、より大きなオフラインモデルでもう一度書き起こして、確定結果を直す構成がよく使われます。レイテンシバジェットが許す範囲だけを、2回目のパスに使います。LabHubの会話練習のASRは、GPUに載せた大きなモデルを、録音の終わりに1回呼び出す方式で、重みが下りていると最初のリクエストが78.7秒、載っていると0.4秒でした。そのため、席に着いた瞬間に無音を送って、あらかじめ載せておきます(backend/app/lang_talk.pyのwarm_asr)。大きなモデルを使うと、「ウォームアップ」が設計の一部になります。
次のラボですること
WER計算機を作り、隠しケースで採点ツールの試験を受けます。LibriSpeechの8つの発話を100 msずつストリーミングで書き起こして、部分結果と確定遅延を記録します。同じ発話をWhisperで書き起こしてWERと遅延を比べ、8 kHzの電話帯域とサンプルレートを偽った場合、ノイズの強さによるWERを測ったあと、どの構成を選ぶかをレポートに書きます。