a) Construya un dataframe con los datos a analizar. Determine cuántas clases existen, cuántos registros por clase y describa el dataset.
In [1]:
import pandas as pd
tweets = pd.read_csv('./data/text_emotion.csv')
tweets.head()
Out[1]:
In [2]:
clases = pd.DataFrame(tweets['sentiment'].value_counts().reset_index())
clases.columns = ["sentiment", "count"]
clases
Out[2]:
El dataset cuenta con cuatro columnas, las que corresponden a el id del tweet, el autor y el contenido de este, junto con el sentimiento asociado al contenido. Los sentimientos encontrados son 13, contando "neutral" como uno de ellos.
b) Construya un conjunto de entrenamiento y otro de pruebas, a través de una máscara aleatoria, para verificar los resultados de los algoritmos.
La máscara se aplicará después de pre-procesar el dataset y antes de entrenar el clasificador
c) Implemente y explique un pre-procesamiento para los tweets para dejarlos en un formato estándarizado en el cual se podrán trabajar.
In [3]:
from nltk.stem import PorterStemmer
from nltk.tokenize import sent_tokenize, TweetTokenizer
from nltk.corpus import stopwords
import nltk
import re
nltk.download('punkt')
nltk.download('stopwords')
Out[3]:
In [4]:
df = tweets.copy()
ps = PorterStemmer()
sw = stopwords.words('english')
tknzr = TweetTokenizer(strip_handles=True, reduce_len=True)
df['norm_content'] = df['content'].apply(lambda x: tknzr.tokenize(x.lower()))
df['norm_content'] = df['norm_content'].apply(lambda x: [w for w in x if (w not in sw and len(w)>2)])
df['norm_content'] = df['norm_content'].apply(lambda x: [ps.stem(w) for w in x])
df.head()
Out[4]:
Ahora se hace la separación de test - train
In [5]:
import numpy as np
np.random.seed(14)
msk = np.random.rand(len(df)) < 0.8
# manual preprocess
df_train = df[msk].copy()
df_test = df[~msk].copy()
# default countvector preprocess
df_train_df = df[msk].copy()
In [6]:
import matplotlib.pyplot as plt
length = df_train['norm_content'].map(len)
plt.figure(figsize=(20,10))
plt.hist(length, bins=(max(length)-min(length)+1), align='right')
plt.title("Tokens count distribution")
plt.xlabel("Lenght")
plt.ylabel("Frequency")
plt.xticks(np.arange(min(length), max(length)+1, 1))
plt.show()
In [7]:
length = df_train['norm_content'].map(len)
df_train = df_train[np.abs(length-length.mean())<=(3*length.std())]
length = df_train['norm_content'].map(len)
plt.figure(figsize=(20,10))
plt.hist(length, bins=(max(length)-min(length)+1), align='right')
plt.title("Tokens count distribution")
plt.xlabel("Lenght")
plt.ylabel("Frequency")
plt.xticks(np.arange(min(length), max(length)+1, 1))
plt.show()
d) Haga una reducción binaria al problema, para trabajarlo como un problema de clasificación de dos clases. Para esto, agrupe las distintas emociones como positivas y negativas (defina un criterio), se recomienda codificar las clases como +1 y −1 respectivamente. Recuerde tener presente que el desbalanceo de los datos puede afectar considerablemente al modelo.
In [8]:
pos = ['neutral', 'happiness', 'love', 'surprise', 'fun', 'relief', 'enthusiasm']
df_train['clase'] = df_train['sentiment'].apply(lambda x: 1 if x in pos else -1)
df_train_df['clase'] = df_train_df['sentiment'].apply(lambda x: 1 if x in pos else -1)
df_test['clase'] = df_test['sentiment'].apply(lambda x: 1 if x in pos else -1)
print(df_train_df['clase'].value_counts()+df_test['clase'].value_counts())
df.head()
Out[8]:
e) Construir un clasificador que determine automáticamente la polaridad de un trozo de texto.
In [9]:
from sklearn.feature_extraction.text import CountVectorizer
def echo(args):
return args
# Pre-proceso propio
vectorizer = CountVectorizer(tokenizer=echo, preprocessor=echo, lowercase=False)
X_train = vectorizer.fit_transform(df_train['norm_content'])
X_test = vectorizer.transform(df_test['norm_content'])
# Pre-proseso por default de CountVectorizer
vectorizer_df = CountVectorizer()
X_train_df = vectorizer_df.fit_transform(df_train_df['content'])
X_test_df = vectorizer_df.transform(df_test['content'])
f) Entrene y compare al menos 5 de los diferentes clasificadores vistos en clases para clasificación binaria (por ejemplo: Navie Bayes, Multinomial Naive Bayes, LDA, QDA, Regresión logística, SVM y Arboles de decisión) sobre el conjunto de entrenamiento verificando su desempeño sobre ambos conjuntos (entrenamiento y de pruebas), construyendo un gráfico resumen del error de éstos.
In [10]:
def train(name, model, X_train, y_train, X_test, y_test):
model.fit(X_train, y_train)
score_train = model.score(X_train, y_train)
score_test = model.score(X_test, y_test)
print(name+" scores: \t train: %.3f \t test: %.3f"%(score_train,score_test))
return (name, model, score_train, score_test)
In [11]:
from sklearn.naive_bayes import BernoulliNB
naive_bayes = train("Naive Bayes",BernoulliNB(), X_train, df_train['clase'], X_test, df_test['clase'])
naive_bayes_df = train("Naive Bayes df",BernoulliNB(), X_train_df, df_train_df['clase'], X_test_df, df_test['clase'])
In [12]:
from sklearn.naive_bayes import MultinomialNB
multi_naive_bayes = train("Multi Naive Bayes",MultinomialNB(), X_train, df_train['clase'], X_test, df_test['clase'])
multi_naive_bayes_df = train("Multi Naive Bayes df", MultinomialNB(), X_train_df, df_train_df['clase'], X_test_df, df_test['clase'])
In [13]:
from sklearn.linear_model import LogisticRegression
logistic = train("LogisticRegression",LogisticRegression(), X_train, df_train['clase'], X_test, df_test['clase'])
logistic_df = train("LogisticRegression df",LogisticRegression(), X_train_df, df_train_df['clase'], X_test_df, df_test['clase'])
In [14]:
from sklearn.tree import DecisionTreeClassifier
decision_tree = train("DecisionTree",DecisionTreeClassifier(), X_train, df_train['clase'], X_test, df_test['clase'])
decision_tree_df = train("DecisionTree df",DecisionTreeClassifier(), X_train_df, df_train_df['clase'], X_test_df, df_test['clase'])
In [15]:
from sklearn.linear_model import SGDClassifier
gradient_descent = train("SGD",SGDClassifier(max_iter=5,tol=None), X_train, df_train['clase'], X_test, df_test['clase'])
gradient_descent_df = train("SGD df",SGDClassifier(max_iter=5,tol=None), X_train_df, df_train_df['clase'], X_test_df, df_test['clase'])
In [16]:
import matplotlib.pyplot as plt
def plot_scores(title,models):
N = len(models)
ind = np.arange(N) # the x locations for the groups
width = 0.45 # the width of the bars
fig, ax = plt.subplots()
test_scores = list(m[2] for m in models)
train_scores = list(m[3] for m in models)
rects1 = ax.bar(ind, test_scores, width, color='#00b1ff')
rects1b = ax.bar(ind+width, train_scores, width, color='#0070ff')
# add some text for labels, title and axes ticks
ax.set_ylabel('Score')
ax.set_title(title)
ax.set_xticks(ind + width/2)
ax.set_xticklabels((m[0] for m in models),fontsize='xx-small')
ax.legend((rects1[0], rects1b[0]), ('Train', 'Test'), loc='lower right')
def autolabel(rects):
for rect in rects:
height = rect.get_height()
ax.text(rect.get_x() + rect.get_width()/2., height,
'%.1f%%' % (100*height),
ha='center', va='bottom', fontsize='x-small')
autolabel(rects1)
autolabel(rects1b)
fig.set_dpi(170)
plt.show()
In [17]:
plot_scores("Pre-procesamiento manual",[naive_bayes, multi_naive_bayes, logistic, decision_tree, gradient_descent])
plot_scores("Pre-procesamiento automático",[naive_bayes_df, multi_naive_bayes_df, logistic_df, decision_tree_df, gradient_descent_df])
Cabe notar que modelos como SVM, LDA y QDA no fueron posibles de estudiar debido a que las operaciones entre matrices que estos necesitan son muy complicadas, probocando que el cálculo fuera muy largo o simplemente matando el kernel de python.
g) Utilice y explique las métricas que calcula la función classification report de la librería sklearn. En base a las distintas métricas calculadas ¿Cuáles clasificadores son los que mejor se comportan?
In [18]:
from sklearn.metrics import classification_report
def score_report(model, X, y):
name = model[0]
acc_tr = model[2]
acc_test = model[3]
print("Scores for "+name+":")
print("\tTraining Accuracy: %f"%(acc_tr))
print("\tTest Accuracy: %f"%(acc_test))
print("\tDetailed Analysis Testing Results ...")
print(classification_report(y, model[1].predict(X), target_names=['+','-']))
print("")
In [19]:
score_report(naive_bayes, X_test, df_test['clase'])
score_report(multi_naive_bayes, X_test, df_test['clase'])
score_report(logistic, X_test, df_test['clase'])
score_report(decision_tree, X_test, df_test['clase'])
score_report(gradient_descent, X_test, df_test['clase'])
f1-score puede ser interpretado como el promedio poderado de precision y recall, siendo 0 y 1 los puntajes mínimos y máximos respectivamente.
precision está definido por $\frac{tp}{tp+fp}$, donde $tp$ y $fp$ son la cantidad de verdaderos y falsos positivos respectivamente, es decir, precision denota la habilidad del clasificador de no etiquetar una muestra que es negativa como positiva.
recall está definido por $\frac{tp}{tp+fn}$, donde $fn$ es el número de falsos negativos. recall denota la habilidad del clasificador de etiquetar todas las muestras positivas como positiva.
support corresponde al número de ocurrencias de cada clase.
h) [Opcional] Visualice las predicciones de algún modelo generativo (probabilístico) definido anteriormente, tomando un subconjunto aleatorio de tweets de pruebas y explorando las probabilidades que asigna el clasificador a cada clase.
In [20]:
import random
random.seed(14)
test_pred = logistic[1].predict_proba(X_test)
spl = random.sample(range(len(test_pred)), 15)
print("Negative \t Positive \t Tweet")
for i in spl:
print(test_pred[i],"\t",df_test.reset_index()['content'][i])
i) Ahora deberá extender el problema a las múltiples clases que tiene presente (las distintas emociones), es decir, su trabajo será el de predecir una de las distintas emociones de cada tweet. Para esto utilice el mismo pre-procesamiento realizado en el punto c) y las características generadas mediante las técnicas en el punto e). Recuerde que tendrá que codificar las distintas clases como valores numéricos enteros.
In [21]:
clases['value']=[0,-1,1,-2,2,3,4,5,-3,-4,6,-5,-6]
sent = list(clases['sentiment'])
sent_v = list(clases['value'])
df_train['sentiment_class'] = df_train['sentiment'].apply(lambda x: sent_v[sent.index(x)])
df_test['sentiment_class'] = df_test['sentiment'].apply(lambda x: sent_v[sent.index(x)])
df_train.head()
Out[21]:
j) Utilice los clasificadores que son extendidos por defecto a múltiples clases para detectar emociones en cada tweet, muestre sus desempeños a través del error de pruebas en un gráfico resumen.
In [23]:
dt = train("DecisionTree",DecisionTreeClassifier(), X_train, df_train['sentiment_class'], X_test, df_test['sentiment_class'])
mnb = train("MultinomialNB",MultinomialNB(), X_train, df_train['sentiment_class'], X_test, df_test['sentiment_class'])
#mlp = train("Multi-layer Perceptron",MLPClassifier(), X_train, df_train['sentiment_class'], X_test, df_test['sentiment_class'])
Gráfico en item m)
k) Utilice clasificadores binarios que pueden ser extendidos a través de otras técnicas, tal como One vs One y One vs All/Rest.
In [24]:
from sklearn.multiclass import OneVsRestClassifier
from sklearn.multiclass import OneVsOneClassifier
nb_ovo = train("Naive Bayes OVO",OneVsOneClassifier(BernoulliNB()), X_train, df_train['sentiment_class'], X_test, df_test['sentiment_class'])
nb_ovr = train("Naive Bayes OVR",OneVsRestClassifier(BernoulliNB()), X_train, df_train['sentiment_class'], X_test, df_test['sentiment_class'])
l) Para el caso de la Regresión Logística compare sus dos métodos para ser extendidos a múltiples clases. Uno a través de One vs Rest y otro definiendo que la variable a predecir se distribuye Multinomial.
In [25]:
lr_mtn = train("Log. Reg. Multi",LogisticRegression(), X_train, df_train['sentiment_class'], X_test, df_test['sentiment_class'])
lr_ovr = train("Log. Reg. OVR",OneVsRestClassifier(LogisticRegression()), X_train, df_train['sentiment_class'], X_test, df_test['sentiment_class'])
In [26]:
plot_scores("Modelos Multi-clase",[dt, mnb, nb_ovo, nb_ovr, lr_mtn, lr_ovr])
Aquí nuevamente la regresión logística vuelve a obtener un score por sobre las otras técnicas, aúnque la diferencia es decimal.
Como se puede notar a simple vista, las diferencias de desempeño que hay entre la clasificación binaria y la multiclase es muy grande, esto puede ser debido principalmente a que la cantidad de datos que se posee no es suficiente para la multiclase, ya que por ejemplo para los tweets clasificados como empty, enthusiasm, boredom, y anger se tienen entre 900 y 100 muestras, lo que hace que las predicciones sean muy poco "educadas", causando overfitting, resultando en los errores mostrados. Esto no ocurre en el caso binario, ya que cada clase tiene al menos 16.000 muestras, siendo así un dataset aceptable para el experimento.
In [ ]: