コンテンツにスキップ

第8回 ニューラルASRモデル — Conformer / WhisperによるEnd-to-End認識

この回の資料


本文の項目番号とHTMLスライドのページ番号は異なる。

本日の到達目標

  1. End-to-Endモデルの概念と従来型との違いを説明できる
  2. CTC・Attention-based Encoder-Decoderの仕組みを理解する
  3. ConformerとWhisperのアーキテクチャ的特徴を説明できる
  4. OpenAI Whisperを使って実際に音声認識を行える
  5. 発展:CTCが「同じ答えになる経路の和」であること、アテンションが重み付き平均であることを、式から自分で説明できる

進化の視点:学習量を比較するときに何をそろえるか

Conformerはエンコーダの構造であり、名前だけでは学習データ量は決まらない。原論文はLibriSpeechの約1,000時間規模のデータで評価した。一方、2022年のWhisperは680,000時間の音声とテキストの対応を利用して学習した(Conformer原論文、Whisper原論文)。

人間の言語獲得との比較では、音声の総時間に加え、同じ入力を何回使うか、文字起こしや視覚・対話の手がかりを使うか、何を達成したと評価するかをそろえる必要がある。時間の差だけから、生得的な仕組みや進化の過程について結論を出すことはできない。

技術への接続:少ない文字起こし付き音声で認識モデルを作るには、第7回の自己教師あり事前学習を利用する方法などがある。ただし、追加学習のラベル量と、事前学習を含む総データ量は区別する。

問い:モデルと子どもの学習効率を比べるなら、入力・反復回数・評価課題をどのようにそろえるべきか。


項目1: End-to-Endモデルとは

従来型との比較

項目 従来型(GMM-HMMなど) End-to-Endの代表例
構成 音響モデル・発音辞書・言語モデルをデコーダで統合 入力から出力列を学ぶニューラルモデル。複数モジュールを含む
入力 MFCCなどの音響特徴量 波形やログメル特徴量など
出力 複数のモデルを使って文字・単語列を探索 文字・サブワードなどの列を推定
学習 モデルごとの学習や段階的な学習を組み合わせる 入力音声と正解テキストの対応から、系列の損失で学ぶ
時間位置のラベル HMM内で推定する方法や、強制アライメントを利用する方法がある CTC・AEDでは各文字の時間位置を事前に与えず学習できる

End-to-End学習の要点

音声とテキストのペアから、認識の目的に沿ってモデルを学習できる。特徴量抽出・エンコーダ・デコーダなどに分かれていてもEnd-to-Endモデルと呼べる。外部の言語モデルや辞書を併用する構成もある。

「アライメント不要」は、正解テキストの各文字に時間位置を手作業で付けなくてよいという意味であり、音声と出力列の対応関係が存在しないという意味ではない。また、従来のHMMも学習中に対応を推定できる。

代表的なEnd-to-Endアーキテクチャ

  1. CTC(Connectionist Temporal Classification)
  2. Attention-based Encoder-Decoder(AED)
  3. RNN-T(Transducer)
  4. これらのハイブリッド

項目2: CTC(Connectionist Temporal Classification)の仕組み

問題:長さが合わない

エンコーダ出力の時刻数\(T\)(例:300)と、出力ラベル数\(L\)(例:20文字)は一般に一致しない。 どのフレームがどの文字に対応するかも事前には不明(アライメント問題)。

CTCは、時間順序を保つ複数の対応経路をまとめて扱う。

CTCの核心:ブランクラベルと多対一マッピング

出力アルファベットにブランクラベル \(\epsilon\) を追加する。

CTCは各時刻でラベル(文字など)とブランクの確率を出す。1本の出力パスをテキストへ変換するときは、次の順序で処理する:

  1. 隣り合う同じラベルをまとめる:aaa → a
  2. ブランクを除去する:a_b__b → a_b_b → abb(_は\(\epsilon\))

ブランクを挟んだ2つのbは、別のラベルとして残る。ブランクは「ここではラベルを出さない」という記号で、無音を直接意味するものではない。

例: 目標文字列 "cat"

あり得るCTC出力パス(すべて "cat" に対応):
  c c a a t t (繰り返し除去→cat)
  c _ a _ t _ (ブランク除去→cat)
  _ c a t _ _ (ブランク除去→cat)
  c _ a a t t (繰り返し+ブランク除去→cat)

CTC損失関数

すべての有効なパスの確率の和を最大化する:

\[\mathcal{L}_{\text{CTC}} = -\log P(Y \mid X) = -\log \sum_{\pi \in \mathcal{B}^{-1}(Y)} \prod_{t=1}^{T} p(\pi_t \mid X)\]

前向き・後ろ向きアルゴリズムで効率的に計算できる(Graves et al., 2006)。有効なパスが存在するには、\(T\)が正解ラベル列を配置できる長さである必要がある。同じラベルが連続する箇所には、その間のブランク分の時刻も必要になる。

各時刻の最大確率ラベルをつなぐ貪欲復号は、最良の1本のパスを選ぶ近似である。複数パスの和が最大のラベル列と一致するとは限らない。

CTCの特徴

  • 特徴:文字ごとの時間ラベルを与えず、単調なアライメントを学習できる
  • ストリーミング:未来の入力を必要としない、または先読みを制限したエンコーダと組み合わせれば利用できる。CTCだけで実現するわけではない
  • 制約:入力\(X\)を条件とした各時刻のラベル出力を独立に扱い、既に出した文字列を明示的には条件にしない。必要に応じて外部言語モデルを組み合わせる
導出①:CTC は「同じ答えになる経路を全部足す」ことでアライメントを消している

問題は、どのフレームがどの文字に対応するか分からないことだった。CTC はこれを決めないことで解決する。

フレームごとの出力を並べた \(\pi = (\pi_1, \ldots, \pi_T)\) を1本の経路と呼ぶ。入力\(X\)を条件として各時刻のラベル出力が互いに独立だと仮定すると、経路1本の確率は積で書ける。

\[P(\pi \mid X) = \prod_{t=1}^{T} p(\pi_t \mid X)\]

ここで \(\mathcal{B}\) を「繰り返しをまとめ、ブランクを除く」変換とする。目標 \(Y\) になる経路は1本ではなく、\(\mathcal{B}^{-1}(Y)\) という集合をなす(上の cat の例のように何通りもある)。これらは互いに排反な出力の仕方なので、目標が出る確率はそれらの和である。

\[P(Y \mid X) = \sum_{\pi \in \mathcal{B}^{-1}(Y)} \prod_{t=1}^{T} p(\pi_t \mid X)\]

損失はこの対数を取って符号を変えただけである。

なぜこれで位置を教えなくてよいのか。 学習時に上げているのは「特定の1本の経路の確率」ではなく「正しい答えになる経路すべての確率の合計」である。どの経路で達成するかはモデルに任されている。 だから正解ラベルに時間位置を書き込む必要がない。これが「アライメント不要」の正体である。

出力アルファベットから長さ\(T\)の全パスを列挙すると、その数は\(T\)について指数的に増える。一方、正解になるパスの確率の和は、前向き・後ろ向きアルゴリズムで効率よく計算できる。第7回の導出②と同様に、経路をまとめる漸化式を使う。CTCでは正解ラベル数\(L\)も計算量に関わり、標準的な動的計画法は\(O(TL)\)となる。

積に分解した1行目が、独立の仮定そのものである

経路の確率を積に分解したところで、入力\(X\)を条件としたラベル出力の独立性を仮定している。音声フレームそのものが独立という仮定ではない。 エンコーダは入力の前後関係を利用できるが、自己回帰デコーダのように過去の出力ラベルを明示的に条件とする構造は持たない。

使った数学:独立な事象の積の法則と、排反な事象の確率の和(統計学Ⅱ第2回)。


項目3: Attention-based Encoder-Decoder

