添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Como hacer Análisis de Sentimiento en español

Este post es una continuación de un articulo previo donde explico como obtener y dibujar en un mapa un mapa de calor de miles de tweets enviados desde mi ciudad
Puedes encontrar el código que he usado en github .
También he subido el archivo de tweets obtenido en el articulo anterior para que puedas seguir este tutorial sin tener que descargarte los tweets.
En este post, me enfocaré en como hacer análisis de sentimiento (Sentiment Analysis) en español.
Hacer Sentiment Analysis en inglés es muy fácil. Hay múltiples paquetes que vienen con modelos preparados para calcular el sentimiento o polaridad de un nuevo texto (ejemplos incluyen TextBlob o word2vec ).
Sin embargo, no tengo constancia de un modelo preparado en español, así que en este post vamos a hacer nuestro propio modelo predictivo :).
Para eso, lo primero que necesitamos es un dataset previamente categorizado. En mi caso utilicé el corpus de TASS .

TASS es un Taller de Análisis de Sentimiento en español organizado cada año por la Sociedad Española del Procesado del Lenguaje Natural(SEPLN) .
Hay que pedir permiso para usar el corpus, por tanto no puedo compartirlo aquí. Para conseguirlo, solo hay que ponerse en contacto con los organizadores del TASS (hay un email de contacto en su pagina).
Los archivos del corpus están en formato XML y contienen miles de tweets en español con su sentimiento (polaridad). Algunos de estos archivos están enfocados en un tópico, por ejemplo política o TV.
La estructura de los archivos es similar a esta:

Los campos que nos interesan para cada tweet son el campo content , que tiene el contenido del tweet, y el campo sentiment.polarity.value , que incluye la polaridad del tweet.
Hay que fijarse en que diversos archivos tienen diferentes esquemas dependiendo de que edición del TASS sean.
Después de procesar y unir todos los archivos, tenemos un archivo con unos 48,000 tweets con una polaridad asociada. Dicha polaridad esta codificada como una variable ordinal que contiene uno de los siguientes valores: N+ (muy negativo), N (negativo), NEU (Neutral), P (Positivo), P+ (muy positivo).
El objetivo de este problema de Machine Learning es predecir el sentimiento de los tweets incluidos en el archivo que creamos en el post anterior usando el corpus de TASS como training data (datos para entrenar al modelo predictivo).
Sin embargo, antes de hacer eso, tenemos que hacer un paso más.
Si analizamos el dataset, nos damos cuenta de que hay tweets en múltiples idiomas, y por lo tanto no podemos predecir la polaridad de tweets que no estén escritos en español mediante el corpus de TASS
Lo que significa que tenemos que asignar el lenguaje a cada tweet, y entonces filtrar sólo aquellos que son en español.

Detección de Lenguaje

Para asignar el lenguaje de cada tweet, he usado 3 paquetes diferentes, langdetect , langid y Textblob , y solo mantuve los tweets en los que al menos un paquete decidiera que el tweet era en español.

import langid
from langdetect import detect
import textblob
def langid_safe(tweet):
        return langid.classify(tweet)[0]
    except Exception as e:
def langdetect_safe(tweet):
        return detect(tweet)
    except Exception as e:
def textblob_safe(tweet):
        return textblob.TextBlob(tweet).detect_language()
    except Exception as e:
#Este paso tarda mucho tiempo
tweets['lang_langid'] = tweets.tweet.apply(langid_safe)
tweets['lang_langdetect'] = tweets.tweet.apply(langdetect_safe)
tweets['lang_textblob'] = tweets.tweet.apply(textblob_safe)
tweets.to_csv('tweets_parsed2.csv', encoding='utf-8')
tweets = tweets.query("lang_langdetect == 'es' or lang_langid == 'es' or lang_langtextblob=='es' ")

Tras filtar los archivos por lenguaje nos queda un archivo de 77,550 tweets en español.

Como he mencionado más arriba, el corpus contiene múltiples niveles de polaridad. No obstante, hay diferencias entre diferentes archivos, por ejemplo algunos archivos sólo tienen los niveles Positivo, Negativo y Neutral
Por lo tanto para poder trabajar con todos los archivos conjuntamente, vamos a convertir la polaridad en una variable dicotómica (binaria) con los valores (Positivo=1, Negativo=0) .

Procesamiento de texto

Para poder analizar los tweets, tenemos que extraer y estructurar la información contenida en el texto. Para ello, usaremos la clase sklearn.feature_extraction.CountVectorizer .
CountVectorizer convierte la columna de texto en una matriz en la que cada palabra es una columna cuyo valor es el número de veces que dicha palabra aparece en cada tweet.
Por ejemplo, si tenemos el tweet:
Machine Learning is very cool
CountVectorizer lo convierte en:

tweet machine learning is very cool
0 Machine Learning is very cool 1 1 1 1 1
1 Machine Learning is cool 1 1 1 0 1

De esta forma podemos trabajar con estos vectores en vez de con texto plano.
Modificaremos nuestro CountVectorizer para que aplique los siguientes pasos a cada tweet:

  1. Tokenizar, este paso convierte una cadena de texto en una lista de palabras (tokens) . Usaremos un tokenizador modificado que no solo tokeniza (mediante el uso de nltk.word_tokenize ), sino que también remueve signos de puntuación. Como estamos tratando con tweets en español, es importante incluir ¿ y ¡ en la lista de signos a eliminar.
  2. Convertir todas las palabras en minúsculas.
  3. Remover stopwords. Se llama stopwords a las palabras que son muy frecuentes pero que no aportan gran valor sintáctico. Ejemplos de stopwords serían de, por, con …
  4. Stemming. Stemming es el proceso por el cual transformamos cada palabra en su raiz. Por ejemplo las palabras maravilloso, maravilla o maravillarse comparten la misma raíz y se consideran la misma palabra tras el stemming.

Este es el código para procesar el texto:

#Tienes que descargarte las stopwords primero via nltk.download()
import nltk
from nltk.corpus import stopwords
from nltk import word_tokenize
from nltk.data import load
from nltk.stem import SnowballStemmer
from string import punctuation
from sklearn.feature_extraction.text import CountVectorizer
spanish_stopwords = stopwords.words('spanish')
stemmer = SnowballStemmer('spanish')
non_words = list(punctuation)
non_words.extend(['¿', '¡'])
non_words.extend(map(str,range(10)))
stemmer = SnowballStemmer('spanish')
def stem_tokens(tokens, stemmer):
    stemmed = []
    for item in tokens:
        stemmed.append(stemmer.stem(item))
    return stemmed
def tokenize(text):
    text = ''.join([c for c in text if c not in non_words])
    tokens =  word_tokenize(text)
    # stem
        stems = stem_tokens(tokens, stemmer)
    except Exception as e:
        print(e)
        print(text)
        stems = ['']
    return stems
vectorizer = CountVectorizer(
                analyzer = 'word',
                tokenizer = tokenize,
                lowercase = True,
                stop_words = spanish_stopwords)

Evaluación del modelo

En este apartado es donde probamos múltiples algoritmos y medimos su eficacia. Herramientas como SciKit-Learn Laboratory (SKLL) ayudan mucho en este proceso.
Un aspecto importante a considerar es elegir una métrica apropiada para evaluar cada modelo. Como este problema es un problema de clasificación binaria (predecir si un tweet es positivo =1 o negativo=0) , una buena métrica es el Area bajo la Curva ROC , que tiene en cuenta tanto los Falsos positivos (es decir, tweets negativos que fueron clasificados como positivos) como los Falsos Negativos (es decir, los tweets postivos que fueron clasificados como negativos)
Tras evaluar varios modelos, el algoritmo SVM (en particular su implementación para casos de clasificación LinearSVC fué el que produjo valores mejores de AUC ( area under the curve ).
Una vez hemos seleccionado nuestro Vectorizer y nuestro clasificador, hacemos una búsqueda en rejilla GridSearchCV . para encontrar los mejores parámetros para nuestros modelos.
GridSearchCV itera sobre los modelos especificados con el rango de parámetros indicado y nos devuelve aquel modelo cuyos parámetros proporcionan los mejores resultados.
Este es el código que hace la búsqueda:

#Definimos el vectorizer de nuevo y creamos un pipeline de vectorizer -> classificador
vectorizer = CountVectorizer(
                analyzer = 'word',
                tokenizer = tokenize,
                lowercase = True,
                stop_words = spanish_stopwords)
#LinearSVC() es el clasificador
pipeline = Pipeline([
    ('vect', vectorizer),
    ('cls', LinearSVC()),
# Aqui definimos el espacio de parámetros a explorar
parameters = {
    'vect__max_df': (0.5, 1.9),
    'vect__min_df': (10, 20,50),
    'vect__max_features': (500, 1000),
    'vect__ngram_range': ((1, 1), (1, 2)),  # unigramas or bigramas
    'cls__C': (0.2, 0.5, 0.7),
    'cls__loss': ('hinge', 'squared_hinge'),
    'cls__max_iter': (500, 1000)
grid_search = GridSearchCV(pipeline, parameters, n_jobs=-1 , scoring='roc_auc')
grid_search.fit(tweets_corpus.content, tweets_corpus.polarity_bin)

Este paso tarda bastante tiempo, pero al terminar nos devolverá el conjunto de parámetros (o como se les llama también, hiperparámetros) que producen el mejor AUC. En este caso, el mejor AUC fue de 0.92, que es un resultado aceptable (con más tiempo, intentaríamos subir ese AUC hasta los 0.97 o aproximado, pero al fin y al cabo, este es un experimento).
Ahora ya tenemos nuestros modelos con los mejores parámetros, asi que solo falta entrenar el modelo en el corpus de TASS y predecir la polaridad del archivo de tweets que descargamos.
Finalmente, guardaremos en un archivo la latitud, longitud y polaridad de cada tweet.

from sklearn.svm import LinearSVC
from sklearn.pipeline import Pipeline
#Creamos un Pipeline con los parámetros mejores
pipeline = Pipeline([
    ('vect', CountVectorizer(
            analyzer = 'word',
            tokenizer = tokenize,
            lowercase = True,
            stop_words = spanish_stopwords,
            min_df = 50,
            max_df = 1.9,
            ngram_range=(1, 1),
            max_features=1000
    ('cls', LinearSVC(C=.2, loss='squared_hinge',max_iter=1000,multi_class='ovr',
             random_state=None,
             penalty='l2',
             tol=0.0001
#ajustamos el modelo at corpus de TASS
pipeline.fit(tweets_corpus.content, tweets_corpus.polarity_bin)
#now we predict on the new tweets dataset
tweets['polarity'] = pipeline.predict(tweets.tweet)

Cuando tenemos el archivo de latitudes y longitudes con su polaridad, seguimos los mismos pasos que seguimos en el tutorial previo y obtenemos el siguiente mapa de calor donde se pueden observar los sitios con polaridad más negativa y más positiva:
¿Que os parece?

Fascinante¡¡¡
Me gustaría hacer algo parecido como ejercicio así que lo primero que he hecho ha sido intentar conseguir el corpus, pero por más que busco, no encuentro la dirección de correo dónde poder solicitar las claves de acceso.
Puedes decirme cuál es la dirección de correo, o dónde encontrarla?
Muchas Gracias.

En esta pagina
http://www.sngularmeaning.team/TASS2015/tass2015.php#corpus
Al final hay un apartado denominado “REGISTRATION” donde se indica como conseguirla.
Saludos.

Muchas gracias;
Por lo visto han cambiado la dirección de correo a la que hay que dirigirse, ahora es [email protected] enviando el pdf de petición.
Ahora a ver si soy capaz de hacer algo 🙂

Estoy intentando hacer un análisis de sentimiento de un conjunto de tweets que he conseguido bajarme, la verdad es que sólo pretendo conseguir que funcione, no más, por lo que parto de 10 tweets que contengan la palabra ‘perro’.
Pero lo que he conseguido es un .json que soy incapaz de pasar a csv para continuar, que podría hacer?
Muchas Gracias.

Hola Paola, la verdad es que sin dar mas datos sobre la estructura de tu json es complicado ayudar.
Asi a nivel general, suponiendo que tu json no esté roto, puedes convertirlo a un pandas.DataFrame directamente haciendo:
import pandas as pd
tweet_corpus = pd.read_json(NOMBRE DEL ARCHIVO AQUI)

hola manuel,
estoy teniendo algunos problemas a la hora de conseguir el TASS,
¿me podrias detallar los pasos que seguiste para conseguirlo?
un saludo

Buenos días manuel, estoy intentando adaptar este código a lo que ya tengo, pero me ha surjido una complicación y es que no entiendo como compones el objeto tweets_corpus para que sea del tipo {array-like, sparse matrix}, podrías darme alguna pista?
Gracias y un saludo

Hola Adrián,
Creo que te refieres a la parte contenida en este notebook, no?
https://github.com/manugarri/tweets_map/blob/master/4.%20Sentiment%20Analysis.ipynb
Se empieza con un pandas Dataframe, y el CountVectorizer lo transforma en sparse.
En este ejemplo ya que uso un Pipeline en realidad la matrix sparse no se usa externamente, solo internamente, con lo que se pasa de pandas.DataFrame a scikitlearn.pipeline automaticamente.

Hola. En la versión TASS 2016, el general corpus está dividido en dos training set y general test set. Solamente el Training set está etiquetado (tagged). Esto implica que sólo contamos con los sentimientos de 7220 tweets, a diferencia tuya que tienes los training y los test sets etiquetados y por lo tanto más tweets.
Repliqué tu código con esta nueva versión utilizando solamente el training set tagged. El AUC me sale del 0.75, lo cual me lleva a muchas preguntas ¿influye que yo tenga menos tweets etiquetados en mi índice de AUC? ¿cómo puedo incrementar este índice aunque tarde más tiempo en procesar?
Muchas gracias por tu atención y por este post

mira tengo un problema, porque al momento de cambiar las coordenadas por mi cuidad que es Bogotá, la consola de mando se queda ejecutando y crea el archivo txt pero cuando abro el archivo txt este esta vacio y por ello no puede realizar los otros pasos, queria saber cual es el problema? y como deberia verse en la consola el resultado al ejecutar el codigo para descargar los tweets.

Hola,
Tengo curiosidad por saber si calculaste la precisión del modelo sobre los tweets, en comparación con la precisión del modelo sobre el corpus de TASS.
Gracias

Deja una respuesta Cancelar la respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *