Skip to content

Latest commit

 

History

History
1129 lines (830 loc) · 38.7 KB

7506R-1C2023-TP2-GRUPO09-CHP01.md

File metadata and controls

1129 lines (830 loc) · 38.7 KB
jupyter
jupytext kernelspec
cell_metadata_filter formats text_representation
-all
ipynb,md
extension format_name format_version jupytext_version
.md
markdown
1.3
1.14.5
display_name language name
Python 3 (ipykernel)
python
python3
try:
  import google.colab
  IN_COLAB = True
except:
  IN_COLAB = False
import pandas as pd 
import numpy as np

import seaborn as sns

from matplotlib import pyplot as plt

from collections import Counter

import sklearn
from sklearn.model_selection import train_test_split
#import sklearn as sk


#modelos y metricas
import seaborn as sns
from matplotlib import pyplot as plt
from joblib import dump, load
from os.path import exists
from sklearn.model_selection import StratifiedKFold, KFold,RandomizedSearchCV, train_test_split, cross_validate
from sklearn.metrics import confusion_matrix, classification_report , f1_score, make_scorer, precision_score, recall_score, accuracy_score,f1_score

#Xval
from sklearn.model_selection import train_test_split, RandomizedSearchCV, GridSearchCV, cross_val_score

#vectorizacion
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.ensemble import VotingClassifier
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import train_test_split
from sklearn.metrics import precision_score, recall_score, accuracy_score,f1_score

from joblib import dump, load

from os.path import exists
from os import environ

import string

import nltk
from sklearn.pipeline import Pipeline

import sklearn ### ESTA NO SE BORRA ???? #TODO
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import MultinomialNB

from os.path import exists
environ['TF_CPP_MIN_LOG_LEVEL'] = '3' 

if IN_COLAB == True:
    !pip install nltk

import nltk
nltk.download('stopwords')
stopwords_es = nltk.corpus.stopwords.words('spanish')   
    
import re
from unicodedata import normalize

import tensorflow as tfr

from tensorflow import keras
from keras.preprocessing.text import one_hot, Tokenizer
from keras.models import Sequential
from keras.layers.core import Activation, Dropout, Dense, SpatialDropout1D
from keras.layers import Flatten, GlobalMaxPooling1D, Embedding, Conv1D, LSTM, TextVectorization
from keras_preprocessing.sequence import pad_sequences
from keras.wrappers.scikit_learn import KerasClassifier
import string

#Random forest
from sklearn.ensemble import RandomForestClassifier 

Constantes

# Constantes
SEED=9
JOBS=-2
reviewDfOriginal = pd.read_csv("./review_train.csv")
reviewDf = reviewDfOriginal.copy()
review_pruebasOriginal = pd.read_csv("./review_test.csv")
review_pruebas = review_pruebasOriginal.copy()
review_pruebas
reviewDf = reviewDf.drop(["ID"],  axis='columns', inplace=False)
reviewDf

FILTRO REVIEWS EN INGLES

Filtramos y nos quedamos solo con las reviews en epanol para mejorar la efectividad del modelo. Hay 3 listas hechas a mano debido a que tuvimos que tener cuidado de no incluir palabras cuya raiz exitiera en español, o fueran parte de apellidos ya que en esos casos se veian contenidas (love-lace-, hate-r, excelent-e). Al tener 3 listas con ditintos grupos, fue mas facil armar la lista final. Reconocemos que no es el mejor filtro y que es posible que hayamos eliminado unas pocas reseñas en español y hayan quedado unas pocas en ingles. Sin embargo estamos seguros de que si saca la mayoria de las reseñas en ingles.

lista_palabras_comunes_en = ["have","scary","nothing","issue"]
lista_palabras_positivas_en =["interesting","amusing","intelligent","pretty","beautiful"]
lista_palabras_negativas_en =["boring","bullshit","risk","loss","poor","ugly"]
lista_completa = lista_palabras_comunes_en + lista_palabras_positivas_en + lista_palabras_negativas_en


reviewDf_espanol = reviewDf [~reviewDf["review_es"].str.contains('|'.join(lista_completa))]

reviewDf_espanol

Particion

Hacemos la particion en test y train con el dataset ya filtrado

reviewDf_x = reviewDf_espanol.drop(["sentimiento"],  axis='columns', inplace=False)

reviewDf_y = reviewDf_espanol['sentimiento'].copy()

x_train, x_test, y_train, y_test = train_test_split(reviewDf_x,
                                                    reviewDf_y,
                                                    test_size=0.30,
                                                    random_state=SEED,
                                                    shuffle=True
                                                    )

Bayes Naive

Creamos un modelo Bayes Naives con el proposito de observar el comportamiento general del mismo y generamos una primera predicción

if not exists('modelos/TP2/naiveBayes-n.joblib'):

    modeloBayesNaive = make_pipeline(TfidfVectorizer(), MultinomialNB())
    modeloBayesNaive.fit(x_train.review_es, y_train)

    dump(modeloBayesNaive, 'modelos/TP2/naiveBayes-n.joblib')

else:
    modeloBayesNaive = load('modelos/TP2/naiveBayes-n.joblib')

prediccion = modeloBayesNaive.predict(x_test.review_es)

Analizamos las metricas del modelo para ver cual es el desempeño global del mismo, de manera que, se puede tener una referencia del comportamiento general del modelo a la hora de buscar hiperparametros

#performance
print(classification_report(y_test,prediccion))

#Creamos la matriz de confusión
tablota=confusion_matrix(y_test, prediccion)

#Grafico la matriz de confusión
sns.heatmap(tablota,cmap='GnBu',annot=True,fmt='g')
plt.xlabel('Predicho')
plt.ylabel('Verdadero')

Generamos una predicción para Kaggle con nuestro primer modelo, el cual tiene un desempeño aceptable

if not exists('submissions/TP2/naiveBayes-nuevo.csv'):
    prediccionTesteo = modeloBayesNaive.predict(review_pruebas.review_es)
    df_submission = pd.DataFrame({'id': review_pruebasOriginal['ID'], 'sentimiento': prediccionTesteo})
    df_submission.to_csv('submissions/TP2/naiveBayes-nuevo.csv', index=False)

Busqueda de hiperparametros

Hacemos un pequeño ajuste sobre los datos de entrada en esta ocación

y_test_transformed = y_test.map(lambda x: 0 if x == 'positivo' else 1)
y_train_transformed = y_train.map(lambda x: 0 if x == 'positivo' else 1)

Para la busqueda de hiperparametros del modelo bayes naives, se debe considerar realizar en simultaneo la optimización tanto del modelo BN como encontrar las condiciones para que la tecnica TF-IDF, implementada por el TfidfVectorizer, mejore las metricas en el analisis de sentimientos

Por lo tanto realizamos la busqueda de hiperparametros usando valores de prueba para intentar mejorar el modelo y por otro lado valores de prueba para el preprocesamiento del texto

if not exists('modelos/TP2/nb_hiper.joblib'):
    frecuencia_maxima = [0.9, 0.95, 0.85, 0.98]
    frecuencia_minima = [0.05, 0.07, 0.1, 0.03]
    ngram_range = [(1,1), (1,2), (3,1)]
    alpha = [0.1, 0.5, 1.0, 2.0, 0.8, 0.3, 1.5]
    fit_prior = [True, False]
    class_prior = [None, [0.2, 0.8], [0.5, 0.5]]

    metrica = make_scorer(sklearn.metrics.f1_score)
    folds = 5

    modelo = Pipeline(steps=[('tfidfvectorizer', TfidfVectorizer()), ('multinomialnb', MultinomialNB())])


    parametros = {
        'tfidfvectorizer__stop_words': ['english', stopwords_es, None],
        'tfidfvectorizer__max_df': frecuencia_maxima,
        'tfidfvectorizer__min_df': frecuencia_minima,
        'tfidfvectorizer__ngram_range': ngram_range,
        'multinomialnb__alpha': alpha,
        'multinomialnb__fit_prior': fit_prior,
        'multinomialnb__class_prior': class_prior
    }

    modelo_rcv = RandomizedSearchCV(modelo, parametros, cv=folds, scoring=metrica)
    modelo_rcv.fit(x_train.review_es, y_train_transformed)

    dump(modelo_rcv, 'modelos/TP2/nb_hiper.joblib')
else: 
    modelo_rcv = load('modelos/TP2/nb_hiper.joblib')
modelo_rcv

Observamos como se comporta el modelo al estudiarlo a detalle usando la tecnica de random search cross validation

print("F1-Score del modelo: " + str(modelo_rcv.best_score_))
for param, value in modelo_rcv.best_params_.items():
    if param == "tfidfvectorizer__stop_words":
        continue
        
    print(str(param) + ":" + str(value))

Se observa que el modelo tiene un F1-Score bastante parecido a la primera parte del analisis, más sin embargo, el añadido de estos hiperparametros puede ayudar en la simplificación del modelo y mejorar la generalización

Entrenamos el modelo con los nuevos hiperparametros y realizamos una validacion cruzada para verificar que no haya ningun problema con el modelo seleccionado

if not exists('modelos/TP2/nb_conhp.joblib'):
    mejores_parametros = modelo_rcv.best_params_

    modelo_final = Pipeline(steps=[
        ('tfidfvectorizer', TfidfVectorizer(stop_words=mejores_parametros['tfidfvectorizer__stop_words'],
                                            max_df=mejores_parametros['tfidfvectorizer__max_df'],
                                            min_df=mejores_parametros['tfidfvectorizer__min_df'],
                                             ngram_range=mejores_parametros['tfidfvectorizer__ngram_range'])),
        ('multinomialnb', MultinomialNB(alpha = mejores_parametros['multinomialnb__alpha'],
                                        fit_prior = mejores_parametros['multinomialnb__fit_prior'],
                                        class_prior= mejores_parametros['multinomialnb__class_prior']))
    ])

    modelo_final.fit(x_train.review_es, y_train_transformed)
    dump(modelo_final, 'modelos/TP2/nb_conhp.joblib')
else:
    modelo_final = load('modelos/TP2/nb_conhp.joblib')
if not exists('modelos/TP2/metricas_nb.joblib'):
    kfoldcv =StratifiedKFold(n_splits=10)
    resultados_nb = cross_validate(modelo_final, x_train.review_es, y_train, cv=kfoldcv,scoring=metrica ,return_estimator=True)
    metricas_nb = resultados_nb['test_score']
    modelo_final = resultados_bb['estimator'][np.where(metricas_nb==max(metricas_nb))[0][0]]
else: 
    metricas_nb = load('modelos/TP2/metricas_nb.joblib')
