Un'esplorazione del token embedding con un transformer¶
Luca Mari, gennaio 2025
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 in una cache locale) (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 associato ('embedded') a un vettore di {model.embedding_dim} numeri.")
Il tokenizzatore ha un vocabolario di 31102 token che riconosce. Ogni token viene associato ('embedded') a un vettore di 768 numeri.
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)}).") # type: ignore
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 una successione di numeri (c'è da considerare che i transformer, come BERT
, operano sulla base di un embedding dinamico, in cui la successione di numeri associata a ogni token dipende anche dal contesto ('embedding posizionale'): qui noi lavoriamo solo con la componente statica del mapping).
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.
n = 10
print(f"\nI {n} token più simili a '{token}' nel vocabolario:")
pprint(model.most_similar(token, top_n=n))
I 10 token più simili a 'bellezza' nel vocabolario: [('bellezze', 0.45), ('dolcezza', 0.38), ('splendore', 0.38), ('estetica', 0.35), ('fascino', 0.34), ('Belle', 0.34), ('eleganza', 0.32), ('bellissima', 0.32), ('meraviglia', 0.31), ('bella', 0.31)]
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 i "casi positivi" e $"uomo"$ è il "caso 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", "Francia"] # capitale di uno stato
negative_examples = ["Italia"]
print(model.most_similar(positive_examples, negative_examples))
[('Parigi', 0.48)]
positive_examples = ["Italia", "Catalogna"] # stato di appartenenza di una regione
negative_examples = ["Lombardia"]
print(model.most_similar(positive_examples, negative_examples, top_n=5))
[('Spagna', 0.39), ('Barcellona', 0.37), ('Turchia', 0.34), ('Europa', 0.34), ('Barcelona', 0.33)]
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, top_n=10))
[('ginnastica', 0.4), ('allenamento', 0.4), ('atletica', 0.38), ('fitness', 0.37), ('pallavolo', 0.36), ('ciclismo', 0.35), ('atleti', 0.35), ('Tennis', 0.34), ('yoga', 0.33), ('rugby', 0.33)]
positive_examples = ["due", "dieci"] # numero successivo
negative_examples = ["uno"]
print(model.most_similar(positive_examples, negative_examples, top_n=5))
[('quattro', 0.56), ('cinque', 0.56), ('tre', 0.49), ('sette', 0.43), ('dodici', 0.41)]
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)]