Perfeziona Wav2Vec2 per la trascrizione automatica del parlato in inglese su Hugging Face con 🤗 Transformers

'Improve Wav2Vec2 for automatic speech transcription in English on Hugging Face with 🤗 Transformers.'

Wav2Vec2 è un modello pre-addestrato per il riconoscimento automatico del parlato (ASR) ed è stato rilasciato nel settembre 2020 da Alexei Baevski, Michael Auli e Alex Conneau.

Utilizzando un nuovo obiettivo di pre-addestramento contrastivo, Wav2Vec2 apprende potenti rappresentazioni del parlato da oltre 50.000 ore di parlato non etichettato. In modo simile all’addestramento di BERT sul modello di linguaggio con maschera, il modello apprende rappresentazioni contestualizzate del parlato mascherando casualmente i vettori di caratteristiche prima di passarli a una rete trasformatrice.

Per la prima volta è stato dimostrato che il pre-addestramento, seguito dalla messa a punto su pochi dati di parlato etichettati, raggiunge risultati competitivi rispetto ai sistemi ASR all’avanguardia. Utilizzando solo 10 minuti di dati etichettati, Wav2Vec2 produce un tasso di errore di parole (WER) inferiore al 5% sul set di test pulito di LibriSpeech – cf. Tabella 9 del paper.

In questo notebook, forniremo una spiegazione dettagliata su come i checkpoint pre-addestrati di Wav2Vec2 possono essere messi a punto su qualsiasi dataset ASR in inglese. Nota che in questo notebook metteremo a punto Wav2Vec2 senza utilizzare un modello di linguaggio. È molto più semplice utilizzare Wav2Vec2 senza un modello di linguaggio come un sistema ASR end-to-end ed è stato dimostrato che un modello acustico Wav2Vec2 autonomo ottiene risultati impressionanti. A scopo dimostrativo, metteremo a punto il checkpoint pre-addestrato di dimensioni “base” sul dataset Timit piuttosto piccolo che contiene solo 5 ore di dati di addestramento.

Wav2Vec2 viene messo a punto utilizzando la Connectionist Temporal Classification (CTC), che è un algoritmo utilizzato per addestrare reti neurali per problemi di sequenze e principalmente nel riconoscimento automatico del parlato e nel riconoscimento della scrittura a mano.

Consiglio vivamente la lettura dell’articolo del blog Sequence Modeling with CTC (2017), un articolo molto ben scritto di Awni Hannun.

Prima di iniziare, installiamo sia datasets che transformers dalla versione master. Inoltre, abbiamo bisogno del pacchetto soundfile per caricare i file audio e del pacchetto jiwer per valutare il nostro modello messo a punto utilizzando la metrica del tasso di errore di parole (WER) 1 {}^1 1 .

!pip install datasets>=1.18.3
!pip install transformers==4.11.3
!pip install librosa
!pip install jiwer

In seguito consigliamo vivamente di caricare i checkpoint di addestramento direttamente su Hugging Face Hub durante l’addestramento. Il Hub ha un controllo integrato delle versioni, quindi puoi essere sicuro che nessun checkpoint del modello venga perso durante l’addestramento.

Per farlo, devi salvare il tuo token di autenticazione dal sito web di Hugging Face (registrati qui se non l’hai già fatto!)

from huggingface_hub import notebook_login

notebook_login()

Stampa Output:

Login successful
Your token has been saved to /root/.huggingface/token
Authenticated through git-crendential store but this isn't the helper defined on your machine.
You will have to re-authenticate when pushing to the Hugging Face Hub. Run the following command in your terminal to set it as the default

git config --global credential.helper store

Successivamente, è necessario installare Git-LFS per caricare i checkpoint del modello:

!apt install git-lfs

1 {}^1 1 Di solito Timit viene valutato utilizzando il tasso di errore dei fonemi (PER), ma la metrica più comune nell’ASR è il tasso di errore delle parole (WER). Per mantenere questo notebook il più generale possibile, abbiamo deciso di valutare il modello utilizzando il WER.