metric_labelsCV = ['F1 Score']*len(metricas_nb)
sns.set_context('talk')
sns.set_style("darkgrid")
plt.figure(figsize=(6,5))
sns.boxplot(metricas_nb)
plt.title("Modelo verificado con 10 folds")

Observamos que la validación cruzada reatifica el modelo generado y los hiperparametros usados por lo tanto realizamos la prediccion de peso para la competencia de kaggle

if not exists('submissions/TP2/NMmejorado8.csv'):
    df_submission = pd.DataFrame({'id': review_pruebas['ID'], 'sentimiento': y_pred})
    df_submission['sentimiento'] = df_submission['sentimiento'].map({0: 'positivo', 1: 'negativo'})
    df_submission.to_csv('NMmejorado8.csv', index=False)

Random Forest

Creamnos el modelo con parametros arbitrarios parta obtener una prediccion inicial. Decidimos fijar los valores de samples_leaf y samples_split y n_estimators por nuestra cuenta ya luuego de varioas pruebas obsevamos que con valores por default (1, 2 y 100 respectictivamente) genera un modelo muy grande que no podremos subir a gitthub y cuya efectividad no mejorara mucho respecto a uno un poco mas chico

if exists('modelos/TP2/modeloRandomForest-sin-optimizar-final.joblib') == False:
    #Creamos un clasificador con hiperparámetros arbitrarios
    
    rfc = RandomForestClassifier(n_jobs=JOBS,
                                 criterion="gini", 
                                 random_state=SEED, 
                                 min_samples_leaf=15,
                                 min_samples_split=40,
                                 n_estimators=40, 
                                 class_weight="balanced")
    
    #rfc = RandomForestClassifier(random_state=SEED, n_jobs=-1)
    
    modeloRandomForest = make_pipeline(TfidfVectorizer(), rfc)

    modeloRandomForest.fit(x_train.review_es, y_train)

    dump(modeloRandomForest, 'modelos/TP2/modeloRandomForest-sin-optimizar-final.joblib')

else:
    modeloRandomForest = load('modelos/TP2/modeloRandomForest-sin-optimizar-final.joblib')

Miramos como fue en test

prediccion_rf = modeloRandomForest.predict(x_test.review_es)

#performance
print(classification_report(y_test,prediccion_rf))


#Creamos la matriz de confusión
tabla=confusion_matrix(y_test, prediccion_rf)

#Grafico la matriz de confusión
sns.heatmap(tabla,cmap='GnBu',annot=True,fmt='g')
plt.xlabel('Predicho')
plt.ylabel('Verdadero')  

Hacemos la submission

if not exists('submissions/TP2/randomForest-sin-opti-final.csv'):
    prediccionTesteo_rf = modeloRandomForest.predict(review_pruebas.review_es)
    df_submission = pd.DataFrame({'id': review_pruebasOriginal['ID'], 'sentimiento': prediccionTesteo_rf})
    df_submission.to_csv('submissions/TP2/randomForest-sin-opti-final.csv', index=False)

Optimizacion de hiperparametros con Cros Validation

if exists('modelos/TP2/GsRandomForest_y_tdif-1.joblib') == False:

    gsrf = RandomForestClassifier(class_weight="balanced")


    modeloRandomForest_cv = Pipeline(steps= [ ('tfidfVectorizer', TfidfVectorizer() ), ('gsrf', gsrf) ] )

    param_grid = { "gsrf__criterion" : ["gini", "entropy"], 
                   "gsrf__min_samples_leaf" : [15, 30, 60], #Vamos a hacer muchas combinaciones ya que solo vamos
                   "gsrf__min_samples_split" : [40, 50, 60],#a correr este modelo 1 sola vez; ya que lo vamos a 
                   "gsrf__n_estimators": [40, 50, 80],  #guardar   
                   
                    #Veo si conviene usar stopwords o no.
                   "tfidfVectorizer__stop_words": [None, 'english', stopwords_es], 

                    "tfidfVectorizer__strip_accents": [None, "ascii"],
                    
                    "tfidfVectorizer__min_df": [0.01, 0.08, 0.1],

                    "tfidfVectorizer__max_df" :[ 0.10 , 0.20 , 0.5],

                    "tfidfVectorizer__lowercase" : [True, False],
                    
                   "tfidfVectorizer__analyzer": ["word", "char", "char_wb"],

                    }

    #Probamos entrenando sólo con 1 métrica: f1_scoree

    rf_gs = GridSearchCV(estimator=modeloRandomForest_cv, param_grid=param_grid, scoring="f1", cv=5, n_jobs=JOBS) #Optimizamos f1_score

    #rf_gs = RandomizedSearchCV (estimator=modeloRandomForest_cv, param_grid=param_grid, scoring="f1", cv=5, n_jobs=JOBS) #Optimizamos f1_score

    gs_fit = rf_gs.fit(x_train.review_es, y_train)
    
    #guardamos el grid search
    dump(gs_fit, 'modelos/TP2/GsRandomForest_y_tdif-1.joblib')

else:
    gs_fit = load('modelos/TP2/GsRandomForest_y_tdif-1.joblib')

gs_fit.best_params_

Podemos ver que para los hiperparametros del RF, Grid Search recomienda son los mismos que los habiamos usado arbitrariamente. Esto se debe a que, Grid search, en general, tiende a elegir el numero mas bajo de sample_leafs, samples_split y n_estimators. Por ello como ya comentamos tomamos como valores mas chicos los usados en el RF sin optimizar para asegurarnos que en la optimizacion de hiperparametros no escoja valores muy pequeños generando un posible overfitting y un modelo muy complejo.
En cuanto a los hiperparametros del vectorizador, recomienda no usar stopwords (las de la lista de la librería NLTK que mandamos como parámetro), no quitar acentos, y usar como analyzer: "word". La principal diferencia es en cuanto al minimo y maximo numero de palabras a tener en cuenta para armar el vocabulario. Recomienda eliminar las palabras que representan menos del 1% del vocabulario y aquellas que representan más del 10% del vocabulario.

#Obtenemos el mejor modelo
mejor_modelo_rf = gs_fit.best_estimator_

#Predicción

prediccion_mejor_modelo_rf = mejor_modelo_rf.predict(x_test.review_es)
prediccion_mejor_modelo_rf
if not exists ('modelos/TP2/modelo_RandomForest_y_tdif-final.joblib'):
    dump(mejor_modelo_rf, "modelos/TP2/modelo_RandomForest_y_tdif-final.joblib")

Vemos como le va en test

#performance
print(classification_report(y_test,prediccion_mejor_modelo_rf))


#Creamos la matriz de confusión
tabla=confusion_matrix(y_test, prediccion_mejor_modelo_rf)

#Grafico la matriz de confusión
sns.heatmap(tabla,cmap='GnBu',annot=True,fmt='g')
plt.xlabel('Predicho')
plt.ylabel('Verdadero') 

Vemos que f1_score disminuyo. En el informe profundizamos en las posibles causa de esto.

Cross Validation

Hacemos Cross validation con 5 folds

kfoldcv =StratifiedKFold(n_splits=5) 
scorer_fn = make_scorer(sklearn.metrics.f1_score, pos_label='positivo' )

if not exists ('modelos/TP2/resultados_cv_randomForest-final'):
    resultados_rf = cross_validate(mejor_modelo_rf, x_train.review_es, y_train, cv=kfoldcv,scoring=scorer_fn,return_estimator=True, n_jobs = JOBS)
    
    dump(resultados_rf, "modelos/TP2/resultados_cv_randomForest-final")
else:
    resultados_rf = load("modelos/TP2/resultados_cv_randomForest-final")

metricas_cv_rf = resultados_rf['test_score']
metric_labels_CV_rf = ['F1 Score']*len(metricas_cv_rf) 
sns.set_context('talk')
sns.set_style("darkgrid")
plt.figure(figsize=(8,8))
sns.boxplot(metricas_cv_rf)
plt.title("Modelo entrenado con 5 folds")

Se puede ver que no hay mucha variacion en los valores obtenidos por lo cual podemos concluir que es un modelo bueno para generalizar.

Submission Random Forest

if not exists('submissions/TP2/randomForest-y_tdif-final-.csv'):
    prediccionTesteoMejorRf = mejor_modelo_rf.predict(review_pruebas.review_es)
    df_submission = pd.DataFrame({'id': review_pruebasOriginal['ID'], 'sentimiento': prediccionTesteoMejorRf})
    df_submission.to_csv('submissions/TP2/randomForest-y_tdif-final.csv', index=False)

La submission nos dio una disminucion de 4 ptos en Kaggle. Al igual que con las metricas en el ocnjunto de test, en el infomre profundizamos en la posibles razones de esto.

XGBoost

Realizamos los mismos ajustes hechos para la busqueda de hiperparametros de NB, en este caso, el modelo XGBoost no es capaz de entender las etiquetas positivo/megativo

y_test_transformed = y_test.map(lambda x: 0 if x == 'positivo' else 1)
y_train_transformed = y_train.map(lambda x: 0 if x == 'positivo' else 1)

Generamos un modelo base XGBoost que permitira observar un comportamiento global del modelo a la hora de analizar sentimientos en el texto de las reseñas

if not exists('modelos/TP2/xgb_base.joblib'):
    xgb_base = make_pipeline(TfidfVectorizer(), XGBClassifier( random_state=0, n_estimators=100))
    xgb_base.fit(x_train.review_es, y_train_transformed)
else:
    xgb_base = load("modelos/TP2/xgb_base.joblib")

Evaluando nuestro modelo con las metricas que nos interese

y_pred = xgb_base.predict(x_test.review_es)

Verificamos la performance del modelo y observamos su matriz de confusión

#performance
print(classification_report(y_test_transformed,y_pred))

#Creamos la matriz de confusión
tabla=confusion_matrix(y_test_transformed, y_pred)

#Grafico la matriz de confusión
sns.heatmap(tabla,cmap='GnBu',annot=True,fmt='g')
plt.xlabel('Predicho')
plt.ylabel('Verdadero')

Observamos que el modelo tiene un buen desempeño a la hora de

Generamos una primera predicción para kaggle

if not exists('submissions/TP2/xgboost_base.csv'):
    pred = xgb_base.predict(review_pruebas.review_es)
    df_submission = pd.DataFrame({'id': review_pruebasOriginal['ID'], 'sentimiento': pred})
    df_submission['sentimiento'] = df_submission['sentimiento'].map({0: 'positivo', 1: 'negativo'})
    df_submission.to_csv('submissions/TP2/xgboost_base.csv', index=False)

Busqueda de hiperparametros del XGBoost

En un intento de mejorar las metricas de prediccion del modelo, como tambien de simplificarlo y generar un modelo más simple con una capacidad de prediccion similar realizamos la busqueda de hiperparamtros del xgboost utilizando parametros conocidos, como también, modificando hiperparametros en el preprocesamiento del texto para mejorar el analisis de sentimientos y ajustar le posible overfit

if not exists('modelos/TP2/hipers_xgb.joblib'):
    
    estimadores = [90, 100, 110, 150]
    frecuencia_maxima = [0.9, 0.95, 0.98]
    frecuencia_minima = [0.05, 0.07, 0.03]
    profundidad_max = [7, 8, 9, 10, 15]
    learning_rate = [0.01, 0.05, 0.1, 0.2]
    metrica = make_scorer(sklearn.metrics.f1_score)
    folds = 5

    parametros = {
    'tfidfvectorizer__stop_words': ['english', stopwords_es],
    'tfidfvectorizer__max_df': frecuencia_maxima,
    'tfidfvectorizer__min_df': frecuencia_minima,
    'tfidfvectorizer__ngram_range': [(1,1), (1,2)],
    'xgbclassifier__n_estimators': estimadores,
    'xgbclassifier__max_depth': profundidad_max,
    'xgbclassifier__learning_rate': learning_rate,
}

    modelo = make_pipeline(TfidfVectorizer(), XGBClassifier())

    modelo_rcv = RandomizedSearchCV(modelo, parametros, cv=folds, scoring = metrica)
    modelo_rcv.fit(x_train.review_es, y_train)
    dump(modelo_rcv, 'modelos/TP2/hipers_xgb.joblib')
else: 
    metrica = make_scorer(sklearn.metrics.f1_score)
    modelo_rcv = load('modelos/TP2/hipers_xgb.joblib')
# print("F1-Score del modelo: " + modelo_rcv.best_score_)
# print("Hiperparametros del modelo con mejor F1-Score:" + modelo_rcv.best_params_)

Entrenamos un nuevo modelo usando los hiperparametros encontrados en la busqueda anterior

if not exists('modelos/TP2/xgb_tuneado.joblib'):
    mejores_parametros = modelo_rcv.best_params_

    modelo_final = Pipeline(steps=[('tfidfvectorizer', TfidfVectorizer(stop_words=mejores_parametros['tfidfvectorizer__stop_words'],
                                                                   max_df=mejores_parametros['tfidfvectorizer__max_df'],
                                                                   min_df=mejores_parametros['tfidfvectorizer__min_df'],
                                                                   ngram_range=mejores_parametros['tfidfvectorizer__ngram_range'])),
                                   ('xgbclassifier', XGBClassifier(n_estimators=mejores_parametros['xgbclassifier__n_estimators'],
                                                                  max_depth=mejores_parametros['xgbclassifier__max_depth'],
                                                                  learning_rate=mejores_parametros['xgbclassifier__learning_rate']))])

    modelo_final.fit(x_train.review_es, y_train_transformed)
else:
    xgb_tuneado = load('modelos/TP2/xgb_tuneado.joblib')

Realizo validacion cruzada del modelo para observar su comportamiento nota TARDA MUCHISIMO

if not exists('modelos/TP2/xgb_metricas.joblib'):
    kfoldcv =StratifiedKFold(n_splits=10)
    resultados_xgb = cross_validate(xgb_tuneado, x_train.review_es, 
                                    y_train_transformed, cv=kfoldcv,
                                    scoring=metrica ,return_estimator=True)
    metricas_xgb = resultados_xgb['test_score']
    dump(metricas_xgb, 'modelos/TP2/xgb_metricas.joblib')
    xgb_tuneado = resultados_xgb['estimator'][np.where(metricas_xgb==max(metricas_xgb))[0][0]]
else: 
    metricas_xgb = load('modelos/TP2/xgb_metricas.joblib')
metric_labelsCV = ['F1 Score']*len(metricas_xgb)
sns.set_context('talk')
sns.set_style("darkgrid")
plt.figure(figsize=(6,5))
sns.boxplot(metricas_xgb)
plt.title("Modelo entrenado con 10 folds")

Notas sobre XGBoost

El XGBoost más efectivo con un score en kaggle de 0.72 fue generando usando unicamente el conjunto de hiperparametros correspondiente al XGboost y tan solo modificando las stopwords en el procesamiento del texto, sin embargo, se realizo multiples pruebas con multiples hiperparametros y esas busquedas son las reflejadas en el analisis

Red Neuronal

Pre tokenizacion

Antes de tokenizar las reviews, vamos a hacer una pequena limpieza adicional.

if not exists('reviews_filtradas.csv'):
    frasesFiltradas = []
    for index, value in reviewDf_espanol["review_es"].items():
        #Ponemos todas las palabras en lowercase
        value = value.lower()

        #Saco las stopwords
        valueFiltrado = [x for x in value.split() if x not in stopwords_es]
        #Vuelvo a unir el texto
        valueFiltrado = " ".join(valueFiltrado)

        #Saca los diacriticos de letras como vocales, etc (la ñ se mantiene)
        #Expresion regular obtenida de: https://es.stackoverflow.com/a/139811
        valueFiltrado = re.sub(r"([^n\u0300-\u036f]|n(?!\u0303(?![\u0300-\u036f])))[\u0300-\u036f]+", r"\1", 
                                normalize( "NFD", valueFiltrado), 0, re.I)
        valueFiltrado = normalize( 'NFC', valueFiltrado)

        #Saco los signos de puntuacion
        #Funcion obtenida de: https://stackoverflow.com/a/266162/13683575
        valueFiltrado =  valueFiltrado.translate(str.maketrans('', '', string.punctuation))
        valueFiltrado =  valueFiltrado.translate(str.maketrans('', '', '¡'))
        valueFiltrado =  valueFiltrado.translate(str.maketrans('', '', '¿'))
        
        #Anadimos la frase a la lista de frases filtradas
        frasesFiltradas.append(valueFiltrado)
    reviewDfFiltrado = pd.DataFrame(data={'review_es':frasesFiltradas, 'sentimiento':reviewDf_espanol['sentimiento']})
    reviewDfFiltrado.to_csv('reviews_filtradas.csv', index=False)

