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 que pertenece a un subconjunto de técnicas denominadas word2vec. 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

Entradas populares de este blog

Criptografía en Python con PyCrypto

A la hora de cifrar información con Python, tenemos algunas opciones, pero una de las más fiables es la librería criptográfica PyCrypto, que soporta funciones para cifrado por bloques, cifrado por flujo y cálculo de hash. Además incorpora sus propios generadores de números aleatorios. Seguidamente os presento algunas de sus características y también como se usa.


Regresión lineal y descenso de gradiente con Python

En machine learning, el objetivo principal es encontrar un modelo que explique el comportamiento de un sistema (en el amplio sentido de la palabra). A partir de unos datos de entrenamiento, un sistema de aprendizaje automático ha de ser capaz de inferir un modelo capaz de explicar, al menos en su mayoría, los efectos observados. Pero también aplicar ese aprendizaje. Por ejemplo, un sistema de machine learning muy lucrativo para las empresas anunciantes es aquél que dado un perfil de usuario (datos de entrada A), sea capaz de predecir si pinchará o no (salida B) sobre un anuncio publicitario de, por ejemplo, comida para gatos. No es sencillo crear un modelo capaz de predecir el comportamiento del usuario (o sí), pero en todo caso, existen diferentes técnicas que nos permiten abordar el problema. En el caso del ejemplo que acabamos de ver, el modelo debería ser capaz de clasificar a los usuarios en dos clases diferentes, los que pulsarán y los que no pulsarán el anuncio de comida de ga…

Desbordamiento de enteros (Integer Overflow)

Ya os he hablado en este blog de posibles problemas potenciales que se pueden dar en los programas y que son susceptibles de ser explotados para hacer que dichos programas se comporten de forma diferente a la que deberían. Uno de estos problemas es el del desbordamiento de la pila. Sin embargo, hay otros posibles errores de programación que, aunque menos obvios, son igual de peligrosos. Uno de ellos es el desbordamiento de enteros o integer overflow. Para entender cómo funciona os presento un ejemplo muy sencillo pero didáctico.