Bag of Words – NLP di base in Python [5 di 9]

Bag of words

La cosiddetta “Bag of words” è probabilmente una delle più note rappresentazioni utilizzate in ambito NLP. Ma cosa è esattamente una “rappresentazione”? Beh, secondo me si tratta di uno dei pilastri dell’intelligenza artificiale e del machine learning in particolare!

Vedremo presto di cosa si tratta, ma procediamo con calma. Come prima cosa ricordiamo che questo articolo è parte di una serie introduttiva al Natural Language Processing (NLP) di base in Python. La serie è composta da 9 argomenti e con questo articolo trattiamo l’argomento numero 5:

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

Rappresentazione nel Machine Learning

Bene dunque, torniamo al nostro pilastro del machine learning: il concetto di “rappresentazione”. Proverò a descriverlo in maniera semplice e sintetica.

Trovare una rappresentazione di un fenomeno per il quale intendiamo costruire un modello di machine learning significa trovare una descrizione quantitativa del fenomeno stesso. Per “descrizione quantitativa” s’intende trovare delle caratteristiche che possano descrivere in maniera numerica tutte le osservazioni del fenomeno. Questo consente poi di creare una mappatura tra ciascuna osservazione del fenomeno ed una serie di numeri, che sono appunto i valori delle caratteristiche per quella specifica osservazione. Le caratteristiche scelte si chiamano “features“; l’insieme dei valori delle feature per un singolo documento si chiama vettore.

Facciamo un esempio molto semplice.

Supponiamo che vogliamo costruire un modello di machine learning per stimare lo stipendio degli impiegati delle poste. Come facciamo a descrivere il fenomeno in maniera quantitativa? Quali grandezze possiamo usare per descrivere tutti gli impiegati? Normalmente indichiamo gli impiegati con nome e cognome, ma questa non è una buona rappresentazione del fenomeno (a meno che non si pensi che nome e cognome possano aiutarci a dare una stima dello stipendio di una persona). Una migliore rappresentazione è quella di caratterizzare ciascuno impiegato con (1) la durata del rapporto lavorativo o anzianità di servizio e (2) l’età della persona. Quindi in questa rappresentazione

  • ci sono 2 feature: (1) “durata del rapporto lavorativo” e (2) “età della persona”. Entrambe le feature possono essere misurate in anni.
  • Ciascun vettore è una serie di 2 numeri, il primo indica la durata del rapporto lavorativo, il secondo l’età della persona. Ad esempio, per un impiegato di 56 anni che lavora alle poste da 12 anni, il vettore sarebbe: (12, 56).

Se ci sono 43 dipendenti nell’ufficio postale che stiamo studiando, allora potremo descrivere l’intero ufficio postale con 43 vettori di due numeri ciascuno. Immaginate adesso di mettere questi 43 vettori in colonna uno sotto l’altro, cosa otteniamo? Con un po’ d’immaginazione potrete visualizzare una bella tabella, o matrice, di 43 righe e 2 colonne. Avremo una riga per ogni dipendente ed una colonna per ogni feature.

ID OsservazioneFeature 1 - Anzianità di servizioFeature 2 - Età
11256
2232
.........
43947

Rappresentazione nell’NLP e document-term matrix

Nel caso del Natural Language Processing l’oggetto di studio sono dei documenti scritti in linguaggio naturale. Lo studio può avere gli scopi più diversi, ad esempio per fare un riassunto dei documenti, o per capire di cosa trattano.

Trovare una rappresentazione significa trovare delle features numeriche che possano descrivere ciascun documento.

La Bag of words è una rappresentazione che usa come feature le parole del vocabolario.

Nell’esempio delle poste le features sono solo 2, qui invece sono probabilmente diverse decine di migliaia, cioè sono pari al numero di parole nel vocabolario della lingua considerata. Nell’esempio delle poste le features sono misurate in anni, qui invece? Nella Bag of words le features vengono misurate in “occorrenze”, cioè il numero di volte che una certa parola (feature) è presente nel documento (osservazione). Esistono anche altre misurazioni, ma per adesso non è importante conoscerle, le vedremo invece nel prossimo articolo.

Nell’esempio delle poste il vettore che rappresenta ciascuna osservazione (una persona) contiene 2 numeri, qui invece? Beh, direi diverse decine di migliaia! Per l’esattezza ciascun vettore conterrà tanti numeri quante sono le parole del vocabolario (feature). La Bag of words dunque riduce ciascun testo ad una serie (molto lunga) di numeri interi, dove ciascun numero indica quante volte è presente una certa parola in quel testo. Per usare un linguaggio più vicino al suo stesso nome, la Bag of words rappresenta ogni documento come se fosse una borsa riempita con le parole che compongono il documento. Nel fare questo si perde ogni informazione sull’ordine delle parole, su eventuali parole composte, sulla grammatica e altro. In effetti la Bag of words è una rappresentazione molto semplificata di un testo, ma ha il grande valore di essere una rappresentazione valida e dunque consente la realizzazione di modelli di machine learning.

L’esperienza ci insegna che i documenti contengono in genere un numero molto limitato di parole (uniche) rispetto a tutte quelle presenti nella lingua (a meno che quel documento non sia un dizionario!). Cosa significa questo per la nostra rappresentazione? In particolare, cosa significa questo per i vettori che descrivono i nostri documenti? Significa che la maggior parte dei numeri in ciascuno dei vettori sarà pari a 0, indicando cioè che quelle parole (feature) non sono presenti nel documento.

Una volta definita la rappresentazione e calcolati i valori delle feature per ciascuna delle osservazioni, possiamo mettere tutti i nostri vettori in colonna e creare la tabella che descrive il nostro sistema, proprio come avevamo fatto nell’esempio delle poste. Nel mondo dell’NLP tale tabella viene chiamata “document-term matrix”, il cui nome deriva dal fatto che la matrice ha una riga per ciascun documento (osservazione) ed una colonna per ciascuna parola (feature).

Passiamo adesso a calcolare la Bag of words in Python!

Bag of words con 1 solo documento

Iniziamo in maniera semplice, cioè con un corpus fatto da un solo documento, per giunta anche molto breve. Ricorriamo al nostro testo preferito, cioè la citazione di Dean Karnazes sulla corsa, che abbiamo usato più volte nei precedenti articoli e che trovate nel repository pubblico su GitHub. Su questo testo dovremo compiere due azioni:

  1. creazione del vocabolario e
  2. conteggio delle occorrenze delle parole.

Per entrambe le azioni facciammo ricorso ad uno dei più utilizzati pacchetti di Data Science per Python: scikit-learn, spesso abbreviato in sklearn. In particolare usiamo la classe CountVectorizer, che si trova in sklearn.feature_extraction.text. Il nome di questa classe viene dall’utilizzo del termine “vectorization”, che indica il processo di trasformare una serie di testi in una serie di vettori, dove ad ogni testo si associa un vettore.

Per installare scikit-learn si segue la procedura standard come per qualsiasi altro pacchetto (come già visto nel primo articolo di questa serie). Basta digitare il seguente comando al terminale e verranno installate anche le dipendenze (tra cui numpy e scipy):

pip install scikit-learn

1. Creazione del vocabolario

La facilità con cui estrarre le parole dai nostri documenti è imbarazzante (grazie ovviamente al duro lavoro di chi ha creato le librerie). Basta infatti usare il metodo fit dell’oggetto CountVectorizer, dopo aver adeguatamente preparato il corpus su cui s’intende operare, che va passato come input. Iniziamo a scrivere il nostro codice, importando le librerie, leggendo il documento da un file e creando poi il vocabolario.

# ### Import section
from sklearn.feature_extraction.text import CountVectorizer
from nltk.corpus import brown  # totally 1.161.100 words, 57.340 sentences, 16.667 paragraphs
from NLP_base_library import text_splitting, print_dtm

# ###############
# PART 1: use a 1-doc corpus

# ### Data Import
# read data from file
filename = 'text_run_eng.txt'
with open(filename, 'r') as reader:
    input_raw_text = reader.read()
corpus = [input_raw_text]

# 1.1 Create Vocabulary
# Use the package scikit-learn (class CountVectorizer)
# Type "pip install scikit-learn" in the terminal to install the package
#   this will also download and install other important and common package such as numpy, scipy
#
# Create a CountVectorizer object named 'features'
features = CountVectorizer()
# can be personalised with several parameters (we keep it simple and use default)
# use the method "fit" to do the job
features.fit(corpus)
# 2 different ways to access the vocabulary
# - the method "get_feature_names" shows the vocabulary words (as a list)
# - the attribute "vocabulary_" stores the vocabulary (as a dictionary)
#       the key in the dictionary is the actual word
#       the value of the key is the word ID (integer)
vocabulary_words = features.get_feature_names()
print('Total amount of words found = {}\n'.format(len(vocabulary_words)))
print('Here is the corpus vocabulary as an ordered list:\n\t{}\n'.format(vocabulary_words))
print('Here is a python dictionary with the corpus vocabulary:\n\t{}\n'.format(features.vocabulary_))

Basta dunque una sola riga di codice per estrarre tutte le parole utilizzate in un corpus di documenti: features.fit(corpus). Notiamo che il corpus è passato come lista  di documenti, in questo caso contiene un solo documento. Il vocabolario viene poi salvato all’interno di uno specifico attributo che si chiama vocabulary_ ed ha la forma di un dizionario, dove la chiave è la parola, mentre il valore è l’indice identificativo di quella parola.

Ecco l’output del codice precedente:

Total amount of words found = 34
Here is the corpus vocabulary as an ordered list:
    ['air', 'along', 'and', 'be', 'because', 'becomes', 'breathe', 'couch', 'didn', 'escape', 'explore', 'fresh', 'glum', 'if', 'intense', 'life', 'like', 'little', 'more', 'much', 'on', 'ordinary', 'run', 'savor', 'sluggish', 'spend', 'that', 'the', 'time', 'to', 'too', 'trip', 'vibrant', 'way']

Here is a python dictionary with the corpus vocabulary:
    {'run': 22, 'because': 4, 'if': 13, 'didn': 8, 'be': 3, 'sluggish': 24, 'and': 2, 'glum': 12, 'spend': 25, 'too': 30, 'much': 19, 'time': 28, 'on': 20, 'the': 27, 'couch': 7, 'to': 29, 'breathe': 6, 'fresh': 11, 'air': 0, 'explore': 10, 'escape': 9, 'ordinary': 21, 'savor': 23, 'trip': 31, 'along': 1, 'way': 33, 'life': 15, 'becomes': 5, 'little': 17, 'more': 18, 'vibrant': 32, 'intense': 14, 'like': 16, 'that': 26}

Dietro le quinte, il metodo fit sta eseguendo una tokenization, vale a dire sta estraendo le singole parole presenti in ciascun documento del corpus che è stato passato in input. Come abbiamo visto nell’articolo dedicato alla tokenization, questa operazione così semplice ed intuitiva può essere compiuta in modi diversi. In questo caso non stiamo passando alcun parametro al metodo fit, questo significa che stiamo usando la configurazione di default che, tra le altre cose, estrae parole di lunghezza almeno 2. Per questo motivo nel nostro vocabolario non è presente la parola “a” che è invece presente nel corpus.

2. Conteggio delle occorrenze delle parole

Il passo successivo, dopo aver ottenuto il vocabolario, è di contare quante volte ciascuna parola è presente in ciascun documento. In questo caso semplificato abbiamo un solo documento, ma il procedimento è lo stesso che per un corpus con molti documenti. Anche per fare questo c’è un metodo apposito: transform. Questo metodo richiede come input il corpus e restituisce in output una struttura dati che contiene i conteggi che ci interessano. Attenzione, il metodo transform funziona solo se l’oggetto a cui appartiene ha un vocabolario non vuoto. Ragionevole no? Come fa transform a contare le occorrenze delle parole nel corpus, se non gli diciamo quali sono le parole che deve contare?

# 1.2 Get the frequency
# Measure how often each feature (vocabulary word) is used in each corpus document (for Part 1 just 1 document)
# Use the "transform" method of the "features" object
#   the method returns a sparse matrix (--> use "toarray" to treat it as dense)
count_matrix = features.transform(corpus)
print('Here are the counts of each word (in order): \n\t{}\n'.format(count_matrix.toarray()))
print('Here is the matrix with the count of the words:\n{}\n'.format(count_matrix))

Ecco le prime righe dell’output a console:

Here are the counts of each word (in order): 
    [[1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 1 1 1 5 1 1 1 1 5 1 4 1 1 1 1]]
Here is the matrix with the count of the words:
  (0, 0)	1
  (0, 1)	1
  (0, 2)	2
  (0, 3)	1
  (0, 4)	1
  (0, 5)	1
  (0, 6)	1
  (0, 7)	1
  (0, 8)	1
  (0, 9)	1
  (0, 10)	1

Document-Term Matrix

Anche se il nostro corpus ha 1 solo documento, possiamo comunque costruire la document-term matrix, che in questo caso caso avrà 1 sola riga e poi tante colonne quante sono le parole (in questo caso 34).

