Modelli di linguaggio per il completamento di frasi

Modelli di linguaggio per completamento frasi

Una applicazione pratica di un modello linguistico che sceglie la parola candidata più probabile che estende una frase inglese di una sola parola

Foto di Brett Jordan su Unsplash

In collaborazione con Naresh Singh.

Tabella dei contenuti

IntroduzioneProblemaSoluzione

  • Algoritmi e Strutture Dati
  • Elaborazione del Linguaggio Naturale (NLP)
  • Deep Learning (Reti Neurali)

Un modello LSTM

  • Tokenizzazione
  • Il modello PyTorch
  • Utilizzo del modello per eliminare suggerimenti non validi
  • Calcolo della probabilità della parola successiva

Un modello Transformer

Conclusioni

Introduzione

I modelli linguistici come GPT sono diventati molto popolari di recente e vengono utilizzati per una varietà di compiti di generazione di testo, come ad esempio in ChatGPT o altri sistemi di intelligenza artificiale conversazionale. Questi modelli linguistici sono enormi, spesso superano decine di miliardi di parametri e richiedono molte risorse di calcolo e denaro per essere eseguiti.

Nel contesto dei modelli linguistici in lingua inglese, questi modelli massicci sono sovradimensionati poiché utilizzano i parametri del modello per memorizzare e apprendere gli aspetti del nostro mondo anziché solo modellare la lingua inglese. È probabile che si possa utilizzare un modello molto più piccolo se abbiamo un’applicazione che richiede al modello di comprendere solo la lingua e le sue strutture.

Il codice completo per l’esecuzione dell’inferenza sul modello addestrato può essere trovato in questo notebook.

Problema

Supponiamo di voler costruire un sistema di tastiera swipe che cerca di prevedere la parola che l’utente digiterà successivamente sul proprio telefono cellulare. Sulla base del pattern tracciato dallo swipe, ci sono molte possibilità per la parola che l’utente intende. Tuttavia, molte di queste possibili parole non sono parole effettive in inglese e possono essere eliminate. Anche dopo questa fase di eliminazione iniziale, rimangono molti candidati e ne dobbiamo scegliere uno come suggerimento per l’utente.

Per ridurre ulteriormente questa lista di candidati, possiamo utilizzare un modello linguistico basato sull’apprendimento profondo che guarda il contesto fornito e ci dice quale candidato è più probabile per completare la frase.

Ad esempio, se l’utente ha digitato la frase “Ho programmato questo” e poi esegue uno swipe come mostrato di seguito

Allora, alcune possibili parole in lingua inglese che l’utente potrebbe aver inteso sono:

  1. confondere
  2. incontrare

Tuttavia, se ci pensiamo, è probabilmente più probabile che l’utente intendesse “incontrare” e non “confondere” a causa della parola “programmato” nella parte precedente della frase.

Dato tutto ciò che sappiamo finora, quali opzioni abbiamo per fare questa eliminazione in modo programmabile? Facciamo una sessione di brainstorming per trovare delle soluzioni nella sezione sottostante.

Soluzione

Algoritmi e Strutture Dati

Utilizzando i principi fondamentali, sembra ragionevole iniziare con un corpus di dati, trovare coppie di parole che si combinano e addestrare un modello di Markov che predice la probabilità della coppia che si verifica in una frase. Noterai due problemi significativi con questo approccio.

  1. Utilizzo dello spazio: Ci sono tra 250.000 e 1 milione di parole nella lingua inglese, che non includono i numerosi nomi propri che aumentano costantemente in volume. Pertanto, qualsiasi soluzione software tradizionale che modella la probabilità di una coppia di parole che si verificano insieme deve mantenere una tabella di ricerca con 250.000 * 250.000 = 62,5 miliardi di coppie di parole, il che è piuttosto eccessivo. È probabile che molte coppie non si verifichino molto spesso e possano essere eliminate. Anche dopo l’eliminazione, ci sono molte coppie di cui preoccuparsi.
  2. Completezza: Codificare la probabilità di una coppia di parole non rende giustizia al problema in questione. Ad esempio, il contesto della frase precedente viene completamente perso quando si guarda solo l’ultima coppia di parole. Nella frase “Come sta andando la tua giornata” se si vuole controllare la parola dopo “andando”, ci sarebbero molte coppie che iniziano con “andando”. Questo perde tutto il contesto della frase precedente quella parola. Si può immaginare l’utilizzo di triplette di parole, ecc… ma questo aggrava il problema dello spazio menzionato in precedenza.

