NLP: Análisis de Sentimientos
Vamos a trabajar con un dataset que contiene unas $700.000$ entradas de reviews de productos de amazon.es; contiene dos columnas: el número de estrellas dadas por un usuario a un determinado producto y el comentario sobre dicho producto. El número de estrellas que un usuario da a un producto es el indicador de si a dicho usuario le ha gustado el producto o no.
Vamos a establecer una regla para convertirlo en un problema de clasificación, si una review tiene 4 o más estrellas se trata de una review positiva, y será negativa si tiene menos de 4 estrellas.
# Importamos librerías necesarias para el análisis
import pandas as pd
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer
from nltk.corpus import stopwords
from nltk import word_tokenize
from nltk.stem import SnowballStemmer
from string import punctuation
from sklearn.model_selection import train_test_split
# Leemos el archivo:
df = pd.read_csv("C:\\Users\\hesca\\Documents\\MASTER DATAHACK\\Machine Learning\\Practicas\\amazon_es_reviews.csv",
sep = ";", encoding ='utf-8')
# Exploramos el dataset para ver que se ha cargado adecuadamente:
df.head(10)
comentario | estrellas | |
---|---|---|
0 | Para chicas es perfecto, ya que la esfera no e... | 4.0 |
1 | Muy floja la cuerda y el anclaje es de mala ca... | 1.0 |
2 | Razonablemente bien escrito, bien ambientado, ... | 3.0 |
3 | Hola! No suel o escribir muchas opiniones sobr... | 5.0 |
4 | A simple vista m parecia una buena camara pero... | 1.0 |
5 | NI para pasar el rato, los personajes no tiene... | 1.0 |
6 | el fabricante decia que es compatible con la d... | 2.0 |
7 | el libro está en muy buenas condiciones, pero ... | 3.0 |
8 | buen aspecto, pero le falta fortaleza. util pa... | 3.0 |
9 | Explica de forma simple y sencilla los pensami... | 5.0 |
df.describe()
estrellas | |
---|---|
count | 702446.000000 |
mean | 3.372171 |
std | 1.435783 |
min | 1.000000 |
25% | 2.000000 |
50% | 4.000000 |
75% | 5.000000 |
max | 5.000000 |
# Ahora vamos transformar la feature estrellas en otra binaria "postivo_negativo", donde los comentarios con :
df_positivo_negativo = []
for i in df["estrellas"]:
if i > 3:
df_positivo_negativo.append(1)
else:
df_positivo_negativo.append(0)
df["positivo_negativo"] = df_positivo_negativo
df.head()
comentario | estrellas | positivo_negativo | |
---|---|---|---|
0 | Para chicas es perfecto, ya que la esfera no e... | 4.0 | 1 |
1 | Muy floja la cuerda y el anclaje es de mala ca... | 1.0 | 0 |
2 | Razonablemente bien escrito, bien ambientado, ... | 3.0 | 0 |
3 | Hola! No suel o escribir muchas opiniones sobr... | 5.0 | 1 |
4 | A simple vista m parecia una buena camara pero... | 1.0 | 0 |
# Vemos cuantos comentarios positivos y negativos hay para poder hacer la selección óptima de la métrica:
print(df["positivo_negativo"].value_counts())
1 363735
0 338711
Name: positivo_negativo, dtype: int64
Hay un 51,78% de comentarios positivos y un 48,22% de comentarios negativos, por lo tanto el dataset está balanceado y podemos utilizar la métrica Accuracy.
# Convertimos en array nuestras columnas del dataframe para agilizar los cálculos:
comentarios = df['comentario'].values
notas = df['positivo_negativo'].values
# Separamos en train (75%) y test (25%)
comentarios_train, comentarios_test, notas_train, notas_test = train_test_split(
comentarios, notas, test_size=0.25, random_state=8)
Comenzamos con el proceso de Vectorización de los comentarios:
# Creamos una bolsa de palabras y definimos la puntuación que eliminaremos de los comentarios:
spanish_stopwords = stopwords.words('spanish')
stemmer = SnowballStemmer('spanish')
non_words = list(punctuation)
non_words.extend(['¿', '¡', ',','...','``'])
non_words.extend(map(str,range(10)))
Definimos las funciones dentro del vectorizer para crear los tokens, limpiar los tokens y hacer el steem:
def stem_tokens (tokens, stemmer):
stemmed = []
for item in tokens:
stemmed.append(stemmer.stem(item))
return stemmed
def clean_data (tokens, stop_words = ()):
clean_tokens = []
for token in tokens:
if token.lower() not in spanish_stopwords and token not in non_words:
clean_tokens.append (token)
return clean_tokens
def tokear(text):
tokens = []
text = ''.join([c for c in text if c not in non_words]) # Limpieza del texto eliminando
tokens = word_tokenize(text)
tokens_limpios = clean_data(tokens)
tokens_stemmed = stem_tokens(tokens_limpios, stemmer)
return tokens_stemmed
# Definimos el vectorizer con los siguientes pasos:
# - Tokenizamos para convertir cada cadena de texto en un token.
# - Convertimos las palabras en minúsculas.
# - Removemos palabras que son muy frecuentes en castellano, pero que no aportan valor semántico.
# - Hacemos el Steem, es decir, convertimos cada palabra en su raíz. (Podríamos realizar la Lematización, para convertir
# cada palabra en su lema, es decir en la palabra tal y como la encontratíamos en el diccionario, pero en este caso
# hemos realizado el steem).
vectorizer = CountVectorizer(
analyzer = 'word',
tokenizer = tokear,
lowercase = True,
stop_words = spanish_stopwords)
# Antes de continuar vamor a realizar una prueba de nuestras funciones con una muestra de 100 comentarios:
tokens = tokear(df["comentario"][:100])
# Observamos si se realiza correctamente la tokenización:
tokens
['chic',
'perfect',
'esfer',
'fin',
'pelin',
'gord',
'gust',
'carg',
'movimient',
'dur',
'despues',
'1-2',
'dias',
'llev',
'para.muy',
'floj',
'cuerd',
'dobl',
'dibuj',
'parec',
'libr',
'recomendable.hol',
'product',
'merec',
'desencant',
'maquin',
'afeit',
'sensibl',
'carn',
'viv',
...
...
...
'from',
'the',
'sell',
'as',
'you',
'hav',
'to',
'return',
'thes',
'lost',
'.el',
'prim',
'contact',
'maquinill',
'afeit',
'buen',
'robust',
'buen',
...]
Vemos que existen algunos errores:
- Hay tokens formados por dos pablaras unidas por un ‘.’ como por ejemplo ‘raton.el’. Deberíamos sustituir el ‘.’ por un especio en blanco y volver a realizar el proceso.
- Tenemos palabras en otros idiomas como por ejemplo: ‘wandering’, ‘days’,… Deberíamos hacer una selección del lenguaje. Podríamos comparar nuestras palabras con los paquetes en español de langdetect, langid y Textblob y solo seleccionar los comentarios que se reconocieses como Español. Este proceso no lo vamos a realizar ya que tarda demasiado, y el número de comentarios en otros idiomas no parece significativo como para alterar el resultado.
- Tenemos también algunos tokens que son números, pero que debido a que tienen algún simbolo o letra “pegado” no se han eliminado como por ejemplo ‘3d’, ‘1-2’, … Tendríamos que ver cómo eliminarlo.
Por lo demás, parece que se ha realizado correctamente.
# Lanzamos el entrenamiento del vectorizer pero con los arrays para mayor velocidad de procesado:
vectorizer.fit(comentarios_train)
CountVectorizer(analyzer='word', binary=False, decode_error='strict',
dtype=<class 'numpy.int64'>, encoding='utf-8', input='content',
lowercase=True, max_df=1.0, max_features=None, min_df=1,
ngram_range=(1, 1), preprocessor=None,
stop_words=['de', 'la', 'que', 'el', 'en', 'y', 'a', 'los',
'del', 'se', 'las', 'por', 'un', 'para', 'con',
'no', 'una', 'su', 'al', 'lo', 'como', 'más',
'pero', 'sus', 'le', 'ya', 'o', 'este', 'sí',
'porque', ...],
strip_accents=None, token_pattern='(?u)\\b\\w\\w+\\b',
tokenizer=<function tokear at 0x000002D18BFF09D8>,
vocabulary=None)
# Realizamos el transform de los comentarios con el set de train y con el de test
Comentarios_train = vectorizer.transform(comentarios_train)
Comentarios_test = vectorizer.transform(comentarios_test)
# Y vemos como ha quedado:
print(Comentarios_train)
print(Comentarios_test)
(0, 934) 1
(0, 1230) 1
(0, 22046) 1
(0, 23316) 1
(0, 34586) 1
(0, 43261) 1
(0, 46684) 1
(0, 51732) 1
(0, 77273) 1
(0, 94896) 1
(0, 120773) 1
(0, 131478) 1
(0, 133160) 2
: :
(526832, 23316) 2
(526832, 26433) 1
(526832, 29286) 1
(526832, 33232) 1
(526832, 43357) 1
(526832, 54280) 1
(526832, 67666) 1
(526832, 118103) 1
(526832, 140158) 1
(526832, 141549) 1
(526832, 144761) 1
(526832, 161704) 1
(526832, 173299) 1
(0, 63464) 2
(0, 69555) 1
(0, 70871) 1
(0, 78592) 1
(0, 104902) 1
(0, 108425) 1
(0, 112646) 1
(0, 116257) 1
(0, 120695) 1
(0, 132578) 1
(0, 133884) 1
(0, 140158) 1
(0, 144818) 1
(0, 153597) 1
: :
(175610, 24992) 1
(175610, 51316) 1
(175610, 61922) 1
(175610, 125549) 1
(175610, 130894) 1
(175610, 157440) 1
(175610, 181222) 1
(175610, 184196) 1
(175611, 1230) 1
(175611, 10686) 1
(175611, 23316) 1
(175611, 25642) 1
(175611, 27778) 1
Ya tenemos vectorizados los comentarios.
# Guardamos los arrays en un pickle:
import pickle
filename = 'Comentarios_train.pickle'
with open(filename, 'wb') as filehandler:
pickle.dump(Comentarios_train, filehandler)
filename2 = 'Comentarios_test.pickle'
with open(filename2, 'wb') as filehandler:
pickle.dump(Comentarios_test, filehandler)
# Volvemos a cargar los archivos:
Comentarios_train=np.load('C:\\Users\\hesca\\Documents\\MASTER DATAHACK\\Machine Learning\\Practicas\\Comentarios_train.pickle',
allow_pickle=True)
Comentarios_test=np.load('C:\\Users\\hesca\\Documents\\MASTER DATAHACK\\Machine Learning\\Practicas\\Comentarios_test.pickle',
allow_pickle=True)
Vamos a probar distintos modelos.
1.- Regresión Logística
# Importamos las librerías necesarias para este proceso:
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
reglog = LogisticRegression() # Solo un paso, no hace falta pipeline
grid_hyper_reglog ={} # En este caso vacío.
gs_reglog = GridSearchCV(reglog,
param_grid = grid_hyper_reglog,
cv=10, # hacemos la validación mediante cross validation
scoring = 'roc_auc',
n_jobs=-1,
verbose=3)
# lanzamos el entrenamiento del modelo:
gs_reglog.fit(Comentarios_train, notas_train)
Fitting 10 folds for each of 1 candidates, totalling 10 fits
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
[Parallel(n_jobs=-1)]: Done 3 out of 10 | elapsed: 14.0min remaining: 32.6min
[Parallel(n_jobs=-1)]: Done 7 out of 10 | elapsed: 15.7min remaining: 6.7min
[Parallel(n_jobs=-1)]: Done 10 out of 10 | elapsed: 18.5min finished
C:\Users\hesca\Anaconda3\lib\site-packages\sklearn\linear_model\logistic.py:432: FutureWarning: Default solver will be changed to 'lbfgs' in 0.22. Specify a solver to silence this warning.
FutureWarning)
GridSearchCV(cv=10, error_score='raise-deprecating',
estimator=LogisticRegression(C=1.0, class_weight=None, dual=False,
fit_intercept=True,
intercept_scaling=1, l1_ratio=None,
max_iter=100, multi_class='warn',
n_jobs=None, penalty='l2',
random_state=None, solver='warn',
tol=0.0001, verbose=0,
warm_start=False),
iid='warn', n_jobs=-1, param_grid={}, pre_dispatch='2*n_jobs',
refit=True, return_train_score=False, scoring='roc_auc',
verbose=3)
# Para ver el acierto de nuestro modelo con crossvalidation:
gs_reglog.best_score_
0.8808200244041702
2.- Árboles de decisión
# Importamos la librería necesaria:
from sklearn.tree import DecisionTreeClassifier
# Definimos pipeline:
arbolito = DecisionTreeClassifier()
# Definimos hiperparámetros:
gryd_hyper_arbolito = {"max_depth": [ 4, 5, 7, 8, 9]}
gs_arbolito = GridSearchCV(arbolito,
param_grid = gryd_hyper_arbolito,
cv=10,
scoring='roc_auc',
n_jobs=-1,
verbose=3)
# lanzamos el entrenamiento del modelo:
gs_arbolito.fit(Comentarios_train, notas_train)
Fitting 10 folds for each of 5 candidates, totalling 50 fits
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
[Parallel(n_jobs=-1)]: Done 16 tasks | elapsed: 8.5min
[Parallel(n_jobs=-1)]: Done 50 out of 50 | elapsed: 30.2min finished
GridSearchCV(cv=10, error_score='raise-deprecating',
estimator=DecisionTreeClassifier(class_weight=None,
criterion='gini', max_depth=None,
max_features=None,
max_leaf_nodes=None,
min_impurity_decrease=0.0,
min_impurity_split=None,
min_samples_leaf=1,
min_samples_split=2,
min_weight_fraction_leaf=0.0,
presort=False, random_state=None,
splitter='best'),
iid='warn', n_jobs=-1, param_grid={'max_depth': [4, 5, 7, 8, 9]},
pre_dispatch='2*n_jobs', refit=True, return_train_score=False,
scoring='roc_auc', verbose=3)
# Vemos el acierto de nuestro modelo:
gs_arbolito.best_score_
0.7371649075581919
# Vemos cual es la mejor profundidad:
gs_arbolito.best_params_
{'max_depth': 9}
Vemos que la mejor profundidad es 9
3.- K-nearest neighbors
# Importamos librerías
from sklearn.neighbors import KNeighborsClassifier
vecinos = KNeighborsClassifier()
gryd_hyper_vecinos = {"n_neighbors": [3]}
# Hacemos prueba solo con 3 vecinos, ya que con más parámetros se eterniza el proceso.
gs_vecinos = GridSearchCV(vecinos,
param_grid = gryd_hyper_vecinos,
cv=10,
scoring='roc_auc',
n_jobs=-1,
verbose=3)
gs_vecinos.fit(Comentarios_train, notas_train)
Fitting 10 folds for each of 1 candidates, totalling 10 fits
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
[Parallel(n_jobs=-1)]: Done 3 out of 10 | elapsed: 95.0min remaining: 221.6min
[Parallel(n_jobs=-1)]: Done 7 out of 10 | elapsed: 98.7min remaining: 42.3min
[Parallel(n_jobs=-1)]: Done 10 out of 10 | elapsed: 112.5min finished
GridSearchCV(cv=10, error_score='raise-deprecating',
estimator=KNeighborsClassifier(algorithm='auto', leaf_size=30,
metric='minkowski',
metric_params=None, n_jobs=None,
n_neighbors=5, p=2,
weights='uniform'),
iid='warn', n_jobs=-1, param_grid={'n_neighbors': [3]},
pre_dispatch='2*n_jobs', refit=True, return_train_score=False,
scoring='roc_auc', verbose=3)
gs_vecinos.best_params_
{'n_neighbors': 3}
Evidentemente, ya que no hemos probado más parámetros.
gs_vecinos.best_score_
0.6939910533080703
Con el tiempo que nos ha llevado cada modelo, vamos a quedarnos con estos 3 modelos: 1. Regresión Logística: 0.8808200244041702 2. Árboles de decisión: 0.7371649075581919 3. K-Nearest Neoghbors: 0.6939910533080703
Como podemos observar, la Regresión logística nos da el mejor resultado con 0.8808200244041702
Probamos el modelo seleccionado con el conjunto test
mejor_modelo = gs_reglog.best_estimator_
mejor_modelo
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
intercept_scaling=1, l1_ratio=None, max_iter=100,
multi_class='warn', n_jobs=None, penalty='l2',
random_state=None, solver='warn', tol=0.0001, verbose=0,
warm_start=False)
mejor_modelo.fit(Comentarios_train, notas_train)
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
intercept_scaling=1, l1_ratio=None, max_iter=100,
multi_class='warn', n_jobs=None, penalty='l2',
random_state=None, solver='warn', tol=0.0001, verbose=0,
warm_start=False)
predicciones_test = mejor_modelo.predict(Comentarios_test)
predicciones_test
array([1, 0, 1, ..., 0, 1, 0], dtype=int64)
Y ahora vemos que resultados tenemos con distintas métricas
AUC-ROC
from sklearn.metrics import roc_auc_score, confusion_matrix, classification_report, f1_score
Score_auc = roc_auc_score(y_true = predicciones_test, y_score = notas_test)
print(Score_auc)
# Vemos el auc_score con el test
0.8061484556357356
F1-Score
F1_score = f1_score(y_true = predicciones_test, y_pred = notas_test)
print(F1_score)
# Vemos el F1_score con el test
0.8141469119206095
Matriz de Confusión
matriz_confusion = confusion_matrix(y_true = predicciones_test, y_pred = notas_test)
matriz_confusion
array([[67110, 16546],
[17464, 74492]], dtype=int64)
matriz_confusion_df = pd.DataFrame(matriz_confusion)
label = ['positivo', 'negativo']
matriz_confusion_df.columns= label
matriz_confusion_df.index = label
# Y nombramos lo que son las columnas y las filas:
matriz_confusion_df.columns.name = "Predicho"
matriz_confusion_df.index.name = "Real"
matriz_confusion_df
Predicho | positivo | negativo |
---|---|---|
Real | ||
positivo | 67110 | 16546 |
negativo | 17464 | 74492 |
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
plt.figure(figsize=(8,4))
sns.heatmap(matriz_confusion_df,
annot=True,
fmt="d",
cmap="Blues")
pass

Probamos nuestro modelo con nuevos comentarios
Para probar nuestro modelo, vamos a meter una serie de comentarios inventados y una valoración de los mismos.
comentarios_prueba = np.array([("Qué gran producto, estoy realmente satisfecho."),
("Es un producto bueno en relación calidad precio"),
("No me convence el producto, creo que lo voy a devolver"),
("Es un producto terrible, no lo recomiendo para nada"),
("No me terminan de agradar, estarían mejor si no fueran tan verdes"),
])
notas_prueba = np.array([1, 1, 0, 0, 0])
# Lanzamos el transform de nuestra prueba:
Comentarios_prueba = vectorizer.transform(comentarios_prueba)
print(Comentarios_prueba)
(0, 81230) 1
(0, 142724) 1
(0, 148949) 1
(0, 158774) 1
(1, 23316) 1
(1, 25642) 1
(1, 140158) 1
(1, 142724) 1
(1, 151888) 1
(2, 39313) 1
(2, 41403) 1
(2, 50444) 1
(2, 142724) 1
(2, 187554) 1
(3, 142724) 1
(3, 149737) 1
(3, 174604) 1
(4, 4061) 1
(4, 111903) 1
(4, 162658) 1
(4, 172049) 1
(4, 174377) 1
(4, 185010) 1
predicciones_prueba = mejor_modelo.predict(Comentarios_prueba)
predicciones_prueba
array([1, 1, 0, 0, 0], dtype=int64)
Vemos el F1_score
F1_score = f1_score(y_true = predicciones_prueba, y_pred = notas_prueba)
print(F1_score)
1.0
Realmente con los comentarios que he introducido era previsible que el modelo acertara a la perfección, tan solo queríamos comprobar que el modelo podría funcionar con otros comentarios.