Tokenization – NLP di base in Python [1 di 9]

Tokenization

Il Natural Language Processing, o NLP in breve, permette ai computer di utilizzare il linguaggio naturale, cioè il linguaggio che usiamo comunemente noi esseri umani quando comunicano tra di noi, sto parlando dell’italiano, l’inglese, l’arabo, il cinese, e così via.

Questo è il primo articolo di una mini-serie con i quali racconterò, in maniera semplice, alcuni elementi di base dell’NLP e li metterò in pratica con Python, commentando ciò che faccio. Il mio obiettivo è di rendere meno misterioso, per alcuni addirittura magico, il modo con cui i computer riescono ad utilizzare il linguaggio naturale e spingere gli interessati ad approfondire l’argomento. Chissà, qualcuno di voi potrebbe presto lavorare alla prossima versione di Siri, Alexa, Cortana o Ok Google 🙂

Condivido subito con voi i 9 argomenti che intendo trattare nella mini-serie, così se non ve ne interessa neanche uno potete interrompere subito la lettura 😛 (gli argomenti sono indicati con il nome inglese, ma tutti gli articoli saranno in italiano):

  1. Tokenization <– questo articolo
  2. Stemming
  3. Lemmatization
  4. Splitting
  5. Bag-of-words
  6. Text classifier
    1. Costruzione del classificatore
    2. Refactoring: funzioni
    3. Refactoring: oggetti
  7. Gender classifier
  8. Sentiment analysis
    1. Sentiment analysis con NLTK
  9. Topic modelling

Attenzione: io non sono uno sviluppatore, ma un astrofisico e data scientist (e runner!), che utilizza la programmazione in maniera funzionale. In poche parole: in genere il mio codice funziona ed è facile da capire, ma potrebbe essere più veloce e scalabile. Magari qualcuno degli sviluppatori che segue questa miniserie potrà migliorare il codice, a beneficio di tutti. Come specificherò più sotto il codice è condiviso in modalità open-source su github.

Bene, torniamo a parlare di NLP.

Brevissima storia (con insight)

Tranquilli, non ho intenzione di annoiarvi con la storia dell’NLP, ma c’è un solo aspetto che vorrei citare perché mi sembra molto significativo.

L’uomo ha sentito l’esigenza di far incontrare computer e linguaggio naturale sin dalla nascita stessa del computer, con il famoso test di Alan Touring nel 1950. Per i primi 40 anni circa, si è cercato di insegnare al computer come funzionasse il linguaggio in maniera esplicita. Venivano date al computer una serie di regole relative al linugaggio, poi il computer usava queste regole per risolvere il task assegnatogli, come ad esempio rispondere ad una domanda o fare il riassunto di un testo. Si parla di NLP Simbolico.

Negli anni 90 tutto è cambiato, in seguito all’applicazione del Machine Learning all’NLP. Adesso l’uomo non cerca più di insegnare al computer le regole del linguaggio, invece fornisce al computer una grande quantità di esempi di linguaggio (cioè dei testi) e lascia che poi il computer utilizzi degli algoritmi per capire da solo cosa imparare da quei testi al fine di risolvere il task assegnatogli. Si parla di NLP Statistico.

L’utilizzo del Machine Learning si dimostra di grande successo e apre la strada agli incredibili sviluppi che ha avuto l’NLP.

Per saperne di più consiglio la pagina wiki in inglese: Natural language processing.

Python setup

In questo articolo, così come in tutti quelli della serie, utilizzerò Python 3.7.

Come IDE, utilizzo PyCham, ma ovviamente non fa alcuna differenza; che ciascuno usi pure quello con cui si trova più a suo agio!

Non è richiesta grande potenza di calcolo, per questo motivo farò girare il codice in un computer portatile dalla potenza piuttosto modesta (i5 dual core, con mac os).

Il codice sorgente lo trovate nel mio repository (github.com/lucanaso/nlp-intro) su github, come open-source sotto licenza di tipo GNU GPL v3.0. Siete dunque liberi di copiare il codice, modificarlo, distribuirlo, persino farne un uso commerciale, ma solo a patto di continuare ad usare la stessa licenza. Io vi invito a scaricarlo e giocarci in modo da seguire al meglio la serie. Non abbiate paura di fare tutte le prove che volete, male che vada potete sempre scaricare di nuovo il codice dal repository 😉 Se poi trovate qualche bug o punto di miglioramento, allora vi invito ad inviare le vostre modifiche direttamente nel repository.

Riguardo le librerie, di volta in volta indicherò quelle che vengono usate. In questo articolo di apertura vale la pena di menzionare le due più importanti:

  1. NLTK (www.nltk.org) un insieme di librerie specifiche per l’NLP di cui faremo largo uso lungo tutta la serie di articoli;
  2. scikit-learn (scikit-learn.org), una delle più diffuse librerie per il Machine Learning in Python.

A proposito di librerie, considerate che farò girare il codice in un ambiente virtuale, e condividerò la lista esplicita di tutte le librerie presenti nell’ambiente.

Per chi vuole studiare più in dettaglio come processare il linguaggio naturale con Python, qui c’è il link al libro sviluppato all’interno stesso del progetto NLTK: http://www.nltk.org/book/

Come installare NLTK (Natural Language Toolkit)

Nell’articolo di oggi useremo solo la libreria NLTK, vediamo dunque subito come installarla!

Personalmente, quando devo installare un qualche pacchetto preferisco usare il terminale. Mi basta andare nell’ambiente virtuale ed usare il comando pip per installare il pacchetto. In questo caso il comando è

pip install nltk

Tutto qua 🙂 Ovviamente, si può installare anche tramite l’IDE. Questione di gusti.

Dato che ormai ho creato una sezione su questo punto, piuttosto che chiuderla dopo 4 righe, aggiungo un altro bit di informazione.

Oltre a librerie di codice, NLTK contiene dei dati. Qualora si vogliano usare i dati di NLKT, questi vanno installati a parte. Si può fare sia dal terminale sia dalla console di Python. Ecco i due comandi qualora volessimo scaricare il dataset chiamato “popular”:

  1. comando da terminale: python -m nltk.downloader popular
  2. comando da Python: nltk.download('popular')

Per maggiori informazioni su come installare NLTK, ecco la pagina ufficiale: www.nltk.org/install.html.

Per vedere la lista dei pacchetti in un certo ambiente basta usare il comando pip freeze. Ecco la lista di tutti i pacchetti installati nell’ambiente virtuale che sto usando per questa miniserie (se ne aggiungeranno altri con i prossimi articoli):

click==7.1.2
joblib==0.16.0
nltk==3.5
regex==2020.7.14
tqdm==4.49.0

Tokenization

Benissimo, siamo adesso pronti a lavorare sul primo concetto utile in NLP: la tokenization. La tokenization è semplicemente la suddivisione di un testo in più parti, dette token. Quando tokenizziamo (permettetemi l’italianizzazione) un testo, non stiamo facendo altro che suddividere un testo di partenza in diversi token. In base a come si effettua la suddivisione avremo un tipo diverso di tokenizer.

Facciamo subito due esempi: tokenizzazione per frasi e per parole. Penso che siano auto-esplicativi, ma aggiungo due parole per essere ancora più chiari.

  1. Il tokenizer per frasi usa le frasi come token, quindi dato un testo composto, ad esempio, da 5 frasi, verrà suddividiso in 5 token, ciascuno contenente una delle frasi del testo iniziale.
  2. Similmente, il tokenizer per parole usa le parole come token, quindi un testo composto, ad esempio, da 253 parole, verrà suddiviso in 253 token.

Adesso realizziamo i nostri tokenizer in Python.

Tokenizer per frasi

Iniziamo con il tokenizer per frasi. Per prima cosa importiamo all’interno del nostro codice il pacchetto che ci serve:

from nltk.tokenize import sent_tokenize

In inglese, “frase” si dice “sentence”. Il nome “sent_tokenize” indica appunto tokenizzare per frasi.

Ci sono vari modi di importare i pachetti in Python, quello usato appena adesso dice al programma di importare solo una parte ben specifica di NLTK e non tutto il pacchetto con tutte le sue classi, metodi e funzioni. Nel caso specifico, stiamo andando a prendere la funzione sent_tokenize che si trova nel path nltk/tokenize.

Adesso che abbiamo una funzione che applica la tokenizzazione per frasi ci serve un testo su cui applicarla. Definiamo allora un testo che ci sarà utile anche più avanti. Prendo una citazione sulla corsa di Dean Karnazes, un atleta famoso, tra le altre cose, per aver corso negli USA 50 maratone in 50 giorni in 50 Stati diversi.

text_run = 'Corro perché se non lo facessi sarei pigro e triste e spenderei il mio tempo sul divano. Corro per respirare l’aria fresca. Corro per esplorare. Corro per sfuggire l’ordinario. Corro... per assaporare il viaggio lungo la strada. La vita diventa un po’ più vivace, un po’ più intensa. A me questo piace. (Dean Karnazes)'

Applichiamo adesso il tokenizer per frasi e salviamo il risultato nella variabile sentence_tokenizer_output:

sentence_tokenizer_output = sent_tokenize(text_run)

Andiamo adesso a stampare il risultato a schermo:

print('Output data (via Sentence Tokenizer):') 
for token in sentence_tokenizer_output:    
    print('\t{}'.format(token))

(Vedi sezione “Comandi trasversali” più sotto per due parole su questo snippet di codice)

Questo è ciò che viene mostrato sullo schermo del computer:

Output data (Sentence Tokenizer):
Corro perché se non lo facessi sarei pigro e triste e spenderei il mio tempo sul divano.
Corro per respirare l’aria fresca.
Corro per esplorare.
Corro per sfuggire l’ordinario.
Corro... per assaporare il viaggio lungo la strada.
La vita diventa un po’ più vivace, un po’ più intensa.
A me questo piace.
(Dean Karnazes)

Direi che tutto procede nella norma no? Intendo dire che il tokenizer per frasi si è comportato come ce lo aspettavamo: ha restituito una lista di elementi (stringhe), dove ciascun elemento è una frase del testo iniziale. In tutto vengono identificate 8 frasi, che sembra essere il numero corretto, a patto di considerare il nome dell’autore a fine testo come una frase a se stante.

Comandi trasversali

Due parole sul pezzettino di codice precedente.

Durante i vari articoli di questa mini serie utilizzerò dei comandi e costrutti di Python dal valore trasversale, cioè che non sono specifici per l’NLP o il Machine Learning, ma valgono in genere, come appunto il comando print ed il “ciclo for” presenti in questo pezzo di codice specifico. Non so se sia il caso di spiegare davvero tutti gli elementi del codice che uso, da un lato se ne beneficia in chiarezza, dall’altro si allunga il testo e molti si potrebbero annoiare. Come via di mezzo proverò a dare solo dei veloci riferimenti, sarò molto lieto di ricevere i vostri feedback a riguardo e modulare il mio comportamento di conseguenza.

print

Il comando print serve per stampare testo e contenuto di variabili. Si può usare semplicemente indicando il testo da stampare tra virgolette, e.g. print('Stampami questo testo'), oppure passando la variabile da stampare, print(token). Io lo uso quasi sempre in combinazione con format passando sia testo sia variabili. Supponiamo di avere le variabili a e b e voler stampare la loro somma, allora potremmo usare questo comando: print('La somma di {0} e {1} vale {2}'.format(a, b, a+b) ).

ciclo for

Ogni linguaggio di programmazione ha la sua sintassi per realizzare il “ciclo for” (confesso che ogni volta che riprendo in mano un linguaggio lo sbaglio sempre). In Python c’è un modo molto “naturale” di scrivere i “cicli for” in combinazione con le liste (“naturale” proprio nell’accezione di “linguaggio naturale” :)). Se io avessi una lista di bandiere e volessi fareun “ciclo for” su questa lista, a parole direi “per ogni bandiera della lista fai …”, ecco in Python si fa proprio nella stessa maniera. Ad esempio:

for flag in flag_list:
    print(flag)

Tokenizer per parole

Se sent_tokenize è la funzione di NLTK per fare la tokenizzazione per frasi, secondo voi quale sarà quella per farla per parole? Tranquilli non è una domanda a trabocchetto, quindi rispondete pure ad istinto.

Ci avete pensato? Secondo me avete indovianto.

Procediamo dunque ad importare la funzione nel nostro codice:

from nltk.tokenize import word_tokenize

Applichiamo questa funzione allo stesso testo precedente, e salviamo il risultato in word_tokenizer_output:

word_tokenizer_output = word_tokenize(text_run)

Andiamo adesso a stampare il risultato a schermo:

from nltk.tokenize import word_tokenize 
print('Output data (via Word Tokenizer):') 
for token in word_tokenizer_output:
     print('\t{}'.format(token))

Il risultato è molto più lungo rispetto a prima, riporto solo la parte iniziale che si riferisce alle parole delle prime due frasi:

Output data (via Word Tokenizer):
Corro
perché
se
non
lo
facessi
sarei
pigro
e
triste
e
spenderei
il
mio
tempo
sul
divano
.
Corro
per
respirare
l
’
aria
fresca
.

Un comune software di videoscrittura, facendo il conteggio della parole del nostro testo di partenza, restituisce 54 parole, mentre il tokenizer per parole ne ha identificate ben 71. Guardando il risultato delle prime due frasi è facile capire da dove vengono quelle in eccesso: la punteggiatura e gli apostrofi.

Non pensate che word_tokenize sia stupido, almeno per due motivi: (1) è ottimizzato per testi inglesi, in quel caso è capace di capire, ad esempio, che “I’m” va suddiviso in 2 token (I + ‘m) e non in 3 (I + ‘ + m); (2) non è scritto da nessuna parte che punteggiatura e apostrofi siano sempre da buttare! word_tokenize non butta via niente, se qualcosa non ci serve possiamo sempre filtrarlo noi dopo (questo è l’approccio standard quando si gestiscono Big Data).

Tokenizer avanzati (con regex)

NLTK consente di definire i propri tokenizer usando le cosiddette regular expressions, o regex. Lo si fa tramite la funzione RegexpTokenizer. Proviamola!

Importiamo:

from nltk.tokenize import RegexpTokenizer

RegexpTokenizer in realtà non è proprio una funzione, ma una classe. Quindi si usa in modo diverso. Una classe è una sorta di modello o template.

Se vogliamo usare una classe, per prima cosa dobbiamo creare una specifica istanza sulla base della classe. Lo facciamo chiamando la classe e passando tra parentesi i parametri che vogliamo abbia la nostra particolare istanza di quella classe. In questo caso, chiamiamo la nostra istanza custom_tokenizer:

custom_tokenizer = RegexpTokenizer('\w+')

Il testo “\w+” è un’espressione regolare, che dice al nostro tokenizer di identificare come parola qualsiasi sequenza di lettere, numeri, o underscore. Qualsiasi altra cosa viene esclusa, inclusi i segni di punteggiatura.

Dopo aver creato l’istanza, usiamo il suo metodo tokenize. Tale metodo si trova all’iterno della nostra istanza in quanto fa parte del modello/template che abbiamo usato per creare l’istanza stessa (cioè fa parte della classe). Il metodo tokenize appartiene dunque  all’istanza custom_tokenizer e va applicato al testo text_run. Salviamo il risultato in una nuova variabile, che chiamiamo custom_tokenizer_output:

custom_tokenizer_output = custom_tokenizer.tokenize(text_run)

Infine stampiamo il risultato:

print('Output data (via Custom Tokenizer):')
for token in custom_tokenizer_output:
    print('\t{}'.format(token))

Ecco il risultato mostrato a schermo, come al solito ci limitiamo alle prime due frasi:

Output data (via Custom Tokenizer):
Corro
perché
se
non
lo
facessi
sarei
pigro
e
triste
e
spenderei
il
mio
tempo
sul
divano
Corro
per
respirare
l
aria
fresca

Questa volta il risultato sembra molto vicino a quello che avrebbe ottenuto una persona se gli avessimo chiesto di identificare tutte le parole del testo di partenza.

In tutto abbiamo ottenuto 56 parole. Uhm … il programma di video-scrittura però ne indicava 54.

Quali sono le due parole in più?

Secondo voi è corretto che queste due parole siano conteggiate a parte o dovrebbero essere unite con quelle che seguono?

Su questo preferisco lasciarvi pensare un po’ in autonomia.

Se vi interessano maggiori dettagli sui tokenizer offerti da NLTK, potete visitare questa pagina: http://www.nltk.org/api/nltk.tokenize.html

Conclusioni

Eccoci dunque giunti alla conclusione di questo primo articolo nella miniserie dedicata all’NLP in Python.

Siamo partiti da quegli argomenti che saranno utili per tutta la serie:

Dopo ci siamo concentrati sul primo tema, quello della tokenizzazione o tokenization. Dato un semplice testo di partenza abbiamo usato

Se qualcosa non è andato come avreste voluto, fatemi sapere! Siamo ancora solo all’inizio, davanti a noi ci sono ben 8 argomenti! Inoltre, gli articoli futuri saranno via via sempre più complessi, quindi se c’è qualcosa da modificare questo è il momento giusto 🙂

Se invece l’articolo vi è piaciuto, beh, fatemelo sapere lo stesso 😛 Così mi assicuro di continuare a proporre le cose migliori anche in futuro!