為多語言 ASR 微調 MMS 介面卡模型
新(2023 年 6 月):這篇博文深受《在多語言 ASR 上微調 XLS-R》的啟發,可視為其改進版。
Wav2Vec2 是一個用於自動語音識別 (ASR) 的預訓練模型,由 Alexei Baevski、Michael Auli 和 Alex Conneau 於 2020 年 9 月釋出。Wav2Vec2 在最流行的英文 ASR 資料集 LibriSpeech 上展示出強大效能後不久,Facebook AI 推出了兩個 Wav2Vec2 的多語言版本,分別稱為 XLSR 和 XLM-R,能夠識別多達 128 種語言的語音。XLSR 代表跨語言語音表示,指的是模型學習在多種語言中有用的語音表示的能力。
Meta AI 最新發布的 大規模多語言語音 (MMS),由 Vineel Pratap、Andros Tjandra、Bowen Shi 等人完成,將多語言語音表示提升到新水平。透過釋出的各種語言識別、語音識別和文字到語音檢查點,可以識別、轉錄和生成超過 1,100 種口語。
在這篇博文中,我們將展示 MMS 的介面卡訓練如何在僅 10-20 分鐘的微調後,達到驚人的低詞錯誤率。
對於低資源語言,我們強烈建議使用 MMS 的介面卡訓練,而不是像《在多語言 ASR 上微調 XLS-R》中所做的那樣微調整個模型。
在我們的實驗中,MMS 的介面卡訓練在記憶體效率、魯棒性方面都更好,並且在低資源語言上產生更好的效能。然而,對於中高資源語言,微調整個檢查點而不是使用介面卡層可能仍然更有利。
保護世界語言多樣性
根據 https://www.ethnologue.com/ 的資料,約 3000 種,即 40% 的“活語言”,由於母語使用者越來越少而面臨瀕危。這種趨勢在全球化日益加劇的世界中只會繼續。
MMS 能夠轉錄許多瀕危語言,例如 Ari 或 Kaivi。未來,MMS 可以透過幫助剩餘的使用者建立書面記錄並用母語交流,在保持語言活力方面發揮至關重要的作用。
為了適應 1000 多個不同的詞彙,MMS 使用了介面卡——一種僅訓練模型權重一小部分的訓練方法。
介面卡層就像語言橋樑,使模型能夠利用一種語言的知識來破譯另一種語言。
MMS 微調
MMS 無監督檢查點在超過 50 萬小時的音訊上進行了預訓練,涵蓋 1400 多種語言,引數量從 3 億到 10 億不等。
您可以在 🤗 Hub 上找到 3 億引數 (300M) 和 10 億引數 (1B) 模型大小的僅預訓練檢查點
注意:如果您想微調基礎模型,可以按照“在多語言 ASR 上微調 XLS-R”中所示的完全相同的方式進行操作。
與BERT 的掩碼語言建模目標類似,MMS 透過在自監督預訓練期間將特徵向量隨機掩碼然後將其傳遞給 Transformer 網路來學習上下文語音表示。
對於 ASR,預訓練的 MMS-1B
檢查點在 1000 多種語言上透過聯合詞彙輸出層進一步進行了監督微調。作為最後一步,聯合詞彙輸出層被丟棄,並保留了特定於語言的介面卡層。每個介面卡層僅包含約 2.5M 個權重,由每個注意力塊的小型線性投影層以及一個特定於語言的詞彙輸出層組成。
已釋出三個用於語音識別 (ASR) 的MMS微調檢查點。它們分別包含 102、1107 和 1162 個介面卡權重(每種語言一個)
您可以看到基礎模型(像往常一樣)儲存為 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
的介面卡權重 🔥
Notebook 設定
正如之前在“在多語言 ASR 上微調 XLS-R”博文中所做的那樣,我們將在 Common Voice 的低資源 ASR 資料集上微調模型,該資料集僅包含約 4 小時的驗證訓練資料。
就像 Wav2Vec2 或 XLS-R 一樣,MMS 使用連線主義時間分類 (CTC) 進行微調,CTC 是一種用於訓練神經網路解決序列到序列問題(例如 ASR 和手寫識別)的演算法。
有關 CTC 演算法的更多詳細資訊,我強烈推薦閱讀 Awni Hannun 撰寫的精彩博文《使用 CTC 進行序列建模 (2017)》。
在開始之前,讓我們安裝 datasets
和 transformers
。此外,我們還需要 torchaudio
來載入音訊檔案,以及 jiwer
來使用詞錯誤率 (WER) 指標 評估我們微調過的模型。
%%capture
!pip install --upgrade pip
!pip install datasets[audio]
!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 模型,例如 mms-1b-all
,已經有一個隨模型檢查點附帶的分詞器。然而,由於我們希望在特定低資源語言的資料上微調模型,建議完全移除該分詞器和詞彙輸出層,並簡單地根據訓練資料本身建立新的。
在 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 中,通常將語音塊分類為字母,所以我們在這裡也這樣做。讓我們提取訓練和測試資料中所有不同的字母,並從這個字母集合構建我們的詞彙表。
我們編寫了一個對映函式,將所有轉錄連線成一個長轉錄,然後將字串轉換為一組字元。重要的是將引數 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 演算法的核心組成部分。有關更多資訊,請參閱此處的“對齊”部分。
vocab_dict["[UNK]"] = len(vocab_dict)
vocab_dict["[PAD]"] = len(vocab_dict)
len(vocab_dict)
37
酷,現在我們的詞彙表已完成,包含 37 個標記,這意味著我們將作為介面卡權重的一部分新增到預訓練 MMS 檢查點頂部的線性層將具有 37 的輸出維度。
由於單個 MMS 檢查點可以為多種語言提供定製的權重,因此分詞器也可以由多個詞彙表組成。因此,我們需要巢狀我們的 vocab_dict
,以便將來可以向詞彙表中新增更多語言。該字典應與用於介面卡權重的名稱進行巢狀,並且該名稱儲存在分詞器配置中,名稱為 target_lang
。
讓我們使用 ISO-639-3 語言程式碼,就像原始的 mms-1b-all
檢查點一樣。
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" # make sure to replace this path with a repo to which you want to add your new adapter weights
# 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/<您的使用者名稱>/wav2vec2-large-mms-1b-tr-colab
下看到剛剛建立的儲存庫
建立 Wav2Vec2FeatureExtractor
語音是一種連續訊號,要由計算機處理,它首先必須離散化,這通常稱為取樣。取樣率在此處扮演重要角色,因為它定義了每秒測量多少語音訊號資料點。因此,以更高的取樣率取樣會更好地逼近真實語音訊號,但每秒也需要更多值。
預訓練的檢查點期望其輸入資料以與訓練時使用的資料大致相同的分佈進行取樣。以兩種不同速率取樣的相同語音訊號具有非常不同的分佈,例如,取樣率加倍會導致資料點數量加倍。因此,在微調 ASR 模型的預訓練檢查點之前,驗證用於預訓練模型的資料取樣率是否與用於微調模型的資料集的取樣率匹配至關重要。
一個 Wav2Vec2FeatureExtractor
物件需要以下引數才能例項化
feature_size
: 語音模型將特徵向量序列作為輸入。雖然這個序列的長度顯然是變化的,但特徵大小不應該變化。在 Wav2Vec2 的情況下,特徵大小為 1,因為模型是在原始語音訊號上訓練的 。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
類中,這樣只需要一個 model
和 processor
物件。
from transformers import Wav2Vec2Processor
processor = Wav2Vec2Processor(feature_extractor=feature_extractor, tokenizer=tokenizer)
接下來,我們可以準備資料集了。
預處理資料
到目前為止,我們只查看了語音訊號的轉錄,而沒有檢視實際值。除了 sentence
之外,我們的資料集中還包含另外兩個列名 path
和 audio
。path
表示音訊檔案的絕對路徑,audio
表示已載入的音訊資料。MMS 期望輸入格式為 16 kHz 的一維陣列。這意味著必須載入並重新取樣音訊檔案。
值得慶幸的是,當列名為 audio
時,datasets
會自動完成此操作。我們來試一下。
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}
這似乎奏效了!讓我們最後檢查一下資料是否正確準備,打印出語音輸入的形狀、其轉錄以及相應的取樣率。
rand_int = random.randint(0, len(common_voice_train)-1)
print("Target text:", common_voice_train[rand_int]["sentence"])
print("Input array shape:", common_voice_train[rand_int]["audio"]["array"].shape)
print("Sampling rate:", common_voice_train[rand_int]["audio"]["sampling_rate"])
Target text: bağış anlaşması bir ağustosta imzalandı
Input array shape: (70656,)
Sampling rate: 16000
好的!一切看起來都沒問題——資料是一維陣列,取樣率總是 16kHz,目標文字也已規範化。
最後,我們可以利用 Wav2Vec2Processor
將資料處理成 Wav2Vec2ForCTC
訓練所需的格式。為此,我們使用 Dataset 的 map(...)
函式。
首先,我們透過呼叫 batch["audio"]
來載入並重新取樣音訊資料。其次,我們從載入的音訊檔案中提取 input_values
。在我們的例子中,Wav2Vec2Processor
僅對資料進行歸一化。然而,對於其他語音模型,此步驟可能包含更復雜的特徵提取,例如對數梅爾特徵提取。第三,我們將轉錄編碼為標籤 ID。
注意:這個對映函式是 Wav2Vec2Processor
類應如何使用的很好的例子。在“正常”情況下,呼叫 processor(...)
會被重定向到 Wav2Vec2FeatureExtractor
的呼叫方法。然而,當將處理器封裝到 as_target_processor
上下文中時,相同的方法會被重定向到 Wav2Vec2CTCTokenizer
的呼叫方法。有關更多資訊,請查閱文件。
def prepare_dataset(batch):
audio = batch["audio"]
# batched output is "un-batched"
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
函式。載入預訓練檢查點。我們需要載入預訓練檢查點並對其進行正確配置以進行訓練。
定義訓練配置。
在微調模型後,我們將在測試資料上對其進行正確評估,並驗證它確實學會了正確轉寫語音。
設定訓練器
我們從定義資料收集器開始。資料收集器的程式碼複製自這個示例。
不贅述過多細節,與常見資料收集器不同,此資料收集器對 input_values
和 labels
進行不同處理,因此對它們應用了兩個獨立的填充函式(再次利用 MMS 處理器的上下文管理器)。這是必要的,因為在語音識別中,輸入和輸出是不同的模態,因此它們不應由相同的填充函式處理。與常見資料收集器類似,標籤中的填充標記用 -100
填充,以便在計算損失時不考慮這些標記。
import torch
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Union
@dataclass
class DataCollatorCTCWithPadding:
"""
Data collator that will dynamically pad the inputs received.
Args:
processor (:class:`~transformers.Wav2Vec2Processor`)
The processor used for proccessing the data.
padding (:obj:`bool`, :obj:`str` or :class:`~transformers.tokenization_utils_base.PaddingStrategy`, `optional`, defaults to :obj:`True`):
Select a strategy to pad the returned sequences (according to the model's padding side and padding index)
among:
* :obj:`True` or :obj:`'longest'`: Pad to the longest sequence in the batch (or no padding if only a single
sequence if provided).
* :obj:`'max_length'`: Pad to a maximum length specified with the argument :obj:`max_length` or to the
maximum acceptable input length for the model if that argument is not provided.
* :obj:`False` or :obj:`'do_not_pad'` (default): No padding (i.e., can output a batch with sequences of
different lengths).
"""
processor: Wav2Vec2Processor
padding: Union[bool, str] = True
def __call__(self, features: List[Dict[str, Union[List[int], torch.Tensor]]]) -> Dict[str, torch.Tensor]:
# split inputs and labels since they have to be of different lengths and need
# different padding methods
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",
)
# replace padding with -100 to ignore loss correctly
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),因此我們在這個 notebook 中也使用它。
from evaluate import load
wer_metric = load("wer")
模型將返回一系列 logits 向量:,其中 和 。
一個 logit 向量 包含我們之前定義的詞彙中每個單詞的對數機率,因此 config.vocab_size
。我們對模型最可能的預測感興趣,因此取 logits 的 argmax(...)
。此外,我們透過將 -100
替換為 pad_token_id
並解碼 ID 將編碼標籤轉換回原始字串,同時確保連續標記不以 CTC 樣式分組 。
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)
# we do not want to group tokens when computing the metrics
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 的空白標記 。
由於我們只訓練一小部分權重,模型不易過擬合。因此,我們確保停用所有 dropout 層。
注意:當使用此筆記本在 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,
)
Some weights of Wav2Vec2ForCTC were not initialized from the model checkpoint at facebook/mms-1b-all and are newly initialized because the shapes did not match:
- lm_head.bias: found shape torch.Size([154]) in the checkpoint and torch.Size([39]) in the model instantiated
- lm_head.weight: found shape torch.Size([154, 1280]) in the checkpoint and torch.Size([39, 1280]) in the model instantiated
You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
注意:一些權重預計會重新初始化。這些權重對應於新初始化的詞彙輸出層。
現在我們要確保只訓練介面卡權重,而模型的其餘部分保持凍結。
首先,我們重新初始化所有介面卡權重,這可以透過方便的 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
透過將輸入長度相似的訓練樣本分組到一個批次中,使訓練更高效。這可以顯著縮短訓練時間,透過大大減少模型中傳遞的無用填充標記的總數。learning_rate
選擇為 1e-3,這是使用 Adam 訓練時的常見預設值。其他學習率可能同樣有效。
有關其他引數的更多解釋,可以檢視文件。為了節省 GPU 記憶體,我們啟用 PyTorch 的梯度檢查點,並將損失減少設定為“均值”。MMS 介面卡微調以極快的速度收斂到非常好的效能,因此即使對於像 4 小時這樣小的資料集,我們也只訓練 4 個 epoch。訓練期間,每 200 個訓練步驟將非同步上傳一個檢查點到 Hub。它還允許您在模型仍在訓練時使用演示小部件。
注意:如果不想將模型檢查點上傳到 Hub,只需設定 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,
)
為了使模型獨立於說話人語速,在 CTC 中,連續的相同標記被簡單地分組為一個標記。然而,在解碼時,編碼的標籤不應分組,因為它們不對應於模型的預測標記,這就是為什麼必須傳遞 group_tokens=False
引數。如果我們不傳遞此引數,像 "hello"
這樣的單詞將被錯誤地編碼並解碼為 "helo"
。 空白標記允許模型預測一個單詞,例如 "hello"
,透過強制它在兩個“l”之間插入空白標記。我們模型對 "hello"
的符合 CTC 的預測將是 [PAD] [PAD] "h" "e" "e" "l" "l" [PAD] "l" "o" "o" [PAD]
。
訓練
訓練時間應少於 30 分鐘,具體取決於所使用的 GPU。
trainer.train()
訓練損失 | 訓練步驟 | 驗證損失 | 詞錯誤率 (Wer) |
---|---|---|---|
4.905 | 100 | 0.215 | 0.280 |
0.290 | 200 | 0.167 | 0.232 |
0.2659 | 300 | 0.161 | 0.229 |
0.2398 | 400 | 0.156 | 0.223 |
訓練損失和驗證 WER 都在穩步下降。
我們發現,僅對 mms-1b-all
的介面卡層進行 100 步的微調,就已大幅超越了此處所示的整個 xls-r-300m
檢查點微調的效能。
從官方論文和這個快速比較可以清楚看出,mms-1b-all
具有更高的知識遷移到低資源語言的能力,應優於 xls-r-300m
。此外,由於只訓練了一小部分層,訓練效率也更高,記憶體佔用更少。
介面卡權重將作為模型檢查點的一部分上傳,但我們也要確保單獨儲存它們,以便可以輕鬆地進行解除安裝和載入。
讓我們將所有介面卡層儲存到訓練輸出目錄中,以便將其正確上傳到 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="<您的語言程式碼>"
,以便載入正確的介面卡。您還應為您的分詞器正確設定目標語言。
我們先來看看如何載入土耳其語檢查點。
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("Prediction:")
print(processor.decode(pred_ids))
print("\nReference:")
print(common_voice_test_tr[0]["sentence"].lower())
輸出:
Prediction:
pekçoğuda roman toplumundan geliyor
Reference:
pek çoğu da roman toplumundan geliyor.
這看起來幾乎完全正確,只是第一個單詞應該多加兩個空字元。現在,透過呼叫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("Prediction:")
print(processor.decode(pred_ids))
print("\nReference:")
print(common_voice_test_swe[0]["sentence"].lower())
輸出:
Prediction:
jag lämnade grovjobbet åt honom
Reference:
jag lämnade grovjobbet åt honom.
太棒了,這看起來是完美的轉錄!
我們在這篇博文中展示了 MMS 介面卡權重微調不僅在低資源語言上提供了最先進的效能,而且還顯著加快了訓練時間,並允許輕鬆構建定製介面卡權重集合。
相關文章和附加連結列於此