Concentriamoci su una soluzione che sfrutti la natura della lingua inglese e vediamo se ciò può aiutarci qui.

NLP (Elaborazione del Linguaggio Naturale)

Storicamente, l’area dell’NLP (elaborazione del linguaggio naturale) consisteva nel comprendere le parti del discorso di una frase e utilizzare tali informazioni per prendere decisioni di potatura e previsione. Si può immaginare di utilizzare un tag POS associato a ogni parola per determinare se la parola successiva in una frase è valida.

Tuttavia, il processo di calcolo delle parti del discorso per una frase è un processo complesso di per sé e richiede una comprensione specializzata della lingua, come evidenziato in questa pagina sul tagging delle parti del discorso di NLTK.

Successivamente, diamo uno sguardo a un approccio basato sull’apprendimento approfondito che richiede molti più dati contrassegnati, ma non richiede una grande competenza linguistica per essere costruito.

Deep Learning (Reti Neurali)

L’area dell’NLP è stata sconvolta dall’avvento del deep learning. Con l’invenzione dei modelli di linguaggio basati su LSTM e Transformer, la soluzione spesso consiste nel fornire alcuni dati di alta qualità a un modello e addestrarlo a prevedere la parola successiva.

In sostanza, questo è ciò che il modello GPT sta facendo. I modelli GPT (Generative Pre-Trained Transformer) sono addestrati a prevedere la parola (token) successiva data un prefisso di una frase.

Dato il prefisso di frase “È una cosa meravigliosa”, è probabile che il modello fornisca le seguenti previsioni ad alta probabilità per la parola successiva alla frase.

  1. giornata
  2. esperienza
  3. mondo
  4. vita

È anche probabile che le seguenti parole abbiano una probabilità più bassa di completare il prefisso di frase.

  1. rosso
  2. <li=topo

  3. linea

L’architettura del modello Transformer è al centro di sistemi come ChatGPT. Tuttavia, per l’uso più limitato di apprendere la semantica della lingua inglese, possiamo utilizzare un’architettura di modello più economica da eseguire, come un modello LSTM (long short-term memory).

Un modello LSTM

Costruiamo un semplice modello LSTM e addestriamolo a prevedere il token successivo dato un prefisso di token. Ora, potresti chiederti cosa sia un token.

Tokenizzazione

Tipicamente per i modelli di linguaggio, un token può significare

  1. Un singolo carattere (o un singolo byte)
  2. Una parola intera nella lingua di destinazione
  3. Qualcosa tra 1 e 2. Questo viene generalmente chiamato un sottotoken

Mappare un singolo carattere (o byte) a un token è molto restrittivo poiché sovraccarichiamo quel token per contenere molti contesti su dove appare. Questo perché ad esempio il carattere “c” appare in molte parole diverse e per prevedere il prossimo carattere dopo aver visto il carattere “c” dobbiamo osservare attentamente il contesto precedente.

Mappare una singola parola a un token è anche problematico poiché l’inglese stesso ha tra i 250.000 e 1 milione di parole. Inoltre, cosa succede quando una nuova parola viene aggiunta alla lingua? Dobbiamo tornare indietro e rieseguire l’addestramento dell’intero modello per tener conto di questa nuova parola?

La tokenizzazione dei sottotoken è considerata lo standard del settore nel 2023. Assegna sottostringhe di byte che si verificano frequentemente insieme a token unici. Tipicamente, i modelli di linguaggio hanno da qualche migliaio (ad esempio 4.000) a decine di migliaia (ad esempio 60.000) di token unici. L’algoritmo per determinare cosa costituisce un token è determinato dall’algoritmo BPE (Byte Pair Encoding).

