Il Machine Learning ha assunto grande rilievo grazie all’elevata efficienza che esibisce nella soluzione di problemi complessi come il riconoscimento di immagini o vocale. Il valore di queste tecniche è legato alla potenza di calcolo dei moderni computer e alla ricchezza di informazioni disponibili in forma digitale (social network, Internet of Things, eBooks, …).
Il settore applicativo a cui questo articolo si interesserà è quello del Natural Language Processing (NLP) o Elaborazione del Linguaggio Naturale. Con questa dicitura (volutamente) generica, vengono etichettati tutti gli sforzi volti a rendere un computer in grado di “comprendere” il nostro linguaggio, parlato e scritto.
Quindi ad esempio il NLP si interessa del riconoscimento di caratteri (OCR), della sentiment analysis (analisi “sentimentale”) di un testo, riconoscimento del parlato o della classificazione di testi per argomento. In quest’ultima categoria si inserisce il progetto che descriverò, che si propone di etichettare in modo automatico (classificazione) il genere di una canzone dall’analisi del suo testo.
Classificazione di testi
In generale la classificazione di testi cerca di rispondere alla domanda: dato un documento, quale categoria lo etichetta meglio? Vediamo subito che ci sono due componenti in questo problema: un input (il testo del documento) identificato con una serie di features (caratteristiche); un output (l’argomento) detto class (classe).
Compito dell’algoritmo di apprendimento è imparare a riconoscere la relazione fra input ed output, grazie ad una serie di esempi. In genere le features di un testo rappresentano la presenza (o numero di occorrenze) di determinate parole o insiemi di parole al suo interno. Nel seguito indicherò le features con la variabile [latex]X[/latex] e le classi con [latex]y[/latex].
Nel Machine Learning tutto parte dai dati e infatti per rendere operativo il nostro algoritmo abbiamo bisogno del cosiddetto training set, composto da una serie di esempi input-output. Utilizzando questi esempi il nostro algoritmo cerca di comprendere il meccanismo alla base della classificazione dei documenti, per poter effettuare una scelta su nuovi input non catalogati. Il primo passo è quindi quello di procurarsi i dati, nel mio caso una raccolta di testi di canzoni e corrispondente genere musicale.
Prima di proseguire: ho fatto tutto utilizzando Python 2.7 linguaggio di alto livello che è molto quotato nell’ambito del ML. Il mio codice fa schifo e mi vergogno di farvelo vedere Se qualcuno si mostrasse interessato potrei pensare di ripulire il codice e renderlo disponibile.
Raccolta dei dati
Dicevamo che per prima cosa ci servono dei dati su cui lavorare e affinare il nostro algoritmo. Per i testi delle canzoni ho fatto un becero scraping del sito A-Z Lyrics Database che mi ha fornito circa 450’000 testi di canzoni di una grande varietà di generi musicali.
Tuttavia A-Z Lyrics non registra il genere delle canzoni, quindi ho utilizzato la Spotify Web API per trovarli. Al netto di canzoni senza genere o in lingua non inglese, mi sono ritrovato con un database di circa 300’000 canzoni (attribuite a circa 14’000 artisti).
L’API di Spotify ritorna una lista di generi musicali associati ad ogni artista, per un totale di non-so-nemmeno-quanti generi. Quello che ho fatto, per avere un numero gestibile di classi, è stato etichettare tutti i sottogeneri con il loro genere padre. Quindi ‘technical brutal death metal’ diventa ‘metal’, ‘underground power pop’ diventa ‘pop’.
Genere | Rock | Pop | Metal | Hip Hop | Jazz | Electronic | R&B | Latin |
# Canzoni | 48139 | 37306 | 20903 | 7033 | 5077 | 1522 | 1122 | 89 |
Rimuovendo le canzoni identificate da più di un genere, o da nessuno degli 8 che ho scelto, ne sono rimaste circa 121 mila. La mia può sembrare una scelta molto drastica – e lo è, non lo nego – ma necessaria per rendere il problema trattabile. Per scegliere i generi mi sono ispirato a una discussione su questo forum.
Il prossimo problema che ho dovuto fronteggiare è stato quello di formattare e ripulire i testi per migliorare le prestazioni del mio programma.
Pre-elaborazione dei dati
Gli algoritmi di classificazione di testi applicano diversi paradigmi per rendere i dati più “digeribili” e per trascurare features dal basso contenuto informativo. Fra i più comuni ci sono:
- Eliminazione di punteggiatura/numeri/simboli (questo può essere però controproducente in alcuni casi, come eliminare “#” dai tweet).
- Portare tutto in minuscolo (anche in questo caso a volte può essere sconsigliabile).
- Eliminare le stopwords: si tratta di parole molto frequenti in tutto il corpus di documenti (e.g. ‘un’, ‘lo’, preposizioni, ecc). Data la loro ubiquità, la capacità di discriminare le classi è trascurabile.
- Stemming/lemmatisation/lemma reduction: si tratta del procedimento di ridurre (conflate) le parole alla loro forma base (e.g. ‘corre’, ‘corrono’, ‘correvi’ vengono ridotte a ‘correre’ o ‘corre’). In questo modo si riduce il numero di parole, conservando però, ai fini della classificazione, lo stesso contenuto di informazione (notare che ridurre ‘bellissimo’ in ‘bello’ quando si fa la sentiment analysis può essere svantaggioso).
- Includere gli n-grams: si tiene conto, nella scelta delle features, di quelle sequenze di n parole (generalmente 2) che appaiono spesso insieme.
Di tutti questi metodi, io ho applicato i primi tre. Una volta normalizzati i documenti, è importante trovare il modo giusto di rappresentarli così che l’algoritmo possa elaborarli in modo efficiente. Come ho già menzionato, in genere si adotta un approccio detto bag-of-words, per cui ogni documento è rappresentato dalle parole che vi appaiono, eventualmente con relativa frequenza. All’occorrenza alle singole parole possono aggiungersi gli n-grams (vedi sopra). L’ultimo passo, per evitare un carico computazionale troppo elevato, è la selezione delle parole più frequenti come features (io ad esempio ne ho utilizzate quasi sempre 5’000).
Logistic Regression
Veniamo allora al pezzo forte di ogni applicazione di ML: l’algoritmo di apprendimento. Ci sono molti algoritmi che ottengono buoni risultati nella classificazione testuale. Fra i più semplici ci sono: i Naïve Bayes classifiers e la Logistic Regression (vedi fonti). I Naïve Bayes si basano sull’assunzione (grossolana) che l’occorrenza di una parola sia indipendente da quella di ogni altra parola. Un po’ sorprendentemente, però, l’efficienza di questi classificatori è molto buona, soprattutto quando il training set è ridotto all’osso. La Logistic Regression è invece un metodo un po’ più raffinato, che si può interpretare come una generalizzazione dei Naïve Bayes. Dato che avevo un dataset abbastanza ampio, ho deciso di usare la Logistic Regression.
Caution, math ahead!
Caution, math ahead! Le formule che ho riportato potrebbero risultare un po’ indigeste, se lo desiderate potete saltare al paragrafo conclusivo di questa sezione.
Consideriamo il caso di classificazione con due sole classi, detta classificazione binaria. Indichiamo con [latex]x_i[/latex] le singole feature, che insieme compongono il vettore [latex]X[/latex] e con [latex]y\in\{0,1\}[/latex] le due classi. La probabilità che un documento [latex]d[/latex] appartenga alla classe 0 è:
[latex]\mathbb{P}(y=0|X) = 1/\left[1+exp(\beta_0+\Sigma_i\beta_i x_i)\right],[/latex]
o alla classe 1:
[latex]\mathbb{P}(y=1|X) =exp(\beta_0+\Sigma_i\beta_i x_i)/\left[1+exp(\beta_0+\Sigma_i\beta_i x_i)\right].[/latex]
I parametri [latex]\beta_i[/latex] sono detti coefficienti di regressione e il nostro obbiettivo in fase di apprendimento è stimarli utilizzando il training set. Per fare questo, detto [latex]N[/latex] il numero delle coppie documento [latex]d_j[/latex] / classe [latex]y_j[/latex] nel training set, dobbiamo risolvere questo problema di ottimizzazione:
[latex]\hat{\beta}=\underset{\beta}{argmax}\sum_{j=1}^{N} log(\mathbb{P}(y_j|d_j)).[/latex]
La formula può essere tradotta così: trova i parametri che massimizzano la probabilità di decidere correttamente con quale classe etichettare un documento. Una volta trovati i coefficienti di regressione, dato un nuovo documento, calcoliamo le probabilità che appartenga a una delle due classi e scegliamo quella con probabilità maggiore.
Rimando alle fonti per una trattazione più approfondita e per il caso di multiclass classification, cioè con più di due classi possibili.
Risultati
Veniamo ora ai risultati concreti del mio “esperimento”. Per prima cosa ho fatto alcune prove con la classificazione binaria, scegliendo le coppie di classi con il maggior numero di canzoni. Nella tabella ci sono le percentuali di classificazioni corrette sul training set e sul cosiddetto validation set, ovvero un insieme di dati non utilizzati per l’apprendimento.
Generi | Training (2/3) | Validation (1/3) |
Pop VS Rock | 76.04% | 70.21% |
Pop VS Metal | 91.85% | 85.54% |
Rock VS Metal | 92.69% | 87.94% |
Vediamo, prima di tutto, che gli errori sul training set sono sempre meno che quelli sul validation set. Questo è un risultato intuitivo, se si pensa che il nostro classificatore è stato addestrato proprio sul training set e quindi massimizza il numero di previsioni correte che fa con quei dati. Possiamo anche notare che Pop e Rock “hanno molte più parole” in comune, rispetto a quante ne hanno con il Metal. Questo si traduce infatti in features più simili, e quindi una classificazione meno accurata.
Multiclass classification
Ho poi deciso di provare con la multiclass classification, ovvero inserendo nel training set tutti e 8 i generi. Qui di seguito riporto in una tabella la cosiddetta confusion matrix (matrice di confusione); in ogni riga e colonna sono riportate le classi. All’incrocio fra una riga (con classe ‘a’) e una colonna (classe ‘b’), viene trascritto il numero di documenti di classe ‘a’ classificati nella classe ‘b’. La tabella si riferisce alla classificazione sul validation set, composto da 39990 canzoni distribuite casualmente fra i generi.
Latin | R&B | Elettr | Jazz | Hip Hop | Metal | Pop | Rock | |
Latin | 3 | 0 | 0 | 1 | 1 | 1 | 13 | 10 |
R&B | 0 | 29 | 1 | 1 | 13 | 12 | 212 | 136 |
Elettr | 0 | 7 | 13 | 6 | 22 | 61 | 198 | 213 |
Jazz | 0 | 3 | 0 | 542 | 9 | 36 | 242 | 871 |
Hip Hop | 0 | 10 | 3 | 6 | 1701 | 89 | 321 | 210 |
Metal | 0 | 5 | 2 | 13 | 23 | 4999 | 821 | 961 |
Pop | 4 | 66 | 22 | 133 | 225 | 852 | 6503 | 4514 |
Rock | 2 | 36 | 18 | 228 | 107 | 808 | 3061 | 11595 |
Come si può vedere i generi con in più alto numero di canzoni sono anche quelli che vengono identificati meglio. In totale l’algoritmo classificava un documento correttamente il 72.01% delle volte sul training set, e il 63.47% sul validation. Bisogna notare che come nel caso della classificazione binaria, anche qui ho usato solo 5’000 features.
Errori e numero di features
La teoria ci dice che (da una certo punto in poi) più è complesso il modello che vogliamo apprendere, più crescerà l’errore di di validazione. Ho potuto (circa) confermare questo risultato eseguendo la classificazione sulla coppia Pop VS Metal cambiando ogni volta il numero di features. Come si vede dall’immagine il numero di predizioni errate sul training set decresce mentre quello sul validation set rimane circa costante.
In questa parte del progetto ho utilizzato la libreria scikit-learn per l’implementazione del Logistic Regression classifier. Si tratta di una libreria davvero molto completa e performante, molto semplice da usare. La consiglio vivamente per chi si approccia per la prima volta al ML, dato che permette di avere un riscontro pratico senza doversi preoccupare dell’implementazione degli algoritmi.
Conclusioni
In questo articolo abbiamo solo grattato la superficie dei problemi di classificazione dei testi, ma spero di essere riuscito a trasmettere le idee di fondo. Ci sono molti miglioramenti che si potrebbero fare a questo progetto.
Ad esempio utilizzare più features, o inserire gli n-grams, o un corpus più ricco, o algoritmi più avanzati. A qualcuno potrebbe non piacere l’assegnazione delle canzoni a 8 macro-gruppi. Per porvi rimedio sarebbe interessante provare un algoritmo di classificazione multilabel. Ammettendo, cioè, la possibilità che un documento appartenga a più di una classe contemporaneamente.
Spero di essere riuscito a farvi interessare al mondo del Machine Learning, che io trovo davvero affascinante. Nelle fonti troverete qualche spunto per approfondire.
- T. Hastie, R. Tibshirani, J. Friedman, The Elements of Statistical Learning. Springer, 2008.
- Snowball: A language for stemming algorithms (introduzione al problema dello stemming)
- Testing & Diagnosing a Text Classification Algorithm (per una discussione sulla matrice di confusione)
- Text Classification and Sentiment Analysis (introduzione alla classificazione di testi)
- D. Jurafsky, J.H. Martin, Speech and Language Processing (capitoli 6 e 7 – disponibile online)
- Classification of text documents using sparse features (tutorial incluso nella documentazione di scikit-learn)
- Learning to Classify Text (capitolo del libro accessibile online scritto dai creatori del Natural Language Toolkit per Python)
- 10 Tips to Improve your Text Classification Algorithm Accuracy and Performance
- Naive Bayes and Text Classification – Introduction and Theory
- X. Zhu, Text Categorization with Logistic Regression. 2007
- H. Shimodaira, Text Classification using Naive Bayes. 2015