{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Attività di classificazione del testo\n", "\n", "Come abbiamo accennato, ci concentreremo su una semplice attività di classificazione del testo basata sul dataset **AG_NEWS**, che consiste nel classificare i titoli delle notizie in una delle 4 categorie: Mondo, Sport, Economia e Scienza/Tecnologia.\n", "\n", "## Il Dataset\n", "\n", "Questo dataset è integrato nel modulo [`torchtext`](https://github.com/pytorch/text), quindi possiamo accedervi facilmente.\n" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import torch\n", "import torchtext\n", "import os\n", "import collections\n", "os.makedirs('./data',exist_ok=True)\n", "train_dataset, test_dataset = torchtext.datasets.AG_NEWS(root='./data')\n", "classes = ['World', 'Sports', 'Business', 'Sci/Tech']" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Qui, `train_dataset` e `test_dataset` contengono collezioni che restituiscono coppie di etichetta (numero di classe) e testo rispettivamente, ad esempio:\n" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(3,\n", " \"Wall St. Bears Claw Back Into the Black (Reuters) Reuters - Short-sellers, Wall Street's dwindling\\\\band of ultra-cynics, are seeing green again.\")" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "list(train_dataset)[0]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Quindi, stampiamo i primi 10 nuovi titoli dal nostro dataset:\n" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "**Sci/Tech** -> Wall St. Bears Claw Back Into the Black (Reuters) Reuters - Short-sellers, Wall Street's dwindling\\band of ultra-cynics, are seeing green again.\n", "**Sci/Tech** -> Carlyle Looks Toward Commercial Aerospace (Reuters) Reuters - Private investment firm Carlyle Group,\\which has a reputation for making well-timed and occasionally\\controversial plays in the defense industry, has quietly placed\\its bets on another part of the market.\n", "**Sci/Tech** -> Oil and Economy Cloud Stocks' Outlook (Reuters) Reuters - Soaring crude prices plus worries\\about the economy and the outlook for earnings are expected to\\hang over the stock market next week during the depth of the\\summer doldrums.\n", "**Sci/Tech** -> Iraq Halts Oil Exports from Main Southern Pipeline (Reuters) Reuters - Authorities have halted oil export\\flows from the main pipeline in southern Iraq after\\intelligence showed a rebel militia could strike\\infrastructure, an oil official said on Saturday.\n", "**Sci/Tech** -> Oil prices soar to all-time record, posing new menace to US economy (AFP) AFP - Tearaway world oil prices, toppling records and straining wallets, present a new economic menace barely three months before the US presidential elections.\n" ] } ], "source": [ "for i,x in zip(range(5),train_dataset):\n", " print(f\"**{classes[x[0]]}** -> {x[1]}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Poiché i dataset sono iteratori, se vogliamo utilizzare i dati più volte dobbiamo convertirli in lista:\n" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "train_dataset, test_dataset = torchtext.datasets.AG_NEWS(root='./data')\n", "train_dataset = list(train_dataset)\n", "test_dataset = list(test_dataset)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Tokenizzazione\n", "\n", "Ora dobbiamo convertire il testo in **numeri** che possono essere rappresentati come tensori. Se vogliamo una rappresentazione a livello di parola, dobbiamo fare due cose:\n", "* utilizzare un **tokenizer** per suddividere il testo in **token**\n", "* costruire un **vocabolario** di quei token.\n" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['he', 'said', 'hello']" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "tokenizer = torchtext.data.utils.get_tokenizer('basic_english')\n", "tokenizer('He said: hello')" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "counter = collections.Counter()\n", "for (label, line) in train_dataset:\n", " counter.update(tokenizer(line))\n", "vocab = torchtext.vocab.vocab(counter, min_freq=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Utilizzando il vocabolario, possiamo facilmente codificare la nostra stringa tokenizzata in un insieme di numeri:\n" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Vocab size if 95810\n" ] }, { "data": { "text/plain": [ "[599, 3279, 97, 1220, 329, 225, 7368]" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vocab_size = len(vocab)\n", "print(f\"Vocab size if {vocab_size}\")\n", "\n", "stoi = vocab.get_stoi() # dict to convert tokens to indices\n", "\n", "def encode(x):\n", " return [stoi[s] for s in tokenizer(x)]\n", "\n", "encode('I love to play with my words')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Rappresentazione testuale Bag of Words\n", "\n", "Poiché le parole rappresentano significati, a volte possiamo capire il senso di un testo semplicemente osservando le singole parole, indipendentemente dal loro ordine nella frase. Ad esempio, quando si classificano notizie, parole come *meteo*, *neve* probabilmente indicano *previsioni del tempo*, mentre parole come *azioni*, *dollaro* potrebbero riferirsi a *notizie finanziarie*.\n", "\n", "La rappresentazione vettoriale **Bag of Words** (BoW) è la rappresentazione vettoriale tradizionale più comunemente utilizzata. Ogni parola è collegata a un indice del vettore, e l'elemento del vettore contiene il numero di occorrenze di una parola in un determinato documento.\n", "\n", "![Immagine che mostra come una rappresentazione vettoriale Bag of Words sia rappresentata in memoria.](../../../../../translated_images/it/bag-of-words-example.606fc1738f1d7ba9.webp) \n", "\n", "> **Nota**: Puoi anche pensare al BoW come alla somma di tutti i vettori one-hot-encoded per le singole parole nel testo.\n", "\n", "Di seguito è riportato un esempio di come generare una rappresentazione Bag of Words utilizzando la libreria python Scikit Learn:\n" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 1, 0, 2, 0, 0, 0, 0, 0]], dtype=int64)" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from sklearn.feature_extraction.text import CountVectorizer\n", "vectorizer = CountVectorizer()\n", "corpus = [\n", " 'I like hot dogs.',\n", " 'The dog ran fast.',\n", " 'Its hot outside.',\n", " ]\n", "vectorizer.fit_transform(corpus)\n", "vectorizer.transform(['My dog likes hot dogs on a hot day.']).toarray()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Per calcolare il vettore bag-of-words dalla rappresentazione vettoriale del nostro dataset AG_NEWS, possiamo utilizzare la seguente funzione:\n" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "tensor([2., 1., 2., ..., 0., 0., 0.])\n" ] } ], "source": [ "vocab_size = len(vocab)\n", "\n", "def to_bow(text,bow_vocab_size=vocab_size):\n", " res = torch.zeros(bow_vocab_size,dtype=torch.float32)\n", " for i in encode(text):\n", " if i **Nota:** Qui stiamo utilizzando la variabile globale `vocab_size` per specificare la dimensione predefinita del vocabolario. Poiché spesso la dimensione del vocabolario è piuttosto grande, possiamo limitarla alle parole più frequenti. Prova a ridurre il valore di `vocab_size` ed eseguire il codice qui sotto, e osserva come influisce sulla precisione. Dovresti aspettarti una diminuzione della precisione, ma non drammatica, a favore di prestazioni più elevate.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Addestramento del classificatore BoW\n", "\n", "Ora che abbiamo imparato a costruire la rappresentazione Bag-of-Words del nostro testo, alleniamo un classificatore su di essa. Per prima cosa, dobbiamo convertire il nostro dataset per l'addestramento in modo tale che tutte le rappresentazioni vettoriali posizionali siano trasformate in rappresentazioni Bag-of-Words. Questo può essere ottenuto passando la funzione `bowify` come parametro `collate_fn` al classico `DataLoader` di torch:\n" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "from torch.utils.data import DataLoader\n", "import numpy as np \n", "\n", "# this collate function gets list of batch_size tuples, and needs to \n", "# return a pair of label-feature tensors for the whole minibatch\n", "def bowify(b):\n", " return (\n", " torch.LongTensor([t[0]-1 for t in b]),\n", " torch.stack([to_bow(t[1]) for t in b])\n", " )\n", "\n", "train_loader = DataLoader(train_dataset, batch_size=16, collate_fn=bowify, shuffle=True)\n", "test_loader = DataLoader(test_dataset, batch_size=16, collate_fn=bowify, shuffle=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ora definiamo una semplice rete neurale classificatrice che contiene un livello lineare. La dimensione del vettore di input è uguale a `vocab_size`, e la dimensione dell'output corrisponde al numero di classi (4). Poiché stiamo risolvendo un compito di classificazione, la funzione di attivazione finale è `LogSoftmax()`.\n" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "net = torch.nn.Sequential(torch.nn.Linear(vocab_size,4),torch.nn.LogSoftmax(dim=1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ora definiremo il ciclo di addestramento standard di PyTorch. Poiché il nostro dataset è piuttosto grande, per il nostro scopo didattico ci alleneremo solo per un'epoca, e a volte anche per meno di un'epoca (specificare il parametro `epoch_size` ci consente di limitare l'addestramento). Riporteremo anche l'accuratezza accumulata durante l'addestramento; la frequenza di segnalazione è specificata utilizzando il parametro `report_freq`.\n" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [], "source": [ "def train_epoch(net,dataloader,lr=0.01,optimizer=None,loss_fn = torch.nn.NLLLoss(),epoch_size=None, report_freq=200):\n", " optimizer = optimizer or torch.optim.Adam(net.parameters(),lr=lr)\n", " net.train()\n", " total_loss,acc,count,i = 0,0,0,0\n", " for labels,features in dataloader:\n", " optimizer.zero_grad()\n", " out = net(features)\n", " loss = loss_fn(out,labels) #cross_entropy(out,labels)\n", " loss.backward()\n", " optimizer.step()\n", " total_loss+=loss\n", " _,predicted = torch.max(out,1)\n", " acc+=(predicted==labels).sum()\n", " count+=len(labels)\n", " i+=1\n", " if i%report_freq==0:\n", " print(f\"{count}: acc={acc.item()/count}\")\n", " if epoch_size and count>epoch_size:\n", " break\n", " return total_loss.item()/count, acc.item()/count" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3200: acc=0.8028125\n", "6400: acc=0.8371875\n", "9600: acc=0.8534375\n", "12800: acc=0.85765625\n" ] }, { "data": { "text/plain": [ "(0.026090790722161722, 0.8620069296375267)" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "train_epoch(net,train_loader,epoch_size=15000)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## BiGrams, TriGrams e N-Grams\n", "\n", "Una limitazione dell'approccio bag of words è che alcune parole fanno parte di espressioni composte da più parole. Ad esempio, la parola 'hot dog' ha un significato completamente diverso rispetto alle parole 'hot' e 'dog' in altri contesti. Se rappresentiamo sempre le parole 'hot' e 'dog' con gli stessi vettori, possiamo confondere il nostro modello.\n", "\n", "Per affrontare questo problema, le **rappresentazioni N-gram** vengono spesso utilizzate nei metodi di classificazione dei documenti, dove la frequenza di ogni parola, coppia di parole o trio di parole è una caratteristica utile per addestrare i classificatori. Nella rappresentazione bigram, ad esempio, aggiungiamo tutte le coppie di parole al vocabolario, oltre alle parole originali.\n", "\n", "Di seguito è riportato un esempio di come generare una rappresentazione bag of words bigram utilizzando Scikit Learn:\n" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Vocabulary:\n", " {'i': 7, 'like': 11, 'hot': 4, 'dogs': 2, 'i like': 8, 'like hot': 12, 'hot dogs': 5, 'the': 16, 'dog': 0, 'ran': 14, 'fast': 3, 'the dog': 17, 'dog ran': 1, 'ran fast': 15, 'its': 9, 'outside': 13, 'its hot': 10, 'hot outside': 6}\n" ] }, { "data": { "text/plain": [ "array([[1, 0, 1, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],\n", " dtype=int64)" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bigram_vectorizer = CountVectorizer(ngram_range=(1, 2), token_pattern=r'\\b\\w+\\b', min_df=1)\n", "corpus = [\n", " 'I like hot dogs.',\n", " 'The dog ran fast.',\n", " 'Its hot outside.',\n", " ]\n", "bigram_vectorizer.fit_transform(corpus)\n", "print(\"Vocabulary:\\n\",bigram_vectorizer.vocabulary_)\n", "bigram_vectorizer.transform(['My dog likes hot dogs on a hot day.']).toarray()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Lo svantaggio principale dell'approccio N-gram è che la dimensione del vocabolario inizia a crescere estremamente rapidamente. In pratica, è necessario combinare la rappresentazione N-gram con alcune tecniche di riduzione della dimensionalità, come le *embedding*, che discuteremo nella prossima unità.\n", "\n", "Per utilizzare la rappresentazione N-gram nel nostro dataset **AG News**, dobbiamo costruire un vocabolario ngram specifico:\n" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Bigram vocabulary length = 1308842\n" ] } ], "source": [ "counter = collections.Counter()\n", "for (label, line) in train_dataset:\n", " l = tokenizer(line)\n", " counter.update(torchtext.data.utils.ngrams_iterator(l,ngrams=2))\n", " \n", "bi_vocab = torchtext.vocab.vocab(counter, min_freq=1)\n", "\n", "print(\"Bigram vocabulary length = \",len(bi_vocab))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Potremmo quindi utilizzare lo stesso codice di cui sopra per addestrare il classificatore, tuttavia, sarebbe molto inefficiente in termini di memoria. Nell'unità successiva, addestreremo un classificatore bigram utilizzando gli embeddings.\n", "\n", "> **Nota:** Puoi mantenere solo quegli ngrammi che compaiono nel testo più di un determinato numero di volte. Questo assicurerà che i bigrammi poco frequenti vengano omessi e ridurrà significativamente la dimensionalità. Per fare ciò, imposta il parametro `min_freq` a un valore più alto e osserva come cambia la lunghezza del vocabolario.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Frequenza Termine-Inversa Frequenza Documento TF-IDF\n", "\n", "Nella rappresentazione BoW, le occorrenze delle parole sono ponderate in modo uniforme, indipendentemente dalla parola stessa. Tuttavia, è evidente che le parole frequenti, come *a*, *in*, ecc., sono molto meno importanti per la classificazione rispetto ai termini specializzati. Infatti, nella maggior parte dei compiti di NLP alcune parole sono più rilevanti di altre.\n", "\n", "**TF-IDF** sta per **frequenza termine–inversa frequenza documento**. È una variazione del bag of words, dove invece di un valore binario 0/1 che indica la presenza di una parola in un documento, viene utilizzato un valore in virgola mobile, che è correlato alla frequenza di occorrenza della parola nel corpus.\n", "\n", "Più formalmente, il peso $w_{ij}$ di una parola $i$ nel documento $j$ è definito come:\n", "$$\n", "w_{ij} = tf_{ij}\\times\\log({N\\over df_i})\n", "$$\n", "dove\n", "* $tf_{ij}$ è il numero di occorrenze di $i$ in $j$, ovvero il valore BoW che abbiamo visto prima\n", "* $N$ è il numero di documenti nella collezione\n", "* $df_i$ è il numero di documenti che contengono la parola $i$ nell'intera collezione\n", "\n", "Il valore TF-IDF $w_{ij}$ aumenta proporzionalmente al numero di volte in cui una parola appare in un documento ed è compensato dal numero di documenti nel corpus che contengono la parola, il che aiuta a correggere il fatto che alcune parole appaiono più frequentemente di altre. Ad esempio, se la parola appare in *ogni* documento della collezione, $df_i=N$, e $w_{ij}=0$, e quei termini verrebbero completamente ignorati.\n", "\n", "Puoi facilmente creare una vettorizzazione TF-IDF del testo utilizzando Scikit Learn:\n" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[0.43381609, 0. , 0.43381609, 0. , 0.65985664,\n", " 0.43381609, 0. , 0. , 0. , 0. ,\n", " 0. , 0. , 0. , 0. , 0. ,\n", " 0. ]])" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from sklearn.feature_extraction.text import TfidfVectorizer\n", "vectorizer = TfidfVectorizer(ngram_range=(1,2))\n", "vectorizer.fit_transform(corpus)\n", "vectorizer.transform(['My dog likes hot dogs on a hot day.']).toarray()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Conclusione\n", "\n", "Tuttavia, anche se le rappresentazioni TF-IDF attribuiscono un peso di frequenza alle diverse parole, non sono in grado di rappresentare il significato o l'ordine. Come disse il famoso linguista J. R. Firth nel 1935: \"Il significato completo di una parola è sempre contestuale, e nessuno studio del significato al di fuori del contesto può essere preso seriamente\". Più avanti nel corso impareremo come catturare le informazioni contestuali dai testi utilizzando la modellazione del linguaggio.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**Disclaimer**: \nQuesto documento è stato tradotto utilizzando il servizio di traduzione AI [Co-op Translator](https://github.com/Azure/co-op-translator). Sebbene ci impegniamo per garantire l'accuratezza, si prega di notare che le traduzioni automatiche possono contenere errori o imprecisioni. Il documento originale nella sua lingua nativa dovrebbe essere considerato la fonte autorevole. Per informazioni critiche, si raccomanda una traduzione professionale effettuata da un esperto umano. Non siamo responsabili per eventuali fraintendimenti o interpretazioni errate derivanti dall'uso di questa traduzione.\n" ] } ], "metadata": { "interpreter": { "hash": "16af2a8bbb083ea23e5e41c7f5787656b2ce26968575d8763f2c4b17f9cd711f" }, "kernelspec": { "display_name": "Python 3.8.12 ('py38')", "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.8.12" }, "coopTranslator": { "original_hash": "7b9040985e748e4e2d4c689892456ad7", "translation_date": "2025-08-28T14:33:32+00:00", "source_file": "lessons/5-NLP/13-TextRep/TextRepresentationPyTorch.ipynb", "language_code": "it" } }, "nbformat": 4, "nbformat_minor": 2 }