Processing math: 100%

Il classificatore Naive Bayes è storicamente uno degli algoritmi più utilizzati nella classificazione di testo, dalla spam detection, alla sentiment analysis, alla classificazione per argomento in più categorie.
Sebbene oggi sia generalmente considerato superato da classificatori più articolati, sia per il fatto che le ipotesi di indipendenza sono raramente soddifatte, sia perchè non tiene in considerazione l'ordine delle parole nel testo, l'algoritmo Naive Bayes può essere utilizzato con successo in task "tipici" per i quali si registrano performance in linea con le attese. 

Un punto a favore è sicuramente l'immediatezza e la facilità di uso e implementazione: il Naive Bayes si fonda esclusivamente sui teoremi del calcolo probabilistico e non prevede una fase di apprendimento iterativa con discesa del gradiente, bensì la semplice costruzione di una tabella relativa alla probabilità condizionata a partire dal training set.
Inoltre il classificatore può operare direttamente su parole/token testuali preprocessati, senza la necessità di fare riferimento a word embedding.

La probabilità condizionata e il teorema di Bayes

Dati due eventi A e B, si definisce probabilità condizionata P(A|B) la probabilità del verificarsi dell'evento A, condizionata al fatto che sia verificato l'evento B. Come dall'esempio in figura, è immediato verificare che questa probabilità è pari alla probabilità che i due eventi siano entrambi verificati P(AB) rapportata alla probabilità che sia verificato l'evento B.  Ovvero  P(A|B)=P(AB)P(B)

 

Il Teorema di Bayes si ricava semplicemente mettendo in rapporto  P(A|B)=P(AB)P(B)  e   P(B|A)=P(AB)P(A)  , ottenendo

P(A|B)=P(B|A) P(A)P(B)   

Questo risultato è molto importante, in quanto ci permettere di esprimere la probabilità a posteriori P(A|B) che una determinata ipotesi A (ad esempio l'appartenza ad una determinata classe) sia verificata in presenza del verificarsi di una evidenza B (ad esempio l'occorrenza di una determinata frase da classificare),  nei termini più gestibili sotto determinate assunzioni della probabilità P(B|A) che l'evidenza B sia verificata al verificarsi dell'ipotesi A,  per il rapporto tra la probabilità a priori P(A) che l'ipotesi si verifichi  e la probabilità marginale P(B) che l'evidenza si verifichi.

Naive Bayes

Una assunzione "naive" ma assai utile per molte applicazioni  è di considerare le caratteristiche dell'evidenza B perfettamente indipendenti tra loro, senza alcuna correlazione tra gli attributi di B.

Nel caso della classificazione di testo, significa assumere che data una frase B da classificare, la presenza di una qualsiasi parola all'interno della frase è indipendente rispetto alla presenza di ogni altra parola della frase.
Questa ipotesi è chiaramente una forzatura, ma semplifica notevolmente il modello e risulta accettabile per molti task di classificazione.

Esprimendo l'evidenza B, nel nostro caso il testo da classificare, in funzione delle sue caratteristiche assunte come indipendenti, ovvero le singole parole del testo, B=(w1,w2,,wn) abbiamo che P(B|A)=P(w1,w2,,wn|A)=ni=1P(wi|A)

Possiamo quindi riscrivere il teorema come P(A|B)=P(A)ni=1P(wi|A)P(B)     , dove vedremo è possibile ricavare le varie probabilità condizionate P(wi|A) semplicemente costruendo una tabella.

I vari P(A) delle classi sono facilmente determinati dal rapporto tra il numero di elementi del corpus (quindi frasi o testi) che rientrano nella classe A e il numero complessivo di elementi del corpus.

Per il task di classificazione non è necessario determinare esplicitamente P(B) in quanto è semplicemente un fattore di normalizzazione - dipendente solo dall'evidenza B; sarà sufficiente calcolare i soli numeratori per le varie classi e scegliere il maggiore.

A livello pratico, specie per dizionari molto estesi e/o frasi molto lunghe, i calcoli possono coinvolgere entità infinitesimali difficili da gestire con precisione, specie per calcolatori. Si preferisce quindi operare confrontando i logaritimi delle grandezze,  passando quindi dal calcolo di P(A)ni=1P(wi|A) a quello di logP(A)+ni=1logP(wi|A)
 

Dizionario e Tabella delle Probabilità

Partendo da un corpus già classificato di m testi in k possibili classi, l'obiettivo è quello di costruire un dizionario delle varie parole presenti nel corpus, con le relative probabilità condizionate rispetto ad ogni possibile classe, organizzate in una tabella.

Preliminarmente, in relazione agli  obiettivi del task, alla tipologia del corpus e al tipo di classificazione, si procede solitamente a un preprocessing del testo attraverso varie fasi (rimozione di punteggiatura, entità e stopword,  stemming o lemmatizzazione), in modo da operare già su token più semplici e significativi.

Successivamente si costruisce una tabella delle frequenze, conteggiando le frequenze dei token in ciascun testo del corpus e aggiungendole ovviamente nella colonna della classe attribuita.

Prendiamo ad esempio un'applicazione di sentiment analysis con 3 possibili classi (0=negativo 1=neutro 2=positivo) e supponiamo di dover aggiungere alla tabella delle frequenze, che abbiamo già popolato con 59 testi (20 positivi, 18 neutri e 21 negativi), una ultima frase "un film normale, nè bello, nè brutto" alla classe 1 (neutro).  Dopo il preprocessamento la frase è trasformata in "film normal bell brutt" e va ad aggiornare la tabella delle frequenze:

V210normal124315film333630bell63121brutt31270ved393630tram101111  V210normal124415film333730bell63131brutt31370ved393630tram101111

Per passare alla tabella delle probabilità condizionate procediamo a dividere ogni elemento della tabella delle frequenze per le frequenze totali della relativa classe.  Con la tabella cui sopra avremo:

V210normal124415film333730bell63131brutt31370ved393630tram101111tot=471160154157   P(w|A)210normal0,0750,2860,096film0,2060,2400,191bell0,3940,0840,006brutt0,0190,0840,446ved0,2440,2340,191tram0,0630,0710,070

Infine per trovare la P(A) di una classe sarà sufficiente dividere il numero degli elementi del corpus della classe per il totale complessivo di tutti gli elementi nel corpus.

A210P(A)2060=0,3331960=0,3172160=0,35
 


Esempio di classificazione

A partire dalle tabelle di probabilità ricavate e dalla formula derivata dal Teorema di Bayes, risulta immediato classificare una nuova frase. Prendiamo ad esempio il testo "Film interessante, trama normale ma bello da vedere", preprocessato come "film interess tram normal bell ved".   Escludendo le parole non nel dizionario costruito (quindi ignoriamo "interess"), e utilizzando la più comoda tabella dei logaritmi delle probabilità riportata sotto, procediamo con i calcoli:

logP(w|A)210normal2,5901,2532,348film1,5791,4261,655bell0,9322,4725,056brutt3,9772,4720,808ved1,4121,4531,655tram2,7732,6392,658logP(A)1,0991,1501,050

logP(A|frase)=logP(A)+ni=1logP(wi|A)  +c    , dove c è il termine invariante relativo a P(B)

logP(2|frase)=1,0991,5792,7732,5900,9321,412+c=10,385+clogP(1|frase)=1,1501,4262,6391,2532,4721,453+c=10,393+clogP(0|frase)=1,0501,6552,6582,3485,0561,655+c=14,422+c

Ne concludiamo quindi che il testo sarà classificato nella classe 2 (positivo) con probabilità maggiore. 

Smoothing laplaciano

Dalla tabella delle probabilità condizionate, ancora più se in forma logaritmica, osserviamo che può verificarsi una situazione patologica nel caso in cui la frequenza di una parola in una determinata classe sia pari a 0,  cosa che di conseguenza azzera anche P(wi|A)  e quindi P(A|B) .  L'impatto dell'assenza di osservazioni della parola nella classe, a prescindere magari da un numero elevato di osservazioni delle altre parole della frase nella stessa classe, avrebbe un effetto estremamente distorsivo - specie su corpus quantitativamente limitati.
Per ovviare a ciò si procede al calcolo della tabella probabilità aggiungendo 1 ad ogni frequenza osservata, introducendo quindi un moderato effetto di "livellamento" tra le probabilità dei token in una classe, ma eliminando la distorsione connessa ad eventuali frequenze nulle.
In pratica P(wi|A)=Freq(wi|A)+1|V|+|V|j=1Freq(wj|A)
Vediamo come si modificano le tabelle dell'esempio cui sopra introducendo lo smoothing laplaciano:
˜P(w|A)210normal0,0780,2810,098film0,2050,2380,190bell0,3860,0880,012brutt0,0240,0880,436ved0,2410,2310,190tram0,0660,0750,074   log˜P(w|A)210normal2,5471,2692,321film1,5861,4381,660bell0,9532,4364,401brutt3,7262,4360,831ved1,4231,4641,660tram2,7142,5902,609
Come atteso, aumentano le probabilità che corrispondono a frequenze minori, a discapito di quelle relative a frequenze maggiori.

Classificazione binaria Naive Bayes

Nel caso di classificazione binaria tra due classi, è possibile semplificare ulteriormente l'algoritmo di calcolo, costruendo a partire probabilità condizionata, una tabella con un solo parametro di decisione per ogni token.
In questo caso infatti la condizione per l'attribuzione della classe positiva è semplicemente P(pos|frase)P(neg|frase)1 , altrimenti è ovviamente attribuita la classe negativa.

Per il teorema di Bayes, applicando l'ipotesi Naive, questa grandezza è uguale a P(pos)ni=1P(wi|pos)P(neg)ni=1P(wi|neg)=P(pos)P(neg)ni=1P(wi|pos)P(wi|neg).

In questo modo è possibile associare ad ogni token l'unico valore caratterizzante P(wi|pos)P(wi|neg) che indica, se >1 un contributo della parola alla classificazione "positiva" della frase, se <1 un contributo alla classificazione negativa della parola.

Anche in questo caso, si preferisce lavorare con grandezze logaritimiche riducendo le operazioni di calcolo a sommatorie, trasformando la condizione di classificazione positiva in:

(logP(pos)logP(neg))+ni=1[logP(wi|pos)logP(wi|neg)]0  , associando quindi a ogni token widel nostro dizionario la sola grandezza logP(wi|pos)logP(wi|neg) ,  il cui segno ci indicherà se contribuisce in sommatoria alla classificazione positiva o negativa.

Rimane ovviamente da aggiungere anche il termine generale logP(pos)logP(neg)  che esprime il bilanciamento del training set tra il numero delle frasi positive e il numero delle frasi negative.

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.

il film è bello , mi è piaciuto molto ! 

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.
 
il film che @Enrico ha scelto su https://www.filminteressanti.it mi è 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').

il film che hai scelto mi è 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()

 pessimo film recitato male  pessim film recit 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.

 evitate soldi buttati  evitare soldo 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:

V10film4346bell636brutt372ved3936tram1011  V10film4446bell656brutt372ved4036tram1111  

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=[36+6+46+6+1140+65+44+65+11]=[105225].

Più in generale x=[x0x1]=[wFreq(w,0)wFreq(w,1)] 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 ww=[w0w1]  e b del modello di previsione  ˆy=σ(wwxx+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 σ(z)=11+ez  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=ylogˆy(1y)log(1ˆ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 ˆy=1y)

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 1m:

J(ww,b)=1mmi=1[y(i)logˆy(i)+(1y(i))log(1ˆy(i))]            =1m[yylogˆyˆy+(1yy)log(1ˆyˆy)]

dove per comodità si definiscono i vettori riga yy e ˆyˆ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 X di dimensione 2 x m ,  il calcolo del gradiente della funzione J rispetto al vettore w e allo scalare b risulta:

dww=1mX(ˆyˆyyy)  e   db=1mmi=1(ˆy(i)y(i))

A questo punto iterando con l'algoritmo della discesa del gradiente, riusciamo ad ottenere progressivamente valori di ww 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  ww e b la predizione della classe per un testo qualsiasi è immediata.
Effettuato il preprocessing è sufficiente determinare il vettore xx di rappresentazione del testo in base alla tabella delle frequenze nota, ignorando ogni token non presente in tabella, ed applicare il modello ˆy=σ(wwxx+b).

Nel caso ˆy0.5 classificheremo il testo nella classe 1, mentre nel caso ˆ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 (w1,w2,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.

©2023 - me@enricogiannini.com -