Ottimizza i modelli di adattatore MMS per ASR a bassa risorsa

Ottimizza modelli MMS per ASR a bassa risorsa.

Nuovo (06/2023) : Questo post sul blog è fortemente ispirato a “Fine-tuning XLS-R on Multi-Lingual ASR” e può essere considerato come una versione migliorata di esso.

Wav2Vec2 è un modello preaddestrato per il riconoscimento automatico della speech (ASR) ed è stato rilasciato nel settembre 2020 da Alexei Baevski, Michael Auli e Alex Conneau. Subito dopo la dimostrazione delle ottime prestazioni di Wav2Vec2 su uno dei dataset più popolari in inglese per ASR, chiamato LibriSpeech, Facebook AI ha presentato due versioni multilingue di Wav2Vec2, chiamate XLSR e XLM-R, in grado di riconoscere la speech in fino a 128 lingue. XLSR sta per rappresentazioni cross-linguistiche della speech e si riferisce alla capacità del modello di apprendere rappresentazioni della speech che sono utili in diverse lingue.

La più recente release di Meta AI, Massive Multilingual Speech (MMS) di Vineel Pratap, Andros Tjandra, Bowen Shi, et al. porta le rappresentazioni multilingue della speech a un nuovo livello. Oltre 1.100 lingue parlate possono essere identificate, trascritte e generate con i vari checkpoints per l’identification della lingua, il riconoscimento della speech e la conversione del testo in speech rilasciati.

In questo post sul blog, mostreremo come l’Adattamento (fine-tuning) di MMS raggiunge errori di parola sorprendentemente bassi dopo appena 10-20 minuti di addestramento.

Per le lingue a bassa risorsa, raccomandiamo vivamente l’utilizzo dell’Adattamento (fine-tuning) di MMS invece di addestrare l’intero modello come viene fatto in “Fine-tuning XLS-R on Multi-Lingual ASR”.

Nelle nostre esperienze, l’Adattamento (fine-tuning) di MMS è sia più efficiente in termini di memoria, più robusto e produce migliori performance per le lingue a bassa risorsa. Per le lingue ad alta risorsa, può comunque essere vantaggioso addestrare l’intero checkpoint anziché utilizzare i livelli dell’Adapter.

Preservare la diversità linguistica nel mondo

Secondo https://www.ethnologue.com/ circa 3000, o il 40% di tutte le lingue “viventi”, sono in pericolo a causa del sempre minor numero di parlanti nativi. Questo trend continuerà solo in un mondo sempre più globalizzato.

MMS è in grado di trascrivere molte lingue in pericolo, come ad esempio Ari o Kaivi. In futuro, MMS può svolgere un ruolo vitale nel mantenere vive le lingue aiutando i parlanti rimanenti a creare registrazioni scritte e comunicare nella loro lingua madre.

Per adattarsi a più di 1000 vocabolari diversi, MMS utilizza gli Adapter – un metodo di addestramento in cui solo una piccola parte dei pesi del modello viene addestrata.

I livelli dell’Adapter agiscono come ponti linguistici, consentendo al modello di sfruttare le conoscenze di una lingua quando decifra un’altra.

Adattamento (fine-tuning) di MMS

I checkpoint non supervisionati di MMS sono stati preaddestrati su oltre mezzo milione di ore di audio in oltre 1.400 lingue, con un numero di parametri compreso tra 300 milioni e un miliardo.

Puoi trovare i soli checkpoint preaddestrati sul 🤗 Hub per dimensioni del modello di 300 milioni di parametri (300M) e un miliardo di parametri (1B):

  • mms-300m
  • mms-1b

Nota: Se desideri addestrare ulteriormente i modelli di base, puoi farlo nello stesso modo mostrato in “Fine-tuning XLS-R on Multi-Lingual ASR”.

Similmente all’obiettivo di BERT di masked language modeling, MMS apprende rappresentazioni contestualizzate della speech mascherando casualmente i vettori delle features prima di passarli a una rete di trasformatori durante l’auto-addestramento non supervisionato.

Per ASR, il checkpoint preaddestrato MMS-1B è stato ulteriormente addestrato in modo supervisionato su oltre 1000 lingue con un layer di output del vocabolario condiviso. Come ultimo passaggio, il layer di output del vocabolario condiviso è stato eliminato e sono stati mantenuti invece i livelli dell’Adapter specifici per la lingua. Ogni livello dell’Adapter contiene solo ~2,5M di pesi, composti da piccoli layer di proiezione lineare per ogni blocco di attenzione e un layer di output del vocabolario specifico per la lingua.

Sono stati rilasciati tre checkpoint MMS ottimizzati per il riconoscimento del discorso (ASR). Essi includono rispettivamente i pesi degli adattatori 102, 1107 e 1162 (uno per ogni lingua):

  • mms-1b-fl102
  • mms-1b-l1107
  • mms-1b-all

Puoi vedere che i modelli di base vengono salvati (come al solito) come file model.safetensors, ma in aggiunta questi repository contengono molti pesi degli adattatori memorizzati nel repository, ad esempio sotto il nome adapter.fra.safetensors per il francese.

La documentazione di Hugging Face spiega molto bene come utilizzare tali checkpoint per l’elaborazione, quindi in questo post del blog ci concentreremo invece su come possiamo addestrare in modo efficiente modelli adattivi altamente performanti basati su uno qualsiasi dei checkpoint ASR rilasciati.

Addestramento dei pesi adattivi

Nell’apprendimento automatico, gli adattatori sono un metodo utilizzato per ottimizzare modelli pre-addestrati mantenendo invariati i parametri del modello originale. Fanno ciò inserendo piccoli moduli addestrabili, chiamati livelli adattatori, tra i livelli preesistenti del modello, che adattano il modello a un compito specifico senza richiedere un addestramento estensivo.

Gli adattatori hanno una lunga storia nel riconoscimento del discorso e in particolare nel riconoscimento del parlante. Nel riconoscimento del parlante, gli adattatori sono stati utilizzati in modo efficace per regolare modelli preesistenti al fine di riconoscere le idiosincrasie individuali del parlante, come evidenziato nel lavoro di Gales e Woodland (1996) e di Miao et al. (2014). Questo approccio non solo riduce notevolmente i requisiti computazionali rispetto all’addestramento del modello completo, ma consente anche di apportare regolazioni specifiche del parlante migliori e più flessibili.

Il lavoro svolto in MMS sfrutta questa idea degli adattatori per il riconoscimento del discorso in diverse lingue. Un piccolo numero di pesi degli adattatori viene ottimizzato per comprendere le caratteristiche fonetiche e grammaticali uniche di ciascuna lingua di destinazione. In questo modo, MMS consente a un unico grande modello di base (ad esempio il checkpoint mms-1b-all) e a più di 1000 piccoli livelli adattatori (2,5 milioni di pesi ognuno per mms-1b-all) di comprendere e trascrivere più lingue. Ciò riduce drasticamente la richiesta computazionale per lo sviluppo di modelli distinti per ogni lingua.

Ottimo! Ora che abbiamo compreso la motivazione e la teoria, vediamo come ottimizzare i pesi degli adattatori per mms-1b-all 🔥

Configurazione del notebook

Come fatto in precedenza nel post del blog “Ottimizzazione di XLS-R su ASR multilingue”, ottimizziamo il modello sul dataset ASR a bassa risorsa di Common Voice, che contiene solo circa 4 ore di dati di addestramento convalidati.

Come Wav2Vec2 o XLS-R, anche MMS viene ottimizzato utilizzando la Connectionist Temporal Classification (CTC), che è un algoritmo utilizzato per addestrare reti neurali per problemi di sequenza, come ASR e riconoscimento della scrittura a mano.

Per ulteriori dettagli sull’algoritmo CTC, consiglio vivamente di leggere il ben scritto post del blog “Modeling delle sequenze con CTC” (2017) di Awni Hannun.

Prima di iniziare, installiamo datasets e transformers. Inoltre, abbiamo bisogno di torchaudio per caricare i file audio e di jiwer per valutare il nostro modello ottimizzato utilizzando la metrica del tasso di errore delle parole (WER) 1 {}^1 1 .

%%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

Suggeriamo vivamente di caricare direttamente i checkpoint di addestramento sul 🤗 Hub durante l’addestramento. I repository di Hub hanno il controllo delle versioni integrato, quindi puoi essere sicuro che durante l’addestramento non verrà perso alcun checkpoint del modello.

Per farlo, è necessario memorizzare il token di autenticazione dal sito web di Hugging Face (registrati qui se non lo hai ancora fatto!)

from huggingface_hub import notebook_login

notebook_login()

Preparazione dati, tokenizer, estrattore di caratteristiche

I modelli ASR trascrivono il parlato in testo, il che significa che abbiamo bisogno sia di un estrattore di caratteristiche che elabori il segnale audio nel formato di input del modello, ad esempio un vettore di caratteristiche, sia di un tokenizer che elabori il formato di output del modello in testo.

In 🤗 Transformers, il modello MMS è accompagnato sia da un estrattore di caratteristiche, chiamato Wav2Vec2FeatureExtractor , sia da un tokenizer, chiamato Wav2Vec2CTCTokenizer .

Iniziamo creando il tokenizer per decodificare le classi di output predette nella trascrizione di output.

Crea Wav2Vec2CTCTokenizer

I modelli MMS preaddestrati, come mms-1b-all, hanno già un tokenizer che accompagna il checkpoint del modello. Tuttavia, poiché vogliamo addestrare ulteriormente il modello su dati specifici a bassa risorsa di una determinata lingua, è consigliabile rimuovere completamente il tokenizer e il livello di output del vocabolario e crearne di nuovi basati sui dati di addestramento stessi.

I modelli simili a Wav2Vec2 addestrati su CTC trascrivono un file audio con un singolo passaggio in avanti, elaborando prima l’input audio in una sequenza di rappresentazioni di contesto elaborate e quindi utilizzando il livello di output del vocabolario finale per classificare ogni rappresentazione di contesto come un carattere che rappresenta la trascrizione.

