Ir al contenido principal

Análisis de sentimiento y procesado del lenguaje natural

El análisis de sentimiento es una técnica de machine learning, basada en el procesado del lenguaje natural, que pretende obtener información subjetiva de una serie de textos o documentos. Un ejemplo clásico de aplicación consiste en dilucidar si un artículo periodístico es favorable o no favorable en relación a un tema determinado. Las aplicaciones en el mundo real son muchas y variadas. No hace mucho leía un artículo sobre la aplicación de estas técnicas para usarlas como un dato más a la hora de predecir índices bursátiles, analizando lo que decía la prensa económica sobre determinadas empresas. El campo del tratamiento del lenguaje natural es una área de estudio bastante activa actualmente, y utiliza técnicas que pueden llegar a ser bastante complejas. Sin embargo, no es necesario complicarse mucho la vida para obtener resultados bastante decentes.


Como ejemplo, vamos a tratar de predecir si un mensaje de Twitter es positivo o negativo. Como datos de entrenamiento usaremos un dataset con 1.600.000 tweets (no los usaremos todos) con su correspondiente etiqueta. Fuente del dataset: https://www.kaggle.com/kazanova/sentiment140. Se puede abordar el problema desde diferentes enfoques, pero en este caso entrenaremos una red neuronal con los datos del dataset. El primer paso será descargar el conjunto de datos y cargarlo en memoria con Pandas.

import pandas as pd

columnas = ['target','ids','date','flag','user','text']
data = pd.read_csv('dataset/training.1600000.processed.noemoticon.csv', encoding = "latin-1", names=columnas)
# nos quedamos con una selección de 50.000 tweets aleatorios
data = data.sample(n=50000, random_state=42)
data = data.reset_index(drop=True) # reconstruimos los índices para que sean consecutivos
data.head()

target ids date flag user text
0 0 2200003196 Tue Jun 16 18:18:12 PDT 2009 NO_QUERY LaLaLindsey0609 @chrishasboobs AHHH I HOPE YOUR OK!!!
1 0 1467998485 Mon Apr 06 23:11:14 PDT 2009 NO_QUERY sexygrneyes @misstoriblack cool , i have no tweet apps fo...
2 0 2300048954 Tue Jun 23 13:40:11 PDT 2009 NO_QUERY sammydearr @TiannaChaos i know just family drama. its la...
3 0 1993474027 Mon Jun 01 10:26:07 PDT 2009 NO_QUERY Lamb_Leanne School email won't open and I have geography ...
4 0 2256550904 Sat Jun 20 12:56:51 PDT 2009 NO_QUERY yogicerdito upper airways problem

Los campos son los siguientes (extraído directamente de la documentación del dataset):
  • target: the polarity of the tweet (0 = negative, 2 = neutral, 4 = positive)
  • ids: The id of the tweet ( 2087)
  • date: the date of the tweet (Sat May 16 23:58:44 UTC 2009)
  • flag: The query (lyx). If there is no query, then this value is NO_QUERY.
  • user: the user that tweeted (robotickilldozr)
  • text: the text of the tweet (Lyx is cool)
Para este ejemplo voy a usar sólo la columna text (contenido del tweet) y target (la etiqueta para entrenar la red neuronal).
Nos hemos quedado sólo con 50.000 tweets aleatorios, ya que ocuparían demasiada memoria para un ordenador de andar por casa.
Vamos a echar un vistazo a cómo se reparten las etiquetas (campo target).

from matplotlib import pyplot as plt
plt.hist(data['target'], bins=5)



Parece que el dataset sólo etiqueta los tweets positivos(=0) y los negativos(=4), ya que no hay ningún tweet etiquetado como neutral. Además, para alimentar la red neuronal, es más conveniente que tengan valores continuos y que no haya valores que no tengan ningún significado (como el 1 o el 3). Vamos a reasignar las etiquetas como 0=negativo y 1=positivo.

# ahora la etiqueta para los tweets positivos es el 1
data.loc[data['target'] == 4, 'target'] = 1
plt.hist(data['target'], bins=3)



Además vemos que hay aproximadamente el mismo número de ejemplos de tweets negativos y positivos, lo que es muy interesante a la hora de entrenar la red.

Tratamiento de los datos


Vamos a diseñar una red neuronal capaz de aprender si un texto es positivo o negativo (sentimiento). Para ello necesitamos codificar el texto de alguna manera para que la red neuronal, que trabaja con valores numéricos preferentemente, sea capaz de entenderlo. Existen varias técnicas enmarcadas en lo que se conoce como tratamiento del lenguaje natural. En este ejemplo voy a usar una técnica más o menos sencilla pero efectiva. La técnica consiste en codificar un conjunto amplio de palabras (corpus) como valores numéricos, es decir, creamos un índice en el que hacemos corresponder un valor a cada palabra. Cada palabra se codificará luego como un vector donde la posición correspondiente al valor de la palabra contendrá un 1 y las demás posiciones un 0. Es decir, usando una codificación one-hot. Existen corpus de palabras ya disponibles para usar, pero vamos a complicarnos un poco más la vida y vamos a crear el nuestro a partir del dataset de tweets del que disponemos. La siguiente función construye un corpus de palabras a partir de los tweets. Tenemos cuidado de no añadir las referencias a los usuarios (empiezan por @), los enlaces (contienen http) y eliminamos las palabras menores de 3 caracteres, que no aportan demasiada información.

from keras.preprocessing.text import Tokenizer

# codificar tweets -> corpus -> one-hot
# con limit podemos limitar el número de palabras del corpus
def construir_corpus(tweets, limit=10000):
    corpus=[]
    i=0
    for t in tweets:
        if i>limit:
            break
        for w in t.lower().split():
            # si la palabra no está en el corpus y no empieza por @, no contiene http y es mayor de 3 caracteres.
            if w[0] not in ('@') and 'http' not in w and len(w)>3 and not w in corpus:
                corpus.append(w)
                i=i+1
    
    # codificar palabras como enteros
    t = Tokenizer()
    t.fit_on_texts(corpus)
    encoded_corpus = t.texts_to_matrix(corpus, mode='count')
    
    return corpus[:limit], encoded_corpus[:limit]

Vamos a probarlo.

# Probamos con tres frases para confirmar que todo va bien
tweets = ['Sólo sé que no sé nada', 'Pienso luego existo', 'viva @socrates y @descartes']

corpus, encoded_corpus = construir_corpus(tweets)
print (corpus)
print (encoded_corpus)

['sólo', 'nada', 'pienso', 'luego', 'existo', 'viva']
[[0. 1. 0. 0. 0. 0. 0.]
 [0. 0. 1. 0. 0. 0. 0.]
 [0. 0. 0. 1. 0. 0. 0.]
 [0. 0. 0. 0. 1. 0. 0.]
 [0. 0. 0. 0. 0. 1. 0.]
 [0. 0. 0. 0. 0. 0. 1.]]

Se ha construido el siguiente corpus a partir de las tres frases: ['sólo', 'nada', 'pienso', 'luego', 'existo', 'viva'].
También se muestra como se codifica cada palabra. por ejemplo, la primera (sólo) se codifica como: [0. 1. 0. 0. 0. 0. 0.].
Obsérvese que si tenemos 10.000 palabras, nuestra matriz de codificación tendrá 10.000 filas. En realidad sólo las he calculado para que veas qué forma tiene la codificación. Como verás más adelante, no vamos a usar esta matriz para nada.
Ahora vamos a construir el corpus real que vamos a utilizar.

# Ahora usamos los tweets del dataset
tweets = data['text']
corpus, encoded_corpus = construir_corpus(tweets)

Para codificar un tweet, por cada palabra de la que esté compuesto, se pone un 1 en la posición correspondiente a dicha palabra en el vector. Así, conforme al ejemplo de arriba (el de Sócrates y Descartes), el tweet con el texto "Sólo pienso" se codificaría como [0. 1. 0. 1. 0. 0. 0.]. Una vez entendida la idea de cómo se codifica un tweet para que sea útil a la hora de alimentar una red neuronal, vamos a crear una función que codifique todo el conjunto de tweets. Y también necesitamos codificar las etiquetas para el entrenamiento, para lo cuál también vamos a usar una codificación one-hot valiéndonos de la función to_categorical() de Keras.

import numpy as np

from keras.utils import to_categorical

def codifica_tweets(tweets, corpus, corpus_size=10000):
    coded = np.zeros((len(tweets), corpus_size))
    for i, tweet_text in enumerate(tweets):
        words = tweet_text.lower().split()
        for w in words:
            if w in corpus:
                coded[i,corpus.index(w)] = 1

    return coded

x_train = codifica_tweets(data['text'], corpus)
y_train = to_categorical(data['target'])

Seguidamente prepararemos los datos con los que vamos a entrenar la red neuronal, separando del conjunto de entrenaiento una porción de los datos para crear el conjunto de validación y de test. También vamos a barajar los datos para añadir un poco de entropía.

# barajamos el dataset
np.random.seed(42)    
permutation = np.random.permutation(x_train.shape[0])
x_train = x_train[permutation]
y_train = y_train[permutation]
     
# obtenemos el conjunto de validación
num_val = 10000
x_val = x_train[:num_val]
x_train = x_train[num_val:]
y_val = y_train[:num_val]
y_train = y_train[num_val:]

# obtenemos el conjunto de test
x_test = x_train[:num_val]
x_train = x_train[num_val:]
y_test = y_train[:num_val]
y_train = y_train[num_val:]

Una vez preparados los datos, vamos a definir la arquitectura de la red neuronal. Como los tweets se codifican en vectores de dimensión 10.000 (el tamaño de nuestro corpus de palabras) y queremos que clasifique los tweets en dos categorías (positivos y negativos), nuestra red tendra 10.000 entradas y dos salidas en la última capa. En esta última capa usaremos una función de activación de tipo softmax, que nos devolverá una distribución de probabilidad entre las dos salidas (una para la positiva y otra para la negativa), indicándonos cuál es la probabilidad de que corresponda a cada una de las dos clases. Usaremos dos capas ocultas de 16 neuronas densamente conectadas (todas con todas).

from keras import models, layers

model = models.Sequential()
model.add(layers.Dense(16, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dense(2, activation='softmax'))

model.compile(optimizer='rmsprop',
             loss='categorical_crossentropy',
             metrics=['acc'])

train_log = model.fit(x_train, y_train,
                     epochs=5, batch_size=512,
                     validation_data=(x_val, y_val))

Train on 30000 samples, validate on 10000 samples
Epoch 1/5
30000/30000 [==============================] - 1s 45us/step - loss: 0.6503 - acc: 0.6775 - val_loss: 0.5995 - val_acc: 0.7202
Epoch 2/5
30000/30000 [==============================] - 1s 36us/step - loss: 0.5471 - acc: 0.7519 - val_loss: 0.5465 - val_acc: 0.7296
Epoch 3/5
30000/30000 [==============================] - 1s 40us/step - loss: 0.4922 - acc: 0.7733 - val_loss: 0.5416 - val_acc: 0.7338
Epoch 4/5
30000/30000 [==============================] - 1s 35us/step - loss: 0.4658 - acc: 0.7841 - val_loss: 0.5487 - val_acc: 0.7313
Epoch 5/5
30000/30000 [==============================] - 1s 36us/step - loss: 0.4471 - acc: 0.7949 - val_loss: 0.5568 - val_acc: 0.7285

Si miramos la salida durante el entrenamiento, vemos que, con el conjunto de validación, los mejores resultados se producen en la iteración 3/5. Después parece que se produce un sobreentrenamiento (overfitting). Si queremos asegurarnos podemos mostrar una gráfica.

import matplotlib.pyplot as plt

acc = train_log.history['acc']
val_acc = train_log.history['val_acc']
loss = train_log.history['loss']
val_loss = train_log.history['val_loss']
epochs = range(1, len(acc) + 1)
plt.plot(epochs, acc, 'r', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()
plt.plot(epochs, loss, 'r', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()






Vamos a evaluar el modelo contra el conjunto de test.

test_loss, test_accuracy = model.evaluate(x_test, y_test)
print(test_accuracy)
10000/10000 [==============================] - 1s 68us/step
0.7278000116348267

Hemos conseguido un 72% de precisión. Si hubieramos cortado el entrenamiento en la tercera iteración seguramente hubieramos llegado al 73% (te dejo que lo pruebes). No es un mal dato teniendo en cuenta que sólo hemos usado una pequeña fracción de los tweets y el corpus que hemos generado tampoco es demasiado bueno. Con esta técnica, usando todo el dataset y un buen corpus de palabras podríamos esperar superar el 80%, pero como primera aproximación no está mal.
Ya con el modelo entrenado podemos hacer predicciones sobre cualquier tweet.

predictions = model.predict(x_test)
print (np.argmax(predictions[1]))# clase más probable
print (y_test[1])

0
[1. 0.]

Como os decía al principio, el procesado del lenguaje natural es un campo amplio y actualmente se están realizando bastantes esfuerzos de investigación en esta materia. Como ves, con técnicas más o menos simples podemos acomenter problemas de cierta complejidad. Espero haberte despertado la curiosidad para que te atrevas con técnicas más avanzadas y también más interesantes.
Os dejo un enlace al notebook de Jupyter con el código de este artículo: https://github.com/albgarse/InteligenciaArtificial/blob/master/Machine%20Learning/SentimentAnalysis.ipynb

Comentarios