ESPnet による音声認識入門 ~ESPnet Model Zoo 編~

こんにちは、製品企画部リサーチャーの古谷(@kk_fry_)です。 レトリバでは、主に音声認識の研究開発を行っています。

今回から、音声認識が実行できるオープンソースのツールキット ESPnet を触ってみる記事を書いていこうと思います。 私も初めて触ります。

初回は手始めに、学習済みモデルを用いた音声認識が手軽にできる ESPnet Model Zoo を試してみたいと思います。

OS は Ubuntu 18.04 、cuda バージョンは 11.0 で実行しています。

ESPnet の概要

ESPnet の紹介はこちらの記事に詳しく書かれています。

End-to-End音声処理ツールキットESPnetの紹介

ESPnet は音声認識音声合成などのタスクに対応しているツールキットです。データの準備から学習、評価までの手順がレシピとして整備されており、設定ファイルを編集するだけで様々な条件での学習を行うことができます。

また、学習済みモデルも多数公開されており、推論だけを行うことも可能です。

そこで今回は、学習済みモデルを手軽に活用できる ESPnet Model Zoo というシステムを利用してみます。

ESPnet Model Zoo

ESPnet Model Zoo を用いると、短いスクリプトを書くだけで ESPnet の学習済みモデルを利用した推論を行うことができます。

さらに、ダウンロードした学習済みを fine-tune することや、自分で学習させたモデルを投稿することも可能です。

学習済みモデルは Zenodo community で公開されています。

学習済みモデルを用いて推論

早速、学習済みモデルを用いて音声認識をしてみましょう。

まず、PyTorch と ESPnet を pip でインストールします。

pip install torch
pip install espnet_model_zoo

続いて、学習済みモデルのダウンロードと音声認識を行うスクリプトを用意します。

今回は、日本語話し言葉コーパス(CSJ)で学習した日本語音声認識モデル kan-bayashi/csj_asr_train_asr_transformer_raw_char_sp_valid.acc.ave を使ってみます。

このモデルはサンプリングレートが 16000 Hz の音声に対応しています。

認識させたい音声のサンプリングレートが異なる場合は、sox コマンド等を用いてサンプリングレートを変更しておきましょう。

音声認識を行うスクリプトは以下のようになります。

import soundfile
from espnet_model_zoo.downloader import ModelDownloader
from espnet2.bin.asr_inference import Speech2Text

# 学習済みをダウンロードし、音声認識モデルを作成
d = ModelDownloader()
speech2text = Speech2Text(
        **d.download_and_unpack("kan-bayashi/csj_asr_train_asr_transformer_raw_char_sp_valid.acc.ave"),
        device="cuda"  # CPU で認識を行う場合は省略
    )

# 音声ファイル読み込み
speech, _ = soundfile.read("speech.wav")  # 認識させたい音声ファイルを指定

# 認識結果の取得と表示
nbests = speech2text(speech)
text, *_ = nbests[0]
print(text)

これを実行すると、認識結果が表示されます。 例として、以下の音声を認識させた結果を掲載します。 上記のスクリプトは recognize.py として保存しています。

# python3 recognize.py
親譲りの無鉄砲で子供の時から損ばかりしている

正しく認識されました。PyTorch のバージョンによっては警告が表示されるかもしれません。

では、このスクリプトの中身を見ていきましょう。

まず、音声ファイルの読み込みを行うために soundfile をインポートします。 また、事前学習モデルをダウンロードするための ModelDownloader音声認識を行うための Speech2Text もインポートします。

import soundfile
from espnet_model_zoo.downloader import ModelDownloader
from espnet2.bin.asr_inference import Speech2Text

続いて、ModelDownloader を用いて学習済みをダウンロードします。

# 学習済みをダウンロードし、音声認識モデルを作成
d = ModelDownloader()
speech2text = Speech2Text(
        **d.download_and_unpack("kan-bayashi/csj_asr_train_asr_transformer_raw_char_sp_valid.acc.ave"),
        device="cuda"  # CPU で認識を行う場合は省略
    )

ModelDownloader.download_and_unpack は、モデル名等を受け取り、必要ならモデルをダウンロードし、モデルや設定ファイルの保存先パスを dict として返すメソッドです。