Per scegliere il numero di token unici nel nostro vocabolario (chiamato dimensione del vocabolario), dobbiamo tenere presente alcune cose:

  1. Se scegliamo troppo pochi token, torniamo al regime di un token per carattere ed è difficile per il modello imparare qualcosa di utile.
  2. Se scegliamo troppi token, finiamo in una situazione in cui le tabelle di incorporamento del modello eclissano il resto dei pesi del modello e diventa difficile distribuire il modello in un ambiente limitato. La dimensione della tabella di incorporamento dipenderà dal numero di dimensioni che utilizziamo per ogni token. Non è raro utilizzare una dimensione di incorporamento di 256, 512, 786, ecc… Se utilizziamo una dimensione di incorporamento del token di 512 e abbiamo 100.000 token, otteniamo una tabella di incorporamento che utilizza 200MiB di memoria.

Quindi, dobbiamo trovare un equilibrio nella scelta della dimensione del vocabolario. In questo esempio, selezioniamo 6600 token e addestriamo il nostro tokenizer con una dimensione del vocabolario di 6600. Ora, diamo un’occhiata alla definizione del modello stesso.

Il Modello PyTorch

Il modello in sé è piuttosto semplice. Abbiamo i seguenti livelli:

  1. Token Embedding (dimensione vocabolario=6600, dimensione embedding=512), per una dimensione totale di circa 15MiB (supponendo che il tipo di dati della tabella di embedding sia float32 a 4 byte)
  2. LSTM (numero di livelli=1, dimensione nascosta=786) per una dimensione totale di circa 16MiB
  3. Multi-Layer Perceptron (dimensioni da 786 a 3144 a 6600) per una dimensione totale di circa 93MiB

Il modello completo ha circa 31M di parametri addestrabili per una dimensione totale di circa 120MiB.

Ecco il codice PyTorch per il modello.

class WordPredictionLSTMModel(nn.Module):    def __init__(self, num_embed, embed_dim, pad_idx, lstm_hidden_dim, lstm_num_layers, output_dim, dropout):        super().__init__()        self.vocab_size = num_embed        self.embed = nn.Embedding(num_embed, embed_dim, pad_idx)        self.lstm = nn.LSTM(embed_dim, lstm_hidden_dim, lstm_num_layers, batch_first=True, dropout=dropout)        self.fc = nn.Sequential(            nn.Linear(lstm_hidden_dim, lstm_hidden_dim * 4),            nn.LayerNorm(lstm_hidden_dim * 4),            nn.LeakyReLU(),            nn.Dropout(p=dropout),            nn.Linear(lstm_hidden_dim * 4, output_dim),        )    #        def forward(self, x):        x = self.embed(x)        x, _ = self.lstm(x)        x = self.fc(x)        x = x.permute(0, 2, 1)        return x    ##

Ecco il riepilogo del modello utilizzando torchinfo.

Riepilogo del Modello LSTM

=================================================================Layer (type:depth-idx) Param #=================================================================WordPredictionLSTMModel - ├─Embedding: 1–1 3,379,200├─LSTM: 1–2 4,087,200├─Sequential: 1–3 - │ └─Linear: 2–1 2,474,328│ └─LayerNorm: 2–2 6,288│ └─LeakyReLU: 2–3 - │ └─Dropout: 2–4 - │ └─Linear: 2–5 20,757,000=================================================================Parametri totali: 30,704,016Parametri addestrabili: 30,704,016Parametri non addestrabili: 0=================================================================

Interpretazione dell’accuratezza: Dopo aver addestrato questo modello su 12M di frasi in lingua inglese per circa 8 ore su una GPU P100, abbiamo ottenuto una perdita del 4.03, un’accuratezza top-1 del 29% e un’accuratezza top-5 del 49%. Ciò significa che il 29% delle volte, il modello è stato in grado di predire correttamente il token successivo, e il 49% delle volte, il token successivo nel set di addestramento era una delle prime 5 previsioni del modello.

