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 mining 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.
📚 Corpus e Tokenizzazione
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 token.
Nelle lingue segmentate come l'Italiano, un modo per estrarre i token consiste nell'usare gli spazi come delimitatori dei token stessi. 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 testo.
Nota Tecnica: 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']
Importante: 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.
🔍 Filtraggio e Stop Words
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 risultati.
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.
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
Info: La stop list della libreria stop-words appare più selettiva, includendo un numero maggiore di stop word.
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.
📖 Vocabolario e Rappresentazione
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 univoco.
Utilizzando la liberia
gensimche è 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.
Nota: 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.
🔢 Matrice Documento-Termine
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 w_ij 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.
Nota: 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)]]
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,w_ij): i è l'indice dell'attributo in V e w_ij è il peso dell'attributo nel documento j-esimo.
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. Un vettorizzatore (CountVectorizer) provvede a creare la rappresentazione BOW. Il risultato è una rappresentazione matriciale del corpus avente dimensioni 5×15 (N=5, M=15).
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.
🔧 Approfondimenti
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.
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.
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 che contengono punteggiatura; potrebbe dunque essere utile applicare preliminarmente un ulteriore filtro sulla punteggiatura.
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.
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.
Maledizione della Dimensionalità: 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.