Tokenizzazione

Con il termine tokenizzazione si intende la riduzione di un testo in unità semantiche fondamentali chiamate tokens. Nella stragrande maggioranza delle applicazioni i token corrispondono alle parole (word tokenization), ma in casi particolari possono essere anche sillabe, frasi, paragrafi o anche singoli caratteri.

In generale, possiamo pensare ai token come alle più elementari unità di testo che il nostro modello sarà in grado di gestire.

Tokenizzazione di testi con nltk

Varie librerie python di NLP includono utilities per la tokenizzazione. Quella che utilizzo e che consiglio è anche una delle più popolari: NLTK (https://www.nltk.org/).

Installazione

NLTK può essere installata sia con pip che con conda:

pip install nltk
:: altrimenti
conda install -c anaconda nltk

Per usare le utility di tokenizzazione dovremo installare alcune estensioni di nltk. Possiamo farlo direttamente dal codice python:

import nltk
nltk.download('punkt')

Come esempio di testo useremo l’articolo 1 della costituzione italiana. L’idea è quella di vedere come cambia il risultato in funzione del tokenizzatore scelto.

s = """
L’Italia è una Repubblica democratica, fondata sul lavoro.

La sovranità appartiene al popolo, che la esercita nelle forme e nei limiti della Costituzione.
"""

White space Tokenizer

Si tratta di un word tokenizer che usa gli spazi bianchi (space, tab e newline) tra le parole per delimitare i token.

Ne esiste un’implementazione in NLTK:

from nltk.tokenize import WhitespaceTokenizer
WhitespaceTokenizer().tokenize(s)

… ma l’effetto sul testo è lo stesso che si otterrebbe invocando il metodo .split() della classe stringa:

['L’Italia', 'è', 'una', 'Repubblica', 'democratica,', 'fondata', 'sul', 'lavoro.', 'La', 'sovranità', 'appartiene', 'al', 'popolo,', 'che', 'la', 'esercita', 'nelle', 'forme', 'e', 'nei', 'limiti', 'della', 'Costituzione.']

Come si può notare, questa tokenizzazione fatta solo sfruttando gli spazi non separa correttamente le parole: gli articoli apostrofati vengono inclusi nello stesso token della parola che li segue, mentre la punteggiatura è inclusa nel token della parola che la precede.

Questo porta ad avere token diversi con lo stesso significato: ad sempio ‘lavoro’ e ‘lavoro,’ sono considerati token diversi, che è un effetto indesiderato.

Word Punct Tokenizer

Il Word Punct Tokenizer è un word tokenizer che utilizza sia gli spazi che la punteggiatura per delimitare le parole:

from nltk.tokenize import WordPunctTokenizer
WordPunctTokenizer().tokenize(s)

Vediamo che ora i segni di punteggiatura non sono più tokenizzati insieme alle parole a loro adiacenti, ma sono considerati tokens a sé stanti:

['L', '’', 'Italia', 'è', 'una', 'Repubblica', 'democratica', ',', 'fondata', 'sul', 'lavoro', '.', 'La', 'sovranità', 'appartiene', 'al', 'popolo', ',', 'che', 'la', 'esercita', 'nelle', 'forme', 'e', 'nei', 'limiti', 'della', 'Costituzione', '.']

Line Tokenizer

Si tratta di un tokenizer che utilizza i caratteri di new line (le andate a capo) per delimitare i token:

from nltk.tokenize import LineTokenizer
LineTokenizer().tokenize(s)

output:

['L’Italia è una Repubblica democratica, fondata sul lavoro.', 'La sovranità appartiene al popolo, che la esercita nelle forme e nei limiti della Costituzione.']

Sentence Tokenizer

Un tokenizer che segmenta il testo in singole frasi:

from nltk.tokenize import PunktSentenceTokenizer
PunktSentenceTokenizer().tokenize(s)

Sulla nostra frase di esempio l’output sarà il seguente:

['\nL’Italia è una Repubblica democratica, fondata sul lavoro.', 'La sovranità appartiene al popolo, che la esercita nelle forme e nei limiti della Costituzione.']

Syllabe Tokenizer

L’ultimo esempio di questa carrellata di tokenizzatori è anche quello concettualmente meno immediato: il syllabe tokenizer. Si tratta di un algoritmo che, tramite regole euristiche (dipendenti dalla lingua che stiamo utilizzando) permette di segmentare un testo in sillabe.

from nltk.tokenize import SyllableTokenizer
sonority_hierarchy = [
               "aàeèéiìoòuù",  # vocali.
               "lmnrw",        # consonanti nasali
               "zvsf",         # consonanti fricative
               "bcdgtkpqxhj,.;",  # stops.
           ]
syllabe_tokenizer = SyllableTokenizer(lang="ita",
                                      sonority_hierarchy=sonority_hierarchy)
syllabe_list = list(map(lambda x : x.replace(" ", "").replace("\n", ""), syllabe_tokenizer.tokenize(s)))
syllabe_tokenizer.validate_syllables(syllabe_list)

Affinché il SyllableTokenizer funzioni correttamente, è necessario specificare la lingua del testo sul quale vogliamo utilizzarlo e la lista delle sonorità della stessa (quali caratteri devono essere interpretati come vocali, quali come consonanti nasali, quali come fricativi e quali implicano una pausa).

Sulla nostra frase di esempio il risultato sarà il seguente:

['', 'L’I', 'ta', 'lia', '', 'è', '', 'u', 'na', 'Re', 'pub', 'bli', 'ca', 'de', 'mo', 'cra', 'ti', 'ca', ',', 'fon', 'da', 'ta', 'sul', 'la', 'vo', 'ro', '.', 'La', 'so', 'vra', 'ni', 'tà', 'ap', 'par', 'tie', 'ne', 'al', 'po', 'po', 'lo,c', 'he', 'la', 'e', 'ser', 'ci', 'ta', 'nel', 'le', 'for', 'me', 'e', '', 'nei', '', 'li', 'mi', 'ti', 'del', 'la', 'Cos', 'ti', 'tu', 'zio', 'ne', '.']

Applicazione: la metrica poetica

Le applicazioni della tokenizzazione sono numerose quanto quelle dell’NLP stessa: ogni pipeline di preprocessing inizia con una qualche forma di tokenizzazione.

Tuttavia, dovendo scegliere un esempio di applicazione che fosse al tempo stesso semplice (o, perlomeno, che non sfruttasse concetti non spiegati in questo articolo) ed interessante, ho pensato di mostrare come è possibile utilizzare il LineTokenizer ed il SyllableTokenizer in combinazione per verificare la struttura metrica di una poesia.

Per questo esercizio ho scelto quelli che sono forse tra i versi più famosi della poesia italiana: l’incipit dell’Inferno di Dante Alighieri:

from nltk.tokenize import LineTokenizer
from nltk.tokenize import SyllableTokenizer

text = """Nel mezzo del cammin di nostra vita
           mi ritrovai per una selva oscura,
           ché la diritta via era smarrita."""

Dai nostri ricordi scolastici sappiamo che la Divina Commedia è composta da endecasillabi, cioé da versi composti da 11 sillabe.

Vogliamo vedere se, con un semplice script python, riusciamo a verificare il conteggio delle sillabe di questi primi tre versi.

Per questo esercizio, come anticipato, possiamo usare il LineTokenizer per scomporre la terzina in versi. Sui singoli versi applicheremo poi il SyllableTokenizer per la divisione in sillabe e stamperemo in std output sia i versi sillabati che il conteggio.

sonority_hierarchy = [
               "aàeèéiìoòuù",  # vocali.
               "lmnrw",        # consonanti nasali
               "zvsf",         # consonanti fricative
               "bcdgtkpqxhj,.;",  # stops.
           ]

lines = LineTokenizer().tokenize(text)


for line in lines:
    syllabe_tokenizer = SyllableTokenizer(lang="ita",
                                          sonority_hierarchy=sonority_hierarchy)

    syll_list = list(map(lambda x : (x.replace(" ", "")
                                      .replace("\n", "")
                                      .replace(",", "")),
                         syllabe_tokenizer.tokenize(line)))
    
    syll = list(filter(len, syllabe_tokenizer.validate_syllables(syll_list)))
    print(syll, len(syll))

L’output che otteniamo è il seguente:

['Nel', 'mez', 'zo', 'del', 'cam', 'min', 'di', 'nos', 'tra', 'vi', 'ta'] 11                                                                                                                      ['mi', 'ri', 'tro', 'vai', 'pe', 'ru', 'na', 'sel', 'va', 'os', 'cu', 'ra'] 12                                                                                                                    ['ché', 'la', 'di', 'rit', 'ta', 'via', 'e', 'ra', 'smar', 'ri', 'ta.'] 11   

Come possiamo vedere, l’algoritmo è riuscito ad identificare correttamente la lunghezza in sillabe del primo e del terzo verso. Il secondo verso viene invece rilevato come composto da 12 sillabe.

In realtà, il nostro script non ha torto, né lo ha l’autore della Commedia (ci mancherebbe!). Il secondo verso ha effettivamente 12 sillabe grammaticali ma, dal punto di vista metrico, l’ultima sillaba di selva e la prima di oscura sono fuse insieme per effetto della sinalefe (https://it.wikipedia.org/wiki/Sinalefe), quindi vanno considerate come un’unica sillaba.

In conclusione, usando solo le utility di tokenizzazione di NLTK, siamo riusciti sia a verificare la struttura metrica della Divina Commedia, sia ad identificare la presenza di una sinalefe nel secondo verso. Non male, come primo passo nel nostro viaggio nella NLP!

Se ti va, prova ad eseguire lo script quì sopra su una poesia a tua scelta, e fammi sapere nei commenti se riesci ad ottenere la sillabazione corretta.

Codice

Il codice usato in questo articolo può essere scaricato quì.