Detección de preguntas repetidas de Quora usando un pipeline

El problema de clasificación de texto, o lenguaje natural, es uno extremadamente común y complicado. Otra forma de referirnos a este tipo de problemas es de datos no estructurados, porque a pesar de que hay alguna gramática detrás de la generación de textos, estos pueden venir con fechas, emoticones, modismos y hasta faltas de ortografía, sin mencionar el estilo personal de cada autor para describir algún concepto. Todo esto dificulta mucho determinar  la carga emocional que una frase puede tener, misma que usualmente varía entre positiva, neutra o negativa (¡y ni que decir del significado!). Sin embargo, sabemos que existe alguna carga emocional en el texto que leemos, esto lo podemos detectar a través de ciertos atributos como puede ser el uso de ciertas palabras o símbolos de puntuación.

En esta entrada vamos a presentar una, de muchas posibles, forma de clasificar texto utilizando un pipeline compuesto por una red neuronal y la regresión logística que vimos en otra entrada. Los datos que vamos a clasificar son las preguntas del sitio Quora y que fueron puestos en un concurso por el sitio Kaggle.

La red neuronal es una Máquina Restringida de Boltzmann (RBM por sus siglas en inglés). Esta red neuronal se usa comúnmente para detectar atributos o también para hacer reducción de dimensionalidad haciendo uso de un método estocástico que detecta distribuciones de probabilidad del conjunto de entrada, su salida será la usaremos como entrada de la  regresión logística, algoritmo que vimos en una entrada anterior.

Una técnica muy estandarizada y conocida para analizar texto es la técnica de Bag of Words (otras técnicas pueden ser LSA/LSI, LDA, etcétera) representa oraciones en vectores (las bolsas de palabras), todos de la misma dimensión, donde cada entrada es la cantidad de veces que se uso la palabra i-ésima, tomadas estas de un conjunto ordenado de todas las palabras usadas en el documento. SciKit Learn (Sklearn) nos permite hacer este conteo de forma muy sencilla.

A continuación veremos los pasos necesarios para abrir el texto y clasificarlo usando los módulos de Sklearn para hacerlo.

Primero los imports, básico, los de cajón pues

import numpy as np
import random as ran
import time
import re     # mododulo para el manejo de expresiones regulares
import pickle # modulo para serializar objetos
from importlib import reload  #python3, no necesario en python2
from numpy import array, hstack, zeros

Ahora los que usaremos a lo largo del procesamiento

#Modulo para hacer TF-IDF
from sklearn.feature_extraction.text import TfidfVectorizer

#Módulo para separar el conjunto de entrenamiento
from sklearn.model_selection import train_test_split

#Búsqueda de parámetros
from sklearn.grid_search import GridSearchCV

#Modelos y pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.neural_network import BernoulliRBM
from sklearn.pipeline import Pipeline

#Reporte de la clasificación
from sklearn.metrics import classification_report

Una vez cargados los módulos, debemos preprocesar el conjunto de datos. En nuestro caso particular significa quitar acentos, mayúsculas y espacios dobles.

df = pd.read_csv('train.csv')
# Esta función lambda quita espacios dobles, convierte a minúsculas
# y se queda con todos los caracteres que sean solo letras.
pp = lambda y: ' '.join([ re.sub('[^a-zA-Z]+', '', x.lower()) for x in filter( lambda x: len(x)>0, str(y).split(' '))]).strip()
df['question2'] = df['question2'].map( pp )
df['question1'] = df['question1'].map( pp )

Ahora separamos nuestro conjunto de datos en conjunto de entrenamiento y prueba

X = df[['qid1', 'qid2','question1','question2']]
y = df[['is_duplicate']]
# Aquí estamos dividiendo nuestro conjunto de datos en 
# conjunto de entrenamiento (Xtr, ytr) 
# y de pruebas (Xts, yts)
Xtr, Xts, ytr, yts = train_test_split(X, y)

Lo que sigue es convertir nuestro conjunto de preguntas en vectores, para esto ocuparemos Tf-Idf que asigna un score a cada palabra dependiendo de qué tan frecuente es y en cuantos documentos aparece

vectorizer = TfidfVectorizer(stop_words='english',max_features=200)
Qp = list(Xtr['question1']) + list(Xtr['question2'])
Qpt = vectorizer.fit_transform(Qp)

Si quieres ver estos vectores debes examinar los vectores almacenados en Qpt con el método toarray() ya que están almacenados en una representación sparse; la cantidad de entradas es, precisamente 200.

Qpt[1].toarray()

Ahora conformaremos una sola matriz donde cada renglon será la representación de cada par de preguntas en el conjunto de entrenamiento. La cantidad de renglones debe ser igual al tamaño del conjunto de entrenamiento y el tamaño de cada vector debería ser 400.

vecs_t = zeros((len(Xtr),400))
for i in range(len(Xtr)):
    vecs_t[i] = hstack((Qpt[i].toarray()[0], Qpt[i+len(Xts)].toarray()[0]))
len(vecs_t), len(Xtr)
(303263, 303263)

Debemos hacer lo mismo con el conjunto de prueba.

Qp = list(Xts['question1']) + list(Xts['question2'])
Qpp = vectorizer.fit_transform(Qp)
vecs_p = zeros((len(Xts),400))
for i in range(len(Xts)):
    vecs_p[i] = hstack((Qpt[i].toarray()[0], Qpt[i+len(Xts)].toarray()[0]))
len(vecs_p), len(Xts)
(101088, 101088)

Despues de esto podemos borrar las variables no necesitadas ya que ahora el conjunto que usaremos para entrenar nuestro clasificador y para probarlo son vecs_t y vecs_p, ambos tienen asociado sus respectivas clasificaciones en ytr y yts.

del Qp, Qpp, Qpt, Xtr, Xts

Ahora ya podemos entrenar nuestro clasificador con estas matrices. Primero vamos a instanciar la red neuronal, la regresión logística y el pipeline conformado por la red neuronal y la logística, en ese orden.

rbm = BernoulliRBM()
logistic = LogisticRegression()
clasif = Pipeline( [("rbm",rbm),("logistic",logistic)] )

Todos estos algoritmos tienen hiperparámetros que requieren ser ajustados para entrenarse óptimamente, eso lo haremos con una heurística de búsqueda en una malla implementado por GridSearchCV. Para poder hacer esto necesitamos la lista de parámetros sobre los que queremos que se haga la búsqueda en un diccionario.

params = { "rbm__learning_rate": [0.1, 0.01, 0.001],
           "rbm__n_iter" : [20, 40, 80],
           "rbm__n_components" : [50, 100, 200],
           "logistic__C" : [1.0, 10.0, 100.0] }

Una vez definido esto podemos hacer nuestra búsqueda.

inicio = time.time() 
gs = GridSearchCV(clasif, params, verbose=1)
gs.fit(vecs_t, ytr) 
fin = time.time()
print("Terminado en {0}".format( fin - inicio ))
print("Mejor: {0}".format( gs.best_score_ ))

Usamos esos parámetros para correr el clasificador y entrenamos. En el siguiente ejemplo usaremos los que encontramos para este ejercicio y son los siguientes:

logistic__C:100.0
rbm__learning_rate:0.1
rbm__n_components:200
rbm__n_iter:20

Entonces los clasificadores quedan de la siguiente manera.

rbm = BernoulliRBM( n_components=200,\
                    learning_rate=0.1,\
                    n_iter=20,\
                    verbose=True)
logistic = LogisticRegression(C = 100.0)
clasificador = Pipeline([ ("rbm",rbm), ("logistic",logistic) ])
inicio = time.time()
clasificador.fit(trainingTriplets, trainingLabels)
fin = time.time()
print(u"Reporte de Clasificacion usando RBM + Reg. Logistica\n")
print(u"Duracion del entrenamiento {0}\n".format( fin-inicio ) )
print(classification_report(vecs_p, clasificador.predict(yts)) )

La salida de estas últimas tres línea es la siguiente:

Reporte de Clasificacion usando RBM + Reg. Logistica
Duracion del entrenamiento 290.9511294364929
             precision recall f1-score support

          0       0.69   0.90     0.78   12464
          1       0.67   0.34     0.45    7535

avg / total       0.68   0.69     0.66   19999

Este resultado tal vez no sea el óptimo para el estándar de los concursos de Kaggle, sin embargo la intención de esta entrada es presentar una metodología para manejar grandes conjuntos de datos (como los que usualmente se presentan en ese sitio) y también un conjunto de herramientas para analizar datos no estructurados.

¡Hasta la próxima!

 

Red neuronal con sklearn

En esta entrada vamos a implementar una red neuronal con la biblioteca de Python SciKit Learn. Las redes neuronales se han convertido en un clasificador estadístico extremadamente popular debido a su versatilidad y robustez para predecir datos ruidosos, esto lo haremos con la popular biblioteca para aprendizaje de máquina, SciKit Learn.

En la década de 1940 por McCulloch y Pitts desarrollaron un perceptrón para poder clasificar patrones, desde entonces y pasando por un largo impasse en los 60s y 70s, las redes neuronales han superado una gran cantidad de retos técnicos, convirtiéndose en uno de los clasificadores preferidos por empresas como Facebook, Google y muchísimas otras quienes ocupan lo que comúnmente denominamos DeepLearning.

En este ejemplo usaremos una red neuronal multicapa para clasificar las diferentes variedades descritas en el conjunto de datos de la flor Iris, que también viene con sklearn.

Primero cargamos nuestros datos.

#!-*-coding:utf8-*-
import sys
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris
from sklearn.neural_network import MLPClassifier
from sklearn.grid_search import GridSearchCV
from sklearn.metrics import classification_report
from numpy import array

iris = load_iris()
iris_d = iris['data']
targets = []
for v in iris['target']:
 z = [0,0,0]
 z[v] = 1
 targets.append(z)
iris_t = array(targets)
Xtr, Xts, Ytr, Yts = train_test_split( iris_d, iris_t )

La mayor complicación técnica con una red neuronal es ajustar los pesos entre neuronas, para hacer esto usualmente se ocupa descenso del gradiente (aunque se pueden utilizar otros métodos de optimización como los algoritmos genéticos). Para buscar los mejores parámetros para este ejemplo tenemos que hacer lo siguiente:

if(sys.argv[1]=='busca'):
## Ajuste de parametros
 params = {"alpha" : [0.1, 0.01, 0.001], "max_iter" : [50, 100, 200],\
 "batch_size" : [5, 10, 20], "activation" : ['relu','tanh'],\
 "hidden_layer_sizes": [10, 20, 50, 100]}
 clf = MLPClassifier()
 gs = GridSearchCV( clf, params, n_jobs=2, verbose=1, scoring='precision_macro' )
 gs.fit(Xtr,Ytr)
 print(gs.best_params_)

Finalmente hacemos la clasificación

elif(sys.argv[1]=='entrena'):
# El resultado de esto son los siguientes parámetros
 params = {'alpha': 0.01, 'activation': 'relu', 'max_iter': 200, 'batch_size': 10,\
 'hidden_layer_sizes': 50}
 clf = MLPClassifier()
 clf.set_params(**params)
 clf.fit(Xtr, Ytr)
 for v, p in zip(Xts, Yts):
 print "{0} vs {1}".format( clf.predict_proba(v.reshape(1,-1)), p )
 print("Reporte")
 print(classification_report(Yts, clf.predict(Xts)))

Para evaluar la eficiencia del clasificador podemos usar la tabla de confusión, con sklearn la salida de esta clasificación es la siguiente:

[[ 0.00234185 0.89763272 0.03804086]] vs [0 1 0]
[[ 9.99308220e-01 8.41076111e-03 4.58573504e-07]] vs [1 0 0]
[[ 3.72051018e-04 8.41891613e-01 5.55910875e-02]] vs [0 1 0]
[[ 4.81020589e-05 2.85644639e-01 7.05813423e-01]] vs [0 0 1]
Reporte  
           precision recall f1-score support

        0       1.00   1.00     1.00      13
        1       1.00   1.00     1.00      13
        2       1.00   1.00     1.00      12

avg / total     1.00   1.00     1.00      38