Un'esplorazione del token embedding con un transformer¶
Luca Mari, ottobre 2024
Quest'opera è distribuita con Licenza Creative Commons Attribuzione - Non commerciale - Condividi allo stesso modo 4.0 Internazionale.
Obiettivo: comprendere la logica della "tokenizzazione", il processo con cui un testo viene trasformato in una successione di elementi linguistici elementari ("token").
Precompetenze: basi di Python.
Per eseguire questo notebook, supponiamo con VSCode, occorre:
- installare un interprete Python
- scaricare da https://code.visualstudio.com/download e installare VSCode
- eseguire VSCode e attivare le estensioni per Python e Jupyter
- ancora in VSCode:
- creare una cartella di lavoro e renderla la cartella corrente
- copiare nella cartella i file di questa attività: embed.ipynb, tokenizeutils.py]
- aprire il notebook
embed.ipynb
- creare un ambiente virtuale locale Python (Select Kernel | Python Environments | Create Python Environment | Venv, e scegliere un interprete Python):
- installare i moduli Python richiesti, eseguendo dal terminale:
pip install torch transformers multimethod colorama python-docx
Per prima cosa, importiamo il modulo che contiene le funzioni per consentire un accesso "di alto livello" al modello pre-addestrato che opererà sia come tokenizzatore sia come sistema di embedding, usando in questo caso una versione pre-addestrata e fine tuned, su testi in italiano, di BERT
, che è un transformer accessibile liberamente (https://it.wikipedia.org/wiki/BERT) ed eseguibile anche localmente (alla prima esecuzione sarà dunque necessario attendere che il modello sia scaricato dal sito di Hugging Face: è un file di circa 400 MB che viene copiato nella cartella HFdata della propria cartella personale) (non discutiamo qui di come questo modello sia stato addestrato a fare embedding).
Dopo aver caricato il modello, verifichiamo che il processo sia andato a buon fine visualizzando le due informazioni principali:
- il numero di token riconosciuti nel vocabolario del modello (
model.vocab_size
); - la dimensione del vettore in cui ogni token viene embedded (
model.embedding_dim
).
from tokenizeutils import Model
from pprint import pprint
model = Model('dbmdz/bert-base-italian-xxl-cased', True)
print(f"Il tokenizzatore ha un vocabolario di {model.vocab_size} token che riconosce.")
print(f"Ogni token viene mappato ('embedded') in un vettore di {model.embedding_dim} numeri.")
print(f"La matrice degli embeddings ha perciò dimensione {model.vocab_embeddings.shape}")
Il tokenizzatore ha un vocabolario di 31102 token che riconosce. Ogni token viene mappato ('embedded') in un vettore di 768 numeri. La matrice degli embeddings ha perciò dimensione (32102, 768)
Il tokenizzatore mantiene un vocabolario dei token che riconosce, in una tabella in cui a ogni token è associato un identificatore univoco (id
).
token = "bellezza"
token_id = model.token_to_id(token)
print(f"Dato un token, come '{token}', il tokenizzatore è in grado di trovarne l'identificatore: {token_id}")
print(f"(ai token non presenti nel vocabolario è associato l'identificatore {model.tokenizer.convert_tokens_to_ids(model.tokenizer.unk_token)}).")
Dato un token, come 'bellezza', il tokenizzatore è in grado di trovarne l'identificatore: 6108 (ai token non presenti nel vocabolario è associato l'identificatore 101).
Il modello è stato addestrato a mappare (to embed, appunto) ogni token, con il suo identificatore, in un vettore di numeri (c'è da considerare che i transformer, come BERT
, operano sulla base di un embedding dinamico, in cui il vettore di numeri associato a ogni token dipende anche dal contesto ('embedding posizionale'): qui noi lavoriamo solo con la componente statica del maaping).
embedding = model.token_to_embedding(token)
print(f"Il token '{token}' è associato a un vettore di {len(embedding)} elementi e i cui primi 5 elementi sono:\n{embedding[:5]}")
Il token 'bellezza' è associato a un vettore di 768 elementi e i cui primi 5 elementi sono: [ 0.03051873 0.01173639 -0.04997671 0.0277972 0.02349026]
L'embedding è dunque una funzione dall'insieme dei token riconosciuti, cioè il vocabolario, allo spazio metrico dei vettori a n dimensioni. Il modello che realizza tale funzione è addestrato in modo tale da cercare di indurre una struttura metrica sul vocabolario, sulla base del principio che token di significato simile dovrebbero essere associati a vettori vicini. Dato un token, è così possibile elencare i token che gli sono più simili nel vocabolario, cioè quelli che associati a vettori più vicini al vettore del token dato.
pprint(model.most_similar(token, top_n=5))
[('bellezze', 0.45), ('dolcezza', 0.38), ('splendore', 0.38), ('estetica', 0.35), ('fascino', 0.34)]
L'embedding consente di operare in modo piuttosto sofisticato sul vocabolario.
Per esempio, dati due token $A$ e $B$ a ognuno dei quali è stato associato un vettore, $v(A)$ e $v(B)$, se la regola di associazione $v(.)$ è sufficientemente ricca da un punto di vista semantico allora il vettore $v(A)-v(B)$ è associato alla relazione tra $A$ e $B$, interpretata dunque come la loro differenza.
In questo modo diventa possibile operare con relazioni semantiche tra token. Per esempio, data la relazione tra $"re"$ e $"uomo"$, qual è il token $X$ che è nella stessa relazione di $"re"$ ma questa volta con $"donna"$? Questa domanda è dunque codificata come $v("re")-v("uomo")=v(X)-v("donna")$, e perciò $v(X)=v("re")+v("donna")-v("uomo)$, in cui $"re"$ e $"donna"$ sono gli "esempi positivi" e $"uomo"$ è l'"esempio negativo".
positive_examples = ["re", "donna"] # sovrano donna
negative_examples = ["uomo"]
pprint(model.most_similar(positive_examples, negative_examples, top_n=1))
[('regina', 0.31)]
In accordo a questo principio, possiamo sperimentare le capacità di relazionalità semantica del modello che stiamo usando con alcuni altri esempi strutturalmente analoghi.
positive_examples = ["Roma", "Spagna"] # capitale di uno stato
negative_examples = ["Italia"]
print(model.most_similar(positive_examples, negative_examples))
[('Madrid', 0.44)]
positive_examples = ["Italia", "Catalogna"] # stato di appartenenza di una regione
negative_examples = ["Lombardia"]
print(model.most_similar(positive_examples, negative_examples))
[('Spagna', 0.39)]
positive_examples = ["Garibaldi", "Francia"] # eroe nazionale
negative_examples = ["Italia"]
print(model.most_similar(positive_examples, negative_examples))
[('Bonaparte', 0.44)]
positive_examples = ["estate", "freddo"] # stagione per temperatura
negative_examples = ["caldo"]
print(model.most_similar(positive_examples, negative_examples))
[('inverno', 0.5)]
positive_examples = ["chitarra", "pianista"] # strumento di un musicista
negative_examples = ["chitarrista"]
print(model.most_similar(positive_examples, negative_examples))
[('pianoforte', 0.63)]
positive_examples = ["nuoto", "palestra"] # sport praticato in un luogo
negative_examples = ["piscina"]
print(model.most_similar(positive_examples, negative_examples))
[('ginnastica', 0.4)]
positive_examples = ["due", "tre"] # numero successivo
negative_examples = ["uno"]
print(model.most_similar(positive_examples, negative_examples))
[('quattro', 0.59)]
positive_examples = ["padre", "figlia"] # genitore per genere
negative_examples = ["figlio"]
print(model.most_similar(positive_examples, negative_examples))
[('madre', 0.56)]
positive_examples = ["attore", "donna"] # femminile di un ruolo professionale
negative_examples = ["uomo"]
print(model.most_similar(positive_examples, negative_examples))
[('attrice', 0.63)]
positive_examples = ["bello", "cattivo"] # opposto di un aggettivo
negative_examples = ["brutto"]
print(model.most_similar(positive_examples, negative_examples))
[('buono', 0.37)]
Se i precedenti sono esempi ricchi semanticamente, proviamo a sperimentare anche con esempi solo grammaticali.
positive_examples = ["bianca", "nero"] # femminile di un aggettivo
negative_examples = ["bianco"]
print(model.most_similar(positive_examples, negative_examples))
[('nera', 0.56)]
positive_examples = ["treno", "automobili"] # singolare di un sostantivo
negative_examples = ["treni"]
print(model.most_similar(positive_examples, negative_examples))
[('automobile', 0.54)]
positive_examples = ["andare", "guardato"] # infinito di un verbo
negative_examples = ["andato"]
print(model.most_similar(positive_examples, negative_examples))
[('guardare', 0.57)]
positive_examples = ["pensando", "ascoltare"] # gerundio di un verbo
negative_examples = ["pensare"]
print(model.most_similar(positive_examples, negative_examples))
[('ascoltando', 0.66)]