Preparazione dei 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 del parlato 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 Wav2Vec2 è quindi accompagnato sia da un tokenizer, chiamato Wav2Vec2CTCTokenizer , sia da un estrattore di caratteristiche, chiamato Wav2Vec2FeatureExtractor .

Iniziamo creando il tokenizer responsabile della decodifica delle previsioni del modello.

Crea Wav2Vec2CTCTokenizer

Il checkpoint preaddestrato di Wav2Vec2 mappa il segnale vocale in una sequenza di rappresentazioni contestuali come illustrato nella figura sopra. Un checkpoint di Wav2Vec2 sintonizzato ha bisogno di mappare questa sequenza di rappresentazioni contestuali alla sua trascrizione corrispondente in modo che uno strato lineare debba essere aggiunto sopra il blocco del trasformatore (mostrato in giallo). Questo strato lineare viene utilizzato per classificare ogni rappresentazione contestuale in una classe di token in modo analogo a come, ad esempio, dopo il preaddestramento viene aggiunto uno strato lineare sopra gli embedding di BERT per ulteriori classificazioni – cf. con la sezione “BERT” di questo post del blog.

La dimensione di output di questo strato corrisponde al numero di token nel vocabolario, che non dipende dal compito di preaddestramento di Wav2Vec2, ma solo dal dataset etichettato utilizzato per il fine-tuning. Quindi nel primo passaggio, daremo uno sguardo a Timit e definiremo un vocabolario basato sulle trascrizioni del dataset.

Iniziamo caricando il dataset e dando uno sguardo alla sua struttura.

from datasets import load_dataset, load_metric

timit = load_dataset("timit_asr")

print(timit)

Output di stampa:

    DatasetDict({
        train: Dataset({
            features: ['file', 'audio', 'text', 'phonetic_detail', 'word_detail', 'dialect_region', 'sentence_type', 'speaker_id', 'id'],
            num_rows: 4620
        })
        test: Dataset({
            features: ['file', 'audio', 'text', 'phonetic_detail', 'word_detail', 'dialect_region', 'sentence_type', 'speaker_id', 'id'],
            num_rows: 1680
        })
    })

Molti dataset ASR forniscono solo il testo target, 'text' per ogni file audio 'file'. Timit fornisce effettivamente molte più informazioni su ogni file audio, come il 'phonetic_detail', ecc., per questo molti ricercatori scelgono di valutare i loro modelli sulla classificazione dei fonemi invece del riconoscimento vocale quando lavorano con Timit. Tuttavia, vogliamo mantenere il notebook il più generale possibile, quindi considereremo solo il testo trascritto per il fine-tuning.

timit = timit.remove_columns(["phonetic_detail", "word_detail", "dialect_region", "id", "sentence_type", "speaker_id"])

Scriviamo una breve funzione per visualizzare alcuni esempi casuali del dataset ed eseguiamola alcune 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(timit["train"].remove_columns(["file", "audio"]))

Output di stampa:

Bene! Le trascrizioni sembrano molto pulite e il linguaggio sembra corrispondere più al testo scritto che al dialogo. Questo ha senso se si considera che Timit è un corpus di lettura.

Possiamo vedere che le trascrizioni contengono alcuni caratteri speciali, come ,.?!;:. Senza un modello linguistico, è molto più difficile classificare i frammenti di discorso in 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 "." no. Inoltre, per comprendere il significato di un segnale vocale, di solito non è necessario includere caratteri speciali nella trascrizione.

Inoltre, normalizziamo il testo in modo che contenga solo lettere minuscole.

import re
chars_to_ignore_regex = '[\,\?\.\!\-\;\:\"]'

def remove_special_characters(batch):
    batch["text"] = re.sub(chars_to_ignore_regex, '', batch["text"]).lower()
    return batch

timit = timit.map(remove_special_characters)

Diamo un’occhiata alle trascrizioni preelaborate.

mostra_elementi_casuali(timit["train"].rimuovi_colonne(["file", "audio"]))

Output di stampa:

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

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

Scriviamo una funzione di mappatura che concatena tutte le trascrizioni in una trascrizione lunga e poi 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 estrai_tutti_caratteri(batch):
  tutto_testo = " ".join(batch["testo"])
  vocabolario = list(set(tutto_testo))
  return {"vocabolario": [vocabolario], "tutto_testo": [tutto_testo]}

vocabolari = timit.map(estrai_tutti_caratteri, batched=True, batch_size=-1, keep_in_memory=True, remove_columns=timit.column_names["train"])

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

lista_vocabolario = list(set(vocabolari["train"]["vocabolario"][0]) | set(vocabolari["test"]["vocabolario"][0]))

dizionario_vocabolario = {v: k for k, v in enumerate(lista_vocabolario)}
dizionario_vocabolario

Output di stampa:

{    
     ' ': 21,
     "'": 13,
     'a': 24,
     'b': 17,
     'c': 25,
     'd': 2,
     'e': 9,
     'f': 14,
     'g': 22,
     'h': 8,
     'i': 4,
     'j': 18,
     'k': 5,
     'l': 16,
     'm': 6,
     'n': 7,
     'o': 10,
     'p': 19,
     'q': 3,
     'r': 20,
     's': 11,
     't': 0,
     'u': 26,
     'v': 27,
     'w': 1,
     'x': 23,
     'y': 15,
     'z': 12
}

Ben fatto, vediamo che tutte le lettere dell’alfabeto compaiono nel dataset (cosa non sorprendente) e abbiamo anche estratto i caratteri speciali " " e '. Nota che non abbiamo escluso questi caratteri speciali perché:

  • Il modello deve imparare a prevedere quando una parola finisce, altrimenti la previsione del modello sarebbe sempre una sequenza di caratteri che renderebbe impossibile separare le parole l’una dall’altra.
  • In inglese, è necessario mantenere il carattere ' per differenziare tra le parole, ad esempio "it's" e "its" che hanno significati molto diversi.

Per rendere più chiaro che " " ha una 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 caratteri non incontrati nell’insieme di addestramento di Timit.

dizionario_vocabolario["|"] = dizionario_vocabolario[" "]
del dizionario_vocabolario[" "]

Infine, aggiungiamo anche un token di padding che corrisponde al “token vuoto” del CTC. Il “token vuoto” è un componente fondamentale dell’algoritmo CTC. Per maggiori informazioni, dai un’occhiata alla sezione “Allineamento” qui .

dizionario_vocabolario["[UNK]"] = len(dizionario_vocabolario)
dizionario_vocabolario["[PAD]"] = len(dizionario_vocabolario)
print(len(dizionario_vocabolario))

Output di stampa:

    30

Perfetto, ora il nostro vocabolario è completo e consiste in 30 token, il che significa che il layer lineare che aggiungeremo sopra il checkpoint preaddestrato di Wav2Vec2 avrà una dimensione di output di 30.

Ora salviamo il vocabolario come file json.

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

In una fase finale, utilizziamo il file json per istanziare un oggetto della classe Wav2Vec2CTCTokenizer.

from transformers import Wav2Vec2CTCTokenizer

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

Se si desidera riutilizzare il tokenizer appena creato con il modello sintonizzato di questo notebook, si consiglia vivamente di caricare il tokenizer su 🤗 Hub. Chiamiamo il repository a cui caricheremo i file "wav2vec2-large-xlsr-turkish-demo-colab":

repo_name = "wav2vec2-base-timit-demo-colab"

e carichiamo il tokenizer su 🤗 Hub.

tokenizer.push_to_hub(repo_name)

Perfetto, puoi vedere il repository appena creato su https://huggingface.co/<your-username>/wav2vec2-base-timit-demo-colab

Creare un estrattore di funzionalità Wav2Vec2

Il linguaggio parlato è un segnale continuo e per essere elaborato dai computer, deve prima essere discretizzato, il che è generalmente chiamato campionamento. Il tasso di campionamento svolge un ruolo importante in quanto definisce quanti punti di dati del segnale vocale vengono misurati al secondo. Pertanto, il 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 di input siano stati campionati più o meno dalla stessa distribuzione dei dati su cui è stato addestrato. Gli stessi segnali vocali campionati a due diversi tassi hanno una distribuzione molto diversa, ad esempio, il raddoppio del tasso di campionamento comporta dati che sono il doppio più lunghi. 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 dell’insieme di dati utilizzato per sintonizzare il modello.

Wav2Vec2 è stato preaddestrato sui dati audio di LibriSpeech e LibriVox, entrambi campionati con 16 kHz. Il nostro set di dati per la sintonizzazione fine, Timit, è stato fortunatamente campionato anche con 16 kHz. Se il set di dati di sintonizzazione fine fosse stato campionato con un tasso inferiore o superiore a 16 kHz, avremmo dovuto prima campionare il segnale vocale in modo da adattarlo al tasso di campionamento dei dati utilizzati per il preaddestramento.

Un oggetto estrattore di funzionalità Wav2Vec2 richiede i seguenti parametri per essere istanziato:

  • feature_size: I modelli di linguaggio parlato prendono in input una sequenza di vettori di funzionalità. Mentre la lunghezza di questa sequenza ovviamente varia, la dimensione della funzionalità non dovrebbe farlo. Nel caso di Wav2Vec2, la dimensione della funzionalità è 1 perché il modello è stato addestrato sul segnale vocale grezzo 2 {}^2 2 .
  • sampling_rate: Il tasso di campionamento su cui è stato addestrato il modello.
  • padding_value: Per l’elaborazione a batch, gli input più corti devono essere riempiti con un valore specifico.
  • do_normalize: Se l’input deve essere normalizzato a zero-media-varianza o meno. Di solito, i modelli di linguaggio parlato funzionano meglio quando normalizzano l’input.
  • return_attention_mask: Se il modello deve utilizzare una attention_mask per l’elaborazione a batch. In generale, i modelli dovrebbero sempre utilizzare la attention_mask per mascherare i token riempiti. Tuttavia, a causa di una scelta di progetto molto specifica del checkpoint “base” di Wav2Vec2, si ottengono risultati migliori non utilizzando alcuna attention_mask. Questo non è raccomandato per altri modelli di linguaggio parlato. Per ulteriori informazioni, è possibile dare un’occhiata a questo problema. Importante Se si desidera utilizzare questo notebook per sintonizzare large-lv60, questo parametro dovrebbe essere impostato su True.
from transformers import Wav2Vec2FeatureExtractor

feature_extractor = Wav2Vec2FeatureExtractor(feature_size=1, sampling_rate=16000, padding_value=0.0, do_normalize=True, return_attention_mask=False)

Ottimo, la pipeline di estrazione delle caratteristiche di Wav2Vec2 è quindi completamente definita!

Per rendere l’utilizzo di Wav2Vec2 il più semplice possibile per gli utenti, l’estrattore di caratteristiche e il tokenizer vengono 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)

Preparazione dei Dati

Fino ad ora, non abbiamo analizzato i valori effettivi del segnale vocale ma solo la trascrizione. Oltre alla frase, i nostri dataset includono altre due colonne con i nomi path e audio. path indica il percorso assoluto del file audio. Diamo un’occhiata.

print(timit[0]["path"])

Output di Stampa:

'/root/.cache/huggingface/datasets/downloads/extracted/404950a46da14eac65eb4e2a8317b1372fb3971d980d91d5d5b221275b1fd7e0/data/TRAIN/DR4/MMDM0/SI681.WAV'

Wav2Vec2 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, il modulo datasets fa questo automaticamente chiamando l’altra colonna audio. Proviamolo.

common_voice_train[0]["audio"]

Output di Stampa:

{'array': array([-2.1362305e-04,  6.1035156e-05,  3.0517578e-05, ...,
        -3.0517578e-05, -9.1552734e-05, -6.1035156e-05], dtype=float32),
 'path': '/root/.cache/huggingface/datasets/downloads/extracted/404950a46da14eac65eb4e2a8317b1372fb3971d980d91d5d5b221275b1fd7e0/data/TRAIN/DR4/MMDM0/SI681.WAV',
 'sampling_rate': 16000}

Possiamo vedere che il file audio è stato caricato automaticamente. Questo è grazie alla nuova "funzionalità Audio" introdotta in datasets == 4.13.3, che carica e campiona i file audio al volo durante la chiamata.

Il tasso di campionamento è impostato su 16kHz, che è quello che Wav2Vec2 si aspetta come input.

Ottimo, ascoltiamo un paio di file audio per comprendere meglio il dataset e verificare che l’audio sia stato caricato correttamente.

import IPython.display as ipd
import numpy as np
import random

rand_int = random.randint(0, len(timit["train"]))

print(timit["train"][rand_int]["text"])
ipd.Audio(data=np.asarray(timit["train"][rand_int]["audio"]["array"]), autoplay=True, rate=16000)

Si può sentire che i parlanti cambiano insieme alla loro velocità di parlato, accento, ecc. Nel complesso, le registrazioni suonano relativamente chiare, come ci si potrebbe aspettare da un corpus di discorsi letti.

Facciamo un ultimo controllo per verificare che i dati siano correttamente preparati, stampando la forma dell’input del discorso, la sua trascrizione e il tasso di campionamento corrispondente.

rand_int = random.randint(0, len(timit["train"]))

print("Testo target:", timit["train"][rand_int]["text"])
print("Forma dell'array di input:", np.asarray(timit["train"][rand_int]["audio"]["array"]).shape)
print("Tasso di campionamento:", timit["train"][rand_int]["audio"]["sampling_rate"])

Output di Stampa:

    Testo target: lei ha il tuo completo scuro nell'acqua di lavaggio sporca tutto l'anno
    Forma dell'array di input: (52941,)
    Tasso di campionamento: 16000

Perfetto! Tutto sembra corretto Рi dati sono un array monodimensionale, il tasso di campionamento corrisponde sempre a 16kHz e il testo target ̬ normalizzato.

Infine, possiamo elaborare il dataset nel formato previsto dal modello per l’addestramento. Utilizzeremo la funzione map(...).

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 linguaggio, tuttavia, questo passaggio può includere un’estrazione delle caratteristiche più complessa, come l’estrazione delle caratteristiche Log-Mel. Terzo, codifichiamo le trascrizioni in identificatori di 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 incapsula il processore nel contesto as_target_processor, lo stesso metodo viene reindirizzato al metodo di chiamata di Wav2Vec2CTCTokenizer. Per ulteriori informazioni, consulta la documentazione.

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

    # l'output in batch viene "scomposto" per assicurarsi che la mappatura sia corretta
    batch["input_values"] = processor(audio["array"], sampling_rate=audio["sampling_rate"]).input_values[0]
    
    with processor.as_target_processor():
        batch["labels"] = processor(batch["text"]).input_ids
    return batch

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

timit = timit.map(prepare_dataset, remove_columns=timit.column_names["train"], num_proc=4)

Nota: Attualmente datasets fa uso di torchaudio e librosa per il caricamento e il ridimensionamento dell’audio. Se desideri implementare il tuo proprio caricamento/ridimensionamento dei dati personalizzato, puoi semplicemente utilizzare la colonna "path" e ignorare la colonna "audio".

Addestramento e Valutazione

I dati vengono elaborati in modo che siamo pronti per iniziare a configurare il flusso di addestramento. Utilizzeremo il Trainer di 🤗 per il quale essenzialmente dobbiamo fare quanto segue:

  • Definire un data collator. A differenza della maggior parte dei modelli di NLP, Wav2Vec2 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 eseguire il padding dinamico dei batch di addestramento, ovvero tutti i campioni di addestramento dovrebbero essere riempiti solo fino al campione più lungo nel loro batch e non al campione più lungo in assoluto. Pertanto, per il fine-tuning di Wav2Vec2 è necessario un data collator speciale per il padding, che definiremo di seguito

  • Metrica di valutazione. Durante l’addestramento, il modello dovrebbe essere valutato sul tasso di errore delle parole. Dobbiamo 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 eseguito il fine-tuning del modello, lo valuteremo correttamente sui dati di test e verificheremo che abbia effettivamente imparato a trascrivere correttamente il linguaggio 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 gli input_values e le labels in modo diverso e quindi applica loro funzioni di padding separate (utilizzando ancora una volta il context manager di Wav2Vec2). Questo è necessario perché nell’input e nell’output del linguaggio parlato sono di diverse modalità, il che significa che non dovrebbero essere trattati dalla stessa funzione di padding. Analogamente ai data collator comuni, i token di padding nelle etichette vengono sostituiti con -100 in modo che quei token non siano presi in considerazione durante il calcolo della loss.

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 dei dati in input.
    Args:
        processor (:class:`~transformers.Wav2Vec2Processor`)
            Il processore utilizzato per elaborare i 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 e all'indice di padding del modello)
            tra:
            * :obj:`True` o :obj:`'longest'`: Effettua il padding fino alla sequenza più lunga nel batch (o nessun padding se viene fornita solo una
              sequenza).
            * :obj:`'max_length'`: Effettua il padding fino a una lunghezza massima specificata con l'argomento :obj:`max_length` o fino alla
              lunghezza massima di input accettabile per il modello se non viene fornito tale argomento.
            * :obj:`False` o :obj:`'do_not_pad'` (default): Nessun padding (ovvero può restituire un batch con sequenze di
              lunghezze diverse).
        max_length (:obj:`int`, `optional`):
            Lunghezza massima delle ``input_values`` della lista restituita e opzionalmente lunghezza del padding (vedi sopra).
        max_length_labels (:obj:`int`, `optional`):
            Lunghezza massima delle etichette restituite nella lista e opzionalmente lunghezza del padding (vedi sopra).
        pad_to_multiple_of (:obj:`int`, `optional`):
            Se impostato, effettua il padding della sequenza fino a un multiplo del valore fornito.
            Questo è particolarmente utile per abilitare l'uso di Tensor Cores su hardware NVIDIA con capacità di calcolo >=
            7.5 (Volta).
    """

    processor: Wav2Vec2Processor
    padding: Union[bool, str] = True
    max_length: Optional[int] = None
    max_length_labels: Optional[int] = None
    pad_to_multiple_of: Optional[int] = None
    pad_to_multiple_of_labels: Optional[int] = None

    def __call__(self, features: List[Dict[str, Union[List[int], torch.Tensor]]]) -> Dict[str, torch.Tensor]:
        # divide gli input e le etichette poiché devono avere lunghezze diverse e richiedono
        # diverse funzioni di padding
        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,
            max_length=self.max_length,
            pad_to_multiple_of=self.pad_to_multiple_of,
            return_tensors="pt",
        )
        with self.processor.as_target_processor():
            labels_batch = self.processor.pad(
                label_features,
                padding=self.padding,
                max_length=self.max_length_labels,
                pad_to_multiple_of=self.pad_to_multiple_of_labels,
                return_tensors="pt",
            )

        # sostituisci 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

Inizializziamo il collatore dei dati.

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 useremo anche in questo notebook.

wer_metric = load_metric("wer")

Il modello restituirà una sequenza di vettori di logiti:

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 logiti y 1 \mathbf{y}_1 y 1 ​ contiene le log-verosimiglianze 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’argomento massimo dei logiti. Inoltre, trasformiamo le etichette codificate 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 Wav2Vec2. L’pad_token_id del tokenizzatore deve essere definito come pad_token_id del modello o nel caso di Wav2Vec2ForCTC anche il token vuoto di CTC 2 {}^2 2 . Per risparmiare memoria GPU, abilitiamo il checkpointing dei gradienti di PyTorch e impostiamo anche la riduzione della perdita su ” mean “.

from transformers import Wav2Vec2ForCTC

model = Wav2Vec2ForCTC.from_pretrained(
    "facebook/wav2vec2-base", 
    ctc_loss_reduction="mean", 
    pad_token_id=processor.tokenizer.pad_token_id,
)

Stampa Output:

Alcuni pesi di Wav2Vec2ForCTC non sono stati inizializzati dal checkpoint del modello a facebook/wav2vec2-base e sono stati appena inizializzati: ['lm_head.weight', 'lm_head.bias']
Probabilmente dovresti ADDESTRARE questo modello su un compito downstream per poterlo utilizzare per le previsioni e l'elaborazione.

Il primo componente di Wav2Vec2 consiste in una serie di livelli CNN che vengono utilizzati per estrarre caratteristiche acusticamente significative – ma contestualmente indipendenti – dal segnale audio grezzo. Questa parte del modello è già stata sufficientemente addestrata durante il preaddestramento e, come indicato nel paper, non ha bisogno di ulteriori affinamenti. Pertanto, possiamo impostare requires_grad su False per tutti i parametri della parte di estrazione delle caratteristiche.

model.freeze_feature_extractor()

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

  • group_by_length rende l’addestramento più efficiente raggruppando campioni di addestramento con lunghezza di input simile in un singolo batch. Ciò 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 e weight_decay sono stati regolati euristicamente fino a quando il fine-tuning è diventato stabile. Si noti che questi parametri dipendono fortemente dal dataset Timit e potrebbero non essere ottimali per altri dataset di speech.

Per ulteriori spiegazioni su altri parametri, è possibile consultare i documenti.

Durante l’addestramento, un checkpoint verrà caricato in modo asincrono nell’hub ogni 400 passi di addestramento. Ciò ti consente di giocare anche con il widget demo mentre il tuo 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=30,
  fp16=True,
  gradient_checkpointing=True, 
  save_steps=500,
  eval_steps=500,
  logging_steps=500,
  learning_rate=1e-4,
  weight_decay=0.005,
  warmup_steps=1000,
  save_total_limit=2,
)

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=timit_prepared["train"],
    eval_dataset=timit_prepared["test"],
    tokenizer=processor.feature_extractor,
)

1 {}^1 1 Per consentire ai modelli di diventare indipendenti dal tasso di lettura del parlante, in CTC, i token consecutivi che sono identici vengono semplicemente raggruppati come un singolo token. Tuttavia, le etichette codificate non dovrebbero 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" verrebbe codificata in modo errato e decodificata come "helo". 2 {}^2 2 Il token vuoto consente al modello di prevedere una parola, come "hello", obbligandolo a 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].

Addestramento

L’addestramento richiederà tra i 90 e i 180 minuti a seconda della GPU allocata al google colab collegato a questo notebook. Sebbene il modello addestrato produca risultati soddisfacenti sui dati di test di Timit, non è in alcun modo un modello ottimizzato al massimo. Lo scopo di questo notebook è dimostrare come i checkpoint base, large e large-lv60 di Wav2Vec2 possano essere ottimizzati al massimo su qualsiasi set di dati in inglese.

Nel caso in cui si desideri utilizzare questo google colab per ottimizzare al massimo il proprio modello, è necessario assicurarsi che l’addestramento non si interrompa a causa dell’inattività. Un semplice trucco per evitare ciò è incollare il seguente codice nella console di questa scheda (clic destro del mouse -> ispeziona -> scheda Console e inserire il codice).

function ConnectButton(){
    console.log("Connect pushed"); 
    document.querySelector("#top-toolbar > colab-connect-button").shadowRoot.querySelector("#connect").click() 
}
setInterval(ConnectButton,60000);

trainer.train()

A seconda della tua GPU, potrebbe essere possibile che tu stia visualizzando un errore "out-of-memory" qui. In questo caso, è probabilmente meglio ridurre per_device_train_batch_size a 16 o anche meno e eventualmente utilizzare gradient_accumulation.

Stampa output:

Il WER finale dovrebbe essere inferiore a 0,3, il che è ragionevole dato che i tassi di errore fonemico (PER) all’avanguardia sono appena al di sotto dello 0,1 (vedi classifica) e che il WER di solito è peggiore del PER.

È ora possibile caricare il risultato dell’addestramento nell’Hub, basta eseguire questa istruzione:

trainer.push_to_hub()

È ora possibile condividere questo modello con tutti i tuoi amici, familiari, animali domestici preferiti: possono tutti caricarlo con l’identificatore “tuo-nomeutente/il-nome-che-hai-scelto” ad esempio:

from transformers import AutoModelForCTC, Wav2Vec2Processor

model = AutoModelForCTC.from_pretrained("patrickvonplaten/wav2vec2-base-timit-demo-colab")
processor = Wav2Vec2Processor.from_pretrained("patrickvonplaten/wav2vec2-base-timit-demo-colab")

Valutazione

Nella parte finale, valutiamo il nostro modello ottimizzato sul set di test e giochiamo un po’ con esso.

Carichiamo ora il processore e il modello.

processor = Wav2Vec2Processor.from_pretrained(repo_name)
model = Wav2Vec2ForCTC.from_pretrained(repo_name)

Ora, faremo uso della funzione map(...) per prevedere la trascrizione di ogni campione di test e per salvare la previsione nel dataset stesso. Chiameremo il dizionario risultante "results".

Nota: valutiamo il set di dati di test con batch_size=1 appositamente a causa di questo problema. Poiché gli input con padding non producono lo stesso output degli input senza padding, è possibile ottenere un WER migliore non applicando alcun padding all’input.

def map_to_result(batch):
  with torch.no_grad():
    input_values = torch.tensor(batch["input_values"], device="cuda").unsqueeze(0)
    logits = model(input_values).logits

  pred_ids = torch.argmax(logits, dim=-1)
  batch["pred_str"] = processor.batch_decode(pred_ids)[0]
  batch["text"] = processor.decode(batch["labels"], group_tokens=False)
  
  return batch

results = timit["test"].map(map_to_result, remove_columns=timit["test"].column_names)

Calcoliamo ora il WER complessivo.

print("WER del test: {:.3f}".format(wer_metric.compute(predictions=results["pred_str"], references=results["text"])))

Output:

    WER del test: 0.221

22,1% WER – non male! Il nostro modello dimostrativo avrebbe probabilmente avuto successo nella classifica ufficiale.

Diamo un’occhiata a alcune previsioni per vedere quali errori commette il modello.

Output:

show_random_elements(results.remove_columns(["speech", "sampling_rate"]))

Diventa chiaro che le trascrizioni previste sono acusticamente molto simili alle trascrizioni target, ma spesso contengono errori di ortografia o grammaticali. Questo non dovrebbe essere molto sorprendente considerando che ci affidiamo esclusivamente a Wav2Vec2 senza utilizzare un modello di linguaggio.

Infine, per comprendere meglio come funziona CTC, vale la pena dare un’occhiata più approfondita all’output esatto del modello. Facciamo passare il primo campione di test attraverso il modello, prendiamo gli id previsti e li convertiamo nei rispettivi token.

model.to("cuda")

with torch.no_grad():
  logits = model(torch.tensor(timit["test"][:1]["input_values"], device="cuda")).logits

pred_ids = torch.argmax(logits, dim=-1)

# convert ids to tokens
" ".join(processor.tokenizer.convert_ids_to_tokens(pred_ids[0].tolist()))

Output:

[PAD] [PAD] [PAD] [PAD] [PAD] [PAD] t t h e e | | b b [PAD] u u n n n g g [PAD] a [PAD] [PAD] l l [PAD] o o o [PAD] | w w a a [PAD] s s | | [PAD] [PAD] p l l e e [PAD] [PAD] s s e n n t t t [PAD] l l y y | | | s s [PAD] i i [PAD] t t t [PAD] u u u u [PAD] [PAD] [PAD] a a [PAD] t t e e e d d d | n n e e a a a r | | t h h e | | s s h h h [PAD] o o o [PAD] o o r r [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD]

L’output dovrebbe rendere un po’ più chiaro come funziona CTC nella pratica. Il modello è in qualche misura invariante al ritmo di parola poiché ha imparato a ripetere lo stesso token nel caso in cui il frammento di discorso da classificare corrisponda ancora allo stesso token. Ciò rende CTC un algoritmo molto potente per il riconoscimento del parlato poiché la trascrizione del file audio è spesso molto indipendente dalla sua lunghezza.

Raccomando ancora una volta al lettore di dare un’occhiata a questo articolo molto interessante per comprendere meglio il CTC.