{ "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 }