Il modello in oggetto, sebbene estremamente semplificato, consente una prima classificazione binaria di un testo in input a partire da un corpus di addestramento già classificato, lavorando direttamente sul dato testuale senza l'utilizzo di word embedding.
Le applicazioni sono quelle tipiche della classificazione binaria: sentiment analysis, rilevazione dello spam e ogni altro task nel quale sia necessario inquadrare uno specifico testo tra due possibili classi.
Chiaramente l'efficacia del sistema, viste le notevoli semplificazioni, è da valutarsi in base al grado di precisione che si ritiene soddisfacente per l'obiettivo prefissato.

Preprocessing e costruzione del dizionario

Uno step preliminare molto importante, sia per il corpus destinato all'addestramento del modello, sia per gli input da classificare, è quello del preprocessamento.

Questo passaggio ha l'obiettivo di sintetizzare e trasformare il dato testuale, escludendo una buona parte delle componenti non funzionali al fine della classificazione, e cercando per quanto possibile di trattare in modo unitario parole che declinano lo stesso significato  - il tutto facendo leva sul singolo dato della singola parola/token, considerato che il modello attribuisce esclusivamente rilevanza alla presenza di parole all'interno del testo, ma non all'ordine.

Sebbene il preprocessamento sia articolato secondo fasi tipiche, è bene evidenziare che non vi è una strategia univoca che garantisce il miglior risultato - la scelta del processo più adeguato è infatti da contestualizzare agli obiettivi del task, alla tipologia e alla sintassi rilevabile nel corpus da trattare.

Vediamo in dettaglio alcuni dei passaggi più tipici del preprocessing:

Punteggiatura

La rimozione della punteggiatura è opportuna praticamente in tutti i casi, vista l'irrilevanza nel modello dell'ordine delle parole.

\(\fbox{il} \ \fbox{film} \ \fbox{è} \ \fbox{bello} \ \bbox[5px,border:2px solid red]{,} \ \fbox{mi} \ \fbox{è} \ \fbox{piaciuto} \ \fbox{molto} \ \bbox[5px,border:2px solid red]{!} \ \)

Entità

La rimozione di entità codificate (url, email, valori numerici, date, etc.) individuabili tramite pattern di caratteri, quando non siano ritenute utili al fine della classificazione.
 
