Procesamiento de lenguaje natural¶

Análisis de sentimientos¶



Autor: Luis Fernando Apáez Álvarez



  • Palabras vacías
  • Operadores de cadena y comparación
  • Stematización y lematización
  • Algoritmo Tf-idf



Bolsa de Palabras ¶

En todos los idiomas hay palabras que tienen demasiada frecuencia pero que no son muy informativas. A veces, es útil deshacerse de ellas antes de crear un modelo de ML. A este tipo de palabras se les suele denominar palabras vacías o stopwords.

Por ejemplo, los artículos, las conjunciones, proposiciones, entre otras más, son palabras vacías . Por otro lado, es posible que deseemos ampliar el conjunto estándar de palabras vacías. Por ejemplo, si consideramos un conjunto de datos de reseñas de películas, es posible que deseemos excluir palabras como cine, película, etcétera. Recordemos que en la primer clase vimos cómo crear nubes de palabras, y lo que éstas significan, así, puede ser que al crear nubes de palabras sobre las reseñas de películas, veamos que la palabra cine es la más frecuente, por lo cual se le daría mayor peso de aparición y tamaño en la nube de palabras, siendo lo anterior que se perdierá verdadera información que pudiésemos extrar de la nube.

Recordemos cómo crear una nube de palabras

In [1]:
# Importaciones necesarias
import pandas as pd
import matplotlib.pyplot as plt
# Importamos las palabras vacias (del ingles) que trae
# la libreria wordcloud
from wordcloud import WordCloud, STOPWORDS

# Cargamos los datos
data = pd.read_csv('IMDB_sample.csv')[['review', 'label']]
In [17]:
# Definimos un set con las palabras vacias
stopwords = set(STOPWORDS)
In [18]:
# Agregaremos algunas palabras vacias, a nuestra consideracion
# y de acuerdo al contexto del problema, al set que creamos antes.
stopwords.update(['movie', 'movies', 'film', 'films', 'watch', 'br'])

donde br simplemente se refiere al metacaracter de salto de línea.

In [20]:
# Convertimos a cadena de texto la resegnia 4930
titanic = str(data.iloc[4930][0])

# Instanciamos un objeto WordCloud cambiando el color de fondo
# y configurando que no se consideren las palabras vacias,
# las cuales definimos antes
word_cloud = WordCloud(background_color='white', stopwords=stopwords)

cloud_titanic = word_cloud.generate(titanic)

# Graficamos
plt.imshow(cloud_titanic, interpolation='bilinear')

# Configuramos que los ejes no sean visibles
plt.axis('off')
plt.show()

También será útil eliminar palabras vacías al momento en el cual estemos construyendo nuestra bolsa de palabras, lo cual se puede implementar fácilmente en la función CountVectorizer

In [22]:
# Importamos ademas la lista predefinida de palabras vacias
# del ingles de sklearn
from sklearn.feature_extraction.text import CountVectorizer, ENGLISH_STOP_WORDS

# Podemos enriquecer, o agregar, mas palabras vacias a la lista predefinida
# de las palabras vacias de sklearn, por ejemplo:
stopwords = ENGLISH_STOP_WORDS.union(['film', 'movie', 'cinema', 'theatre'])

donde el método .union() es el referente a la operación de unión referente a los conjuntos (sets). Continuando

In [24]:
# Instanciamos un objeto CountVectorizer configurando
# las palabras vacias que definimos antes
vect = CountVectorizer(stop_words=stopwords)

# Ajustamos a nuestro texto, el cual es en particular las
# resegnias del dataframe
vect.fit(data.review)

# Para crear la representacion de bolsa de palabras llamamos al metodo
# transform()
X = vect.transform(data.review)

Veamos el dataframe de las palabras versus su frecuencia de aparición

In [25]:
X_df = pd.DataFrame(X.toarray(), columns = vect.get_feature_names())
X_df
C:\ProgramData\Miniconda3\lib\site-packages\sklearn\utils\deprecation.py:87: FutureWarning: Function get_feature_names is deprecated; get_feature_names is deprecated in 1.0 and will be removed in 1.2. Please use get_feature_names_out instead.
  warnings.warn(msg, category=FutureWarning)
Out[25]:
00 000 000s 007 0080 0083 0093638 00am 00pm 00s ... zukovic zulu zuniga zvyagvatsev zwick zx81 zy zzzzzzzzzzzz zzzzzzzzzzzzz â½
0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
2 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
3 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
4 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
7496 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
7497 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
7498 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
7499 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
7500 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0

7501 rows × 45529 columns

Notamos del dataframe anterior la importancia de realizar un preprocesamiento de los datos, pues vemso que varias palabras que se muestra no son relevantes.

Un punto importante de mencionar es que, al eliminar las palabras vacías, el vocabulario generado por la bolsa de palabras disminuye.

Operadores de cadena y comparación ¶

  • isalpha(): devuelve verdadero si la cadena está compuesta de solo letras y falso caso contrario.

  • isdigit(): devuelve verdadero si la cadena se compone solo de dígitos.

  • isalnum(): devuelve verdadero si la cadena se compone de caracteres alfanuméricos.

Por ejemplo, considerando nuestro conjunto de datos sobre las reseñas de la películas, podemos crear una nueva columna en el dataframe correspondiente para almacenar las reseñas tokenizadas, pero, por ejemplo, solo reteniendo los tokens que consten de letras

In [4]:
# Primero tokenizamos las resegnias
from nltk import word_tokenize

data['tokens'] = data.review.apply(lambda x: word_tokenize(x))
data.head()
Out[4]:
review label tokens
0 This short spoof can be found on Elite's Mille... 0 [This, short, spoof, can, be, found, on, Elite...
1 A singularly unfunny musical comedy that artif... 0 [A, singularly, unfunny, musical, comedy, that...
2 An excellent series, masterfully acted and dir... 1 [An, excellent, series, ,, masterfully, acted,...
3 The master of movie spectacle Cecil B. De Mill... 1 [The, master, of, movie, spectacle, Cecil, B, ...
4 I was gifted with this movie as it had such a ... 0 [I, was, gifted, with, this, movie, as, it, ha...

Una vez tokenizadas las reseñas, haremos un filtro para quedarnos solo con los tokens que consten de letras

In [11]:
data['tokens_letras'] = data.tokens.apply(lambda tokens: [token for token in tokens if token.isalpha()])
data.head()
Out[11]:
review label tokens tokens_letras
0 This short spoof can be found on Elite's Mille... 0 [This, short, spoof, can, be, found, on, Elite... [This, short, spoof, can, be, found, on, Elite...
1 A singularly unfunny musical comedy that artif... 0 [A, singularly, unfunny, musical, comedy, that... [A, singularly, unfunny, musical, comedy, that...
2 An excellent series, masterfully acted and dir... 1 [An, excellent, series, ,, masterfully, acted,... [An, excellent, series, masterfully, acted, an...
3 The master of movie spectacle Cecil B. De Mill... 1 [The, master, of, movie, spectacle, Cecil, B, ... [The, master, of, movie, spectacle, Cecil, B, ...
4 I was gifted with this movie as it had such a ... 0 [I, was, gifted, with, this, movie, as, it, ha... [I, was, gifted, with, this, movie, as, it, ha...

Veamos la comparación respecto a una reseña en particular

In [14]:
# Vemos que al final hay numeros
data.iloc[4][0]
Out[14]:
"I was gifted with this movie as it had such a great premise, the friendship of three women bespoiled by one falling in love with a younger man.<br /><br />Intriguing.<br /><br />NOT! I hasten to add. These women are all drawn in extreme caricature, not very supportive of one another and conspiring and contriving to bring each other down.<br /><br />Anna Chancellor and Imelda Staunton could do no wrong in my book prior to seeing this, but here they are handed a dismal script and told to balance the action between slapstick and screwball, which doesn't work too well when the women are all well known professionals in a very small town.<br /><br />And for intelligent women they spend a whole pile of time bemoaning the lack of men/sex/lust in their lives. I felt much more could have been made of it given a decent script and more tension, the lesbian sub-plot went nowhere and those smoking/drinking women (all 3 in their forties???) were very unrealistic - even in the baby scene - screw the baby, gimme a cigarette! Right.<br /><br />Like I said, a shame of a waste. 4 out of 10."
In [18]:
# Consideramos los tokens de la columna tokens_letras
# Vemos que ahi no se consideraron los numeros
rev = ''
for l in data.iloc[4][3]:
    rev += l + ' '
rev
Out[18]:
'I was gifted with this movie as it had such a great premise the friendship of three women bespoiled by one falling in love with a younger br br br br NOT I hasten to add These women are all drawn in extreme caricature not very supportive of one another and conspiring and contriving to bring each other br br Anna Chancellor and Imelda Staunton could do no wrong in my book prior to seeing this but here they are handed a dismal script and told to balance the action between slapstick and screwball which does work too well when the women are all well known professionals in a very small br br And for intelligent women they spend a whole pile of time bemoaning the lack of in their lives I felt much more could have been made of it given a decent script and more tension the lesbian went nowhere and those women all in their forties were very unrealistic even in the baby scene screw the baby gim me a cigarette br br Like I said a shame of a waste out of '

Por otro lado, las expresiones regulares son una forma estándar de extraer ciertos caracteres de una cadena y tienen una funcionalidad importante a la hora de realizar el preprocesamiento de los datos. Recordemos que vimos un poco de expresiones regulares en la clase 4 de la primera sección.

En CountVectorizer toma una expresión regular como argumento para el filtrado para la creación de la bolsa de palabras. El patrón por defecto que toma es \b\w\w+\b, el cual coincide con las palabras que constan de al menos dos letras o números que están separados por límites de palabras \b, de donde se ignorarán las palabras de una sola letra. Entonces, podemos modificar el patrón por defecto en CountVectorizer, para lo cual escribimos


CountVectorizer(token_pattern = <patron>)

Stematización y lematización ¶

En un idioma, las palabras a menudo se derivan de otras palabras, lo que significa que diviersas palabras pueden estar compartiendo la misma raíz. A este proceso se le conoce como derivación o steamming:

La derivación se define como la transformación de palabras en sus formas de raíz, incluso si la raíz en sí no es una palabra válida en el idioma

Por ejemplo:

  • staying, stays, stayed ---> stay
  • house, houses, housing ---> hous

En general, la derivación tenderá a eliminar sufijos, formas plurales o posesivas.

La lematización es un proceso similar a la derivación, con la principal diferencia que las raíz resultantes de la lematización sí son palabras válidas en el idioma. Por ejemplo:

  • staying, stays, stayed ---> stay
  • house, houses, housing ---> house

Así, si en el problema en cuestión lo importante es retener palabras, entonces es más conveniente utilizar la lematización, sin embargo, si queremos eficiencia, utilizar la derivación sería el camino adecuado dado que es mucho más rápido.

Un algoritmo de derivación muy utilizado es el algoritmo Porter, el cual podemos utilizar:

In [19]:
# Importacion necesaria
from nltk.stem import PorterStemmer

# Instanciamos
porter = PorterStemmer()

Con base en ello podemos derivar palabras escribiendo porter.stem() como sigue

In [21]:
porter.stem('houses')
Out[21]:
'hous'

La derivación es también posible utilizando otros idiomas, como el danés, holandés, francés, español, alemán, etc. Para utilizar derivaciones para idiomas distintos del inglés, como los que mencionamos antes, utilizando para ello

In [25]:
# Utilizaremos la derivacion para palabras en espagnol
from nltk.stem.snowball import SnowballStemmer

# Instanciamos colocando el idioma
esStemmer = SnowballStemmer('spanish')

# Por ejemplo
esStemmer.stem('jugando')
Out[25]:
'jug'
In [26]:
# O tambien derivando para palabras del aleman

# Instanciamos colocando el idioma
alStemmer = SnowballStemmer('dutch')

# Por ejemplo
alStemmer.stem('beginnen')
Out[26]:
'beginn'

Ahora, podemos también lematizar palabras escribiendo

In [29]:
# Importacion necesaria
from nltk.stem import WordNetLemmatizer

# Instanciamos
wnlemm =  WordNetLemmatizer()

# Por ejemplo
wnlemm.lemmatize('houses')
Out[29]:
'house'

Así, podemos agregar más columnas a nuestro dataframe colocando los tokens lematizados

In [30]:
# Instanciamos
wnl =  WordNetLemmatizer()

# Tomamos la lista de tokens de cada resegnia, luego, retornaremos de nuevo una lista
# de tokens, pero dichos tokens estaran lematizados
data['tokens_lemma'] = data.tokens.apply(lambda l_tokens: [wnl.lemmatize(token) for token in l_tokens])
data.head()
Out[30]:
review label tokens tokens_letras tokens_lemma
0 This short spoof can be found on Elite's Mille... 0 [This, short, spoof, can, be, found, on, Elite... [This, short, spoof, can, be, found, on, Elite... [This, short, spoof, can, be, found, on, Elite...
1 A singularly unfunny musical comedy that artif... 0 [A, singularly, unfunny, musical, comedy, that... [A, singularly, unfunny, musical, comedy, that... [A, singularly, unfunny, musical, comedy, that...
2 An excellent series, masterfully acted and dir... 1 [An, excellent, series, ,, masterfully, acted,... [An, excellent, series, masterfully, acted, an... [An, excellent, series, ,, masterfully, acted,...
3 The master of movie spectacle Cecil B. De Mill... 1 [The, master, of, movie, spectacle, Cecil, B, ... [The, master, of, movie, spectacle, Cecil, B, ... [The, master, of, movie, spectacle, Cecil, B, ...
4 I was gifted with this movie as it had such a ... 0 [I, was, gifted, with, this, movie, as, it, ha... [I, was, gifted, with, this, movie, as, it, ha... [I, wa, gifted, with, this, movie, a, it, had,...
In [37]:
# Notemos, en particular, la palabras sensibilities
data.iloc[1][0][:250]
Out[37]:
"A singularly unfunny musical comedy that artificially tries to marry the then-cutting edge rock 'n' roll explosion with the middle-class sensibilities of a suburban sitcom. The result is a jarringly dated mish-mash that will satisfy none of the audie"
In [38]:
# Notamos ahora que, referente a la cuarta columna sobre los tokens 
# lematizados, que la palabra sensibilities ha sido cambiada
# por sensibility
rev = ''
for l in data.iloc[1][4]:
    rev += l + ' '
rev[:250]
Out[38]:
"A singularly unfunny musical comedy that artificially try to marry the then-cutting edge rock 'n ' roll explosion with the middle-class sensibility of a suburban sitcom . The result is a jarringly dated mish-mash that will satisfy none of the audienc"

Algoritmo Tf-idf ¶

La bolsa de palabras es un método bastante bueno, no obstante, a veces es posible que deseemos utilizar algún otro enfoque más sofisticado, como es el caso del algoritmo de frecuencia de término inverso-frecuencia de documentos, mejor conocido como algoritmo tf-idf.

  • El término de frecuencia nos dice con qué frecuencia aparece una palabra determinada en un documento del corpus, donde cada palabra tiene su propia frecuencia de término. Lo anterior nos dará un valor o puntuación denominado $tf$.

  • La frecuencia inversa del documento se define comúnmente como la relación lograrítmica entre el número total de documentos y el número de documentos que contiene una palabra en específico. Esto significa que la frecuencia inversa de documentos es, básicamente, que las palabras raras tendrán un valor alto de ésta. Lo anterior nos dará un valor $idf$.

Cuando multiplicamos las puntuaciones $tf$ e $idf$ obtenemos de manera global la puntuación $tf-idf$ de una palabra en el corpus.

Cabe mencionar que en la bolsa de palabras, las palabras tenían diferetes recuentos de frecuencia en los documentos, pero no contabilizamos respecto a la longitud de un documento, lo cual el algoritmo tf-idf sí hace.

tf-idf también destacará las palabras que son más interesantes y las palabras que son comunes en un documento, pero no en todos los demás.

Un ejemplo es que, si en cierto corpus tenemos que el tópico es referente a viajes y aeropuertos, entonces los nombres de las aerolíneas tendrán puntajes bajos de tf-idf, debido a que ocurren muchas veces y en muchos documentos. Si en cierto documento se habla sobre un tema referente a viajes y aeropuertos, pero en éste se discute un tema que no es discutido en muchos otros documentos, entonces el primer documento tiene una alta probabilidad de tener una puntuación tf-idf elevada.

Notemos que, dado que tf-idf penaliza las palabras frecuentes, entonces hay menos necesidad de definir explícitamente las palabras vacías. Procederemos a aplicar dicho algoritmo en Python:

In [4]:
# Importacion de TfidfVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer

donde TfidfVectorizer es muy similar a CountVectorizer y recibe los mismo parámetros. Para continuar con el ejemplo, carguemos el conjunto de datos con el cual trabajaremos

In [5]:
data = pd.read_csv('tweets.csv')
data.head()
Out[5]:
tweet_id airline_sentiment airline_sentiment_confidence negativereason negativereason_confidence airline airline_sentiment_gold name negativereason_gold retweet_count text tweet_coord tweet_created tweet_location user_timezone
0 570306133677760513 neutral 1.0000 NaN NaN Virgin America NaN cairdin NaN 0 @VirginAmerica What @dhepburn said. NaN 2015-02-24 11:35:52 -0800 NaN Eastern Time (US & Canada)
1 570301130888122368 positive 0.3486 NaN 0.0000 Virgin America NaN jnardino NaN 0 @VirginAmerica plus you've added commercials t... NaN 2015-02-24 11:15:59 -0800 NaN Pacific Time (US & Canada)
2 570301083672813571 neutral 0.6837 NaN NaN Virgin America NaN yvonnalynn NaN 0 @VirginAmerica I didn't today... Must mean I n... NaN 2015-02-24 11:15:48 -0800 Lets Play Central Time (US & Canada)
3 570301031407624196 negative 1.0000 Bad Flight 0.7033 Virgin America NaN jnardino NaN 0 @VirginAmerica it's really aggressive to blast... NaN 2015-02-24 11:15:36 -0800 NaN Pacific Time (US & Canada)
4 570300817074462722 negative 1.0000 Can't Tell 1.0000 Virgin America NaN jnardino NaN 0 @VirginAmerica and it's a really big bad thing... NaN 2015-02-24 11:14:45 -0800 NaN Pacific Time (US & Canada)

donde consideraremos específicamente la columna

In [6]:
data.text
Out[6]:
0                      @VirginAmerica What @dhepburn said.
1        @VirginAmerica plus you've added commercials t...
2        @VirginAmerica I didn't today... Must mean I n...
3        @VirginAmerica it's really aggressive to blast...
4        @VirginAmerica and it's a really big bad thing...
                               ...                        
14635    @AmericanAir thank you we got on a different f...
14636    @AmericanAir leaving over 20 minutes Late Flig...
14637    @AmericanAir Please bring American Airlines to...
14638    @AmericanAir you have my money, you change my ...
14639    @AmericanAir we have 8 ppl so we need 2 know h...
Name: text, Length: 14640, dtype: object
In [7]:
data.text[4]
Out[7]:
"@VirginAmerica and it's a really big bad thing about it"

Así, procedemos a ajustar TfidfVectorizer a dicha columna

In [9]:
vect = TfidfVectorizer(max_features=100).fit(data.text)
X = vect.transform(data.text)

Lo cual devuelve, también, una matriz dispersa

In [10]:
X
Out[10]:
<14640x100 sparse matrix of type '<class 'numpy.float64'>'
	with 119182 stored elements in Compressed Sparse Row format>

donde una matriz dispersa es una matriz con valores en su mayoría cero, que almacena solo los valores distintos de cero. Transformamos dicha matriz dispersa como sigue

In [12]:
import warnings
warnings.filterwarnings('ignore')

X_df = pd.DataFrame(X.toarray(), columns=vect.get_feature_names())
X_df
Out[12]:
about after again airline all am americanair amp an and ... was we what when why will with would you your
0 0.000000 0.0 0.0 0.0 0.0 0.0 0.000000 0.000000 0.0 0.000000 ... 0.0 0.000000 0.668165 0.0 0.0 0.0 0.0 0.0 0.000000 0.000000
1 0.000000 0.0 0.0 0.0 0.0 0.0 0.000000 0.000000 0.0 0.000000 ... 0.0 0.000000 0.000000 0.0 0.0 0.0 0.0 0.0 0.329040 0.000000
2 0.000000 0.0 0.0 0.0 0.0 0.0 0.000000 0.000000 0.0 0.000000 ... 0.0 0.000000 0.000000 0.0 0.0 0.0 0.0 0.0 0.000000 0.000000
3 0.000000 0.0 0.0 0.0 0.0 0.0 0.000000 0.431149 0.0 0.000000 ... 0.0 0.000000 0.000000 0.0 0.0 0.0 0.0 0.0 0.000000 0.332355
4 0.494872 0.0 0.0 0.0 0.0 0.0 0.000000 0.000000 0.0 0.279754 ... 0.0 0.000000 0.000000 0.0 0.0 0.0 0.0 0.0 0.000000 0.000000
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
14635 0.000000 0.0 0.0 0.0 0.0 0.0 0.293653 0.000000 0.0 0.000000 ... 0.0 0.402305 0.000000 0.0 0.0 0.0 0.0 0.0 0.268285 0.000000
14636 0.000000 0.0 0.0 0.0 0.0 0.0 0.181266 0.000000 0.0 0.000000 ... 0.0 0.248334 0.000000 0.0 0.0 0.0 0.0 0.0 0.000000 0.000000
14637 0.000000 0.0 0.0 0.0 0.0 0.0 0.487504 0.000000 0.0 0.000000 ... 0.0 0.000000 0.000000 0.0 0.0 0.0 0.0 0.0 0.000000 0.000000
14638 0.000000 0.0 0.0 0.0 0.0 0.0 0.188272 0.000000 0.0 0.179597 ... 0.0 0.000000 0.000000 0.0 0.0 0.0 0.0 0.0 0.344014 0.232585
14639 0.000000 0.0 0.0 0.0 0.0 0.0 0.164169 0.000000 0.0 0.000000 ... 0.0 0.449822 0.000000 0.0 0.0 0.0 0.0 0.0 0.000000 0.000000

14640 rows × 100 columns

lo cual nos arroja un resultado bastante similar al obtenido mediante la bolsa de palabras, donde cada columna es una característica y cada fila contiene la puntuación tf-idf de la característica en un tweet determinado.