Cultivando datos de la web

En esta entrada, cultivaremos datos de la web con la biblioteca de Rrvest. Actualmente, innumerables aplicaciones dependen directamente de la tecnología de raspado o cultivo de información, la cual nos permite generar información actualizada por medio de la extracción directa de la misma a partir de la red. La librería rvest contiene funciones que facilitan este proceso. Por otro lado, utilizaremos la librería stringr para manipular cuerdas de texto por medio de patrones y expresiones regulares.

En este ejemplo se aprecia el proceso de cultivo de los datos de artistas amigos a partir de información del artista de interés. La información mencionada será raspada de la página web https://www.discogs.com automáticamente. Con lo que podemos escribir un script, de unas veinte líneas, que reciba un nombre de artista y regrese una lista de sus colaboradores.

Primero ingresemos a la página web https://www.discogs.com y hagamos una búsqueda de algún músico. En la siguiente imagen vemos que se realiza una búsqueda del artista Lassi Nikko. Observe la dirección de enlace en la barra de direcciones de su explorador después de realizar la búsqueda (encerrada en un elipse rojo en la siguiente imagen).

search_artiststructure
Estructura de enlace de búsqueda de artista

Aquí se presenta el enlace

En él podemos ver que la estructura parece ser: el nombre del artista separado por signos de más, es decir, «lassi+nikko». Patrón que se encuentra entre “https://www.discogs.com/search/?q=” y “&type=all”. Nos gustaría que el programa reciba el nombre de un artista y que de forma automática se realice la búsqueda de dicho artista, en caso de existir, que se ingrese a la página respectiva del artista, para raspar y regresar una lista con los colaboradores.

Para recibir el nombre de artista ingresado por el usuario utilizaremos la función de R base readLines

# INPUT
cat("Nombre Artista: "); # instrucción para usuario
nombre_artista <- readLines("stdin", n = 1); # almacenando entrada
cat("Usted ingreso") # Mensaje de confirmación
str(nombre_artista);
cat( "\n Generando lista de artista amigos \n" ) # Mensaje de espera

Utilizaremos la función de R base paste0 para construir la petición http con lainformación mencionada anteriormente y almacenarla en la variable «busqueda», no sin antes cambiar los posibles espacios que ingresó el usuario por signos de más «+», para después realizar la petición http con la función read_html, contenida en rvest

#PROCESS
library(stringr)
nombre_artista <- str_replace_all(nombre_artista, " ", "+") #cambiando espacio por +
busqueda <- paste0("https://www.discogs.com/search/?q=", nombre_artista, "&type=all") # construyendo petición de búsqueda (URL) con paste0
library(rvest)
html_page <- read_html(busqueda) # guardando respuesta a petición

Ahora vamos a extraer a partir del primer elemento de la lista de resultados, el enlace que nos lleva a la pagina del artista. Para lo cual nos serviremos de la tecnología css, utilizada ampliamente para dar estilo a las páginas html. Utilizaremos la ruta css de los elementos requeridos de forma que podamos extraer información de nodos html. Para conocer dicha ruta vamos a hacer uso del  «inspector de código» que viene en casi cualquier navegador web. Aquí se observa cómo abrirlo en el explorador de internet firefox

inspector
Inspector de código (firefox)

Haciendo flotar el cursor sobre los distintos elementos de la página web, el inspector resaltará el código respectivo al elemento en la parte inferior, como se muestra en la siguiente imagen. Al dar clic sobre algún elemento se resalta el bloque de código al que corresponde

css_id
«ruta CSS»

Se puede ver que la información que requerimos (subdirectorio de artista) se encuentra dentro del divisor seleccionado, con ruta «div.card.card…»

Para obtener la información de secciones css podemos utilizar la función html_nodes, la cual recibe como argumentos la pagina web y la ruta css de los elementos de los cuales queremos extraer el código.

enlace_artista <- html_nodes(html_page, "div.card.card_large.float_fix.shortcut_navigable")
enlace_artista

## {xml_nodeset (50)}
##  [1] <div class="card card_large float_fix\n        \n        shortcut_n ...
##  [2] <div class="card card_large float_fix\n        \n        shortcut_n ...
##  [3] <div class="card card_large float_fix\n        \n        shortcut_n ...
##  [4] <div class="card card_large float_fix\n        \n        shortcut_n ...
##  [5] <div class="card card_large float_fix\n        \n        shortcut_n ...
##  [6] <div class="card card_large float_fix\n        \n        shortcut_n ...
##  [7] <div class="card card_large float_fix\n        \n        shortcut_n ...
##  [8] <div class="card card_large float_fix\n        \n        shortcut_n ...
##  [9] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [10] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [11] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [12] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [13] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [14] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [15] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [16] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [17] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [18] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [19] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [20] <div class="card card_large float_fix\n        \n        shortcut_n ...
## ...

Observe que html_nodes nos entrega una lista con varios nodos que cumplieron con tener esa ruta. Estos nodos representan el código que hay en cada resultado de la búsqueda, queremos el que se encuentra en el primer elemento de la lista. Vamos a acceder a él y a convertirlo a texto con la función as.character de R base, observe que ahí dentro se encuentra el subdirectorio del artista, información que queremos obtener.

enlace_artista_char <- as.character(enlace_artista[1]) #accediendo a primer elemento de la lista de nodos y convirtiéndolo a texto
  enlace_artista_char # imprimir
## [1] "<div class=\"card card_large float_fix\n        \n        shortcut_navigable\" data-id=\"a182658\" data-object-id=\"182658\" data-object-type=\"artist\">\n                                                            \n    \n                <a href=\"/artist/182658-Lassi-Nikko\" class=\"thumbnail_link\n            thumbnail_size_large\n            thumbnail_orientation_landscape\n            thumbnail-lazyload\n        \">\n        <span class=\"thumbnail_border\"></span>\n        <span class=\"thumbnail_center\">\n                                <img data-src=\"https://img.discogs.com/oqpsbvuIwt87D9aqvSx7w6SqaU0=/300x300/smart/filters:strip_icc():format(jpeg):mode_rgb():quality(40)/discogs-images/A-182658-1433076899-8016.jpeg.jpg\" src=\"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7\" alt=\"Lassi Nikko\"></span>\n    </a>\n    \n    <h4><a href=\"/artist/182658-Lassi-Nikko\" class=\"search_result_title\" title=\"Lassi Nikko\" data-followable=\"true\">Lassi Nikko</a></h4>\n            \n    <div class=\"card_actions skittles\">\n                        <span class=\"skittle skittle_collection needs_delegated_tooltip\" data-title=\"0 in Collection\" aria-label=\"0 in Collection\" title=\"0 in Collection\" style=\"display: none;\"><i class=\"icon icon-collection\"></i><span class=\"count\">0</span>\n    </span>                <span class=\"skittle skittle_wantlist needs_delegated_tooltip\" data-title=\"0 in Wantlist\" aria-label=\"0 in Wantlist\" title=\"0 in Wantlist\" style=\"display: none;\"><i class=\"icon icon-wantlist\"></i><span class=\"count\">0</span>\n    </span>                            <a class=\"skittle skittle_inventory needs_delegated_tooltip\" data-title=\"0 in Inventory\" aria-label=\"0 in Inventory\" title=\"0 in Inventory\" style=\"display: none;\" href=\"/sell/release/2662813?seller=Anonymous\"><i class=\"icon icon-store\"></i><span class=\"count\">0</span>\n    </a>\n    </div>\n\n    \n    </div>"

Ahora, de toda esta maraña, extraemos únicamente el subdirectorio del artista. Para extraer exactamente este pedazo de texto utilizaremos la función str_extract de la librería stringr y una expresión regular para obtener uno o más caracteres alfanuméricos, guiones medios y diagonales que existan después del primer ‘href=\»‘. Una vez obtenido el subdirectorio lo pegamos al resto de la url utilizando paste0.

artist_link <- str_extract(enlace_artista_char, "(?<= href=\")[[:alnum:]-/]*") # expresión regular
artist_link <- paste0("https://www.discogs.com", artist_link) # construyendo petición
artist_link # imprimir
## [1] "https://www.discogs.com/artist/182658-Lassi-Nikko"

Resta entrar a esta página y raspar la información de colaboradores. Si entramos a la página del artista e inspeccionamos el bloque de código del nombre de los artistas en los discos, podemos ver que su ruta css es “span.artist_in_title a”

