Questo post è il primo di una serie denominata “Fondamenti”. L’obiettivo della serie è fornire una panoramica delle principali tecniche utilizzate per estrarre automaticamente conoscenza da testo non strutturato, come pagine web, email, forum e documenti in generale.

Il focus principale è sul topic modeling, ossia l’approccio semantico per identificare gli argomenti di documenti attraverso l’analisi della distribuzione delle parole. Il topic modeling è una delle tante applicazioni del text mining1 e si fonda su algoritmi di apprendimento che suddividono la collezione di documenti in raggruppamenti ciascuno facente riferimento ad un certo topic o argomento in senso generale. L’individuazione dei raggruppamenti avviene in modo automatico senza ausilio di addestramenti basati su esempi e quindi senza una preventiva supervisione da parte dell’uomo: il topic modeling rientra pertanto nella classe dei metodi di apprendimento non supervisionati su dati testuali.

Solitamente, i suddetti argomenti richiedono una competenza multidisciplinare e i materiali per lo studio, quando disponibili in Italiano, tendono ad approfondire aspetti teorici tralasciando gli impieghi pratici. Scopo della serie “Fondamenti” è riassumere i concetti chiave, fornendo un apparato nozionistico ridotto ai fondamenti e focalizzando l’impiego pratico attraverso esempi concreti su una estesa varietà di ambienti di programmazione (tra cui, in primis, Python e R) e librerie specializzate (e.g. scikit-learn, gensim, NLTK, Pattern). L’obiettivo della serie è anche suggerire un percorso attraverso una molteplicità di strumenti tecnici, lasciando poi al lettore ogni approfondimento e applicazione ai suoi casi particolari.

In questo post, introdurremo gli elementi per giungere alla piena comprensione del concetto di modello di rappresentazione dei documenti di una collezione (corpus). Il modello di rappresentazione è l’elemento centrale attorno al quale si sviluppa ogni processo di analisi automatica del testo.


Un corpus (il plurale è corpora) è un insieme di testi confrontabili tra di loro ed appartenenti ad uno stesso contesto. Considereremo un testo come una sequenza di frasi e una frase come una sequenza di token2. Nelle lingue segmentate come l’Italiano, un modo per estrarre i token consiste nell’usare gli spazi come delimitatori dei token stessi3. L’algoritmo di estrazione dei token è chiamato tokenizzatore ed esistono molteplici implementazioni ciascuna basata su specifici criteri di estrazione.

Un tipo particolare di token sono le parole testuali (word token) che possono denotare: un oggetto (sostantivo), un’azione o uno stato (verbo), una qualità (aggettivo, avverbio), una relazione (preposizione). Altri tipi di token sono per esempio: date, numeri, valute, titoli, sigle, abbreviazioni. Dunque le parole testuali sono da considerare come un sottoinsieme dei possibili token estraibili da un testo4.

Tecnicamente considereremo un testo come una sequenza di caratteri in codifica Unicode UTF-8 (tipo stringa in Python) e un corpus come una sequenza ordinata di stringhe ognuna identificata da un indice numerico (il tipo lista in Python).

A titolo esemplificativo, definiamo il seguente corpus a cui in seguito ci riferiremo con il nome di rawcorpus1:

rawcorpus1=["La volpe voleva mangiare l´uva", "L´uva era troppo in alto per la volpe", "La volpe non riusciva a raggiungere l´uva", "La volpe rinunciò sostenendo che l´uva non era ancora matura", 'La volpe era furba, ma a volte la furbizia non paga']

In queste note assumiamo che i documenti del corpus siano già stati sottoposti a text cleaning, ossia siano stati ripuliti da tutti gli elementi che potrebbero alterarne le successive elaborazioni: si è eseguita una spoliazione dei formati di gestione del testo (XML o altro). Per esempio, i testi sorgenti potrebbero essere stati incapsulati in pagine HTML e in tal caso si sarebbe resa necessaria la rimozione del mark-up HTML, l’eliminazione dei titoli per la barra di navigazione, frammenti di codice JavaScript, link, ecc. Altre forme di cleaning (non necessariamente legate ad HTML) prevedono la rimozione di tabelle, didascalie delle figure, intestazioni delle pagine e in generale materiale ripetuto per ragioni tipografiche.

Non tutte le parole in un testo sono significative, per esempio: articoli, congiunzioni e preposizioni contengono uno scarso potere informativo e quindi non sono utili alla nostra analisi. Se queste parole fossero conservate si incrementerebbe il numero di parametri (dimensioni) da elaborare con una penalizzazione sui costi computazionali e con il rischio di confondere i risultati5.

In particolare, definiamo vuote le parole che non sono portatrici di significato autonomo (dette anche stop word), in quanto elementi necessari alla costruzione della frase; oppure sono parole strumentali con funzioni grammaticali e/o sintattiche (e.g. “hanno”, “questo”, “perché”, “non”, “tuttavia”). La rimozione delle stop word è eseguita mediante un filtraggio basato su stop list. Una stop list è un elenco precostituito di stop word. Esistono stop list per ogni lingua. Tecnicamente sono disponibili molte implementazioni di filtri basati su stop list. In seguito ne valutiamo due.

1) Impiego della libreria NLTK. NLTK è un’ampia libreria con funzioni per la processazione del linguaggio naturale (Natural Language Processing, NLP) e con estensioni multilingua (incluso il supporto dell’Italiano per gran parte delle funzionalità). Tra le funzioni offerte è incluso il supporto per il filtraggio con stop list. Nell’esempio seguente, si ottiene da NLTK la stop list per l’Italiano e si visualizza il numero di stop word contenute nella lista.

from nltk.corpus import stopwords
stoplist = stopwords.words('italian')

len(stoplist)

219

2) Impiego della libreria stop-words. Questa è un libreria multilingua specializzata unicamente nel filtraggio su stop list. Nell’esempio seguente, si ottiene la stop list per l’Italiano e si visualizza il numero di stop word contenute nella lista.

from stop_words import get_stop_words
stoplist=get_stop_words('italian')

len(stoplist)

 
308

[themify_box style=”info”]La stop list della libreria stop-words appare più selettiva, includendo un numero maggiore di stop word.[/themify_box]

Il filtraggio mediante stop list si ottiene in Python con una riga di codice:

filtered_corpus = [[word for word in unicode(document,'utf-8').lower().split() if word not in stoplist] for document in rawcorpus1]

print filtered_corpus

[[u'volpe', u'voleva', u'mangiare', u"l´uva"]
[u"l´uva", u'troppo', u'alto', u'volpe']
[u'volpe', u'riusciva', u'raggiungere', u"l´uva"]
[u'volpe', u'rinunciò', u'sostenendo', u"l´uva", u'matura']
[u'volpe', u'furba,', u'volte', u'furbizia', u'paga']]

Dopo il filtraggio, ogni documento del corpus consiste di una lista di token “sopravvissuti” alla rimozione. I token possono essere parole singole o anche combinazioni di parole dette n-grammi (come vedremo negli approfondimenti in calce al presente post).


Il complesso di token estratti e filtrati dal corpus prende il nome di vocabolario (solitamente indicato con la lettera V). In seguito chiameremo attributo ogni generico token contenuto nel vocabolario. Ad ogni corpus è sempre associato un vocabolario.

Il vocabolario è un oggetto speciale che incapsula un elenco indicizzato degli attributi. A ciascun attributo è associato un indice numerico univoco6.

Utilizzando la liberia gensim che è specializzata nella modellazione e recupero di contenuti testuali, la creazione di un vocabolario si risolve in poche istruzioni:

from gensim import corpora

V = corpora.Dictionary(filtered_corpus)

for i in range(0,len(V)):
	print i, V[i]

print(V.token2id)

0 voleva
1 l´uva
2 volpe
3 mangiare
4 alto
5 troppo
6 riusciva
7 raggiungere
8 rinunciò
9 sostenendo
10 matura
11 furbizia
12 paga
13 furba,
14 volte

{u'alto': 4, u'rinunciò': 8,
u"l´uva": 1, u'furba,': 13,
u'raggiungere': 7, u'voleva': 0,
u'matura': 10, u'troppo': 5,
u'furbizia': 11, u'sostenendo': 9,
u'volpe': 2, u'mangiare': 3,
u'riusciva': 6, u'paga': 12,
u'volte': 14}

Nell’esempio precedente, abbiamo visualizzato il vocabolario per avere un’evidenza delle associazioni tra ciascun attributo e il rispettivo indice numerico.

Con riferimento all’esempio precedente, il vocabolario gensim (chiamato dizionario) è un oggetto complesso dotato di molte funzioni e proprietà. Per esempio, la proprietà token2id è un array associativo contenente il numero di occorrenze nel corpus di ogni attributo.
Vocabolario o Dizionario? Vocabolario e dizionario non sono la stessa cosa. Il termine vocabolario, rispetto a dizionario, può avere anche il significato di corpus lessicale ossia “patrimonio lessicale di una lingua” o “insieme dei vocaboli propri di un certo settore o di un singolo autore”. In tal senso, per i nostri scopi appare più indicato l’impiego della parola vocabolario. Comunque, i due termini sono spesso usati in modo interscambiabile.

Un vocabolario agevola la rappresentazione dei documenti nel corpus come vettori. Se N è il numero di documenti di un corpus e M è il numero di attributi indicizzati nel vocabolario, il corpus può essere rappresentato come una matrice di dimensioni NxM chiamata matrice documento-termine (DTM).

L’i-esima riga di una matrice DTM corrisponde alla rappresentazione vettoriale dell’i-esimo documento del corpus ed è chiamato vettore documento. A volte si preferisce usare la trasposta della DTM che è chiamata matrice termine-documento (TDM). Le matrici DTM e TDM sono anche chiamate matrici lessicali.

Gli elementi di un vettore documento sono chiamati pesi e ciascun peso è associato ad uno specifico attributo di V. Per calcolare i valori dei pesi sono disponibili varie metriche di pesatura. Una metrica basilare consiste nell’assegnare il valore 0 per indicare l’assenza dell’attributo nel documento e il valore 1 per indicarne la presenza. In questo caso si parla di schema di rappresentazione booleano del corpus.

Un’altra metrica è quella frequentista e assegna un valore che è pari al numero di occorrenze dell’attributo nel documento: il generico peso wij ha un valore corrispondente al numero di occorrenze dell’attributo i-esimo di V nel documento j-esimo. Usando la metrica frequentista, si ottiene una rappresentazione del corpus chiamata schema di ponderazione o più comunemente bag of words (BOW). Più specificatamente, si parla di schema di rappresentazione BOW del corpus.

