「JoeyNMT」で音声データを使った自動音声認識、音声翻訳モデルを作る「Python+PyTorch」と「JoeyNMT」で学ぶニューラル機械翻訳(終)

精度向上により、近年利用が広まっている「ニューラル機械翻訳」。その仕組みを、自分で動かしながら学んでみましょう。第3回は「JoeyNMT」を音声に対応させて、音声認識や音声翻訳のタスクをエンドツーエンドで解くモデルを構築してみましょう。

» 2022年08月17日 05時00分 公開
[太田 麻裕美八楽]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

 ハイデルベルク大学の博士課程に在籍しながら、八楽という会社で「ヤラクゼン」の開発に携わっている太田です。ヤラクゼンは、AI翻訳から翻訳文の編集、ドキュメントの共有、翻訳会社への発注までを1つにする翻訳プラットフォームです。

 第2回は、Discordのチャットbotでニューラル機械翻訳を試す方法と「JoeyNMT」のカスタマイズ方法を紹介しました。第3回は「JoeyNMT」を音声に対応させて、音声認識や音声翻訳のタスクをエンドツーエンド(E2E)で解くモデルを構築する方法を紹介します。

「自動音声認識」タスクとは

 自動音声認識(Automatic Speech Recognition: ASR)といえば、ある言語での音声の入力を受け付け、音声を書き起こしたテキストを返すタスクです。音声翻訳(Speech Translation: ST)は、ある言語で音声の入力を受け付ける部分は同じですが、別の言語に翻訳されたテキストを出力するタスクです。今回紹介するE2Eは、入力音声の言語で書き起こしせず、ダイレクトに別の言語のテキストを生成するモデルになります。

 例えば、日本語音声を入力してその日本語を書き起こすのは自動音声認識タスク、日本語音声を入力して日本語が書き起こされることなくダイレクトに英語のテキストに翻訳されるのはE2E音声翻訳タスクに分類されます。

 本記事では、音声認識と音声翻訳を合わせて、Speech-to-Text(S2T)タスクと呼ぶことにします。

インストール

 S2TのためのコードをGitHubにアップロードしました。まずは下記リポジトリからインストールしてください。

$ pip install git+https://github.com/may-/joeys2t.git

モデルの構成

 テキストの機械翻訳のモデルをベースに、S2Tに対応させるため、以下のモジュールを実装していきます。

 それでは一つ一つ詳しく見ていきましょう。

入出力フォーマット

入力:スペクトログラム

 モデルに音声を入力する際、生の音声波形(waveform)はS2Tタスクにおいてあまり良い特徴量とはいえません。ニューラルネットへの移行が起こる以前から、音声スペクトログラムと呼ばれる、横軸にフレーム数、縦軸に周波数をとって各フレーム、周波数におけるエネルギーの強さを2次元配列で表現する特徴量が音声認識タスクで広く使われてきました。

 スペクトログラムの抽出方法は幾つか種類がありますが、人間の音声の周波数帯に特化したMel Filterbankという変換を採用している論文が、E2Eモデルでは主流になっています。

 加えて、スペクトログラムを抽出する前の音声波形の段階で、背景ノイズの低減やスピード調整などが行われることもあります。また録音の質(背景雑音、非母語話者による録音など)にばらつきが多いときはメタデータによるフィルタリングや、女声男声の数のバランスを取るといったことも行われることがあります。今回はフィルタリングせず、データセットに入っている音声波形をそのまま使ってMel Filterbankスペクトログラムを抽出します。

 本記事では割愛しますが、スペクトログラムのような音響工学に基づく特徴量抽出方法以外にも、wav2vecという特徴量ベクトルの値を深層学習で学習させる手法が2019年に提案され、盛んに研究されています。言語モデルがさまざまな自然言語処理タスクの事前訓練として定着していったように、今後、wav2vecが音声系のタスクの事前訓練として広く用いられるようになっていくのではと思います(※1)。

※1:Fine-Tune Wav2Vec2 for English ASR with Transformers

出力:テキスト

 英語の音声認識ではアルファベットの文字が出力ラベルとして使われています。しかし、BPEの手法が広まって以降はどの言語のS2Tでもサブワードレベルの分割が出力ラベルになるケースが増えてきました(※2)。トークナイズについては基本的にはテキストの機械翻訳のときとほとんど変わりませんが、語彙(ごい)サイズはテキストの機械翻訳よりも小さく作ることが多いようです。

※2:他にもマルチリンガルタスクへの応用で、IPAの発音記号を使ったり、音素(Phoneme)を使ったりする工夫も提案されています(https://doi.org/10.1109/ICASSP.2014.6855086など)。https://arxiv.org/abs/2009.04707など、文字レベルの分割とサブワードレベルの分割を比較した研究も参考にしてみてください。

 音声の前処理は、テキストの前処理とは異なります。「えーと」といった言いよどみの書き起こしや、音声では発話されない句読点やかぎかっこなどの記号を取り除いたり、音声と表記にずれがある数字などを正規化したりする処理が挙げられます。特に字幕から作られたデータセットでは「(拍手)」など発話されていない描写が書き起こしテキストに入っていることが多々あります。これらを取り除いたり、トークナイズしたりする際に「(拍手)」が分割されないよう1つのトークンとして扱うなどの工夫も必要です。

 入力データの句読点を外す前処理を施す場合は、モデルによる生成後、出力された書き起こしにも句読点を付け戻す後処理が必要になることもあります。

JoeyS2T実装

 JoeyS2Tでは、音声ファイルへのパスと書き起こしテキストを各行に入れたtsvファイルを入力に取るようにしています。「src」の列は、.wavなどの音声波形ファイル名、.npyのスペクトログラムファイル名、もしくは.zipファイル名とバイトオフセットのいずれかの形式で入力音声へのパスを指定します(※3)。音声波形をスペクトログラムに変化するため「torchaudio」のsox warpperを利用しています。この変換は時間がかかるので、訓練を始める前にあらかじめ抽出しておき、numpyの2次元配列として保存しておくことにします。

※3:fairseq S2Tの入力形式に準拠しています。

id src n_frames trg
1272-128104-0 LibriSpeech/dev-clean/1272/128104/1272-128104-0000.flac 584 mister quilter is the apostle of the middle classes and we are glad to welcome his gospel
1272-128104-1 LibriSpeech/dev-clean/1272/128104/1272-128104-0001.flac 480 nor is mister quilter's manner less interesting than his matter
1272-128104-2 LibriSpeech/dev-clean/1272/128104/1272-128104-0002.flac 1247 he tells us that at this festive season of the year with christmas and roast beef looming before us similes drawn from eating and its results occur most readily to the mind
.flacファイル名を指定した例

id src n_frames trg
1688-142285-3 fbank80/1688-142285-3.npy 504 i really liked that account of himself better than anything else he said
1688-142285-4 fbank80/1688-142285-4.npy 446 his statement of having been a shop boy was the thing i liked best of all
1688-142285-5 fbank80/1688-142285-5.npy 428 you who were always accusing people of being shoppy at helstone
.npyのスペクトログラムファイル名を指定した例

id src n_frames trg
1089-134686-0 fbank80.zip:56130780594:333568 1042 he hoped there would be stew for dinner turnips and carrots and bruised potatoes and fat mutton pieces to be ladled out in thick peppered flour fattened sauce
1089-134686-1 fbank80.zip:78098607294:104448 326 stuff it into you his belly counselled him
1089-134686-2 fbank80.zip:90906859847:211648 661 after early nightfall the yellow lamps would light up here and there the squalid quarter of the brothels
.zipファイル名とオフセットを指定した例

 このスペクトログラムの抽出と入力tsvファイルの生成をするスクリプトを準備しました。

$ python scripts/prepare_librispeech.py --data_root $WORK_DIR/LibriSpeech

 LibriSpeechデータセットは、英語の音声認識タスクでよく使われるベンチマークです。960時間の音声を含む大きなデータセットで、私の環境では上記処理に丸1日かかりました。また、ダウンロードした生の音声波形と抽出したスペクトログラムのファイルを合わせると160GB程度の大きさになります。ディスク容量に注意してください。

 LibriSpeech以外にも幾つかサンプルのスクリプトがJoeyS2Tに入っています。別のデータセットを使いたい場合は、このスクリプトを書き換えるところから始めてみてください。

2. データ拡張(CMVN、SpecAugment)

CMVN

 Cepstral Mean Variance Normalization: CMVNは、スペクトログラムの入力値の平均を0、分散を1にすることでスケールの偏りやノイズを軽減する正規化手法です。

 各インスタンスのスペクトログラム2次元配列の平均、分散を計算し、インスタンスごとに平均を引いて分散で割る手法(Utterance-CMVN)と、全てのインスタンスから平均、分散を計算し、その1つの値を使って正規化する手法(Global-CMVN)の2種類があります。JoeyS2Tでは前者のUtterance-CMVNを実装しています(※4)。

※4:Utterance-CMVNとGlobal-CMVNの比較は、https://arxiv.org/abs/2011.04884での議論が参考になります。

SpecAugment

 過学習を防ぐ工夫として、SpecAugmentと呼ばれるマスキングを適用します。SpecAugmentは、スペクトログラムの値を、時間軸方向(垂直向き)、周波数方向(水平向き)とランダムに選んでマスクアウトする手法です。マスクした場所は、そのインスタンスのスペクトログラムの平均値で埋めてしまいます。各エポックでそのインスタンスが呼び出されるたびに違うマスクが適用されるので、疑似的にデータ数をかさ増しする効果もあります(※5)。

※5:画像処理の分野で、入力イメージを回転したり反転したりするなど、ラベルに対して不変な変換を施してデータ数を増やすのに似ているかもしれません。

 イメージとしては、通話などをしていて途中で数カ所接続が途切れる、マイクの設定などのせいで高い声の周波数帯だけ聞こえづらい、低い声の周波数帯だけゆがんでいて声が普段と違って聞こえるといった障害が起きたとしても、文脈から推測すれば話している内容が補完できるというような状況を想像してみてください。SpecAugmentのマスキングは、その通信障害のようなものを疑似的に取り入れることで過学習を避け、よりロバストなモデルを作ることに貢献しているといえます。

JoeyS2T実装

 CMVN、SpecAugmentは「joeynmt/data_augmentation.py」で定義されています。ここで定義されたクラスを、トークナイザーが呼ばれるタイミングで適用します。JoeyNMT v2では、バッチイテレーションの中でインスタンスを取ってくる(データセットの「__getitem__()」関数を参照)たびにトークナイザーが呼ばれるので、1つのインスタンスでも毎回違うマスクを適用することができます。

# joeynmt/tokenizer.py
class SpeechProcessor:
  def __init__(self, [...], **kwargs):
    [...]
    self.specaugment = SpecAugment(**kwargs["specaugment"])
    self.cmvn = CMVN(**kwargs["cmvn"])
  def __call__(self, line: str, is_train: bool) -> np.ndarray:
    # tsvファイルの`src`列で指定されたパスから音声データを読み込む
    item = get_features(self.root_path, line)
    [...]
    # CMVN(正規化)はすべてのsplitのデータに適用
    item = self.cmvn(item)
    # SpecAugment(マスキング)は訓練データに適用
    if is_train:
      item = self.specaugment(item)
    return item

3. 畳み込みレイヤー

 音声入力のスペクトログラムは、テキストの入力に比べて系列長が10倍程度長くなります。LibriSpeechのdevセットで比較すると、テキストのサブワード系列長の中央値は17であるのに対し、スペクトログラムフレーム数の中央値は590となっています。

 このような長い系列をトランスフォーマーで扱うのは、計算効率でも性能の面でも困難です。そこで、入力系列を畳み込みレイヤーで短くしてからエンコーダー渡すようにします。時間軸方向に畳み込む1d-convをストライド2でn回適用すると系列長を2nだけ減らすことができます。

 音声スペクトログラムでは、1つのフレームが1つのターゲットトークンに対応しているということはあまりなく、複数のフレームが1つのターゲットトークンを表している場合がほとんどです。音声のこのような冗長性を考えると、畳み込みが役に立つことも感覚的に納得できるのではないでしょうか。

JoeyS2T実装

# joeynmt/encoders.py
class Conv1dSubsampler(nn.Module):
  """1次元畳み込みによるサブサンプリング"""
  def __init__(self,
    in_channels: int,
    mid_channels: int,
    out_channels: int = None,
    kernel_sizes: List[int] = (3, 3)
  ):
    super().__init__()
    self.kernel_sizes = kernel_sizes
    self.n_layers = len(kernel_sizes)
    # n_layersの数だけ1次元畳み込みレイヤーをスタックする
    self.conv_layers = nn.ModuleList(
      nn.Conv1d(
        in_channels if i == 0 else mid_channels // 2,
        mid_channels if i < self.n_layers - 1 else out_channels * 2,
        kernel_size=k,
        stride=2,
        padding=k // 2,
      ) for i, k in enumerate(kernel_sizes)
    )
    def get_out_seq_lens_tensor(self, in_seq_lens_tensor):
      # 公式ドキュメンテーションの計算式から系列長を求める
      # https://pytorch.org/docs/stable/generated/torch.nn.Conv1d.html
      out = in_seq_lens_tensor.clone()
      for k in self.kernel_sizes:
        out = ((out.float() + 2 * (k // 2) - (k - 1) - 1) / 2 + 1).floor().long()
      return out
    def forward(self, src_tokens, src_lengths):
      # DataParallelによるバッチの割り当て後に最大系列長を計算し直す
      max_len = torch.max(src_lengths).item()
      assert max_len > 0, "empty batch!"
      if src_tokens.size(1) != max_len:
        src_tokens = src_tokens[:, :max_len, :]
      assert src_tokens.size(1) == max_len, (src_tokens.size(), max_len, src_lengths)
      _, in_seq_len, _ = src_tokens.size()  # -> B x T x (C x D)
      x = src_tokens.transpose(1, 2).contiguous()  # -> B x (C x D) x T
      # 畳み込みレイヤーの後、非線形アクティベーション(glu)を適用
      for conv in self.conv_layers:
        x = conv(x)
        x = nn.functional.glu(x, dim=1)
        _, _, out_seq_len = x.size()
        x = x.transpose(1, 2).contiguous()  # -> B x T x (C x D)
      # 畳み込み後の系列長を計算。
      out_seq_lens = self.get_out_seq_lens_tensor(src_lengths)
      assert x.size(1) == torch.max(out_seq_lens).item(), \
        (x.size(), in_seq_len, out_seq_len, out_seq_lens)
      return x, out_seq_lens

 畳み込みレイヤーを適用すると系列長が短くなるので、それに合わせてパディングマスクも計算し直す必要があります。PyTorch DataParallelでバッチが複数GPUに割り当てられると、バッチの元の最大系列長が分からなくなります。そのため、複数GPUに割り当てられる前に最大長を保持しておき、その最大長に合わせて全てのGPUのバッチのパディングを計算し直しています。

# joeynmt/encoders.py
class TransformerEncoder(Encoder):
  [...]
  def forward(self, embed_src, src_length, mask, **kwargs):
    # 1次元畳み込みによるサブサンプリング
    if self.subsample:
      embed_src, src_length = self.subsampler(embed_src, src_length)
    # パディングマスクを計算し直す
    if mask is None:
      mask = lengths_to_padding_mask(src_length).unsqueeze(1)
    [...]
    return x, None, mask

 損失関数に渡すパディングマスクも、バッチオブジェクトのメンバーである「batch.src_mask」ではなくEncoder内で計算し直した「src_mask」に置き換える必要があります。

# joeynmt/model.py
class Model(nn.Module):
  [...]
  def forward(self, return_type, **kwargs):
    if return_type == "loss":
      # エンコーダー・デコーダーからの出力を受け取る
      out, ctc_out, src_mask = self._encode_decode(**kwargs)
      # 交差エントロピーの計算につかう各トークンの対数確率を計算
      log_probs = F.log_softmax(out, dim=-1)
      # src_maskを計算し直したもので書き換える
      kwargs["src_mask"] = src_mask
      # CTC損失の計算につかうフレームごとの対数確率を計算
      kwargs["ctc_log_probs"] = F.log_softmax(ctc_out, dim=-1)
      # 損失関数からの返り値(バッチ内で和をとったもの)
      batch_loss = self.loss_function(log_probs, **kwargs)
      [...]

4. エンコーダー/デコーダー

 エンコーダー/デコーダーの構成は、テキストの機械翻訳とほぼ同じです。コードは(畳み込みレイヤーに関する変更を除けば)全く同じものを使いますが、設定で少し気を付けるべきポイントを挙げます。

深いエンコーダー

 テキストの機械翻訳では、同じレイヤー数のエンコーダーとデコーダーを使うことが多いでしょう。S2Tタスクの場合、エンコーダーをデコーダーより深くし、より複雑な学習により多くのパラメーターを割り当てると性能が上がるという報告があります(※6)。

※6:https://arxiv.org/abs/1904.13377など

転移学習

 音声翻訳の場合に使われるテクニックとして、エンコーダーのパラメーターをASR事前学習モデルで、デコーダーのパラメーターをMT事前学習モデルで初期化するという方法があります(※7)。スクラッチから音声翻訳モデルを訓練するのは不安定になったり、収束させるのに時間がかかりすぎたりすることがあります。事前学習モデルからパラメーターの値を転移させることで訓練を安定させたり、処理時間を短縮させたりすることを狙っています。

※7:https://arxiv.org/abs/1911.08870

JoeyS2T実装

 チェックポイントを読み込む際、レイヤーの名前を確認して、あるチェックポイントからはエンコーダーのパラメーターのみを、別のチェックポイントからはデコーダーのパラメーターのみを読み込むようにします。

# joeynmt/training.py
class TrainManager:
  [...]
  def __init__(self, model: Model, cfg: dict) -> None:
    [...]
    for layer_name, load_path in [("encoder", load_encoder), ("decoder", load_decoder)]:
      if load_path is not None:
        self.init_layers(path=load_path, layer=layer_name)
  def init_layers(self, path: Path, layer: str) -> None:
    layer_state_dict = OrderedDict()
    logger.info("Loading %s laysers from %s", layer, path)
    ckpt = load_checkpoint(path=path, device=self.device)
    for k, v in ckpt["model_state"].items():
      if k.startswith(layer):
        layer_state_dict[k] = v
    self.model.load_state_dict(layer_state_dict, strict=False)

 Source側の単語埋め込みはスキップしてスペクトログラムを直接エンコーダーに送るため「Model.src_embed」には「torch.nn.Embeding」オブジェクトではなく「torch.nn.Identity」オブジェクトを入れておきます。

# joeynmt/model.py
def build_model(cfg, src_vocab, trg_vocab):
  [...]
  src_embed = Embeddings(
    **enc_cfg["embeddings"],
    vocab_size=len(src_vocab),
    padding_idx=src_vocab.pad_index
  ) if task ==  "MT" else None
  [...]
  model = Model(
    encoder=encoder,  
    decoder=decoder,  
    src_embed=src_embed if task == "MT" else nn.Identity(),  
    trg_embed=trg_embed,  
    src_vocab=src_vocab,  
    trg_vocab=trg_vocab,  
    task=task)
  [...]
  return model

5. CTC損失

 多くの機械学習タスク同様、テキストの機械翻訳でも、交差エントロピーを損失関数として採用することがほとんどです。E2E音声認識でも、交差エントロピーを最小化する目的関数を採用したトランスフォーマー型のモデルがより高い精度を達成してきました。この交差エントロピーに加えて「Connectionist Temporal Classification: CTC」と呼ばれる長い系列をうまく扱う工夫を損失関数に取り込む手法が提案されています(※8)。

※8:https://doi.org/10.1109/ICASSP.2017.7953075

 CTC損失は、手書き文字認識や音声認識のような、入力と出力の長さが大きく異なるようなタスクで使われています。これらのタスクでは、正解のテキスト系列を出力させることが目的であって、各出力ラベルがどの入力フレームに対応するかはあまり重視されていません。

 例えば、仮に100フレームの「こんにちは」という音声入力があるとします。この音声が、20フレームずつ均等に「こ」「ん」「に」「ち」「は」というラベルにそれぞれ対応していたとしても、最初の60フレームが「こ」に対応していて残りの40フレームが「んにちは」に対応していたとしても、どちらの場合でも「こんにちは」という正解ラベルを出力できれば、音声認識の目的は達成できると考えます。

 JoeyS2Tでは、交差エントロピーとCTCの両方の損失を最小化する目的関数を採用しています(※9)。

※9:https://doi.org/10.1109/JSTSP.2017.2763455

 ここで、λはハイパーパラメーターで、設定ファイルの「ctc_weight」に指定します。一般に、CTC損失が交差エントロピー損失よりも大きな値になることが多いです。両方の損失がともに全体の損失に貢献するようなλの値を、予備実験を行って決めるのがよいかもしれません。例えば、以下のような学習曲線の場合、10kステップ時点で交差エントロピーの損失がおよそ40、CTC損失がおよそ80ですので、λ=0.3くらいに設定すると(1-0.3)×40=28、0.3×80=24となり、ちょうど両方の損失のバランスが取れそうです。

JoeyS2T実装

 損失関数のコードをカスタマイズする方法については第2回の記事を参照してください。

6. 評価(WER)

 音声認識の評価には「Word Error Rate: WER」という評価尺度がよく使われます。モデルの出力と正解ラベルの間の編集距離(edit distance)に基づく指標で、小さい値ほどモデルの出力と正解ラベルの間に違いが少ない、つまり精度が良いことを示しています。

JoeyS2T実装

 JoeyS2Tでは、Cythonで実装されたeditdistanceパッケージをインポートしています。

 モデルが出力したone-hot-encodingのトークンのリストを1つの文字列に戻した後、sacrebleuのトークナイザーとともに「wer()」関数に渡しています。設定ファイルの「sacrebleu_cfg」の項目で、トークナイザーの種類を指定することができます。

# joeynmt/metrics.py
import editdistance
def wer(
  hypotheses: List[str],
  references: List[str],
  tokenizer: Callable,
) -> float:  
  numerator = 0.0  # 分子
  denominator = 0.0  # 分母
  # コーパスレベルで編集距離のカウントの総数を累積
  for hyp, ref in zip(hypotheses, references):
    # 単語分割
    hyp = tokenizer(hyp)
    ref = tokenizer(ref)
    # utteranceごとにモデル出力と正解ラベル間の編集距離を計算
    numerator += editdistance.eval(hyp, ref)  
    denominator += len(ref)
  return (numerator / denominator) * 100 if denominator else 0.0
scliteでの計算結果と一致することを確認しています。

【補足】参考文献ガイド

モデルの訓練

 「入出力フォーマット」のセクションで言及したLibriSpeechコーパスから、clean100カテゴリーのデータを使ってモデルを訓練します(※10)。

※10:LibriSpeechはデータサイズが大きいのでGoogle Colabでは扱うのが難しいかもしれません。Google Colabで動かしてみたい場合は、小さいデータを扱ったnotebookを参照してください。

 コンフィギュレーションファイルで、Speech-to-Textタスクの設定をします。「src」の「min_length」は、1d-Convで圧縮するカーネルサイズより長くなるように設定してください。

data:
  task: "S2T"            # Speech-to-Textタスク
  train: "path/to/LibriSpeech/joey_train-clean-100"  # 訓練データ
  dev: "path/to/LibriSpeech/joey_dev-clean"          # 開発データ
  test: "path/to/LibriSpeech/joey_test-clean"        # テストデータ
  dataset_type: "speech" # データセットタイプは"speech"に設定
  src:
    lang: "en"           # 言語タグ
    level: "frame"       # 入力レベルは"frame"に設定
    num_freq: 80         # スペクトログラムの周波数
    min_length: 10       # スペクトログラムの最小フレーム数
    max_length: 6000     # スペクトログラムの最大フレーム数
    tokenizer_type: "speech" # トークナイザータイプは"speech"に設定
    tokenizer_cfg:
      specaugment:       # SpecAugmentのパラメーター
        freq_mask_n: 2
        freq_mask_f: 27
        time_mask_n: 2
        time_mask_t: 100
        time_mask_p: 1.0
      cmvn:              # CMVNのパラメーター
        norm_means: True
        norm_vars: True
        before: True  
    trg:  
      lang: "en"  
      level: "bpe"
      lowercase: True  
      max_length: 512
      voc_min_freq: 1
      voc_limit: 5000
      voc_file: "path/to/LibriSpeech/spm_train-clean-100_unigram5000.vocab.txt"
      tokenizer_type: "sentencepiece"  
      tokenizer_cfg:  
        model_file: "path/to/LibriSpeech/spm_train-clean-100_unigram5000.model"
        pretokenizer: "none"
testing:
  n_best: 1  
  beam_size: 20
  beam_alpha: 1.0
  batch_size: 10000  
  batch_type: "token"  
  max_output_length: 100  # 書き起こしテキストの最大出力長
  eval_metrics: ["wer"]   # 評価尺度
  sacrebleu_cfg:  
    tokenize: "13a"     # 評価する時に使うトークナイザー
training:
  #load_model: "models/librispeech100h/best.ckpt"
  #load_encoder: "models/ASR/best.ckpt"  # 音声翻訳の際、エンコーダーをASR事前学習モデルのパラメーターで初期化する
  #load_decoder: "models/MT/best.ckpt"   # 音声翻訳の際、デコーダーをMT事前学習モデルのパラメーターで初期化する
  reset_best_ckpt: False
  reset_scheduler: False  
  reset_optimizer: False  
  reset_iter_state: False
  random_seed: 321
  optimizer: "adam"
  adam_betas: [0.9, 0.98]
  scheduling: "warmupinversesquareroot"
  learning_rate: 2.0e-3
  learning_rate_min: 1.0e-6
  learning_rate_warmup: 10000
  clip_grad_norm: 10.0
  weight_decay: 0.
  batch_size: 20000
  batch_type: "token"  
  batch_multiplier: 4
  normalization: "batch"
  epochs: 300
  updates: 100000
  validation_freq: 1000
  logging_freq: 100
  early_stopping_metric: "wer"
  model_dir: "models/librispeech100h"
  overwrite: False
  shuffle: True
  use_cuda: True
  print_valid_sents: [0, 1, 2]  
  keep_best_ckpts: 10
  label_smoothing: 0.1
  loss: "crossentropy-ctc" # 交差エントロピーとCTCのジョイントオブジェクティブ
  ctc_weight: 0.3          # CTC損失の係数
model:  
  initializer: "xavier"
  init_gain: 1.0  
  bias_initializer: "zeros"  
  embed_initializer: "xavier"
  embed_init_gain: 1.0  
  tied_embeddings: False
  tied_softmax: False  
  encoder:  
    type: "transformer"
    num_layers: 16
    num_heads: 4
    embeddings:  
      embedding_dim: 80  # 入力スペクトログラムの周波数(エンコーダー側は単語埋め込み層なし。)
    hidden_size: 512
    ff_size: 2048
    dropout: 0.1
    freeze: False
    subsample: True      # 1d-conv を使って入力系列を圧縮するかどうか
    conv_kernel_sizes: [5, 5] # 1d-convのカーネルサイズ
    conv_channels: 512   # 1d-convの隠れ層のサイズ
    in_channels: 80      # 入力スペクトログラムの周波数
    layer_norm: "pre"  
  decoder:  
    type: "transformer"
    num_layers: 8
    num_heads: 4
    embeddings:
      embedding_dim: 512
      scale: True
      dropout: 0.1
      hidden_size: 512
    ff_size: 2048
    dropout: 0.1
    freeze: False  
    layer_norm: "pre"

 「train」モードで訓練を始めます。

$ python -m joeynmt train configs/librispeech_100h.yaml --skip_test

 およそ30kステップで、Accuracyが0.9、Perplexityが2、Word Error Rateが15くらいの値に落ち着いてきます。

 以降はあまり変化が見られなかったので、100kステップでいったん訓練を打ち切りました。100kステップを回すのに、NVIDIA RTX A6000のGPUで約22時間かかりました。

モデルの評価

 保存されたチェックポイント10個の平均を取ります。

$ python scripts/average_checkpoints.py --inputs models/librispeech100h/*00.ckpt --output models/librispeech100h/avg10.py

 「test」モードでモデルの性能を評価します。

$ python -m joeynmt test configs/librispeech_100h.yaml --ckpt models/librispeech100h/avg10.py
2022-06-30 01:01:47,581 - INFO - root - Hello! This is Joey-NMT (version 2.0.0).  
[...]
2022-06-30 01:02:14,239 - INFO - joeynmt.prediction - Decoding on dev set...  
2022-06-30 01:02:14,239 - INFO - joeynmt.prediction - Predicting 2703 example(s)...
2022-06-30 01:14:47,924 - INFO - joeynmt.prediction - Evaluation result wer:  11.04
2022-06-30 01:14:47,928 - INFO - joeynmt.prediction - Decoding on test set...
2022-06-30 01:14:47,928 - INFO - joeynmt.prediction - Predicting 2620 example(s)...
2022-06-30 01:23:56,345 - INFO - joeynmt.prediction - Evaluation result wer:  12.33

 この学習済みモデルは公開しています。jupyter notebook(※11)では、モデルの書き起こし結果とともに、その入力音声を聞くこともできます。ぜひリポジトリからアクセスしてみてください。

※11:このnotebookは、AIMS Senegalでの「NMT in Practice」の講義で使ったものが基になっています。

最後に

 3回にわたってお届けしてきた本連載も今回で最後となりました。「JoeyNMT」は2019年にオープンソース化されて以降、多くのコントリビューターに支えられて少しずつ成長してきました。ミニマリスティックな哲学は保ちつつ、新機能の実装、古い依存ライブラリからくる問題への対処など、ボランティアの手で継続的にアップデートされています。

 当初の開発目的であった教育用途はもちろん、翻訳の枠を超え、画像キャプション生成、手話翻訳、強化学習などさまざまなプロジェクトで用いられています。本連載での音声認識により、応用の幅はさらに広がったのではないでしょうか。また日本語で「JoeyNMT」を紹介する機会をいただけたことをとてもうれしく思います。本連載が、ニューラル機械翻訳の世界に飛び込むきっかけになれば幸いです。

Copyright © ITmedia, Inc. All Rights Reserved.

スポンサーからのお知らせPR

注目のテーマ

Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。