IA generativa strutturata.  Come vincolare il modello all'output… |  di Oren Matar |  Aprile 2024

 | Intelligenza-Artificiale

Come vincolare il modello all'output di formati definiti

In questo post spiegherò e dimostrerò il concetto di “AI generativa strutturata”: AI generativa vincolata a formati definiti. Alla fine del post capirai dove e quando può essere utilizzato e come implementarlo, sia che tu stia creando un modello di trasformatore da zero o utilizzando i modelli di Hugging Face. Inoltre, tratteremo un suggerimento importante per la tokenizzazione che è particolarmente rilevante per i linguaggi strutturati.

Uno dei tanti usi dell’intelligenza artificiale generativa è come strumento di traduzione. Ciò spesso comporta la traduzione tra due lingue umane, ma può includere anche linguaggi o formati informatici. Ad esempio, la tua applicazione potrebbe dover tradurre il linguaggio naturale (umano) in SQL:

Natural language: “Get customer names and emails of customers from the US”

SQL: "SELECT name, email FROM customers WHERE country = 'USA'"

Oppure per convertire i dati di testo in un formato JSON:

Natural language: “I am John Doe, phone number is 555–123–4567,
my friends are Anna and Sara”

JSON: {name: "John Doe",
phone_number: "555–123–5678",
friends: {
name: (("Anna", "Sara"))}
}

Naturalmente sono possibili molte più applicazioni per altri linguaggi strutturati. Il processo di formazione per tali compiti prevede l'alimentazione di esempi di linguaggio naturale insieme a formati strutturati in un modello di codificatore-decodificatore. In alternativa, può essere sufficiente sfruttare un modello linguistico pre-addestrato (LLM).

Anche se raggiungere una precisione del 100% è irraggiungibile, esiste una classe di errori che possiamo eliminare: gli errori di sintassi. Si tratta di violazioni del formato del linguaggio, come la sostituzione di virgole con punti, l'utilizzo di nomi di tabelle non presenti nello schema SQL o l'omissione di chiusure tra parentesi, che rendono SQL o JSON non eseguibili.

Il fatto che stiamo traducendo in un linguaggio strutturato significa che l'elenco dei token legittimi in ogni fase di generazione è limitato e predeterminato. Se potessimo inserire questa conoscenza nel processo di intelligenza artificiale generativa potremmo evitare un’ampia gamma di risultati errati. Questa è l’idea alla base dell’IA generativa strutturata: vincolarla a un elenco di token legittimi.

Un rapido promemoria su come vengono generati i token

Sia che si utilizzi un codificatore-decodificatore o un'architettura GPT, la generazione dei token funziona in modo sequenziale. La selezione di ogni token si basa sia sull'input che sui token precedentemente generati, continuando fino a quando a viene generato il token, a indicare il completamento della sequenza. Ad ogni passaggio, un classificatore assegna valori logit a tutti i token nel vocabolario, rappresentando la probabilità di ciascun token come selezione successiva. Il token successivo viene campionato in base a tali logit.

Il classificatore del decodificatore assegna un logit ad ogni token del vocabolario (Immagine dell'autore)

Limitazione della generazione di token

Per limitare la generazione di token, incorporiamo la conoscenza della struttura del linguaggio di output. I token illegittimi hanno i loro logit impostati su -inf, garantendo la loro esclusione dalla selezione. Ad esempio, se solo una virgola o “FROM” è valida dopo “Seleziona nome”, tutti gli altri logit dei token vengono impostati su -inf.

Se stai utilizzando Hugging Face, questo può essere implementato utilizzando un “processore logit”. Per utilizzarlo è necessario implementare una classe con un metodo __call__, che verrà chiamato dopo il calcolo dei logit, ma prima del campionamento. Questo metodo riceve tutti i log dei token e gli ID di input generati, restituendo i log modificati per tutti i token.

I logit restituiti dal processore logits: tutti i token illegittimi ottengono un valore di -inf (Immagine dell'autore)

Dimostrerò il codice con un esempio semplificato. Per prima cosa inizializziamo il modello, in questo caso useremo Bart, ma può funzionare con qualsiasi modello.

from transformers import BartForConditionalGeneration, BartTokenizerFast, PreTrainedTokenizer
from transformers.generation.logits_process import LogitsProcessorList, LogitsProcessor
import torch

name = 'facebook/bart-large'
tokenizer = BartTokenizerFast.from_pretrained(name, add_prefix_space=True)
pretrained_model = BartForConditionalGeneration.from_pretrained(name)

Se vogliamo generare una traduzione dal linguaggio naturale a SQL, possiamo eseguire:

to_translate = 'customers emails from the us'
words = to_translate.split()
tokenized_text = tokenizer((words), is_split_into_words=True)

out = pretrained_model.generate(
torch.tensor(tokenized_text("input_ids")),
max_new_tokens=20,
)
print(tokenizer.convert_tokens_to_string(
tokenizer.convert_ids_to_tokens(
out(0), skip_special_tokens=True)))

Ritorno

'More emails from the us'

Poiché non abbiamo ottimizzato il modello per le attività da testo a SQL, l'output non assomiglia a SQL. Non addestreremo il modello in questo tutorial, ma lo guideremo a generare una query SQL. Raggiungeremo questo obiettivo utilizzando una funzione che mappa ciascun token generato su un elenco di token successivi consentiti. Per semplicità, ci concentreremo solo sul token immediatamente precedente, ma meccanismi più complicati sono facili da implementare. Utilizzeremo un dizionario che definisce per ciascun token quali token possono seguirlo. Ad esempio, la query deve iniziare con “SELECT” o “DELETE”, e dopo “SELECT” sono consentiti solo “name”, “email” o “id” poiché queste sono le colonne nel nostro schema.

rules = {'<s>': ('SELECT', 'DELETE'), # beginning of the generation
'SELECT': ('name', 'email', 'id'), # names of columns in our schema
'DELETE': ('name', 'email', 'id'),
'name': (',', 'FROM'),
'email': (',', 'FROM'),
'id': (',', 'FROM'),
',': ('name', 'email', 'id'),
'FROM': ('customers', 'vendors'), # names of tables in our schema
'customers': ('</s>'),
'vendors': ('</s>'), # end of the generation
}

Ora dobbiamo convertire questi token negli ID utilizzati dal modello. Ciò avverrà all'interno di una classe che eredita da LogitsProcessor.

def convert_token_to_id(token):
return tokenizer(token, add_special_tokens=False)('input_ids')(0)

class SQLLogitsProcessor(LogitsProcessor):
def __init__(self, tokenizer: PreTrainedTokenizer):
self.tokenizer = tokenizer
self.rules = {convert_token_to_id(k): (convert_token_to_id(v0) for v0 in v) for k,v in rules.items()}

Infine, implementeremo la funzione __call__, che viene chiamata dopo il calcolo dei logit. La funzione crea un nuovo tensore di -infs, controlla quali ID sono legittimi secondo le regole (il dizionario) e inserisce i loro punteggi nel nuovo tensore. Il risultato è un tensore che ha solo valori validi per i token validi.

class SQLLogitsProcessor(LogitsProcessor):
def __init__(self, tokenizer: PreTrainedTokenizer):
self.tokenizer = tokenizer
self.rules = {convert_token_to_id(k): (convert_token_to_id(v0) for v0 in v) for k,v in rules.items()}

def __call__(self, input_ids: torch.LongTensor, scores: torch.LongTensor):
if not (input_ids == self.tokenizer.bos_token_id).any():
# we must allow the start token to appear before we start processing
return scores
# create a new tensor of -inf
new_scores = torch.full((1, self.tokenizer.vocab_size), float('-inf'))
# ids of legitimate tokens
legit_ids = self.rules(int(input_ids(0, -1)))
# place their values in the new tensor
new_scores(:, legit_ids) = scores(0, legit_ids)
return new_scores

E questo è tutto! Ora possiamo eseguire una generazione con il processore logit:

to_translate = 'customers emails from the us'
words = to_translate.split()
tokenized_text = tokenizer((words), is_split_into_words=True, return_offsets_mapping=True)

logits_processor = LogitsProcessorList((SQLLogitsProcessor(tokenizer)))

out = pretrained_model.generate(
torch.tensor(tokenized_text("input_ids")),
max_new_tokens=20,
logits_processor=logits_processor
)
print(tokenizer.convert_tokens_to_string(
tokenizer.convert_ids_to_tokens(
out(0), skip_special_tokens=True)))

Ritorno

 SELECT email , email , id , email FROM customers

Il risultato è un po’ strano, ma ricorda: non abbiamo nemmeno addestrato il modello! Abbiamo applicato la generazione di token solo in base a regole specifiche. In particolare, limitare la generazione non interferisce con la formazione; i vincoli si applicano solo durante la generazione post-addestramento. Pertanto, se opportunamente implementati, questi vincoli possono solo migliorare la precisione della generazione.

La nostra implementazione semplicistica non riesce a coprire tutta la sintassi SQL. Un'implementazione reale deve supportare più sintassi, considerando potenzialmente non solo l'ultimo token ma diversi, e abilitare la generazione batch. Una volta implementati questi miglioramenti, il nostro modello addestrato può generare in modo affidabile query SQL eseguibili, vincolate a nomi di tabelle e colonne validi dallo schema. Un approccio simile può imporre vincoli nella generazione di JSON, garantendo la presenza della chiave e la chiusura delle parentesi.

Fai attenzione alla tokenizzazione

La tokenizzazione viene spesso trascurata, ma la corretta tokenizzazione è fondamentale quando si utilizza l'intelligenza artificiale generativa per output strutturati. Tuttavia, dietro le quinte, la tokenizzazione può avere un impatto sull'addestramento del tuo modello. Ad esempio, puoi ottimizzare un modello per tradurre il testo in un JSON. Come parte del processo di perfezionamento, fornisci al modello esempi di coppie testo-JSON, che tokenizza. Come sarà questa tokenizzazione?

(Immagine dell'autore)

Mentre leggi “((” come due parentesi quadre, il tokenizzatore le converte in un singolo ID, che verrà trattato come una classe completamente distinta dalla singola parentesi dal classificatore di token. Ciò rende l'intera logica che il il modello deve imparare – più complicato (ad esempio, ricordare quante parentesi chiudere). Allo stesso modo, aggiungere uno spazio prima delle parole può modificare la loro tokenizzazione e il loro ID di classe.

(Immagine dell'autore)

Ancora una volta, ciò complica la logica che il modello dovrà apprendere poiché i pesi collegati a ciascuno di questi ID dovranno essere appresi separatamente, per casi leggermente diversi.

Per un apprendimento più semplice, assicurati che ogni concetto e punteggiatura vengano convertiti in modo coerente nello stesso token, aggiungendo spazi prima di parole e caratteri.

Le parole distanziate portano a una tokenizzazione più coerente (Immagine dell'autore)

L'inserimento di esempi distanziati durante la messa a punto semplifica i modelli che il modello deve apprendere, migliorandone la precisione. Durante la previsione, il modello genererà il JSON con spazi, che potrai quindi rimuovere prima dell'analisi.

Riepilogo

L’intelligenza artificiale generativa offre un approccio prezioso per la traduzione in un linguaggio formattato. Sfruttando la conoscenza della struttura di output, possiamo vincolare il processo generativo, eliminando una classe di errori e garantendo l'eseguibilità delle query e la capacità di analisi delle strutture dati.

Inoltre, questi formati possono utilizzare punteggiatura e parole chiave per indicare determinati significati. Assicurarsi che la tokenizzazione di queste parole chiave sia coerente può ridurre drasticamente la complessità dei modelli che il modello deve apprendere, riducendo così la dimensione richiesta del modello e il suo tempo di addestramento, aumentandone al contempo l'accuratezza.

L’intelligenza artificiale generativa strutturata può tradurre efficacemente il linguaggio naturale in qualsiasi formato strutturato. Queste traduzioni consentono l'estrazione di informazioni dal testo o la generazione di query, che costituisce un potente strumento per numerose applicazioni.

Fonte: towardsdatascience.com

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *