Potenziamento di Wav2Vec2 con n-grammi in 🤗 Transformers

'Enhancing Wav2Vec2 with n-grams in 🤗 Transformers'

Wav2Vec2 è un modello pre-addestrato popolare per il riconoscimento del linguaggio parlato. Rilasciato nel settembre 2020 da Meta AI Research, l’architettura innovativa ha catalizzato il progresso nel pre-addestramento auto-supervisionato per il riconoscimento del linguaggio parlato, ad esempio G. Ng et al., 2021, Chen et al., 2021, Hsu et al., 2021 e Babu et al., 2021. Sul Hugging Face Hub, il checkpoint pre-addestrato più popolare di Wav2Vec2 attualmente conta oltre 250.000 download mensili.

Utilizzando la Connectionist Temporal Classification (CTC), i checkpoint pre-addestrati simili a Wav2Vec2 sono estremamente facili da adattare a compiti di riconoscimento del linguaggio parlato. In poche parole, l’adattamento dei checkpoint pre-addestrati di Wav2Vec2 funziona nel seguente modo:

Uno strato lineare singolo inizializzato casualmente viene impilato sopra il checkpoint pre-addestrato e addestrato per classificare l’input audio raw in una sequenza di lettere. Lo fa:

  1. estraendo rappresentazioni audio dall’audio raw (utilizzando strati CNN),
  2. elaborando la sequenza di rappresentazioni audio con una pila di strati di trasformatori, e,
  3. classificando le rappresentazioni audio elaborate in una sequenza di lettere di output.

In precedenza, i modelli di classificazione audio richiedevano un modello di linguaggio (LM) aggiuntivo e un dizionario per trasformare la sequenza di frame audio classificati in una trascrizione coerente. L’architettura di Wav2Vec2 si basa su strati di trasformatori, dando così a ogni rappresentazione audio elaborata il contesto di tutte le altre rappresentazioni audio. Inoltre, Wav2Vec2 sfrutta l’algoritmo CTC per l’adattamento, che risolve il problema dell’allineamento tra un rapporto “lunghezza audio di input”-“lunghezza testo di output” variabile.

Avere classificazioni audio contestualizzate e nessun problema di allineamento fa sì che Wav2Vec2 non richieda un modello di linguaggio esterno o un dizionario per produrre trascrizioni audio accettabili.

Come si può vedere nell’Appendice C dell’articolo ufficiale, Wav2Vec2 offre prestazioni impressionanti su LibriSpeech senza utilizzare alcun modello di linguaggio. Tuttavia, dall’appendice, diventa anche chiaro che l’utilizzo di Wav2Vec2 in combinazione con un modello di linguaggio può portare a un miglioramento significativo, soprattutto quando il modello è stato addestrato solo su 10 minuti di audio trascritto.

Fino a poco tempo fa, la libreria 🤗 Transformers non offriva un’interfaccia utente semplice per decodificare file audio con un Wav2Vec2 adattato e un modello di linguaggio. Fortunatamente, questo è cambiato. 🤗 Transformers offre ora un’integrazione semplice da usare con la libreria pyctcdecode di Kensho Technologies. Questo post sul blog è una guida tecnica passo dopo passo per spiegare come è possibile creare un modello di linguaggio n-gram e combinarlo con un checkpoint Wav2Vec2 adattato esistente utilizzando 🤗 Datasets e 🤗 Transformers.

Iniziamo con:

  1. Come differisce la decodifica audio con un modello di linguaggio rispetto alla decodifica audio senza un modello di linguaggio?
  2. Come ottenere dati adatti per un modello di linguaggio?
  3. Come costruire un n-gram con KenLM?
  4. Come combinare l’n-gram con un checkpoint Wav2Vec2 adattato?

Per una disamina approfondita su come funziona Wav2Vec2 – che non è necessaria per questo post sul blog – si consiglia al lettore di consultare il materiale seguente:

  • wav2vec 2.0: A Framework for Self-Supervised Learning of Speech Representations
  • Fine-Tune Wav2Vec2 for English ASR with 🤗 Transformers
  • An Illustrated Tour of Wav2vec 2.0

1. Decodifica dei dati audio con Wav2Vec2 e un modello di linguaggio

Come mostrato nell’esempio dei documenti di 🤗 Transformers di Wav2Vec2, l’audio può essere trascritto come segue.

Prima di tutto, installiamo datasets e transformers.

pip install datasets transformers

Carichiamo un breve estratto del dataset Librispeech per dimostrare le capacità di trascrizione del linguaggio parlato di Wav2Vec2.

from datasets import load_dataset

dataset = load_dataset("hf-internal-testing/librispeech_asr_demo", "clean", split="validation")
dataset

Output:

    Riutilizzo del dataset librispeech_asr (/root/.cache/huggingface/datasets/hf-internal-testing___librispeech_asr/clean/2.1.0/f2c70a4d03ab4410954901bde48c54b85ca1b7f9bf7d616e7e2a72b5ee6ddbfc)

    Dataset({
        features: ['file', 'audio', 'testo', 'id_speaker', 'id_capitolo', 'id'],
        numero_righe: 73
    })

Possiamo scegliere una delle 73 campioni audio e ascoltarla.

audio_sample = dataset[2]
audio_sample["testo"].lower()

Output:

    ci dice che in questa stagione festiva dell'anno, con il Natale e il roast beef che ci attendono, le similitudini tratte dal cibo e dai suoi risultati vengono alla mente con maggiore facilità

Dopo aver scelto un campione di dati, carichiamo il modello e il processore pre-ottimizzati.

from transformers import Wav2Vec2Processor, Wav2Vec2ForCTC

processore = Wav2Vec2Processor.from_pretrained("facebook/wav2vec2-base-100h")
modello = Wav2Vec2ForCTC.from_pretrained("facebook/wav2vec2-base-100h")

Successivamente, elaboriamo i dati

input = processore(audio_sample["audio"]["array"], sampling_rate=audio_sample["audio"]["sampling_rate"], return_tensors="pt")

lo inviamo al modello

import torch

with torch.no_grad():
  logits = modello(**input).logits

e lo decodifichiamo

predicted_ids = torch.argmax(logits, dim=-1)
trascrizione = processore.batch_decode(predicted_ids)

trascrizione[0].lower()

Output:

'ci dice che in questa stagione festiva dell'anno, con il Natale e il roast beef che ci attendono, le similitudini tratte dal cibo e dai suoi risultati vengono alla mente con maggiore facilità'

Confrontando la trascrizione con la trascrizione target sopra, possiamo vedere che alcune parole suonano corrette, ma non sono ortograficamente corrette, ad esempio:

  • ci dice che in questa stagione festiva dell’anno, con il Natale e il roast beef che ci attendono, le similitudini tratte dal cibo e dai suoi risultati vengono alla mente con maggiore facilità vs. ci dice che in questa stagione festiva dell’anno, con il Natale e il roast beef che ci attendono, le similitudini tratte dal cibo e dai suoi risultati vengono alla mente con maggiore facilità
  • rose vs. roast
  • simalyis vs. similitudini

Vediamo se combinare Wav2Vec2 con un modello linguistico n-gram può aiutare in questo caso.

Prima, dobbiamo installare pyctcdecode e kenlm.

pip install https://github.com/kpu/kenlm/archive/master.zip pyctcdecode

A scopo dimostrativo, abbiamo preparato un nuovo repository di modelli patrickvonplaten/wav2vec2-base-100h-with-lm che contiene lo stesso checkpoint di Wav2Vec2 ma ha un modello linguistico 4-gram aggiuntivo per l’inglese.

Invece di utilizzare Wav2Vec2Processor, questa volta utilizziamo Wav2Vec2ProcessorWithLM per caricare il modello 4-gram oltre all’estratore di caratteristiche e al tokenizer.

from transformers import Wav2Vec2ProcessorWithLM

processore = Wav2Vec2ProcessorWithLM.from_pretrained("patrickvonplaten/wav2vec2-base-100h-with-lm")

A differenza della decodifica dell’audio senza modello linguistico, il processore riceve direttamente in input gli output del modello logits invece dell’argmax(logits) (chiamato predicted_ids) sopra. Il motivo è che durante la decodifica con un modello linguistico, ad ogni passo temporale, il processore tiene conto delle probabilità di tutti i possibili caratteri di output. Vediamo le dimensioni dell’output logits.

logits.shape

Output:

    torch.Size([1, 624, 32])

Possiamo vedere che i logits corrispondono a una sequenza di 624 vettori, ognuno dei quali ha 32 elementi. Ogni dei 32 elementi rappresenta la probabilità logaritmica di uno dei 32 possibili caratteri di output del modello:

" ".join(sorted(processor.tokenizer.get_vocab()))

Output:

"' </s> <pad> <s> <unk> A B C D E F G H I J K L M N O P Q R S T U V W X Y Z |"

In modo intuitivo, si può comprendere il processo di decodifica di Wav2Vec2ProcessorWithLM come l’applicazione di una ricerca a fascio attraverso una matrice di dimensioni 624 $\times$ 32 di probabilità, sfruttando le probabilità delle lettere successive fornite dal modello di linguaggio n-gramma.

OK, eseguiamo nuovamente il passaggio di decodifica. Il decodificatore del modello di linguaggio pyctcdecode non converte automaticamente i tensori torch in numpy, quindi dovremo convertirli manualmente prima.

transcription = processor.batch_decode(logits.numpy()).text
transcription[0].lower()

Output:

'ci dice che in questa festosa stagione dell'anno, con il Natale e il roast beef che si avvicinano, le similitudini tratte dal mangiare e dai suoi risultati si presentano più facilmente alla mente'

Grandioso! Ripensando alle parole facebook/wav2vec2-base-100h senza un modello di linguaggio trascritte in modo errato in precedenza, ad esempio:

  • christmaus vs. christmas
  • rose vs. roast
  • simalyis vs. similes

possiamo dare un’occhiata alla trascrizione di facebook/wav2vec2-base-100h con un modello di linguaggio 4-gramma. 2 degli 3 errori sono stati corretti; christmas e similes sono stati trascritti correttamente.

Curiosamente, la trascrizione errata di rose persiste. Tuttavia, questo non dovrebbe sorprenderci molto. La decodifica audio senza un modello di linguaggio è molto più incline a generare errori di ortografia, come christmaus o similes (quelle parole non esistono nella lingua inglese per quanto ne so). Questo perché il sistema di riconoscimento vocale si basa quasi esclusivamente sull’input acustico che gli è stato fornito e non realmente sul contesto di modellazione linguistica delle lettere predette precedenti e successive 1 {}^1 1 . Se invece aggiungiamo un modello di linguaggio, possiamo essere abbastanza sicuri che il sistema di riconoscimento vocale ridurrà notevolmente gli errori di ortografia poiché un modello n-gramma ben addestrato sicuramente non prevederà una parola con errori di ortografia. Ma la parola rose è una parola inglese valida e quindi il 4-gramma prevederà questa parola con una probabilità non insignificante.

Il modello di linguaggio da solo probabilmente favorisce la parola corretta roast poiché la sequenza di parole roast beef è molto più comune in inglese rispetto a rose beef . Poiché la trascrizione finale deriva da una combinazione ponderata delle probabilità di output di facebook/wav2vec2-base-100h e di quelle del modello di linguaggio n-gramma, è piuttosto comune vedere parole trascritte in modo errato come rose .

Per ulteriori informazioni su come è possibile modificare diversi parametri durante la decodifica con Wav2Vec2ProcessorWithLM , si prega di consultare la documentazione ufficiale qui .


1 {}^1 1 Alcune ricerche mostrano che un modello come facebook/wav2vec2-base-100h – quando sufficientemente grande e addestrato su dati sufficienti – può apprendere le dipendenze di modellazione del linguaggio tra le rappresentazioni audio intermedie simili a un modello di linguaggio.

Ottimo, ora che hai visto i vantaggi di aggiungere un modello di linguaggio n-gramma, immergiamoci nel processo di creazione di un n-gramma e di Wav2Vec2ProcessorWithLM da zero.

2. Ottenere i dati per il tuo modello di linguaggio

Un modello di linguaggio che è utile per un sistema di riconoscimento vocale dovrebbe supportare il modello acustico, ad esempio Wav2Vec2, nella previsione della parola successiva (o del token, della lettera) e quindi modellare la seguente distribuzione: P ( w n ∣ w 0 t − 1 ) \mathbf{P}(w_n | \mathbf{w}_0^{t-1}) P ( w n ​ ∣ w 0 t − 1 ​ ) con w n w_n w n ​ che rappresenta la parola successiva e w 0 t − 1 \mathbf{w}_0^{t-1} w 0 t − 1 ​ che rappresenta la sequenza di tutte le parole precedenti dall’inizio dell’enunciato. In parole semplici, il modello di linguaggio dovrebbe essere bravo a prevedere la parola successiva dato tutte le parole trascritte precedentemente, indipendentemente dall’input audio fornito al sistema di riconoscimento vocale.

Come sempre, un modello di linguaggio è tanto buono quanto i dati su cui è stato addestrato. Nel caso del riconoscimento vocale, dovremmo quindi chiederci per quale tipo di dati il riconoscimento vocale sarà utilizzato: conversazioni, audiolibri, film, discorsi, ecc…?

Il modello di linguaggio dovrebbe essere in grado di modellare correttamente il linguaggio corrispondente alle trascrizioni target del sistema di riconoscimento vocale. A scopo dimostrativo, assumiamo qui di aver addestrato un checkpoint pre-addestrato facebook/wav2vec2-xls-r-300m su Common Voice 7 in svedese. Il checkpoint addestrato può essere trovato qui. Common Voice 7 è un dataset audio relativamente crowdsourcing e valuteremo il modello sui suoi dati di test.

Ora cerchiamo dati testuali adatti sul Hugging Face Hub. Cercare tra tutti i dataset quelli che contengono dati svedesi. Dopo aver navigato un po’ tra i dataset, cerchiamo un dataset simile ai dati audio di Common Voice. Le scelte ovvie di oscar e mc4 potrebbero non essere le più adatte qui perché:

  • sono generati attraverso il crawling sul web, il che potrebbe non essere molto pulito e corrispondente al linguaggio parlato
  • richiedono molta pre-elaborazione
  • sono molto grandi, il che non è ideale per scopi dimostrativi 😉