\(\fbox{il} \ \fbox{film} \ \fbox{che} \ \bbox[5px,border:2px solid red]{\text{@Enrico}} \ \fbox{ha} \ \fbox{scelto} \ \fbox{su} \ \bbox[5px,border:2px solid red]{\text{https://www.filminteressanti.it}} \ \fbox{mi} \ \fbox{è} \ \fbox{piaciuto} \ \)

Stopword

Le parole utilizzate per la mera strutturazione sintattica della frase (congiunzioni, articoli, preposizioni, ausiliari, pronomi, aggettivi possessivi, etc.), con scarso apporto di significato intrinseco, vengono generalmente inquadrate come potenziali stopword da rimuovere, specie in applicazioni nel quale l'ordine delle parole non assume rilevanza.
E' comunque bene ripetere che la scelta delle stopword non è univoca, ma deve essere contestualizzata e verificata in relazione alla particolare applicazione e alla specifica tipologia del corpus - effettuando se del caso gli opportuni tuning.
Attualmente su Python è possibile fare riferimento a diverse librerie e framework per il NLP come ad esempio NLTK, Gensim, SpiCy che offrono classi e funzioni con stopword predefinite nelle varie lingue - per quanto riguarda NLTK ad esempio abbiamo una lista di stopword in Italiano accessibili da nltk.corpus.stopwords.words('italian').

\(\bbox[5px,border:2px solid red]{\text{il}} \ \fbox{film} \ \bbox[5px,border:2px solid red]{\text{che}} \ \bbox[5px,border:2px solid red]{\text{hai}} \ \fbox{scelto} \ \bbox[5px,border:2px solid red]{\text{mi}} \ \bbox[5px,border:2px solid red]{\text{è}} \ \fbox{piaciuto} \ \)

Stemming

Specialmente nell'elaborazione diretta di informazione testuale, parole con stessa radice ma desinenza diversa, determinata dal genere e dal numero per quanto riguarda nomi e aggettivi, oppure dal modo, dal tempo o dalla persona per quanto riguarda i verbi,  possono essere opportunamente troncate in modo da ricondurle ad una unica radice.
Ciò permette di avere una rappresentazione più corretta dell'utilizzo di diverse parole che in pratica sono portatrici della stessa informazione. 
Ovviamente ciò è possibile quando è effettivamente indentificabile una radice di caratteri coincidente - senza ricomprendere quindi forme irregolari o particolarmente articolate.
Anche in questo caso i principali framework di NLP offrono la relativà funzionalità, ad esempio per quanto riguarda la lingua italiana NLTK  offre la classe  nltk.stem.snowball.ItalianStemmer()

\(\ \bbox[3px,border:1px solid black]{pessim \color{red}{o} } \ \fbox{film} \ \bbox[3px,border:1px solid black]{recit\color{red}{ato}} \ \fbox{male} \ \rightarrow \ \fbox{pessim} \ \fbox{film} \ \fbox{recit} \ \fbox{male} \)

Lemmatizzazione

Dove assuma rilevanza la massima convergenza delle parole verso un lemma comune in grado di coglierne il significato, è possibile utilizzare in alternativa la lemmatizzazione, ovvero la riduzione di una forma flessa di una parola alla sua forma canonica.
Mentre lo stemming è limitato dalla esatta identificazione della radice testuale, che in forme verbali irregolari può risultare ambigua o inefficace (es:  vado andiamo), la lemmatizzazione opera perciò ad un livello di complessità superiore attraverso l'analisi pregressa di corpus e regole linguistiche, mirando a ricongiungere la parola alla forma canonica (es: furono -> essere).
A livello pratico questa complessità si traduce, specialmente per lingue diverse dall'inglese storicamente ben supportate, nella necessità di verificare se la precisione e affidabilità delle specifiche implementazioni risulta adeguata agli obiettivi dell'applicazione.
Per la lingua italiana indichiamo ad esempio:
- il package pattern sviluppato dal CLiPS che permette, tra le molteplici funzioni, anche una lemmatizzazione tramite la funzione parse;
- il framework spaCy , previo caricamento di un core module della lingua italiana, offre la lemmatizzazione come attributo lemma_ di ogni token.

\(\ \bbox[3px,border:1px solid black]{evit \color{red}{ate} } \ \bbox[3px,border:1px solid black]{sold\color{red}{i}} \ \bbox[3px,border:1px solid black]{butt\color{red}{ati}} \ \rightarrow \ \fbox{evitare} \ \fbox{soldo} \ \fbox{buttare}\)

Tabella delle frequenze

Dopo aver effettuato il preprocessing del testo del corpus di training, si procede a costruire un tabella con \(|V|\) righe, una per ogni token del corpus trattato, e 2 colonne, una  per la classe 1 e per una la classe 0.
Per ogni frase del corpus, si procederà a conteggiare i token utilizzati ed incrementare in modo corrispondente il valore delle righe relative, ovviamente nella colonna in cui risulta classificata la frase.

Come esempio, immaginiamo di sviluppare un'applicazione di sentiment analysis binaria (quindi classi 1 = "ok" e 0 = "ko") e di star costruendo la tabella delle frequenze sul corpus di training.
Inseriamo la frase "Da vedere! Bello il film, bella la trama!", preprocessata come "ved bell film bell tram", e chiaramente classificata positivamente nella classe 1:

\(\begin{array}{r|rr} V & 1 & 0 \\ \hline \vdots & \vdots & \vdots \\ \text{film} & 43 & 46 \\ \text{bell} & 63 & 6 \\ \text{brutt} & 3 & 72 \\ \text{ved} & 39 & 36 \\ \text{tram} & 10 & 11 \\ \vdots & \vdots & \vdots \end{array} \ \Rightarrow \ \begin{array}{r|rr} V & 1 & 0 \\ \hline \vdots & \vdots & \vdots \\ \text{film} & \color{red}{44} & 46 \\ \text{bell} & \color{red}{65} & 6 \\ \text{brutt} & 3 & 72 \\ \text{ved} & \color{red}{40} & 36 \\ \text{tram} & \color{red}{11} & 11 \\ \vdots & \vdots & \vdots \end{array} \)  

Costruita la tabella con tutte le frasi/documenti del corpus di training, otterremmo proprio una tabella con le frequenze dei vari token nelle varie classi.

Rappresentazione di un documento

Il modello ci permette di rappresentare in modo semplificato ogni documento o frase come un vettore di dimensione pari al numero delle classi, le cui componenti sono la somma delle relative frequenze dei token in tabella.
Ad esempio, supponendo di considerare definitiva la tabella sopra riportata, avremo che la frase "ved bell film bell tram" sarà rappresentata come \(x= \begin{bmatrix} 36 +6+46+6+11 \\ 40+65+44+65+11 \end{bmatrix} = \begin{bmatrix} 105 \\ 225 \end{bmatrix} \).

Più in generale \(x = \begin{bmatrix} x_0 \\ x_1 \end{bmatrix} = \begin{bmatrix} \sum_{w} Freq(w,0) \\ \sum_{w} Freq(w,1) \end{bmatrix}\) dove la somma è effettuata su tutti i \(w\) token del documento da rappresentare.

Regressione logistica

A questo punto,  individuato un criterio per rappresentare in forma vettoriale i documenti del nostro training set e ogni altro input che potrà essere dato al modello, procediamo ad addestrare un modello minimale di regressione logistica , che riepiloghiamo sinteticamente.

L'obiettivo è individuare i parametri \(\pmb w=\begin{bmatrix} w_0 \\ w_1 \end{bmatrix}\)  e \(b\) del modello di previsione  \(\hat{y} = \sigma(\pmb w^\top \pmb x + b)\)  che riescano ad approssimare meglio,  per ogni frase del training set, la classe effettiva \(y\)  (che dovrà essere alternativamente 0 o 1).

Ricordiamo che la funzione sigmoide \(\sigma(z)=\frac{1}{1+e^{-z}}\)  restituisce ovviamente un valore compreso tra 0 e 1.

La funzione di costo che meglio rappresenta questo obiettivo è l'entropia incrociata binaria che per un singolo testo del training set è \(L=- y \log \hat {y} - (1-y)\log {(1-\hat y)}\)  , difatti nel caso predizione e valore reale della classe coincidano abbiamo L=0, altrimenti L aumenta sino a diventare teoricamente infinita se il valore della predizione è completamente errato (caso \(\hat y=1-y\))

La funzione obiettivo globale da minimizzare rispetto ai parametri del modello, deve essere chiaramente estesa a tutti gli \(m\) testi del training set e normalizzata per un fattore \(\frac{1}{m}\):

\(\displaystyle J(\pmb w,b)=- \frac{1}{m} \sum_{i=1}^m [y^{(i)} \log \hat {y}^{(i)}+ (1-y^{(i)})\log {(1-\hat y^{(i)})}] \\ \ \ \ \ \ \ \ \ \ \ \ \ = -\frac{1}{m} [ \pmb y \log \pmb{\hat{y}}^\top + (1-\pmb y) \log (1-\pmb{\hat{y}})^\top]\)

dove per comodità si definiscono i vettori riga \(\pmb y\) e \(\pmb {\hat y}\) di dimensione 1 x m con tutte le classi reali e le previsioni sul set di training. 
Collocando tutti gli \(m\) campioni del training set nelle colonne di una matrice \(\bf X\) di dimensione 2 x m ,  il calcolo del gradiente della funzione J rispetto al vettore w e allo scalare b risulta:

\( \text d\pmb w=\frac{1}{m}\bf X(\pmb{\hat y}-\pmb{y})^{\top} \)  e   \(\text d b=\frac{1}{m}\sum_{i=1}^{m}(\hat y^{(i)}-y^{(i)})\)

A questo punto iterando con l'algoritmo della discesa del gradiente, riusciamo ad ottenere progressivamente valori di \(\pmb w\) e \(b\) sempre migliori, riducendo ogni volta il valore della funzione obiettivo globale sino al raggiungimento di un livello di errore ritenuto soddisfacente.

Applicazione del modello e predizione

Una volta determinati i parametri  \(\pmb w\) e \(b\) la predizione della classe per un testo qualsiasi è immediata.
Effettuato il preprocessing è sufficiente determinare il vettore \(\pmb x\) di rappresentazione del testo in base alla tabella delle frequenze nota, ignorando ogni token non presente in tabella, ed applicare il modello \(\hat{y} = \sigma(\pmb w^\top \pmb x + b)\).

Nel caso \(\hat y \ge 0.5\) classificheremo il testo nella classe 1, mentre nel caso \(\hat y < 0.5\) classificheremo il testo nella classe 0.

Limiti del modello

L'estema semplicità del modello, nel quale abbiamo ogni input rappresentato solo da 2 dimensioni (frequenze della classe 1 e della classe 0), e solo 3 parametri da apprendere (\(w_1 ,w_2,b\)),  ci fa intuire che l'utilizzo è da destinarsi a task che si rilevano non eccessivamente complessi per i quali il modello è in grado di fornire un livello di errore accettabile.

Come dalla figura seguente, la regressione logistica presenta una frontiera di decisione lineare, in grado di rappresentare bene realtà nelle quali vi è una separazione quasi-lineare tra le classi, senza pattern eccessivamente complessi che necessitano di altri modelli.  Per le applicazioni indicate all'inizio, con classi generalmente correlate linearmente alla frequenza di token caratterizzanti, i risultati sono spesso accettabili.

Evoluzioni del modello

Il modello presentato può evolversi con modesti sforzi implementativi in 2 direzioni:

1) Modello di classificazione multiclasse con SoftMax: il modello consente di associare all'input 1 classe tra k arbitrarie, modificando il layer di uscita che sarà costituito da k uscite che rappresenteranno una distribuzione di probabilità sulle k possibili classi. Chiaramente il modello apprenderà un maggior numero di parametri (3 x k).
Il semplice utilizzo del SoftMax non modifica comunque la linearità delle frontiere che andranno a suddividere lo spazio dei possibili input.

2) Modello di rete neurale con aggiunta di layer nascosto: il modello prevede l'inserimento di un layer nascosto tra gli ingressi e l'uscita, con una relativa funzione di attivazione che permetta di generare una frontiera più articolata e non lineare.
Sebbene ciò permetta di rappresentare realtà più complesse c'è un rischio elevato - specie con ingressi a dimensioni ridotte come in questo caso - di overfit del modello: in pratica si rischia di ottenere un modello estremamente aderente alla realtà del training set con un errore bassissimo o nullo, ma con alti errori di previsione quando deve generalizzare su altri input . 
In questo è caso è essenziale la suddivisione del corpus in training set e validation/test set, oltre all'utilizzo di meccanismi di regolarizzazione, per ottimizzare la capacità di generalizzare del modello.

GloVe: un modello efficiente per word embedding

Mercoledì, 22 Luglio 2020 | NLP |

Con il paper GloVe - Global Vectors for Word Representation (C.D.Manning, R.Socher, J.Pennington. 2014) viene presentato un modello di word embedding con i benefici sia dei metodi Latent Semantic Analysis basati su matrici statistiche globali di co-occorrenza, particolarmente efficaci nel cogliere la similarità tra parole, sia dei modelli iterativi in ottica deep-learning come Word2Vec (CBoW/Skip-Gram) basati sulla capacità di predizione della parola/contesto, efficaci nel cogliere analogie e strutture complesse.

Rapporto tra probabilità, Modello bilineare logaritmico e funzione obiettivo di GloVe

Dato un corpus testuale, dalla definizione di matrice di co-occorrenza \(X\) , l'elemento \(X_{ij}\) rappresenta il numero di volte che la parola \(j\) appare nel contesto della parola \(i\).
Di conseguenza \(\displaystyle X_i=\sum_{k \in corpus} X_{ik} \)  rappresenta il numero di volte che una qualsiasi parola appare nel contesto di \(i\).
\(\displaystyle P_{ij}=(w_j|w_i)= \frac{X_{ij}}{X_i} \) è quindi la probabilità che la parola \(j\) appaia nel contesto di \(i\).

Gli autori del paper hanno osservato, come dall'esempio a seguire, che il rapporto tra probabilità di co-occorrenza può essere utilizzato proficuamente per rilevare componenti e "direttrici" di significato rilevanti:

\(\displaystyle \begin{array}{|c|c|c|c|c|} \hline & x=\text{solido} & x=\text{gas} & x=\text{acqua} & x=\text{moda} \\ \hline P(x|\text{ghiaccio}) & \color{red}{1.9 \times 10^{-4}} & \color{blue}{6.6 \times 10^{-5}} & \color{red}{3.0 \times 10^{-3}} & \color{blue}{1.7 \times 10^{-5}} \\ \hline P(x|\text{vapore}) & \color{blue}{2.2 \times 10^{-5}} & \color{red}{7.8 \times 10^{-4}} & \color{red}{2.2 \times 10^{-3}} & \color{blue}{1.8 \times 10^{-5}} \\ \hline \frac{P(x|\text{ghiaccio})}{P(x|\text{vapore})} & \color{red}{\mathbf {8.9}} & \color{blue}{\mathbf {8.5 \times 10^{-2}}} & \color{grey}{\mathbf {1.36}} & \color{grey}{\mathbf {0.96}} \\ \hline \end{array}\)

In questo esempio si rileva che "acqua" e "moda" sono due parole non discriminative in questo contesto rispetto alle parole "ghiaccio" e "vapore", avendo un rapporto tra probabilità di co-occorrenza vicino ad 1.
Invece la parola "solido", con un rapporto molto maggiore di 1, è correlabile con proprietà specifiche di "ghiaccio", mentre la parola "gas" con un rapporto molto minore di 1, è correlabile con proprietà specifiche di "vapore".
Da questo tipo di considerazione, nasce l'esigenza di strutturare un modello di embedding in grado di cogliere il significato contenuto nel rapporto di probabilità.
La soluzione individuata è quella di scegliere un modello bilineare-logaritmico tale che \(w_i \cdot \tilde w_j=\log P(i|j)\)

A questo punto si verifica che il rapporto di probabilità è riportato a una differenza di vettori  \(\displaystyle w_x \cdot (\tilde w_a - \tilde w_b)=\log \frac{P(x|a)}{P(x|b)}\) che ben riflette il significato ricercato - intuitivamente, ad esempio nel caso di "moda" o "acqua", avremo degli embedding pressochè ortogonali alla direttrice degli embedding "ghiaccio"-"vapore", e quindi a prodotto scalare tendenzialmente nullo (che corrisponde ad un rapporto di probabilità unitario).

A partire da questa relazione cerchiamo di capire la struttura di una funzione di costo che ci consenta di ottimizzare i vettori. 
Partendo da  \(w_i^\top \tilde w_j=\log P(i|j)=\log \frac{X_{ij}}{X_i}=\log X_{ij} - \log X_i \)     otteniamo che

\(w_i^\top \tilde w_j + \log X_i =\log X_{ij} \)  e considerando che \(X_i \) non dipende dalla parola j può essere ricompreso in uno scalare di bias \(b_i\) da apprendere. Aggiungeremo inoltre, per salvaguardare la simmetria, un ulteriore scalare di bias \(\tilde b_j\) da apprendere relativo a \(\tilde w_j\).

Otterremo quindi una relazione  \(w_i^\top \tilde w_j + b_i + \tilde b_j=\log X_{ij} \)  che cercheremo di ottimizzare complessivamente per ogni i,j .
In via generale dovremmo tenere conto del caso in cui l'occorrenza \(X_{ij}\) sia nulla - evenienza molto frequente, considerando la sparsità della matrice di co-occorrenza - considerando eventualmente \(X_{ij} + 1\)
Il problema è comunque superato proprio dalla scelta della funzione obiettivo di GloVe, per la quale si utilizza il metodo dei minimi quadrati e moderata da una funzione di ponderazione \(f(X_{ij})\) nulla per \(X_{ij}=0\)

\(\displaystyle \bbox[5px,border:2px solid red] {J=\sum_{i,j=1}^V f(X_{ij})( w_i^\top \tilde w_j + b_i + \tilde b_j - \log X_{ij} )^2} \)

Nel dettaglio la \(f(X_{ij})\) sarà scelta monotona crescente con \(f(0)=0\) e relativamente contenuta per valori elevati dell'argomento , in modo da non dare eccessivo peso a co-occorrenze particolarmente frequenti.
Nel paper originale è stata utilizzata con buoni risultati la funzione
\(f(x)=\begin{cases} (x/x_\text{max})^\alpha & \text{se} \ \ x<x_\text{max} \\ 1 & \text{altrimenti} \end{cases}\)   con un \(\alpha=3/4\) e un un cutoff \(x_\text{max}=100\)  per non attribuire eccessivo peso a occorrenze comuni

 

Da Word2Vec a GloVe: denormalizzazione della funzione di costo e minimi quadrati

Vediamo come un risultato analogo può essere riscontrato partendo invece dal modello Word2Vec e introducendo la matrice di co-occorrenza per integrare e modificare le relazioni e la funzione di costo.
Riprendendo le considerazioni sugli word embedding del modello Word2Vec / Skipgram, la probabilità che una parola \(j \) compaia nel contesto della parola \(i\) viene definita come \(Q_{ij}=\frac{e^{u_j^T v_i}}{\sum_{w=1}^W e^{u_w^T v_i}}\)

In questo caso la funzione di costo globale in accordo alla cross entropy è \(\displaystyle J=-\sum_{i \in corpus}\sum_{j \in context(i)} \log Q_{ij}\)  

Utilizzando convenientemente la matrice di co-occorrenza otteniamo \(J=- \sum_{i=1}^W \sum_{j=1}^W X_{ij} \log Q_{ij}\)

che può essere espresso come \(J=- \sum_{i=1}^W X_i \sum_{j=1}^W P_{ij} \log Q_{ij}=- \sum_{i=1}^W X_i H(P_i,Q_j)\)  ovvero una sommatoria pesata della cross entropy tra le distribuzioni di probabilità degli word embedding e quella della matrice di co-occorrenza.

Uno dei problemi operativi legati all'utilizzo della cross entropy è il fatto che ogni distribuzione Q deve essere normalizzata sull'intero dizionario. L'approccio di GloVe è quello di utilizzare invece una funzione di costo basata invece sul modello dei minimi quadrati, senza la normalizzazione delle distribuzioni P e Q. Abbiamo quindi:

\(\displaystyle \hat{J}=\sum_{i=1}^W \sum_{j=1}^W X_i(\hat{P}_{ij}-\hat{Q}_{ij})^2\)  dove \(\hat P_{ij}=\hat X_{ij} \ \ , \ \ \hat Q_{ij}=e^{u_j^\top v_i}\)  sono valori non normalizzati.  Per ridurre il rischio di valori esponenzialmente alti nella funzione di costo, si preferisce ottimizzare il quadrato della differenza dei logaritmi, ovvero:

\(\displaystyle \hat{J}=\sum_{i=1}^W \sum_{j=1}^W X_i(\log\hat{P}_{ij}-\log \hat{Q}_{ij})^2=\sum_{i=1}^W \sum_{j=1}^W X_i(u_j^\top v_i -\log X_{ij})^2\)

Infine, come già visto sopra, è dimostrato che si ottengono migliori risultati utilizzano un fattore di peso più articolato che il semplice \(X_i\) , utilizzando una funzione \(f(X_{ij})\) che tiene in considerazione anche la parola di contesto, e aggiungendo un parametro di bias \(b_i \) per la parola centrale ed uno \(\tilde b_j \) per la parola di contesto. Nella sua forma finale la funzione di costo, riprendendo sia i termini di bias che la funzione di ponderazione, è proprio nella forma già vista

\(\displaystyle \hat{J}=\sum_{i=1}^W \sum_{j=1}^W f(X_{ij})(u_j^\top v_i + b_i + \tilde b_j -\log X_{ij})^2\)
 

Considerazioni finali: efficienza nel training e risultati

I benefici del modello GloVe si ritrovano, oltre che nelle performance generalmente superiori nei task usuali (similitudini, analogie, NER) riportate nel paper ufficiale, in un training più efficiente dei parametri del modello legato ad una funzione obiettivo più agevole da ottimizzare.
La funzione è difatti condizionata da una matrice di co-occorrenza ad alta sparsità, con una computazione più snella e mirata rispetto a Word2Vec nel quale l'utilizzo di softmax prevedeva un esubero computazionale per la normalizzazione - tale da necessitare nella pratica di varianti quali il Negative Sampling per efficientare il calcolo.
 

__________________________________________

Riferimenti

http://nlp.stanford.edu/pubs/glove.pdf 

http://web.stanford.edu/class/cs224n/readings/cs224n-2019-notes02-wordvecs2.pdf