Salta al contenuto principale

01 - NLP Fondamenti: Tokenizzazione, Embeddings

Guida completa a NLP Fondamenti: Tokenizzazione, Embeddings: architettura, implementazione pratica e best practices per developer e team tecnici.

Cosa ne pensi di questo articolo?

01 - NLP Fondamenti: Tokenizzazione, Embeddings
NLP ModernoFoto:Ruben BoekelooPexels

In Breve

Guida completa a NLP Fondamenti: Tokenizzazione, Embeddings: architettura, implementazione pratica e best practices per developer e team tecnici.

NLP Fondamenti: Tokenizzazione, Embeddings e la Pipeline Moderna

Ogni volta che chiedi qualcosa a un assistente vocale, traduci un testo con Google Translate, filtri lo spam nella tua casella email o leggi i sottotitoli automatici di un video, stai usando il Natural Language Processing (NLP). Questa disciplina, all'intersezione tra linguistica, informatica e intelligenza artificiale, si occupa di insegnare alle macchine a comprendere, interpretare e generare il linguaggio umano.

Negli ultimi anni, il campo dell'NLP ha subito una trasformazione radicale. Siamo passati da regole scritte a mano e dizionari statici a modelli neurali capaci di comprendere sfumature, contesto e persino ironia. BERT, GPT, LLaMA e i modelli di nuova generazione non sarebbero possibili senza i fondamenti che esploreremo in questo articolo: la tokenizzazione, gli embeddings e la pipeline NLP moderna.

Questo e il primo articolo della serie NLP Moderno: da BERT ai LLM. Partiremo dalle basi assolute, costruendo passo dopo passo le intuizioni necessarie per comprendere i modelli di linguaggio più avanzati. Dedicheremo particolare attenzione alle secificità della lingua italiana, spesso trascurate nelle risorse anglofone.

Cosa Imparerai

  • Cos'è l'NLP e perchè e alla base di quasi ogni applicazione AI moderna
  • Come il testo viene preprocessato: lowercasing, stopwords, stemming e lemmatization
  • I diversi approcci alla tokenizzazione: word-level, character-level e subword (BPE, WordPiece, SentencePiece)
  • Rappresentazioni classiche del testo: Bag of Words e TF-IDF
  • Word embeddings: Word2Vec, GloVe e l'intuizione geometrica del significato
  • Embeddings contestuali: da rappresentazioni statiche a BERT
  • Sentence embeddings e le loro applicazioni pratiche
  • La pipeline NLP moderna: dal testo grezzo alla predizione
  • Specificità del preprocessing per la lingua italiana
  • Un esempio end-to-end completo con codice Python

Panoramica della Serie

# Articolo Focus
1Sei qui - Fondamenti NLPTokenizzazione, Embeddings, Pipeline
2BERT e TransformerArchitettura Attention, Pre-training
3Sentiment AnalysisClassificazione del testo con BERT
4Named Entity RecognitionEstrazione di entità dal testo
5HuggingFace TransformersLibreria e modelli pre-addestrati
6Fine-Tuning di ModelliAdattare BERT al tuo dominio
7NLP per l'ItalianoModelli e risorse per la lingua italiana
8Da BERT ai LLMGPT, LLaMA e la generazione di testo

1. Preprocessing del Testo: Preparare i Dati

Prima che qualsiasi modello NLP possa lavorare con il testo, questo deve essere pulito e normalizzato. Il testo grezzo e pieno di rumore: punteggiatura, maiuscole, abbreviazioni, emoji, HTML, URL. Il preprocessing trasforma questo caos in un formato strutturato e coerente.

1.1 Lowercasing e Normalizzazione

Il primo passo e convertire tutto il testo in minuscolo. Per un computer, "Casa", "casa" e "CASA" sono tre stringhe completamente diverse. Il lowercasing le unifica.

Python - Normalizzazione Base
import re
import unicodedata

def normalize_text(text: str) -> str:
    """Normalizzazione base del testo."""
    # Lowercasing
    text = text.lower()

    # Rimozione accenti (opzionale, NON consigliato per italiano)
    # text = unicodedata.normalize('NFKD', text)

    # Rimozione punteggiatura
    text = re.sub(r'[^\w\s]', '', text)

    # Rimozione spazi multipli
    text = re.sub(r'\s+', ' ', text).strip()

    return text

testo = "L'NLP e FANTASTICO! Analizza testi in 50+ lingue."
print(normalize_text(testo))
# Output: "lnlp e fantastico analizza testi in 50 lingue"

Attenzione agli Accenti in Italiano

In molte pipeline NLP anglofone, la rimozione degli accenti e un passo standard. In italiano, pero, gli accenti cambiano il significato delle parole: "pero" (congiunzione) vs "pero" (albero), "e" (congiunzione) vs "e" (verbo essere). Non rimuovere mai gli accenti quando lavori con testo italiano.

1.2 Stopwords: Parole Senza Contenuto Informativo

Le stopwords sono parole molto frequenti che portano poco significato semantico: articoli, preposizioni, congiunzioni. Rimuoverle riduce la dimensionalità dei dati e aiuta i modelli a concentrarsi sulle parole significative.

Stopwords Italiane vs Inglesi

Lingua Esempi di Stopwords Conteggio Tipico
Inglese the, is, at, which, on, a, an, and, or ~180 parole
Italiano il, lo, la, di, a, da, in, con, su, per, che, e, non, un ~300 parole

L'italiano ha più stopwords dell'inglese a causa della maggiore ricchezza di articoli (il, lo, la, i, gli, le), preposizioni articolate (del, dello, della, nei, negli, nelle) e forme verbali ausiliari.

Python - Rimozione Stopwords con NLTK e spaCy
# Approccio 1: NLTK
from nltk.corpus import stopwords
import nltk
nltk.download('stopwords')

stop_it = set(stopwords.words('italian'))
print(f"Stopwords italiane NLTK: {len(stop_it)}")
# Output: Stopwords italiane NLTK: 279

testo = "il gatto mangia il pesce sul tavolo della cucina"
tokens = testo.split()
filtrato = [t for t in tokens if t not in stop_it]
print(filtrato)
# Output: ['gatto', 'mangia', 'pesce', 'tavolo', 'cucina']

# Approccio 2: spaCy (più completo)
import spacy
nlp = spacy.load("it_core_news_lg")

doc = nlp("il gatto mangia il pesce sul tavolo della cucina")
filtrato_spacy = [token.text for token in doc if not token.is_stop]
print(filtrato_spacy)
# Output: ['gatto', 'mangia', 'pesce', 'tavolo', 'cucina']

1.3 Stemming vs Lemmatization

Entrambe le tecniche riducono le parole alla loro forma base, ma lo fanno in modi molto diversi.

Stemming vs Lemmatization - Confronto

Aspetto Stemming Lemmatization
Metodo Taglia suffissi con regole euristiche Usa un dizionario e analisi morfologica
Risultato Stem (non sempre una parola reale) Lemma (parola reale dal dizionario)
Esempio IT "mangiando" -> "mangi" "mangiando" -> "mangiare"
Velocita Molto veloce Più lenta (richiede dizionario)
Precisione Bassa (over-stemming comune) Alta (forme corrette)
Per l'italiano Snowball Stemmer (Porter italiano) spaCy it_core_news_lg
Python - Stemming e Lemmatization per l'Italiano
from nltk.stem.snowball import SnowballStemmer
import spacy

# Stemming con Snowball
stemmer = SnowballStemmer("italian")
parole = ["mangiando", "mangiare", "mangiato", "correre", "correndo", "bellissimo"]
stems = [stemmer.stem(p) for p in parole]
print(dict(zip(parole, stems)))
# {'mangiando': 'mang', 'mangiare': 'mang', 'mangiato': 'mang',
#  'correre': 'corr', 'correndo': 'corr', 'bellissimo': 'bellissim'}

# Lemmatization con spaCy
nlp = spacy.load("it_core_news_lg")
doc = nlp("Le ragazze stavano mangiando le mele più belle")
lemmi = [(token.text, token.lemma_, token.pos_) for token in doc]
for testo, lemma, pos in lemmi:
    print(f"  {testo:15s} -> {lemma:15s} ({pos})")
# le              -> il              (DET)
# ragazze         -> ragazza         (NOUN)
# stavano         -> stare           (AUX)
# mangiando       -> mangiare        (VERB)
# le              -> il              (DET)
# mele            -> mela            (NOUN)
# più             -> più             (ADV)
# belle           -> bello           (ADJ)

Per l'italiano, la lemmatization con spaCy e quasi sempre preferibile allo stemming. Il modello it_core_news_lg contiene 500.000 vettori di parole e supporta tokenizzazione, POS tagging, parsing delle dipendenze, NER e lemmatizzazione.

2. Tokenizzazione: Come le Macchine Leggono il Testo

La tokenizzazione e il processo di suddivisione del testo in unita discrete chiamate token. E il primo e più critico passo di qualsiasi pipeline NLP: la qualità della tokenizzazione influenza direttamente le prestazioni di ogni modello successivo.

Esistono tre approcci fondamentali, ognuno con vantaggi e compromessi diversi.

2.1 Word-Level Tokenization

L'approccio più intuitivo: ogni parola diventa un token.

Python - Tokenizzazione Word-Level
# Approccio naive: split per spazio
testo = "L'intelligenza artificiale cambia il mondo"
tokens_naive = testo.split()
print(tokens_naive)
# ["L'intelligenza", 'artificiale', 'cambia', 'il', 'mondo']
# Problema: "L'intelligenza" e un singolo token!

# Approccio migliore: spaCy
import spacy
nlp = spacy.load("it_core_news_lg")
doc = nlp(testo)
tokens_spacy = [token.text for token in doc]
print(tokens_spacy)
# ["L'", 'intelligenza', 'artificiale', 'cambia', 'il', 'mondo']
# spaCy gestisce correttamente le elisioni italiane

Limiti della Tokenizzazione Word-Level

  • Vocabolario enorme: Ogni parola unica richiede un'entry nel vocabolario. L'italiano ha centinaia di migliaia di forme flesse
  • Parole fuori vocabolario (OOV): Parole mai viste durante il training diventano <UNK> (sconosciute)
  • Nessuna condivisione morfologica: "mangiare", "mangiando", "mangiato" sono tre token completamente separati, senza relazione

2.2 Character-Level Tokenization

All'estremo opposto, ogni carattere diventa un token. Il vocabolario e minuscolo (26 lettere + cifre + punteggiatura), ma le sequenze diventano lunghissime.

Confronto Lunghezza Sequenza

Testo: "ciao mondo"

Word-level:   ["ciao", "mondo"]           -> 2 token
Char-level:   ["c","i","a","o"," ","m","o","n","d","o"] -> 10 token

Testo di 1000 parole:
  Word-level:  ~1.000 token
  Char-level:  ~5.000 token  (5x più lungo!)
    

La tokenizzazione a caratteri risolve il problema delle parole sconosciute (qualsiasi parola può essere rappresentata), ma le sequenze molto lunghe rendono difficile per i modelli catturare relazioni a lungo raggio nel testo.

2.3 Subword Tokenization: Il Compromesso Ottimale

La tokenizzazione subword e il metodo usato da tutti i modelli moderni (BERT, GPT, LLaMA, T5). L'idea e brillante: le parole comuni restano intere, mentre le parole rare vengono spezzate in sotto-unita (subword) che il modello ha già visto.

Algoritmi di Subword Tokenization

Algoritmo Usato Da Strategia Direzione
BPE (Byte Pair Encoding) GPT-2, GPT-3, GPT-4, LLaMA, RoBERTa Merge iterativo delle coppie più frequenti Bottom-up
WordPiece BERT, DistilBERT, ELECTRA Merge che massimizza la verosimiglianza Bottom-up
SentencePiece T5, ALBERT, XLNet, mBART Tratta il testo come stream grezzo di caratteri Indipendente da lingua
Unigram SentencePiece (opzionale), ALBERT Parte da vocabolario grande, rimuove token meno utili Top-down

Come funziona BPE (Byte Pair Encoding)

BPE parte dai singoli caratteri e iterativamente fonde le coppie più frequenti fino a raggiungere la dimensione di vocabolario desiderata.

BPE - Esempio Passo per Passo

Corpus: "basso basso bassa basso"

Passo 0 - Vocabolario iniziale (caratteri):
  b, a, s, o

Passo 1 - Coppia più frequente: (s, s) -> "ss"
  b a ss o   b a ss o   b a ss a   b a ss o

Passo 2 - Coppia più frequente: (a, ss) -> "ass"
  b ass o   b ass o   b ass a   b ass o

Passo 3 - Coppia più frequente: (b, ass) -> "bass"
  bass o   bass o   bass a   bass o

Passo 4 - Coppia più frequente: (bass, o) -> "basso"
  basso   basso   bass a   basso

Vocabolario finale: [b, a, s, o, ss, ass, bass, basso]
    

WordPiece vs BPE

WordPiece usa un approccio simile a BPE, ma invece di scegliere la coppia più frequente, sceglie quella che massimizza la verosimiglianza del corpus di addestramento. In pratica, WordPiece preferisce merge che producono token più utili per il modello linguistico, non semplicemente quelli più comuni. I token che non iniziano una parola sono prefissati con ##.

SentencePiece: Indipendenza dalla Lingua

La differenza chiave di SentencePiece e che non richiede pre-tokenizzazione. BPE e WordPiece assumono che il testo sia già diviso in parole (tipicamente per spazio), il che funziona bene per inglese e italiano, ma fallisce per lingue come cinese, giapponese o thai che non usano spazi tra le parole. SentencePiece tratta il testo come uno stream grezzo di byte, rendendolo veramente indipendente dalla lingua.

3. Esempio Pratico: Tokenizzazione con HuggingFace

Vediamo concretamente come BERT e GPT-2 tokenizzano lo stesso testo italiano. Useremo la libreria transformers di HuggingFace.

Python - Confronto Tokenizzatori BERT vs GPT-2
from transformers import AutoTokenizer

# Testo di esempio in italiano
testo = "L'intelligenza artificiale sta rivoluzionando il mondo"

# --- BERT (WordPiece) ---
bert_tok = AutoTokenizer.from_pretrained("dbmdz/bert-base-italian-cased")
bert_tokens = bert_tok.tokenize(testo)
bert_ids = bert_tok.encode(testo)
print("BERT tokens:", bert_tokens)
print("BERT IDs:   ", bert_ids)
# BERT tokens: ['L', "'", 'intelligenza', 'artificiale', 'sta',
#               'rivoluzionando', 'il', 'mondo']
# BERT IDs:    [102, 55, 7, 5765, 6892, 379, 28648, 42, 1601, 103]

# --- GPT-2 (BPE) ---
gpt2_tok = AutoTokenizer.from_pretrained("gpt2")
gpt2_tokens = gpt2_tok.tokenize(testo)
gpt2_ids = gpt2_tok.encode(testo)
print("\nGPT-2 tokens:", gpt2_tokens)
print("GPT-2 IDs:   ", gpt2_ids)
# GPT-2 tokens: ['L', "'", 'int', 'ell', 'ig', 'enza', ' art',
#                'ific', 'iale', ' sta', ' riv', 'oluz', 'ion',
#                'ando', ' il', ' mondo']

