TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

WhisperのモデルをONNXにする その2

前回、WhisperのモデルをONNXにする方法について記述した。

Whisperのモデルは、単体では音声認識はできず、音声をメルスペクトログラムにする前処理と、トークンをデコードして文字列にする後処理が必要になる。
今回は、前処理と後処理をC#で実装する方法について記述する。

音声認識の流れ

以下のような流れで音声をテキストに変換する。

  1. 音声を16kHzにリサンプリングする
  2. 対数メルスペクトログラムに変換する
  3. ONNXモデルで推論する
  4. 推論結果をトークン列にする
  5. トークン列をデコードしてUTF8の文字列にする

以下ではそれぞれの処理の実装方法について記述する。

音声を16kHzにリサンプリングする

Whisperでは、ffmpeg-pythonを使用してリサンプリングしている。
.NETでffmpegを扱う良いライブラリがなかったので、.NETのオーディオライブラリのNAudioを使用して、音声をリサンプリングした。

        using (var reader = new AudioFileReader("a.wav"))
        {
            var resampler = new WdlResamplingSampleProvider(reader, SAMPLE_RATE).ToMono();
            resampler.Read(audio, 0, audio.Length);
        }

対数メルスペクトログラムに変換する

メルスペクトログラムは、音声をフーリエ変換したスペクトルを2乗した、パワースペクトルメル尺度を適用したものである。

WhisperのPython実装では、フーリエ変換にPyTorchのtorch.stftを使用しているが、C#では別の方法で実装が必要である。
NAudioにもFFTのメソッドがあるが、フレームのサンプル数が2のべき乗という制約がある。Whisperは1フレームが400サンプルのため使用できない。
そのため、.NETの数値ライブラリのMathNet.Numericsを使用した。

窓関数

Whisperでは、窓関数には、ハン窓が使用されている。
MathNet.Numericsの、Window.Hannで実装した。

パディング

時刻0をフレームの中心にして、スライディングウィンドウで、フレームを切り出す際に、両端でパディングが必要になる。
Whisperでは、パディングの方法には、reflectが使用されている。
ライブラリがないため、スクラッチで実装した。

フーリエ変換

MathNet.NumericsのFourier.Forwardを使用して実装した。
サンプル数でスケーリングは行わない。

Fourier.Forward(frame, FourierOptions.NoScaling);
メル尺度の適用

メル尺度の係数は、whisper/assets/mel_filters.npzに保存されている。
C#で読み取れるように、PythonのNumpyでRawデータにしてバイナリファイルにしておき、それを読み込むようにした。

対数変換

対数にした後に、スケールの変換を行っているので、Whisperの実装と同じに実装した。

        logSpec = logSpec.PointwiseMaximum(1e-10).PointwiseLog10();
        logSpec = logSpec.PointwiseMaximum(logSpec.Enumerate().Max() - 8.0);
        logSpec = (logSpec + 4.0) / 4.0;

前処理は、以上である。

推論

ONNXRuntimeを使用して、推論を行う。
推論の箇所は、言語の自動判定と、メインループの2か所にある。

メインループでは、推論結果に対して、いくつか特定のトークンを抑止するためのマスク処理がある。
Pythonの実装をそのまま移植して実装した。

推論結果をトークン列にする

推論結果は、トークンIDごとの確率として出力されるので、確率が最大となるトークンIDを見つけて、トークン列とする。

トークン列は、そのままでは可読可能な文字列ではないため、デコード処理が必要である。

トークンのデコード

Whisperでは、文字列のトークン化に、バイトレベルBPEという方法が使われている。
その処理には、transformersライブラリのGPT2TokenizerFastが使用されている。

デコード処理の内部は、Rustで実装されているため、Rustのコードを読み解いてC#で同様の処理を実装した。

処理は単純で、

  1. UTF8の各バイトを1文字に変換するルールに従った対応表
  2. トークンIDから対応表のキー列となる文字列に変換する対応表

の2つの対応表を使って、トークン列からUFT8のバイト列に変換を行う。
1.の対応表は、Rustの実装C#に移植した。
2.の対応表は、whisper/assets/multilingual/vocab.jsonに保存されているjsonを使う。

以上で、音声からテキストに変換できる。

テスト

C#で実装した処理で、音声ファイルをテキストに変換できるか確認した。

「これはテストです。」とマイクで録音した音声ファイルを使って、言語が正しく認識されて、日本語テキストに変換できることを確認した。

>dotnet run a.wav
ja
これはテストです。

Whisperのtestsにあるjfk.flacでも試した。

>dotnet run jfk.flac
en
 And so my fellow Americans ask not what your country can do for you, ask what you can do for your country.

まとめ

C#で、ONNXにしたWhisperのモデルを使用して、音声をテキストに変換する処理の実装方法について記述した。
Pythonなしで実装できたことで、スタンドアロンのアプリに組み込みしやすくなる。

まだモデルの最適化を行っていないため、FP16化や、プルーニング(枝刈り)も試してみたい。

また、ONNXRuntimeはWebAssemblyでも動かせるため、ブラウザで動くデモを作成してみたい。

続き
tadaoyamaoka.hatenablog.com

2023/6/3 追記

ONNXに変換するソースを公開した。
github.com