はじめに
こんにちは、AIチームの大竹です。
最近、高性能な日本語音声認識モデルのリリースが相次いでいます。普段、音声認識を用いたプロダクト開発をしている中で、各モデルの音声認識性能や推論速度がどれくらいのものなのか気になったので簡単な実験をして性能を比較してみました。
書き起こしや評価周りの実装も記載しているので参考にしていただけたら幸いです。
モデルの直近のリリースをまとめると、以下のようになっています。ReazonSpeechコーパスのリリースを皮切りに日本語に特化した音声認識モデルの開発の勢いが加速しているように思えます。ReazonSpeechコーパスは、地上波テレビ放送から収集された音声に基づいて構築されています。v1では19,000時間、v2では35,000時間の音声が含まれていて、日本語音声認識モデルの学習リソースとしては世界一の規模となっています。
公開時期 | モデル名 | 公開元 |
---|---|---|
2024/4 | kotoba-whisper | Kotoba Technologies |
2024/2 | ReazonSpeech v2 | |
2023/12 | nue-asr | rinna株式会社 |
2023/1 | ReazonSpeech v1 | Reazon Human Interaction Lab |
比較対象モデル
今回の検証で比較するモデルは以下のようにしたいと思います。
モデル名 | 説明 |
---|---|
蒸留技術を用いてopenai/whisper-large-v3をもとに作成されたモデル。学習データセットは 1,253時間のReazonSpeechコーパスのサブセット。 | |
reazonspeech-nemo | NeMoのスクリプトを用いて35,000時間に拡張されたReazonSpeechコーパスでスクラッチから訓練されたモデル。アーキテクチャはRNN-T。 |
reazonspeech-espnet | ESPNetのスクリプトを用いて35,000時間に拡張されたReazonSpeechコーパスでスクラッチから訓練されたモデル。アーキテクチャはHybrid CTC/Attention。 |
nue-asr | 自己教師あり学習で事前学習された音声モデルとLLMを統合し、19,000時間のReazonSpeechコーパスを用いてファインチューニングしたモデル。 |
reazonspeech-nemoとreazonspeech-espnetは前述した表のReazonSpeech v2に該当します。
これらのモデルはいずれもReazonSpeechコーパスを用いて訓練されていますが、訓練手法およびモデルのアーキテクチャが異なるので書き起こし結果に特徴があると思います。
実験条件
今回の検証では以下の条件で実験を進めていきます。
- Vertex AIで以下の環境のインスタンスを確保
- CPU: n1-standard-8(8 vCPU、4 コア、30 GB メモリ)
- GPU: T4 1枚
- CUDA: 11.8
- その他の条件
- バッチサイズ1で推論
- コードは公開されているものをそのまま使用する
- 推論の際の数値のデータ型は公開されているコードおよびモデルに準拠する
環境構築
データセットのダウンロード
今回の検証では日本語の音声認識評価のためにTEDxJP-10kというデータセットを用います。TEDxJP-10K は、TEDxの日本語プレゼンテーションを含むYouTubeのプレイリスト「TEDx talks in Japanese」から選ばれた音声と字幕データを利用して構築されたデータセットです。このデータセットには、合計で8.8時間の音声が含まれ、異なる性別・年齢・出身の273名の話者の声が収録されています。
どのモデルの訓練データにも含まれていないドメインの音声なので今回のような異なる複数のモデルを評価する際に有用だと思います。
公式リポジトリにしたがってデータセットをダウンロードして整形します。想定通り処理が実行されると、TEDxJP-10K_v1.1
というディレクトリが生成されます。
ディレクトリ直下のsegments
というテキストファイルを用いて、長い音声ファイルをセグメント分割します。(この処理はリポジトリの指示に記載されていないので別途実行する必要があます。)
ライブラリのインストール
name="python環境の名前"
conda create -n $name -y python=3.10 poetry ffmpeg
poetry install # pyproject.tomlを事前に用意してください
pyproject.tomlのdependenciesは以下のようになっています。
[tool.poetry.dependencies]
python = "3.10"
torch = { url = "https://download.pytorch.org/whl/cu118/torch-2.3.0%2Bcu118-cp310-cp310-linux_x86_64.whl#sha256=ceb81d79fc7b017f51b1613f83b878efb8974cc946631fb9cd577bbaa2873293" }
torchvision = { url = "https://download.pytorch.org/whl/cu118/torchvision-0.18.0%2Bcu118-cp310-cp310-linux_x86_64.whl#sha256=86b2970a75316a90e00ac66863b5414269ffd3e8c58683f1f40128da989285b6" }
torchaudio = { url = "https://download.pytorch.org/whl/cu118/torchaudio-2.3.0%2Bcu118-cp310-cp310-linux_x86_64.whl#sha256=f21f08ebf87510a09f4ee3698997eeec4b8f2520e95218defbcd88e5c28d5112" }
pandas = "^2.2"
librosa = "^0.10.2.post1"
jiwer = "^3.0.4"
mecab-python3 = "^1.0.9"
ruff = "^0.4.7"
num2words = "^0.5"
neologdn = "^0.5"
今回の検証では依存関係の解決のため、モデルごとに異なる仮想環境を作ります。
kotoba-whisper
pip install --upgrade transformers accelerate
reazonspeech-espnet
git clone https://github.com/reazon-research/ReazonSpeech
pip install ReazonSpeech/pkg/espnet-asr
reazonspeech-nemo
git clone https://github.com/reazon-research/ReazonSpeech
pip install Cython
pip install ReazonSpeech/pkg/nemo-asr
nue-asr
pip install git+https://github.com/rinnakk/nue-asr.git
書き起こし
今回の実験で使用するコードは基本的に各ライブラリでtutorial用に公開されているコードをそのまま使用したいと思います。データ型の設定なども公開されているものにします。
以下にReazonSpeechを用いた書き起こしコードを記載します。モデルのロードと推論の部分をモデルによって変更する必要があるので注意してください。kotoba-whisperとnue-asrの推論コードについては後述します。
import time
from tqdm import tqdm
wav_dir = Path('/path/to/tedxjp')
def calc_duration(file_id):
wav_path = wav_dir / f'{file_id}.wav'
audio, sr = sf.read(wav_path)
return len(audio) / sr
def main():
#######################################################
# モデルごとに記述を変える部分
from reazonspeech.espnet.asr import load_model, transcribe, audio_from_path
model = load_model()
#######################################################
df = pd.read_csv(data_dir / 'text', sep=' ', header=None, names=['id', 'text'])
df['duration'] = df['id'].apply(calc_duration)
id2text = df[['id', 'text']].set_index('id').to_dict()['text']
paths2audio_files = [str(wav_dir / f'{file_id}.wav') for file_id in id2text.keys()]
results = []
tic = time.perf_counter()
for path in tqdm(paths2audio_files):
#######################################################
# モデルごとに記述を変える部分
audio = audio_from_path(path)
ret = transcribe(model, audio)
results.append({
'gt_text': id2text[Path(path).stem],
'pred_text': ret.text,
},
)
#######################################################
toc = time.perf_counter() - tic
pd.DataFrame(results).to_csv('results_reazon-nemo.csv', index=False)
print(f'Elapsed time: {toc:.2f} seconds')
print(f'RTF: {toc / df["duration"].sum():.3f}')
モデルごとに記述を変える必要がある部分は以下のコードで置き換えてください。
kotoba-whisper
モデルのロード
model_id = "kotoba-tech/kotoba-whisper-v1.0"
torch_dtype = torch.bfloat16 if device == 'cuda' else torch.float32
model_kwargs = {"attn_implementation": "sdpa"} if device == 'cuda' else {}
model = pipeline(
"automatic-speech-recognition",
model=model_id,
torch_dtype=torch_dtype,
device=device,
model_kwargs=model_kwargs
)
generate_kwargs = {"language": "japanese", "task": "transcribe"}
推論
ret = model(path, generate_kwargs=generate_kwargs)
results.append({
'gt_text': id2text[Path(path).stem],
'pred_text': ret['text'],
},
)
nue-asr
モデルのロード
model = nue_asr.load_model("rinna/nue-asr")
tokenizer = nue_asr.load_tokenizer("rinna/nue-asr")
推論
ret = nue_asr.transcribe(model, tokenizer, path)
results.append({
'gt_text': id2text[Path(path).stem],
'pred_text': ret.text,
},
)
評価
今回の実験では、TEDxJP-10kデータセットと3つの評価指標を用いて音声認識モデルの性能を評価していきたいと思います。
評価指標
- CER (Character Error Rate) :書き起こし結果の文字レベルの誤り率
- WER (Word Error Rate):書き起こし結果の単語レベルの誤り率
- RTF (Real Time Factor):1秒の音声を書き起こすのにどれくらいの時間がかかる実験
評価コード
今回用いた評価コードです。
書き起こしのコードを実行して得られたcsvファイルを評価コードの入力として渡します。
まず、書き起こされたテキストと正解テキストの正規化を行うためにnormalizeという関数を作用させます。normalizeでは以下の処理を行ってテキストを正規化しています。
- こちらで定義されている特殊文字(句読点、!?など)を削除
- UTF-16のラテン文字をASCIIに変換
- 数値を日本語へ変換 (1→一, 12→十二など)
正規化を行った後、jiwerというライブラリを用いて正解テキストと書き起こしテキストとの間の編集距離を計算し、WERおよびCERを算出しています。日本語の音声認識性能はCERを用いて評価することが一般的ですが、今回はmecabを用いて分かち書きをした上でWERの計算を行っています。
import pandas as pd
import jiwer
from argparse import ArgumentParser
import MeCab
from dataclasses import dataclass
from num2words import num2words
import re
import neologdn
symbols = [
"、",
"。",
" ",
"!"
... # 省略
]
_SPECIALS = {ord(c): "" for c in symbols}
def normalize(utt_txt):
"""Normalize text.
Use for Japanese text.
Args:
utt_txt: String of Japanese text.
Returns:
utt_txt: Normalized
"""
# trim non-phonatory symbols in the text
utt_txt = utt_txt.translate(_SPECIALS)
# convert UTF-16 latin chars to ASCII
utt_txt = neologdn.normalize(utt_txt)
# replace all the numbers
numbers = re.findall(r"\d+\.?\d*", utt_txt)
transcribed_numbers = [num2words(item, lang="ja") for item in numbers]
for nr in range(len(numbers)):
old_nr = numbers[nr]
new_nr = transcribed_numbers[nr]
utt_txt = utt_txt.replace(old_nr, new_nr, 1)
return utt_txt
@dataclass
class TotalErrors:
deletions: int = 0
insertions: int = 0
substitutions: int = 0
distance: int = 0
length: int = 0
@property
def error_rate(self):
return self.distance / self.length
def main():
parser = ArgumentParser()
parser.add_argument("input", type=str, help="Path to the result csv file")
args = parser.parse_args()
df = pd.read_csv(args.input)
mecab = MeCab.Tagger("-Owakati")
total_word_errors = TotalErrors()
total_char_errors = TotalErrors()
for row in df.itertuples():
gt_text = normalize(row.gt_text)
if pd.isna(row.pred_text):
asr_text = ""
else:
asr_text = normalize(row.pred_text)
gt_words = mecab.parse(gt_text)
asr_words = mecab.parse(asr_text)
word_errors = jiwer.process_words(gt_words, asr_words)
total_word_errors.deletions += word_errors.deletions
total_word_errors.insertions += word_errors.insertions
total_word_errors.substitutions += word_errors.substitutions
distance = (
word_errors.deletions + word_errors.insertions + word_errors.substitutions
)
total_word_errors.distance += distance
total_word_errors.length += len(gt_words.split())
char_errors = jiwer.process_characters(gt_text, asr_text)
total_char_errors.deletions += char_errors.deletions
total_char_errors.insertions += char_errors.insertions
total_char_errors.substitutions += char_errors.substitutions
distance = (
char_errors.deletions + char_errors.insertions + char_errors.substitutions
)
total_char_errors.distance += distance
total_char_errors.length += len(gt_text)
print("----------- Word Error Rate -----------")
print(f"WER: {total_word_errors.error_rate:.3f}")
print("deletions:", total_word_errors.deletions)
print("insertions:", total_word_errors.insertions)
print("substitutions:", total_word_errors.substitutions)
print("---------- Character Error Rate ----------")
print(f"CER: {total_char_errors.error_rate:.3f}")
print("deletions:", total_char_errors.deletions)
print("insertions:", total_char_errors.insertions)
print("substitutions:", total_char_errors.substitutions)
評価結果
model | WER | CER | RTF |
---|---|---|---|
ReazonSpeech v2 (ESPNet) | 0.111 | 0.093 | 0.335 |
ReazonSpeech v2 (NeMo) | 0.122 | 0.104 | 0.203 |
kotoba-whisper (transformers) | 0.124 | 0.104 | 0.379 |
nue-asr (transformers) | 0.177 | 0.166 | 0.142 |
CERやWERでは、ESPNet→NeMo≒kotoba-whisper→nue-asrという順に、
推論速度(RTF)ではnue-asr→NeMo→ESPNet→kotoba-whisperという順に性能が良い結果となりました。
次に、いくつかのサンプルをピックアップして各モデルの書き起こし結果を比較したいと思います。
Ground Truth | kotoba-whisper (transformers) | nue-asr (transofrmers) | ReazonSpeech v2 (ESPNet) | RezonSpeech v2 (NeMo) |
---|---|---|---|---|
いいんじゃないかしらと思ったんです | いいんじゃないかしらと思ったんです | 思ったんです。 | いいんじゃないかしらと思ったんです。 | いいんじゃないかしらと思ったんです。 |
それがまあ応用が出てきて | それが応用が出てきて | それが応用が出てきて。 | それがまあ応用が出てきて。 | それがまあ応用が出てきて。 |
え福島ってこんなふうなの | 福島ってこんなふうなの? | 福島ってこんなふうなの? | えっ福島ってこんなふうなの? | えっ福島ってこんなふうなの? |
nue-asrは書き起こし結果が正解テキストと比べて短くなる傾向があることがわかります。フィラーや感嘆符の削除だけでなく、発話の一部が無視されてしまうような挙動があります。kotoba-whisperはフィラーや感嘆符を書き起こさず、文として簡潔に書き起こせています。対して、ReazonSpeechのモデルは音に忠実にフィラーや感嘆符を書き起こしている傾向があります。
このようにモデルごとに書き起こしに特徴があるのは面白いですね。
また、今回の検証では、faster-whisperというwhisperの高速推論を可能にするライブラリを用いることでkotoba-whisperがどの程度高速化されるのかについても試してみました。ベースラインとしてオリジナルのOpenAI whisperに対する性能も測定していいます。
model | WER | CER | RTF |
---|---|---|---|
kotoba-whisper (transformers) | 0.124 | 0.104 | 0.379 |
kotoba-whisper (faster-whisper, fp16) | 0.153 | 0.133 | 0.087 |
whisper-large-v3 (faster-whisper, fp16) | 0.152 | 0.136 | 0.228 |
faster-whisperを用いることで、速度を約4~5倍に向上させることが可能です。オリジナルのモデルより性能が低下してしまうものの、whisper-large-v3と比較して性能に大きな差がないのでかなり有用なのではないでしょうか。
終わりに
この記事では、オープンソースの日本語End-to-End音声認識モデルの性能を評価しました。
性能を比較したところ、どのモデルも音声認識性能が高く、CERやWERだけではその性能を十分に評価することが難しくなってるなと感じます。そのため、定性的な分析を行い、実際の使用シナリオに適した書き起こしをしてくれるモデルを選択することが重要だと改めて思いました。
今後も引き続き、日本語音声認識技術の発展に注目し、プロダクトへの適用可能性を探っていきたいと思います。