Qual dovrebbe essere la nostra metrica di successo? Sebbene i numeri di accuratezza top-1 e top-5 per il nostro modello non siano impressionanti, non sono così importanti per il nostro problema. Le parole candidate sono un piccolo insieme di possibili parole che si adattano al pattern di scorrimento. Quello che vogliamo dal nostro modello è essere in grado di selezionare un candidato ideale per completare la frase in modo che sia sintatticamente e semanticamente coerente. Dato che il nostro modello impara la natura del linguaggio attraverso i dati di addestramento, ci aspettiamo che assegni una probabilità più alta a frasi coerenti. Ad esempio, se abbiamo la frase “Il giocatore di baseball” e possibili completamenti (“corse”, “nuotò”, “si nascose”), allora la parola “corse” è una parola di continuazione migliore delle altre due. Quindi, se il nostro modello prevede la parola “corse” con una probabilità più alta rispetto alle altre, funziona per noi.

Interpretazione della perdita: Una perdita di 4.03 significa che il logaritmo negativo della probabilità di predire correttamente il prossimo token è 4.03, il che significa che la probabilità di predire correttamente il prossimo token è e^-4.03 = 0.0178 o 1/56. Un modello inizializzato casualmente ha tipicamente una perdita di circa 8.8, che è -log_e(1/6600), poiché il modello prevede casualmente 1/6600 token (6600 essendo la dimensione del vocabolario). Anche se una perdita di 4.03 potrebbe non sembrare eccezionale, è importante ricordare che il modello addestrato è circa 120 volte migliore di un modello non addestrato (o inizializzato casualmente).

In seguito, diamo un’occhiata a come possiamo utilizzare questo modello per migliorare le suggerimenti della nostra tastiera swipe.

Utilizzare il modello per eliminare i suggerimenti non validi

Diamo un’occhiata a un esempio reale. Supponiamo di avere una frase parziale “Penso”, e l’utente fa il pattern swipe mostrato in blu sotto, partendo da “o”, passando tra le lettere “c” e “v”, e finendo tra le lettere “e” e “v”.

Alcune possibili parole che potrebbero essere rappresentate da questo pattern swipe sono

  1. Oltre
  2. Oct (abbreviazione di ottobre)
  3. Ghiaccio
  4. Ho (con l’apostrofo implicito)

Di queste proposte, quella più probabile sarà probabilmente “Ho”. Alimentiamo queste proposte nel nostro modello e vediamo cosa ne esce.

[Penso] [Ho] = 0,00087[Penso] [Oltre] = 0,00051[Penso] [Ghiaccio] = 0,00001[Penso] [Oct] = 0,00000

Il valore dopo il segno = è la probabilità che la parola sia un completamento valido del prefisso della frase. In questo caso, vediamo che alla parola “Ho” è stata assegnata la probabilità più alta. Quindi, è la parola più probabile a seguire il prefisso della frase “Penso”.

La prossima domanda potrebbe essere come possiamo calcolare queste probabilità delle parole successive. Diamo un’occhiata.

Calcolare la probabilità della parola successiva

Per calcolare la probabilità che una parola sia un completamento valido di un prefisso di frase, eseguiamo il modello in modalità eval (inferenza) e inseriamo il prefisso di frase tokenizzato. Tokenizziamo anche la parola dopo aver aggiunto un prefisso di spazio bianco alla parola. Questo viene fatto perché il pre-tokenizzatore HuggingFace divide le parole con spazi all’inizio della parola, quindi vogliamo assicurarci che i nostri input siano coerenti con la strategia di tokenizzazione utilizzata da HuggingFace Tokenizers.

Supponiamo che la parola candidata sia composta da 3 token T0, T1 e T2.

  1. Prima eseguiamo il modello con il prefisso di frase tokenizzato originale. Per l’ultimo token, controlliamo la probabilità di predire il token T0. Aggiungiamo questo alla lista “probs”.
  2. Successivamente, eseguiamo una previsione sul prefisso + T0 e controlliamo la probabilità del token T1. Aggiungiamo questa probabilità alla lista “probs”.
  3. Successivamente, eseguiamo una previsione sul prefisso + T0 + T1 e controlliamo la probabilità del token T2. Aggiungiamo questa probabilità alla lista “probs”.

