Lemmatization – NLP di base in Python [3 di 9]

Lemmatization

Premessa: in questo articolo l’equilibrio tra NLP in senso stretto e programmazione sarà più spostato verso la programmazione. Per la parte di NLP, spiegheremo cosa sia la lemmatizzazione e faremo molti esempi. Per la parte di programmazione, introdurremo degli strumenti molto importanti ed utili (le funzioni), e le utilizzeremo più volte lungo tutto il corso dell’articolo. Ma faremo anche molto altro credetemi 😉

La lemmatization (come nostra convenzione useremo il termine inglese) è il terzo argomento di questa serie introduttiva in 9 passi sull’NLP (Natural Language Processing) in Python. Nel primo passo abbiamo affrontato la tokenization, nel secondo lo stemming. Ecco i temi previsti per tutta la serie:

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

Lemmatization

La lemmatization è molto simile allo stemming, ma non proprio identica, vediamo in cosa sono uguali ed in cosa si differenziano.

  • Come lo stemming, anche la lemmatization cerca di ricondurre diverse forme flesse allo stesso tema.
  • Diversamente dallo stemming, la lemmatization cerca di usare come tema una parola di senso compiuto, il “lemma” appunto.

Ad esempio, se nel testo da analizzare compaiono le parole “correre”, “corro”, “corriamo”, “correremo”, lo stemming ricondurrà tutte queste forme flesse tutte al tema “corr”; mentre la lemmatization le ricondurrà tutte al lemma “correre”. Il risultato dello stemming non è errato, ma quello della lemmatization è migliore. A me piace pensare che lemmatization consente in qualche modo di mettere meglio a fuoco il tema.

Lemmatization in Python (vs Stemming)

Quick and dirty

Esistono numerosi pacchetti per implementare la lemmatization in Python, noi usiamo la classe WordNetLemmatizer che fa parte del pacchetto NLTK (che ci accompagna per tutta la serie). WordNet è un esteso database lessicale della lingua inglese realizzato dalla Princeton University nel 2010. E’ gratuito e pubblicamente disponibile per l’utilizzo ed il download (basta seguire il link indicato sopra). Se siete interessati potete anche giocarci online da questo link: use WordNet online.

Costruiamo subito il nostro primo lemmatizer in Python e confrontiamo il risultato con lo stemmer. Facciamo un esempio con le parole “wolves” e “wolf” (rispettivamente “lupi” e “lupo” in inglese). Il procedimento è sempre lo stesso usato negli articoli precedenti, i.e. si crea un’istanza della classe e poi si applica il metodo appropriato (che in questo caso è lemmatize) all’istanza:

from nltk.stem import WordNetLemmatizer, SnowballStemmer

print('\n=== Get a lemma quickly ===\n')
first_lemmatizer = WordNetLemmatizer()
usual_stemmer = SnowballStemmer('english')
input_wordlist = ['wolves', 'wolf']
for word in input_wordlist:
    lemma = first_lemmatizer.lemmatize(word)
    stem = usual_stemmer.stem(word)
    print('Input word: \t{}'.format(word))
    print('\tOutput Lemma: \t{}'.format(lemma))
    print('\tOutput Stem: \t{}\n'.format(stem))

Se facciamo girare il codice otteniamo questo output:

=== Get a lemma quickly ===

Input word: 	wolves
    Output Lemma:   wolf
    Output Stem:    wolv

Input word: 	wolf
    Output Lemma:   wolf
    Output Stem:    wolf

Il lemmatizer restituisce come output “wolf” per entrambe le parole, invece lo stemmer dà “wolv” per la prima e “wolf” per la seconda. E’ il comportamento che ci aspettavamo?
Beh, direi di si! Il lemmatizer ha capito che “wolves” e “wolf” sono forme flesse dello stesso lemma “wolf”, mentre lo stemmer ha ritenuto che le due parole si rifacessero a due temi diversi. Diciamo che ho giocato un po’ sporco, dato che “wolves” è una parola che fa eccezione, ed il pover stemmer è caduto nella trappola!

Molto bene, passiamo adesso ad applicare la lemmatization ad un testo più corposo. Come abbiamo appena visto, WordNetlemmatizer opera sulle singole parole, dobbiamo dunque prendere il testo di interesse, spezzarlo in parole singole (tokenizzarlo) ed inserire i token all’interno di una lista.

Scriviamo la nostra prima funzione

Nessuno ha per caso un senso di deja vu? Eh si, perché questa stessa procedura l’abbiamo applicata nel caso dello stemming.

Quando si programma è bene evitare di avere del codice ripetuto, (anche) per questa ragione si usano le funzioni. Procediamo dunque a creare una funzione che, dato il nome di un file dove è salvato il testo di interesse, restituisce in output le sue parole all’interno di una lista. In questo modo, ogni volta che si dovrà eseguire la tokenization di un testo, basterà chiamare questa funzione, senza bisogno di scrivere il codice di nuovo.

Andiamo dunque a copiare il codice che abbiamo già scritto per lo stemming (quando abbiamo preparato i dati di input per lo stemmer di Porter e di Lancaster) e inseriamo poi questo codice dentro un blocco che inizia per “def”:

def split_to_words(input_filename):
    # read data from file
    with open(input_filename, 'r') as reader:
        input_raw_text = reader.read()
    print('\nInput raw text is: \n{}'.format(input_raw_text))
    # split data into words
    custom_tokenizer = RegexpTokenizer('\w+')
    word_list = custom_tokenizer.tokenize(input_raw_text)
    return word_list

Vale la pena soffermarsi su due punti:

  1. il termine def deve essere seguito dal nome che vogliamo dare alla funzione, che a sua volta deve essere seguito dagli eventuali parametri di input che accetta la funzione, inseriti tra parentesi tonde, e poi la riga si deve concludere con i due punti;
  2. tutte le operazioni da compiere all’interno della funzione devono essere indentate sotto il def.

Secondo le best practice di scrittura di Python (PEP 8) è consigliato di lasciare 2 righe vuote prima dell’inizio e dopo la fine di ogni def.

Se adesso vogliamo ottenere la lista delle parole contenute in un documento non ci resta che chiamare la funzione fornendo tra parentesi il nome del file di testo che contiene il documento:

from nltk.tokenize import RegexpTokenizer
test_filename = 'text_run_eng.txt'
test_word_list = split_to_words(test_filename)

Vi siete ricordati di importare RegexpTokenizer? Viene usato dalla funzione.

Per verificare il corretto funzionamento della nostra prima funzione basta esplorare il contenuto della variabile test_word_list (che deve essere una lista di stringhe).

“They had lots of chances of turning back, only they didn’t”

Questa citazione fa parte del testo con cui lavoreremo oggi: un estratto da The Two Towers, Book 2, “Chapter VIII: The Stairs of Cirith Ungol,” p. 719. Lo trovate nel repository github con il nome “text_lotr_sam_turningback.txt”. Come Frodo e Sam, procediamo dunque nel nostro viaggio, senza cedere alla tentazione di tornare indietro 🙂 Il prossimo step è quello di preparare i dati secondo quanto già fatto nell’articolo sullo stemming. Ci sono 3 semplici passi:

  1. Come prima cosa carichiamo i dati (usando la nostra bellissima nuova funzione)
  2. poi rimuoviamo i duplicati
  3. ed infine rimuoviamo le parole da 1 a 3 caratteri.

Ecco il codice per la preparazione del testo di input:

# ###    Prepare input data    ###
# Get the data with our custom function
filename = 'text_lotr_sam_turningback.txt'
word_list = split_to_words(filename)
# remove duplicates
word_list_unique = list(set(word_list))
# remove words with less than 4 characters
word_list_final = [x for x in word_list_unique if len(x) > 3]

Se adesso vogliamo conoscere i lemmi di questo testo, non dobbiamo fare altro che creare un lemmatizer ed applicarlo a ciascun elemento della lista di parole appena creata, word_list_final:

# Create the instance of the Lemmatizer
lemmatizer = WordNetLemmatizer()
# apply the lemmatizer to the word list and store result into a new list
lemma_list = []
for word in word_list_final:
    lemma_list.append(lemmatizer.lemmatize(word))