download_and_unpack の返り値は次の dict になっていました。パスの途中に見られる謎の文字列は、モデル名から計算されるハッシュです。

{
    'asr_train_config': '/usr/local/lib/python3.6/dist-packages/espnet_model_zoo/b54054d8e1cec327cdaa8a114953ed97/exp/asr_train_asr_transformer_raw_char_sp/config.yaml',
    'lm_train_config' : '/usr/local/lib/python3.6/dist-packages/espnet_model_zoo/b54054d8e1cec327cdaa8a114953ed97/exp/lm_train_lm_char/config.yaml',
    'asr_model_file'  : '/usr/local/lib/python3.6/dist-packages/espnet_model_zoo/b54054d8e1cec327cdaa8a114953ed97/exp/asr_train_asr_transformer_raw_char_sp/valid.acc.ave_10best.pth',
    'lm_file'         : '/usr/local/lib/python3.6/dist-packages/espnet_model_zoo/b54054d8e1cec327cdaa8a114953ed97/exp/lm_train_lm_char/31epoch.pth'
}

音響モデルと言語モデルのパラメータを記述する config ファイルとそれぞれのモデルファイルのダウンロード先パスが返ってきています。

これらの情報を与えて Speech2Text クラスをインスタンス化します。

音響モデルと言語モデルの重みや、ビームサーチの幅など、デコーディングに関連するパラメータを指定したい場合は、Speech2Text クラスのコンストラクタに渡すことができます。

最後に、音声を読み込み、Speech2Text オブジェクトに認識させます。

# 音声ファイル読み込み
speech, _ = soundfile.read("speech.wav")  # 認識させたい音声ファイルを指定

# 認識結果の取得と表示
nbests = speech2text(speech)
text, *_ = nbests[0]
print(text)

Speech2Text.__call__(speech) は、引数として与えられた音声を音声認識モデルに入力し、ビームサーチによって N-best を計算します。

N-best とは、認識結果の候補を良い方から N 個選んだものです。この個数 N は Speech2Text のコンストラクタの引数 nbest で指定することができます。 nbest のデフォルト値は 1 です。

したがって、今回はデフォルト状態なので、最も良い候補のみを取得することになります。

1 つの認識結果候補は、テキスト、トークン列、トークン ID 列、Hypothesis オブジェクトの 4 つの値の組として得られます。

テキストは認識結果の文字列、トークン列は認識結果のトークン(今回だと文字)の配列、トークン ID 列は各トークンに対応する ID の値の列です。

Hypothesis オブジェクトは、音響モデルと言語モデルのスコアなどの細かい情報も含む、認識結果全体を表すオブジェクトです。

nbests[0] の内容は以下のようになりました。

(
    '親譲りの無鉄砲で子供の時から損ばかりしている',
    ['親', '譲', 'り', 'の', '無', '鉄', '砲', 'で', '子', '供', 'の', '時', 'か', 'ら', '損', 'ば', 'か', 'り', 'し', 'て', 'い', 'る'],
    [333, 1407, 28, 2, 332, 910, 2088, 4, 100, 271, 2, 52, 19, 29, 1241, 69, 19, 28, 11, 7, 3, 21],
    Hypothesis(
        yseq=tensor([3261, 333, 1407, 28, 2, 332, 910, 2088, 4, 100, 271, 2, 52, 19, 29, 1241, 69, 19, 28, 11, 7, 3, 21, 3261]),
        score=tensor(-93.7444),
        scores={
            'decoder': tensor(-1.6280),
            'ctc': tensor(-0.2589),
            'lm': tensor(-92.8010)
        },
        states={'decoder': [tensor([[ ... ]])]}
    )
)

Hypothesis.yesq が認識結果のトークン ID 列を表していますが、最初と最後に発話開始と終了を表す特殊トーク3621 が付いています。

今回はテキストだけ欲しいので、nbests[0] の最初の要素のみを取得しています。

ということで、ESPnet Model Zoo を用いて学習済みモデルによる音声認識の実行ができました。

おわりに

今回は ESPnet Model Zoo を使って手軽に音声認識を試してみました。

あまりにも手軽すぎて "やった感" に乏しいので、次回は ESPnet 本体をインストールして学習済みモデルによる音声認識を実行してみたいと思います。