La lista “probs” contiene le probabilità individuali di generare i token T0, T1 e T2 in sequenza. Poiché questi token corrispondono alla tokenizzazione della parola candidata, possiamo moltiplicare queste probabilità per ottenere la probabilità combinata che il candidato sia un completamento del prefisso di frase.

Il codice per calcolare le probabilità di completamento è mostrato di seguito.

 def get_completion_probability(self, input, completion, tok):      self.model.eval()      ids = tok.encode(input).ids      ids = torch.tensor(ids, device=self.device).unsqueeze(0)      completion_ids = torch.tensor(tok.encode(completion).ids, device=self.device).unsqueeze(0)      probs = []      for i in range(completion_ids.size(1)):          y = self.model(ids)          y = y[0,:,-1].softmax(dim=0)          # prob is the probability of this completion.          prob = y[completion_ids[0,i]]          probs.append(prob)          ids = torch.cat([ids, completion_ids[:,i:i+1]], dim=1)      #      return torch.tensor(probs)  #

Vediamo qualche altro esempio qui sotto.

[Quel gelato sembra] [davvero] = 0,00709[Quel gelato sembra] [delizioso] = 0,00264[Quel gelato sembra] [assolutamente] = 0,00122[Quel gelato sembra] [vero] = 0,00031[Quel gelato sembra] [pesce] = 0,00004[Quel gelato sembra] [carta] = 0,00001[Quel gelato sembra] [atroce] = 0,00000[Dato che ci stiamo dirigendo] [verso] = 0,01052[Dato che ci stiamo dirigendo] [lontano] = 0,00344[Dato che ci stiamo dirigendo] [contro] = 0,00035[Dato che ci stiamo dirigendo] [entrambi] = 0,00009[Dato che ci stiamo dirigendo] [morte] = 0,00000[Dato che ci stiamo dirigendo] [bolla] = 0,00000[Dato che ci stiamo dirigendo] [nascita] = 0,00000[Ho fatto] [un] = 0,22704[Ho fatto] [il] = 0,06622[Ho fatto] [bene] = 0,00190[Ho fatto] [cibo] = 0,00020[Ho fatto] [colore] = 0,00007[Ho fatto] [casa] = 0,00006[Ho fatto] [colore] = 0,00002[Ho fatto] [matita] = 0,00001[Ho fatto] [fiore] = 0,00000[Vogliamo un candidato] [con] = 0,03209[Vogliamo un candidato] [che] = 0,02145[Vogliamo un candidato] [esperienza] = 0,00097[Vogliamo un candidato] [che] = 0,00094[Vogliamo un candidato] [più] = 0,00010[Vogliamo un candidato] [meno] = 0,00007[Vogliamo un candidato] [scuola] = 0,00003[Questa è la guida definitiva per] [il] = 0,00089[Questa è la guida definitiva per] [completo] = 0,00047[Questa è la guida definitiva per] [frase] = 0,00006[Questa è la guida definitiva per] [rapper] = 0,00001[Questa è la guida definitiva per] [illustrato] = 0,00001[Questa è la guida definitiva per] [stravagante] = 0,00000[Questa è la guida definitiva per] [wrapper] = 0,00000[Questa è la guida definitiva per] [minuscolo] = 0,00000[Per favore puoi] [controllare] = 0,00502[Per favore puoi] [confermare] = 0,00488[Per favore puoi] [cessare] = 0,00002[Per favore puoi] [cullare] = 0,00000[Per favore puoi] [laptop] = 0,00000[Per favore puoi] [busta] = 0,00000[Per favore puoi] [opzioni] = 0,00000[Per favore puoi] [cordone] = 0,00000[Per favore puoi] [corolla] = 0,00000[Penso] [Ho] = 0,00087[Penso] [Oltre] = 0,00051[Penso] [Ghiaccio] = 0,00001[Penso] [Oct] = 0,00000[Per favore] [puoi] = 0,00428[Per favore] [cab] = 0,00000[Ho programmato questa] [riunione] = 0,00077[Ho programmato questa] [confusione] = 0,00000

Questi esempi mostrano la probabilità della parola che completa la frase prima di essa. I candidati sono ordinati in ordine decrescente di probabilità.