else:
    reviewDfFiltrado = pd.read_csv("./reviews_filtradas.csv")
reviewDfFiltrado

Creacion de los sets de entrenamiento

Transformamos la columna objetivo de "positivo" y "negativo" a 0 y 1 para poder usarlos en la red neuronal

y = reviewDfFiltrado['sentimiento']
y = np.array(list(map(lambda x: 1 if x=="positivo" else 0, y)))
y

Creamos set de train y test

x_train, x_test, y_train, y_test = train_test_split(reviewDfFiltrado["review_es"],
                                                    y, 
                                                    test_size=0.3,  #proporcion 70/30
                                                    random_state=SEED) #Semilla 9, como el Equipo !!

Creamos el vocabulario en base a nuestro set de train. Las palabras van a estar ordenadas por su frecuencia

vocabulary = Counter()
for fila in x_train:
    fila = fila.split()
    for i in range(0, len(fila)):
        fila[i] = bytes(fila[i], 'utf-8')
    vocabulary.update(list(fila))

Vamos a ver si el vocabulario se genero correctamente

vocabulary.most_common()[1:5]

Vemos que la palabra mas frecuente es "pelicula", cosa que hace sentido.

Si no hubiesemos hecho el proceso de remocion de stopwords, es probable que la palabra mas frecuente seria una preposicion

Para reducir el tamaño del vocabulario, vamos a quedarnos solamente con las 10000 palabras mas frecuentes.

vocab_size = 10000
truncated_vocabulary = [
word for word, count in vocabulary.most_common()[:vocab_size]]

Vamos a crear una tabla que asocia cada palabra del diccionario con su coordena (la cual representa su frecuencia en el dataset de entrenamiento)

table = {}
for i in range(0, len(truncated_vocabulary)):
    table[truncated_vocabulary[i]] = i
table

Tokenizacion de las reviews

Pasamos de la representacion en texto de la reviews a una version tokenizada. Vamos a usar los ids de la tabla para lograrlo

Todas tienen que tener la misma longitud, asique decidimos que todas tengan 300 palabras como maximo.

Si faltan palabras o sobran, le ponemos el valor 10001 (para indicar que esta fuera de nuestro vocabulario)

def pasarDePalabraANumero(conjuntosDePalabras, cantMaxPalabras=300):
    listaDeNumeros = []
    i=0
    for fila in conjuntosDePalabras:
        fila = fila.split()
        fila = fila[:300]
        tokenDeFrase = []
        
        for word in fila:
            tokenDeFrase.append(int(table.get(bytes(word, 'utf-8'),10001)))
            
        while len(tokenDeFrase) < 300:
            tokenDeFrase.append(10001)
            
        listaDeNumeros.append(np.array(tokenDeFrase))
        i+=1
    return listaDeNumeros
numerico = pasarDePalabraANumero(x_train)
numericoTest = pasarDePalabraANumero(x_test)
numericoSubmit =  pasarDePalabraANumero(review_pruebas['review_es'])

Ejemplo de como se ven:

numerico[66]
data_list = np.stack(numerico)
data_list_test = np.stack(numericoTest)
data_list_submit = np.stack(numericoSubmit)

Creacion de la red (sin hiperparametros)

Vamos a usar la layer intermedia "GRU", siguiendo la guia del libro de la bibiliografia. Esta tiene comportamiento muy similar a la LSTM (las cuales son optimas para el analisis de sentimiento)

embed_size = 128
redNeuronal = keras.models.Sequential([
    
keras.layers.Embedding(len(table), embed_size,
        input_shape=[None]),
    
    keras.layers.GRU(128, return_sequences=True),
    
    keras.layers.GRU(128),
    
    keras.layers.Dense(1, activation="sigmoid")
])
redNeuronal.compile(loss="binary_crossentropy", optimizer="adam",
            metrics=["accuracy"])
redNeuronal.summary()
if exists('modelos/TP2/redNeuronalSentimiento2.joblib') == False:
    historia_modelo=redNeuronal.fit(x = data_list, 
                                    y = y_train, epochs=4)
    dump(redNeuronal, 'modelos/TP2/redNeuronalSentimiento2.joblib')
else:
    redNeuronal = load('modelos/TP2/redNeuronalSentimiento2.joblib')
if exists('modelos/TP2/predTestRedSinOptimi.joblib') == False:
    y_pred = redNeuronal.predict(data_list_test)
    y_predCerteza = np.where(y_pred>0.7,1,0)
    dump(y_predCerteza, 'modelos/TP2/predTestRedSinOptimi.joblib')
else:
    y_predCerteza = load('modelos/TP2/predTestRedSinOptimi.joblib')
y_predCerteza
ds_validacion=pd.DataFrame(y_predCerteza,y_test).reset_index()
ds_validacion.columns=['y_pred','y_real']

tabla=pd.crosstab(ds_validacion.y_pred, ds_validacion.y_real)
grf=sns.heatmap(tabla,annot=True, cmap = 'Blues', fmt='g')
plt.show()
#Calculo las métricas en el conjunto de evaluación
accuracy=accuracy_score(y_test,y_predCerteza)
recall=recall_score(y_test,y_predCerteza)
f1=f1_score(y_test,y_predCerteza,)
precision=precision_score(y_test,y_predCerteza)

print("Accuracy: "+str(accuracy))
print("Recall: "+str(recall))
print("Precision: "+str(precision))
print("f1 score: "+str(f1))
if not exists('submissions/TP2/redesNeuronales2.csv'):
    yEnEspanol =y_predCerteza
    yEnEspanol = np.array(list(map(lambda x: "positivo" if x==1 else "negativo", y_predCerteza)))
    df_submission = pd.DataFrame({'id': review_pruebasOriginal['ID'], 'sentimiento': yEnEspanol})
    df_submission.to_csv('submissions/TP2/redesNeuronales2.csv', index=False)

Busqueda de hiperparametros

Vamos a realizar la busqueda de hiperparametros:

loss='binary_crossentropy'
metrics=['accuracy']
optimizer="adam"

def creador_modelo(learning_rate = 0.1, 
                   activation = 'sigmoid', 
                   output = 2, 
                  hidden_layers = 2
                  ):
    modeloHiper = keras.Sequential()
    
    modeloHiper.add(keras.layers.Embedding(len(table), embed_size,
        input_shape=[None]))
    
    modeloHiper.add(keras.layers.GRU(128, return_sequences=True))
    modeloHiper.add(keras.layers.GRU(128))
    
    for i in range(hidden_layers):
        modeloHiper.add(keras.layers.Dense(output, activation=activation))

    modeloHiper.add(keras.layers.Dense(1, activation="sigmoid"))
    
    modeloHiper.compile(
      optimizer=optimizer,
      loss=loss, 
      metrics=metrics, 
    )
    return modeloHiper
model = KerasClassifier(build_fn=creador_modelo, 
                        verbose=1)
param_grid = { 
                  "hidden_layers" : [1, 5, 10, 15, 20], 
                    "output" : [1, 2, 4, 8, 32, 64], 
                    "batch_size" : [5, 10, 20],
                    "epochs" : [5, 10, 15],
                   "activation": ["sigmoid", "relu", "softmax", "softplus", "elu", ]
             } 

Vamos a realizar 5 random searches para buscar los mejores hiperparametros para la red

rs = RandomizedSearchCV(estimator=model, 
                        param_distributions=param_grid,
                        n_jobs=JOBS, cv=3,
                        n_iter=8)
if exists('modelos/TP2/rs_redNeuronal.joblib') == False:
    rs_redNeuronal=rs.fit(X = data_list, 
                            y = y_train)

    dump(rs_redNeuronal, 'modelos/TP2/rs_redNeuronal.joblib')
else:
    rs_redNeuronal = load('modelos/TP2/rs_redNeuronal.joblib')
rs_redNeuronal.best_params_

Creamos la red con los hiperparametros encontrados arriba

embed_size = 128
modeloHiper = keras.models.Sequential()
    
modeloHiper.add(keras.layers.Embedding(len(table), embed_size,
        input_shape=[None]))
    
modeloHiper.add(keras.layers.GRU(128, return_sequences=True))
    
modeloHiper.add(keras.layers.GRU(128))
    
for i in range(rs_redNeuronal.best_params_['hidden_layers']):
    modeloHiper.add(keras.layers.Dense(rs_redNeuronal.best_params_['output'],
                                       activation=rs_redNeuronal.best_params_['activation']))
    
modeloHiper.add(keras.layers.Dense(1, activation="sigmoid"))

modeloHiper.compile(loss="binary_crossentropy", optimizer="adam",
            metrics=["accuracy"])
if exists('modelos/TP2/redNeuronalHiper.joblib') == False:
    historia_modelo=modeloHiper.fit(x = data_list, 
                                    y = y_train, epochs=rs_redNeuronal.best_params_['epochs'])

    dump(modeloHiper, 'modelos/TP2/redNeuronalHiper.joblib')

else:
    modeloHiper = load('modelos/TP2/redNeuronalHiper.joblib')
if exists('modelos/TP2/predTestRedConOptimi.joblib') == False:
    y_pred = modeloHiper.predict(data_list_test)
    y_pred
    y_predCerteza = np.where(y_pred>0.7,1,0)
    y_predCerteza
    dump(y_predCerteza, 'modelos/TP2/predTestRedConOptimi.joblib')
else:
    y_predCerteza = load('modelos/TP2/predTestRedConOptimi.joblib')
y_predCerteza
ds_validacion=pd.DataFrame(y_predCerteza,y_test).reset_index()
ds_validacion.columns=['y_pred','y_real']

tabla=pd.crosstab(ds_validacion.y_pred, ds_validacion.y_real)
grf=sns.heatmap(tabla,annot=True, cmap = 'Blues', fmt='g')
plt.show()
#Calculo las métricas en el conjunto de evaluación
accuracy=accuracy_score(y_test,y_predCerteza)
recall=recall_score(y_test,y_predCerteza)
f1=f1_score(y_test,y_predCerteza,)
precision=precision_score(y_test,y_predCerteza)

print("Accuracy: "+str(accuracy))
print("Recall: "+str(recall))
print("Precision: "+str(precision))
print("f1 score: "+str(f1))

Prediccion al set de datos de test

if exists('modelos/TP2/predTestRedConOptimi.joblib') == False:
    y_pred = modeloHiper.predict(data_list_submit)
    y_predCerteza = np.where(y_pred>0.7,1,0)
    dump(y_predCerteza, 'modelos/TP2/predTestRedConOptimi.joblib')
else:
    y_predCerteza = load('modelos/TP2/predTestRedConOptimi.joblib')
y_predCerteza
if not exists('submissions/TP2/redesNeuronales3.csv'):
    yEnEspanol =y_predCerteza
    yEnEspanol = np.array(list(map(lambda x: "positivo" if x==1 else "negativo", y_predCerteza)))
    df_submission = pd.DataFrame({'id': review_pruebasOriginal['ID'], 'sentimiento': yEnEspanol})
    df_submission.to_csv('submissions/TP2/redesNeuronales3.csv', index=False)

Ensamble de 3 modelos

Armamos un ensamble sencillo de tipo voting con los modelos bases generados a lo largo del analisis.

La utilización de los modelos bases se debe a que utilizamos varios predictores debiles con el proposito de construir uno que en conjunto funcione mejor y asi aprovechar tambien los modelos diseñados previamente

if not exists('modelos/TP2/voting.joblib.gz'): #Tenemos el archivo comprimido porque era muy grande
    naives = load("modelos/TP2/nb_conhp.joblib")
    xgb = load("modelos/TP2/xgb_base.joblib")
    rf = load("modelos/TP2/modeloRandomForest-sin-optimizar-final.joblib")

    ensamble = VotingClassifier(estimators = [
        ('nb', naives),
        ('xgb', xgb),
        ('rf', rf)
    ])
    ensamble.fit(x_train, y_train)
    dump(ensamble, 'modelos/TP2/voting.joblib')
    !gzip modelos/TP2/voting.joblib #Comprimimos

else:
    !gzip -d -k modelos/TP2/voting.joblib.gz #Descomprimimos
    ensamble = load('modelos/TP2/voting.joblib')
    !rm modelos/TP2/voting.joblib
    

Entrenamos el modelo

ensamble

Sobre el ensamble realizamos una validacion cruzada para comprobar que su metricas al estar asociadas a las metricas individuales de los modelos siguie siendo optima

kfoldcv = StratifiedKFold(n_splits=5) 
scorer_fn = make_scorer(sklearn.metrics.f1_score)

if not exists ('modelos/TP2/ensambleCrossvalidation.joblib.gz.part-aa'):
    resultados_rf = cross_validate(ensamble, x_train, 
                                   y_train, cv=kfoldcv,
                                   scoring=scorer_fn,
                                   return_estimator=True, n_jobs = JOBS)
    
    dump(resultados_rf, "modelos/TP2/ensambleCrossvalidation.joblib")
    #Comprimimos el archivo
    !gzip -v -9 modelos/TP2/ensambleCrossvalidation.joblib
    #Separamos en varios archivos para que github no nos haga drama. #GithubDestruidoConElPoderDeUnix
    !split -b 50m modelos/TP2/ensambleCrossvalidation.joblib.gz modelos/TP2/ensambleCrossvalidation.joblib.gz.part-
    !rm modelos/TP2/ensambleCrossvalidation.joblib.gz

else:
    #Reconstruimos el zip
    !cat modelos/TP2/ensambleCrossvalidation.joblib.gz.part-a* > modelos/TP2/ensambleCrossvalidation.joblib.gz
    #Descomprimimos
    !gzip -d -k modelos/TP2/ensambleCrossvalidation.joblib.gz
    resultados_rf = load('modelos/TP2/ensambleCrossvalidation.joblib')
    !rm modelos/TP2/ensambleCrossvalidation.joblib
    !rm modelos/TP2/ensambleCrossvalidation.joblib.gz


metricas_cv_rf = resultados_rf['test_score']
metricas_cv_rf
metric_labels_CV_rf = ['F1 Score']*len(metricas_cv_rf) 
sns.set_context('talk')
sns.set_style("darkgrid")
plt.figure(figsize=(8,8))
sns.boxplot(metricas_cv_rf)
plt.title("Modelo entrenado con 5 folds")

Hacemos prediccion sobre el set de testeo

if not exists('submissions/TP2/ensamble.csv'):
    pred = ensamble.predict(review_pruebas.review_es)
    df_submission = pd.DataFrame({'id': review_pruebasOriginal['ID'], 'sentimiento': pred})
    df_submission['sentimiento'] = df_submission['sentimiento'].map({1: 'positivo', 0: 'negativo'})
    df_submission.to_csv('submissions/TP2/ensamble.csv', index=False)

Nota sobre el ensamble hibrido:

El modelo de mejor performance en kaggle que elevo la posicion del equipo es un modelo incompleto puesto que esta armado unicamente con dos modelos, pero, el modelo expuesto en esta porción del analisis incluye los dos modelos usados en el armado del ensamble que genero la mejor predicción

De manera conclusiva es importante destacar que los ensambles son los modelos que mejor encaran la predicción en el analisis de sentimientos de las reviews de peliculas