低リソースASRのためのMMSアダプターモデルの微調整
'MMSアダプターモデルの微調整'
新しい(06/2023):このブログ記事は、「多言語ASRでのXLS-Rの微調整」に強く触発され、それの改良版として見なされるものです。
Wav2Vec2は、自動音声認識(ASR)のための事前学習モデルであり、Alexei Baevski、Michael Auli、およびAlex Conneauによって2020年9月にリリースされました。Wav2Vec2の強力なパフォーマンスが、ASRの最も人気のある英語データセットであるLibriSpeechで示された直後、Facebook AIはWav2Vec2の2つのマルチリンガルバージョンであるXLSRとXLM-Rを発表しました。これらのモデルは128の言語で音声を認識することができます。XLSRはクロスリンガル音声表現を意味し、モデルが複数の言語で有用な音声表現を学習する能力を指します。
Meta AIの最新リリースであるMassive Multilingual Speech(MMS)(Vineel Pratap、Andros Tjandra、Bowen Shiなどによる)は、マルチリンガル音声表現を新たなレベルに引き上げています。1,100以上の話されている言語が識別、転写、生成され、さまざまな言語識別、音声認識、テキスト読み上げのチェックポイントがリリースされます。
このブログ記事では、MMSのアダプタートレーニングが、わずか10〜20分の微調整後でも驚くほど低い単語エラーレートを達成する方法を示します。
低リソース言語の場合、私たちは「多言語ASRでのXLS-Rの微調整」と同様にモデル全体を微調整するのではなく、MMSのアダプタートレーニングの使用を強くお勧めします。
私たちの実験では、MMSのアダプタートレーニングはメモリ効率がよく、より堅牢であり、低リソース言語に対してはより優れたパフォーマンスを発揮することがわかりました。ただし、VoAGIから高リソース言語への場合は、Adapterレイヤーの代わりにモデル全体のチェックポイントを微調整する方が依然として有利です。
世界の言語多様性の保存
https://www.ethnologue.com/によると、約3000の「生きている」言語のうち、40%、つまり約1200の言語が、話者が減少しているために危機に瀕しています。このトレンドはますますグローバル化する世界で続くでしょう。
MMSは、アリ語やカイビ語など、絶滅危惧種である多くの言語を転写することができます。将来的には、MMSは、残された話者が母国語での記録作成やコミュニケーションをサポートすることで、言語を生き続けるために重要な役割を果たすことができます。
1000以上の異なる語彙に適応するために、MMSはアダプターを使用します。アダプターレイヤーは言語間の知識を活用し、モデルが別の言語を解読する際に役立つ役割を果たします。
MMSの微調整
MMSの非監視チェックポイントは、1400以上の言語で300万〜10億のパラメータを持つ、50万時間以上のオーディオで事前学習されました。
事前学習のためのモデルサイズ(300Mおよび1B)の事前学習のみのチェックポイントは、🤗 Hubで見つけることができます:
mms-300m
mms-1b
注意:ベースモデルを微調整する場合は、「多言語ASRでのXLS-Rの微調整」と同じ方法で行うことができます。
BERTのマスク言語モデリング目的と同様に、MMSは自己教師あり事前学習中に特徴ベクトルをランダムにマスキングし、トランスフォーマーネットワークに渡すことで文脈化された音声表現を学習します。
ASRでは、事前学習されたMMS-1B
チェックポイントは、共通の語彙出力層を持つ1000以上の言語で監視付き学習を行った後、最終的に共通の語彙出力層を除外し、言語固有のアダプターレイヤーのみを保持しました。各アダプターレイヤーは、各アテンションブロックの小さな線形射影層と言語固有の語彙出力層で構成される、わずか約2.5Mの重みです。
音声認識(ASR)用に調整されたMMSチェックポイント3つがリリースされました。それらには、それぞれ102、1107、1162のアダプターウェイトが含まれています(各言語に1つずつ):
mms-1b-fl102
mms-1b-l1107
mms-1b-all
ベースモデルは通常どおりmodel.safetensors
ファイルとして保存されていることがわかりますが、さらにこれらのリポジトリには多くのアダプターウェイトがリポジトリに保存されています。たとえば、フランス語の場合はadapter.fra.safetensors
という名前で保存されています。
Hugging Faceのドキュメントでは、このようなチェックポイントの推論方法が非常によく説明されています。したがって、このブログ記事では、リリースされたASRチェックポイントのいずれかを基に、効率的に高性能なアダプターモデルをトレーニングする方法に焦点を当てます。
アダプティブウェイトのトレーニング
機械学習では、アダプターは事前学習済みモデルを微調整するための手法であり、元のモデルのパラメータを変更せずに行います。これは、モデルの既存のレイヤーの間に小さなトレーニング可能なモジュールであるアダプターレイヤーを挿入し、詳細な再トレーニングを必要とせずにモデルを特定のタスクに適応させることによって行います。
アダプターは、音声認識、特に話者認識において長い歴史を持っています。話者認識では、アダプターは既存のモデルを微調整して個々の話者の特徴を認識するために効果的に使用されています。これは、GalesとWoodlandの(1996年)およびMiaoらの(2014年)の研究で強調されています。このアプローチは、フルモデルのトレーニングと比較して計算要件を大幅に削減するだけでなく、より優れた柔軟な話者固有の調整も可能にします。
MMSで行われた研究は、このアダプターのアイデアを使用して異なる言語の音声認識にアダプターを適用しています。少数のアダプターウェイトが各ターゲット言語の固有の音声および文法的特徴を把握するために微調整されます。これにより、MMSは単一の大規模なベースモデル(例:mms-1b-all
チェックポイント)と1000以上の小さなアダプターレイヤー(mms-1b-all
ごとに2.5Mウェイト)を使用して、複数の言語を理解して転写することが可能になります。これにより、異なる言語の個別のモデルを開発するための計算要件が大幅に削減されます。
素晴らしいですね!モチベーションと理論を理解したので、mms-1b-all
のアダプターウェイトを微調整する方法について見ていきましょう🔥
ノートブックのセットアップ
以前の「Multi-Lingual ASRでのXLS-Rのファインチューニング」ブログ記事と同様に、我々はCommon Voiceの低リソースASRデータセットでモデルをファインチューニングします。このデータセットにはバリデーション済みのトレーニングデータが約4時間含まれています。
Wav2Vec2やXLS-Rと同様に、MMSはConnectionist Temporal Classification(CTC)を使用してファインチューニングされます。CTCは、ASRや手書き認識などのシーケンス対シーケンスの問題のためにニューラルネットワークをトレーニングするために使用されるアルゴリズムです。
CTCアルゴリズムの詳細については、Awni Hannunによる「Sequence Modeling with CTC (2017)」という素晴らしいブログ記事をお読みいただくことをお勧めします。
開始する前に、datasets
とtransformers
をインストールします。また、オーディオファイルをロードするためにtorchaudio
と、単語エラーレート(WER)メトリックを使用してファインチューニングモデルを評価するためにjiwer
も必要です。
%%capture
!pip install --upgrade pip
!pip install datasets
!pip install evaluate
!pip install git+https://github.com/huggingface/transformers.git
!pip install jiwer
!pip install accelerate
トレーニング中にチェックポイントを🤗 Hubに直接アップロードすることを強くお勧めします。Hubのリポジトリにはバージョン管理が組み込まれているため、トレーニング中にモデルチェックポイントが失われることはありません。
これを行うには、Hugging Faceのウェブサイトから認証トークンを保存する必要があります(まだ登録していない場合はこちらでサインアップしてください!)
from huggingface_hub import notebook_login
notebook_login()
データ、トークナイザー、特徴抽出の準備
ASRモデルは音声をテキストに変換します。つまり、音声信号をモデルの入力形式である特徴ベクトルに変換する特徴抽出器と、モデルの出力形式であるテキストに変換するトークナイザの両方が必要です。
🤗 Transformersでは、MMSモデルにはWav2Vec2FeatureExtractorという特徴抽出器と、Wav2Vec2CTCTokenizerというトークナイザが付属しています。
予測された出力クラスを出力の転写にデコードするために、まずトークナイザを作成しましょう。
Wav2Vec2CTCTokenizerを作成する
「mms-1b-all」というようなファインチューニングされたMMSモデルには、モデルチェックポイントと一緒にトークナイザが付属しています。ただし、特定の言語の特定の低リソースデータでモデルをファインチューニングしたい場合は、トークナイザとボキャブラリの出力層を完全に削除し、トレーニングデータ自体に基づいて新しいものを作成することが推奨されます。
CTCを使ってファインチューニングされたWav2Vec2のようなモデルでは、オーディオファイルを単一の順方向パスで転写します。まず、オーディオ入力を処理して処理されたコンテキスト表現のシーケンスに変換し、最終的なボキャブラリ出力層を使用して各コンテキスト表現を転写を表す文字に分類します。
この層の出力サイズは、ボキャブラリのトークン数に対応します。これは、ファインチューニングに使用されるラベル付きデータセットから抽出します。したがって、最初のステップでは、Common Voiceという選択したデータセットを確認し、転写に基づいてボキャブラリを定義します。
このノートブックでは、Common Voiceの6.1データセットをトルコ語で使用します。トルコ語の言語コードは"tr"
です。
素晴らしい、これで🤗 DatasetsのシンプルなAPIを使用してデータをダウンロードできます。データセットの名前は"mozilla-foundation/common_voice_6_1"
であり、構成名は言語コードに対応しています。今回は"tr"
です。
注意:データセットをダウンロードするには、Hugging Faceアカウントにログインして、データセットのリポジトリページにアクセスし、「同意してリポジトリにアクセスする」をクリックする必要があります。
Common Voiceには、invalidated
というデータも含まれており、「十分にクリーンではない」と評価されなかったデータを指します。このノートブックでは、"train"
、"validation"
、"test"
のスプリットのみを使用します。
トルコ語のデータセットが非常に小さいため、バリデーションとトレーニングデータを結合してトレーニングデータセットとし、テストデータのみをバリデーションに使用します。
from datasets import load_dataset, load_metric, Audio
common_voice_train = load_dataset("mozilla-foundation/common_voice_6_1", "tr", split="train+validation", use_auth_token=True)
common_voice_test = load_dataset("mozilla-foundation/common_voice_6_1", "tr", split="test", use_auth_token=True)
多くのASRデータセットでは、各オーディオ配列('audio'
)およびファイル('path'
)に対してターゲットテキスト('sentence'
)のみを提供します。しかし、Common Voiceはオーディオファイルに関する他の情報('accent'
など)も提供しています。このノートブックを可能な限り一般化するために、ファインチューニングでは転写されたテキストのみを考慮します。
common_voice_train = common_voice_train.remove_columns(["accent", "age", "client_id", "down_votes", "gender", "locale", "segment", "up_votes"])
common_voice_test = common_voice_test.remove_columns(["accent", "age", "client_id", "down_votes", "gender", "locale", "segment", "up_votes"])
データセットのランダムなサンプルを表示するための短い関数を作成し、何度か実行して転写の感触をつかみましょう。
from datasets import ClassLabel
import random
import pandas as pd
from IPython.display import display, HTML
def show_random_elements(dataset, num_examples=10):
assert num_examples <= len(dataset), "Can't pick more elements than there are in the dataset."
picks = []
for _ in range(num_examples):
pick = random.randint(0, len(dataset)-1)
while pick in picks:
pick = random.randint(0, len(dataset)-1)
picks.append(pick)
df = pd.DataFrame(dataset[picks])
display(HTML(df.to_html()))
show_random_elements(common_voice_train.remove_columns(["path", "audio"]), num_examples=10)
Oylar teker teker elle sayılacak.
Son olaylar endişe seviyesini yükseltti.
Tek bir kart hepsinin kapılarını açıyor.
Blogcular da tam bundan bahsetmek istiyor.
Bu Aralık iki bin onda oldu.
Fiyatın altmış altı milyon avro olduğu bildirildi.
Ardından da silahlı çatışmalar çıktı.
"Romanya'da kurumlar gelir vergisi oranı yüzde on altı."
Bu konuda neden bu kadar az şey söylendiğini açıklayabilir misiniz?
よし!転写はかなりきれいですね。転写された文を翻訳した結果、言語はノイズのない対話よりも書き出されたテキストに対応しているようです。これは、Common Voiceがクラウドソーシングされた音声コーパスであるため、理にかなっています。
転写には,.?!;:
などの特殊文字が含まれていることがわかります。言語モデルがないと、このような特殊文字を音響的な特徴音素に分類することはより困難です。なぜなら、特殊文字は実際には特徴的な音の単位に対応していないからです。たとえば、文字"s"
は比較的明確な音を持っていますが、特殊文字"."
にはありません。また、音声信号の意味を理解するためには、通常、特殊文字を転写に含める必要はありません。
単語の意味に貢献せず、音響的な音で表現することができないすべての文字を単純に削除し、テキストを正規化しましょう。
import re
chars_to_remove_regex = '[\,\?\.\!\-\;\:\"\“\%\‘\”\�\']'
def remove_special_characters(batch):
batch["sentence"] = re.sub(chars_to_remove_regex, '', batch["sentence"]).lower()
return batch
common_voice_train = common_voice_train.map(remove_special_characters)
common_voice_test = common_voice_test.map(remove_special_characters)
処理されたテキストラベルをもう一度見てみましょう。
show_random_elements(common_voice_train.remove_columns(["path","audio"]))
i̇kinci tur müzakereler eylül ayında başlayacak
jani ve babası bu düşüncelerinde yalnız değil
onurun gözlerindeki büyü
bandiç oyların yüzde kırk sekiz virgül elli dördünü topladı
bu imkansız
bu konu açık değildir
cinayet kamuoyunu şiddetle sarstı
kentin sokakları iki metre su altında kaldı
muhalefet partileri hükümete karşı ciddi bir mücadele ortaya koyabiliyorlar mı
festivale tüm dünyadan elli film katılıyor
素晴らしい!これはより良く見えますね。転写からほとんどの特殊文字を削除し、すべてを小文字に正規化しました。
前処理を最終化する前に、ターゲット言語のネイティブスピーカーに相談して、テキストをさらに簡素化できるかどうかを確認することは常に有利です。このブログ投稿では、Merveさんが快く見てくれて、「â」などの「帽子のついた」文字は実際にはもはやトルコ語では使用されておらず、その「帽子のない」相当する文字(たとえば「a」)で置き換えることができると指摘してくれました。
これは、例えば"yargı sistemi hâlâ sağlıksız"
という文を"yargı sistemi hala sağlıksız"
に置き換える必要があることを意味します。
テキストラベルをさらに簡素化するための別のマッピング関数を作成しましょう。覚えておいてください-テキストラベルが簡単であればあるほど、モデルがそれらのラベルを予測するのは簡単です。
def replace_hatted_characters(batch):
batch["sentence"] = re.sub('[â]', 'a', batch["sentence"])
batch["sentence"] = re.sub('[î]', 'i', batch["sentence"])
batch["sentence"] = re.sub('[ô]', 'o', batch["sentence"])
batch["sentence"] = re.sub('[û]', 'u', batch["sentence"])
return batch
common_voice_train = common_voice_train.map(replace_hatted_characters)
common_voice_test = common_voice_test.map(replace_hatted_characters)
CTCでは、音声チャンクを文字に分類することが一般的ですので、ここでも同じようにします。トレーニングデータとテストデータのすべての異なる文字を抽出し、この文字のセットからボキャブラリーを構築しましょう。
すべての転写を1つの長い転写に連結し、文字のセットに変換するマッピング関数を作成します。マッピング関数が一度にすべての転写にアクセスできるように、引数batched=True
をmap(...)
関数に渡すことは重要です。
def extract_all_chars(batch):
all_text = " ".join(batch["sentence"])
vocab = list(set(all_text))
return {"vocab": [vocab], "all_text": [all_text]}
vocab_train = common_voice_train.map(extract_all_chars, batched=True, batch_size=-1, keep_in_memory=True, remove_columns=common_voice_train.column_names)
vocab_test = common_voice_test.map(extract_all_chars, batched=True, batch_size=-1, keep_in_memory=True, remove_columns=common_voice_test.column_names)
今、トレーニングデータセットとテストデータセットのすべての重複しない文字の結合を作成し、その結果のリストを列挙された辞書に変換します。
vocab_list = list(set(vocab_train["vocab"][0]) | set(vocab_test["vocab"][0]))
vocab_dict = {v: k for k, v in enumerate(sorted(vocab_list))}
vocab_dict
{' ': 0,
'a': 1,
'b': 2,
'c': 3,
'd': 4,
'e': 5,
'f': 6,
'g': 7,
'h': 8,
'i': 9,
'j': 10,
'k': 11,
'l': 12,
'm': 13,
'n': 14,
'o': 15,
'p': 16,
'q': 17,
'r': 18,
's': 19,
't': 20,
'u': 21,
'v': 22,
'w': 23,
'x': 24,
'y': 25,
'z': 26,
'ç': 27,
'ë': 28,
'ö': 29,
'ü': 30,
'ğ': 31,
'ı': 32,
'ş': 33,
'̇': 34}
素晴らしいですね、データセットにはアルファベットのすべての文字が含まれていることがわかります(それほど驚くことではありません)。また、特殊文字""
と'
も抽出しました。特別な文字を除外しなかったのは、モデルが単語の終わりを予測することを学習しなければならないためです。さもなければ、予測は常に単語を区切ることができない文字のシーケンスになってしまいます。
モデルの訓練の前に前処理は非常に重要なステップであることを常に心に留めておくべきです。たとえば、データを正規化するのを忘れたためにa
とA
を区別することは望ましくありません。 a
とA
の違いは、文字の「音」には全く依存せず、むしろ文法的なルールに依存します。たとえば、文の最初に大文字の文字を使用します。したがって、大文字と小文字の文字の違いを取り除いて、モデルが音声を書き写すことを学びやすくする方が合理的です。
「 」が独自のトークンクラスを持つことがより明確になるように、より目立つ文字「 | 」を与えます。さらに、Common Voiceのトレーニングセットで出会わなかった文字に後でモデルが対応できるように、「不明な」トークンも追加します。
vocab_dict["|"] = vocab_dict[" "]
del vocab_dict[" "]
最後に、CTCの「ブランクトークン」に対応するパディングトークンも追加します。 「ブランクトークン」はCTCアルゴリズムの重要な要素です。詳細については、こちらの「Alignment」セクションをご覧ください。
vocab_dict["[UNK]"] = len(vocab_dict)
vocab_dict["[PAD]"] = len(vocab_dict)
len(vocab_dict)
37
素晴らしいですね、今やボキャブラリーは37個のトークンから成り立ち、トレーニング済みMMSチェックポイントの上に追加する線形層は出力次元37を持つことになります。
1つのMMSチェックポイントは複数の言語に対してカスタマイズされた重みを提供できるため、トークナイザーも複数のボキャブラリーで構成される場合があります。そのため、将来的にボキャブラリーにさらに言語を追加するために、vocab_dict
をネストする必要があります。ネストされた辞書には、アダプターの重みで使用される名前と、トークナイザーの構成でtarget_lang
という名前で保存される名前が含まれるべきです。
元のmms-1b-all
チェックポイントと同様に、ISO-639-3言語コードを使用しましょう。
target_lang = "tur"
作成したボキャブラリーを追加するために、空の辞書を定義しましょう。
new_vocab_dict = {target_lang: vocab_dict}
注意:既存のモデルリポジトリに新しいアダプターレイヤーを追加するためにこのノートブックを使用する場合、新しい空のボキャブラリーディクショナリを作成せずに、既に存在するものを再利用するようにしてください。これを行うには、次のセルのコメントを解除し、「patrickvonplaten/wav2vec2-large-mms-1b-turkish-colab」をアダプターの重みを追加したいモデルリポジトリのIDに置き換えてください。
# from transformers import Wav2Vec2CTCTokenizer
# mms_adapter_repo = "patrickvonplaten/wav2vec2-large-mms-1b-turkish-colab" # このパスは新しいアダプタの重みを追加するリポジトリに置き換えてください
# tokenizer = Wav2Vec2CTCTokenizer.from_pretrained(mms_adapter_repo)
# new_vocab = tokenizer.vocab
# new_vocab[target_lang] = vocab_dict
さて、語彙をjsonファイルとして保存しましょう。
import json
with open('vocab.json', 'w') as vocab_file:
json.dump(new_vocab_dict, vocab_file)
最後のステップでは、jsonファイルを使用してWav2Vec2CTCTokenizer
クラスのインスタンスに語彙をロードします。
from transformers import Wav2Vec2CTCTokenizer
tokenizer = Wav2Vec2CTCTokenizer.from_pretrained("./", unk_token="[UNK]", pad_token="[PAD]", word_delimiter_token="|", target_lang=target_lang)
このノートブックのファインチューニング済みモデルで作成したトークナイザを再利用したい場合、tokenizer
を🤗 Hubにアップロードすることを強くお勧めします。アップロード先のリポジトリを"wav2vec2-large-mms-1b-turkish-colab"
と呼びましょう:
repo_name = "wav2vec2-large-mms-1b-turkish-colab"
そして、トークナイザを🤗 Hubにアップロードしましょう。
tokenizer.push_to_hub(repo_name)
CommitInfo(commit_url='https://huggingface.co/patrickvonplaten/wav2vec2-large-mms-1b-turkish-colab/commit/48cccbfd6059aa6ce655e9d94b8358ba39536cb7', commit_message='Upload tokenizer', commit_description='', oid='48cccbfd6059aa6ce655e9d94b8358ba39536cb7', pr_url=None, pr_revision=None, pr_num=None)
素晴らしいですね。今作成したリポジトリはhttps://huggingface.co/<your-username>/wav2vec2-large-mms-1b-tr-colab
で確認できます。
Wav2Vec2FeatureExtractor
の作成
音声は連続信号であり、コンピュータで処理するためにはまず離散化する必要があります。これは通常サンプリングと呼ばれます。ここで、サンプリングレートは、1秒あたりに測定される音声信号のデータポイント数を定義する重要な役割を果たします。したがって、サンプリングレートが高いほど、実際の音声信号により近い近似が得られますが、1秒あたりの値も増える必要があります。
事前学習済みのチェックポイントは、入力データがモデルの学習に使用されたデータとほぼ同じ分布からサンプリングされたものであることを期待しています。同じ音声信号を異なるサンプリングレートでサンプリングすると、例えば、サンプリングレートを倍にするとデータポイントが2倍になります。したがって、ASRモデルの事前学習済みチェックポイントをファインチューニングする前に、モデルをファインチューニングするために使用されたデータのサンプリングレートが、モデルの事前学習に使用されたデータのサンプリングレートと一致することを確認することが重要です。
Wav2Vec2FeatureExtractor
オブジェクトをインスタンス化するには、次のパラメータが必要です:
feature_size
:音声モデルは、入力としての特徴ベクトルのシーケンスを取ります。このシーケンスの長さは明らかに異なりますが、特徴のサイズは異なるべきではありません。Wav2Vec2の場合、特徴のサイズは1です。なぜなら、モデルは生の音声信号2 {}^2 2 を学習したからです。sampling_rate
:モデルの学習に使用されたサンプリングレートです。padding_value
:バッチ推論のために、より短い入力は特定の値でパディングする必要があります。do_normalize
:入力をゼロ平均単位分散で正規化するかどうか。通常、音声モデルは入力を正規化すると性能が向上します。return_attention_mask
:モデルがバッチ推論にattention_mask
を使用するかどうか。一般的に、XLS-Rモデルのチェックポイントではattention_mask
を常に使用する必要があります。
from transformers import Wav2Vec2FeatureExtractor
feature_extractor = Wav2Vec2FeatureExtractor(feature_size=1, sampling_rate=16000, padding_value=0.0, do_normalize=True, return_attention_mask=True)
素晴らしい、MMSの特徴抽出パイプラインが完全に定義されました!
ユーザーフレンドリーにするために、特徴抽出器とトークナイザーはWav2Vec2Processor
クラスに統合され、モデルとプロセッサオブジェクトだけで済むようになっています。
from transformers import Wav2Vec2Processor
processor = Wav2Vec2Processor(feature_extractor=feature_extractor, tokenizer=tokenizer)
次に、データセットを準備できます。
データの前処理
これまで、音声信号の実際の値ではなく、転写のみを見てきました。 sentence
に加えて、データセットにはpath
とaudio
という2つの列名があります。 path
は音声ファイルの絶対パスを示し、audio
はすでにロードされたオーディオデータを表します。 MMSは、入力を16 kHzの1次元配列の形式で期待しています。つまり、オーディオファイルをロードしてリサンプリングする必要があります。
ありがたいことに、datasets
は列名がaudio
の場合にはこれを自動的に行います。試してみましょう。
common_voice_train[0]["audio"]
{'path': '/root/.cache/huggingface/datasets/downloads/extracted/71ba9bd154da9d8c769b736301417178729d2b87b9e00cda59f6450f742ed778/cv-corpus-6.1-2020-12-11/tr/clips/common_voice_tr_17346025.mp3',
'array': array([ 0.00000000e+00, -2.98378618e-13, -1.59835903e-13, ...,
-2.01663317e-12, -1.87991593e-12, -1.17969588e-12]),
'sampling_rate': 48000}
上記の例では、オーディオデータが48kHzのサンプリングレートでロードされており、モデルは16kHzを期待していることがわかります。 cast_column
を使用して、オーディオフィーチャを正しいサンプリングレートに設定できます。
common_voice_train = common_voice_train.cast_column("audio", Audio(sampling_rate=16_000))
common_voice_test = common_voice_test.cast_column("audio", Audio(sampling_rate=16_000))
再び"audio"
を見てみましょう。
common_voice_train[0]["audio"]
{'path': '/root/.cache/huggingface/datasets/downloads/extracted/71ba9bd154da9d8c769b736301417178729d2b87b9e00cda59f6450f742ed778/cv-corpus-6.1-2020-12-11/tr/clips/common_voice_tr_17346025.mp3',
'array': array([ 9.09494702e-13, -6.13908924e-12, -1.09139364e-11, ...,
1.81898940e-12, 4.54747351e-13, 3.63797881e-12]),
'sampling_rate': 16000}
うまくいったようです!音声データは1次元配列であり、サンプリングレートは常に16kHzに対応しており、ターゲットテキストは正規化されています。
rand_int = random.randint(0, len(common_voice_train)-1)
print("ターゲットテキスト:", common_voice_train[rand_int]["sentence"])
print("入力配列の形状:", common_voice_train[rand_int]["audio"]["array"].shape)
print("サンプリングレート:", common_voice_train[rand_int]["audio"]["sampling_rate"])
ターゲットテキスト: bağış anlaşması bir ağustosta imzalandı
入力配列の形状: (70656,)
サンプリングレート: 16000
よし!すべてうまく見えます – データは1次元配列で、サンプリングレートは常に16kHzに対応しており、ターゲットテキストは正規化されています。
最後に、Wav2Vec2Processor
を使用して、トレーニングに必要な形式にデータを処理できます。それを行うために、Datasetのmap(...)
関数を使用します。
まず、オーディオデータをロードしてリサンプリングします。単にbatch["audio"]
を呼び出すだけです。次に、ロードされたオーディオファイルからinput_values
を抽出します。この場合、Wav2Vec2Processor
はデータを正規化するだけです。ただし、他の音声モデルの場合、このステップにはLog-Mel特徴抽出など、より複雑な特徴抽出が含まれることがあります。最後に、トランスクリプションをラベルIDにエンコードします。
注意: このマッピング関数は、Wav2Vec2Processor
クラスの使用方法の良い例です。通常のコンテキストでは、processor(...)
を呼び出すと、Wav2Vec2FeatureExtractor
の呼び出しメソッドにリダイレクトされます。ただし、プロセッサをas_target_processor
コンテキストにラップすると、同じメソッドがWav2Vec2CTCTokenizer
の呼び出しメソッドにリダイレクトされます。詳細については、ドキュメントをご確認ください。
def prepare_dataset(batch):
audio = batch["audio"]
# バッチの出力を"アンバッチ"する
batch["input_values"] = processor(audio["array"], sampling_rate=audio["sampling_rate"]).input_values[0]
batch["input_length"] = len(batch["input_values"])
batch["labels"] = processor(text=batch["sentence"]).input_ids
return batch
すべての例にデータ準備関数を適用しましょう。
common_voice_train = common_voice_train.map(prepare_dataset, remove_columns=common_voice_train.column_names)
common_voice_test = common_voice_test.map(prepare_dataset, remove_columns=common_voice_test.column_names)
注意: datasets
はオーディオのロードとリサンプリングを自動的に処理します。独自のカスタマイズされたデータのロード/サンプリングを実装したい場合は、"path"
列を使用し、"audio"
列を無視してください。
素晴らしいですね、これでトレーニングを開始する準備が整いました!
トレーニング
データは処理され、トレーニングパイプラインの設定を開始する準備が整いました。トレーニングには🤗のTrainerを使用します。以下の手順を実行する必要があります:
-
データコレーターを定義する。ほとんどのNLPモデルとは異なり、MMSは入力長が出力長よりもはるかに大きいです。たとえば、入力長が50000のサンプルの出力長は100を超えません。大きな入力サイズの場合、トレーニングバッチを動的にパディングすることが効率的です。つまり、すべてのトレーニングサンプルは、バッチ内の最長サンプルにのみパディングされ、全体の最長サンプルにはパディングされません。そのため、MMSのファインチューニングには特別なパディングデータコレーターが必要であり、以下で定義します
-
評価メトリクスを定義する。トレーニング中にモデルは単語エラーレートで評価されるべきです。適切な
compute_metrics
関数を定義する必要があります -
事前学習済みのチェックポイントをロードする。事前学習済みのチェックポイントをロードし、トレーニングに適切に設定する必要があります。
-
トレーニングの構成を定義する。
モデルをファインチューニングした後、テストデータで正しく評価し、音声の正しい転写を学習したことを確認します。
Trainerの設定
まず、データコレーターを定義しましょう。データコレーターのコードは、この例からコピーされました。
詳細には立ち入りませんが、一般的なデータコレーターとは異なり、このデータコレーターはinput_values
とlabels
を異なる方法で扱い、それぞれに別々のパディング関数を適用します(再びMMSプロセッサのコンテキストマネージャーを使用します)。これは、音声認識では入力と出力が異なるモダリティであるため、同じパディング関数で処理すべきではないためです。一般的なデータコレーターと同様に、ラベルのパディングトークンは-100
で置き換えられるため、それらのトークンは損失の計算に含まれません。
import torch
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Union
@dataclass
class DataCollatorCTCWithPadding:
"""
受け取った入力を動的にパディングするデータコレータ。
Args:
processor (:class:`~transformers.Wav2Vec2Processor`)
データの前処理に使用するプロセッサ。
padding (:obj:`bool`, :obj:`str`または:class:`~transformers.tokenization_utils_base.PaddingStrategy`, `optional`, デフォルトは :obj:`True`)
パディング戦略を選択して、返されるシーケンスをパディングする。以下のいずれか:
* :obj:`True`または :obj:`'longest'`: バッチ内の最長のシーケンスにパディングする(単一のシーケンスの場合はパディングなし)。
* :obj:`'max_length'`: 引数 :obj:`max_length` で指定された最大長さまでパディングするか、モデルに許容される最大の入力長さまでパディングする。
* :obj:`False`または :obj:`'do_not_pad'` (デフォルト): パディングしない(つまり、長さが異なるシーケンスのバッチを出力できる)。
"""
processor: Wav2Vec2Processor
padding: Union[bool, str] = True
def __call__(self, features: List[Dict[str, Union[List[int], torch.Tensor]]]) -> Dict[str, torch.Tensor]:
# 入力とラベルを分割する(長さが異なるため、異なるパディング方法が必要)
input_features = [{"input_values": feature["input_values"]} for feature in features]
label_features = [{"input_ids": feature["labels"]} for feature in features]
batch = self.processor.pad(
input_features,
padding=self.padding,
return_tensors="pt",
)
labels_batch = self.processor.pad(
labels=label_features,
padding=self.padding,
return_tensors="pt",
)
# ロスを正しく無視するためにパディングを -100 に置き換える
labels = labels_batch["input_ids"].masked_fill(labels_batch.attention_mask.ne(1), -100)
batch["labels"] = labels
return batch
data_collator = DataCollatorCTCWithPadding(processor=processor, padding=True)
次に、評価メトリックを定義します。ASRで主要なメトリックは単語エラーレート(WER)ですので、このノートブックでも使用します。
from evaluate import load
wer_metric = load("wer")
モデルは、ログットベクトルのシーケンスを返します: y 1 , … , y m \mathbf{y}_1, \ldots, \mathbf{y}_m y 1 , … , y m ただし、y 1 = f θ ( x 1 , … , x n ) [ 0 ] \mathbf{y}_1 = f_{\theta}(x_1, \ldots, x_n)[0] y 1 = f θ ( x 1 , … , x n ) [ 0 ] および n > > m n >> m n > > m です。
ログットベクトル y 1 \mathbf{y}_1 y 1 には、前に定義した語彙の各単語の対数オッズが含まれているため、len ( y i ) = \text{len}(\mathbf{y}_i) = len ( y i ) = config.vocab_size
です。モデルの最も可能性の高い予測を求めるために、ログットの argmax(...)
を取ります。また、エンコードされたラベルを元の文字列に戻すために、-100
を pad_token_id
で置き換え、連続するトークンが CTC スタイルで同じトークンにグループ化されないように、ID をデコードします 1 {}^1 1 。
def compute_metrics(pred):
pred_logits = pred.predictions
pred_ids = np.argmax(pred_logits, axis=-1)
pred.label_ids[pred.label_ids == -100] = processor.tokenizer.pad_token_id
pred_str = processor.batch_decode(pred_ids)
# メトリックを計算する際にトークンをグループ化したくないため
label_str = processor.batch_decode(pred.label_ids, group_tokens=False)
wer = wer_metric.compute(predictions=pred_str, references=label_str)
return {"wer": wer}
それでは、mms-1b-all
の事前学習済みチェックポイントをロードできます。トークナイザの pad_token_id
はモデルの pad_token_id
または Wav2Vec2ForCTC
の CTC のブランクトークン 2 {}^2 2 を定義する必要があります。
小さな重みのサブセットのみをトレーニングしているため、モデルは過学習しにくいです。したがって、すべてのドロップアウト層を無効にすることを確認します。
注意:このノートブックを他の言語のCommon VoiceでMMSをトレーニングする場合、これらのハイパーパラメータ設定はうまく機能しないかもしれません。使用ケースに応じて適応してください。
from transformers import Wav2Vec2ForCTC
model = Wav2Vec2ForCTC.from_pretrained(
"facebook/mms-1b-all",
attention_dropout=0.0,
hidden_dropout=0.0,
feat_proj_dropout=0.0,
layerdrop=0.0,
ctc_loss_reduction="mean",
pad_token_id=processor.tokenizer.pad_token_id,
vocab_size=len(processor.tokenizer),
ignore_mismatched_sizes=True,
)
Wav2Vec2ForCTCの一部の重みがfacebook/mms-1b-allのモデルチェックポイントから初期化されず、形状が一致しなかったため、新しく初期化されました:
- lm_head.bias:チェックポイントではtorch.Size([154])の形状が見つかり、モデルのインスタンス化ではtorch.Size([39])の形状が見つかりました
- lm_head.weight:チェックポイントではtorch.Size([154, 1280])の形状が見つかり、モデルのインスタンス化ではtorch.Size([39, 1280])の形状が見つかりました
推論と予測に使用するために、おそらくこのモデルを下流のタスクでトレーニングする必要があります。
注意:一部の重みが新しく初期化されることが予想されます。これらの重みは新しく初期化されたボキャブラリの出力層に対応します。
これで、アダプターの重みのみがトレーニングされ、モデルの残りの部分はフリーズされるようにしたいと思います。
まず、便利なinit_adapter_layers
メソッドを使用して、すべてのアダプターの重みを再初期化します。アダプターの重みを再初期化せずにファインチューニングを続けることも可能ですが、その場合はトレーニング前にload_adapter(...)
メソッドを使用して適切なアダプターの重みをロードする必要があります。ただし、ボキャブラリは通常、カスタムのトレーニングデータとはあまりよく一致しない場合が多いため、アダプターレイヤーをすべて再初期化して簡単にファインチューニングできるようにする方が通常は簡単です。
model.init_adapter_layers()
次に、アダプターレイヤーを除いてすべての重みをフリーズします。
model.freeze_base_model()
adapter_weights = model._get_adapters()
for param in adapter_weights.values():
param.requires_grad = True
最後のステップでは、トレーニングに関連するすべてのパラメータを定義します。いくつかのパラメータの詳細について説明すると:
group_by_length
は、類似の入力長を持つトレーニングサンプルを1つのバッチにグループ化することで、トレーニングを効率化します。これにより、モデルを通過する不要なパディングトークンの総数が大幅に減少し、トレーニング時間が大幅に短縮されます。learning_rate
は、Adamでのトレーニングのための一般的なデフォルト値である1e-3が選ばれています。他の学習率でも同じくらいうまく機能する場合があります。
他のパラメータの詳細については、ドキュメントを参照してください。GPUメモリの節約のために、PyTorchの勾配チェックポイントを有効にし、損失の削減を「mean」に設定します。MMSアダプターのファインチューニングは非常に高速に非常に良いパフォーマンスに収束するため、4時間のデータセットでも4エポックしかトレーニングしません。トレーニング中に、チェックポイントが200ステップごとに非同期でハブにアップロードされます。モデルがまだトレーニング中でも、デモウィジェットで遊ぶことができます。
注意:モデルのチェックポイントをハブにアップロードしたくない場合は、push_to_hub=False
に設定します。
from transformers import TrainingArguments
training_args = TrainingArguments(
output_dir=repo_name,
group_by_length=True,
per_device_train_batch_size=32,
evaluation_strategy="steps",
num_train_epochs=4,
gradient_checkpointing=True,
fp16=True,
save_steps=200,
eval_steps=100,
logging_steps=100,
learning_rate=1e-3,
warmup_steps=100,
save_total_limit=2,
push_to_hub=True,
)
これで、すべてのインスタンスをTrainerに渡して、トレーニングを開始する準備が整いました!
from transformers import Trainer
trainer = Trainer(
model=model,
data_collator=data_collator,
args=training_args,
compute_metrics=compute_metrics,
train_dataset=common_voice_train,
eval_dataset=common_voice_test,
tokenizer=processor.feature_extractor,
)
1 {}^1 1 モデルが話者の速度に依存しないようにするために、CTCでは連続する同じトークンは単一のトークンとしてグループ化されます。しかし、エンコードされたラベルはデコードする際にはグループ化されず、モデルの予測トークンに対応しないため、group_tokens=False
パラメータを渡す必要があります。このパラメータを渡さない場合、"hello"
のような単語は誤ってエンコードされ、"helo"
としてデコードされます。2 {}^2 2 ブランクトークンは、モデルが"hello"
のような単語を予測するために、2つのlの間にブランクトークンを挿入するように強制します。モデルのCTC準拠の予測では、[PAD] [PAD] "h" "e" "e" "l" "l" [PAD] "l" "o" "o" [PAD]
となります。
トレーニング
使用するGPUによっては、トレーニングには30分以下かかります。
trainer.train()
トレーニングの損失と検証のWERはうまく下がります。
mms-1b-all
のアダプターレイヤーを100ステップだけファインチューニングする方が、すでにここで示されているxls-r-300m
全体をファインチューニングするよりもはるかに優れています。
公式の論文とこの簡単な比較から明らかなように、mms-1b-all
は低リソースの言語に対する知識の転送能力がはるかに高く、xls-r-300m
よりも優れているため、mms-1b-all
を選択するべきです。さらに、トレーニングもメモリ効率が良くなり、一部のレイヤーのみがトレーニングされます。
アダプターの重みはモデルのチェックポイントとしてアップロードされますが、オフロードとオンロードを容易にするために、それらを別個に保存することも確認する必要があります。
トレーニングの出力ディレクトリにすべてのアダプターレイヤーを保存して、正しくHubにアップロードできるようにしましょう。
from safetensors.torch import save_file as safe_save_file
from transformers.models.wav2vec2.modeling_wav2vec2 import WAV2VEC2_ADAPTER_SAFE_FILE
import os
adapter_file = WAV2VEC2_ADAPTER_SAFE_FILE.format(target_lang)
adapter_file = os.path.join(training_args.output_dir, adapter_file)
safe_save_file(model._get_adapters(), adapter_file, metadata={"format": "pt"})
最後に、トレーニングの結果を🤗 Hubにアップロードできます。
trainer.push_to_hub()
アダプターの重みのトレーニングの主な利点の一つは、モデルの重みのうち約99%を占める「ベース」モデルが変更されずに保持されることで、トレーニング済みのチェックポイントを使用するためには小さな2.5Mのアダプターチェックポイントを共有するだけで済むことです。
これにより、追加のアダプターレイヤーを簡単にトレーニングしてリポジトリに追加することができます。
これを行うには、このスクリプトを再実行し、トレーニングする言語を異なる言語(例:スウェーデン語の場合はswe
)に変更するだけで簡単に行うことができます。また、新しい言語の語彙が既存の語彙を完全に上書きしないようにする必要がありますが、上のコメントアウトされたセルで述べられているように、新しい言語の語彙が既存の語彙に追加されるようにする必要があります。
異なるアダプターレイヤーをロードする方法を示すために、スウェーデン語用のアダプターレイヤーもトレーニングしてアップロードしました(iso言語コードswe
で示されています)。
通常どおりfrom_pretrained(...)
を使用してファインチューニングされたチェックポイントをロードすることができますが、メソッドにtarget_lang="<your-lang-code>"
を追加して、正しいアダプターがロードされるようにする必要があります。また、トークナイザーのターゲット言語も正しく設定する必要があります。
まず、トルコ語のチェックポイントをロードする方法を見てみましょう。
model_id = "patrickvonplaten/wav2vec2-large-mms-1b-turkish-colab"
model = Wav2Vec2ForCTC.from_pretrained(model_id, target_lang="tur").to("cuda")
processor = Wav2Vec2Processor.from_pretrained(model_id)
processor.tokenizer.set_target_lang("tur")
モデルがトルコ語を正しく転写できるか確認しましょう
from datasets import Audio
common_voice_test_tr = load_dataset("mozilla-foundation/common_voice_6_1", "tr", data_dir="./cv-corpus-6.1-2020-12-11", split="test", use_auth_token=True)
common_voice_test_tr = common_voice_test_tr.cast_column("audio", Audio(sampling_rate=16_000))
音声を処理し、順方向のパスを実行してIDを予測しましょう
input_dict = processor(common_voice_test_tr[0]["audio"]["array"], sampling_rate=16_000, return_tensors="pt", padding=True)
logits = model(input_dict.input_values.to("cuda")).logits
pred_ids = torch.argmax(logits, dim=-1)[0]
最後に、例をデコードできます。
print("予測結果:")
print(processor.decode(pred_ids))
print("\n参照:")
print(common_voice_test_tr[0]["sentence"].lower())
出力 :
予測結果:
pekçoğuda roman toplumundan geliyor
参照:
pek çoğu da roman toplumundan geliyor.
<pこれはほぼ正しいようですが、最初の単語に2つの空白が追加されるべきでした。アダプタをスウェーデン語に変更するには、model.load_adapter(...)
を呼び出してアダプタを変更し、トークナイザもスウェーデン語に変更するだけです。
model.load_adapter("swe")
processor.tokenizer.set_target_lang("swe")
再びスウェーデン語のテストセットをcommon voiceからロードします
common_voice_test_swe = load_dataset("mozilla-foundation/common_voice_6_1", "sv-SE", data_dir="./cv-corpus-6.1-2020-12-11", split="test", use_auth_token=True)
common_voice_test_swe = common_voice_test_swe.cast_column("audio", Audio(sampling_rate=16_000))
そして、サンプルを転写します:
input_dict = processor(common_voice_test_swe[0]["audio"]["array"], sampling_rate=16_000, return_tensors="pt", padding=True)
logits = model(input_dict.input_values.to("cuda")).logits
pred_ids = torch.argmax(logits, dim=-1)[0]
print("予測結果:")
print(processor.decode(pred_ids))
print("\n参照:")
print(common_voice_test_swe[0]["sentence"].lower())
出力 :
予測結果:
jag lämnade grovjobbet åt honom
参照:
jag lämnade grovjobbet åt honom.
素晴らしい、これは完璧な転写のようです!
このブログ記事では、MMS Adapter Weightsファインチューニングが、低リソースの言語で最先端のパフォーマンスを提供するだけでなく、トレーニング時間を大幅に短縮し、カスタマイズされたアダプタウェイトのコレクションを簡単に構築できることを示しました。
関連する記事や追加のリンクは以下にリストされています:
- 公式論文
- 元のcobebase
- 公式デモ
- Transformersドキュメント
- 関連するXLS-Rブログ記事
- ハブ上のモデル
We will continue to update VoAGI; if you have any questions or suggestions, please contact us!
Was this article helpful?
93 out of 132 found this helpful
Related articles