A seconda dello schema di rappresentazione utilizzato il contenuto della matrice DTM (o TDM) cambia.

In gensim la creazione di uno schema BOW del corpus si ottiene così:

corpus_bow = [V.doc2bow(document) for document in filtered_corpus]

print corpus_bow

[[(0, 1), (1, 1), (2, 1), (3, 1)],
[(1, 1), (2, 1), (4, 1), (5, 1)],
[(1, 1), (2, 1), (6, 1), (7, 1)],
[(1, 1), (2, 1), (8, 1), (9, 1), (10, 1)],
[(2, 1), (11, 1), (12, 1), (13, 1), (14, 1)]]

Nell’esempio precedente, è stato anche visualizzato il corpus BOW. Come si può notare, gensim non memorizza esattamente il corpus come una matrice NxM, ma ne crea una versione compressa dove ogni documento è rappresentato come una lista di coppie di valori (tante coppie quanti sono gli attributi con peso non nullo nel documento). Nella generica coppia (i,wij): i è l’indice dell’attributo in V e wij è il peso dell’attributo nel documento j-esimo. Dunque, con riferimento all’esempio precedente, nel primo documento del corpus, gli attributi voleva (0), l’uva (1), volpe (2), mangiare (3) compaiono rispettivamente con occorrenza unitaria.

Anche con la libreria di apprendimento automatico scikit-learn (in seguito sklearn), l’operazione di creazione di un dizionario e della rappresentazione BOW del corpus è alla portata di poche linee di codice Python:

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction import text

my_stop_words = text.ENGLISH_STOP_WORDS.union(get_stop_words('italian'))
vectorizer = CountVectorizer(analyzer=u'word', stop_words=set(my_stop_words))
corpus_bow=vectorizer.fit_transform(rawcorpus1)

corpus_bow.shape # visualizza la dimensione della matrice DTM

(5,15)

sklearn dispone di un facilitatore nativo per il filtraggio delle stop word e già integra una stop list localizzata in inglese. Come mostrato nell’esempio precedente, la stop list nativa di sklearn può essere facilmente estesa con altre liste (p.e. quella ottenuta con la libreria stop-words). Un vettorizzatore (CountVectorizer) provvede a creare la rappresentazione BOW. Il risultato è una rappresentazione matriciale del corpus avente dimensioni 5×15 (N=5, M=15). Per comparazione con gensim, diamo ora uno sguardo al vocabolario e al corpus BOW ottenuti con sklearn:

V=vectorizer.get_feature_names()

for i in range(0,len(V)):
	print i,V[i]

print corpus_bow

corpus_bow.todense()

0 alto
1 furba
2 furbizia
3 mangiare
4 matura
5 paga
6 raggiungere
7 rinunciò
8 riusciva
9 sostenendo
10 troppo
11 uva
12 voleva
13 volpe
14 volte

(0, 13) 1
(0, 12) 1
(0, 3) 1
(0, 11) 1
(1, 13) 1
(1, 11) 1
(1, 10) 1
(1, 0) 1
(2, 13) 1
(2, 11) 1
(2, 8) 1
(2, 6) 1
(3, 13) 1
(3, 11) 1
(3, 7) 1
(3, 9) 1
(3, 4) 1
(4, 13) 1
(4, 1) 1
(4, 14) 1
(4, 2) 1
(4, 5) 1

[[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0],
[0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0],
[0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1]]

sklearn gestice la rappresentazione matriciale del corpus mediante la classe csr_matrix della libreria scientifica SciPy. Nell’esempio precedente, è immediato ottenere la visualizzazione in formato sparso (non compresso) della matrice DTM, mediante una semplice invocazione del metodo todense.

Come si può notare la matrice DTM è composta da un gran numero di zeri (poiché non tutti gli attributi di V sono presenti in ogni documento, in ogni riga ci saranno molti pesi nulli corrispondenti ad occorrenze nulle dei vari attributi in ciascun documento). In analisi numerica, una matrice i cui valori sono quasi tutti uguali a zero è definita matrice sparsa[Source].


Approfondimento 1: Rimozione parole in base a specifiche soglie di occorrenza

Quando si costruisce un vocabolario si potrebbe decidere di rimuovere gli attributi aventi un’occorrenza superiore ad una soglia massima e/o inferiore ad una soglia minima (quest’ultima chiamata cut-off). L’assegnazione di valori alle soglie minima e massima può richiedere un’indagine preliminare sul corpus.

Per quanto concerne il cut-off, un valore potrebbe essere 1, cioè decidiamo di rimuovere tutte le parole che compaiono una sola volta nell’intero corpus (queste parole sono chiamate hapax).

Con la libreria gensim si può procedere come segue:

from six import iteritems

once_ids = [tokenid for tokenid, docfreq in iteritems(V.dfs) if docfreq == 1]

V.filter_tokens(once_ids)

