{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Un'esplorazione del *token embedding* con un transformer\n",
"\n",
"\n",
"Luca Mari, ottobre 2024 \n",
"\n",
"Quest'opera è distribuita con Licenza Creative Commons Attribuzione - Non commerciale - Condividi allo stesso modo 4.0 Internazionale. \n",
"
\n",
"\n",
"**Obiettivo**: comprendere la logica della \"tokenizzazione\", il processo con cui un testo viene trasformato in una successione di elementi linguistici elementari (\"token\"). \n",
"**Precompetenze**: basi di Python.\n",
"\n",
"> Per eseguire questo notebook, supponiamo con VSCode, occorre:\n",
"> * installare un interprete Python\n",
"> * scaricare da https://code.visualstudio.com/download e installare VSCode\n",
"> * eseguire VSCode e attivare le estensioni per Python e Jupyter\n",
"> * ancora in VSCode:\n",
"> * creare una cartella di lavoro e renderla la cartella corrente\n",
"> * copiare nella cartella i file di questa attività: [embed.ipynb](embed.ipynb), [tokenizeutils.py](tokenizeutils.py)]\n",
"> * aprire il notebook `embed.ipynb`\n",
"> * creare un ambiente virtuale locale Python (Select Kernel | Python Environments | Create Python Environment | Venv, e scegliere un interprete Python):\n",
"> * installare i moduli Python richiesti, eseguendo dal terminale: \n",
"> `pip install torch transformers multimethod colorama python-docx`"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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).\n",
"\n",
"Dopo aver caricato il modello, verifichiamo che il processo sia andato a buon fine visualizzando le due informazioni principali:\n",
"* il numero di token riconosciuti nel vocabolario del modello (`model.vocab_size`);\n",
"* la dimensione del vettore in cui ogni token viene embedded (`model.embedding_dim`)."
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Il tokenizzatore ha un vocabolario di 31102 token che riconosce.\n",
"Ogni token viene mappato ('embedded') in un vettore di 768 numeri.\n",
"La matrice degli embeddings ha perciò dimensione (32102, 768)\n"
]
}
],
"source": [
"from tokenizeutils import Model\n",
"from pprint import pprint\n",
"\n",
"model = Model('dbmdz/bert-base-italian-xxl-cased', True)\n",
"\n",
"print(f\"Il tokenizzatore ha un vocabolario di {model.vocab_size} token che riconosce.\")\n",
"print(f\"Ogni token viene mappato ('embedded') in un vettore di {model.embedding_dim} numeri.\")\n",
"print(f\"La matrice degli embeddings ha perciò dimensione {model.vocab_embeddings.shape}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Il tokenizzatore mantiene un vocabolario dei token che riconosce, in una tabella in cui a ogni token è associato un identificatore univoco (`id`)."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Dato un token, come 'bellezza', il tokenizzatore è in grado di trovarne l'identificatore: 6108\n",
"(ai token non presenti nel vocabolario è associato l'identificatore 101).\n"
]
}
],
"source": [
"token = \"bellezza\"\n",
"token_id = model.token_to_id(token)\n",
"print(f\"Dato un token, come '{token}', il tokenizzatore è in grado di trovarne l'identificatore: {token_id}\")\n",
"print(f\"(ai token non presenti nel vocabolario è associato l'identificatore {model.tokenizer.convert_tokens_to_ids(model.tokenizer.unk_token)}).\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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)."
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Il token 'bellezza' è associato a un vettore di 768 elementi e i cui primi 5 elementi sono:\n",
"[ 0.03051873 0.01173639 -0.04997671 0.0277972 0.02349026]\n"
]
}
],
"source": [
"embedding = model.token_to_embedding(token)\n",
"print(f\"Il token '{token}' è associato a un vettore di {len(embedding)} elementi e i cui primi 5 elementi sono:\\n{embedding[:5]}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('bellezze', 0.45),\n",
" ('dolcezza', 0.38),\n",
" ('splendore', 0.38),\n",
" ('estetica', 0.35),\n",
" ('fascino', 0.34)]\n"
]
}
],
"source": [
"pprint(model.most_similar(token, top_n=5))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"L'embedding consente di operare in modo piuttosto sofisticato sul vocabolario. \n",
"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. \n",
"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\"."
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('regina', 0.31)]\n"
]
}
],
"source": [
"positive_examples = [\"re\", \"donna\"] # sovrano donna\n",
"negative_examples = [\"uomo\"]\n",
"pprint(model.most_similar(positive_examples, negative_examples, top_n=1))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In accordo a questo principio, possiamo sperimentare le capacità di _relazionalità semantica_ del modello che stiamo usando con alcuni altri esempi strutturalmente analoghi."
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('Madrid', 0.44)]\n"
]
}
],
"source": [
"positive_examples = [\"Roma\", \"Spagna\"] # capitale di uno stato\n",
"negative_examples = [\"Italia\"]\n",
"print(model.most_similar(positive_examples, negative_examples))"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('Spagna', 0.39)]\n"
]
}
],
"source": [
"positive_examples = [\"Italia\", \"Catalogna\"] # stato di appartenenza di una regione\n",
"negative_examples = [\"Lombardia\"]\n",
"print(model.most_similar(positive_examples, negative_examples))"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('Bonaparte', 0.44)]\n"
]
}
],
"source": [
"positive_examples = [\"Garibaldi\", \"Francia\"] # eroe nazionale\n",
"negative_examples = [\"Italia\"]\n",
"print(model.most_similar(positive_examples, negative_examples))"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('inverno', 0.5)]\n"
]
}
],
"source": [
"positive_examples = [\"estate\", \"freddo\"] # stagione per temperatura \n",
"negative_examples = [\"caldo\"]\n",
"print(model.most_similar(positive_examples, negative_examples))"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('pianoforte', 0.63)]\n"
]
}
],
"source": [
"positive_examples = [\"chitarra\", \"pianista\"] # strumento di un musicista\n",
"negative_examples = [\"chitarrista\"]\n",
"print(model.most_similar(positive_examples, negative_examples))"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('ginnastica', 0.4)]\n"
]
}
],
"source": [
"positive_examples = [\"nuoto\", \"palestra\"] # sport praticato in un luogo\n",
"negative_examples = [\"piscina\"]\n",
"print(model.most_similar(positive_examples, negative_examples))"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('quattro', 0.59)]\n"
]
}
],
"source": [
"positive_examples = [\"due\", \"tre\"] # numero successivo\n",
"negative_examples = [\"uno\"]\n",
"print(model.most_similar(positive_examples, negative_examples))"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('madre', 0.56)]\n"
]
}
],
"source": [
"positive_examples = [\"padre\", \"figlia\"] # genitore per genere\n",
"negative_examples = [\"figlio\"]\n",
"print(model.most_similar(positive_examples, negative_examples))"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('attrice', 0.63)]\n"
]
}
],
"source": [
"positive_examples = [\"attore\", \"donna\"] # femminile di un ruolo professionale\n",
"negative_examples = [\"uomo\"]\n",
"print(model.most_similar(positive_examples, negative_examples))"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('buono', 0.37)]\n"
]
}
],
"source": [
"positive_examples = [\"bello\", \"cattivo\"] # opposto di un aggettivo\n",
"negative_examples = [\"brutto\"]\n",
"print(model.most_similar(positive_examples, negative_examples))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Se i precedenti sono esempi ricchi semanticamente, proviamo a sperimentare anche con esempi solo grammaticali."
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('nera', 0.56)]\n"
]
}
],
"source": [
"positive_examples = [\"bianca\", \"nero\"] # femminile di un aggettivo\n",
"negative_examples = [\"bianco\"]\n",
"print(model.most_similar(positive_examples, negative_examples))"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('automobile', 0.54)]\n"
]
}
],
"source": [
"positive_examples = [\"treno\", \"automobili\"] # singolare di un sostantivo\n",
"negative_examples = [\"treni\"]\n",
"print(model.most_similar(positive_examples, negative_examples))"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('guardare', 0.57)]\n"
]
}
],
"source": [
"positive_examples = [\"andare\", \"guardato\"] # infinito di un verbo\n",
"negative_examples = [\"andato\"]\n",
"print(model.most_similar(positive_examples, negative_examples))"
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[('ascoltando', 0.66)]\n"
]
}
],
"source": [
"positive_examples = [\"pensando\", \"ascoltare\"] # gerundio di un verbo\n",
"negative_examples = [\"pensare\"]\n",
"print(model.most_similar(positive_examples, negative_examples))"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.6"
}
},
"nbformat": 4,
"nbformat_minor": 2
}