Un dataset che sembra sensato qui e che è relativamente pulito e facile da pre-elaborare è europarl_bilingual in quanto è un dataset basato su discussioni e incontri del Parlamento europeo. Dovrebbe quindi essere relativamente pulito e corrispondere bene ai dati audio di lettura. Il dataset è originariamente progettato per la traduzione automatica e può quindi essere accessibile solo in coppie di traduzioni. Estrarremo solo il testo della lingua target, lo svedese (sv), dalle traduzioni dall’inglese allo svedese.

target_lang="sv"  # cambia con la tua lingua target

Scarichiamo i dati.

from datasets import load_dataset

dataset = load_dataset("europarl_bilingual", lang1="en", lang2=target_lang, split="train")

Vediamo che i dati sono abbastanza grandi: hanno oltre un milione di traduzioni. Tuttavia, essendo solo dati testuali, dovrebbero essere relativamente facili da elaborare.

Successivamente, vediamo come sono stati pre-elaborati i dati durante l’addestramento del checkpoint XLS-R in svedese. Guardando il file run.sh, possiamo vedere che dai testi ufficiali sono stati rimossi i seguenti caratteri:

chars_to_ignore_regex = '[,?.!\-\;\:"“%‘”�—’…–]'  # cambia con i caratteri ignorati dal tuo modello pre-addestrato

Facciamo lo stesso qui in modo che l’alfabeto del nostro modello di linguaggio corrisponda a quello dei checkpoint acustici pre-addestrati.

Possiamo scrivere una singola funzione di mappatura per estrarre il testo svedese e elaborarlo immediatamente.

import re

def extract_text(batch):
  text = batch["translation"][target_lang]
  batch["text"] = re.sub(chars_to_ignore_regex, "", text.lower())
  return batch

Applichiamo la funzione .map(). Ciò richiederà circa 5 minuti.

dataset = dataset.map(extract_text, remove_columns=dataset.column_names)

Fantastico. Carichiamolo su Hub in modo da poterlo ispezionare e riutilizzare meglio.

Puoi effettuare il login eseguendo la cella seguente.

from huggingface_hub import notebook_login

notebook_login()

Output:

    Login effettuato con successo
    Il tuo token è stato salvato in /root/.huggingface/token
    Autenticato tramite git-credential store, ma questo non è l'helper definito sulla tua macchina.
    Potresti dover ri-autenticarti quando fai push su Hugging Face Hub. Esegui il comando seguente nel tuo terminale nel caso in cui vuoi impostare questo credential helper come predefinito

    git config --global credential.helper store

Successivamente, chiamiamo il metodo push_to_hub di 🤗 Hugging Face per caricare il dataset nel repository "sv_corpora_parliament_processed".

dataset.push_to_hub(f"{target_lang}_corpora_parliament_processed", split="train")

È stato facile! La visualizzazione del dataset è automaticamente abilitata durante il caricamento di un nuovo dataset, il che è molto comodo. Ora puoi ispezionare direttamente il dataset online.

Sentiti libero di consultare direttamente il nostro dataset preelaborato su hf-test/sv_corpora_parliament_processed. Anche se non siamo madrelingua svedesi, possiamo vedere che i dati sono ben elaborati e sembrano puliti.

Successivamente, utilizzeremo i dati per creare un modello linguistico.

3. Creare un n-gramma con KenLM

Mentre i grandi modelli linguistici basati sull’architettura Transformer sono diventati lo standard in NLP, è ancora molto comune utilizzare un LM basato su n-grammi per migliorare i sistemi di riconoscimento vocale, come mostrato nella Sezione 1.

Riguardando la Tabella 9 dell’Appendice C del documento ufficiale di Wav2Vec2, si può notare che l’utilizzo di un LM basato su Transformer per la decodifica produce chiaramente risultati migliori rispetto all’utilizzo di un modello n-gramma, ma la differenza tra l’LM basato su n-gramma e quello basato su Transformer è molto meno significativa rispetto alla differenza tra n-gramma e nessun LM.

Ad esempio, per il grande checkpoint di Wav2Vec2 che è stato sottoposto a fine-tuning su soli 10 minuti, un n-gramma riduce il tasso di errore delle parole (WER) rispetto a nessun LM di circa l’80%, mentre un LM basato su Transformer riduce il WER di un ulteriore 23% rispetto al n-gramma. Questa riduzione relativa del WER diventa minore quanto più dati il modello acustico è stato addestrato. Ad esempio, per il grande checkpoint un LM basato su Transformer riduce il WER di soli l’8% rispetto a un LM basato su n-gramma, mentre il n-gramma produce ancora una riduzione del WER del 21% rispetto a nessun modello linguistico.