アーキテクチャ概要

ここでは全音声を参照するエンコーダと、出力履歴を使う自己回帰デコーダの例を示す。

[Encoder]                          [Decoder]
音声特徴量列                          テキスト列(自己回帰)
x₁, x₂, ..., xT                     y₀(BOS), y₁, y₂, ...
      ↓                                    ↓
  Encoder                         Decoder(Causal)
  (双方向)                        ← Cross-Attention ←┐
  h₁, h₂, ..., hT ────────────────────────────────────┘
                                         ↓
                                    y₁, y₂, ..., yL (EOS)

Attention機構

デコーダの各ステップで、エンコーダ出力のどの部分をどの程度参照するかを動的に決める。以下は再帰型デコーダでの基本的な定式化。Transformer型ではQuery・Key・Valueを使う。

\[e_{ij} = \text{score}(\mathbf{s}_{i-1}, \mathbf{h}_j)\]
\[\alpha_{ij} = \frac{\exp(e_{ij})}{\sum_{k=1}^{T}\exp(e_{ik})}\]
\[\mathbf{c}_i = \sum_{j=1}^{T} \alpha_{ij} \mathbf{h}_j\]
  • \(\mathbf{s}_{i-1}\): デコーダの前ステップの隠れ状態
  • \(\mathbf{h}_j\): エンコーダの\(j\)番目の出力
  • \(\alpha_{ij}\): アテンションウェイト(どのフレームに注目するか)

AEDの特徴

  • メリット: 長距離依存関係を捉えやすい、言語的な文脈を自然に組み込める
  • 制約:自己回帰の推論では出力を順に生成する。Transformer型は学習時に正解の出力履歴を与えることで、出力位置を並列に処理できる
  • 全音声を参照する構成は、そのままでは逐次入力向けでない。ストリーミングには参照範囲や先読みを制限する設計が必要

項目4: Transformerの復習(Self-Attention)

系列内の位置を直接結び付ける

RNNでは、同じ層の状態を時間順に更新するため、時刻方向の依存がある。Self-Attentionは、各位置から他の位置への重みを直接計算できる。学習時には多くの位置の計算を並列に行えるが、自己回帰推論の出力生成は順番に進む。

Self-Attentionの計算

入力列 \(\mathbf{X} \in \mathbb{R}^{T \times d}\) を Query, Key, Value に線形変換:

\[\mathbf{Q} = \mathbf{X}\mathbf{W}_Q, \quad \mathbf{K} = \mathbf{X}\mathbf{W}_K, \quad \mathbf{V} = \mathbf{X}\mathbf{W}_V\]

アテンションの計算:

\[\text{Attention}(\mathbf{Q}, \mathbf{K}, \mathbf{V}) = \text{softmax}\left(\frac{\mathbf{Q}\mathbf{K}^\top}{\sqrt{d_k}}\right)\mathbf{V}\]

音声での課題

  • 音声フレーム数は多い(100フレーム/秒 × 10秒 = 1000フレーム)
  • 全位置を比較するSelf-Attentionは、特徴次元を固定すると計算量が\(O(T^2)\)。長い音声ではコストが高い
  • Conformerも大域的Self-Attentionを使うため、この二乗の計算量を解消する方式ではない
  • Conformerは畳み込みを加えて局所的な構造を扱う。サブサンプリングで\(T\)を減らす工夫も使う(原論文)
導出②:アテンションの正体は「重み付き平均」である

項目3の式を分解する。まず重みを作る部分はソフトマックスである。

\[\alpha_{ij} = \frac{\exp(e_{ij})}{\sum_{k=1}^{T}\exp(e_{ik})}\]

ここではマスクなし・有限のスコア・ドロップアウト前の重みを考える。指数関数は必ず正なので\(\alpha_{ij}>0\)。また分母が分子の総和そのものなので、

\[\sum_{j=1}^{T} \alpha_{ij} = \frac{\sum_{j}\exp(e_{ij})}{\sum_{k}\exp(e_{ik})} = 1\]

正で、足して1。つまり \(\alpha_{i1}, \ldots, \alpha_{iT}\) は確率と同じ形をしている。 そしてこれを値ベクトルに掛けて足したものが出力だった。

\[\mathbf{c}_i = \sum_{j=1}^{T} \alpha_{ij}\, \mathbf{h}_j\]

これは \(\mathbf{h}_1, \ldots, \mathbf{h}_T\) の加重平均にほかならない。重みがすべて等しければただの平均になり、1か所が1に近づき残りが0に近づけば、その位置の値を取り出す操作に近づく。アテンションは、この両極のあいだを連続的に動ける「やわらかい選択」である。 行列で書いた\(\text{softmax}(\mathbf{Q}\mathbf{K}^\top/\sqrt{d_k})\mathbf{V}\)も、行ごとに見れば同じことをしている。これは各ヘッドの値ベクトルの加重平均であり、出力射影や残差接続まで含むブロック全体が単純な平均という意味ではない。

使った数学:指数関数が正であること、総和の約分。

導出③:なぜ \(\sqrt{d_k}\) で割るのか

重みのもとになる値は、クエリとキーの内積である。

\[\mathbf{q}\cdot\mathbf{k} = \sum_{m=1}^{d_k} q_m k_m\]

\(q_m,k_m\)の全成分が互いに独立で、平均0・分散1だと仮定すると、各項 \(q_m k_m\) の分散は1になる。独立な確率変数の和の分散は加法的(統計学Ⅰ第6回)なので、

\[\mathrm{Var}[\mathbf{q}\cdot\mathbf{k}] = \sum_{m=1}^{d_k} \mathrm{Var}[q_m k_m] = d_k\]

標準偏差は \(\sqrt{d_k}\) である。次元が大きいほど内積は大きく振れる。

振れが大きいと何が困るか。ソフトマックスは入力の差が大きいほど、最大の要素の重みが1に、残りが0に近づく。ほぼ1か0に張り付いた状態では、入力を少し動かしても出力がほとんど変わらない。そのため、ソフトマックスを通る勾配が小さくなり、学習しにくくなる場合がある。

そこで \(\sqrt{d_k}\) で割り、次元によらずスケールを一定に戻す。

\[\mathrm{Var}\!\left[\frac{\mathbf{q}\cdot\mathbf{k}}{\sqrt{d_k}}\right] = \frac{d_k}{d_k} = 1\]

数値で確かめるなら:独立な標準正規乱数で内積を繰り返し作ると、\(d_k=8,64,512\)のとき、割る前の分散はそれぞれ\(8,64,512\)に、割った後は1に近づく。有限回の実験では値は変動する。

実際に学習されたQuery・Keyがこの独立性や分散を厳密に満たすとは限らない。この導出は、次元によるスケールの増大を抑える理由を説明する近似である(Vaswani et al., 2017, §3.2.1)。

使った数学:分散の加法性と定数倍の規則(統計学Ⅰ第6回)。


項目5: Conformerとは何か

提案の動機

Transformerの強み: 大域的(長距離)な依存関係の捕捉 CNNの強み: 局所的な構造(隣接フレームの音響パターン)の捕捉

→ 両方の良いところを組み合わせる!

Conformerブロックの構造

Conformerはエンコーダの構造であり、CTC・Transducerなどの出力方式と組み合わせて使う。原論文の評価はConformerを用いたTransducerで行われた。

順序 モジュール 役割
1 Feed-Forward(FFN) 位置ごとの非線形変換。残差へ加える出力に\(1/2\)を掛ける
2 Multi-Head Self-Attention 系列内の大域的な関係を扱う
3 Convolution 近隣フレームの局所的なパターンを扱う
4 Feed-Forward(FFN) もう一度、出力に\(1/2\)を掛けて残差へ加える
5 LayerNorm ブロックの最後に正規化する

概略の式は次のとおり(各モジュール内部の正規化やドロップアウトは省略):

\[ \begin{aligned} \tilde{\mathbf{x}}&=\mathbf{x}+\tfrac12\mathrm{FFN}_1(\mathbf{x})\\ \mathbf{x}'&=\tilde{\mathbf{x}}+\mathrm{MHSA}(\tilde{\mathbf{x}})\\ \mathbf{x}''&=\mathbf{x}'+\mathrm{Conv}(\mathbf{x}')\\ \mathbf{y}&=\mathrm{LayerNorm}(\mathbf{x}''+\tfrac12\mathrm{FFN}_2(\mathbf{x}'')) \end{aligned} \]

Macaron形式の\(1/2\)はFFNの出力への係数であり、隠れ次元を半分にするという意味ではない。 畳み込みモジュールでは、正規化、pointwise convolutionとGLU、depthwise convolution、BatchNormとSwish、pointwise convolutionなどを使う。

Conformerの性能

Gulati et al. (2020), Table 2のConformer Transducerの結果。LibriSpeechでのWER(%)を、外部言語モデル(LM)の有無を区別して示す。

モデル パラメータ数 LMなし clean / other LMあり clean / other
Conformer (S) 10.3M 2.7 / 6.3 2.1 / 5.0
Conformer (M) 30.7M 2.3 / 5.0 2.0 / 4.3
Conformer (L) 118.8M 2.1 / 4.3 1.9 / 3.9

ここでclean / otherはtest-clean / test-other。単にモデル名を並べるだけでなく、学習データ・モデルサイズ・言語モデル・評価データを確認して比較する。


項目6: Whisperの詳細

Whisperとは

Radford et al. (2022, OpenAI)が発表した多言語・多タスクの音声認識モデル。ここでは主に2022年の原論文と、実習で使う多言語版smallを説明する。

  • Web上の音声とテキストの対応を利用した、弱教師あり学習によるモデル
  • 文字起こし、英語への音声翻訳、言語識別などを特殊トークンで切り替える
  • 言語ごとの学習量・精度は異なり、すべての言語で同等の性能という意味ではない

学習データの特徴

2022年の原論文の680,000時間の内訳(概算):

課題 音声の時間
英語音声の文字起こし 438,000時間
英語以外の音声の文字起こし 117,000時間
他言語の音声から英語への翻訳 125,000時間

原論文§1。すべてが人手で精密に転記されたデータという意味ではない。

アーキテクチャ

TransformerのEncoder-Decoderを使う。実習のsmallでは、16 kHz音声を最大30秒の窓で扱い、80チャネルのログメルスペクトログラムに変換する。

音声 → ログメル特徴量(smallでは80チャネル)
   ↓
2層Conv + GELU(時間方向を1/2に縮小)
   ↓
Transformerエンコーダ
   ↓
Transformerデコーダ(自己回帰)
   ・<|startoftranscript|>
   ・<|ja|> などの言語トークン
   ・<|transcribe|> または <|translate|>
   ・必要に応じて時刻トークン
   ↓
テキスト・セグメントのタイムスタンプ

後発のlarge-v3は128メルチャネルで、学習データも原論文の680,000時間とは異なる(公式モデルカード)。モデルの版を区別する。

モデルサイズのバリエーション

モデル パラメータ数 必要なGPUメモリの目安
tiny 39M 約1 GB
base 74M 約1 GB
small 244M 約2 GB
medium 769M 約5 GB
large系 1550M 約10 GB
turbo 809M 約6 GB

公式READMEの概数。CPUメモリとは別で、実際の消費量と速度は精度設定・音声の長さ・ハードウェアなどで変わる。日本語には.enの付かない多言語版を使う。実習はsmallを基本とし、メモリや待ち時間に応じてbaseやtinyへ変更できる。turboは音声翻訳用には訓練されていないため、この回の翻訳実習にはsmallを使う。


項目7: Whisperの強み・弱み

利用できる機能と評価上の特徴

  1. ゼロショット評価:原論文は、評価対象データセットで追加学習をせずに複数の課題で評価した。「その言語や関連ドメインの音声を一切学習していない」という意味ではない
  2. 多言語・多タスク:文字起こし、英語への翻訳、言語識別を切り替えられる
  3. さまざまな条件での評価:原論文では雑音やドメインの違いに対する性能も検証している。新しい条件での精度は実データで確認する
  4. タイムスタンプ:通常はセグメント単位の時刻を出す。公式実装のword_timestamps=Trueはcross-attentionと動的時間伸縮法(DTW)を使って単語時刻を推定する。large-v2以降だけの機能ではない

制約と誤り

  1. ハルシネーション:沈黙や雑音などに対し、話されていないテキストを出力する場合がある
  2. 長い音声:transcribe()は30秒の窓を順に処理する。既定では前の認識テキストを次の窓の条件に使うが、音声全体を一度に参照するわけではない
  3. リアルタイム処理:実行時間はモデルと機器に依存し、標準実装はそのまま低遅延ストリーミングASRになるわけではない
  4. 専門用語・固有名詞:誤認識する場合がある。分野に適応したモデルや設定と、同じ評価音声で比較する

language="ja"は日本語として認識する条件を与える設定で、正しい認識を保証しない。省略時の自動言語検出は、公式実装では最初の最大30秒を使う。単語時刻も推定値であり、字幕や分析で使う場合は音声と照合する(公式transcribe実装、モデルカード)。


項目8: SpeechBrain・ESPnetなどのフレームワーク紹介

ASR研究・開発フレームワーク

フレームワーク・ライブラリ 役割・特徴
ESPnet 多様なEnd-to-End音声処理モデルと学習レシピ
SpeechBrain PyTorchベースの音声認識・音声合成・話者処理など
Kaldi GMM-HMMやDNN-HMMなどの音声認識ツール群
fairseq 系列モデルの学習ツール。wav2vec 2.0・HuBERTの実装を含む
Hugging Face Transformers 対応する事前学習モデルを共通APIで利用するライブラリ

Hugging Faceはプラットフォーム・組織の名前でもあり、ここで使うライブラリ名はtransformersである。別々のツールで公開されたモデルが、すべて同じAPIで動くわけではない。

Hugging Face Transformersでの利用例

次の例は、transformersとPyTorch、音声ファイルのデコード用FFmpegが導入された環境を想定する。後の実習ではopenai-whisperを直接使う。

from transformers import pipeline

asr = pipeline("automatic-speech-recognition",
               model="openai/whisper-small", device="cpu")
# 短い日本語の音声ファイルの例
result = asr("audio.wav",
             generate_kwargs={"language": "japanese", "task": "transcribe"})
print(result["text"])

APIの説明。モデルの初回利用時には重みをダウンロードする。


項目9: 実際に使ってみる(Whisper実習概要)

今日の実習の流れ

  1. openai-whisper パッケージのインストール
  2. Whisperモデルのロード
  3. 用意した音声ファイルを認識させる
  4. 自分の声を録音して認識させる
  5. 誤りのパターンを観察する

以下に実装コードを示す。


Python実習:OpenAI Whisperを使った音声認識(日本語)

以下はGoogle Colab上で順に実行するコード。GPUの割り当ては環境による。GPUがなくてもCPUで動くが、時間がかかる場合がある。ここではダウンロードしたモデルをColab上で実行する。

Step 1: パッケージのインストール

# Google Colabのセルで実行
%pip install -q openai-whisper jiwer
!apt-get -qq update
!apt-get -qq install -y ffmpeg

Step 2: 必要なライブラリのインポート

import whisper
import torch

device = "cuda" if torch.cuda.is_available() else "cpu"
print("使用デバイス:", device)

Step 3: モデルのロード

# 日本語なので .en の付かない多言語版を使う
# メモリや待ち時間に応じて "base" / "tiny" に変更できる
model = whisper.load_model("small", device=device)
decode_options = {"fp16": device == "cuda"}
print(f"モデルパラメータ数: {sum(p.numel() for p in model.parameters()):,}")

Step 4: 音声ファイルの認識

from google.colab import files

# 短い音声ファイルを1つ選ぶ
uploaded = files.upload()
if not uploaded:
    raise ValueError("音声ファイルを1つ選んでください")
file_path = next(iter(uploaded))

result_file = model.transcribe(
    file_path, language="ja", task="transcribe", **decode_options)
print("認識結果:", result_file["text"])

# これはセグメント単位のタイムスタンプ
for segment in result_file["segments"]:
    start, end = segment["start"], segment["end"]
    print(f"[{start:.1f}s → {end:.1f}s] {segment['text']}")

Step 5: Google Colabでマイク録音して認識

Colabを開いているブラウザでマイクの使用を許可する。録音データの形式はブラウザによって異なるので、WAVと仮定せず、FFmpegで内容を判別して変換する(MediaRecorder仕様)。

from google.colab import output
from base64 import b64decode
from pathlib import Path
import subprocess

# eval_jsへ渡す関数は、録音完了後のデータURLで解決するPromiseを返す
RECORD_JS = r"""
async (durationMs) => {
  const stream = await navigator.mediaDevices.getUserMedia({audio: true});
  try {
    const recorder = new MediaRecorder(stream);
    const blob = await new Promise((resolve, reject) => {
      const chunks = [];
      let timer;
      recorder.ondataavailable = e => {
        if (e.data.size > 0) chunks.push(e.data);
      };
      recorder.onstop = () => {
        clearTimeout(timer);
        resolve(new Blob(chunks, {type: recorder.mimeType}));
      };
      recorder.onerror = e => {
        clearTimeout(timer);
        reject(e.error || new Error("録音に失敗しました"));
      };
      recorder.start();
      timer = setTimeout(() => {
        if (recorder.state !== "inactive") recorder.stop();
      }, durationMs);
    });
    return await new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onloadend = () => resolve(reader.result);
      reader.onerror = () => reject(reader.error);
      reader.readAsDataURL(blob);
    });
  } finally {
    stream.getTracks().forEach(track => track.stop());
  }
}
"""

def record_audio(sec=5):
    """録音を待ち、16 kHz・モノラルのPCM WAVへ変換する。"""
    if sec <= 0:
        raise ValueError("録音時間は正の秒数にしてください")
    print(f"{sec}秒間録音します。マイクの許可後に話してください。")
    data_url = output.eval_js(f"({RECORD_JS})({int(sec * 1000)})")
    raw_path = Path("recording_input.bin")
    raw_path.write_bytes(b64decode(data_url.split(",", 1)[1]))
    subprocess.run([
        "ffmpeg", "-y", "-hide_banner", "-loglevel", "error",
        "-i", str(raw_path), "-ac", "1", "-ar", "16000",
        "-c:a", "pcm_s16le", "recorded.wav"
    ], check=True)
    return "recorded.wav"

recorded_path = record_audio(sec=5)
result_mic = model.transcribe(
    recorded_path, language="ja", task="transcribe", **decode_options)
print("認識結果:", result_mic["text"])

ブラウザの録音が利用できない場合は、端末で録音した音声をStep 4でアップロードして同じ分析を行える。

Step 6: 様々なオプションを試す

# 比較する音声を固定する。マイク音声なら recorded_path に変更
# Step 7の正解文も、この音声で実際に話された内容に合わせる
audio_path = file_path

result_ja = model.transcribe(
    audio_path, language="ja", task="transcribe", **decode_options)
result_auto = model.transcribe(
    audio_path, task="transcribe", **decode_options)
result_en = model.transcribe(
    audio_path, language="ja", task="translate", **decode_options)

print(f"日本語認識: {result_ja['text']}")
print(f"自動検出言語: {result_auto.get('language', '不明')}")
print(f"英語翻訳: {result_en['text']}")

translateの出力先は英語。翻訳文は日本語の正解文に対するCERでは評価しない。

Step 7: 認識精度の確認

日本語のWERは単語の分割規則によって変わるため、ここでは文字単位のCERを使う。句読点・空白・漢字/かな・数字の表記の違いも結果に影響する。前処理の規則を決め、すべての条件で同じ規則を使う(jiwer公式)。

from jiwer import cer

# audio_pathで実際に話された内容へ必ず書き換える
reference = "今日の天気はとても良くて気持ちがいい"
hypothesis = result_ja["text"].strip()
reference = reference.strip()

print(f"正解: {reference}")
print(f"認識: {hypothesis}")
# この例は両端の空白だけを除き、句読点などは残して評価
cer_value = cer(reference, hypothesis)
print(f"CER(文字誤り率): {cer_value:.1%}")

CERは編集距離を正解の文字数で割った値で、挿入が多いと100%を超えることもある。1例の結果は、その条件全体の性能を代表するとは限らない。


演習:Whisperの誤りパターン分析

課題

Whisperに様々な音声を認識させ、誤りのパターンを分析する。

実施手順

以下は比較する音声の条件例。用意できる2〜3条件から取り組んでもよい。

  1. 標準的な読み上げ音声 — 教科書の一節をゆっくり読む
  2. 速い発話 — 同じ文を早口で話す
  3. 騒音環境 — 周囲で雑音を出しながら話す(例: 手をたたく)
  4. 方言・アクセント — 用意できる範囲で自然な発話を使う。模倣した訛りだけで、その方言全体の性能を評価しない
  5. 専門用語 — 音声処理の専門用語(「メルスペクトログラム」「HuBERT」等)を話す

速度・雑音の比較では同じ文、同じ話者、同じモデルと認識設定を使い、主に1つの条件を変える。方言・専門用語の例も、正解文は実際の発話に合わせる。可能なら複数回試し、結果を一例だけで一般化しない。

分析フォーマット

条件 正解文 認識結果 CER 主な誤りの種類
標準 ... ... ...% —
速発話 ... ... ...% 語境界の崩壊 など
騒音 ... ... ...% 音の誤認識 など
方言 ... ... ...% 語彙の置換 など
専門用語 ... ... ...% 一般語への置換 など

考察のポイント

  1. 誤りはどのような「パターン」を持つか?(ランダムか、系統的か)
  2. 日本語に特有の誤りはあるか?(漢字変換ミス、同音異義語 等)
  3. モデルサイズ(small vs medium)で誤りの種類は変わるか?
  4. CERと誤りの具体例を照合し、今回用意した音声ではどの条件に誤りが多かったか説明する。意味の誤りと表記の違いを分けて考える

提出

分析表と100〜200字程度の考察を、翌週末23:59までにGoogle Driveまたはメール添付で提出する。

共有(任意)

最も興味深い誤りパターンを1つ選び、チャットや共有ドキュメントに書き込む。Zoom参加中の場合は口頭で紹介することもできる。


項目(まとめ): 今日のポイント

  1. End-to-End ASR:音声から出力列を系列の損失で学ぶ。文字ごとの時間位置ラベルを事前に与えず学習できる。
  2. CTC: ブランクラベルにより長さ不一致問題を解決。前向き・後ろ向きアルゴリズムで効率計算。
  3. AED: Attention機構でエンコーダ出力を参照しながら自己回帰デコード。
  4. Conformer:Self-Attention(大域)とConvolution(局所)を組み合わせたエンコーダ。出力方式や学習条件とは区別する。
  5. Whisper:2022年の原論文は680,000時間の弱教師あり学習。モデルの版・言語・評価条件を区別して使う。
  6. 実習: WhisperをColabで実際に動かし、誤りパターンを分析した。

次回予告(第9回)

音声認識システムの評価指標(WER・CER・SLUEベンチマーク)と誤り分析の方法論を学ぶ。WER・CERの手計算を行う。

参考資料