# --- Confronto ---
print(f"\nBERT:  {len(bert_tokens)} token")
print(f"GPT-2: {len(gpt2_tokens)} token")

Osservazioni Chiave

  • BERT italiano (dbmdz/bert-base-italian-cased) riconosce "intelligenza" e "rivoluzionando" come token interi, perchè il suo vocabolario e stato addestrato su testo italiano
  • GPT-2 spezza le parole italiane in molti più subword, perchè il suo vocabolario e stato addestrato principalmente su testo inglese
  • Un tokenizzatore addestrato sulla lingua target produce meno token, il che significa più contesto nella finestra di attenzione e costi minori per token
  • BERT aggiunge token speciali: [CLS] all'inizio e [SEP] alla fine. GPT-2 non lo fa
Python - Vocabolario e Token Speciali
from transformers import AutoTokenizer

tok = AutoTokenizer.from_pretrained("dbmdz/bert-base-italian-cased")

# Dimensione vocabolario
print(f"Vocabolario BERT italiano: {tok.vocab_size} token")
# Output: Vocabolario BERT italiano: 31102 token

# Token speciali
print(f"[CLS] = {tok.cls_token} (ID: {tok.cls_token_id})")
print(f"[SEP] = {tok.sep_token} (ID: {tok.sep_token_id})")
print(f"[PAD] = {tok.pad_token} (ID: {tok.pad_token_id})")
print(f"[UNK] = {tok.unk_token} (ID: {tok.unk_token_id})")
print(f"[MASK] = {tok.mask_token} (ID: {tok.mask_token_id})")

# Decodifica: da token IDs -> testo
ids = tok.encode("NLP e fantastico")
print(f"\nEncode: {ids}")
print(f"Decode: {tok.decode(ids)}")
# Decode: [CLS] NLP e fantastico [SEP]

4. Bag of Words e TF-IDF: Le Rappresentazioni Classiche

Prima dei word embeddings, il testo veniva rappresentato come vettori sparsi basati sulla frequenza delle parole. Questi metodi sono ancora usati in molti contesti e comprendere i loro limiti aiuta a capire perchè gli embeddings sono stati una rivoluzione.

4.1 Bag of Words (BoW)

Il modello Bag of Words rappresenta un documento come un vettore dove ogni posizione corrisponde a una parola del vocabolario e il valore e il numero di occorrenze di quella parola nel documento.

Python - Bag of Words con scikit-learn
from sklearn.feature_extraction.text import CountVectorizer

documenti = [
    "il gatto mangia il pesce",
    "il cane mangia la carne",
    "il gatto insegue il cane"
]

vectorizer = CountVectorizer()
bow_matrix = vectorizer.fit_transform(documenti)

print("Vocabolario:", vectorizer.get_feature_names_out())
# ['cane', 'carne', 'gatto', 'il', 'insegue', 'la', 'mangia', 'pesce']

print("\nMatrice BoW:")
print(bow_matrix.toarray())
# [[0, 0, 1, 2, 0, 0, 1, 1],   # doc 1
#  [1, 1, 0, 1, 0, 1, 1, 0],   # doc 2
#  [1, 0, 1, 2, 1, 0, 0, 0]]   # doc 3

4.2 TF-IDF (Term Frequency - Inverse Document Frequency)

TF-IDF migliora BoW pesando le parole per la loro importanza relativa. Parole frequenti in un documento ma rare nel corpus complessivo ricevono un peso maggiore. Parole comuni ovunque (come "il", "la") ricevono peso basso.

Formula TF-IDF

TF-IDF(t, d) = TF(t, d) x IDF(t)

dove:
  TF(t, d)  = frequenza del termine t nel documento d
  IDF(t)    = log(N / df(t))
  N         = numero totale di documenti
  df(t)     = numero di documenti che contengono il termine t

Esempio:
  Parola "gatto" in documento 1:
    TF  = 1/5 = 0.2    (1 occorrenza su 5 parole)
    IDF = log(3/2) = 0.405  (appare in 2 documenti su 3)
    TF-IDF = 0.2 x 0.405 = 0.081
    
Python - TF-IDF con scikit-learn
from sklearn.feature_extraction.text import TfidfVectorizer
import numpy as np

documenti = [
    "il gatto mangia il pesce",
    "il cane mangia la carne",
    "il gatto insegue il cane"
]

tfidf = TfidfVectorizer()
tfidf_matrix = tfidf.fit_transform(documenti)

print("Feature:", tfidf.get_feature_names_out())
print("\nMatrice TF-IDF (arrotondata):")
print(np.round(tfidf_matrix.toarray(), 3))
# "pesce" e "carne" hanno pesi più alti perchè appaiono
# in un solo documento (più discriminanti)

Limiti di BoW e TF-IDF

  • Nessuna semantica: "cane" e "canino" sono completamente diversi; "banca" (fiume) e "banca" (istituto) sono identici
  • Nessun ordine: "il gatto mangia il topo" e "il topo mangia il gatto" hanno la stessa rappresentazione
  • Alta dimensionalità: Un vocabolario di 100.000 parole produce vettori a 100.000 dimensioni, quasi tutti zeri (vettori sparsi)
  • Nessuna generalizzazione: Non catturano relazioni tra parole ("re" e "regina" non hanno alcuna vicinanza)

5. Word Embeddings: Il Significato Come Geometria

I word embeddings hanno rivoluzionato l'NLP trasformando le parole in vettori densi a bassa dimensionalità (tipicamente 100-300 dimensioni) che catturano le relazioni semantiche tra le parole. Due parole con significato simile avranno vettori vicini nello spazio vettoriale.

5.1 Word2Vec: L'Invenzione che ha Cambiato Tutto

Introdotto da Tomas Mikolov e colleghi (Google, 2013), Word2Vec impara i vettori delle parole a partire dal contesto in cui appaiono. L'intuizione fondamentale e la distributional hypothesis: "una parola e caratterizzata dalla compagnia che tiene" (J.R. Firth, 1957).

Due Architetture Word2Vec

Architettura Input Output Intuizione
CBOW (Continuous Bag of Words) Contesto (parole circostanti) Parola target Dato il contesto "il ___ mangia", predici "gatto"
Skip-gram Parola target Contesto (parole circostanti) Data la parola "gatto", predici "il", "mangia", ecc.

In pratica, Skip-gram funziona meglio con piccoli dataset e cattura meglio le parole rare. CBOW e più veloce e funziona bene con parole frequenti.

Schema Skip-gram

Frase: "il gatto nero mangia il pesce fresco"
                    ^
              parola target

Con window_size = 2, Skip-gram impara:
  gatto -> il      (contesto a sinistra, distanza 1)
  gatto -> nero    (contesto a destra, distanza 1)
  gatto -> mangia  (contesto a destra, distanza 2)

Dopo milioni di frasi, parole che appaiono in contesti
simili avranno vettori simili:
  gatto ~ felino ~ micio  (contesti simili: "il ___ mangia")
  cane ~ canino ~ cucciolo (contesti simili: "il ___ corre")
    

5.2 L'Aritmetica delle Parole

La proprietà più sorprendente dei word embeddings e che le relazioni semantiche diventano operazioni algebriche sui vettori. La famosa analogia:

re - uomo + donna = regina

Non e una coincidenza: il vettore che porta da "uomo" a "donna" e lo stesso che porta da "re" a "regina". Questo funziona per molte relazioni: paese-capitale, verbo-tempo passato, aggettivo-superlativo.

Python - Aritmetica dei Vettori con Gensim
import gensim.downloader as api

# Carica word embeddings pre-addestrati
model = api.load("word2vec-google-news-300")

# Analogia: re - uomo + donna = ?
result = model.most_similar(
    positive=["king", "woman"],
    negative=["man"],
    topn=3
)
print("king - man + woman =")
for word, score in result:
    print(f"  {word}: {score:.4f}")
# king - man + woman =
#   queen: 0.7118
#   monarch: 0.6189
#   princess: 0.5902

# Similarità tra parole
print(f"\ncat ~ dog: {model.similarity('cat', 'dog'):.4f}")
print(f"cat ~ car: {model.similarity('cat', 'car'):.4f}")
# cat ~ dog: 0.7609
# cat ~ car: 0.2004

5.3 GloVe: Global Vectors for Word Representation

GloVe (Stanford, 2014) prende un approccio diverso: invece di predire il contesto parola per parola come Word2Vec, GloVe costruisce prima una matrice di co-occorrenza globale di tutto il corpus, poi fattorizza questa matrice per ottenere i vettori. Combina i vantaggi dei metodi basati sulla statistica globale con quelli dell'apprendimento locale di Word2Vec.

Word2Vec vs GloVe

Aspetto Word2Vec GloVe
Metodo Predittivo (rete neurale) Count-based (fattorizzazione matrice)
Contesto Finestra locale Statistiche globali del corpus
Training Online (scorre il testo) Batch (matrice completa)
Dimensioni comuni 100, 200, 300 50, 100, 200, 300
Prestazioni Eccellente per analogie Eccellente per similarità

6. Embeddings Contestuali: La Stessa Parola, Significati Diversi

Word2Vec e GloVe hanno un limite fondamentale: assegnano un unico vettore a ogni parola, indipendentemente dal contesto. Ma il linguaggio e pieno di ambiguità: la parola "banco" ha un significato completamente diverso in "il banco di scuola" e "il banco di Napoli".

Gli embeddings contestuali risolvono questo problema: il vettore di una parola dipende dall'intera frase in cui appare. Questo e l'approccio usato da BERT, GPT e tutti i modelli basati su Transformer.

Embeddings Statici vs Contestuali

Aspetto Statici (Word2Vec, GloVe) Contestuali (BERT, GPT)
Vettore per parola Uno fisso, sempre uguale Diverso in base al contesto
Polisemia Non gestita ("banco" ha un solo vettore) Gestita ("banco" ha vettori diversi per ogni significato)
Modello Lookup table Rete neurale profonda (Transformer)
Dimensione modello Pochi MB Centinaia di MB o GB
Velocita Istantanea Richiede forward pass
Python - Embeddings Contestuali con BERT Italiano
from transformers import AutoTokenizer, AutoModel
import torch
import torch.nn.functional as F