La dimensione di output di questo livello corrisponde al numero di token nel vocabolario, che estrarremo dal dataset etichettato utilizzato per l’addestramento ulteriore. Quindi, nel primo passaggio, daremo un’occhiata al dataset scelto di Common Voice e definiremo un vocabolario basato sulle trascrizioni.

In questo notebook, useremo il dataset 6.1 di Common Voice per il turco. Il turco corrisponde al codice di lingua "tr" .

Ottimo, ora possiamo utilizzare l’API semplice di 🤗 Datasets per scaricare i dati. Il nome del dataset è "mozilla-foundation/common_voice_6_1" , il nome di configurazione corrisponde al codice di lingua, che nel nostro caso è "tr" .

Nota : Prima di poter scaricare il dataset, devi accedervi effettuando l’accesso al tuo account Hugging Face, andando sulla pagina del repository del dataset e facendo clic su “Accetta e accedi al repository”

Common Voice ha molti set di dati diversi, tra cui invalidated , che si riferisce ai dati che non sono stati valutati come “abbastanza puliti” per essere considerati utili. In questo notebook, utilizzeremo solo i set "train" , "validation" e "test" .

Dato che il dataset turco è molto piccolo, uniremo sia i dati di convalida che quelli di addestramento in un unico dataset di addestramento e utilizzeremo solo i dati di test per la convalida.

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)

Molti dataset ASR forniscono solo il testo di destinazione ( 'sentence' ) per ogni array audio ( 'audio' ) e file ( 'path' ). Common Voice fornisce effettivamente molte più informazioni su ogni file audio, come l’accento ( 'accent' ), ecc. Per mantenere il notebook il più generale possibile, consideriamo solo il testo trascritto per l’addestramento ulteriore.

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"])

Scriviamo una breve funzione per visualizzare alcuni esempi casuali del dataset e la eseguiamo un paio di volte per avere un’idea delle trascrizioni.

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), "Non è possibile selezionare più elementi di quanti sono presenti nel 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?

Va bene! Le trascrizioni sembrano abbastanza pulite. Dopo aver tradotto le frasi trascritte, sembra che il linguaggio corrisponda più a un testo scritto che a un dialogo rumoroso. Questo ha senso considerando che Common Voice è un corpus di lettura trascritta di tipo crowd-sourcing.

Possiamo vedere che le trascrizioni contengono alcuni caratteri speciali, come ,.?!;:. Senza un modello linguistico, è molto più difficile classificare le parti del discorso rispetto a tali caratteri speciali perché non corrispondono realmente a un’unità sonora caratteristica. Ad esempio, la lettera "s" ha un suono più o meno chiaro, mentre il carattere speciale "." non lo ha. Inoltre, per comprendere il significato di un segnale sonoro, di solito non è necessario includere caratteri speciali nella trascrizione.

Semplicemente rimuoviamo tutti i caratteri che non contribuiscono al significato di una parola e che non possono essere rappresentati realmente da un suono acustico, e normalizziamo il testo.

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)

Diamo un’occhiata alle etichette di testo elaborate nuovamente.

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

Bene! Questo sembra migliorato. Abbiamo rimosso la maggior parte dei caratteri speciali dalle trascrizioni e le abbiamo normalizzate in minuscolo.

Prima di finalizzare la pre-elaborazione, è sempre vantaggioso consultare un madrelingua della lingua di destinazione per vedere se il testo può essere ulteriormente semplificato. Per questo post sul blog, Merve è stata così gentile da dare un’occhiata veloce e ha notato che i caratteri “cappellati” – come â – non vengono più utilizzati in turco e possono essere sostituiti dal loro equivalente “senza cappello”, ad esempio a.

Ciò significa che dovremmo sostituire una frase come "yargı sistemi hâlâ sağlıksız" con "yargı sistemi hala sağlıksız".

Scriviamo un’altra breve funzione di mappatura per semplificare ulteriormente le etichette di testo. Ricordiamo che più semplici sono le etichette di testo, più facile è per il modello imparare a prevedere tali etichette.

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)

Nel CTC, è comune classificare le parti del discorso in lettere, quindi faremo lo stesso qui. Estraiamo tutte le lettere distinte dai dati di addestramento e di test e costruiamo il nostro vocabolario da questo insieme di lettere.

Scriviamo una funzione di mappatura che concatena tutte le trascrizioni in una lunga trascrizione e quindi trasforma la stringa in un insieme di caratteri. È importante passare l’argomento batched=True alla funzione map(...) in modo che la funzione di mappatura abbia accesso a tutte le trascrizioni contemporaneamente.

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)

Ora, creiamo l’unione di tutte le lettere distinte nel set di dati di addestramento e nel set di dati di test e convertiamo la lista risultante in un dizionario enumerato.

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}

Bene, vediamo che tutte le lettere dell’alfabeto si trovano nel set di dati (cosa non sorprendente) e abbiamo anche estratto i caratteri speciali "" e '. Notare che non abbiamo escluso quei caratteri speciali perché il modello deve imparare a predire quando una parola è finita, altrimenti le previsioni sarebbero sempre una sequenza di lettere che renderebbe impossibile separare le parole l’una dall’altra.

Bisogna sempre tenere presente che la pre-elaborazione è un passaggio molto importante prima di addestrare il proprio modello. Ad esempio, non vogliamo che il nostro modello differenzi tra a e A solo perché abbiamo dimenticato di normalizzare i dati. La differenza tra a e A non dipende affatto dal “suono” della lettera, ma piuttosto dalle regole grammaticali – ad esempio, utilizzare una lettera maiuscola all’inizio della frase. Pertanto, ha senso rimuovere la differenza tra lettere maiuscole e minuscole in modo che il modello abbia più facilmente imparare a trascrivere il discorso.

Per rendere più chiaro che " " ha la propria classe di token, gli assegniamo un carattere più visibile |. Inoltre, aggiungiamo anche un token “sconosciuto” in modo che il modello possa gestire in seguito i caratteri non incontrati nel set di addestramento di Common Voice.

vocab_dict["|"] = vocab_dict[" "]
del vocab_dict[" "]

Infine, aggiungiamo anche un token di padding che corrisponde al “token vuoto” di CTC. Il “token vuoto” è un componente fondamentale dell’algoritmo CTC. Per ulteriori informazioni, si prega di dare uno sguardo alla sezione “Allineamento” qui .

vocab_dict["[UNK]"] = len(vocab_dict)
vocab_dict["[PAD]"] = len(vocab_dict)
len(vocab_dict)

    37

Bene, ora il nostro vocabolario è completo e consiste di 37 token, il che significa che il livello lineare che aggiungeremo in cima al checkpoint MMS pre-addestrato come parte dei pesi dell’adattatore avrà una dimensione di output di 37.

Poiché un singolo checkpoint MMS può fornire pesi personalizzati per più lingue, il tokenizer può anche consistere di più vocabolari. Pertanto, dobbiamo annidare il nostro vocab_dict per poter aggiungere eventualmente più lingue al vocabolario in futuro. Il dizionario dovrebbe essere annidato con il nome che viene utilizzato per i pesi dell’adattatore e che viene salvato nella configurazione del tokenizer con il nome target_lang.

Utilizziamo i codici delle lingue ISO-639-3 come il checkpoint originale mms-1b-all.

target_lang = "tur"

Definiamo un dizionario vuoto a cui possiamo aggiungere il vocabolario appena creato

new_vocab_dict = {target_lang: vocab_dict}

Nota: Nel caso in cui si desideri utilizzare questo notebook per aggiungere uno strato di adattatore a un repository di modelli esistente, assicurarsi di non creare un dizionario vocabolario vuoto, ma invece riutilizzarne uno già esistente. Per farlo, è necessario rimuovere il commento alle celle seguenti e sostituire "patrickvonplaten/wav2vec2-large-mms-1b-turkish-colab" con un ID di repository di modello a cui si desidera aggiungere i pesi dell’adattatore.

# da transformers importa Wav2Vec2CTCTokenizer

# mms_adapter_repo = "patrickvonplaten/wav2vec2-large-mms-1b-turkish-colab"  # assicurati di sostituire questo percorso con un repository a cui desideri aggiungere i nuovi pesi dell'adattatore

# tokenizer = Wav2Vec2CTCTokenizer.from_pretrained(mms_adapter_repo)
# nuovo_vocab = tokenizer.vocab

# nuovo_vocab[target_lang] = vocab_dict

Ora salviamo il vocabolario come file json.

import json
with open('vocab.json', 'w') as vocab_file:
    json.dump(new_vocab_dict, vocab_file)

In un passaggio finale, utilizziamo il file json per caricare il vocabolario in un’istanza della classe Wav2Vec2CTCTokenizer.

from transformers import Wav2Vec2CTCTokenizer

tokenizer = Wav2Vec2CTCTokenizer.from_pretrained("./", unk_token="[UNK]", pad_token="[PAD]", word_delimiter_token="|", target_lang=target_lang)

Se si desidera riutilizzare il tokenizer appena creato con il modello sintonizzato di questo notebook, è vivamente consigliato caricare il tokenizer nell’🤗 Hub. Chiamiamo il repository in cui caricheremo i file "wav2vec2-large-mms-1b-turkish-colab":

nome_repo = "wav2vec2-large-mms-1b-turkish-colab"

e carichiamo il tokenizer nell’🤗 Hub.

tokenizer.push_to_hub(nome_repo)

    CommitInfo(commit_url='https://huggingface.co/patrickvonplaten/wav2vec2-large-mms-1b-turkish-colab/commit/48cccbfd6059aa6ce655e9d94b8358ba39536cb7', commit_message='Carica tokenizer', commit_description='', oid='48cccbfd6059aa6ce655e9d94b8358ba39536cb7', pr_url=None, pr_revision=None, pr_num=None)

Perfetto, puoi vedere il repository appena creato su https://huggingface.co/<your-username>/wav2vec2-large-mms-1b-tr-colab

Crea Wav2Vec2FeatureExtractor

La voce è un segnale continuo e per essere trattata dai computer, deve prima essere discretizzata, cosa che viene solitamente chiamata campionamento. Il tasso di campionamento svolge un ruolo importante in quanto definisce quanti punti dati del segnale vocale vengono misurati al secondo. Pertanto, un campionamento con un tasso di campionamento più elevato produce una migliore approssimazione del segnale vocale reale, ma richiede anche più valori al secondo.

Un checkpoint preaddestrato si aspetta che i dati in input siano stati campionati più o meno dalla stessa distribuzione dei dati su cui è stato addestrato. Gli stessi segnali vocali campionati a due diverse velocità hanno una distribuzione molto diversa, ad esempio, raddoppiare il tasso di campionamento comporta il doppio dei punti dati. Pertanto, prima di sintonizzare un checkpoint preaddestrato di un modello ASR, è fondamentale verificare che il tasso di campionamento dei dati utilizzati per preaddestrare il modello corrisponda al tasso di campionamento del dataset utilizzato per sintonizzare il modello.

Un oggetto Wav2Vec2FeatureExtractor richiede i seguenti parametri per essere istanziato:

  • feature_size: I modelli vocali prendono in input una sequenza di vettori di caratteristiche. Mentre la lunghezza di questa sequenza ovviamente varia, la dimensione delle caratteristiche non dovrebbe. Nel caso di Wav2Vec2, la dimensione delle caratteristiche è 1 perché il modello è stato addestrato sul segnale vocale grezzo 2 {}^2 2 .
  • sampling_rate: Il tasso di campionamento con cui il modello viene addestrato.
  • padding_value: Per l’inferenza batched, gli input più brevi devono essere riempiti con un valore specifico
  • do_normalize: Se l’input deve essere normalizzato a zero-mean-unit-variance o meno. Di solito, i modelli vocali funzionano meglio quando normalizzano l’input
  • return_attention_mask: Se il modello deve utilizzare una attention_mask per l’inferenza batched. In generale, i checkpoint dei modelli XLS-R dovrebbero sempre utilizzare la 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)

Ottimo, il processo di estrazione delle caratteristiche di MMS è ora completamente definito!

Per migliorare la facilità d’uso, l’estratore di caratteristiche e il tokenizer sono racchiusi in una singola classe Wav2Vec2Processor in modo che sia sufficiente avere un oggetto model e processor.

from transformers import Wav2Vec2Processor

processor = Wav2Vec2Processor(feature_extractor=feature_extractor, tokenizer=tokenizer)

Successivamente, possiamo preparare il dataset.

Preparazione dei Dati

Fino ad ora, non abbiamo analizzato i valori effettivi del segnale vocale ma solo la trascrizione. Oltre a sentence, i nostri dataset includono altri due nomi di colonne path e audio. path rappresenta il percorso assoluto del file audio e audio rappresenta i dati audio già caricati. MMS si aspetta l’input nel formato di un array monodimensionale a 16 kHz. Ciò significa che il file audio deve essere caricato e campionato nuovamente.

Fortunatamente, datasets fa questo automaticamente quando il nome della colonna è audio. Proviamolo.

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}

Nell’esempio sopra possiamo vedere che i dati audio sono caricati con una frequenza di campionamento di 48kHz, mentre il modello si aspetta 16kHz, come abbiamo visto. Possiamo impostare la frequenza di campionamento audio alla frequenza corretta utilizzando 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))

Diamo un’occhiata a "audio" nuovamente.

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}

Sembra che abbia funzionato! Facciamo un ultimo controllo che i dati siano correttamente preparati, stampando la forma dell’input vocale, la trascrizione corrispondente e la frequenza di campionamento.

rand_int = random.randint(0, len(common_voice_train)-1)

print("Testo obiettivo:", common_voice_train[rand_int]["sentence"])
print("Forma dell'array di input:", common_voice_train[rand_int]["audio"]["array"].shape)
print("Frequenza di campionamento:", common_voice_train[rand_int]["audio"]["sampling_rate"])

    Testo obiettivo: bağış anlaşması bir ağustosta imzalandı
    Forma dell'array di input: (70656,)
    Frequenza di campionamento: 16000

Ben fatto! Tutto sembra corretto: i dati sono un array monodimensionale, la frequenza di campionamento corrisponde sempre a 16kHz e il testo obiettivo è normalizzato.

Finalmente, possiamo sfruttare Wav2Vec2Processor per elaborare i dati nel formato richiesto da Wav2Vec2ForCTC per l’addestramento. Per farlo, utilizziamo la funzione map(...) del Dataset.

Prima di tutto, carichiamo e ridimensioniamo i dati audio, semplicemente chiamando batch["audio"]. In secondo luogo, estraiamo i input_values dal file audio caricato. Nel nostro caso, il Wav2Vec2Processor normalizza solo i dati. Per altri modelli di riconoscimento del parlato, tuttavia, questa fase può includere estrazioni di caratteristiche più complesse, come l’estrazione delle caratteristiche Log-Mel. In terzo luogo, codifichiamo le trascrizioni in ID etichetta.