Obtenemos la lista de artistas en el título extrayendo estos nodos y extraemos el texto html de dichos nodos con html_text de rvest. Al final pasamos este texto por la función unique de R base para eliminar elementos repetidos

artist_html_page <- read_html(artis_link)
colaborators <- html_nodes(artist_html_page, "span.artist_in_title a")
# OUTPUT
(lista_colab <- unique(html_text(colaborators)))
##  [1] "MD"               "Seremoniamestari" "brothomStates"   
##  [4] "Brothomstates"    "Origami (9)"      "Blamstrain"      
##  [7] "Lackluster"       "Various"          "Luke Slater"     
## [10] "Surgeon"          "Sundial Aeon"     "Dune (31)"

Basta juntar los bloques de código para construir el script. Recuerda que debemos guardarlo con extensión .R (en la siguiente ejemplificación lo guardamos como ask_discogs.R) y para ejecutarlo basta escribir el siguiente comando dentro de la terminal, desde el directorio en donde hayamos guardado el script

Rscript "ask_discogs.R"

¡Ánimo!

El modelo logístico y COVID-19

La modelación de crecimiento de poblaciones es una de las tareas científicas más recurrentes (y en alguna forma también lo es dentro de la ciencia de datos). Cuando nos referimos a población nos estamos refiriendo, a personas pero también puede verse como dinero, en este caso personas enfermas, etcétera.

En esta entrada abordaremos la modelación del crecimiento de COVID-19, enfermedad respiratoria causada por el virus SARS-CoV-2. Para esto ocuparemos el modelo logístico, que es el más popular y común para modelar crecimientos poblacionales. Usaremos los datos del caso italiano pero puede ser aplicable para cualquier otro conjunto de datos con las respectivas consideraciones específicas de cada elección.

Puede que haya otros modelos mucho más refinados, sin embargo la intención de esta entrada es abordar el proceso de interpretación de un modelo así como su ajuste de parámetros. No ocuparemos el mexicano debido a que todavía nos encontramos en la fase exponencial. Los paquetes de Python que ocuparemos son los siguientes:

import pandas as pd
import numpy as np
from datetime import datetime,timedelta
from sklearn.metrics import mean_squared_error
from scipy.optimize import curve_fit
from scipy.optimize import fsolve
import matplotlib.pyplot as plt
import seaborn as sns
from pprint import pprint
%matplotlib inline

Obtención y preprocesamiento de datos.

Una de las partes más importantes de los procesos de modelación es la obtención de datos y la limpieza de los mismos. Pandas puede recuperar datos desde varias fuentes de datos, en este caso lo recuperaremos desde una url como se muestra a continuación.

url = "https://raw.githubusercontent.com/pcm-dpc/COVID-19/master/dati-andamento-nazionale/dpc-covid19-ita-andamento-nazionale.csv"
df = pd.read_csv(url)
df.iloc[:,[0,11]].tail()

Nos vamos a quedar con las columnas 0 (fecha)y 11 (casos_totales). La última línea nos muestra las últimas 5 líneas del dataset.

Selección_212

Ahora, si examinamos las fechas podemos ver que tienen un formato que no podemos usar así que usaremos las funciones python.datetime. Primero hacemos que nuestro dataframe sea la selección de columnas de nuestro interés (0 y 11). Después de eso pondremos las fechas en la variable date y con el formato definido "%Y-%m-%dT%H:%M:%S", que es el formato en el que vienen los dato, aplicamos la función fdt para generar toda una columna de fechas manipulables por python.

df = df.iloc[:umb,[0,11]]
formato = "%Y-%m-%dT%H:%M:%S"
date = df.data
fdt = lambda x : (datetime.strptime(x,formato) - datetime.strptime("2020-01-01T00:00:00", formato)).days
df['data'] = date.map( fdt )

Nuestro dataframe df ahora tiene dos columnas, una de fechas manipulables por Python y la otra de número de casos totales. Vamos a usar el paquete Seaborn para ver como se ven estos puntos en scatter plot.

sns.scatterplot(x=df['data'], y=df['totale_casi'])

La gráfica se ve así

Selección_213

Modelo Logístico

Hemos escuchado el término de «crecimiento exponencial» y hemos visto las gráficas que lo exhiben ya que han estado en redes sociales, medio de comunicación, vaya, en todos lados. Esto puede resultar angustiante porque efectivamente muestran el crecimiento que parece desmedido de este particular virus que tiene la característica de ser muy contagioso. Una de las características del crecimiento exponencial es que una proporción de la población, al infectarse, deja de ser susceptible y se convierte en infectada con la capacidad de transmitir el virus.

Sin embargo, este proceso no puede ser infinito, no puede crecer para siempre básicamente porque esa población susceptible va disminuyendo conforme pasa el tiempo y los que eran infectados se convierten en recuperados (o bien en decesos pero el modelo que ocuparemos hace la consideración de que la población es fija). En algún momento la capacidad del virus para infectar agentes susceptibles se detiene debido a que una buena parte de la población ya tiene memoria inmunológica que impide que el virus se propague con la misma facilidad que al inicio del proceso. La expresión que modela este comportamiento es la siguiente:

f_{a,b,c} (x) = \frac {c} {1 + \exp( -(x-b)/a)}

A esta función la llamamos modelo logístico y tiene tres parámetros. En donde 𝑥 representa la población cuyo crecimiento deseamos medir como crece, y los números 𝑎,𝑏,𝑐 son parámetros de nuestro modelo. 𝑎 mide la rapidez de la infección, 𝑏 es el día con la mayor cantidad de infecciones, 𝑐 es el número total de casos registrados al final de la infección. Decimos que c regula el final del proceso de infección ya que cuando el denominador se acerca a 𝑐 entonces 𝑓(𝑥) vale 1 que es el valor máximo que puede tomar.

Para encontrar los parámetros que mejor ajustan nuestros datos. Esto lo podemos hacer con la función scipy.optimize.curve_fit. Primero definiremos nuestra función y luego nuestras variables dependiente (x) y variable de respuesta (y).

def logistico(x, a,b,c):
    """
    Modelo logístico con argumento x (días) y con parámetros
    P=[a,b,c]
    Regresa la función f(x) = 1/(1+e(-(x-b)/a))
    """
    return c/(1 + np.exp(-(x-b)/a))

x = df.iloc[:,0]
y = df.iloc[:,1]

Para encontrar el mejor ajuste usaremos curve_fit pasándole x e y con un punto inicial para cada parámetro. Estos valores iniciales se pueden encontrar a través de otros métodos.

popt, pcov = curve_fit(logistico,x,y,p0=[2,100,20000])

En popt se guardan los valores encontrados mientras que en pcov se guarda la matriz de covarianza de los parámetros encontrados. Los parámetros encontrados son los siguientes

pprint(popt)
a, b, c = popt
array([5.22809110e+00, 8.13290036e+01, 1.20569392e+05])

La matriz de covarianza pcov nos muestra como variaron los parámetros entre sí, la diagonal es la varianza de cada parámetro. Vamos a tomar el la raíz cuadrada para calcular los errores estándar.

errores = [np.sqrt(pcov[i,i]) for i in [0,1,2]]
print(errores, sep='\t')
[0.07688318769831534, 0.22581878353538162, 2354.0038555475603]

Entonces la cantidad total de infectados se espera alrededor de c error_c

c_rango = list(map(np.round, [ popt[2] - errores[2] , popt[2] + errores[2]]))
c_rango
[118215.0, 122923.0]

El pico de la infección se espera el día b del año, es decir el día 81 del año, eso corresponde al 22 de Marzo.

pico = datetime.fromordinal(int(b))
print(b)
print(pico)
81.32900363914275
0001-03-22 00:00:00

Ahora nos gustaría saber cuándo terminará el proceso de infección y esto se puede determinar a partir cuando el acumulado de las personas infectadas iguale al parámetro 𝑐. Para esto requerimos usar la función fsolve para encontrar las raíces de 𝑓(𝑥) tomando como punto inicial el punto 𝑏

sol = int(fsolve(lambda x: logistico(x,a,b,c) - int(c),b))
sol
147

El día 147 corresponde al 27 de mayo

fin = datetime.fromordinal(sol)
fin
datetime.datetime(1, 5, 27, 0, 0)

Al momento de la publicación la cantidad de casos confirmados es de 147,577. Es importante notar que estos modelos son muy sensibles a las condiciones iniciales por lo que una variación pequeña puede resultar en cambios importantes en el resultado final.

Los datos graficados vs. la predicción del modelo lo podemos ver en la siguiente gráfica.

pred_x = list(range(max(x),sol))
plt.rcParams['figure.figsize'] = [7, 7]
preds = list(set(x).union(pred_x))
plt.rc('font', size=14)

plt.scatter(x,y,label="Real data",color="red")

plt.plot(preds, [logistico(i,popt[0],popt[1],popt[2]) for i in preds], label="Modelo logístico" )

Selección_214

En esta imagen la línea azul representa la predicción mientras que los puntos rojos representan los datos originales.

Conclusiones

Hay modelos más precisos para ajustar los datos de un proceso infeccioso. Particularmente hay modelos basados en ecuaciones diferenciales que pueden ser más precisos al tomar más consideraciones, de hecho parece que dadas las medidas de distanciamiento social la evolución de casos confirmados parece variar y se requieren modelos con parámetros distintos para diferentes etapas. Sin embargo en esta entrada lo que hicimos fue ajustar un modelo logístico, que es de los más comunes para modelar el crecimiento de una población, usando algunos parámetros que hay que determinar. En este particular caso la población es la cantidad de enfermos confirmados y los parámetros encontrados a través de resolución de ecuaciones no lineales.

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

Regresión logística

     La regresión logística es un algoritmo supervisado fundamental dentro de los algoritmos de clasificación en aprendizaje de máquina. ¿Qué es lo que hace? Es un algoritmo que toma un conjunto de datos, cada uno de ellos etiquetado como un ejemplo positivo o negativo, entonces lo que la regresión logística hace es encontrar el conjunto de parámetros de un hiperplano para clasificar ejemplos no vistos y asignarles la clasificación positiva o negativa.

logit(p_i) = \ln ( \frac {p} {1-p} ) = \beta_0 + \beta_1 x_1,i + \ldots + \beta_k x_k,i

     Donde \beta_j son los parámetros que deseamos encontrar. La función antes mostrada supone que la razón entre la probabilidad de pertenecer al conjunto entre la de no pertenecer se comporta, más o menos, como una linea recta (o un plano para dimensiones mayores a 2) entonces a partir de esta función podemos determinar p_i mediante el uso de la función exponencial (supondremos únicmaente dos parámetros en el siguiente ejemplo, es decir \beta_0 y \beta_1:

\frac{p}{1-p} = \exp (\beta_0 + \beta_1 x)

     Entonces despejando p obtenemos:

p = \frac {1} {1 + \exp(\beta_0 + \beta_1 x)}

     A esto se le conoce como la función logística y en este caso la x representa el valor de nuestra variable. Esta variable puede ser cualquier valor real como la temperatura, el área, o cualquier valor en el que esten medidos nuestros datos.

      Por ejemplo, en la página de Wikipedia usan el ejemplo de determinar si un estudiante pasará un examen si sólo conocemos la cantidad de horas que ha estudiado. Pero ese ejemplo es muy sencillo y se puede revisar en la misma página de Wikipedia.

      Un ejemplo mucho más interesante es considerar que nuestro conjunto de casos tiene muchos más atributos, por ejemplo determinar en el mercado de bienes raíces, si una casa se va a vender o no es un problema que depende de varias variables como pueden ser el área de la casa, el año en que se construyó, etcétera:

X = { 1, x_1, x_2, \ldots , x_n}

     Y dependiendo de estos valores queremos determinar a priori si una casa que no está dentro de X se va a vender o no encontrando los valores del espacio de parámetros

\beta = { \beta_0, \beta_1, \ldots, \beta_n }

Pero aproximar tantos parámetros (y por tantos nos referimos a 5 en adelante) puede que ya no sea tan sencillo o directo de determinar, ya que el espacio de parámetros puede tener una forma muy complicada.

      Para este tipo de problemas existen algoritmos que van «caminando» sobre el espacio de parámetros \beta y determinan la mejor combinación que encuentren a partir de esta caminata. Este algoritmo se llama Descenso del Gradiente.

      Un ejemplo de este algoritmo se puede encontrar en la  siguiente página:

http://github.com/sergiohzlz/reglogistica

Academia

academia

Conoce a profundidad la teoría que sustenta nuestros desarrollos y procesos.

Forma parte de nuestra comunidad en línea compartiendo con otros estos contenidos, ofreciéndonos tus puntos de vista o comentando aquí acerca de las experiencias con tus propios proyectos.