In genere l’output del metodo transform è una “matrice sparsa”. Si tratta di matrici che hanno molti zeri al loro interno. Spesso più del 99% di tutti i valori di una matrice sparsa sono pari a 0.  Per gestire tali matrici al meglio da un punto di vista computazionale sono state create delle strutture dati particolari. In sklearn si utilizza l’implementazione disponibile nel pacchetto scipy.sparse. Per interagire con l’output di trasform come si farebbe con una matrice o array ordinario si possono usare diversi comandi, qui usiamo toarray().

Nel nostro caso quanti zeri ci sono nella document-term matrix?

Provate a pensarci. Il corpus è fatto da un solo documento. Quindi il vocabolario estratto dal coprus coincide con le parole del documento. Quanti zeri vi aspettate che ci siano nel vettore che rappresenta il nostro documento?

La risposta corretta è “nessuno” (alla faccia della matrice sparsa!). Dato che il vocabolario è estratto da un solo documento, quando andiamo a contare l’occorenza delle parole del vocabolario nel documento, ogni parola sarà presente almeno una volta (altrimenti non sarebbe nel vocabolario).

Se mostriamo la matrice a schermo possiamo vedere una struttura del tipo

(0,1)  1

che significa che nel documento 0, per la parola 1, è presente 1 occorrenza.

Più sotto si legge

(0, 22)  5

e significa che nel documento 0, per la parola 22, sono presenti 5 occorrenze.

Lavoriamo con i dati a disposizione per migliorare l’output:

# let's print the results more nicely, e.g. word - count
print('Word\t--\tCount')
for word, count in zip(vocabulary_words, count_matrix.toarray()[0]):
    print('{}\t--\t{}'.format(word, count))
print('')

Prime righe dell’output:

Word	--	Count
air	--	1
along	--	1
and	--	2
be	--	1
because	--	1
becomes	--	1
breathe	--	1
couch	--	1
didn	--	1

Questa visualizzazione è più utile perché mostra la parola e non il suo id (anche se non è che sia poi un granché!).

fit e transform

Spesso i metodi fit e transform vengono eseguiti uno dopo l’altro (proprio come abbiamo fatto noi adesso). A dire il vero questo succede tanto spesso che è stato creato un metodo apposito che esegue automaticamente entrambi i metodi. Il metodo si chiama fit_transform ed è equivalente all’utilizzo di fit prima e transform poi. Il vantaggio di questo metodo è che implementa le operazioni in maniera più efficiente rispetto ad eseguire i due metodi separatamente (anche se immagino che per poterne vedere gli effetti positivi si debba operare su corpus molto più grandi di quelli che stiamo usando noi!).

# 1.3 fit & transform
# Method "fit_transform"
# "is equivalent to fit followed by transform, but more efficiently implemented."
ft_test = CountVectorizer()
ft_count_matrix = ft_test.fit_transform(corpus)
# this will give the same vocabulary as we obtained before
print('Here is the corpus vocabulary obtained with the "fit_transform" method:\n\t{}'.format(ft_test.get_feature_names()))

Bag of words con 10 documenti

Molto bene, complimenti per avere applicato con successo la Bag of words!

Passiamo adesso a generalizzare un poco. Questa volta vogliamo usare un corpus che abbia 10 documenti invece che 1 solo.

Corpus

Per la creazione del corpus utilizziamo il brown dataset e la nostra funzione text_splitting (per entrambi vedi articolo sullo splitting). Seguiremo la seguente procedura:

  1. leggiamo le prime 10.000 parole del dataset brown e le salviamo in una varibile
  2. impostiamo a 1.000 il numero di parole che vogliamo avere in ciascun documento
  3. utilizziamo la nostra funzione text_splitting per suddividere il dataset
  4. l’output di text_splitting è proprio il nostro corpus, cioè una lista di 10 elementi, ciascun elemento è una stringa che contiene 1.000 parole.

A questo punto non ci resta che creare un nuovo oggetto CountVectorizer() ed usare il suo metodo fit_transform per estrarre il vocabolario e contare le occorrenze delle parole, cioè per applicare la rappresentazione Bag of words.

# ###############
# PART 2: use a 10-doc corpus
# Now let's do the same as for Part 1, but for a corpus with several documents

# 2.1 Build the corpus
#   2.1.1
#   Get the words from the brown dataset (already in NLTK) and join them together into a single document
input_raw_text = ' '.join(brown.words()[:10000])  # take the first 10k words (out of about 1M)
#   2.1.2
#   Split the single document into chunks (using the function we built in previous article)
#   and consider each chunk as a separate document
doc_size = 1000  # Number of words in each chunk
my_corpus = text_splitting(input_raw_text, doc_size)  # this returns a list, each list is one chunk

# 2.2 Get the vocabulary and the word count (Bag of words)
features2 = CountVectorizer()
count_matrix2 = features2.fit_transform(my_corpus)
# Quick look at the results
print('=== Using the Brown dataset ===')
print('Total number of words in the corpus vocabulary: {}\n'.format(len(features2.vocabulary_)))
print('10 random words from the vocabulary:\n\t {}\n'.format(list(features2.vocabulary_)[0:10]))
# Some more statistics:
print('Total number of documents in corpus: {}\n'.format(len(my_corpus)))
print('Statistics for each document:')
matrix = count_matrix2.toarray()  # Return a dense ndarray representation of the sparse matrix (same shape & data)
for i in list(range(0, len(my_corpus))):
    print('\tDoc {}: Words = {}, Unique words: {}'.format(i, len(my_corpus[i].split(' ')),
                                                          len(matrix[i, matrix[i, ] != 0])))
print('')
# Code explanation:
# - len(my_corpus[i].split(' ')) --> split the document into its words, then count the words
#       my_corpus[0] = the 1st document of the corpus
# - len(matrix[i, matrix[i, ] != 0] --> only select the elements that have a count different than zero, then count them
#       some elements have 0 --> this word is not present in the document
#       some elements have 1 --> this word is present only once in the document
#       some elements have C --> this word is present C times in the document

Ed ecco l’output con qualche statistica:

=== Using the Brown dataset ===
Total number of words in the corpus vocabulary: 2395
10 random words from the vocabulary:
     ['the', 'fulton', 'county', 'grand', 'jury', 'said', 'friday', 'an', 'investigation', 'of']
Total number of documents in corpus: 10
Statistics for each document:
    Doc 0: Words = 1000, Unique words: 400
    Doc 1: Words = 1000, Unique words: 411
    Doc 2: Words = 1000, Unique words: 439
    Doc 3: Words = 1000, Unique words: 436
    Doc 4: Words = 1000, Unique words: 433
    Doc 5: Words = 1000, Unique words: 365
    Doc 6: Words = 1000, Unique words: 432
    Doc 7: Words = 1000, Unique words: 446
    Doc 8: Words = 1000, Unique words: 438
    Doc 9: Words = 1000, Unique words: 364

Visualizzazione della document-term matrix

Cerchiamo adesso di visualizzare meglio la document-term matrix. Per fare questo realizziamo una nuova funzione, che andiamo subito ad aggiungere alla nostra libreria (creata nell’articolo 4 della serie). Seguiamo lo stesso approccio usato nell’articolo 2 sullo stemming, quando avevamo bisogno di mettere a confronto tre i diversi stemmer (Porter, Snowball e Lancaster).

Adesso il nostro obiettivo è di visualizzare una matrice che abbia una riga per ciascuna parola ed una colonna per ciascun documento (la trasposta della document-term matrix). Il valore da mostrare all’interno delle celle della matrice sarà il numero di occorrenze della parola di riga per il documento di colonna. Al fine di facilitare la lettura “umana” della tabella aggiungiamo due colonne: una con l’indice della parola ed una con la parola stessa.

Come al solito, riporto subito il codice con il suo output, a seguire alcuni commenti.

def print_dtm(count_vectorizer_obj, dtm, print_indices):
    # print the matrix with words on rows and documents on cols
    # read the total number of documents
    tot_docs = len(dtm.toarray())
    # set the print format
    row_format = '{:^5}|' + '{:^15}|' + '{:^8}|'*tot_docs
    row_length = 1 + 5 + 15 + 9 * tot_docs+1
    # print the header
    print('\n'+'-' * row_length)
    doc_names = []
    for number in range(tot_docs):
        doc_names.append('Doc-{:02d}'.format(number+1))
    print(row_format.format('#', 'Word', *doc_names))
    print('-' * row_length)
    # print the rows
    start = print_indices[0]  # index of the first word/feature
    end = print_indices[1]  # index of the last word/feature
    for i, word, count in zip(range(start, end), count_vectorizer_obj.get_feature_names()[start:end],
                              dtm.T.toarray()):
        print(row_format.format(i+1, word, *count))
    print('-' * row_length)

Chiamiamo poi la funzione nella terza ed ultima parte del codice:

# ###############
# Part 3
# Visualise the document-term matrix nicely, using our custom function
# see 'print_dtm' in NLP_baselibrary
words_to_print = 5
start_index = 100 # index of the first word/feature to print
print_dtm(features2, count_matrix2, [start_index, start_index+words_to_print])

Output:

--------------------------------------------------------------------------------------------------------------
  #  |     Word      | Doc-01 | Doc-02 | Doc-03 | Doc-04 | Doc-05 | Doc-06 | Doc-07 | Doc-08 | Doc-09 |Doc-10|
--------------------------------------------------------------------------------------------------------------
 101 |    action     |   0    |   0    |   2    |   6    |   1    |   1    |   2    |   0    |   0    |   0  |
 102 |    actions    |   0    |   1    |   0    |   0    |   2    |   3    |   3    |   0    |   0    |   1  |
 103 |   activity    |   0    |   1    |   0    |   0    |   1    |   0    |   0    |   0    |   0    |   0  |
 104 |     acts      |   0    |   0    |   0    |   0    |   0    |   0    |   1    |   0    |   0    |   0  |
 105 |   actually    |   0    |   0    |   0    |   1    |   0    |   0    |   0    |   0    |   0    |   0  |
--------------------------------------------------------------------------------------------------------------

Questa visualizzazione mi piace proprio! E a voi?

Innanzitutto penso che questa visualizzazione aiuti a capire meglio il senso stesso della Bag of words e della document-term matrix. Inoltre rende più semplice capire quante volte è presente una certa parola in un certo documento. Ad esempio, se ci chiedessimo quante volte la parola “acts” è presente nel documento 8, bastano pochi secondi per trovare la risposta giusta, che è 0. E’ un gioco da ragazzi, come giocare alla battaglia navale 🙂

Ovviamente se dovessimo inserire tutte le parole, allora la tabella non sarebbe più tanto utile, dato che avrebbe ben 2395 righe e servirebbe molto tempo solo per trovare la parola che ci interessa. A dire il vero le cose stanno ancora peggio, dato che in condizioni normali in NLP ci sono ancora più parole (anche oltre centomila) e molti più documenti (anche qualche migliaio).

Conclusioni

In questo articolo ci siamo occupati di uno temi più importanti dell’intelligenza artificiale, la rappresentazione di un fenomeno, e lo abbiamo fatto trattando una delle rappresentazioni più diffuse nel Natural Language Processing, la Bag of words. Ripercorriamo con ordine quanto abbiamo fatto ed imparato:

  1. Definizione ed esempi della rappresentazione nel machine learning
  2. Descrizione della document-term matrix e della Bag of words
  3. Implementazione 1 della Bag of words in Python con sklearn, su un semplice corpus di 1 documento
    • creazione del vocabolario
    • conteggio delle occorrenze
  4. Implementazione 2 della Bag of words, su un corpus di 10 documenti (creato a partire dal dataset brown)
  5. Creazione di una funzione ad hoc per la visualizzazione della document-term matrix

L’articolo stesso contiene già il codice che abbiamo prodotto. In ogni caso, il codice per la Bag of words, così come tutto quello realizzato finora, è disponbile in modalità Open Source in questo repository pubblico su GitHub.

Per chiunque volesse saperne di più sul tema della Bag of words in rete si trovano tantissime altre risorse e tutorial. Qui mi limito a fornire delle risorse per approfondire gli strumenti usati nel presente tutorial:

  1. Documentazione ufficiale di Scikit Learn riguardo la Text feature extraction (scikit-learn.org/stable/modules/feature_extraction.html#text-feature-extraction)
    • Qui in particolare la documentazione della classe CountVectorizer (scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html#sklearn.feature_extraction.text.CountVectorizer) molto utile per conoscere gli attributi ed i metodi della classe.
  2. Documentazione sulle matrici sparse di scipy (docs.scipy.org/doc/scipy/reference/sparse.html)
    • Qui in particolare la documentazione sulla matrice sparsa da noi usata, di tipo csr_matrix (docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csr_matrix.html)

Arrivederci al prossimo articolo, quando utilizzeremo la Bag of words per costruire un vero e proprio modello di machine learning!

Modifiche

Il codice usato in questo articolo è stato aggiornato il 27 ottobre 2021 in seguito alle modifiche della funzione print_dtm, effettuate nello step 6.2. Tra queste modifiche c’è anche la correzione di un bug, visibile nel codice di questo articolo alla riga 19 (dentro il ciclo for), dove mancano gli indici nell’array.