La variabile lemma_list contiene i lemmi delle parole del testo di input.

L’articolo potrebbe concludersi qua. Infatti adesso sappiamo cosa sia la lemmatization, sappiamo applicarla in Python ed abbiamo anche imparato a scrivere ed usare le funzioni.

C’è però un semplice dettaglio che aumenta il potere della lemmatization enormemente. Per questo motivo ho deciso di continuare (ed anche per fare un po’ di pratica in più con le funzioni). Se avete un minimo di interesse verso l’NLP e la programmazione continuate il viaggio con me, vi garantisco che non ve ne pentirete 😉

PoS: Part of Speech

Per funzionare al meglio, le lemmatization ha bisogno di sapere il tipo di parola su cui sta operando, quello che in inglese viene denominato PoS, Part of Speech (letteralmente “parte del discorso”). Si tratta di sapere se la parola è ad esempio un verbo o un nome. Il lemmatizer di NLTK accetta 4* diversi PoS (aggetivo, avverbio, nome e verbo) che possono essere passati direttamente quando si applica il lemmatizer (usando rispettivamente pos=a, r, n, v). *In realtà sono 5, perché gli aggettivi sono divisi in due categorie.

Vediamo un veloce esempio che illustra anche i diversi risultati in base al PoS che si usa.

# ### Testing the PoS ###
print('\nGet a lemma using PoS')
test_word = 'known'
print('Testing the word "{}"'.format(test_word))
print('\t Lemma when using "noun" = {}'.format(lemmatizer.lemmatize(test_word, pos='n'))) # if no 'pos' is passed, 'pos=n' is the default
print('\t Lemma when using "verb" = {}'.format(lemmatizer.lemmatize(test_word, pos='v')))
# where pos='n' means Part Of Speech = noun
# and pos='n' means Part Of Speech = verb

Output:

Get a lemma using PoS
Testing the word "known"
     Lemma when using "noun" = known
     Lemma when using "verb" = know

In questo esempio vediamo come per la parola “known” vengano suggeriti due lemmi diversi in base al PoS che si usa:

  1. se PoS = noun, allora il lemmatizer restituisce “known”
  2. se PoS = verbo, allora il lemmatizer restituisce “know”

In questo caso la scelta corretta è PoS = verbo ed il lemma è “know”.

Confronto tra Lemmatizer con PoS e Stemmer (seconda funzione)

Vorrei vedere degli altri esempi per capire meglio gli effetti della PoS. Ma non voglio soltanto confrontare la lemmatization con e senza la PoS, voglio anche confrontarla con lo stemming. Per questo motivo creiamo una nuova funzione che, data una lista di parole di input, restituisce le lemmatization con la PoS = n (nome), quella con la PoS = v (verbo) e lo stemming (indipendente dalla PoS).

# Compare results of Stemming and Lemmatizing (with noun and with verb)
def compare_lemmatize_vn_stemmer(word_list):
    # for each word of the input list
    #   get the stemmed word
    #   get the lemmas with pos = noun and pos = verb
    #   print the original word, the stem and the two lemmas
    stemmer_snowball = SnowballStemmer('english')
    header = ['Original', 'Stem', 'Lemma noun', 'Lemma verb']
    row_format = '{:>10}:' + '  {:^12} -' * 2 + '{:^12}|'
    print('-' * 56)
    print(row_format.format(*header))
    print('-' * 56)
    for word in word_list:
        stemmed = stemmer_snowball.stem(word)
        lemma_n = lemmatizer.lemmatize(word, pos='n')
        lemma_v = lemmatizer.lemmatize(word, pos='v')
        print(row_format.format(word, stemmed, lemma_n, lemma_v))
    print('-' * 56 + '\n')
    return

Grazie a questa funzione, non solo fare il confronto è facilissimo (basta solo passare la lista di parole di interesse), ma inoltre l’output sarà più gradevole alla lettura e faciliterà l’analisi.

Proviamo la nostra seconda funzione con una lista di 4 forme flesse del verbo “to go”. Ci servono solo una manciate di righe di codice (ne basterebbe una sola, ma qui abbondo e ne uso 3):

print('Test with verb "to go"')
input_wordlist = ['gone', 'goes', 'went', 'go']
compare_lemmatize_vn_stemmer(input_wordlist)

Ecco l’output:

Test with verb "to go"
--------------------------------------------------------
  Original:      Stem     -   Lemma noun  - Lemma verb |
--------------------------------------------------------
      gone:      gone     -      gone     -     go     |
      goes:      goe      -       go      -     go     |
      went:      went     -      went     -     go     |
        go:       go      -       go      -     go     |
--------------------------------------------------------

In questo caso sia “Stem” che “Lemma noun” non fanno un buon lavoro. “Lemma verb” invece funziona perfettamente, restituendo “go” per tutte e 4 le parole.

Se adesso vogliamo fare un altro test, non ci basta che usare le stesse tre righe di codice di prima, cambiando solo le parole da testare. Ad esempio, scegliamo adesso 4 parole riguardo al verbo “to dream”:

print('Test with verb "to dream"')
input_wordlist = ['dreamt', 'dreams', 'dreaming', 'dream']
compare_lemmatize_vn_stemmer(input_wordlist)

Output:

Test with verb "to dream"
--------------------------------------------------------
  Original:      Stem     -   Lemma noun  - Lemma verb |
--------------------------------------------------------
    dreamt:     dreamt    -     dreamt    -   dream    |
    dreams:     dream     -     dream     -   dream    |
  dreaming:     dream     -    dreaming   -   dream    |
     dream:     dream     -     dream     -   dream    |
--------------------------------------------------------

In questo caso “Stem” fa un lavoro migliore di prima, restituendo valori quasi identici a “Lemma verb”. “Lemma noun” invece è completamente fuori strada.

Infine, possiamo usare la funzione per testare nuovamente il caso di “wolves” e “wolf” visto all’inizio:

print('Test with noun "wolf"')
input_wordlist = ['wolves', 'wolf']
compare_lemmatize_vn_stemmer(input_wordlist)

Output:

Test with noun "wolf"
--------------------------------------------------------
  Original:      Stem     -   Lemma noun  - Lemma verb |
--------------------------------------------------------
    wolves:      wolv     -      wolf     -   wolves   |
      wolf:      wolf     -      wolf     -    wolf    |
--------------------------------------------------------

Qui vediamo che “Lemma verb” non funziona più bene, mentre “Lemma noun” è corretto, come è giusto che sia dato che la parola su cui stiamo lavorando è un nome e non un verbo. “Stem” è diverso da entrambi e continua a non essere la soluzione migliore.

Dovrebbe ormai essere chiaro che, quando si usa la PoS appropriata, le lemmatization restituice lo stesso lemma per tutte le forme flesse della parola, che è proprio il comportamento desiderato.

Automatizzare la PoS: sistema di taggatura automatico

Adesso viene la parte che mi piace di più: automazione!

Abbiamo capito che per utilizzare la lemmatization per bene occorre indicare la PoS corretta per ciascuna parola. Ma mica possiamo farlo a mano parola per parola, no? Appunto, no! Ci piacerebbe invece avere un sistema che possa fare questo per noi, vogliamo creare un cosiddetto sistema di taggatura automatico, un sistema cioè che in maniera autonoma possa indicare la PoS di ciascuna parola.

Benissimo, costruiamolo insieme. Lo faremo in due passi:

  1. Taggatura della parola di interesse, tramite pos_tag;
  2. Mappatura del tag generato nello step 1 in uno dei tag accettati dal lemmatizer di WordNet.

1. Taggatura con pos_tag

pos_tag è una funzione disponibile nel pacchetto NLTK che prende in input una parola (in inglese) e restituisce in output il suo tag da una lista di 35 possibili valori.

Il procedimento è molto semplice, come indicato di seguito:

from nltk.tag import pos_tag

print('Auto tag for the test word "{}" = {}'.format(test_word, pos_tag([test_word])))

Nel caso della parola di test “known” (si noti che deve essere passata come lista) il codice restituisce l’output “[(‘known’, ‘VBN’)]”:

Auto tag for the test word "known" = [('known', 'VBN')]

L’output è dunque una lista di tuple, dove ogni tupla contiene 2 elementi: il primo è la parola stessa di input, il secondo è la PoS. Se quindi vogliamo indicare solo il tag di output, dobbiamo scrivere pos_tag([test_word])[0][1].

Come vedete usare pos_tag è abbastanza semplice, bisogna solo fare attenzione a preparare l’input in maniera corretta.

2. Mappatura delle PoS

Come indicavo in precedenza, pos_tag ha un dizionario di 35 possibili valori per la PoS, mentre il lemmatizer di WordNet ne accetta solo 5. Dobbiamo dunque creare un sistema che associ ciascuno dei 35 valori di PoS di pos_tag ad uno solo dei 5 valori di PoS accettato dal lemmatizer di WordNet. Questo lo faremo creando una nuova funzione. La funzione userà una mappatura basata sulla lettera iniziale dei tag restituiti da pos_tag:

  1. iniziale = J –> aggettivo –> pos = a
  2. iniziale = N –> nome –> pos = n
  3. iniziale = V –> verbo –> pos = v
  4. iniziale = R –> avvernio –> pos = r

Con questa mappatura il quinto caso pos = s, che è usato dal WordNet lemmatizer per aggettivi satellite, viene ricondotto al caso degli aggettivi (pos = a).

Ecco una funzione che esegue questa mappatura:

from nltk.corpus import wordnet

def map_postag_into_wordnet(postag):
    # input: value from pos_tag
    # output: value for WordNet lemmatizer
    # mapping logic:
    #   pos_tags that begin with J are adjectives
    #                       with V are verbs
    #                       with N are nouns
    #                       with R are adverbs
    #
    # Create a dictionary with the mapping:
    tag_map = {"j": wordnet.ADJ,
               "n": wordnet.NOUN,
               "v": wordnet.VERB,
               "r": wordnet.ADV}
    # Create a default option, to be used when the mapping fails:
    default_pos = wordnet.NOUN
    # Now return the value for the appropriate key (key = 1st letter of the postag)
    #   if the key is not found, return the chosen default
    wordnet_tag = tag_map.get(postag[0].lower(), default_pos)
    return wordnet_tag

Piccola nota:

il metodo .get applicata ad un dizionario restituisce il valore associato alla chiave passata come argomento. Se la chiave non viene trovata nel dizionario, allora restituisce il secondo argomento passato in input.

Facciamo un test passando a mano una serie di 5 tag e vediamo quali output restituisce la funzione:

# TEST for map_postag_into_wordnet function:
test_pos_tag = ['JJ', 'NNS', 'VBN', 'RBR', 'missing_tag']
print('\nTest for the mapping function:')
for input_tag in test_pos_tag:
    mapped_tag = map_postag_into_wordnet(input_tag)
    print('\tInput tag \'{}\' is mapped into \'{}\''.format(input_tag, mapped_tag))

L’output è quello atteso:

Test for the mapping function:
    Input tag 'JJ ' is mapped into 'a'
    Input tag 'NNS' is mapped into 'n'
    Input tag 'VBN' is mapped into 'v'
    Input tag 'RBR' is mapped into 'r'
    Input tag 'missing_tag' is mapped into 'n'

3. Mettiamo i due pezzi insieme

Adesso non ci resta che mettere insieme i due pezzi che abbiamo costruito per poter taggare un qualsiasi testo di input con delle PoS compatibili per il nostro WordNet lemmatizer. Questo ci permetterà di usare il lemmatizer al massimo delle sue potenzialità. Mettiamo subito in pratica con un test:

auto_tag = pos_tag([test_word])[0][1]  # step 1
wrdnt_tag = map_postag_into_wordnet(auto_tag)  # step 2
lemma_2 = lemmatizer.lemmatize(test_word, pos=wrdnt_tag)  # step 3
print('\nGet the lemma using auto-tagging and custom mapping function.')
print('\tInput word = \'{}\' ---> Lemma = \'{}\' \t(tag = {})'.format(test_word, lemma_2, wrdnt_tag))

Che restituisce il seguente output:

Get the lemma using auto-tagging and custom mapping function.
    Input word = 'known' ---> Lemma = 'know' 	(tag = v)

Lemmatization su un testo, con PoS automatica

Siamo adesso pronti a mettere tutti i pezzi insieme:

  1. prendiamo un testo di input e suddividiamolo in parole singole (già fatto con la funzione split_to_words)
  2. ripuliamolo (già fatto nella sezione di codice # Prepare input data)
  3. applichiamo la taggatura automatica (già fatto con pos_tag)
  4. mappiamo i tag automatici su tag di WordNet (già fatto con la funzione map_postag_into_wordnet)
  5. applichiamo il lemmatizer a ciascuna parola del testo passando la PoS appropriata

L’ultimo punto è l’unico per cui non abbiamo scritto il codice. Facciamolo subito!

# Get the lemmas for all of the words:
print('\nGet the lemmas with PoS for all of the words')
tagged_word_list = pos_tag(word_list_final)  # list of tuples with (word, postag)
lemma_list = []
for word, tag in tagged_word_list:  # split each tuple within the loop
    wrdnt_tag = map_postag_into_wordnet(tag)
    lemma = lemmatizer.lemmatize(word, pos=wrdnt_tag)
    lemma_list.append(lemma)

La variabile lemma_list contiene adesso tutti i lemmi del nostro testo iniziale, ottenuti con la corretta PoS.

Giusto per togliermi uno sfizio faccio ancora due cose:

  1. confronto lo stemming e la lemmatization con PoS sul testo completo
  2. creo una funzione per stampare le tabelle di confronto che tanto mi piacciono (quelle introdotte nell’articolo 2, sezione sulla visualizzazione)

Funzione per la tabella di confronto

Per prima cosa faccio la funzione di confronto.

Ne faccio una semplice e la chiamo con il suffisso “1”, magari in futuro ne farò delle versioni migliori:

# Function for my Comparison Table
def print_cmp_table1(header, row_format, row_length, col1, col2, col3, col4):
    print(row_format.format(*header))
    print('-' * row_length)
    for c1, c2, c3, c4 in zip(col1, col2, col3, col4):
        print(row_format.format(c1, c2, c3, c4))
    print('-' * row_length)
    print(row_format.format(*header))

Come vedete sto assumento che la tabella abbia 4 colonne e che ciascuna colonna venga passata come lista nell’input della funzione.

Adesso per stampare la tabella di confronto bassa passare alla funzione le 4 grandezze che si vogliono confrontare ed i parametri della tabella, cioè l’intestazione (“header”), il formato di ogni riga (“row_format”) e la lunghezza della riga (“row_length”).

Confronto finale

Procediamo adesso con il confronto finale tra (1) la parola originaria per come è presente nel testo, (2) il risultato dello Stemmer, (3) il risultato della Lemmatization senza PoS (che equivale ad usare pos = n per tutte le parole) e (4) il risultato della Lemmatization con PoS (calcolata automaticamente). Ovviamente uso la funzione appena definita.

# Print a comparison table for Stemmer, Lemmatizer without PoS and Lemmatizer with PoS
# for the whole input text
print('\n\nComparison among Stemmer, Lemmatizer without PoS (i.e. use noun for all the words) '
'and Lemmatizer with PoS on the whole text.\n')
# Get the list of words for the Stemmer and for the Lemmatizer without PoS
stemmer_snowball = SnowballStemmer('english')
stem_list = []
lemma_wo_POS = []
for word in word_list_final:
    stem_list.append(stemmer_snowball.stem(word))
    lemma_wo_POS.append(lemmatizer.lemmatize(word))
# Set the parameters for the comparison table
header = ['Original Word', 'SnowBall', 'Lemmatizer wo PoS', 'Lemmatizer w PoS']
row_format = '{:<15}:' + '{:^12} -' + '{:^20} -' + '{:^21}|'
raw_length = 74
# call the function
print_cmp_table1(header, row_format, raw_length, word_list_final, stem_list, lemma_wo_POS, lemma_list)

Ed ecco la prima parte dell’output (fino alle prime 10 parole):

Comparison among Stemmer, Lemmatizer without PoS (i.e. use noun for all the words) and Lemmatizer with PoS on the whole text.

Original Word  :  SnowBall   - Lemmatizer wo PoS   -  Lemmatizer w PoS   |
--------------------------------------------------------------------------
expect         :   expect    -       expect        -       expect        |
coming         :    come     -       coming        -        come         |
here           :    here     -        here         -        here         |
started        :   start     -      started        -        start        |
home           :    home     -        home         -        home         |
wonderful      :   wonder    -     wonderful       -      wonderful      |
just           :    just     -        just         -        just         |
things         :   thing     -       thing         -        thing        |
forgotten      : forgotten   -     forgotten       -       forget        |
tale           :    tale     -        tale         -        tale         |

Per fare un confronto dei 3 metodi bisogna valutare un gruppo più ampio di parole (in tutto ne abbiamo 81). Se ci limitiamo a queste 10 parole scelte a caso, possiamo notare che:

  1. nel 50% dei casi la parola originaria non è in una forma flessa (si tratta di “expect”, “here”, “home”, “just” e “tale”). Tutti e 3 i metodi concordano su questo punto;
  2. nel restante 50%:
    • i 3 metodi concordano solo quando si tratta di plurale/singolare (parola iniziale “things” che viene ricondatta a “thing”)
    • su tutti gli altri casi, di verbi ed aggettivi (“coming”, “started”, “wonderful” e “forgotten”), sono in disaccordo.

Nel complesso, a me sembra che la lemmatization con la PoS sia effettivamente la soluzione migliore.

Conclusioni

Il tema trattato in questo articolo, la lemmatization, è molto simile a quello dell’articolo precedente, lo stemming. Per questo motivo ci sono pochi elementi di novità rispetto a quanto già visto in ambito NLP. Mi sembrava dunque il momento giusto per dare più spazio alla parte di programmazone in Python, introducendo le funzioni ed utilizzandole in vari punti, e creando un sistema di taggatura automatica della PoS (Part of Speech) per poter eseguire la lemmatizzazione in maniera completa senza intervento umano.

Riepiloghiamo con ordine tutto quello che abbiamo fatto in questo articolo:

  1. Definizione della lemmatization, in confronto con lo stemming
  2. Creazione immediata di un lemmatizer in Python con WordNet in NLTK
  3. Creazione di una funzione per leggere un file di testo e suddividerlo in parole
  4. Definizione della PoS (Part of Speech) e numerosi esempi di quanto sia importante per la lemmatization
    • Creazione di una funzione per confrontare lemmatization con PoS = v, n e stemming
  5. Sistema automatico per applicare il lemmatizer con la PoS ad un qualunque testo di input anche se le parole non sono eitchettate con la loro PoS
    • utilizzo di pos_tag per il tag automatico della parola
    • creazione di una funzione di mapping per associare il tag di pos_tag ad un tag compatibile con il nostro lemmatizer
  6. Confronto finale tra (1) parola usata nel testo, (2) tema restituito dallo stemmer, (3) lemma restituito dal lemmatizer senza PoS e (4) lemma restituito dal lemmatizer con PoS

 

Ecco alcuni link utili sulla lemmatization per chi vuole approfondire:


Con questo si conclude il terzo argomento della serie di introduzione all’NLP (Natural Language Processing) in Python. La serie affronta in tutto 9 argomenti, il primo è stato la tokenization, il secondo lo stemming, il terzo la lemmatization.

Vi ricordo che tutto il codice utilizzato per questa serie (inclusi i testi di input) è condiviso secondo i canoni del software Open Source e lo trovate nel mio repository pubblico su github. Invito tutti a scaricarlo e modificarlo tutte le volte che volete 🙂

Come al solito, sarò molto lieto di ricevere qualsiasi vostro dubbio, domanda o suggerimento.