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.
Los campos son los siguientes (extraído directamente de la documentación del dataset):
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).
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.
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.
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.
Vamos a probarlo.
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.
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.
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.
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).
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.
Vamos a evaluar el modelo contra el conjunto de test.
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.
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
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)
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
Publicar un comentario