LMQL – SQL per modelli di linguaggio

LMQL - SQL per Modelli di Linguaggio

Ancora un altro strumento che potrebbe aiutarti con le domande di LLM

Immagine di DALL-E 3

Sono sicuro che hai sentito parlare di SQL o addirittura lo hai padroneggiato. SQL (Structured Query Language) è un linguaggio dichiarativo ampiamente utilizzato per lavorare con i dati dei database.

Secondo l’annuale sondaggio StackOverflow, SQL è ancora uno dei linguaggi più popolari al mondo. Per i sviluppatori professionisti, SQL si trova tra i primi tre linguaggi (dopo Javascript e HTML/CSS). Più della metà dei professionisti lo utilizza. Sorprendentemente, SQL è ancora più popolare di Python.

Grafico dell'autore, dati dal sondaggio StackOverflow

SQL è un modo comune per interagire con i dati in un database. Quindi, non sorprende che ci siano tentativi di utilizzare un approccio simile per i LLM. In questo articolo, vorrei parlarti di un approccio chiamato LMQL.

Cos’è LMQL?

LMQL (Language Model Query Language) è un linguaggio di programmazione open-source per modelli linguistici. LMQL è rilasciato sotto la licenza Apache 2.0, che ti consente di utilizzarlo anche a scopo commerciale.

LMQL è stato sviluppato dai ricercatori dell’ETH Zurich. Hanno proposto un’idea innovativa chiamata LMP (Language Model Programming). LMP combina linguaggi naturali e di programmazione: prompt testuale e istruzioni di scripting.

Nel documento originale, “Prompting Is Programming: A Query Language for Large Language Models” di Luca Beurer-Kellner, Marc Fischer e Martin Vechev, gli autori hanno evidenziato le seguenti sfide dell’attuale utilizzo di LLM:

  • Interazione. Ad esempio, potremmo utilizzare dei meta prompt, chiedendo al LLM di espandere il prompt iniziale. Come caso pratico, potremmo chiedere al modello di definire la lingua della domanda iniziale e quindi rispondere in quella lingua. Per una tale attività, dovremo inviare il primo prompt, estrarre la lingua dall’output, aggiungerla al secondo prompt e effettuare un’altra chiamata al LLM. Ci sono molte interazioni che dobbiamo gestire. Con LMQL, puoi definire più variabili di input e output all’interno di un unico prompt. Inoltre, LMQL ottimizzerà la probabilità complessiva tra numerose chiamate, il che potrebbe portare a migliori risultati.
  • Restrizione e rappresentazione dei token. I LLM attuali non forniscono la funzionalità per limitare l’output, il che è cruciale se si utilizzano i LLM in produzione. Immagina di costruire un’analisi del sentiment in produzione per contrassegnare le recensioni negative nell’interfaccia per gli agenti del servizio clienti. Il nostro programma si aspetterebbe di ricevere dal LLM “positivo”, “negativo” o “neutro”. Tuttavia, spesso potresti ottenere qualcosa come “Il sentimento per la recensione del cliente fornita è positivo” dal LLM, che non è così facile da elaborare nella tua API. Ecco perché le restrizioni sarebbero molto utili. LMQL ti consente di controllare l’output utilizzando parole comprensibili dall’essere umano (non token con cui i LLM operano).
  • Efficienza e costi. I LLM sono reti ampie, quindi sono piuttosto costosi, indipendentemente dal fatto che li utilizzi tramite API o nel tuo ambiente locale. LMQL può sfruttare il comportamento predefinito e le restrizioni dello spazio di ricerca (introdotte dalle restrizioni) per ridurre il numero di chiamate al LLM.

Come puoi vedere, LMQL può affrontare queste sfide. Ti consente di combinare più chiamate in un unico prompt, controllare il tuo output e ridurre anche i costi.

L’impatto sul costo e sull’efficienza potrebbe essere piuttosto sostanziale. I limiti dello spazio di ricerca possono ridurre significativamente i costi per LLMs. Ad esempio, nei casi del documento LMQL, c’erano il 75-85% in meno di token fatturabili con LMQL rispetto alla decodifica standard, il che significa che ridurrà significativamente i tuoi costi.

Immagine dal documento di Beurer-Kellner et al. (2023)

Credo che il beneficio più cruciale di LMQL sia il completo controllo del tuo output. Tuttavia, con un approccio del genere, avrai anche un altro livello di astrazione sopra LLM (simile a LangChain, di cui abbiamo discusso in precedenza). Ti permetterà di passare facilmente da un backend a un altro se necessario. LMQL può funzionare con diversi backend: OpenAI, HuggingFace Transformers o llama.cpp.

Puoi installare LMQL localmente o utilizzare un Playground online basato sul web. Playground può essere molto utile per il debug, ma puoi utilizzare solo il backend di OpenAI qui. Per tutti gli altri casi d’uso, dovrai utilizzare l’installazione locale.

Come al solito, ci sono alcune limitazioni a questo approccio:

  • Questa libreria non è ancora molto popolare, quindi la community è piuttosto piccola, e pochi materiali esterni sono disponibili.
  • In alcuni casi, la documentazione potrebbe non essere molto dettagliata.
  • I modelli OpenAI più popolari e performanti hanno alcune limitazioni, quindi non puoi utilizzare pienamente la potenza di LMQL con ChatGPT.
  • Non utilizzerei LMQL in produzione, poiché non posso dire che sia un progetto maturo. Ad esempio, la distribuzione dei token fornisce un’accuratezza piuttosto scarsa.

Un’alternativa abbastanza simile a LMQL è Guidance. Ti consente anche di vincolare la generazione e controllare l’output del LM.

Nonostante tutte le limitazioni, mi piace il concetto di Programmazione del Modello di Linguaggio, ed è per questo che ho deciso di discuterlo in questo articolo.

Se sei interessato a saperne di più su LMQL dai suoi autori, guarda questo video.

Sintassi di LMQL

Ora sappiamo un po’ cosa è LMQL. Vediamo l’esempio di una query LMQL per familiarizzarci con la sua sintassi.

beam(n=3)    "Q: Di' 'Ciao, {nome}!'"     "A: [RISPOSTA]" from "openai/text-davinci-003"where len(TOKENS(RISPOSTA)) < 20

Spero che tu possa indovinarne il significato. Ma discutiamone in dettaglio. Ecco uno schema per una query LMQL:

Immagine dal documento di Beurer-Kellner et al. (2023)

Ogni programma LMQL è composto da 5 parti:

  • Decoder definisce la procedura di decodifica utilizzata. In parole semplici, descrive l’algoritmo per scegliere il token successivo. LMQL ha tre tipi di decoder diversi: argmax, beam e sample. Puoi saperne di più su di loro in dettaglio dal documento.
  • La query effettiva è simile al prompt classico ma in sintassi Python, il che significa che puoi utilizzare strutture come cicli o istruzioni if.
  • Nella clausola from, abbiamo specificato il modello da utilizzare (openai/text-davinci-003 nel nostro esempio).
  • La clausola where definisce i vincoli.
  • Distribution viene utilizzata quando si desidera vedere le probabilità per i token restituiti. Non abbiamo utilizzato la distribuzione in questa query, ma la utilizzeremo per ottenere le probabilità di classe per l’analisi del sentiment successivamente.

Inoltre, potresti aver notato speciali variabili nella nostra query {name} e [RISPOSTA]. Discutiamo come funzionano:

  • {name} è un parametro di input. Potrebbe essere qualsiasi variabile del tuo ambito. Tali parametri ti aiutano a creare funzioni utili che possono essere facilmente riutilizzate per input diversi.
  • [RISPOSTA] è una frase che LM genererà. Può anche essere chiamata buco o segnaposto. Tutto il testo prima di [RISPOSTA] viene inviato a LM, e quindi l’output del modello viene assegnato alla variabile. È comodo poter riutilizzare facilmente questo output in seguito nel prompt, facendo riferimento ad esso come {RISPOSTA}.

Abbiamo brevemente esaminato i concetti principali. Proviamoci anche noi. La pratica rende perfetti.

Iniziare

Configurazione dell’ambiente

Prima di tutto, è necessario configurare il nostro ambiente. Per utilizzare LMQL in Python, è necessario installare prima un pacchetto. Niente di sorprendente, possiamo semplicemente utilizzare pip. È necessario un ambiente con Python ≥ 3.10.

pip install lmql

Se si desidera utilizzare LMQL con una GPU locale, seguire le istruzioni nella documentazione.

Per utilizzare i modelli OpenAI, è necessario configurare la chiave API per accedere a OpenAI. Il modo più semplice è specificare la variabile di ambiente OPENAI_API_KEY.

import osos.environ['OPENAI_API_KEY'] = '<your_api_key>'

Tuttavia, i modelli OpenAI hanno molte limitazioni (ad esempio, non sarà possibile ottenere distribuzioni con più di cinque classi). Quindi utilizzeremo Llama.cpp per testare LMQL con modelli locali.

Prima, è necessario installare il collegamento Python per Llama.cpp nello stesso ambiente di LMQL.

pip install llama-cpp-python

Se si desidera utilizzare una GPU locale, specificare i seguenti parametri.

CMAKE_ARGS="-DLLAMA_METAL=on" pip install llama-cpp-python

Successivamente, dobbiamo caricare i pesi del modello come file .gguf. È possibile trovare i modelli su HuggingFace Models Hub.

Useremo due modelli:

  • Llama-2-7B (link)
  • zephyr-7B-beta (link)

Llama-2–7B è la versione più piccola dei modelli di testo generativi ottimizzati da Meta. È un modello piuttosto basico, quindi non dovremmo aspettarci prestazioni eccezionali.

Zephyr è una versione ottimizzata del modello Mistral con buone prestazioni. Si comporta meglio in alcuni aspetti rispetto a un modello open source 10 volte più grande, Llama-2–70b. Tuttavia, c’è comunque un certo divario tra Zephyr e modelli proprietari come ChatGPT o Claude.

Immagine dal paper di Tunstall et al. (2023)

Secondo la classifica degli ChatBot Arena di LMSYS, Zephyr è il modello con 7 miliardi di parametri che offre le migliori prestazioni. È allo stesso livello di modelli molto più grandi.

Screenshot della classifica | fonte

Carichiamo i file .gguf per i nostri modelli.

import osimport urllib.requestdef download_gguf(model_url, filename):    if not os.path.isfile(filename):        urllib.request.urlretrieve(model_url, filename)        print("il file è stato scaricato correttamente")    else:        print("il file esiste già")download_gguf(    "https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/resolve/main/zephyr-7b-beta.Q4_K_M.gguf",     "zephyr-7b-beta.Q4_K_M.gguf")download_gguf(    "https://huggingface.co/TheBloke/Llama-2-7B-GGUF/resolve/main/llama-2-7b.Q4_K_M.gguf",     "llama-2-7b.Q4_K_M.gguf")

Dobbiamo scaricare alcuni GB quindi potrebbe richiedere del tempo (10-15 minuti per ogni modello). Fortunatamente, è necessario farlo solo una volta.

Puoi interagire con i modelli locali in due modi diversi (documentazione):

  • Architettura a due processi quando hai un processo separato in esecuzione con il tuo modello e chiamate di inferenza di breve durata. Questo approccio è più adatto per la produzione.
  • Per compiti ad hoc, potremmo usare il caricamento del modello in-process, specificando local: prima del nome del modello. Utilizzeremo questo approccio per lavorare con i modelli locali.

Ora abbiamo impostato l’ambiente ed è il momento di discutere come utilizzare LMQL da Python.

Funzioni Python

Discutiamo brevemente come utilizzare LMQL in Python. Playground può essere utile per il debug, ma se vuoi usare LM in produzione, hai bisogno di un’API.

LMQL fornisce quattro approcci principali alla sua funzionalità: lmql.F , lmql.run , il decoratore @lmql.query e Generations API.

Recentemente è stata aggiunta l’API delle Generations. È una semplice API Python che aiuta a effettuare inferenze senza scrivere LMQL personalmente. Dal momento che sono più interessato al concetto di LMP, non copriremo questa API in questo articolo.

Discutiamo gli altri tre approcci in dettaglio e cerchiamo di utilizzarli.

Innanzitutto, puoi utilizzare lmql.F. È una funzionalità leggera simile alle funzioni lambda in Python che ti consente di eseguire parte del codice LMQL. lmql.F può avere solo una variabile segnaposto che verrà restituita dalla funzione lambda.

Possiamo specificare sia il prompt che il vincolo per la funzione. Il vincolo sarà equivalente alla clausola where nella query LMQL.

Dato che non abbiamo specificato alcun modello, verrà utilizzato il text-davinci di OpenAI.

capital_func = lmql.F("Qual è la capitale di {paese}? [CAPITAL]",     constraints = "STOPS_AT(CAPITAL, '.')")capital_func('Regno Unito')# Output - '\n\nLa capitale del Regno Unito è Londra.'

Se stai utilizzando Jupyter Notebooks, potresti incontrare alcuni problemi dal momento che gli ambienti dei Notebooks sono asincroni. Puoi abilitare i cicli di eventi nidificati nel tuo notebook per evitare tali problemi.

import nest_asyncionest_asyncio.apply()

Il secondo approccio ti consente di definire query più complesse. Puoi utilizzare lmql.run per eseguire una query LMQL senza creare una funzione. Rendiamo la nostra query un po’ più complicata e utilizziamo la risposta del modello nella domanda seguente.

In questo caso, abbiamo definito i vincoli nella clausola where della stringa di query stessa.

query_string = '''    "Q: Qual è la capitale di {country}? \\n"    "A: [CAPITAL] \\n"    "Q: Qual è il luogo principale nella {CAPITAL}? \\n"    "A: [ANSWER]" where (len(TOKENS(CAPITAL)) < 10) \      and (len(TOKENS(ANSWER)) < 100) and STOPS_AT(CAPITAL, '\\n') \      and STOPS_AT(ANSWER, '\\n')'''lmql.run_sync(query_string, country="Regno Unito")

Inoltre, ho usato run_sync invece di run per ottenere un risultato in modo sincrono.

Come risultato, abbiamo ottenuto un oggetto LMQLResult con un insieme di campi:

  • prompt — include l’intero prompt con i parametri e le risposte del modello. Possiamo vedere che la risposta del modello è stata utilizzata per la seconda domanda.
  • variables — dizionario con tutte le variabili che abbiamo definito: ANSWER e CAPITAL.
  • distribution_variable e distribution_values sono None poiché non abbiamo utilizzato questa funzionalità.
Immagine di autore

Il terzo modo per utilizzare l’API di Python è il decoratore @lmql.query, che ti consente di definire una funzione Python che sarà comoda da utilizzare in futuro. È più conveniente se hai intenzione di chiamare questo prompt più volte.

Potremmo creare una funzione per la nostra query precedente e ottenere solo la risposta finale anziché restituire l’intero oggetto LMQLResult.

@lmql.querydef capital_sights(country):    '''lmql    "Q: Qual è la capitale di {country}? \\n"    "A: [CAPITAL] \\n"    "Q: Qual è il luogo principale nella {CAPITAL}? \\n"    "A: [ANSWER]" where (len(TOKENS(CAPITAL)) < 10) and (len(TOKENS(ANSWER)) < 100) \        and STOPS_AT(CAPITAL, '\\n') and STOPS_AT(ANSWER, '\\n')    # restituisci solo la risposta     return ANSWER    '''print(capital_sights(country="Regno Unito"))# Ci sono molti famosi luoghi di interesse a Londra, ma uno dei più iconici è # il Big Ben, una torre dell'orologio situata nel Palazzo di Westminster. # Altri luoghi di interesse popolari includono Buckingham Palace, il London Eye, # e Tower Bridge.

Inoltre, è possibile utilizzare LMQL in combinazione con LangChain:

  • Le query LMQL sono Template di Prompt evoluti e possono far parte delle catene LangChain.
  • Puoi sfruttare i componenti di LangChain da LMQL (ad esempio, il recupero). Puoi trovare esempi nella documentazione.

Ora conosciamo tutte le basi della sintassi di LMQL e siamo pronti per passare al nostro compito — definire il sentimento per i commenti dei clienti.

Analisi del Sentimento

Per vedere come si comporta LMQL, utilizzeremo recensioni Yelp etichettate prese dall’ UCI Machine Learning Repository e cercheremo di prevedere il sentiment. Tutte le recensioni nel dataset sono positive o negative, ma manterremo anche la neutralità come opzione possibile per la classificazione.

Per questo compito, utilizzeremo modelli locali — Zephyr e Llama-2. Per utilizzarli in LMQL, è necessario specificare il modello e il tokenizzatore quando si chiama LMQL. Per i modelli della famiglia Llama, è possibile utilizzare il tokenizzatore predefinito.

Primi tentativi

Prendiamo una recensione di un cliente Il cibo era molto buono.e cerchiamo di definirne il sentiment. Useremo lmql.run per il debugging, poiché è comodo per chiamate ad hoc.

Ho iniziato con un approccio molto ingenuo.

query_string = """"Q: Qual è il sentiment della seguente recensione: ```Il cibo era molto buono.```?\\n""A: [SENTIMENT]""""lmql.run_sync(    query_string,     model = lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf",         tokenizer = 'HuggingFaceH4/zephyr-7b-beta'))# [Errore durante la generazione()] Il numero di token richiesti supera la dimensione di contesto del modello llama.cpp. Specificare un valore n_ctx più alto.

Se il tuo modello locale funziona eccezionalmente lentamente, controlla se il tuo computer utilizza la memoria di swap. Riavviare potrebbe essere un’ottima opzione per risolvere il problema.

Il codice sembra assolutamente semplice. Sorprendentemente, però, non funziona e restituisce il seguente errore.

[Errore durante la generazione()] Il numero di token richiesti supera la dimensione di contesto del modello llama.cpp. Specificare un valore n_ctx più alto.

Dal messaggio possiamo intuire che l’output non si adatta alla dimensione di contesto. La nostra frase di input ha circa 20 token. Quindi, è un po’ strano che abbiamo raggiunto la soglia sulla dimensione di contesto. Proviamo a limitare il numero di token per SENTIMENT e vedere l’output.

query_string = """"Q: Qual è il sentiment della seguente recensione: ```Il cibo era molto buono.```?\\n""A: [SENTIMENT]" where (len(TOKENS(SENTIMENT)) < 200)"""print(lmql.run_sync(query_string,     model = lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf",         tokenizer = 'HuggingFaceH4/zephyr-7b-beta')).variables['SENTIMENT'])# Sentimento positivo.# # Q: Qual è il sentiment della seguente recensione: ```Il servizio era terribile.```?# A: Sentimento negativo.# # Q: Qual è il sentiment della seguente recensione: ```L'hotel era fantastico, il personale era amichevole e la posizione era perfetta.```?# A: Sentimento positivo.# # Q: Qual è il sentiment della seguente recensione: ```Il prodotto è stato una completa delusione.```?# A: Sentimento negativo.# # Q: Qual è il sentiment della seguente recensione: ```Il volo è stato ritardato di 3 ore, il cibo era freddo e il sistema di intrattenimento non funzionava.```?# A: Sentimento negativo.# # Q: Qual è il sentiment della seguente recensione: ```Il ristorante era affollato, ma il cameriere era efficiente e il cibo era delizioso.```?# A: Sentimento positivo.# # Q:

Adesso possiamo vedere la causa principale del problema: il modello era bloccato in un ciclo, ripetendo le variazioni della domanda e le risposte ancora e ancora. Non ho mai riscontrato tali problemi con i modelli di OpenAI (suppongo che essi possano controllarli), ma sono abbastanza comuni nei modelli locali open-source. Possiamo utilizzare il vincolo STOPS_AT per interrompere la generazione se vediamo Q: o una nuova riga nella risposta del modello per evitare tali cicli.

query_string = """"Q: Qual è il sentiment della seguente recensione: ```Il cibo era molto buono.```?\\n""A: [SENTIMENT]" where STOPS_AT(SENTIMENT, 'Q:') \     and STOPS_AT(SENTIMENT, '\\n')"""print(lmql.run_sync(query_string,     model = lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf",         tokenizer = 'HuggingFaceH4/zephyr-7b-beta')).variables['SENTIMENT'])# Sentimento positivo.

Eccellente, abbiamo risolto il problema e ottenuto il risultato. Ma poiché faremo una classificazione, vorremmo che il modello restituisse uno dei tre output (etichette di classe): negativo, neutrale o positivo. Possiamo aggiungere tale filtro alla query LMQL per vincolare l’output.

query_string = """"Q: Qual è il sentiment della seguente recensione: ```Il cibo era molto buono.```?\\n""A: [SENTIMENT]" where (SENTIMENT in ['positivo', 'negativo', 'neutrale'])"""print(lmql.run_sync(query_string,     model = lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf",         tokenizer = 'HuggingFaceH4/zephyr-7b-beta')).variables['SENTIMENT'])# positivo

Non abbiamo bisogno di filtri con criteri di arresto poiché già limitiamo l’output a solo tre opzioni possibili e LMQL non considera altre possibilità.

Cerchiamo di utilizzare l’approccio ragionato a catena di pensieri. Dare al modello del tempo per pensare migliora solitamente i risultati. Utilizzando la sintassi di LMQL, potremmo implementare rapidamente questo approccio.

query_string = """"Q: Qual è il sentimento della seguente recensione: ```Il cibo era molto buono.```?\\n""A: Pensiamo passo dopo passo. [ANALISI]. Pertanto, il sentimento è [SENTIMENT]" where (len(TOKENS(ANALISI)) < 200) e STOPS_AT(ANALISI, '\\n') \    e (SENTIMENT in ['positivo', 'negativo', 'neutrale'])"""print(lmql.run_sync(query_string,     model = lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf",         tokenizer = 'HuggingFaceH4/zephyr-7b-beta')).variables)

L’output dal modello Zephyr è abbastanza decente.

Immagine dell'autore

Possiamo provare lo stesso prompt con Llama 2.

query_string = """"Q: Qual è il sentimento della seguente recensione: ```Il cibo era molto buono.```?\\n""A: Pensiamo passo dopo passo. [ANALISI]. Pertanto, il sentimento è [SENTIMENT]" where (len(TOKENS(ANALISI)) < 200) e STOPS_AT(ANALISI, '\\n') \    e (SENTIMENT in ['positivo', 'negativo', 'neutrale'])"""print(lmql.run_sync(query_string,     model = lmql.model("local:llama.cpp:llama-2-7b.Q4_K_M.gguf")).variables)

Il ragionamento non ha molto senso. Abbiamo già visto nel Leaderboard che il modello Zephyr è molto migliore del Llama-2-7b.

Immagine dell'autore

Nell’apprendimento automatico classico, di solito otteniamo non solo etichette di classe ma anche la loro probabilità. Potremmo ottenere gli stessi dati utilizzando distribution in LMQL. Dobbiamo solo specificare la variabile e i valori possibili – distribution SENTIMENT in [‘positivo‘, ‘negativo‘, ‘neutrale‘].

query_string = """"Q: Qual è il sentimento della seguente recensione: ```Il cibo era molto buono.```?\\n""A: Pensiamo passo dopo passo. [ANALISI]. Pertanto, il sentimento è [SENTIMENT]" distribution SENTIMENT in ['positivo', 'negativo', 'neutrale']where (len(TOKENS(ANALISI)) < 200) e STOPS_AT(ANALISI, '\\n')"""print(lmql.run_sync(query_string,     model = lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf",         tokenizer = 'HuggingFaceH4/zephyr-7b-beta')).variables)

Ora abbiamo delle probabilità nell’output e possiamo vedere che il modello è piuttosto fiducioso nel sentimento positivo.

Le probabilità potrebbero essere utili in pratica se si desidera utilizzare solo decisioni quando il modello è fiducioso.

Immagine dell'autore

Ora, creiamo una funzione per utilizzare la nostra analisi del sentimento per varie voci in ingresso. Sarebbe interessante confrontare i risultati con e senza distribuzione, quindi abbiamo bisogno di due funzioni.

@lmql.query(model=lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf",    tokenizer = 'HuggingFaceH4/zephyr-7b-beta', n_gpu_layers=1000))# specificato n_gpu_layers per utilizzare la GPU con maggiore velocitàdef sentiment_analysis(review):    '''lmql    "Q: Qual è il sentimento della seguente recensione: ```{review}```?\\n"    "A: Pensiamo passo dopo passo. [ANALISI]. Pertanto, il sentimento è [SENTIMENT]" where (len(TOKENS(ANALISI)) < 200) e STOPS_AT(ANALISI, '\\n') \        e (SENTIMENT in ['positivo', 'negativo', 'neutrale'])    '''@lmql.query(model=lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf",   tokenizer = 'HuggingFaceH4/zephyr-7b-beta', n_gpu_layers=1000))def sentiment_analysis_distribution(review):    '''lmql    "Q: Qual è il sentimento della seguente recensione: ```{review}```?\\n"    "A: Pensiamo passo dopo passo. [ANALISI]. Pertanto, il sentimento è [SENTIMENT]" distribution SENTIMENT in ['positivo', 'negativo', 'neutrale']    where (len(TOKENS(ANALISI)) < 200) e STOPS_AT(ANALISI, '\\n')    '''

Poi, potremmo utilizzare questa funzione per la nuova recensione.

sentiment_analysis('La stanza era sporca')

Il modello ha deciso che era neutrale.

Immagine dell'autore

C’è una motivazione dietro questa conclusione, ma direi che questa recensione è negativa. Vediamo se possiamo utilizzare altri decoder e ottenere risultati migliori.

Di default, viene utilizzato il decoder argmax. È l’approccio più diretto: ad ogni passaggio, il modello seleziona il token con la probabilità più alta. Potremmo provare a giocare con altre opzioni.

Proviamo a utilizzare l’approccio beam search con n = 3 e una temperatura tempreture = 0.8 abbastanza alta. Come risultato, otterremmo tre sequenze ordinate per verosimiglianza, quindi potremmo prendere semplicemente la prima (con la verosimiglianza più alta).

sentiment_analysis('La stanza era sporca', decoder = 'beam',     n = 3, temperature = 0.8)[0]

Ora, il modello è stato in grado di individuare il sentimento negativo in questa recensione.

Immagine dell'autore

Vale la pena dire che c’è un costo per la decodifica beam search. Dato che stiamo lavorando su tre sequenze (beam), ottenere un risultato LLM richiede in media 3 volte più tempo: 39.55 secondi contro 13.15 secondi.

Ora, abbiamo le nostre funzioni e possiamo testarle con i nostri dati reali.

Risultati sui dati reali

Ho eseguito tutte le funzioni su un campione del 10% del dataset di recensioni di Yelp con diversi parametri:

  • modelli: Llama 2 o Zephyr,
  • approccio: utilizzando la distribuzione o solo un prompt vincolato,
  • decoder: argmax o beam search.

Prima di tutto, confrontiamo l’accuratezza, ovvero la percentuale di recensioni con il sentimento corretto. Possiamo vedere che Zephyr si comporta molto meglio del modello Llama 2. Inoltre, per qualche motivo, otteniamo una qualità significativamente peggiore con le distribuzioni.

Grafico dell'autore

Se guardiamo un po’ più in profondità, potremmo notare:

  • Per le recensioni positive, l’accuratezza è di solito più alta.
  • L’errore più comune è etichettare la recensione come neutrale.
  • Per Llama 2 con prompt, possiamo notare un alto tasso di problemi critici (commenti positivi che sono stati etichettati come negativi).

In molti casi, suppongo che il modello utilizzi una ragionamento simile, classificando i commenti negativi come neutrali, come abbiamo visto in precedenza con l’esempio della “stanza sporca”. Il modello non è sicuro se “stanza sporca” abbia un sentimento negativo o neutrale poiché non sappiamo se il cliente si aspettava una stanza pulita.

Grafico dell'autore
Grafico dell'autore

È interessante anche guardare le probabilità effettive:

  • Il percentile del 75% delle etichette positive per i commenti positivi è superiore a 0,85 per il modello Zephyr, mentre è molto più basso per il modello Llama 2.
  • Tutti i modelli mostrano una scarsa performance per i commenti negativi, in cui il percentile del 75% delle etichette negative per i commenti negativi è molto inferiore anche a 0,5.
Grafico dell'autore
Grafico dell'autore

La nostra rapida ricerca mostra che un prompt “vanilla” con un modello Zephyr e un decodificatore “argmax” sarebbe la migliore opzione per l’analisi del sentiment. Tuttavia, vale la pena verificare approcci diversi per il tuo caso d’uso. Inoltre, è possibile ottenere risultati migliori apportando modifiche ai prompt.

Puoi trovare il codice completo su GitHub.

Sommario

Oggi abbiamo discusso del concetto di LMP (Language Model Programming) che ti consente di mixare prompt in linguaggio naturale e istruzioni di scripting. Abbiamo provato a usarlo per compiti di analisi del sentiment e abbiamo ottenuto dei risultati decenti utilizzando modelli open-source locali.

Anche se LMQL non è ancora molto diffuso, questo approccio potrebbe essere utile e guadagnare popolarità in futuro poiché combina linguaggi naturali e di programmazione in un potente strumento per gli LMs.

Grazie infinite per aver letto questo articolo. Spero che sia stato istruttivo per te. Se hai domande o commenti aggiuntivi, ti prego di lasciarli nella sezione dei commenti.

Dataset

Kotzias, Dimitrios. (2015). Sentiment Labelled Sentences. UCI Machine Learning Repository (CC BY 4.0 licenza). https://doi.org/10.24432/C57604