# Carica BERT italiano
tokenizer = AutoTokenizer.from_pretrained("dbmdz/bert-base-italian-cased")
model = AutoModel.from_pretrained("dbmdz/bert-base-italian-cased")

def get_word_embedding(sentence: str, word: str):
    """Ottieni l'embedding contestuale di una parola nella frase."""
    inputs = tokenizer(sentence, return_tensors="pt")
    tokens = tokenizer.tokenize(sentence)

    with torch.no_grad():
        outputs = model(**inputs)

    # outputs.last_hidden_state: [batch, seq_len, hidden_dim]
    embeddings = outputs.last_hidden_state[0]  # [seq_len, 768]

    # Trova l'indice del token target
    word_idx = tokens.index(word) + 1  # +1 per [CLS]
    return embeddings[word_idx]

# "banco" in contesti diversi
emb_scuola = get_word_embedding("Il banco di scuola e rotto", "banco")
emb_banca = get_word_embedding("Il banco di Napoli e storico", "banco")
emb_pesce = get_word_embedding("Il banco del pesce e fresco", "banco")

# Calcola similarità coseno
sim_12 = F.cosine_similarity(emb_scuola.unsqueeze(0), emb_banca.unsqueeze(0))
sim_13 = F.cosine_similarity(emb_scuola.unsqueeze(0), emb_pesce.unsqueeze(0))
sim_23 = F.cosine_similarity(emb_banca.unsqueeze(0), emb_pesce.unsqueeze(0))

print(f"banco(scuola) ~ banco(banca): {sim_12.item():.4f}")
print(f"banco(scuola) ~ banco(pesce): {sim_13.item():.4f}")
print(f"banco(banca)  ~ banco(pesce): {sim_23.item():.4f}")
# I vettori saranno DIVERSI perchè BERT capisce il contesto!

Questo e il salto concettuale fondamentale: con BERT, la parola "banco" non ha più un significato fisso. Il suo vettore cambia in base a ciò che la circonda, proprio come accade nella comprensione umana del linguaggio.

7. Sentence Embeddings: Un Vettore per un'Intera Frase

Spesso non ci serve l'embedding di una singola parola, ma di un'intera frase o paragrafo. I sentence embeddings comprimono il significato di un testo di qualsiasi lunghezza in un singolo vettore a dimensione fissa.

Il modello di riferimento e Sentence-BERT (SBERT), che modifica l'architettura BERT per produrre sentence embeddings ottimizzati per il confronto di similarità. Per l'italiano, il modello paraphrase-multilingual-MiniLM-L12-v2 supporta 50+ lingue con vettori a 384 dimensioni.

Applicazioni dei Sentence Embeddings

Applicazione Descrizione Come Funziona
Semantic Search Ricerca per significato, non per keyword Embeddi la query, cerca i documenti più vicini
Clustering Raggruppa testi simili automaticamente K-means o HDBSCAN sugli embeddings
Duplicate Detection Trova duplicati o quasi-duplicati Soglia di cosine similarity > 0.9
Classificazione Zero-Shot Classifica senza dati di training Confronta embedding testo con embedding etichette
Python - Sentence Embeddings con sentence-transformers
from sentence_transformers import SentenceTransformer, util

# Modello multilingue (supporta italiano)
model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")

# Frasi in italiano
frasi = [
    "Il gatto dorme sul divano",
    "Il felino riposa sul sofa",
    "La borsa e salita oggi",
    "I mercati finanziari sono in crescita",
    "Ho comprato un nuovo computer portatile"
]

# Genera embeddings (1 vettore per frase, 384 dimensioni)
embeddings = model.encode(frasi, convert_to_tensor=True)
print(f"Shape: {embeddings.shape}")  # [5, 384]

# Calcola matrice di similarità
cosine_scores = util.cos_sim(embeddings, embeddings)

print("\nMatrice di Similarità:")
for i in range(len(frasi)):
    for j in range(i + 1, len(frasi)):
        print(f"  {frasi[i][:40]:40s} <-> {frasi[j][:40]:40s}")
        print(f"    Similarità: {cosine_scores[i][j]:.4f}")

# Risultati attesi:
# "gatto dorme"   <-> "felino riposa"  : ~0.85 (molto simili)
# "borsa salita"  <-> "mercati crescita": ~0.70 (correlati)
# "gatto dorme"   <-> "borsa salita"   : ~0.10 (non correlati)

8. La Pipeline NLP Moderna

Abbiamo visto i singoli componenti. Ora mettiamoli insieme per capire come funziona una pipeline NLP moderna end-to-end, quella usata da BERT, GPT e tutti i modelli basati su Transformer.

Pipeline NLP Moderna - Flusso Completo

Testo Grezzo
    |
    v
[1. TOKENIZZAZIONE]
    Input:  "L'intelligenza artificiale e fantastica"
    Output: ["L'", "intelligenza", "artificiale", "e", "fantastica"]
    |
    v
[2. ENCODING (Token -> ID)]
    Input:  ["L'", "intelligenza", "artificiale", "e", "fantastica"]
    Output: [102, 55, 5765, 6892, 15, 23456, 103]
            ^[CLS]                            ^[SEP]
    |
    v
[3. EMBEDDING LAYER]
    Input:  [102, 55, 5765, 6892, 15, 23456, 103]
    Output: Matrice [7 x 768] - un vettore 768-dim per ogni token
    |
    v
[4. TRANSFORMER ENCODER/DECODER]
    Self-Attention: ogni token "guarda" tutti gli altri
    Input:  Matrice [7 x 768]
    Output: Matrice [7 x 768] (vettori contestualizzati)
    |
    v
[5. TASK HEAD]
    Classificazione:  [CLS] embedding -> softmax -> classe
    NER:              ogni token -> etichetta entità
    Generazione:      ultimo token -> prossimo token
    QA:               posizione inizio/fine risposta
    

Il Ruolo del Token [CLS]

In BERT, il token speciale [CLS] viene inserito all'inizio di ogni input. Dopo il passaggio attraverso tutti i layer del Transformer, il suo embedding rappresenta l'intera sequenza. E usato come input per i task di classificazione (sentiment analysis, rilevamento spam, ecc.).

9. NLP per l'Italiano: Specificità e Strumenti

La lingua italiana presenta sfide uniche per l'NLP che la distinguono dall'inglese e da altre lingue. Conoscere queste secificità e essenziale per costruire sistemi NLP efficaci per l'italiano.

9.1 Sfide Linguistiche dell'Italiano

Particolarita dell'Italiano per l'NLP

Sfida Descrizione Esempio
Ricca morfologia Ogni verbo ha decine di forme coniugate "mangiare" ha 50+ forme (mangio, mangi, mangia, mangiamo...)
Elisioni e apostrofi Articoli e preposizioni si fondono "l'uomo", "dell'arte", "un'amica", "quest'anno"
Preposizioni articolate Preposizione + articolo in una parola "del" (di+il), "nello" (in+lo), "sulla" (su+la)
Accenti significativi Cambiano il significato "e" (and) vs "e" (is), "da" (from) vs "da" (gives)
Pronomi clitici Si attaccano al verbo "dammelo" (dai+me+lo), "portarglielo"
Ordine libero SVO non e obbligatorio "La torta la mangia Marco" = "Marco mangia la torta"

9.2 Modelli Pre-addestrati per l'Italiano

Principali Modelli Italiani

Modello Base Task Repository
dbmdz/bert-base-italian-cased BERT General-purpose NLP italiano HuggingFace
AlBERTo BERT Social media italiano (Twitter) HuggingFace
feel-it-italian-sentiment UmBERTo Sentiment analysis italiano MilaNLProc
feel-it-italian-emotion UmBERTo Emotion detection (gioia, rabbia, paura, tristezza) MilaNLProc
Italian-Legal-BERT BERT Testi legali italiani dlicari
DeepMount00/Italian_NER_XXL BERT Named Entity Recognition italiano HuggingFace
it_core_news_lg spaCy CNN Tokenizzazione, POS, NER, lemma, parsing spaCy

9.3 Preprocessing Specifico per l'Italiano

Python - Pipeline Preprocessing Italiano Completa
import spacy
import re

class ItalianPreprocessor:
    """Pipeline di preprocessing specifica per l'italiano."""

    def __init__(self):
        self.nlp = spacy.load("it_core_news_lg")
        # Stopwords aggiuntive regionali/informali
        self.custom_stops = {
            "cioe", "quindi", "comunque", "praticamente",
            "allora", "insomma", "magari", "ecco", "tipo",
            "boh", "mah", "vabbe", "ok", "okay"
        }

    def preprocess(self, text: str, remove_stops: bool = True,
                   lemmatize: bool = True) -> list[str]:
        """Preprocessing completo per testo italiano."""
        # 1. Normalizzazione base
        text = text.lower()
        text = re.sub(r'http\S+|www\.\S+', '', text)  # rimuovi URL
        text = re.sub(r'[^\w\s\']', '', text)  # mantieni apostrofi
        text = re.sub(r'\d+', '', text)  # rimuovi numeri
        text = re.sub(r'\s+', ' ', text).strip()

        # 2. Analisi con spaCy
        doc = self.nlp(text)

        # 3. Filtraggio e lemmatizzazione
        tokens = []
        for token in doc:
            # Salta punteggiatura e spazi
            if token.is_punct or token.is_space:
                continue
            # Salta stopwords se richiesto
            if remove_stops and (token.is_stop or
                                  token.text in self.custom_stops):
                continue
            # Lemmatizza o usa la forma originale
            word = token.lemma_ if lemmatize else token.text
            if len(word) > 1:  # salta caratteri singoli
                tokens.append(word)

        return tokens


# Esempio d'uso
prep = ItalianPreprocessor()

testo = """L'intelligenza artificiale sta rivoluzionando
il modo in cui le aziende italiane gestiscono i loro
processi, cioe praticamente tutto sta cambiando."""

risultato = prep.preprocess(testo)
print("Token processati:", risultato)
# ['intelligenza', 'artificiale', 'rivoluzionare', 'modo',
#  'azienda', 'italiano', 'gestire', 'processo', 'cambiare']

10. Esempio End-to-End: Semantic Search in Italiano

Mettiamo insieme tutto ciò che abbiamo imparato in un esempio completo: una ricerca semantica su un corpus di testi italiani. Dato un insieme di documenti e una query dell'utente, troveremo i documenti più rilevanti usando sentence embeddings.

Python - Semantic Search Completo in Italiano
from sentence_transformers import SentenceTransformer, util
import torch

class SemanticSearchIT:
    """Motore di ricerca semantica per testi italiani."""

    def __init__(self, model_name: str =
                 "paraphrase-multilingual-MiniLM-L12-v2"):
        self.model = SentenceTransformer(model_name)
        self.documents: list[str] = []
        self.embeddings = None

    def index_documents(self, documents: list[str]) -> None:
        """Indicizza i documenti calcolando gli embeddings."""
        self.documents = documents
        self.embeddings = self.model.encode(
            documents,
            convert_to_tensor=True,
            show_progress_bar=True
        )
        print(f"Indicizzati {len(documents)} documenti")
        print(f"Shape embeddings: {self.embeddings.shape}")

    def search(self, query: str, top_k: int = 3) -> list[dict]:
        """Cerca i documenti più rilevanti per la query."""
        query_embedding = self.model.encode(
            query, convert_to_tensor=True
        )
        scores = util.cos_sim(query_embedding, self.embeddings)[0]
        top_results = torch.topk(scores, k=min(top_k, len(self.documents)))

        results = []
        for score, idx in zip(top_results.values, top_results.indices):
            results.append({
                "documento": self.documents[idx],
                "score": round(score.item(), 4),
                "indice": idx.item()
            })
        return results


# --- Esempio d'uso ---
corpus = [
    "Python e un linguaggio di programmazione versatile e facile da imparare",
    "Il machine learning permette ai computer di imparare dai dati",
    "Angular e un framework per costruire applicazioni web moderne",
    "La pasta alla carbonara e un piatto tipico della cucina romana",
    "I database relazionali usano SQL per interrogare i dati",
    "Il deep learning utilizza reti neurali con molti strati nascosti",
    "Roma e la capitale d'Italia e ha una storia millenaria",
    "Le API REST permettono la comunicazione tra servizi web",
    "Il Natural Language Processing analizza e comprende il testo",
    "La pizza napoletana e patrimonio UNESCO dal 2017"
]

# Crea il motore di ricerca e indicizza
search_engine = SemanticSearchIT()
search_engine.index_documents(corpus)

# Esegui alcune ricerche
queries = [
    "come analizzare il linguaggio naturale",
    "framework per sviluppo frontend",
    "cucina tradizionale italiana"
]

for query in queries:
    print(f"\nQuery: '{query}'")
    print("-" * 60)
    results = search_engine.search(query, top_k=3)
    for i, r in enumerate(results, 1):
        print(f"  {i}. [{r['score']:.4f}] {r['documento']}")

Risultati Attesi

La ricerca semantica comprende il significato, non solo le parole. Ad esempio:

  • "come analizzare il linguaggio naturale" trovera il documento sull'NLP anche se non contiene esattamente quelle parole
  • "framework per sviluppo frontend" trovera Angular, anche se "frontend" non appare nel documento (ma "applicazioni web moderne" e semanticamente correlato)
  • "cucina tradizionale italiana" trovera sia la carbonara che la pizza, perchè il modello capisce la relazione semantica

Roadmap: Da Qui ai LLM

In questo articolo abbiamo costruito le fondamenta dell'NLP moderno, partendo dal preprocessing del testo fino agli embeddings contestuali e alla pipeline completa. Riassumiamo il percorso che abbiamo compiuto:

Riepilogo dei Concetti

Concetto Cosa Fa Evoluzione
Preprocessing Pulisce e normalizza il testo grezzo Regole manuali -> spaCy pipeline
Tokenizzazione Divide il testo in unita discrete Word -> Char -> Subword (BPE/WordPiece)
BoW / TF-IDF Rappresenta il testo come vettori sparsi Semplice ma senza semantica
Word Embeddings Vettori densi che catturano il significato Word2Vec -> GloVe -> FastText
Embeddings Contestuali Vettori che dipendono dal contesto ELMo -> BERT -> GPT
Sentence Embeddings Un vettore per un'intera frase Media pooling -> Sentence-BERT

Nel prossimo articolo faremo il salto verso l'architettura che ha rivoluzionato tutto: il Transformer. Vedremo in dettaglio il meccanismo di Self-Attention, capiremo perchè BERT e stato un punto di svolta, e impareremo a usarlo per task reali come la classificazione del testo e il question answering.

Risorse per Approfondire

  • spaCy Italian Models: Documentazione ufficiale per i modelli italiani di spaCy (spacy.io/models/it)
  • HuggingFace Models: Repository di modelli italiani pre-addestrati (huggingface.co/models?language=it)
  • Sentence-BERT: Documentazione di sentence-transformers (sbert.net)
  • FEEL-IT: Sentiment analysis e emotion classification per l'italiano (MilaNLProc)
  • Paper Word2Vec: "Efficient Estimation of Word Representations in Vector Space" (Mikolov et al., 2013)
  • Paper GloVe: "Global Vectors for Word Representation" (Pennington et al., 2014)
  • Paper BERT: "BERT: Pre-training of Deep Bidirectional Transformers" (Devlin et al., 2019)

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti