La Sentiment Analysis è uno dei campi più popolari del Natural Language Processing, il suo scopo è quello di classificare documenti di testo, come post, commenti o recensioni, in base alla loro polarità, cioè in base al sentimento positivo o negativo espresso nel testo.

Le principali tecniche per la Sentiment Analysis possono essere raggruppate in 2 categorie: lessicali e statistiche. Le ultime, grazie ai progressi esponenziali nel settore del machine learning e all’applicazione delle reti neurali artificiali, sono diventate estremamente popolari.

In questo tutorial costruiremo una Rete Neurale Artificiale per eseguire la sentiment analysis di recensioni di film usando Keras, una popolare libreria per il Deep Learning, che funziona al di sopra di altre librerie per il calcolo tensoriale come Tensorflow, CNTK o Theano e permette di sviluppare diverse architetture di reti neurali artificiali con poche righe di codice

Prerequisiti

Per seguire questo tutorial ai bisogno di:

  • Avere installato Anaconda, un gestore di ambienti e di pacchetti per Machine Learning e Data Science.
  • Avere delle conoscenze basilari di programmazione con Python, se sei già un programmatore ti basta leggere questo.
  • Avere familiarità con il Machine Learning e concetti come features, target addestramento e test. Se non sai proprio nulla a riguardo parti da qui e poi dai uno sguardo a questo.

Dopo aver installato Anaconda dobbiamo creare e attivare un’ambiente per Tensorflow, che utilizzeremo al di sotto di Keras. Apriamo il terminale  ed eseguiamo:

conda create -n tensorflow_env tensorflow
conda activate tensorflow_env

Per questo tutorial ti consiglio di utilizzare Jupyter Notebook, che viene installato insieme ad Anaconda. Puoi anche eseguire il Notebook di questo tutorial in cloud usando Google Colaboratory, senza dover installare nulla.

 

Step 1 – Procuriamoci i dati

Il dataset che utilizzeremo per addestrare la nostra è l’IMDB Movie Review Dataset, che contiene 50.000 esempi di recensioni di film (25.000 per l’addestramento e 25.000 per il testing) correttamente etichettate come positive o negative.

Possiamo importare il dataset dentro un’array numpy usando le API di Keras.

from keras.datasets import imdb 

(X_train, y_train), (X_test, y_test) = imdb.load_data(num_words=5000)

Il parametro num_words ci serve per definire il numero massimo di parole più frequenti da considerare. Così facendo abbiamo ottenuto 4 array:

  • X_train che contiene le features degli esempi per l’addestramento.
  • y_train che contiene i target per l’addestramento, cioè un singolo valore 0 o 1 che indicano rispettivamente se la recensione è negativa o positiva.
  • X_test che contiene le features degli esempi per il test.
  • y_test che contiene i target per il test.

Ogni riga degli array con le features corrisponde ad una recensione, le recensioni sono già state codificate in numeri, ognuna di esse è infatti una lista di numeri dove ogni numero corrisponde alla posizione della corrispondente parola all’interno del vocabolario dell’intero corpus di testo.

print(X_train[159]) # [1, 6, 675, 7, 300, 127, 24, 895, 8, 2623, 89, 753, 2279, 5, 2, 78, 14, 20, 9]

Step 2 (opzionale) – Come decodificare una recensione

Nonostante non sia essenziale ai fini della costruzione del modello, può essere utile conoscere come decodificare una recensione per risalire al testo originale. Per farlo dobbiamo procurarci il vocabolario che mappa le parole agli indici ed utilizzarlo per costruire un dizionario con la relazione inversa, cioè che mappa gli indici alle parole.

word_index = imdb.get_word_index()
index_word = dict([(value, key) for (key, value) in word_index.items()])

Ora possiamo usare il dizionario index_word per ricostruire una recensione partendo dagli indici delle parole che la compongono.

decoded_review = [word_index.get(i - 3, '?') for i in X_train[159]]
decoded_review = ' '.join(decoded_review)
print(decoded_review) # ? a rating of 1 does not begin to express how dull depressing and ? bad this movie is

Gli indici delle parole hanno un offset di 3 rispetto al vocabolario, per questo otteniamo la parola corrispondente facendo i-3, utilizzando il metodo join delle stringhe uniamo la lista di parole ottenuta dalla list comprehensions dividendole con degli spazi, in modo da ottenere una stringa. Dato che abbiamo limitato il vocabolario a solamente le 5000 parole più frequenti, alcune parole presenti all’interno di una recensione potrebbero essere mancanti, al loro posto verrà inserito un punto interrogativo.

Step 3 – Preprocessiamo i dati

Utilizziamo il One Hot Encoding per creare quelle che saranno le features del nostro modello. Il one hot encoding si esegue creando, per ogni recensione, un array di lunghezza pari al numero totale di parole presenti all’interno dell’intero corpus di testo e inserendo dei valori 1 agli indici corrispondenti alle parole presenti nella frase e dei valori 0 altrimenti.

import numpy as np

def onehot_encoding(data, size):
    onehot = np.zeros((len(data), size))
    for i, d in enumerate(data):
        onehot[i,d] = 1.
    return onehot
  
X_train = onehot_encoding(X_train, 5000) # len = (25000, 5000)
X_test = onehot_encoding(X_test, 5000) # len = (25000, 5000)

Il risultato di questa operazione è una matrice sparsa, cioè una matrice contenete per la maggior parte dei valori 0.

Step 4 – Costruiamo la Rete Neurale

Una Rete Neurale Artificiale è un modello di machine learning che riesce apprendere relazioni non lineari nei dati, anche molto complesse, ispirandosi al funzionamento del cervello animale. Diversi neuroni sono disposti su diversi strati posti in sequenza, i neuroni di strati successivi sono connessi ai neuroni degli strati precedenti tramite dei pesi.

Il primo strato di una rete neurale prende in input le features, l’ultimo strato fornisce l’output della rete, mentre gli strati intermedi, chiamati anche strati nascosti, utilizzano le features provenienti dallo strato precedente per apprendere nuove features più significative per l’obiettivo della nostra rete. Nell’ambito del deep learning i neuroni vengono chiamati anche unità o nodi, noi adotteremo quest’ultimo termine.

Il numero di strati e di nodi di una rete neurale è uno degli iperparametri del modello, cioè quei parametri che tocca a noi definire e ottimizzare. Strati nascosti differenti possono avere un numero di nodi differenti, per questo motivo le reti neurali sono uno strumento potente quanto complesso e andrebbero utilizzare solo nei casi in cui modelli più semplici, come la regressione lineare o la regressione logistica, si rivelano inefficaci per il nostro problema.

Nel nostro caso utilizzeremo una rete neurale con la seguente architettura:

  • Uno strato di input con 512 nodi, cioè il numero di features del dataset.
  • Uno strato nascosto con 128 nodi.
  • Uno strato nascosto con 32 nodi.
  • Uno strato nascosto con 8 nodi.
  • Un’ultimo strato di output, con un solo nodo che conterrà l’output della rete, cioè il risultato della classificazione.
from keras.models import Sequential
from keras.layers import Dense

model = Sequential()
model.add(Dense(512, activation='relu', input_shape=(5000,)))
model.add(Dense(128,activation='relu'))
model.add(Dense(32,activation='relu'))
model.add(Dense(8,activation='relu'))
model.add(Dense(1, activation='sigmoid'))

La classe Sequential ci permette di inizializzare un nuovo stack lineare di strati, inizialmente vuoto, utilizzando il metodo .add possiamo aggiungere nuovi strati.

La classe Dense ci permette di creare un nuovo strato denso, cioè uno strato in cui tutti i nodi dello strato precedente sono connessi a tutti i nodi dello strato successivo tramite dei pesi. Per questa classe dobbiamo specificare dei parametri:

  • il primo parametro è il numero di nodi dello strato, per l’ultimo strato è pari al numero di output, nel nostro caso, trattandosi di un problema di classificazione binaria (recensione positiva/negativa) avremo un unico nodo di output che conterrà la probabilità che la recensione sia positiva o negativa.
  • il parametro activation è la funzione di attivazione da utilizzare per lo strato, quella che ci permette di ottenere risultati non lineari, la funzione di attivazione più utilizzata per gli strati nascosti è la Rectified Linear Units (RELU), mentre per lo strato di output, trattandosi di un problema di classificazione binaria, dobbiamo utilizzare la Sigmoide.
  • il parametro input_shape contiene la dimensione dell’input, va specificato solo per lo strato di input, mentre per gli altri strati viene calcolato in automatico da Keras.

Dopo aver definito l’architettura della rete dobbiamo configurare la fase di addestramento utilizzando il metodo compile.

model.compile(optimizer='adamax', loss='binary_crossentropy', metrics=['accuracy'])
  • All’interno del parametro loss specifichiamo la funzione di costo da utilizzare, cioè la funzione che ci permette di calcolare la performance del nostro modello in base alla qualità delle sue predizioni. Nel caso di classificazioni binarie la funzione di costo da utilizzare è la binary crossentropy, che tiene conto della probabilità che il nostro modello abbiamo fornito il risultato corretto.
  • All’interno del parametro metrics specifichiamo una lista con altre metriche aggiuntive che ci permetteranno di misurare la qualità del modello, come l’Accuracy che non è altro che la percentuale di classificazioni eseguite correttamente dal modello.
  • All’interno del parametro optimizer specifichiamo l’algoritmo di ottimizzazione da utilizzare per l’addestramento del modello. Un’algoritmo di ottimizzazione ci permette di trovare i pesi del modello che minimizzano la funzione di costo da noi specificata.

L’algoritmo di ottimizzazione principale è il Gradient Descent, che utilizza un processo iterativo in cui, ad ogni iterazione, ogni peso viene sommata alla relativa derivata rispetto alla funzione di costo moltiplicata per un’ulteriore iperparametro chiamato Learning Rate, utilizzato per controllare la dimensione di ogni step del processo di ottimizzazione.

Nel nostro caso utilizzeremo adamax, una variante del gradient descent che dovrebbe permettere di ottenere risultati migliori nel caso in cui le features siano rappresentate da una matrice sparsa, non specificando un Learning Rate ma utilizzando semplicemente il valore che utilizza di default Keras.

Adesso siamo pronti per avviare la fase di addestramento utilizzando il metodo fit, alla quale dobbiamo passare gli array con i dati per l’addestramento, features e target, il numero di epoche, ovvero di iterazioni dell’algoritmo di ottimizzazione, è la dimensione di ogni batch di addestramento, cioè il numero di esempi che verranno utilizzati per uno step dell’algoritmo di ottimizzazione.

model.fit(X_train, y_train, epochs=10, batch_size=512)

# Epoch 10/10
# 25000/25000 [==============================] - 1s 48us/step - loss: 4.7194e-04 - acc: 1.0000

Nota bene: Considerando che i pesi del modello vengono inizializzati casualmente, potremmo ottenere risultati leggermente differenti.

Nel mio caso, alla decima epoca ho ottenuto un’accuracy del 100% e un valore per la funzione di costo tendente allo zero, questo vuol dire che il modello ha classificato correttamente tutti gli esempi del set di addestramento con un grado di incertezza bassissimo.

Vediamo come se la cava con recensioni che non ha già visto durante l’addestramento usando il metodo evaluate sul set di test.

print(model.evaluate(X_test, y_test)) # [0.6953321468254924, 0.87136]

L’accuracy sul set di test è decisamente più bassa, mentre il valore della binary cross entropy è disastroso, come mai ? Perché il modello ha memorizzati i dati di addestramento piuttosto che apprendere da essi e quindi ora fallisce nel generalizzare su nuovi dati. Questa condizione è conosciuta come overfitting ed è uno dei problemi principali del Machine Learning.

Vediamo come possiamo risolverlo.

Step 5 – Contrastiamo l’overfitting

Le soluzioni migliori per contrastare l’overfitting consistono nel ridurre la complessità del modello, riducendo ad esempio il numero di strati nascosti, oppure nel raccogliere un quantità maggiore di esempi per l’addestramento.

Quando queste non sono opzioni attuabili possiamo adoperare delle tecniche di regolarizzazione, come:

  • le regolarizzazioni L1 ed L2: che ci permettono di penalizzare i pesi eccessivamente grandi, che sono proprio quelli che causano l’overfitting.
  • il Dropout: che ci permette di “spegnere” una percentuale prefissata di nodi selezionati casualmente, in questo modo i nodi eviteranno di farsi carico degli errori di altri nodi riducendo il rischio di overfitting.

Ridefiniamo l’architettura della rete aggiungendo la regolarizzazione L2 e il dropout tra gli strati.

from keras.regularizers import l2
from keras.layers import Dropout


model = Sequential()

model.add(Dense(512, activation='relu', input_shape=(5000,), kernel_regularizer=l2(0.1)))
model.add(Dropout(0.4))
model.add(Dense(128,activation='relu', kernel_regularizer=l2(0.01)))
model.add(Dropout(0.4))
model.add(Dense(32,activation='relu',kernel_regularizer=l2(0.001)))
model.add(Dropout(0.4))
model.add(Dense(8,activation='relu', kernel_regularizer=l2(0.01)))
model.add(Dropout(0.4))
model.add(Dense(1, activation='sigmoid'))

La classe Dropout ha bisogno di un unico parametro, che rappresenta la percentuale di nodi da disattivare ad ogni iterazione. Anche la funzione l2 necessita di un’unico parametro, che è il parametro di regolarizzazione, un valore che indica l’intensità della regolarizzazione da applicare.

Fatto questo, riconfiguriamo la fase di addestramento e avviamola nuovamente, questa volta per 100 epoche.

model.compile(optimizer='adamax', loss='binary_crossentropy', metrics=['accuracy'])
model.fit(X_train, y_train, epochs=100, batch_size=512)

I valori della funzione di costo e dell’accuracy sono più realistici rispetto a prima, verifichiamo se abbiamo risolto, o almeno ridotto, il nostro problema di overfitting testando il modello sul set di test.

print(model.evaluate(X_test, y_test)) # [0.4818225428390503, 0.87684]

Le metriche sul set di test sono migliori rispetto a prima e più vicine a quelle ottenute sul set di addestramento, specialmente la binary cross entropy, questo sta ad indicare che il modello che abbiamo addestrato è più sicuro sulle sue predizioni.

Step 6 – Mettiamo la Rete Neurale all’opera

Ora che la nostra rete neurale funziona abbastanza bene, mettiamola alla prova su nuove recensioni, cominciamo definendo una funzione che prende in input una recensione e la converte in un array numpy codificato tramite One Hot Encoding, pronto per essere dato in pasto alla nostra rete neurale.

from re import sub

def preprocess(review):
    
    # Rimuoviamo un'eventuale punteggiatura utilizzando un'espressione regolare
    review = sub(r'[^\w\s]','',review) 
    # Convertiamo tutto in minuscolo
    review = review.lower()
    # Creiamo un array di parole
    review = review.split(" ")

    # Qui dentro metteremo gli IDs
    review_array = []

    # Iteriamo lungo le parole della recensione
    for word in review:
        # proseguiamo se la parola si trova all'interno
        # della lista di parole del corpus di addestramento
        if word in word_index:
            # estraiamo l'indice della parola 
            index = word_index[word] 
            # Proseguiamo se l'indice è minore o uguale a 5000
            # cioè il numero di parole che abbiamo utilizzato
            # per l'addestramento
            if index <= 5000:
                # aggiungiamo l'indice all'array
                review_array.append(word_index[word]+3)
                
    # Eseguiamo il one hot encoding sulla lista di indici
    review_array = onehot_encoding([review_array],5000)
    
    return review_array

L’output della nostra rete neurale sarà un valore compreso tra 0 ed 1 che indica la probabilità che la recensione sia positiva, quindi un output di 0 indicherà una recensione sicuramente non positiva (cioè negativa), mentre un valore di 1 indicherà una recensione sicuramente positiva.

Definiamo una funzione che prendendo in input questo valore ritorna una stringa che rappresenta un possibile sentiment associato.

def prob_to_sentiment(prob):
    
    if(prob>0.9): return "fantastica"
    elif(prob>0.75): return "ottima"
    elif(prob>0.55): return "buona" 
    elif(prob>0.45): return "neutrale"
    elif(prob>0.25): return "negativa"
    elif(prob>0.1): return "brutta"
    else: return "pessima"

Ora mettiamo tutto insieme per classificare qualche recensione pescata dal web, cominciamo con una relativa a uno dei film più brutti che ho avuto la sciagura di vedere: Paranormal Activity 4.

review = "what a waste of time and cash.. the movie was pointless. with no flow. no questions answered. just a waste. I never review movies but had to share how bad this was..compared to part 1- 2- and 3.... i don't know what else to say other than how misleading the commercial is.. the commercial was cut and spliced with video and audio that didn't even match what happened in the movie... you have been warned. when the movie was over.. people actually Boo'd. hopefully people will spread the word, and save others from throwing their money away. i know die-hard fans will go and give it a shot, but will be disappointed as well. Sinister was better and actually made you jump quite a few times."

x = preprocess(review)
y = model.predict(x)[0]
print("REVIEW: %s" % review)
print("\n")
print("La recensione è %s [%.6f]" % (prob_to_sentiment(y), y)) # La recesione è pessima [0.051200]

La nostra rete indica che la recensione è (ovviamente) pessima, ma proprio tanto tanto.

Proviamo adesso con una recensione che riguarda Avengers: Infinity War.

review = "This movie will blow your mind and break your heart - and make you desperate to go back for more. Brave, brilliant and better than it has any right to be."

x = preprocess(review)
y = model.predict(x)

print("REVIEW:", review)
print("\n")
print("La recensione è %s [%.6f]" % (prob_to_sentiment(y), y)) # La recesione è fantastica [0.915910]

La rete dice che la recensione è positiva (dai, a chi non è piaciuto questo film ?).

Se vuoi divertirti un po’ prova a scrivere la tua recensione, tenendo conto che, dato che abbiamo addestrato la rete su di un corpus di testo inglese, la recensione deve essere in lingua inglese.

review = input("Write your review: ")
x = preprocess(review)
prob = model.predict(review)

print("REVIEW", review)
print("\n")
print("La recensione è %s [%.6f]" % (prob_to_sentiment(prob), prob))

 

About Giuseppe Gullo

Cresciuto a pane e bit, ho cominciato a programmare a 13 anni, durante un periodo di convalescenza forzata dovuta ad un brutto incidente.

Durante la mia adolescenza ho utilizzato un mio approccio hacker all'apprendimento per passare da un argomento all'altro senza sosta, sviluppo web, programmazione software, sviluppo mobile per android ed iOS, sviluppo di videogame 2d e 3d con Unity.

Poco più che ventenne mi sono avvicinato all'intelligenza artificiale, ed è stato amore a prima vista.

Ho lavorato come sviluppatore indipendente e freelancer, creando diverse dozzine di servizi che hanno raggiunto centinaia di migliaia di persone in tutto il mondo.

Il mio life goal è riuscire a sfruttare le enormi potenzialità dell'AI per migliorare le condizioni di vita degli esseri umani.