Il motivo per cui si preferisce un n-gramma rispetto a un LM basato su Transformer è che gli n-grammi hanno un costo computazionale significativamente inferiore. Per un n-gramma, recuperare la probabilità di una parola data le parole precedenti è quasi altrettanto costoso in termini computazionali come interrogare una tabella di ricerca o una struttura dati ad albero, ovvero è molto veloce rispetto ai moderni modelli linguistici basati su Transformer che richiederebbero un passaggio completo per recuperare le probabilità delle parole successive.

Per ulteriori informazioni su come funzionano gli n-grammi e perché sono (ancora) così utili per il riconoscimento vocale, si consiglia al lettore di dare un’occhiata a questo eccellente riassunto da Stanford.

Perfetto, vediamo passo dopo passo come creare un n-gramma. Utilizzeremo la popolare libreria KenLM per farlo. Iniziamo installando le dipendenze della libreria Ubuntu:

sudo apt install build-essential cmake libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-test-dev libeigen3-dev zlib1g-dev libbz2-dev liblzma-dev

prima di scaricare e decomprimere il repository di KenLM.

wget -O - https://kheafield.com/code/kenlm.tar.gz | tar xz

KenLM è scritto in C++, quindi faremo uso di cmake per compilare i binari.

mkdir kenlm/build && cd kenlm/build && cmake .. && make -j2
ls kenlm/build/bin

Eccellente, come possiamo vedere, le funzioni eseguibili sono state costruite con successo in kenlm/build/bin/.

KenLM calcola per impostazione predefinita un n-gramma con smoothing Kneser-Ney. Tutti i dati di testo utilizzati per creare l’n-gramma devono essere salvati in un file di testo. Scarichiamo il nostro dataset e lo salviamo come file .txt.

from datasets import load_dataset

username = "hf-test"  # cambia con il tuo username

dataset = load_dataset(f"{username}/{target_lang}_corpora_parliament_processed", split="train")

with open("text.txt", "w") as file:
  file.write(" ".join(dataset["text"]))

Ora, dobbiamo solo eseguire il comando lmplz di KenLM per creare il nostro n-gramma, chiamato "5gram.arpa". Poiché è relativamente comune nel riconoscimento vocale, creiamo un 5-gramma passando il parametro -o 5. Per ulteriori informazioni sui diversi modelli di LM n-gramma che possono essere creati con KenLM, è possibile consultare il sito web ufficiale di KenLM.

L’esecuzione del comando sottostante potrebbe richiedere un minuto circa.

kenlm/build/bin/lmplz -o 5 <"text.txt" > "5gram.arpa"

Output:

    === 1/5 Conteggio e ordinamento degli n-grammi ===
    Lettura di /content/swedish_text.txt
    ----5---10---15---20---25---30---35---40---45---50---55---60---65---70---75---80---85---90---95--100
    tcmalloc: grande allocazione 1918697472 byte == 0x55d40d0f0000 @  0x7fdccb1a91e7 0x55d40b2f17a2 0x55d40b28c51e 0x55d40b26b2eb 0x55d40b257066 0x7fdcc9342bf7 0x55d40b258baa
    tcmalloc: grande allocazione 8953896960 byte == 0x55d47f6c0000 @  0x7fdccb1a91e7 0x55d40b2f17a2 0x55d40b2e07ca 0x55d40b2e1208 0x55d40b26b308 0x55d40b257066 0x7fdcc9342bf7 0x55d40b258baa
    ****************************************************************************************************
    Token unigramma 42153890 tipi 360209
    === 2/5 Calcolo e ordinamento dei conteggi adattati ===
    Dimensioni catene: 1:4322508 2:1062772928 3:1992699264 4:3188318720 5:4649631744
    tcmalloc: grande allocazione 4649631744 byte == 0x55d40d0f0000 @  0x7fdccb1a91e7 0x55d40b2f17a2 0x55d40b2e07ca 0x55d40b2e1208 0x55d40b26b8d7 0x55d40b257066 0x7fdcc9342bf7 0x55d40b258baa
    tcmalloc: grande allocazione 1992704000 byte == 0x55d561ce0000 @  0x7fdccb1a91e7 0x55d40b2f17a2 0x55d40b2e07ca 0x55d40b2e1208 0x55d40b26bcdd 0x55d40b257066 0x7fdcc9342bf7 0x55d40b258baa
    tcmalloc: grande allocazione 3188326400 byte == 0x55d695a86000 @  0x7fdccb1a91e7 0x55d40b2f17a2 0x55d40b2e07ca 0x55d40b2e1208 0x55d40b26bcdd 0x55d40b257066 0x7fdcc9342bf7 0x55d40b258baa
    Statistiche:
    1 360208 D1=0.686222 D2=1.01595 D3+=1.33685
    2 5476741 D1=0.761523 D2=1.06735 D3+=1.32559
    3 18177681 D1=0.839918 D2=1.12061 D3+=1.33794
    4 30374983 D1=0.909146 D2=1.20496 D3+=1.37235
    5 37231651 D1=0.944104 D2=1.25164 D3+=1.344
    Stima di memoria per il modello binario LM:
    tipo      MB
    sondaggio 1884 assumendo -p 1.5
    sondaggio 2195 assumendo -r models -p 1.5
    trie     922 senza quantizzazione
    trie     518 assumendo -q 8 -b 8 quantizzazione 
    trie     806 assumendo -a 22 compressione puntatore array
    trie     401 assumendo -a 22 -q 8 -b 8 compressione puntatore array e quantizzazione
    === 3/5 Calcolo e ordinamento delle probabilità iniziali ===
    Dimensioni catene: 1:4322496 2:87627856 3:363553620 4:728999592 5:1042486228
    ----5---10---15---20---25---30---35---40---45---50---55---60---65---70---75---80---85---90---95--100
    ####################################################################################################
    === 4/5 Calcolo e scrittura delle probabilità interpolate per ordine ===
    Dimensioni catene: 1:4322496 2:87627856 3:363553620 4:728999592 5:1042486228
    ----5---10---15---20---25---30---35---40---45---50---55---60---65---70---75---80---85---90---95--100
    ####################################################################################################
    === 5/5 Scrittura del modello ARPA ===
    ----5---10---15---20---25---30---35---40---45---50---55---60---65---70---75---80---85---90---95--100
    ****************************************************************************************************
    Nome: lmplz  VmPeak: 14181536 kB  VmRSS: 2199260 kB    RSSMax: 4160328 kB   utente: 120.598    sys: 26.6659 CPU: 147.264 reale: 136.344

Ottimo, abbiamo costruito un modello di linguaggio 5-grammi! Esaminiamo le prime righe.

head -20 5gram.arpa

Output:

    \data\
    ngram 1=360208
    ngram 2=5476741
    ngram 3=18177681
    ngram 4=30374983
    ngram 5=37231651

    \1-grammi:
    -6.770219   <unk> 0
    0   <s>   -0.11831701
    -4.6095004  återupptagande  -1.2174699
    -2.2361007  av  -0.79668784
    -4.8163533  sessionen   -0.37327805
    -2.2251768  jag -1.4205662
    -4.181505   förklarar   -0.56261665
    -3.5790775  europaparlamentets  -0.63611007
    -4.771945   session -0.3647111
    -5.8043895  återupptagen    -0.3058712
    -2.8580177  efter   -0.7557702
    -5.199537   avbrottet   -0.43322718

C’è un piccolo problema che 🤗 Transformers non apprezzerà in seguito. Il modello 5-grammi include correttamente un token “Sconosciuto” o <unk>, così come un token di inizio frase <s>, ma non un token di fine frase </s>. Purtroppo, al momento è necessario correggere questo problema dopo la creazione del modello.

Possiamo semplicemente aggiungere il token di fine frase inserendo la riga 0 </s> -0.11831701 sotto il token di inizio frase e aumentando il conteggio di ngram 1 di 1. Poiché il file ha circa 100 milioni di righe, questo comando richiederà circa 2 minuti.

with open("5gram.arpa", "r") as read_file, open("5gram_correct.arpa", "w") as write_file:
  has_added_eos = False
  for line in read_file:
    if not has_added_eos and "ngram 1=" in line:
      count=line.strip().split("=")[-1]
      write_file.write(line.replace(f"{count}", f"{int(count)+1}"))
    elif not has_added_eos and "<s>" in line:
      write_file.write(line)
      write_file.write(line.replace("<s>", "</s>"))
      has_added_eos = True
    else:
      write_file.write(line)

Ora esaminiamo il modello 5-grammi corretto.

head -20 5gram_correct.arpa

Output:

    \data\
    ngram 1=360209
    ngram 2=5476741
    ngram 3=18177681
    ngram 4=30374983
    ngram 5=37231651

    \1-grammi:
    -6.770219   <unk> 0
    0   <s>   -0.11831701
    0   </s>  -0.11831701
    -4.6095004  återupptagande  -1.2174699
    -2.2361007  av  -0.79668784
    -4.8163533  sessionen   -0.37327805
    -2.2251768  jag -1.4205662
    -4.181505   förklarar   -0.56261665
    -3.5790775  europaparlamentets  -0.63611007
    -4.771945   session -0.3647111
    -5.8043895  återupptagen    -0.3058712
    -2.8580177  efter   -0.7557702

Ottimo, ora sembra migliore! A questo punto abbiamo finito e tutto ciò che resta da fare è integrare correttamente il "ngram" con pyctcdecode e 🤗 Transformers.

4. Combina un n-gramma con Wav2Vec2

In un ultimo passaggio, vogliamo avvolgere il 5-gramma in un oggetto Wav2Vec2ProcessorWithLM per rendere la decodifica potenziata del 5-gramma senza soluzione di continuità come mostrato nella Sezione 1. Iniziamo scaricando l’attuale processore “senza LM” di xls-r-300m-sv.

from transformers import AutoProcessor

processor = AutoProcessor.from_pretrained("hf-test/xls-r-300m-sv")

Successivamente, estraiamo il vocabolario del suo tokenizer in quanto rappresenta le “etichette” della classe BeamSearchDecoder di pyctcdecode.

vocab_dict = processor.tokenizer.get_vocab()
sorted_vocab_dict = {k.lower(): v for k, v in sorted(vocab_dict.items(), key=lambda item: item[1])}

Le “etichette” e il file 5gram_correct.arpa precedentemente creato sono tutto ciò che serve per costruire il decoder.

from pyctcdecode import build_ctcdecoder

decoder = build_ctcdecoder(
    labels=list(sorted_vocab_dict.keys()),
    kenlm_model_path="5gram_correct.arpa",
)

Output:

    Trovate voci di lunghezza > 1 nell'alfabeto. Questo è insolito a meno che lo stile sia BPE, ma l'alfabeto non è stato riconosciuto come tipo BPE. È corretto?
    Unigrammi ed etichette non sembrano essere concordi.

Possiamo tranquillamente ignorare l’avvertimento e tutto ciò che resta da fare ora è avvolgere il decoder appena creato, insieme al tokenizer e al feature_extractor del processore, in una classe Wav2Vec2ProcessorWithLM.

from transformers import Wav2Vec2ProcessorWithLM

processor_with_lm = Wav2Vec2ProcessorWithLM(
    feature_extractor=processor.feature_extractor,
    tokenizer=processor.tokenizer,
    decoder=decoder
)

Vogliamo caricare direttamente il processore potenziato da LM nella cartella del modello di xls-r-300m-sv per avere tutti i file pertinenti in un unico posto.

Cloniamo il repository, aggiungiamo i nuovi file del decoder e li carichiamo successivamente. Prima, è necessario installare git-lfs.

sudo apt-get install git-lfs tree

Clonare e caricare i file di modellazione può essere fatto comodamente con la classe Repository di huggingface_hub.

Per ulteriori informazioni su come utilizzare huggingface_hub per caricare qualsiasi file, si prega di consultare la documentazione ufficiale.

from huggingface_hub import Repository

repo = Repository(local_dir="xls-r-300m-sv", clone_from="hf-test/xls-r-300m-sv")

Output:

    Clonazione di https://huggingface.co/hf-test/xls-r-300m-sv nella directory locale vuota.

Dopo aver clonato xls-r-300m-sv, salviamo il nuovo processore con LM al suo interno.

processor_with_lm.save_pretrained("xls-r-300m-sv")

Esaminiamo il repository locale. Il comando tree può mostrare comodamente anche le dimensioni dei diversi file.

tree -h xls-r-300m-sv/

Output:

    xls-r-300m-sv/
    ├── [  23]  added_tokens.json
    ├── [ 401]  all_results.json
    ├── [ 253]  alphabet.json
    ├── [2.0K]  config.json
    ├── [ 304]  emissions.csv
    ├── [ 226]  eval_results.json
    ├── [4.0K]  language_model
    │   ├── [4.1G]  5gram_correct.arpa
    │   ├── [  78]  attrs.json
    │   └── [4.9M]  unigrams.txt
    ├── [ 240]  preprocessor_config.json
    ├── [1.2G]  pytorch_model.bin
    ├── [3.5K]  README.md
    ├── [4.0K]  runs
    │   └── [4.0K]  Jan09_22-00-50_brutasse
    │       ├── [4.0K]  1641765760.8871996
    │       │   └── [4.6K]  events.out.tfevents.1641765760.brutasse.31164.1
    │       ├── [ 42K]  events.out.tfevents.1641765760.brutasse.31164.0
    │       └── [ 364]  events.out.tfevents.1641794162.brutasse.31164.2
    ├── [1.2K]  run.sh
    ├── [ 30K]  run_speech_recognition_ctc.py
    ├── [ 502]  special_tokens_map.json
    ├── [ 279]  tokenizer_config.json
    ├── [ 29K]  trainer_state.json
    ├── [2.9K]  training_args.bin
    ├── [ 196]  train_results.json
    ├── [ 319]  vocab.json
    └── [4.0K]  wandb
        ├── [  52]  debug-internal.log -> run-20220109_220240-1g372i3v/logs/debug-internal.log
        ├── [  43]  debug.log -> run-20220109_220240-1g372i3v/logs/debug.log
        ├── [  28]  latest-run -> run-20220109_220240-1g372i3v
        └── [4.0K]  run-20220109_220240-1g372i3v
            ├── [4.0K]  files
            │   ├── [8.8K]  conda-environment.yaml
            │   ├── [140K]  config.yaml
            │   ├── [4.7M]  output.log
            │   ├── [5.4K]  requirements.txt
            │   ├── [2.1K]  wandb-metadata.json
            │   └── [653K]  wandb-summary.json
            ├── [4.0K]  logs
            │   ├── [3.4M]  debug-internal.log
            │   └── [8.2K]  debug.log
            └── [113M]  run-1g372i3v.wandb

    9 directories, 34 files

Come si può vedere, il modello linguistico 5-grammi è piuttosto grande: supera i 4 GB. Per ridurre la dimensione dell’n-gramma e velocizzare il caricamento, kenLM consente di convertire i file .arpa in binari utilizzando l’eseguibile build_binary.

Facciamone uso qui.

kenlm/build/bin/build_binary xls-r-300m-sv/language_model/5gram_correct.arpa xls-r-300m-sv/language_model/5gram.bin

Output:

    Lettura di xls-r-300m-sv/language_model/5gram_correct.arpa
    ----5---10---15---20---25---30---35---40---45---50---55---60---65---70---75---80---85---90---95--100
    ****************************************************************************************************
    SUCCESSO

Grande, ha funzionato! Rimuoviamo il file .arpa e controlliamo la dimensione del modello linguistico 5-grammi binario.

rm xls-r-300m-sv/language_model/5gram_correct.arpa && tree -h xls-r-300m-sv/

Output:

    xls-r-300m-sv/
    ├── [  23]  added_tokens.json
    ├── [ 401]  all_results.json
    ├── [ 253]  alphabet.json
    ├── [2.0K]  config.json
    ├── [ 304]  emissions.csv
    ├── [ 226]  eval_results.json
    ├── [4.0K]  language_model
    │   ├── [1.8G]  5gram.bin
    │   ├── [  78]  attrs.json
    │   └── [4.9M]  unigrams.txt
    ├── [ 240]  preprocessor_config.json
    ├── [1.2G]  pytorch_model.bin
    ├── [3.5K]  README.md
    ├── [4.0K]  runs
    │   └── [4.0K]  Jan09_22-00-50_brutasse
    │       ├── [4.0K]  1641765760.8871996
    │       │   └── [4.6K]  events.out.tfevents.1641765760.brutasse.31164.1
    │       ├── [ 42K]  events.out.tfevents.1641765760.brutasse.31164.0
    │       └── [ 364]  events.out.tfevents.1641794162.brutasse.31164.2
    ├── [1.2K]  run.sh
    ├── [ 30K]  run_speech_recognition_ctc.py
    ├── [ 502]  special_tokens_map.json
    ├── [ 279]  tokenizer_config.json
    ├── [ 29K]  trainer_state.json
    ├── [2.9K]  training_args.bin
    ├── [ 196]  train_results.json
    ├── [ 319]  vocab.json
    └── [4.0K]  wandb
        ├── [  52]  debug-internal.log -> run-20220109_220240-1g372i3v/logs/debug-internal.log
        ├── [  43]  debug.log -> run-20220109_220240-1g372i3v/logs/debug.log
        ├── [  28]  latest-run -> run-20220109_220240-1g372i3v
        └── [4.0K]  run-20220109_220240-1g372i3v
            ├── [4.0K]  files
            │   ├── [8.8K]  conda-environment.yaml
            │   ├── [140K]  config.yaml
            │   ├── [4.7M]  output.log
            │   ├── [5.4K]  requirements.txt
            │   ├── [2.1K]  wandb-metadata.json
            │   └── [653K]  wandb-summary.json
            ├── [4.0K]  logs
            │   ├── [3.4M]  debug-internal.log
            │   └── [8.2K]  debug.log
            └── [113M]  run-1g372i3v.wandb

    9 directories, 34 file

Bene, abbiamo ridotto l’n-gram di più della metà a meno di 2 GB ora. Nell’ultimo passaggio, carichiamo tutti i file.

repo.push_to_hub(commit_message="Carica il decoder lm-boosted")

Output:

    Git LFS: (1 di 1 file) 1.85 GB / 1.85 GB
    Conteggiando gli oggetti: 9, completato.
    Compressione delta utilizzando fino a 2 thread.
    Comprimendo gli oggetti: 100% (9/9), completato.
    Scrittura degli oggetti: 100% (9/9), 1.23 MiB | 1.92 MiB/s, completato.
    Totale 9 (delta 3), riutilizzati 0 (delta 0)
    Verso https://huggingface.co/hf-test/xls-r-300m-sv
       27d0c57..5a191e2  main -> main

Ecco fatto. Ora dovresti essere in grado di utilizzare il 5gram per la decodifica LM-boosted come mostrato nella Sezione 1.

Come si può vedere sulla scheda del modello di xls-r-300m-sv, il nostro decoder LM-boosted 5gram restituisce un WER dell’18.85% sul set di test di Common Voice, che è una prestazione relativa di ca. 30% 🔥.