Nota: Questa funzione di mappatura è un buon esempio di come dovrebbe essere utilizzata la classe Wav2Vec2Processor. In un contesto “normale”, chiamare processor(...) viene reindirizzato al metodo di chiamata di Wav2Vec2FeatureExtractor. Tuttavia, quando si avvolge il processore nel contesto as_target_processor, lo stesso metodo viene reindirizzato al metodo di chiamata di Wav2Vec2CTCTokenizer. Per ulteriori informazioni, si prega di consultare la documentazione.

def prepare_dataset(batch):
    audio = batch["audio"]

    # L'output raggruppato è "disaggregato"
    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

Applichiamo la funzione di preparazione dei dati a tutti gli esempi.

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)

Nota: datasets si occupa automaticamente del caricamento e del ridimensionamento dell’audio. Se si desidera implementare il proprio caricamento/ridimensionamento dati personalizzato, è possibile utilizzare semplicemente la colonna "path" e ignorare la colonna "audio".

Fantastico, ora siamo pronti per iniziare l’addestramento!

Addestramento

I dati vengono elaborati in modo che siamo pronti per configurare il processo di addestramento. Useremo il Trainer di 🤗 e dobbiamo fare essenzialmente quanto segue:

  • Definire un data collator. A differenza della maggior parte dei modelli di NLP, MMS ha una lunghezza di input molto maggiore rispetto alla lunghezza di output. Ad esempio, un campione di lunghezza di input 50000 ha una lunghezza di output di non più di 100. Date le grandi dimensioni di input, è molto più efficiente riempire dinamicamente i batch di addestramento, il che significa che tutti i campioni di addestramento dovrebbero essere riempiti solo fino al campione più lungo nel loro batch e non al campione più lungo complessivo. Pertanto, per il fine-tuning di MMS è necessario un data collator di riempimento speciale, che definiremo di seguito.

  • Metrica di valutazione. Durante l’addestramento, il modello dovrebbe essere valutato sul tasso di errore delle parole. Dovremmo definire una funzione compute_metrics di conseguenza.

  • Caricare un checkpoint preaddestrato. Dobbiamo caricare un checkpoint preaddestrato e configurarlo correttamente per l’addestramento.

  • Definire la configurazione di addestramento.

Dopo aver fatto il fine-tuning del modello, lo valuteremo correttamente sui dati di test e verificheremo che abbia effettivamente imparato a trascrivere correttamente il parlato.

Configurazione del Trainer

Iniziamo definendo il data collator. Il codice per il data collator è stato copiato da questo esempio.

Senza entrare troppo nei dettagli, a differenza dei data collator comuni, questo data collator tratta i input_values e le labels in modo diverso e applica quindi due funzioni di riempimento separate su di essi (utilizzando nuovamente il contesto manager del processore MMS). Questo è necessario perché, nel riconoscimento del parlato, l’input e l’output sono di diverse modalità e quindi non dovrebbero essere trattati dalla stessa funzione di riempimento. Analogamente ai data collator comuni, i token di riempimento nelle etichette vengono impostati a -100 in modo che quei token non vengano presi in considerazione durante il calcolo della perdita.

import torch

from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Union

@dataclass
class DataCollatorCTCWithPadding:
    """
    Data collator che effettuerà il padding dinamico degli input ricevuti.
    Args:
        processor (:class:`~transformers.Wav2Vec2Processor`)
            Il processore utilizzato per l'elaborazione dei dati.
        padding (:obj:`bool`, :obj:`str` o :class:`~transformers.tokenization_utils_base.PaddingStrategy`, `optional`, default a :obj:`True`):
            Seleziona una strategia per effettuare il padding delle sequenze restituite (in base al lato di padding del modello e all'indice di padding)
            tra:
            * :obj:`True` o :obj:`'longest'`: Esegue il padding alla sequenza più lunga nel batch (o nessun padding se viene fornita solo una sequenza).
            * :obj:`'max_length'`: Esegue il padding a una lunghezza massima specificata con l'argomento :obj:`max_length` o alla lunghezza massima accettabile in input per il modello se quell'argomento non viene fornito.
            * :obj:`False` o :obj:`'do_not_pad'` (default): Nessun padding (ossia può produrre un batch con sequenze di lunghezze diverse).
    """

    processor: Wav2Vec2Processor
    padding: Union[bool, str] = True

    def __call__(self, features: List[Dict[str, Union[List[int], torch.Tensor]]]) -> Dict[str, torch.Tensor]:
        # divide gli input e le etichette in quanto devono avere lunghezze diverse e necessitano
        # di metodi di padding diversi
        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",
        )

        # sostituisce il padding con -100 per ignorare correttamente la loss
        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)

Successivamente, viene definita la metrica di valutazione. Come accennato in precedenza, la metrica predominante nell’ASR è il tasso di errore delle parole (WER), quindi lo utilizzeremo anche in questo notebook.

from evaluate import load

wer_metric = load("wer")

Il modello restituirà una sequenza di vettori di logit: y 1 , … , y m \mathbf{y}_1, \ldots, \mathbf{y}_m y 1 ​ , … , y m ​ con 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 ] e n > > m n >> m n > > m .

Un vettore di logit y 1 \mathbf{y}_1 y 1 ​ contiene le log-odds per ogni parola nel vocabolario che abbiamo definito in precedenza, quindi len ( y i ) = \text{len}(\mathbf{y}_i) = len ( y i ​ ) = config.vocab_size . Siamo interessati alla previsione più probabile del modello e quindi prendiamo l’argmax(…) dei logit. Inoltre, trasformiamo le etichette codificate nuovamente nella stringa originale sostituendo -100 con pad_token_id e decodificando gli id assicurandoci che i token consecutivi non vengano raggruppati nello stesso token in stile CTC 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)
    # non vogliamo raggruppare i token durante il calcolo delle metriche
    label_str = processor.batch_decode(pred.label_ids, group_tokens=False)

    wer = wer_metric.compute(predictions=pred_str, references=label_str)

    return {"wer": wer}

Ora possiamo caricare il checkpoint preaddestrato di mms-1b-all. L’pad_token_id del tokenizer deve essere definito come pad_token_id del modello o nel caso di Wav2Vec2ForCTC anche come il token blank di CTC 2 {}^2 2 .

Dato che stiamo allenando solo un piccolo sottoinsieme di pesi, il modello non è incline all’overfitting. Pertanto, ci assicuriamo di disabilitare tutti i dropout layers.

Nota: Quando si utilizza questo notebook per allenare MMS su un’altra lingua di Common Voice, quelle impostazioni iper-parametriche potrebbero non funzionare molto bene. Sentitevi liberi di adattarle in base al vostro caso d’uso.

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,
)

    Alcuni pesi di Wav2Vec2ForCTC non sono stati inizializzati dal modello di checkpoint a facebook/mms-1b-all e sono stati appena inizializzati perché le forme non corrispondevano:
    - lm_head.bias: forma trovata torch.Size([154]) nel checkpoint e torch.Size([39]) nel modello istanziato
    - lm_head.weight: forma trovata torch.Size([154, 1280]) nel checkpoint e torch.Size([39, 1280]) nel modello istanziato
    Probabilmente dovreste ALLENARE questo modello su un compito downstream per poterlo utilizzare per previsioni e inferenza.

Nota: È previsto che alcuni pesi siano appena inizializzati. Questi pesi corrispondono al layer di output del vocabolario appena inizializzato.

Ora vogliamo assicurarci che vengano addestrati solo i pesi dell’adattatore e che il resto del modello rimanga congelato.

Prima di tutto, reinizializziamo tutti i pesi dell’adattatore, che può essere fatto con il comodo metodo init_adapter_layers. È anche possibile non reinizializzare i pesi dell’adattatore e continuare il fine-tuning, ma in questo caso è necessario assicurarsi di caricare i pesi dell’adattatore adatti tramite il metodo load_adapter(...) prima dell’addestramento. Spesso il vocabolario non corrisponderà ancora molto bene ai dati di addestramento personalizzati, quindi di solito è più facile reinizializzare tutti i layer dell’adattatore in modo che possano essere facilmente sottoposti a fine-tuning.

model.init_adapter_layers()

Successivamente, congeliamo tutti i pesi, tranne quelli dei layer dell’adattatore.

model.freeze_base_model()

adapter_weights = model._get_adapters()
for param in adapter_weights.values():
    param.requires_grad = True

In un ultimo passaggio, definiamo tutti i parametri relativi all’addestramento. Per fornire ulteriori spiegazioni su alcuni dei parametri:

  • group_by_length rende l’addestramento più efficiente raggruppando i campioni di addestramento con una lunghezza di input simile in un unico batch. Questo può velocizzare significativamente il tempo di addestramento riducendo notevolmente il numero complessivo di token di padding inutili che vengono passati attraverso il modello.
  • learning_rate è stato scelto come 1e-3, che è un valore predefinito comune per l’addestramento con Adam. Altre velocità di apprendimento potrebbero funzionare altrettanto bene.

Per ulteriori spiegazioni su altri parametri, è possibile consultare la documentazione. Per risparmiare memoria GPU, abilitiamo la checkpointing del gradiente di PyTorch e impostiamo anche la riduzione della loss su “mean”. L’adattamento fine-tuning di MMS converge estremamente velocemente verso prestazioni molto buone, quindi anche per un dataset di soli 4h, addestreremo solo per 4 epoche. Durante l’addestramento, un checkpoint verrà caricato asincronamente nell’hub ogni 200 step di addestramento. Ciò consente anche di giocare con il widget demo anche mentre il modello è ancora in fase di addestramento.

Nota: Se non si desidera caricare i checkpoint del modello nell’hub, impostare semplicemente 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,
)

Ora, tutte le istanze possono essere passate a Trainer e siamo pronti per iniziare l’addestramento!

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 Per consentire ai modelli di diventare indipendenti dalla velocità del parlante, in CTC, i token consecutivi identici vengono semplicemente raggruppati come un singolo token. Tuttavia, le etichette codificate non devono essere raggruppate durante la decodifica poiché non corrispondono ai token predetti dal modello, motivo per cui il parametro group_tokens=False deve essere passato. Se non passassimo questo parametro, una parola come "hello" sarebbe codificata in modo errato e decodificata come "helo". 2 {}^2 2 Il token vuoto consente al modello di prevedere una parola, come "hello", obbligandolo ad inserire il token vuoto tra le due l. Una previsione conforme a CTC di "hello" del nostro modello sarebbe [PAD] [PAD] "h" "e" "e" "l" "l" [PAD] "l" "o" "o" [PAD].

Formazione

La formazione dovrebbe richiedere meno di 30 minuti a seconda della GPU utilizzata.

trainer.train()

La perdita di formazione e il WER di convalida diminuiscono in modo appropriato.

Vediamo che il raffinamento dei livelli dell’adattatore di mms-1b-all per soli 100 passaggi supera di gran lunga il raffinamento dell’intero checkpoint xls-r-300m qui mostrato.

Dal documento ufficiale e da questo rapido confronto diventa chiaro che mms-1b-all ha una capacità molto maggiore di trasferire conoscenze a una lingua a bassa risorsa e dovrebbe essere preferito rispetto a xls-r-300m. Inoltre, la formazione è anche più efficiente in termini di memoria in quanto vengono addestrati solo un piccolo sottoinsieme di livelli.

I pesi degli adattatori verranno caricati come parte del checkpoint del modello, ma vogliamo anche assicurarci di salvarli separatamente in modo che possano essere facilmente caricati e scaricati.

Salviamo tutti i livelli dell’adattatore nella directory di output di formazione in modo che possano essere correttamente caricati nell’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"})

Infine, è possibile caricare il risultato della formazione nell’Hub 🤗.

trainer.push_to_hub()

Uno dei principali vantaggi della formazione dei pesi degli adattatori è che il modello “base”, che costituisce circa il 99% dei pesi del modello, rimane invariato e solo un piccolo checkpoint dell’adattatore di 2,5M deve essere condiviso per utilizzare il checkpoint addestrato.

Questo rende estremamente semplice addestrare ulteriori livelli dell’adattatore e aggiungerli al tuo repository.

Puoi farlo molto facilmente semplicemente riavviando questo script e cambiando la lingua su cui desideri eseguire la formazione con un’altra, ad esempio swe per lo svedese. Inoltre, assicurati che il vocabolario non venga completamente sovrascritto, ma che il nuovo vocabolario della lingua venga aggiunto a quello esistente, come indicato sopra nelle celle commentate.

Per dimostrare come possono essere caricati diversi livelli dell’adattatore, ho addestrato e caricato anche un livello dell’adattatore per lo svedese con il codice di lingua iso swe come puoi vedere qui

Puoi caricare il checkpoint raffinato come al solito utilizzando from_pretrained(...), ma assicurati anche di aggiungere un target_lang="<codice-lingua>" al metodo in modo che l’adattatore corretto venga caricato. Assicurati anche di impostare correttamente la lingua di destinazione per il tuo tokenizer.

Vediamo come possiamo caricare prima il checkpoint turco.

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")

Verifichiamo che il modello possa trascrivere correttamente il turco

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))

Elaboriamo l’audio, eseguiamo un passaggio in avanti e prevediamo gli 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]

Infine, possiamo decodificare l’esempio.

print("Previsione:")
print(processor.decode(pred_ids))

print("\nRiferimento:")
print(common_voice_test_tr[0]["sentence"].lower())

Output :

    Previsione:
    pekçoğuda roman toplumundan geliyor

    Riferimento:
    pek çoğu da roman toplumundan geliyor.

Sembra che sia quasi esattamente corretto, solo due spazi vuoti dovrebbero essere stati aggiunti nella prima parola. Ora è molto semplice cambiare l’adattatore in svedese chiamando model.load_adapter(...) e cambiando anche il tokenizer in svedese.

model.load_adapter("swe")
processor.tokenizer.set_target_lang("swe")

Carichiamo di nuovo il set di test in svedese da 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))

e trascriviamo un campione:

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("Previsione:")
print(processor.decode(pred_ids))

print("\nRiferimento:")
print(common_voice_test_swe[0]["sentence"].lower())

Output :

    Previsione:
    jag lämnade grovjobbet åt honom

    Riferimento:
    jag lämnade grovjobbet åt honom.

Grande, sembra una trascrizione perfetta!

In questo post del blog abbiamo mostrato come il fine-tuning dei pesi dell’adattatore MMS non solo fornisca prestazioni all’avanguardia per le lingue a risorse limitate, ma acceleri significativamente il tempo di addestramento e consenta anche di creare facilmente una collezione di pesi dell’adattatore personalizzati.

Qui sono elencati post correlati e link aggiuntivi:

  • Articolo ufficiale
  • Cobebase originale
  • Demo ufficiale
  • Documentazione di Transformers
  • Post del blog correlato su XLS-R
  • Modelli nell’hub