Splitting – NLP di base in Python [4 di 9]

Splitting

A volte le operazioni più semplici sono anche quelle più utili, lo splitting è una di queste. Oggi vedremo come implementarlo all’interno della nostra serie di introduzione al Natural Language Processing NLP in Python. La serie è composta da 9 argomenti e con questo articolo trattiamo l’argomento numero 4:

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

Splitting vs Tokenizer vs Stemmer/Lemmatizer

Spesso in NLP si lavora con testi molto lunghi e per questo motivo può essere utile suddividerli in pezzi. “Splitting” si riferisce proprio a questo. Non si tratta di un termine tecnico dell’NLP, ma di un termine generico, che indica appunto il suddividere qualcosa in più parti.

Confrontiamo subito lo splitting con le altre procedure trattate nei precedenti articoli:

  1. splitting vs tokenizer:
    • in comune: entrambi suddividono il testo iniziale in diverse partiò
    • diverso: nello splitting il testo iniziale non viene suddiviso in singoli token (o parole), bensì in gruppi di parole (o chunk).
  2. splitting vs stemmer / lemmatizer:
    • diverso: nello splitting i contenuti del testo iniziale non vengono in alcun modo modificati, le parole restano identiche a come sono nel testo iniziale (mantengono la loro forma flessa).

 

Splitting in Python

Data la semplicità dell’operazione, questa è una buona occasione per realizzare noi stessi tutto l’algoritmo per fare lo splitting.

Voi come lo fareste? Prendetevi qualche minuto per pensarci.

 

Non vi viene in mente nulla? Suvvia non siate intellettualmente pigri.

Non sapete da dove iniziare? Pensate al punto di partenza ed a quello di arrivo.

 

Da cosa partiamo?

–> Un testo in uno certo linguaggio naturale.

 

Dove vogliamo arrivare?

–> Un insieme di diversi testi, tutti nello stesso linguaggio naturale di partenza, tali che la loro unione coincide con il testo di partenza.

 

 

Voi come fareste?

Prendetevi qualche minuto per pensarci.

 

 

Funzione text_splitting

Ecco come lo faremo noi adesso.

  1. Decidiamo di spezzare il testo di partenza in parti che contengono tutte lo stesso numero di parole, diciamo N;
  2. leggiamo il testo di partenza da un file e lo salviamo in una varibile locale;
  3. spezziamo il testo di partenza nelle sue singole parole e salviamo tutte le parole in una lista (mantenendo il loro ordine);
  4. prendiamo le prime N parole della lista e le mettiamo insieme, abbiamo così creato la prima parte;
  5. ripetiamo il punto 4 con le successive N parole, fintanto che abbiamo esaurito le parole.

Ecco di seguito il codice Python che implementa questo algoritmo. Per indicare le parti di testo usiamo il termine “chunks”. A segueire c’è una spiegazione delle varie parti del codice.

def text_splitting(input_text, chunk_size):
    output_chunks = []  # this is where we will be storing all of the chunks
    current_chunk_size = 0  # a counter to keep track of the number of words in the chunk
    current_chunk_words = []  # a list with the words to include in the chunk
    all_words = input_text.split(' ') # split the whole input_text by white spaces
    for word in all_words:
        current_chunk_words.append(word)  # add the word to the chunk words
        current_chunk_size += 1  # update the chunk size
        if current_chunk_size == chunk_size:  # check if the chunk contains the desired number of words
            chunk = ' '.join(current_chunk_words)  # bring the words back together to create a readable chunk
            output_chunks.append(chunk)  # add the chunk to the final list
            # reset 'current' variables:
            current_chunk_words = []
            current_chunk_size = 0
    # Usually the total number of words in the text is not a multiple of the chunk_size. 
    #     In this case: the last (current) chunk does not reach the target size,
    #     to avoid losing the data we add the smaller chunk.
    if current_chunk_words:
        chunk = ' '.join(current_chunk_words)
        output_chunks.append(chunk)
    return output_chunks

Notiamo subito come la procedura sia stata inserita dentro una nuova funzione. Questo è il modo standard di procedere (vedi articolo sulla lemmatization per saperne di più sulle funzioni).

Breve spiegazione delle varie parti della funzione:

  1. input_text.split(' ') – questo comando prende la variabile input_text e ne spezza una parte ogni volta che viene trovato uno spazio. Questo equivale a suddividere il testo di partenza nelle sue parole. Ad esempio, il testo “oggi è una giornata meravigliosa” viene spezzato in “oggi” + “è” + “una” + “giornata” + “meravigliosa.” Attenzione alla punteggiatura: il testo “Ciao, sono Kiki” viene spezzato in “Ciao,” + “sono” + “Kiki”. La virgola viene agganciata alla parola che la precede;
  2. output_chunks – questa è una lista; ciascun elemento della lista è una delle parti (detto “chunk”) in cui abbiamo suddiviso il testo;
  3. current_chunk_size – questo è un numero intero, che indica quante parole sono presenti nella parte di testo che stiamo costruendo (il “current chunk”);
  4. current_chunk_words – questa è una lista; ciasun elemento della lista è una delle parole che fa parte del chunk che stiamo costruendo;
  5. ' '.join(current_chunk_words) – questo comando fa un’operazione opposta a quella di split (primo punto), prende gli elementi della lista current_chunk_words e li mette tutti insieme, inserendo uno spazio tra un elemento e l’altro.

Una volta concluso il ciclo  for word in all_words facciamo una verifica per gestire le ultime parole che sono rimaste. Cerchiamo di capire il perché di questa verifica con un esempio.

Supponiamo che il testo iniziale contenga 25 parole e che vogliamo suddividerlo in parti con 10 parole ciascuna. Ci aspettiamo che alla fine ci siano 3 parti, le prime due con 10 parole ciascuna e l’ultima con 5 parole, giusto?

Benissimo, se non avessimo quel pezzettino di codice dopo il ciclo for, le ultime 5 parole si perderebbero e la funzione restituirebbe solo le prime 2 parti, di 10 parole ciascuna. Grazie a queste 3 righe aggiuntive, invece, riusciamo a creare un’ulteriore parte di testo, che contiene un chunk con le parole che sono rimaste.

Main code

Adesso che abbiamo la funzione che esegue lo splitting dobbiamo utilizzarla nel codice completo. In questo codice dobbiamo fare 3 operazioni:

  1. leggere il testo e caricarlo dentro una variabile
  2. scegliere quante parole vogliamo che abbiano i chunk
  3. chiamare la funzione di splitting e far fare a lei tutto il lavoro 🙂
# read data from file
filename = 'text_run_eng.txt'
with open(filename, 'r') as reader:
    input_raw_text = reader.read()
print('\nInput raw text is: \n{}\n'.format(input_raw_text))

# set the parameters for splitting the text
words_in_chunk = 10

# split the input text, call our new function:
output = text_splitting(input_raw_text, words_in_chunk)

Come testo iniziale abbiamo utilizzato quello con la citazione di Dean Karnazes sulla corsa, in lingua originale (inglese). E’ un testo molto breve, proprio quello che ci serve dato che il nostro obiettivo al momento è di scrivere lo splitter e verificare che funzioni.

Essendo un amante delle statistiche, non posso non includere anche un piccolo riepilogo quantitativo di quello che abbiamo fatto:

# print some general statistics
print('# General Statistics')
tot_chunk = len(output)
print('- The original text contains {} words'.format(len(input_raw_text.split(' '))))
print('- The desired chunk size is {} words'.format(words_in_chunk))
print('==>\n- The splitting function created {} chunks. Here they are:'.format(tot_chunk))
for i, chunk in zip(list(range(0, tot_chunk)), output):
    print('\tChunk n. {} ({} words) = {}'.format(i+1, len(chunk.split(' ')), chunk))

Ecco l’output a schermo del codice:

Input raw text is: 
I run because if I didn’t, I’d be sluggish and glum and spend too much time on the couch. I run to breathe the fresh air. I run to explore. I run to escape the ordinary. I run…to savor the trip along the way. Life becomes a little more vibrant, a little more intense. I like that.

# General Statistics
- The original text contains 57 words
- The desired chunk size is 10 words
==>
- The splitting function created 6 chunks. Here they are:
    Chunk n. 1 (10 words) = I run because if I didn’t, I’d be sluggish and
    Chunk n. 2 (10 words) = glum and spend too much time on the couch. I
    Chunk n. 3 (10 words) = run to breathe the fresh air. I run to explore.
    Chunk n. 4 (10 words) = I run to escape the ordinary. I run…to savor the
    Chunk n. 5 (10 words) = trip along the way. Life becomes a little more vibrant,
    Chunk n. 6 (7 words) = a little more intense. I like that.

Moduli in Python: a cosa servono

Il lavoro sullo splitting è finito e siamo stati bravi ad effettuarlo usando una funzione. In questo modo il codice principale è snello e semplice da seguire. Inoltre l’operazione di splitting può adesso essere fatta più volte senza codice ripetuto.

Ma non sarebbe bello se la funzione di splitting potesse essere usata anche da altri programmi e non solo da questo?

Ma certo che sarebbe bello! I moduli (o le librerie, come piace chiamarle a me in maniera un po’ vintage) servono proprio a questo. Grazie all’uso dei moduli possiamo gestire il nostro codice sorgente in maniera più ordinata, con diversi vantaggi (meno codice ripetuto, meno codice da mantenere, maggiore facilità di debug, …).

Tra l’altro i moduli sono anche estremamente facili da realizzare. Bisogna fare solo due cose: (i) impacchettare tutte le funzioni che ci interessano all’interno di un file separato e (ii) richiamare quel file dall’interno del codice che ne vuole fare uso.

Fino ad ora abbiamo usato sempre 1 solo file .py, dove inserivamo tutto il codice che ci serviva. E’ arrivato il momento di cambiare. Adesso usiamo 2 file .py, uno con il codice specifico del problema che stiamo risolvendo ed un con le funzioni che servono.

Come creare un modulo in Python

Procediamo subito a creare il nostro primo modulo in Python. Lo chiameremo NLP_base_library perché si tratta della nostra libreria con le funzioni di base per l’NLP. Questa sarà probabilmente la cosa più facile dell’intera serie …

Infatti non dobbiamo fare altro che creare un nuovo file .py, chiamarlo come abbiamo deciso, cioè NLP_base_library.py e fare un copia ed incolla al suo interno della funzione che abbiamo scritto prima.

Come usare i moduli in Python

Come facciamo adesso ad utilizzare la funzione che si trova nella libreria NLP_base_library.py all’interno del nostro codice?

Usiamo il comando import, come segue:

# ### Import section
from NLP_base_library import text_splitting

Come vedete è molto semplice. Possiamo generalizzare la struttura del comando in:

from <Nome libreria> import <nome funzione>

Una volta fatto ciò, possiamo utilizzare la funzione text_splitting semplicemente usando il suo nome, proprio come abbiamo fatto in precedenza.

Una variante del comando import è quella con cui s’importano tutte le funzioni all’interno della libreria, in questo caso il comando sarebbe

# ### Import section
import NLP_base_library

Struttura comando:

import <Nome libreria>

In questo caso per usare la funzione di splitting dobbiamo ripetere il nome della libreria, cioè non basta scrivere text_splitting(), ma bisogna invece scrivere NLP_base_library.text_splitting().

Alias

Infine, un’ultima variante del comando di import riguarda l’utilizzo di alias. Gli alias ci permettono di scegliere il nome che vogliamo usare nel nostro codice per chiamare ciò che importiamo, sia esso una libreria intera o una singola funzione. Ecco le due varianti viste prima con l’aggiunta di un alias:

# ### Import section
import NLP_base_library as NLP
from NLP_base_library import text_splitting as ts

Nel primo caso, per usare la funzione di text splitting dovremo scrivere NLP.text_splitting(); nel secondo invece ci basterà digitare ts().

Splitting sul Brown corpus e codice finale

Per ragioni puramente didattiche, mettiamo da parte il codice che abbiamo scritto finora e creiamo un nuovo file che utilizzerà il modulo che abbiamo appena costruito. In questo modo, il file precedente sarà sempre a disposizione e si potranno seguire nuovamente tutti i passaggi della parte iniziale di questo post.

Dato che ormai abbiamo avuto modo di testare la nostra nuova funzione, perché non applicarla adesso su un testo più consistente? Per questo possiamo usare il corpus Brown che è disponibile in NLTK. Andiamo dunque a modificare la parte del nostro codice dove leggiamo il testo di input da un file. Ricordo che, dopo aver installato NLTK (vedi primo post della serie), è necessario scariare i dataset che ci interessano (solo una volta).

# ### Import section
import nltk
# nltk.download('brown') # run only once 
from nltk.corpus import brown # totally 1.161.100 words 
from NLP_base_library import text_splitting  # as ts
# import NLP_base_library  # as NLP

# ### Data Import
# read data from file
# filename = 'text_run_eng.txt'
# with open(filename, 'r') as reader:
#     input_raw_text = reader.read()
# Read the data from the Brown corpus
input_raw_text = ' '.join(brown.words()[:10000])  # only take the first 10k words

Il resto del codice continua esattamente come prima. Beh si tratta di sole 2 righe di codice …

# set the parameters for splitting the text
words_in_chunk = 900

# split the input text, call our new function:
output = text_splitting(input_raw_text, words_in_chunk)
# output = ts(input_raw_text, words_in_chunk)
# output = NLP_base_library.text_splitting(input_raw_text, words_in_chunk)
# output = NLP.text_splitting(input_raw_text, words_in_chunk)

Per chiudere ci sono le statistiche generali. In questo caso però non è il caso di mostrare per intero il contenuto di ogni chunck dato che sarebbe troppo lungo. Così possiamo inserire un limite parole, in modo che vengano mostrate solo le prime parole di ciascun chunk (ad esempio le prime 20).

# print some general statistics
print('# General Statistics')
tot_chunk = len(output)
print('- The original text contains {} words'.format(len(input_raw_text.split(' '))))
print('- The desired chunk size is {} words'.format(words_in_chunk))
words_limit = 20
print('==>\n- The splitting function created {} chunks. Here they are:'.format(tot_chunk))
for i, chunk in zip(list(range(0, tot_chunk)), output):
    print('\tChunk n. {:2} ({} words) = {}'.format(i + 1, len(chunk.split(' ')), chunk[:20]))

Ed ecco l’ultimo output per questo articolo:

# General Statistics
- The original text contains 10000 words
- The desired chunk size is 900 words
==>
- The splitting function created 12 chunks. Here they are:
    Chunk n.  1 (900 words) = The Fulton County Gr
    Chunk n.  2 (900 words) = Aj . Henry L. Bowden
    Chunk n.  3 (900 words) = in a privilege resol
    Chunk n.  4 (900 words) = provide special scho
    Chunk n.  5 (900 words) = a water development 
    Chunk n.  6 (900 words) = family unity . Resea
    Chunk n.  7 (900 words) = them . Washington , 
    Chunk n.  8 (900 words) = nursing homes In the
    Chunk n.  9 (900 words) = always clear , despi
    Chunk n. 10 (900 words) = would be held respon
    Chunk n. 11 (900 words) = burglary and larceny
    Chunk n. 12 (100 words) = . There has been mor

Conclusioni

In questo articolo abbiamo visto come prendere un testo e suddividerlo in più parti di ugual numero di parole (tranne al più l’ultima parte). Non si tratta di un particolare concetto di NLP, ma è una procedura che a volte può essere molto comoda. Questo ci ha consentito di fare pratica con la programmazione in Python e di introdurre il concetto molto importante dei moduli, o librerie, esterne al codice principale.

Andando con ordine, in questo articolo abbiamo

  1. dato la nostra definizione di splitting;
  2. creato la funzione text_splitting(), che prende un testo di partenza e lo restituisce suddiviso in tante parti, ciascuna con lo stesso numero di parole (tranne al più l’ultima);
  3. creato un modulo esterno (o libreria) in modo tale che la funzione appena creata possa essere usata in qualsiasi altro codice Python realizziamo;
  4. imparato ad usare il comando import per caricare i moduli e le loro funzioni (in due varianti ed anche con e senza alias);
  5. imparato a caricare un corpus di quelli già inclusi in NLTK.

 

Per chi volesse approfondire

 

Nel complesso abbiamo generato 3 file. Tutti i file realizzati ed utilizzati in questa serie sono disponibili nel repository pubblico su GitHub. I file di questo argomento sono:

  1. 4.1_splitting.py questo è il file che contiene il codice principale ed anche la funzione di text splitting insieme;
  2. 4.2_splitting.py questo è il file che contiene solo il codice principale, senza la funzione;
  3. NLP_base_library.py è il file libreria, che contiene la funzione di text splitting.

 

Siamo quasi a metà strada, ben 4 argomenti su 9.

Il prossimo argomento sarà decisamente molto interessante, si tratta di uno dei modelli più utilizzati nell’NLP: Bag-of-words.

Non perdetevelo 😉