Dal momento che i modelli Transformer stanno gradualmente sostituendo i modelli LSTM e RNN per compiti basati su sequenze, diamo un’occhiata a come sarebbe un modello Transformer per lo stesso obiettivo.

Un modello Transformer

I modelli basati su Transformer sono una popolare architettura per addestrare modelli di linguaggio a prevedere la parola successiva in una frase. La tecnica specifica che useremo è il meccanismo di attenzione causale. Addestreremo solo il livello di codificatore del Transformer in PyTorch utilizzando l’attenzione causale. L’attenzione causale significa che consentiremo a ogni token nella sequenza di guardare solo i token che lo precedono. Questo assomiglia alle informazioni che uno strato LSTM unidirezionale utilizza quando addestrato solo nella direzione in avanti.

Il modello Transformer che vedremo qui si basa direttamente su nn.TransformerEncoder e nn.TransformerEncoderLayer in PyTorch.

import mathdef generate_src_mask(sz, device):    return torch.triu(torch.full((sz, sz), True, device=device), diagonal=1)#class PositionalEmbedding(nn.Module):    def __init__(self, sequence_length, embed_dim):        super().__init__()        self.sqrt_embed_dim = math.sqrt(embed_dim)        self.pos_embed = nn.Parameter(torch.empty((1, sequence_length, embed_dim)))        nn.init.uniform_(self.pos_embed, -1.0, 1.0)    #        def forward(self, x):        return x * self.sqrt_embed_dim + self.pos_embed[:,:x.size(1)]    ##class WordPredictionTransformerModel(nn.Module):    def __init__(self, sequence_length, num_embed, embed_dim, pad_idx, num_heads, num_layers, output_dim, dropout, norm_first, activation):        super().__init__()        self.vocab_size = num_embed        self.sequence_length = sequence_length        self.embed_dim = embed_dim        self.sqrt_embed_dim = math.sqrt(embed_dim)        self.embed = nn.Sequential(            nn.Embedding(num_embed, embed_dim, pad_idx),            PositionalEmbedding(sequence_length, embed_dim),            nn.LayerNorm(embed_dim),            nn.Dropout(p=0.1),        )        encoder_layer = nn.TransformerEncoderLayer(            d_model=embed_dim, nhead=num_heads, dropout=dropout, batch_first=True, norm_first=norm_first, activation=activation,        )        self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)        self.fc = nn.Sequential(            nn.Linear(embed_dim, embed_dim * 4),            nn.LayerNorm(embed_dim * 4),            nn.LeakyReLU(),            nn.Dropout(p=dropout),            nn.Linear(embed_dim * 4, output_dim),        )    #        def forward(self, x):        src_attention_mask = generate_src_mask(x.size(1), x.device)        x = self.embed(x)        x = self.encoder(x, is_causal=True, mask=src_attention_mask)        x = self.fc(x)        x = x.permute(0, 2, 1)        return x    ##

Possiamo inserire questo modello al posto del modello LSTM che abbiamo usato prima, poiché la sua API è compatibile. Questo modello impiega più tempo per l’addestramento per la stessa quantità di dati di addestramento e ha prestazioni comparabili.

I modelli Transformer sono migliori per sequenze lunghe. Nel nostro caso, abbiamo sequenze di lunghezza 256. La maggior parte del contesto necessario per completare la parola successiva tende a essere locale, quindi non abbiamo realmente bisogno della potenza dei Transformer qui.

Conclusione

Abbiamo visto come possiamo risolvere problemi NLP molto pratici utilizzando tecniche di deep learning basate su modelli LSTM (RNN) e Transformer. Non ogni compito di linguaggio richiede l’uso di modelli con miliardi di parametri. Applicazioni specializzate che richiedono la modellazione del linguaggio stesso, e non la memorizzazione di grandi volumi di informazioni, possono essere gestite utilizzando modelli molto più piccoli che possono essere implementati facilmente e in modo più efficiente rispetto ai massicci modelli di linguaggio che siamo abituati a vedere in questi giorni.

Tutte le immagini tranne la prima sono state create dagli autori.