Con la libreria sklearn basta istanziare il vettorizzatore specificando il parametro min_df a cui possiamo assegnare un valore reale oppure intero. Il valore intero specifica il numero di occorrenze minimo al di sotto del quale un attributo deve essere scartato. Un valore reale (tra 0 e 1) specifica invece la quota percentuale di documenti in cui un attributo deve comparire per non essere scartato. Per esempio, gli attributi uva e volpe sono quelli più presenti nel corpus. In particolare, l’attributo uva compare in 4 documenti su 5 ossia nell’80% del corpus. Se impostiamo mid_df a 0.8, nel dizionario saranno certamente inclusi gli attributi volpe e uva. Se impostiamo a 0.9 (90%), nel dizionario sarà incluso solo l’onnipresente volpe. Per escludere gli attributi che occorrono una sola volta (cioè considerare solo quelli che compaiono almeno 2 volte), possiamo procedere come segue:

vectorizer = CountVectorizer(analyzer=u'word', stop_words=set(my_stop_words), mid_df=2)

I codici esemplificativi mostrati in precedenza possono essere facilmente adattati per la gestione di una soglia massima. Nel caso di gensim basta modificare la condizione nell’istruzione if. Nel caso di sklearn il parametro da impostare è max_df che può assumere valore intero (per un conteggio assoluto) o reale (indicante una proporzione di documenti come nel caso di min_df).


Approfondimento 2: Rimozione punteggiatura

Nel processo di tokenizzazione assume particolare importanza il trattamento della punteggiatura. I segni di punteggiatura devono essere trattati come segni indipendenti anche quando sono attaccati ad una parola. Sono difficilmente gestibili perché possono avere impieghi differenti. Per esempio il punto può indicare la fine della frase, un’abbreviazione, il punto decimale in valori numerici, ecc. L’apostrofo che compare di norma in mezzo a due parole diverse, secondo il criterio di tokenizzazione basato su spazi bianchi, comporterebbe l’errata identificazione delle due parole come un’unica.

Dagli esempi precedenti possiamo verificare che il vettorizzatore integrato nella libreria sklearn esegue una processazione dei testi più robusta riconoscendo come separatori lo spazio bianco, la punteggiatura, le virgolette, i trattini (-/|), le parentesi ([{}]) e i caratteri speciali (#@$%°&^*).

Nel caso in cui abbiamo usato gensim, invece, avendo implementato una tokenizzazione basata esclusivamente sugli spazi bianchi, ci ritroviamo con attributi estratti come “l’uva” e “furba,” che contengono punteggiatura; potrebbe dunque essere utile applicare preliminarmente un ulteriore filtro sulla punteggiatura come segue:

import re

filtered_corpus = [re.sub(ur"[^\w\d'\s]+",'',document).split() for document in rawcorpus1]

print filtered_corpus

[['La', 'volpe', 'voleva', 'mangiare', "l´uva"],
["L´uva", 'era', 'troppo', 'in', 'alto', 'per', 'la', 'volpe'],
['La', 'volpe', 'non', 'riusciva', 'a', 'raggiungere', "l´uva"],
['La', 'volpe', 'rinunciò', 'sostenendo', 'che', "l´uva", 'non', 'era', 'ancora', 'matura'],
['La', 'volpe', 'era', 'furba', 'ma', 'a', 'volte', 'la', 'furbizia', 'non', 'paga']]

oppure usando il tokenizzatore wordpunct_tokenize incluso nella libreria NLTK e in grado di suddividere il testo usando gli spazi bianchi e i segni di punteggiatura (;:,.!?):

import nltk
filtered_corpus=[]
for document in rawcorpus1:
	tokens=nltk.wordpunct_tokenize(document)
	text=nltk.Text(tokens)
	filtered_corpus.append([word for word in text if len(word)>1])

print filtered_corpus

[['La', 'volpe', 'voleva', 'mangiare', 'uva'],
['uva', 'era', 'troppo', 'in', 'alto', 'per', 'la', 'volpe'],
['La', 'volpe', 'non', 'riusciva', 'raggiungere', 'uva'],
['La', 'volpe', 'rinunciò', 'sostenendo', 'che', 'uva', 'non', 'era', 'ancora', 'matura'],
['La', 'volpe', 'era', 'furba', 'ma', 'volte', 'la', 'furbizia', 'non', 'paga']]

Dopo la rimozione della punteggiatura si può applicare il filtraggio con stop list.

Approfondimento 3: Bag of N-grams
Data una sequenza ordinata di elementi, un n-gramma è una sua sottosequenza di n elementi. Secondo l’applicazione, gli elementi in questione possono essere fonemi, sillabe, lettere, parole, ecc. Un n-gramma è di lunghezza 1 è chiamato “unigramma”, di lunghezza 2 “digramma”, di lunghezza 3 “trigramma” e, da lunghezza 4 in poi, “n-gramma”. La vettorizzazione del corpus che abbiamo analizzato fino a questo momento prevede la selezione di attributi che sono essenzialmente unigrammi. Tuttavia è possibile estendere la selezione ad attributi che sono anche combinazioni di 2 o più attributi. Il modello di rappresentazione BOW esteso agli n-grammi è chiamato Bag of N-grams.

Seguono alcuni esempi per chiarire il punto.

Usando la libreria sklearn, possiamo eseguire una vettorizzazione basata sulla selezione sia di unigrammi sia di digrammi, impostando il parametro ngram_range:

vectorizer = CountVectorizer(analyzer=u'word', stop_words=set(my_stop_words), ngram_range = (1,2))
....
for i in range(0,len(V)): print i,V[i]

0 alto
1 alto volpe
2 furba
3 furba volte
4 furbizia
5 furbizia paga
6 mangiare
7 mangiare uva
8 matura
9 paga
10 raggiungere
11 raggiungere uva
12 rinunciò
13 rinunciò sostenendo
14 riusciva
15 riusciva raggiungere
16 sostenendo
17 sostenendo uva
18 troppo
19 troppo alto
20 uva
21 uva matura
22 uva troppo
23 voleva
24 voleva mangiare
25 volpe
26 volpe furba
27 volpe rinunciò
28 volpe riusciva
29 volpe voleva
30 volte
31 volte furbizia

Utilizzando la libreria gensim in combinazione con NLTK, possiamo procedere nel seguente modo:

import nltk
import string
from gensim.models import Phrases
from nltk.corpus import stopwords

bigram = Phrases()
for document in documents:
	document = [word.decode('utf-8') for word in nltk.word_tokenize(document.lower()) if word not in string.punctuation]
	bigram.add_vocab([document])

bigram.vocab

defaultdict(int,
{'a': 2,
'a_raggiungere': 1,
'a_volte': 1,
'alto': 1,
'alto_per': 1,
'ancora': 1,
'ancora_matura': 1,
'che': 1,
'che_l'uva': 1,
'era': 3,
'era_ancora': 1,
'era_furba': 1,
'era_troppo': 1,
'furba': 1,
'furba_ma': 1,
'furbizia': 1,
'furbizia_non': 1,
'in': 1,
'in_alto': 1,
'l´uva': 4,
'l´uva_era': 1,
'l´uva_non': 1,
'la': 6,
'la_furbizia': 1,
'la_volpe': 5,
'ma': 1,
'ma_a': 1,
'mangiare': 1,
'mangiare_l'uva': 1,
'matura': 1,
'non': 3,
'non_era': 1,
'non_paga': 1,
'non_riusciva': 1,
'paga': 1,
'per': 1,
'per_la': 1,
'raggiungere': 1,
'raggiungere_l'uva': 1,
'rinunciò': 1,
'rinunciò_sostenendo': 1,
'riusciva': 1,
'riusciva_a': 1,
'sostenendo': 1,
'sostenendo_che': 1,
'troppo': 1,
'troppo_in': 1,
'voleva': 1,
'voleva_mangiare': 1,
'volpe': 5,
'volpe_era': 1,
'volpe_non': 1,
'volpe_rinunciò': 1,
'volpe_voleva': 1,
'volte': 1,
'volte_la': 1})

Incrementare il numero di attributi vuol dire aumentare la dimensione delle rappresentazioni vettoriali dei documenti. Di conseguenza, le operazioni che implicheranno l’uso di questi vettori ne risentiranno sul piano computazionale.

L’espressione maledizione della dimensionalità (coniata da Richard Bellman) indica il problema derivante dal rapido incremento delle dimensioni dello spazio matematico associato all’aggiunta di variabili (qui degli attributi); questo incremento porta ad una maggiore dispersione dei dati all’interno dello spazio descritto dalle variabili rilevate (qui la sparsità della matrice termine-documento), ad una maggiore difficoltà nella stima e, in generale, nel cogliere delle strutture nei dati stessi.

Approfondimento 4: Analisi del testo, un breve recap

Nelle note precedenti abbiamo visto che il testo grezzo (raw) necessita di opportuni pre-trattamenti. La tokenizzazione, ovvero l’operazione mediante la quale si suddivide il testo in token si estrinseca in passi di identificazione ed estrazione secondo specifici criteri di trattamento dei caratteri di separazione. I token comprendono svariate categorie di parti del testo (parole, punteggiatura, numeri, ecc) o possono anche essere delle unità complesse (come le date). E’ bene ricordare che esistono implementazioni più sofisticate della semplice suddivisione basata su spazi che abbiamo adottato in uno dei nostri esempi precedenti. La libreria NLTK offre una varietà di tokenizzatori (questo è il nome degli algoritmi che eseguono la tokenizzazione).

Nell’esempio seguente, definiremo per comodità un corpus a cui ci riferiremo con il nome di rawcorpus2 e, riusando del codice esemplificativo già introdotto in precedenza per la libreria gensim, richiameremo il tokenizzatore Word Tokenizer di NLTK:

rawcorpus2=["Il 12 GENNAIO 2002 l´Euro diventa moneta corrente in 12 paesi dell´Unione Europea", 'Il 29/10/1929, definito in seguito come il giovedì nero, avvenne il crollo finanziario della borsa di Wall Street']

from nltk.tokenize import word_tokenize

filtered_corpus = [[word for word in word_tokenize(unicode(document,"utf-8"))] for document in rawcorpus2]

print filtered_corpus

[[u'Il', u'12', u'GENNAIO', u'2002', u"l´Euro", u'diventa', u'moneta', u'corrente', u'in', u'12', u'paesi', u"dell´Unione", u'Europea'],
[u'Il', u'29/10/1929', u',', u'definito', u'in', u'seguito', u'come', u'il', u'giovedì', u'nero', u',', u'avvenne', u'il', u'crollo', u'finanziario', u'della', u'borsa', u'di', u'Wall', u'Street']]

Si noti dall’output dell’esempio come la versione filtrata dei documenti consista di token che sono parole, numeri, date, punteggiatura7. Questi token sono candidati ad essere gli attributi che compariranno nel vocabolario, si rendono dunque necessari ulteriori filtraggi.

Un passaggio ulteriore, per un filtraggio più selettivo, consiste della rimozione delle stop word. Sempre su rawcorpus2:

import string
from stop_words import get_stop_words

stoplist=get_stop_words('italian')

filtered_corpus = [[word for word in word_tokenize(unicode(document,"utf-8").lower()) if word not in stoplist and word not in string.punctuation] for document in rawcorpus2]

print filtered_corpus

[[u'12', u'gennaio', u'2002', u"l´euro", u'diventa', u'moneta', u'corrente', u'12', u'paesi', u"dell´unione", u'europea'],
[u'29/10/1929', u'definito', u'seguito', u'giovedì', u'nero', u'avvenne', u'crollo', u'finanziario', u'borsa', u'wall', u'street']]
[themify_box style=”info”]A volte conviene eliminare le stop word, altre volte invece è preferibile mantenerle nel corpus stesso. La rimozione delle stop word dal testo potrebbe causare una perdita di informazioni rilevanti. Le stopword sono utili, per esempio, quando sono presenti in una parola composta (per esempio l’articolo nel titolo di un film), oppure l’eliminazione della negazione “non” in una frase cambierebbe completamente il messaggio dell’autore. Dunque, è necessario valutare, caso per caso, l’eliminazione delle stop word ed eventualmente ricorrere a normalizzazioni preliminari (p.e. basate su elenchi predefiniti) per selezionare le parole che non dovrebbero essere trattate come stop word (p.e. mediante la costruzione di digrammi o n-grammi).[/themify_box]

Nell’esempio precedente contestualmente alla rimozione delle stopword si è anche provveduto a:

  • rimozione dei token corrispondenti a caratteri speciali, parentesi e trattini
  • trasformazione di tutti i caratteri in minuscoli

Il passaggio di trasformazione in minuscolo può essere inteso come una forma semplificata di normalizzazione. La normalizzazione consente di escludere ogni possibile differenza del tipo maiuscolo/minuscolo (p.e. abbassando le maiuscole non rilevanti8), uniformando la grafia dei nomi propri, sigle ed altre entità, trasformando gli apostrofi in accenti9. Per rendere più efficace la normalizzazione, si possono utilizzare elenchi precompilati di parole specifiche per ogni lingua e/o argomento.

Tra le possibili pre-elaborazioni c’è lo stemming (troncamento) delle parole. Lo stemming sostituisce ad una parola il suo rispettivo stem. Uno stem è la porzione di parola ottenuta rimuovendo prefissi e suffissi. Un esempio è dato da mang che potrebbe essere lo stem per mangiare, mangio, mangiato, mangi, ecc. Lo stemming è utile perchè riduce le varianti di una stessa parola-radice ad un concetto comune (rappresentato appunto dallo stem) contribuendo altresì a ridurre il numero di attributi che andranno a comporre il vocabolario.

Lo stemming a volte può causare ambiguità e, in particolare, negli algoritmi di stemming si possono verificare due tipi di errore:

  • Overstemming: lo stemmer rende alla stessa radice parole che, in realtà, hanno significati diversi facendo sì che il termine non sia correttamente interpretato;
  • Understemming: lo stemmer crea diverse radici da parole che, in realtà, hanno la stessa origine.

La libreria NLTK include l’implementazione di diversi algoritmi per lo stemming (e.g. Porter, Lancaster, Snowball) chiamati stemmer. Nell’esempio seguente usiamo lo Snowball Stemmer sul corpus rawcorpus2 tokenizzato mediante wordpunct_tokenize di NLTK:

from nltk.stem import SnowballStemmer

stemmed_corpus=[[snowball_stemmer.stem(word) for word in document] for document in filtered_corpus]

stemmed_corpus

[[u'12',
u'gennai',
u'2002',
u"l´eur",
u'divent',
u'monet',
u'corrent',
u'12',
u'paes',
u"dell´union",
u'europe'],
[u'29/10/1929',
u'defin',
u'segu',
u'gioved',
u'ner',
u'avvenn',
u'croll',
u'finanziar',
u'bors',
u'wall',
u'street']]

Mentre lo stemmer esegue un troncamento della parola, il lemmatizzatore è un algoritmo che riconduce ogni parola di un testo alla forma base o canonica chiamata lemma, ossia nella forma in cui comparirebbe in un dizionario. Dunque un lemmatizzatore assocerebbe alla parola “mangiato” la parola “mangiare” piuttosto che una versione troncata come “mang” dello stemmer.

Esistono varie implementazioni di lemmatizzatori che risultano specializzate a seconda delle lingue supportate10. Il lemmatizzatore individua il lemma corrispondente ad ogni parola, attribuendo allo stesso lemma tutte le parole che da quel lemma derivano. Un vocabolario V si dice lemmatizzato se contiene solo le forme canoniche delle parole (lemmi). Un vocabolario lemmatizzato ha dimensioni significativamente ridotte rispetto ad un vocabolario completo cioè contenente le forme flesse (mangiato, mangio, ecc.)11.

Un’implementazione efficace di lemmatizzatore è fornita dal tool TreeTragger, sviluppato dall’Institute for Computational Linguistics of the University of Stuttgart con licenza parzialmente libera, che supporta una estesa varietà di lingue tra cui l’Italiano. TreeTragger non è sviluppato nativamente in Python ma è disponibile un wrapper (TreeTaggerWrapper) che consente un’integrazione più stretta con il linguaggio.

import treetaggerwrapper
tagger=treetaggerwrapper.TreeTagger(TAGLANG='it')
lemmatized_corpus=[tagger.make_tags(unicode(document,"utf-8")) for document in rawcorpus2]

for document in lemmatized_corpus:
	for element in document:
		print element

Il DET:def il
12 NUM @card@
GENNAIO NOM gennaio
2002 NUM @card@
l´ DET:def il
Euro NOM euro
diventa VER:pres diventare
moneta NOM moneta
corrente ADJ corrente
in PRE in
12 NUM @card@
paesi NOM paese
dell´ PRE:det del
Unione NOM unione
Europea ADJ europeo
Il DET:def il
29 NUM @card@
/ PON /
10 NUM @card@
/ PON /
1929 NUM @card@
, PON ,
definito VER:pper definire
in PRE in
seguito NOM seguito
come CON come
il DET:def il
giovedì NOM giovedì
nero ADJ nero
, PON ,
avvenne VER:remo avvenire
il DET:def il
crollo NOM crollo
finanziario ADJ finanziario
della PRE:det del
borsa NOM borsa
di PRE di
Wall NPR Wall
Street NPR Street

Nell’esempio è visualizzata la versione lemmatizzata del corpus rawcorpus2. La processazione del linguaggio naturale prevede una fase chiamata Part-Of-Speech tagging o POS-tagging (in italiano la traduzione sarebbe “etichettatura delle parti del discorso”) in cui a ogni attributo estratto dal testo si associa un’etichetta indicante la sua categoria grammaticale. Nell’esempio precedente queste etichette sono visualizzate in corrispondenza di ciascuna parola  e ad ogni parola è automaticamente associato il lemma riconosciuto. Le etichette, o tag, sono reperite attraverso tagset12.

L’etichettatura POS permette di estrarre parole in funzione della forma grammaticale. Nell’esempio che segue, si estraggono i lemmi dei soli verbi riconosciuti.

Un’implementazione nativa in Python di lemmatizzatore compatibile anche con l’Italiano è inclusa nella libreria Pattern del CLiPS (Conputational Linguistics & Psycholinguistics Research Center), i cui sorgenti sono rilasciati sotto licenze BSD. Pattern è una libreria estesa che include funzionalità per il data mining, processazione del linguaggio naturale13 (tra cui il lemmatizzatore), apprendimento automatico, analisi delle reti e visualizzazione.

from pattern.it import parsetree
from pattern.search import search
lemmatized_corpus=[parsetree(document,relations=True,lemmata=True) for document in rawcorpus2]

for document in lemmatized_corpus:
	for match in search('VB',document):
		for words in match:
			print words.tags

[[Sentence("Il/DT/B-NP/O/O/il 12/CD/I-NP/O/O/12 GENNAIO/NN/I-NP/O/O/gennaio 2002/CD/B-NP/O/NP-SBJ-1/2002 l´/DT/I-NP/O/NP-SBJ-1/l´ Euro/NN/I-NP/O/NP-SBJ-1/euro diventa/VB/B-VP/O/VP-1/diventare moneta/NN/B-NP/O/NP-OBJ-1/moneta corrente/NN/I-NP/O/NP-OBJ-1/corrente in/IN/B-PP/B-PNP/O/in 12/CD/B-NP/I-PNP/O/12 paesi/NNS/I-NP/I-PNP/O/paese dell´/IN/B-PP/B-PNP/O/dell´ Unione/NNP/B-NP/I-PNP/O/unione Europea/NNP/I-NP/I-PNP/O/europea")],
[Sentence('Il/DT/O/O/O/il 29&slash;10&slash;1929/CD/O/O/O/29 ,/,/O/O/O/, definito/JJ/B-ADJP/O/O/definito in/IN/B-PP/B-PNP/O/in seguito/NN/B-NP/I-PNP/O/seguito come/IN/B-PP/B-PNP/O/come il/DT/B-NP/I-PNP/O/il giovedì/NN/I-NP/I-PNP/O/giovedì nero/JJ/I-NP/I-PNP/O/nero ,/,/O/O/O/, avvenne/NNS/B-NP/O/O/avvenna il/DT/B-NP/O/O/il crollo/NN/I-NP/O/O/crollo finanziario/JJ/I-NP/O/O/finanziario della/IN/B-PP/B-PNP/O/della borsa/NN/B-NP/I-PNP/O/borsa di/IN/B-PP/B-PNP/O/di Wall/NNP/B-NP/I-PNP/O/wall Street/NNP/I-NP/I-PNP/O/street')]]

[u'diventa', u'VB', u'B-VP', 'O', 'VP-1', u'diventare']

E’ interessante notare le differenze tra gli output dei due lemmatizzatori sulla lingua italiana. In particolare il POS tagging in Pattern non ha riconosciuto come verbi le parole “definito” e “avvenne”, di conseguenza, i lemmi generati non sono quelli attesi. TreeTagger appare raggiungere un più elevato livello di qualità nella individuazione del giusto lemma per l’Italiano. C’è da aggiungere, comunque, che Pattern è una libreria con una grande quantità di funzioni non specializzata esclusivamente su POS tagging e lemmatizzazione. Si noti, inoltre, che le prestazioni degli strumenti di processazione del linguaggio naturale, in termini di precisione ed accuratezza, sono superiori per la lingua inglese rispetto ad altre lingue e ciò è confermato anche da molteplici risultati di ricerche in letteratura.

Una delle due domande più frequenti quando si tratta di filtrare documenti grezzi è: quali e quanti filtraggi applicare e in che ordine?

Non esiste una risposta unica. Il pretrattamento della collezione di documenti grezzi è finalizzato alla selezione degli attributi per la costruzione del vocabolario V. La dimensione di V impatta direttamente sui vettori usati per la rappresentazione dei documenti e sul peso computazionale delle operazioni eseguite sui vettori stessi. Bisogna tuttavia considerare un aspetto fondamentale: sebbene fino a questo momento si sia considerato il solo modello di rappresentazione BOW, come vedremo nei prossimi post della serie “Fondamenti”, esistono altri modelli che trasformano la BOW in rappresentazioni più sofisticate e ridotte (in termini dimensionali dei vettori) in grado di catturare secondo vari approcci (e.g. algebrico, probabilistico) gli elementi più significativi dei testi.

In generale, la rimozione delle stop word è un tipo di filtraggio che ben si adatta alla maggioranza dei modelli di rappresentazione.

Alcuni modelli (e.g. probabilistici o deep basati su reti neurali) manifestano un’intrinseca capacità di selezione delle parole anche in assenza di un esplicito prefiltraggio. Tuttavia non è possibile generalizzare e ogni corpus richiede una specifica attenzione. Comunque, un fattore comune a tutti i modelli è che un mancato prefiltraggio influenza la dimensionalità delle rappresentazioni e ciò ha sempre un impatto negativo sui tempi di elaborazione e richiede corpus più estesi (cioè composti da un numero maggiore di documenti rispetto al caso con prefiltraggio).

In altri casi un pretrattamento appare condizione essenziale per ottenere prestazioni migliori, per esempio il modello Random Indexing che incontreremo in uno dei prossimi post della serie “Fondamenti”, nonostante la sua leggerezza in termini computazionali, è in grado di raggiungere e superare le prestazioni di modelli più complessi (come il Latent Semantic Analysis) se si applica in via preventiva una catena di prefiltraggio basata su normalizzazione e stemming.


Ulteriori link per approfondimenti:

  1. Il data mining estrae sapere o conoscenza a partire da grandi quantità di dati, attraverso metodi automatici o semi-automatici. Il text mining o text data mining è una forma particolare di data mining dove i dati sono costituiti da testi scritti in linguaggio naturale, quindi da documenti “destrutturati”.
  2. Tokenizzare un testo significa dividere le sequenze di caratteri in unità minime di analisi dette appunto token.
  3. Nelle lingue segmentate i confini di parola sono marcati da spazi bianchi. Nelle lingue non segmentate i confini di parola non sono marcati esplicitamente nella scrittura (e.g. cinese, giapponese) e il processo di tokenizzazione richiede anzitutto una segmentazione chiamata word segmentation (qualcosa di simile all’algoritmo di Viterbi).
  4. La nozione di token è distinta da quella di parola, poiché la tokenizzazione non si basa generalmente su criteri morfosintattici o semantici: la forma “mandarglielo” corrisponde a 1 token ma ha 3 parole morfologiche (mandare + gli + lo).
  5. L’incremento del numero di parametri influenzerebbe la dimensione del corpus di documenti nel senso che sarebbero richiesti molti documenti in più per poter condurre il text mining. Corpus limitati e un elevato numero di parametri inducono gli algoritmi di text mining nel difettare in generalizzazione, non riuscendo ad operare in modo accettabile su documenti aggiunti successivamente.
  6. In Python è un intero positivo che parte da 0.
  7. Il Word Tokenizer di NLTK si basa su semplici regole euristiche: le sequenze di stringhe alfabetiche ininterrotte fanno parte di un unico token; i token sono separati fra loro tramite spazi o simboli di punteggiatura.
  8. Nel nostro esempio abbiamo ridotto le maiuscole in modo “massivo”, senza una specifica distinzione per parola.
  9. Quando forme tipografiche diverse vengono condotte a una stessa forma standard, si dice che sono state ricondotte a una forma normalizzata.
  10. Anche gli stemmer, come i lemmatizzatori, sono specializzati a seconda delle lingue supportate.
  11. Nelle lingue ricche di forme flesse, come l’Italiano, il Tedesco o il Francese, un vocabolario completo può avere anche dieci o venti volte il numero di parole di un vocabolario lemmatizzato. Anche nelle lingue povere di versioni flesse, un vocabolario completo ha però tipicamente almeno il doppio dei termini di un vocabolario lemmatizzato. Per esempio, nel caso dell’Inglese che è una lingua povera di forme flesse, la forma canonica di un verbo prevede la creazione di tre forme flesse aggiuntive, mediante suffissazione con -s (per la terza persona singolare del presente), -ed (per il passato) e con -ing (per il gerundio).
  12. Il tagset per l’Italiano usato in TreeTagger è disponibile seguendo il link Italian tagset used in the TreeTagger parameter file.
  13. Il tagset utilizzato da Pattern è disponibile su Penn Treebank II tag set.

Pubblicato da lorenzo

Full-time engineer. I like to write about data science and artificial intelligence.

Vuoi commentare?