Machine learning con H2O y Python

Joaquín Amat Rodrigo
Enero, 2020 (actualizado Junio 2020)

Introducción

En este documento se pretende mostrar cómo crear modelos de machine learning combinando H2O y el lenguaje de programación Python.

Aunque son muchos los pasos que preceden al entrenamiento de un modelo (exploración de los datos, transformaciones, selección de predictores, etc.), para no añadir una capa extra de complejidad, se va a asumir que los datos se encuentran prácticamente preparados para que puedan ser aceptados por los algoritmos de aprendizaje. Se puede encontrar una descripción más detallada de las etapas que forman parte el ciclo de vida del modelado en el documento Machine Learning con R y caret.

¿Qué es H2O?

H2O es un producto creado por la compañía H2O.ai con el objetivo de combinar los principales algoritmos de machine learning y aprendizaje estadístico con el Big Data. Gracias a su forma de comprimir y almacenar los datos, H2O es capaz de trabajar con millones de registros en un único ordenador (emplea todos sus cores) o en un cluster de muchos ordenadores. Internamente, H2O está escrito en Java y sigue el paradigma Key/Value para almacenar los datos y Map/Reduce para sus algoritmos. Gracias a sus API, es posible acceder a todas sus funciones desde R, Python o Scala, así como por una interfaz web llamada Flow.

Aunque la principal ventaja de H2O frente a otras herramientas es su escalabilidad, sus algoritmos son igualmente útiles cuando se trabaja con un volumen de datos reducido.

Comunicación entre H2O y Python

El manejo de H2O puede hacerse íntegramente desde Python: iniciar el cluster, carga de datos, entrenamiento de modelos, predicción de nuevas observaciones, etc. Es importante tener en cuenta que, aunque los comandos se ejecuten desde Python, los datos se encuentran en el cluster de H2O no en memoria. Solo cuando los datos se cargan en memoria, se les pueden aplicar funciones propias de Python.

Las funciones h2o.H2OFrame() y as_data_frame(use_pandas=True) permiten transferir los datos de la sesión de Python al cluster H2O y viceversa. Hay que tener especial precaución cuando el movimiento de datos se hace desde H2O a Python, ya que esto implica cargar en RAM todos los datos y, si son muchos, pueden ocupar toda la memoria. Para evitar este tipo de problemas, conviene realizar todos los pasos posibles (filtrado, agregaciones, cálculo de nuevas columnas...) con las funciones de H2O antes de transferir los datos.

Instalación

In [1]:
#!pip install requests
#!pip install tabulate
#!pip install "colorama>=0.3.8"
#!pip install future

#!pip uninstall h2o
#!pip install -f http://h2o-release.s3.amazonaws.com/h2o/latest_stable_Py.html h2o

Datos

Para los ejemplos de este documento se emplea el set de datos Adult Data Set disponible en UCI Machine Learning Repository. Este set de datos contiene información sobre 48842 ciudadanos de diferentes países. Para cada ciudadano, se dispone de 15 variables:

  • age: edad.
  • workclass: tipo de empleo.
  • fnlwgt: importancia relativa de cada observación.
  • education: nivel de estudios.
  • education-num: años de estudios.
  • marital-status: estado civil.
  • occupation: sector de trabajo
  • relationship: estado familiar.
  • race: raza.
  • sex: género.
  • capital-gain: ganancia de capital.
  • capital-loss: perdidas de capital.
  • hours-per-week: horas de trabajo semanal.
  • native-countr: país de origen.
  • salario: si el salario supera o no 50000 dólares anuales.

El objetivo del problema es crear un modelo capaz de predecir correctamente si un ciudadano tiene un salario superior o inferior a 50000 dólares anuales. Se trata de un problema de clasificación binaria supervisado.

Los datos se han modificado para eliminar observaciones con valores ausentes y para recodificar los niveles de algunas variables. El archivo resultante (adult_custom_python.csv) puede descargarse directamente del repositorio Github.

In [2]:
import warnings
warnings.filterwarnings("ignore")
In [3]:
# EL SIGUIENTE CÓDIGO MUESTRA COMO SE HAN PREPROCESADO LOS DATOS ORIGINALES.
# NO ES NECESARIO EJECUTARLO SI SE HA DESCARGADO EL ARCHIVO adult_custom_python.csv.
# ------------------------------------------------------------------------------
import pandas as pd
import numpy as np

# Datos de train.
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data"
datos_train = pd.read_csv(
                  filepath_or_buffer = url,
                  sep = ",",
                  skipinitialspace = True,
                  header = None)
datos_train.columns = ["age", "workclass", "final_weight", "education",
                       "education_number", "marital_status", "occupation",
                       "relationship", "race", "sex", "capital_gain",
                       "capital_loss", "hours_per_week", "native_country",
                       "salario"]

# Datos de test.
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.test"
datos_test = pd.read_csv(
                  filepath_or_buffer = url,
                  sep = ",",
                  skipinitialspace = True,
                  skiprows = 1,
                  header = None)
datos_test.columns = ["age", "workclass", "final_weight", "education",
                      "education_number", "marital_status", "occupation",
                      "relationship", "race", "sex", "capital_gain",
                      "capital_loss", "hours_per_week", "native_country",
                      "salario"]
# Se combinan los dos conjuntos de datos.
datos = pd.concat([datos_train, datos_test], axis=0).reset_index(drop=True)

# Se eliminan registros con missing values, que en este set de datos están 
# codificados como ?.
datos = datos.replace(to_replace = '?', value=np.nan)
datos = datos.dropna().reset_index(drop=True)

# Recodificación de niveles.
dic_replace = {"Without-pay": "desempleado",
               "Self-emp-inc": "autonomo",
               "Self-emp-not-inc": "autonomo",
               "Federal-gov": "funcionario",
               "Local-gov": "funcionario",
               "State-gov": "funcionario"}

datos["workclass"] = datos["workclass"] \
                     .map(dic_replace) \
                     .fillna(datos["workclass"])

dic_replace = {"Married-AF-spouse": "casado",
               "Married-civ-spouse": "casado",
               "Married-spouse-absent": "casado",
               "Widowed": "separado",
               "Never-married": "soltero",
               "Separated":"separado",
               "Divorced": "separado"}

datos["marital_status"] = datos["marital_status"] \
                          .map(dic_replace) \
                          .fillna(datos["marital_status"])


norte_america = ["Canada", "Cuba", "Dominican-Republic", "El-Salvador",
                  "Guatemala", "Haiti", "Honduras", "Jamaica", "Mexico",
                  "Nicaragua", "Outlying-US(Guam-USVI-etc)", "Puerto-Rico",
                  "Trinadad&Tobago", "United-States"]
europa        = ["England", "France", "Germany", "Greece", "Holand-Netherlands",
                 "Hungary", "Ireland", "Italy", "Poland", "Portugal", "Scotland",
                 "Yugoslavia"]
asia          = ["Cambodia", "China", "Hong", "India", "Iran", "Japan", "Laos",
                 "Philippines", "Taiwan", "Thailand", "Vietnam"]
sud_america   = ["Columbia", "Ecuador", "Peru"]
otros         = ["South"]

norte_america = dict.fromkeys(norte_america, "Norte America")
europa        = dict.fromkeys(europa, "Europa")
asia          = dict.fromkeys(asia, "Asia")
sud_america   = dict.fromkeys(sud_america, "Sud America")
otros         = dict.fromkeys(otros, "Otros")

dic_replace = dict(norte_america, **europa, **asia, **sud_america, **otros)

datos["native_country"] = datos["native_country"] \
                          .map(dic_replace) \
                          .fillna(datos["native_country"])

primaria     = ["Preschool", "1st-4th", "5th-6th", "7th-8th", "9th", "10th","11th","12th"]
secuendaria  = ["HS-grad"]
bachillerato = ["Some-college", "Assoc-acdm", "Assoc-voc"]
universidad  = ["Bachelors"]
master       = ["Masters", "Prof-school"]
doctorado    = ["Doctorate"]

primaria     = dict.fromkeys(primaria, "primaria")
secuendaria  = dict.fromkeys(secuendaria, "primaria")
bachillerato = dict.fromkeys(bachillerato, "bachillerato")
universidad  = dict.fromkeys(universidad, "primaria")
master       = dict.fromkeys(master, "master")
doctorado    = dict.fromkeys(doctorado, "primaria")

dic_replace = dict(primaria, **secuendaria, **bachillerato, **universidad,
                   **master, **doctorado)

datos["education"] = datos["education"] \
                     .map(dic_replace) \
                     .fillna(datos["education"])

dic_replace = {"<=50K." : "<=50K", ">50K." : ">50K"}
datos["salario"] = datos["salario"] \
                   .map(dic_replace) \
                   .fillna(datos["salario"])
                           
# Se agrupan las observaciones cuya variable occupation == "Armed-Forces" con 
# "Other-service" ya que solo hay 14 observaciones (0.03% del total).
                           
dic_replace = {"Armed-Forces" : "Other-service"}
datos["occupation"] = datos["occupation"] \
                      .map(dic_replace) \
                      .fillna(datos["occupation"])

datos.to_csv("adult_custom_python.csv", header=True, index=False)

Librerías

Las librerías utilizadas en este documento son:

In [4]:
# Librerías
# ------------------------------------------------------------------------------
import h2o
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import os
from matplotlib import style
from tabulate import tabulate
import multiprocessing
In [5]:
# Configuración matplotlib
# ------------------------------------------------------------------------------
plt.rcParams['image.cmap'] = "bwr"
plt.rcParams['figure.dpi'] = "100"
plt.rcParams['savefig.bbox'] = "tight"
style.use('ggplot') or plt.style.use('ggplot')

Iniciación de H2O

Una vez que H2O ha sido instalado (pip install h2o), hay que iniciarlo, bien en todo el cluster o en un solo ordenador. Para este ejemplo, se emplea un único ordenador del que se utilizan todos sus cores en paralelo.

Tras iniciar el cluster (local), se muestran por pantalla sus características, entre las que están: el número de cores activados (4), la memoria total del cluster (3.56GB), el número de nodos (1 porque se está empleando un único ordenador) y el puerto con el que conectarse a la interfaz web de H2O (FLOW) (http://localhost:54321/flow/index.html).

Si se desea lanzar H2O en un cluster Hadoop ya establecido, solo es necesario especificar la dirección IP y puerto de acceso en h2o.init().

In [6]:
# Creación de un cluster local H2O
# ------------------------------------------------------------------------------
h2o.init(ip = "localhost",
         # -1 indica que se empleen todos los cores disponibles.
         nthreads = -1,
         # Máxima memoria disponible para el cluster.
         max_mem_size = "4g")
Checking whether there is an H2O instance running at http://localhost:54321 ..... not found.
Attempting to start a local H2O server...
  Java Version: openjdk version "1.8.0_265"; OpenJDK Runtime Environment (build 1.8.0_265-8u265-b01-0ubuntu2~18.04-b01); OpenJDK 64-Bit Server VM (build 25.265-b01, mixed mode)
  Starting server from /home/ubuntu/miniconda3/lib/python3.7/site-packages/h2o/backend/bin/h2o.jar
  Ice root: /tmp/tmpaubuyvwv
  JVM stdout: /tmp/tmpaubuyvwv/h2o_ubuntu_started_from_python.out
  JVM stderr: /tmp/tmpaubuyvwv/h2o_ubuntu_started_from_python.err
  Server is running at http://127.0.0.1:54321
Connecting to H2O server at http://127.0.0.1:54321 ... successful.
Warning: Your H2O cluster version is too old (4 months and 18 days)! Please download and install the latest version from http://h2o.ai/download/
H2O_cluster_uptime: 01 secs
H2O_cluster_timezone: Etc/UTC
H2O_data_parsing_timezone: UTC
H2O_cluster_version: 3.30.0.4
H2O_cluster_version_age: 4 months and 18 days !!!
H2O_cluster_name: H2O_from_python_ubuntu_grn9kp
H2O_cluster_total_nodes: 1
H2O_cluster_free_memory: 3.556 Gb
H2O_cluster_total_cores: 8
H2O_cluster_allowed_cores: 8
H2O_cluster_status: accepting new members, healthy
H2O_connection_url: http://127.0.0.1:54321
H2O_connection_proxy: {"http": null, "https": null}
H2O_internal_security: False
H2O_API_Extensions: Amazon S3, XGBoost, Algos, AutoML, Core V3, TargetEncoder, Core V4
Python_version: 3.7.4 final
In [7]:
# Se eliminan los datos del cluster por si ya había sido iniciado.
h2o.remove_all()

Carga de datos

La carga de datos puede hacerse directamente al cluster H2O, o bien cargándolos primero en memoria en la sesión de Python y después transfiriéndolos. La segunda opción no es aconsejable si el volumen de datos es muy grande.

In [8]:
# Carga de datos en el cluster H2O desde url.
url = "https://github.com/JoaquinAmatRodrigo/Estadistica-con-R/raw/master/datos/adult_custom_python.csv"

datos_h2o = h2o.import_file(
                path   = url,
                header = 1,
                sep    = ",",
                destination_frame = "datos_h2o"
            )
Parse progress: |█████████████████████████████████████████████████████████| 100%
In [9]:
# Carga de datos en Python y transferencia a H2O.
url = "https://github.com/JoaquinAmatRodrigo/Estadistica-con-R/raw/master/datos/adult_custom_python.csv"
datos_python = pd.read_csv(
                  filepath_or_buffer = url,
                  sep = ",",
                  skipinitialspace = True
                )
datos_h2o = h2o.H2OFrame(python_obj = datos_python, destination_frame = "datos_h2o")
Parse progress: |█████████████████████████████████████████████████████████| 100%
In [10]:
# Carga de datos en el cluster H2O desde local.
datos_h2o = h2o.import_file(
                path   = "./adult_custom_python.csv",
                header = 1,
                sep    = ",",
                destination_frame = "datos_h2o"
            )
Parse progress: |█████████████████████████████████████████████████████████| 100%

Exploración de los datos

Si bien el set de datos empleado en este ejemplo es suficientemente pequeño para cargarlo todo en memoria y emplear directamente las magníficas posibilidades de análisis exploratorio que ofrece Python, para representar mejor la forma de proceder con grandes volúmenes de datos, se emplean funciones propias de H2O.

In [11]:
# Dimensiones del set de datos
datos_h2o.shape
Out[11]:
(45222, 15)
In [12]:
# Nombre de las columnas
datos_h2o.col_names
Out[12]:
['age',
 'workclass',
 'final_weight',
 'education',
 'education_number',
 'marital_status',
 'occupation',
 'relationship',
 'race',
 'sex',
 'capital_gain',
 'capital_loss',
 'hours_per_week',
 'native_country',
 'salario']

El método .describe() es muy útil para obtener un análisis rápido que muestre el tipo de datos, la cantidad de valores ausentes, el valor mínimo, máximo, media y desviación típica. H2O emplea el nombre enum para los datos de tipo str y object.

In [13]:
datos_h2o.describe()
Rows:45222
Cols:15


age workclass final_weight education education_number marital_status occupation relationship race sex capital_gain capital_loss hours_per_week native_country salario
type int enum int enum int enum enum enum enum enum int int int enum enum
mins 17.0 13492.0 1.0 0.0 0.0 1.0
mean 38.54794126752464 189734.73431073368 10.118460041572684 1101.430343638052488.595418159303 40.938016894431925
maxs 90.0 1490400.0 16.0 99999.0 4356.0 99.0
sigma 13.217870219055516 105639.19513422118 2.5528811950758743 7506.430083745247 404.9560920589649412.007508230033451
zeros 0 0 0 41432 43082 0
missing0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 39.0 funcionario77516.0 primaria 13.0 soltero Adm-clerical Not-in-family White Male 2174.0 0.0 40.0 Norte America <=50K
1 50.0 autonomo 83311.0 primaria 13.0 casado Exec-managerial Husband White Male 0.0 0.0 13.0 Norte America <=50K
2 38.0 Private 215646.0 primaria 9.0 separado Handlers-cleanersNot-in-family White Male 0.0 0.0 40.0 Norte America <=50K
3 53.0 Private 234721.0 primaria 7.0 casado Handlers-cleanersHusband Black Male 0.0 0.0 40.0 Norte America <=50K
4 28.0 Private 338409.0 primaria 13.0 casado Prof-specialty Wife Black Female0.0 0.0 40.0 Norte America <=50K
5 37.0 Private 284582.0 master 14.0 casado Exec-managerial Wife White Female0.0 0.0 40.0 Norte America <=50K
6 49.0 Private 160187.0 primaria 5.0 casado Other-service Not-in-family Black Female0.0 0.0 16.0 Norte America <=50K
7 52.0 autonomo 209642.0 primaria 9.0 casado Exec-managerial Husband White Male 0.0 0.0 45.0 Norte America >50K
8 31.0 Private 45781.0 master 14.0 soltero Prof-specialty Not-in-family White Female14084.0 0.0 50.0 Norte America >50K
9 42.0 Private 159449.0 primaria 13.0 casado Exec-managerial Husband White Male 5178.0 0.0 40.0 Norte America >50K

Para conocer el índice o nombre de las columnas que son de un determinado tipo, por ejemplo, numérico, se emplea la función h2o.columns_by_type().

In [14]:
# Índices
datos_h2o.columns_by_type(coltype='numeric')
Out[14]:
[0.0, 2.0, 4.0, 10.0, 11.0, 12.0]

Con el método h2o.cor() se calcula la correlación entre dos o más columnas numéricas.

In [15]:
indices = list(map(int,datos_h2o.columns_by_type(coltype='numeric')))
datos_h2o[indices].cor(use = "complete.obs")
age0 final_weight0 education_number0 capital_gain0 capital_loss0 hours_per_week0
1 -0.075792 0.037623 0.0796832 0.0593506 0.101992
-0.075792 1 -0.041993 -0.00411048 -0.00434878 -0.0186787
0.037623 -0.041993 1 0.126907 0.0817113 0.146206
0.0796832 -0.00411048 0.126907 1 -0.0321023 0.0838804
0.0593506 -0.00434878 0.0817113 -0.0321023 1 0.0541949
0.101992 -0.0186787 0.146206 0.0838804 0.0541949 1
Out[15]:

Para contar el número de observaciones de cada clase en una variable categórica, como es en este caso la variable respuesta salario, se emplea el método .table().

In [16]:
# Se crea una tabla con el número de observaciones de cada tipo.
tabla = datos_h2o["salario"].table()
print(tabla)
salario Count
<=50K 34014
>50K 11208

In [17]:
# Una vez creada la tabla, se carga en el entorno de python para poder graficar.
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(6, 3.8))
tabla = tabla.as_data_frame(use_pandas=True, header=True)
tabla.plot.barh(x="salario", y = "Count", ax = ax, legend = None)
fig.suptitle('Número de observaciones por grupo', fontsize= 'medium');

Separación de training, validación y test

Evaluar la capacidad predictiva de un modelo consiste en comprobar cómo de próximas son sus predicciones a los verdaderos valores de la variable respuesta. Para poder cuantificar de forma correcta este error, se necesita disponer de un conjunto de observaciones, de las que se conozca la variable respuesta, pero que el modelo no haya "visto", es decir, que no hayan participado en su ajuste. Con esta finalidad, se separan los datos disponibles en un conjunto de entrenamiento, uno de validación y uno de test. El conjunto de entrenamiento se emplea para ajustar el modelo, el de validación para encontrar los mejores hiperparámetros (model tuning) y el de test para estimar el error que comete el modelo al exponerse a nuevos datos. El tamaño adecuado de las particiones depende en gran medida de la cantidad de datos disponibles y la seguridad que se necesite en la estimación del error, 70%-15%-15% suele dar buenos resultados.

Si el número de observaciones es limitado, crear 3 particiones puede generar grupos demasiado pequeños y variables. En estos casos, es preferible dividir los datos únicamente en un conjunto de entrenamiento y otro de test, y utilizar validación cruzada (cross validation) sobre el conjunto de entrenamiento durante la optimización de los hiperparámetros del modelo. Para mostrar ambas posibilidades, en el ejemplo de modelo GLM se empleará validación cruzada, mientras que en el resto de modelos (GBM, RF y Deep Learning), se emplea un solo conjunto de validación. Es importante tener en cuenta que, la validación cruzada, requiere ajustar el modelo repetidas veces, lo que supone un coste computacional muy alto. Cuando se trabaja con big data no suele ser factible aplicar validación cruzada, además, al disponer de tantos datos, un conjunto de validación único es suficiente para obtener buenas estimaciones del comportamiento del modelo. Para una descripción más detallada de los diferentes métodos de validación, consultar el Anexo4 del documento Machine learning con R y Caret.

El método .splitFrame() realiza particiones aleatorias, pero no permite hacerlas de forma estratificada, por lo que no asegura que la distribución de clases de variable respuesta sea igual en todas particiones. Esto puede ser problemático con datos muy desbalanceados (alguno de los grupos es muy minoritario). Para obtener un reparto estratificado puede emplearse el método stratified_split().

In [18]:
# Separación de las observaciones en conjunto de entrenamiento y test.
# En los ejemplos de GBM y deep learning se repetirá la separación, pero en 
# tres conjuntos en lugar de dos.
datos_train_h2o, datos_test_h2o = datos_h2o.split_frame(
                                       ratios=[0.8],
                                       destination_frames= ["datos_train_H2O",
                                                            "datos_test_H2O"],
                                       seed = 123
                                   )

Número de observaciones de cada clase por partición.

In [19]:
print(datos_train_h2o["salario"].table())
print(datos_test_h2o["salario"].table())
salario Count
<=50K 27229
>50K 9009

salario Count
<=50K 6785
>50K 2199

In [20]:
# En porcentaje
print(datos_train_h2o["salario"].table()["Count"]/datos_train_h2o.shape[0])
print(datos_test_h2o["salario"].table()["Count"]/datos_test_h2o.shape[0])
Count
0.751394
0.248606

Count
0.755232
0.244768

La proporción de los dos grupos de la variable respuesta es muy similar en ambas particiones.

Preprocesado de datos

H2O incorpora y automatiza muchas de las transformaciones necesarias para que los datos puedan ser ingeridos por los algoritmos de machine learning. Esto es distinto a la mayoría de librerías de machine learning de otros lenguajes como Python y R, donde las etapas de preprocesado suelen ser independientes del entrenamiento del modelo.

En concreto, H2O automatiza las siguientes transformaciones:

Variables categóricas en H2O

En el momento de ajustar un modelo (GBM, DRF, Deep Learning, K-Means, Aggregator, XGBoost), H2O identifica automáticamente que variables son categóricas y crea internamente las variables dummy correspondientes. Es altamente recomendable permitir que H2O realice este proceso en lugar de hacerlo externamente, ya que su implementación está muy optimizada. El comportamiento de la codificación de las variables categóricas puede controlarse con el argumento categorical_encoding, por defecto su valor es "AUTO".

Estandarización

Por defecto, H2O estandariza los predictores numéricos antes de ajustar los modelos para que todos tengan media cero y varianza uno. Este comportamiento se puede controlar con el argumento standardize. Es importante tener en cuenta que, para muchos modelos (lasso, ridge, Deep Learning...), es necesario realizar la estandarización de predictores.

Eliminación de variables con varianza cero

No se deben de incluir en un modelo predictores que contengan un único valor (varianza cero), ya que no aportan información. Los algoritmos de H2O excluyen directamente las columnas con valor constante. Este comportamiento se puede controlar mediante el argumento ignore_const_cols.

Balance de clases

En problemas de clasificación, puede ocurrir que los datos estén desbalanceados, es decir, que el número de observaciones pertenecientes a cada grupo sea muy dispar. Por ejemplo, que de 1000 observaciones, 800 pertenezcan a un grupo y solo 200 al otro. En estos casos, los modelos pueden tener dificultades para aprender a identificar las observaciones del grupo minoritario. Con el argumento balance_classes se puede indicar que antes de ajustar el modelo se equilibren las clases mediante undersampling u oversampling. Por defecto, este comportamiento está desactivado.



Todas las trasformaciones descritas anteriormente se aprenden a partir de los datos de entrenamiento en el momento del ajuste, y se aplican automáticamente cuando el modelo se emplea para predecir nuevas observaciones. De esta forma, se garantiza que no se viola la condición de que ninguna información procedente de las observaciones de test participe o influya en el ajuste del modelo.

H2O dispone de un listado de funciones para manipular y transformar los datos, sin embargo, con frecuencia no son suficientes para resolver todas las necesidades de preprocesado. En estos casos es necesario recurrir a Python, si el volumen de datos es limitado, o a Spark si la información no se puede cargar en memoria.

Nota: en mi experiencia propia, aunque las tecnologías distribuidas como Spark son muy útiles, antes de recurrir a ellas, recomiendo reflexionar sobre la manipulación que se tiene que realizar y analizar si es necesario que se haga sobre todos los datos a la vez. Por ejemplo, el proceso de imputación se debe aprender únicamente de los datos de entrenamiento y luego aplicarlo al resto de datos. Puede que todo el dataset no se pueda cargar en memoria, pero sí el conjunto de entrenamiento. Esto permitiría aprender la imputación y después aplicarla por separado a cada conjunto.

Modelos con H2O

Modelos disponibles

H2O incorpora los siguientes algoritmos de machine learning. Todos ellos están implementados de forma que puedan trabajar, en la medida de lo posible, de forma distribuida y/o en paralelo.

Modelos supervisados

  • Cox Proportional Hazards (CoxPH)

  • Deep Learning (Neural Networks)

  • Distributed Random Forest (DRF)

  • Generalized Linear Model (GLM)

  • Generalized Aditive Model (GAM)

  • Gradient Boosting Machine (GBM)

  • Naïve Bayes Classifier

  • Stacked Ensembles

  • XGBoost

Modelos no supervisados

  • Aggregator

  • Generalized Low Rank Models (GLRM)

  • K-Means Clustering

  • Isolation Forest

  • Principal Component Analysis (PCA)

Otros

  • Word2vec

En este documento se muestran ejemplos con Generalized Linear Model (GLM), Generalized Aditive Model (GAM), Gradient Boosting Machine (GBM) y Deep Learning (Neural Networks).

Parada temprana

Los algoritmos de H2O están pensados para poder ser utilizados de forma eficiente con grandes volúmenes de datos. Emplear miles o millones de datos suele implicar tiempos largos de entrenamiento. H2O incorpora criterios de parada para que no se continúe mejorando un modelo si ya se ha alcanzado una solución aceptable. Dependiendo del algoritmo, el criterio de parada es distinto. En los siguientes apartados se describen con detalle.

Optimización de hiperparámetros

Muchos modelos contienen parámetros que no pueden aprenderse a partir de los datos de entrenamiento y que, por lo tanto, deben de ser establecidos por el analista. A estos se les conoce como hiperparámetros. Los resultados de un modelo pueden depender en gran medida del valor que tomen sus hiperparámetros, sin embargo, no se puede conocer de antemano cuál es el adecuado. Aunque con la práctica, los especialistas en machine learning ganan intuición sobre qué valores pueden funcionar mejor en cada problema, no hay reglas fijas. La forma más común de encontrar los valores óptimos es probando diferentes posibilidades, lo que comúnmente se conoce como grid search.

H2O emplea la clase H2OGridSearch para realizar la búsqueda de los mejores hiperparámetros, sus argumentos principales son: el nombre del algoritmo, los parámetros del algoritmo, un diccionario con los valores de los hiperparámetros que se quieren comparar y un diccionario opcional con criterios más abanzados como por ejemplo el tipo de búsqueda ("Cartesian" o "RandomDiscrete"). Una vez que la búsqueda ha finalizado, el objeto creado contiene todos los modelos, para acceder a ellos es necesario extraerlos.

Generalized linear models (GLMs)

Introducción

El modelo de regresión lineal (Legendre, Gauss, Galton y Pearson) considera que, dado un conjunto de observaciones, la media $\mu$ de la variable respuesta $Y$ se relaciona de forma lineal con la o las variables regresoras $X$.

$$\mu = \beta_0 + \beta_1 x_{1} + \beta_2 x_{2} + ... + \beta_p x_{p}$$

o en notación matricial (incorporando $\beta_0$ en el vector $\mathbf{\beta}$:

$$\mu = \mathbf{X}^T \mathbf{\beta}$$

Ajustar el modelo consiste en estimar los valores de los coeficientes de regresión $\hat{\beta}$ y la varianza $\hat{\sigma}^2$ que maximizan la verosimilitud (likelihood) de los datos, es decir, los que dan lugar al modelo que con mayor probabilidad puede haber generado los datos observados. El método empleado con más frecuencia es el ajuste por mínimos cuadrados (OLS):

$$\hat{\beta} = \underset{\beta_0,\beta}{\arg\min} (Y - \mathbf{X}^T \mathbf{\beta})^2$$$$\hat{\mu} = \mathbf{X}^T \mathbf{\hat{\beta}}$$$$\hat{\sigma}^2 = \frac{\sum^n_{i=1} \hat{\epsilon}_i^2}{n-p} = \frac{\sum^n_{i=1} (y_i - \hat{\mu})^2}{n-p}$$

Para que esta aproximación sea válida, se necesita que el error se distribuya de forma normal y que la varianza sea constante. El requisito de que la variable respuesta se distribuya de forma normal hace que el modelo de regresión lineal no sea aplicable a muchos problemas reales. Los modelos GLM (John Nelder y Robert Wedderburn) permiten superar esta limitación haciendo una abstracción a un modelo más genérico, en el que la variable respuesta puede seguir cualquier distribución de la familia exponencial (Gaussian, Poisson, binomial…). Para conseguirlo, la media de la variable respuesta ya no está relacionada directamente con los predictores, sino que se hace a través de una función link $g(.)$. Por ejemplo, el modelo lineal generalizado es equivale al modelo de regresión lineal por mínimos cuadrados cuando la función link es la identidad, o a la regresión logística cuando es binomial (logit).

$$g(\mu) = \beta_0 + \beta_1 x_{1} + \beta_2 x_{2} + ... + \beta_p x_{p}$$$$g(\mu) = \mathbf{X}^T \mathbf{\beta}$$

Además, el modelo lineal generalizado puede incluir regularización durante su ajuste, por lo que también incluye los modelos ridge regression, lasso y elastic net.

Cuando se emplea una función link gausiana (con o sin regularización L2), la máxima verosimilitud equivale a minimizar la suma de errores cuadrados, lo que tiene una solución analítica. Para el resto de familias o cuando se emplea la regularización L1, no existe una solución analítica con la que encontrar la máxima verosimilitud, por lo que se requiere un método de optimización iterativo como el iteratively reweighted least squares (IRLSM), L-BFGS, método de Newton o descenso de gradiente. H2O selecciona automáticamente el método apropiado en dependiendo de la función link especificada por el usuario.

Regularización

H2O incorpora 3 tipos de regularización para los modelos GLM con el objetivo de evitar overfitting, reducir varianza y atenuar el efecto de la correlación entre predictores. Por lo general, aplicando regularización se consigue modelos con mayor poder predictivo (generalización).

  • El modelo lasso es un modelo lineal por mínimos cuadrados que incorpora una regularización que penaliza la suma del valor absolutos de los coeficientes de regresión $(||\beta||_1 = \sum_{k=1} ^p |\beta_k|)$. Ha esta penalización se le conoce como l1 y tiene el efecto de forzar a que los coeficientes de los predictores tiendan a cero. Dado que un predictor con coeficiente de regresión cero no influye en el modelo, lasso consigue seleccionar los predictores más influyentes. El grado de penalización está controlado por el hiperparámetro $\lambda$. Cuando $\lambda = 0$, el resultado es equivalente al de un modelo lineal por mínimos cuadrados ordinarios. A medida que $\lambda$ aumenta, mayor es la penalización y más predictores quedan excluidos.

  • El modelo ridge es un modelo lineal por mínimos cuadrados que incorpora una regularización que penaliza la suma de los coeficientes elevados al cuadrado $(||\beta||^2_2 = \sum_{k=1} ^p \beta^2_k)$. Ha esta penalización se le conoce como l2 y tiene el efecto de reducir de forma proporcional el valor de todos los coeficientes del modelo pero sin que estos lleguen a cero. Al igual que lasso, el grado de penalización está controlado por el hiperparámetro $\lambda$.

La principal diferencia práctica entre lasso y ridge es que el primero consigue que algunos coeficientes sean exactamente cero, por lo que realiza selección de predictores, mientras que el segundo no llega a excluir ninguno. Esto supone una ventaja notable de lasso en escenarios donde no todos los predictores son importantes para el modelo y se desea que los menos influyentes queden excluidos. Por otro lado, cuando existen predictores altamente correlacionados (linealmente), ridge reduce la influencia de todos ellos a la vez y de forma proporcional, mientras que lasso tiende a seleccionar uno de ellos, dándole todo el peso y excluyendo al resto. En presencia de correlaciones, esta selección varía mucho con pequeñas perturbaciones (cambios en los datos de entrenamiento), por lo que, las soluciones de lasso, son muy inestables si los predictores están altamente correlacionados.

Para conseguir un equilibrio óptimo entre estas dos propiedades, se puede emplear lo que se conoce como penalización elastic net, que combina ambas estrategias.

  • El modelo elastic net incluye una regularización que combina la penalización l1 y l2 $(\alpha \lambda ||\beta||_1 + \frac{1}{2}(1- \alpha)||\beta||^2_2)$. El grado en que influye cada una de las penalizaciones está controlado por el hiperparámetro $\alpha$. Su valor debe estar comprendido en el intervalo [0,1], cuando $\alpha = 0$, H2O aplica ridge regression y cuando $\alpha = 1$ aplica lasso. La combinación de ambas penalizaciones suele dar lugar a buenos resultados. Una estrategia frecuentemente utilizada es asignarle casi todo el peso a la penalización l1 ($\alpha$ muy próximo a 1) para conseguir seleccionar predictores y un poco a la l2 para dar cierta estabilidad en el caso de que algunos predictores estén correlacionados.

Encontrar el mejor modelo GLM implica identificar los valores óptimos de los hiperparámetros de regularización $\alpha$ y $\lambda$. Por defecto, H2O selecciona $\alpha = 0.5$ y, con cada uno de ellos, analiza un rango de valores $\lambda$. También es posible hacer una búsqueda del valor óptimo de $\alpha$ mediante un grid seach.

Lambda Search

La búsqueda del valor óptimo de lambda se habilita indicando lambda_search = True. Además, esta búsqueda puede ser configurada con los siguientes argumentos:

  • alpha: el valor de $\alpha$ que se emplea en el modelo para distribuir la penalización l1 y l2. Por defecto, alpha=0.5.

  • validation_frame: método de validación empleado para identificar el mejor modelo, puede ser mediante un único conjunto de validación o mediante validación cruzada (cross-validation). En el segundo caso, no es necesario crear un conjunto de validación, las particiones se generan a partir del conjunto de entrenamiento.

  • n_folds: número de particiones en la validación cruzada.

  • lambda_min_ratio y nlambdas: determinan la secuencia de valores lambda que se generan automáticamente y sobre los que se realiza la búsqueda. El rango de valores va desde el valor mínimo con el que se alcanza una penalización total (lambda_max), es decir, el valor de lambda a partir del cual el coeficiente de todos los predictores es cero (valor identificado automáticamente por H2O), hasta un valor de lambda igual a lambda_min_ratio x lambda_max. Dentro de ese rango, se generan un número de lambdas igual al valor especificado con nlambdas. Por defecto, se emplean nlambda=100 y lambda_min_ratio=1e−4.

  • max_active_predictors: establece el número máximo de predictores incluidos en el modelo (predictores con coeficientes de regresión distintos de cero). Esta opción es muy útil para evitar tiempos de computación excesivos cuando se trabaja con sets de datos que contienen cientos de predictores pero se sabe que solo unos pocos son importantes.

Método de ajuste

Todos los modelos GLM se ajustan encontrando el valor de los coeficientes $\beta$ que maximizan el likelihood. La estimación de estos coeficientes se realiza mediante métodos iterativos, ya que, a excepción de unos pocos casos, no existe una solución analítica. H2O incorpora dos métodos (solvers) distintos, cuya correcta elección, que depende del tipo de datos, puede influir notablemente en el rendimiento computacional.

  • Iteratively Reweighted Least Squares Method (IRLSM): funciona especialmente bien cuando el set de datos empleado tiene muchas observaciones (largo) pero un número limitado de predictores (estrecho). Los autores recomiendan utilizarlo cuando el número de predictores no es mayor de 500.

  • Limited-memory Broyden-Fletcher-Goldfarb-Shanno algorithm (L-BFGS): es el método recomendado cuando se dispone de muchos predictores (>500).

Las anteriores son solo recomendaciones generales, en caso de duda, la mejor forma de elegir es comparando el resultado de ambos solvers.

Criterio de parada

Los algoritmos de H2O están pensados para poder ser aplicados a grandes volúmenes de datos, por lo que, entre sus características, incorporan criterios de parada para que no se continúe mejorando un modelo si ya se ha alcanzado una solución aceptable. En el caso de los GLM, el criterio de parada es el número máximo de predictores permitidos, que se especifica mediante el argumento max_active_predictors.

Modelo

El problema que se describió en la introducción sobre predecir el salario es un problema de clasificación binaria. El principal modelo lineal empleado para clasificaciones binarias es la regresión logística, que es el resultado de emplear, en un modelo lineal generalizado, la función logit como link. Aunque la selección se puede hacer manualmente, H2O es capaz de identificar el tipo de variable respuesta (numérica o categórica) y en función de eso seleccionar automáticamente el tipo de modelo, regresión o clasificación.

In [21]:
# Se comprueba que la variable respuesta es de tipo factor.
datos_train_h2o["salario"] = datos_train_h2o["salario"].asfactor()
datos_test_h2o["salario"]  = datos_test_h2o["salario"].asfactor()
print(datos_train_h2o["salario"].isfactor())
print(datos_train_h2o["salario"].isfactor())
[True]
[True]
In [22]:
# Se define la variable respuesta y los predictores.
var_respuesta = "salario"
# Para este modelo se emplean todos los predictores disponibles.
predictores = datos_h2o.columns
predictores.remove("salario")
In [23]:
# Ajuste del modelo y validación mediente 5-CV para estimar su error
# ------------------------------------------------------------------------------
from h2o.estimators.glm import H2OGeneralizedLinearEstimator

modelo_glm = H2OGeneralizedLinearEstimator(
                     family = "binomial",
                     link   = "logit",
                     standardize       = True,
                     balance_classes   = False,
                     ignore_const_cols = True,
                     # Se especifica que hacer con observaciones incompletas
                     missing_values_handling = "Skip",
                     # Se hace una búsqueda del hiperparámetro lamba.
                     lambda_search = True,
                     # Selección automática del solver adecuado.
                     solver = "AUTO",
                     alpha  = 1,
                     # Validación cruzada de 5 folds para estimar el error
                     seed   = 123,
                     nfolds = 5,
                     # Reparto estratificado de las observaciones en la creación
                     # de las particiones.
                     fold_assignment = "Stratified",
                     keep_cross_validation_predictions = False,
                     model_id = "modelo_glm"
)

modelo_glm.train(
    y = var_respuesta,
    x = predictores,
    training_frame = datos_train_h2o
)
glm Model Build progress: |███████████████████████████████████████████████| 100%

Al imprimir el modelo por pantalla, se muestra toda la información disponible: tipo de modelo, coeficientes de regresión obtenidos, métricas, resultados de la validación cruzada... Esta suele ser más información de la que se necesita en un análisis convencional. Para poder acceder directamente a la información de interés, H2O incorpora una serie de métodos que extraen información concreta del modelo.

In [24]:
# print(modelo_glm)

Coeficientes de regresión

Si se especifica en la creación del modelo que se estandaricen los predictores (standardize=True), se generan dos tipos de coeficientes de regresión:

  • Coeficientes estandarizados: son los obtenidos directamente por el modelo al emplear los predictores estandarizados. Es importante tener en cuenta que, al estar estandarizados, su valor no está en las mismas unidades que los predictores originales, lo que dificulta su interpretación. Sin embargo, sí que son útiles (su valor absoluto) para ordenar los predictores de mayor a menor influencia en el modelo.

  • Coeficientes: son los coeficientes "normales" obtenidos a partir de los coeficientes estandarizados pero revirtiendo la estandarización. Sus unidades son las mismas que las de los predictores originales y pueden ser interpretados de forma normal.

Para obtener más información sobre la correcta interpretación de los coeficientes de regresión de un modelo de regresión logística consultar Regresión logística simple y múltiple en R.

In [25]:
# Coeficientes de regresión de cada uno de los predictores
# ------------------------------------------------------------------------------
coeficientes = modelo_glm._model_json['output']['coefficients_table'].as_data_frame()
coeficientes
Out[25]:
names coefficients standardized_coefficients
0 Intercept -8.198865e+00 -2.613324
1 occupation.Adm-clerical 4.247153e-02 0.042472
2 occupation.Craft-repair 0.000000e+00 0.000000
3 occupation.Exec-managerial 7.208765e-01 0.720877
4 occupation.Farming-fishing -1.184367e+00 -1.184367
5 occupation.Handlers-cleaners -7.062620e-01 -0.706262
6 occupation.Machine-op-inspct -3.233566e-01 -0.323357
7 occupation.Other-service -9.380158e-01 -0.938016
8 occupation.Priv-house-serv -7.690422e-01 -0.769042
9 occupation.Prof-specialty 5.106773e-01 0.510677
10 occupation.Protective-serv 2.643752e-01 0.264375
11 occupation.Sales 2.356768e-01 0.235677
12 occupation.Tech-support 4.622797e-01 0.462280
13 occupation.Transport-moving -1.500296e-01 -0.150030
14 relationship.Husband 9.439594e-01 0.943959
15 relationship.Not-in-family 0.000000e+00 0.000000
16 relationship.Other-relative -2.010507e-01 -0.201051
17 relationship.Own-child -7.929713e-01 -0.792971
18 relationship.Unmarried -2.505878e-01 -0.250588
19 relationship.Wife 2.017748e+00 2.017748
20 race.Amer-Indian-Eskimo -1.343826e-01 -0.134383
21 race.Asian-Pac-Islander 3.407680e-01 0.340768
22 race.Black 0.000000e+00 0.000000
23 race.Other -4.046197e-02 -0.040462
24 race.White 1.449839e-01 0.144984
25 native_country.Asia 0.000000e+00 0.000000
26 native_country.Europa 3.419820e-01 0.341982
27 native_country.Norte America 2.954999e-01 0.295500
28 native_country.Otros -1.021259e+00 -1.021259
29 native_country.Sud America -5.674959e-01 -0.567496
30 workclass.Private 2.249614e-02 0.022496
31 workclass.autonomo -2.536405e-01 -0.253640
32 workclass.desempleado 0.000000e+00 0.000000
33 workclass.funcionario 0.000000e+00 0.000000
34 education.bachillerato 0.000000e+00 0.000000
35 education.master 1.398477e-02 0.013985
36 education.primaria -2.719947e-02 -0.027199
37 marital_status.casado 8.135382e-01 0.813538
38 marital_status.separado 0.000000e+00 0.000000
39 marital_status.soltero -4.967991e-01 -0.496799
40 sex.Female -7.121053e-01 -0.712105
41 sex.Male 0.000000e+00 0.000000
42 age 2.586912e-02 0.342470
43 final_weight 6.853858e-07 0.072477
44 education_number 2.798588e-01 0.715425
45 capital_gain 3.126539e-04 2.369079
46 capital_loss 6.348509e-04 0.257005
47 hours_per_week 2.981347e-02 0.357393

Predictores incluidos en el modelo

Si el modelo GLM incorpora penalización L1 (lasso o elastic net), entonces, el propio ajuste del modelo realiza selección de predictores. Los predictores incluidos en el modelo son aquellos cuyo coeficiente de regresión es distinto de cero.

In [26]:
# Predictores seleccionados
# ------------------------------------------------------------------------------
coeficientes = modelo_glm._model_json['output']['coefficients_table'].as_data_frame()
coeficientes[coeficientes["coefficients"] != 0]
Out[26]:
names coefficients standardized_coefficients
0 Intercept -8.198865e+00 -2.613324
1 occupation.Adm-clerical 4.247153e-02 0.042472
3 occupation.Exec-managerial 7.208765e-01 0.720877
4 occupation.Farming-fishing -1.184367e+00 -1.184367
5 occupation.Handlers-cleaners -7.062620e-01 -0.706262
6 occupation.Machine-op-inspct -3.233566e-01 -0.323357
7 occupation.Other-service -9.380158e-01 -0.938016
8 occupation.Priv-house-serv -7.690422e-01 -0.769042
9 occupation.Prof-specialty 5.106773e-01 0.510677
10 occupation.Protective-serv 2.643752e-01 0.264375
11 occupation.Sales 2.356768e-01 0.235677
12 occupation.Tech-support 4.622797e-01 0.462280
13 occupation.Transport-moving -1.500296e-01 -0.150030
14 relationship.Husband 9.439594e-01 0.943959
16 relationship.Other-relative -2.010507e-01 -0.201051
17 relationship.Own-child -7.929713e-01 -0.792971
18 relationship.Unmarried -2.505878e-01 -0.250588
19 relationship.Wife 2.017748e+00 2.017748
20 race.Amer-Indian-Eskimo -1.343826e-01 -0.134383
21 race.Asian-Pac-Islander 3.407680e-01 0.340768
23 race.Other -4.046197e-02 -0.040462
24 race.White 1.449839e-01 0.144984
26 native_country.Europa 3.419820e-01 0.341982
27 native_country.Norte America 2.954999e-01 0.295500
28 native_country.Otros -1.021259e+00 -1.021259
29 native_country.Sud America -5.674959e-01 -0.567496
30 workclass.Private 2.249614e-02 0.022496
31 workclass.autonomo -2.536405e-01 -0.253640
35 education.master 1.398477e-02 0.013985
36 education.primaria -2.719947e-02 -0.027199
37 marital_status.casado 8.135382e-01 0.813538
39 marital_status.soltero -4.967991e-01 -0.496799
40 sex.Female -7.121053e-01 -0.712105
42 age 2.586912e-02 0.342470
43 final_weight 6.853858e-07 0.072477
44 education_number 2.798588e-01 0.715425
45 capital_gain 3.126539e-04 2.369079
46 capital_loss 6.348509e-04 0.257005
47 hours_per_week 2.981347e-02 0.357393
In [27]:
# {k: v for k, v in modelo_glm.coef().items() if v !=0}

Importancia de los predictores

En los modelos lineales generalizados, la importancia de los predictores puede estudiarse a partir del valor absoluto de los coeficientes de regresión estandarizados.

In [28]:
# Top 10 predictores más importantes
# ------------------------------------------------------------------------------
imporatancia_predictores = modelo_glm.varimp(use_pandas=True)
imporatancia_predictores.head(10)
Out[28]:
variable relative_importance scaled_importance percentage
0 capital_gain 2.369079 1.000000 0.119324
1 relationship.Wife 2.017748 0.851701 0.101628
2 occupation.Farming-fishing 1.184367 0.499927 0.059653
3 native_country.Otros 1.021259 0.431078 0.051438
4 relationship.Husband 0.943959 0.398450 0.047545
5 occupation.Other-service 0.938016 0.395941 0.047245
6 marital_status.casado 0.813538 0.343399 0.040976
7 relationship.Own-child 0.792971 0.334717 0.039940
8 occupation.Priv-house-serv 0.769042 0.324617 0.038734
9 occupation.Exec-managerial 0.720877 0.304286 0.036309
In [29]:
#modelo_glm.std_coef_plot(num_of_features=10)
#modelo_glm.varimp_plot(num_of_features=10)

fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(6, 3.8))
imporatancia_predictores.head(10).plot.barh(x='variable', y='scaled_importance', ax=ax)
ax.invert_yaxis()
fig.suptitle('Importancia de los predictores (Top 10)', fontsize='large');

Métricas de entrenamiento

Se pueden obtener toda una serie de métricas calculadas a partir de los datos de entrenamiento. Al tratarse de métricas de entrenamiento, no son válidas como estimación del error que comete el modelo al predecir nuevos valores (son demasiado optimistas) ni tampoco para comparar modelos, para esto hay que emplear métricas de validación o de test. Sin embargo si son útiles para ver si el modelo está aprendiendo.

In [30]:
# Métricas de entrenamiento
# ------------------------------------------------------------------------------
# Área bajo la curva
print("auc: ", modelo_glm.auc(train=True))
# Mean Squared Error
print("Mean Squared Error: ",modelo_glm.mse(train=True))
# R2
print("R2: ",modelo_glm.r2(train=True))
# LogLoss
print("LogLoss: ",modelo_glm.logloss(train=True))
# Coeficiente de Gini
print("Gini: ",modelo_glm.gini(train=True))
# Desviance del modelo nulo
print("Desviance del modelo nulo: ",modelo_glm.null_deviance(train=True))
# Desviance del modelo final
print("Desviance del modelo final: ",modelo_glm.residual_deviance(train=True))
# AIC
print("AIC: ",modelo_glm.aic(train=True))
auc:  0.9031912892686333
Mean Squared Error:  0.1054932552817481
R2:  0.43526480241511134
LogLoss:  0.3289055679151141
Gini:  0.8063825785372667
Desviance del modelo nulo:  40644.46630034207
Desviance del modelo final:  23837.75994021581
AIC:  23915.75994021581
In [31]:
modelo_glm.confusion_matrix(train = True)
Confusion Matrix (Act/Pred) for max f1 @ threshold = 0.315284294756554: 
<=50K >50K Error Rate
0 <=50K 22946.0 4283.0 0.1573 (4283.0/27229.0)
1 >50K 2004.0 7005.0 0.2224 (2004.0/9009.0)
2 Total 24950.0 11288.0 0.1735 (6287.0/36238.0)
Out[31]:

Se pueden obtener todas a la vez con el método .model_performance().

In [32]:
#performance_train = modelo_glm.model_performance(train=True)
#performance_train

Métricas de validación y validación cruzada

Al igual que para el entrenamiento, se pueden obtener las métricas obtenidas en el conjunto de validación (valid=True) o el promedio obtenido mediante validación cruzada (xval=True).

In [33]:
# Métricas de validación cruzada
# ------------------------------------------------------------------------------
print("auc: ", modelo_glm.auc(xval=True))
print("MSE: ",modelo_glm.mse(xval=True))
print("R2 : ",modelo_glm.r2(xval=True))
print("Gini: ",modelo_glm.gini(xval=True))
print("AIC: ",modelo_glm.aic(xval=True))
print("LogLoss: ",modelo_glm.logloss(xval=True))
auc:  0.9024732006112153
MSE:  0.10585096003164049
R2 :  0.43334990783660954
Gini:  0.8049464012224306
AIC:  23993.404161743172
LogLoss:  0.3300044726770679
In [34]:
# Métricas de validación cruzada para cada partición
# ------------------------------------------------------------------------------
modelo_glm.cross_validation_metrics_summary().as_data_frame()
Out[34]:
mean sd cv_1_valid cv_2_valid cv_3_valid cv_4_valid cv_5_valid
0 accuracy 0.83082914 0.0074754166 0.82930183 0.84086895 0.8220292 0.83560514 0.8263407
1 auc 0.9025468 0.0028152089 0.9057314 0.90516317 0.9017484 0.8991608 0.9009305
2 aucpr 0.76444674 0.008099501 0.757934 0.7748235 0.76975733 0.7644435 0.7552754
3 err 0.16917086 0.0074754166 0.17069818 0.15913102 0.17797086 0.16439489 0.17365935
4 err_count 1225.6 40.94875 1242.0 1172.0 1270.0 1194.0 1250.0
5 f0point5 0.6559277 0.01904723 0.6420901 0.6856272 0.64230597 0.66454375 0.64507145
6 f1 0.6908634 0.006589507 0.696184 0.697314 0.6902439 0.68074864 0.6898263
7 f2 0.7309139 0.026234256 0.7602308 0.7094062 0.7459146 0.6977636 0.74125427
8 lift_top_group 3.9796085 0.089733034 4.0844216 3.8625588 3.9754875 4.048495 3.9270794
9 logloss 0.32999742 0.0038173534 0.32381657 0.3293234 0.33159786 0.3313467 0.3339025
10 max_per_class_error 0.23889652 0.044767473 0.19009675 0.28229666 0.21169916 0.2904125 0.21997756
11 mcc 0.5817506 0.008880444 0.5919401 0.58993137 0.5768278 0.5711509 0.5789026
12 mean_per_class_accuracy 0.8075997 0.011242974 0.82269037 0.80040896 0.8108327 0.79326516 0.81080145
13 mean_per_class_error 0.19240029 0.011242974 0.17730966 0.19959107 0.1891673 0.20673487 0.18919852
14 mse 0.10584916 0.0014098084 0.10339785 0.10592665 0.10666626 0.10646025 0.1067948
15 null_deviance 8129.3784 139.07416 8047.1416 8372.253 8050.236 8120.4985 8056.7627
16 pr_auc 0.76444674 0.008099501 0.757934 0.7748235 0.76975733 0.7644435 0.7552754
17 precision 0.6349783 0.02977562 0.6104676 0.67805123 0.61388284 0.6541624 0.6183274
18 r2 0.43324572 0.006608459 0.43549898 0.44298807 0.43343502 0.4276137 0.42669275
19 recall 0.76110345 0.044767473 0.80990326 0.71770334 0.7883008 0.7095875 0.78002244
20 residual_deviance 4783.1357 58.421173 4712.1787 4850.934 4732.565 4813.1416 4806.8604
21 rmse 0.32533887 0.0021751947 0.32155538 0.32546374 0.32659802 0.32628247 0.32679474
22 specificity 0.85409594 0.023964196 0.8354775 0.8831145 0.83336455 0.87694275 0.8415805

Predicciones y error

Una vez que el modelo ha sido entrenado, puede emplearse para predecir nuevas observaciones con la el método .predict(), que recibe como argumentos un modelo y un nuevo set de datos.

In [35]:
predicciones = modelo_glm.predict(test_data = datos_test_h2o)
glm prediction progress: |████████████████████████████████████████████████| 100%
In [36]:
predicciones.head(5)
predict <=50K >50K
>50K 0.09750080.902499
>50K 0.08621890.913781
<=50K 0.949124 0.0508765
<=50K 0.709756 0.290244
<=50K 0.923976 0.076024
Out[36]:

El resultado devuelto por .predict() para un modelo GLM logístico es una tabla con 3 columnas, una con la clase predicha y otras dos con la probabilidad de pertenecer a cada una de las clases. El orden de las filas es el mismo que el de los datos pasados al argumento test_data.

Si el nuevo set de datos incluye la variable respuesta, se pueden calcular métricas que cuantifiquen el grado de acierto.

In [37]:
# Cálculo de las métricas para el conjunto de test
# ------------------------------------------------------------------------------
performance_test = modelo_glm.model_performance(test_data = datos_test_h2o)
In [38]:
# Métricas de test
# ------------------------------------------------------------------------------
print(f"auc: {performance_test.auc()}")
print(f"MSE: {performance_test.mse()}")
print(f"R2: {performance_test.r2()}")
print(f"Gini: {performance_test.gini()}")
print(f"AIC: {performance_test.aic()}")
print(f"LogLoss: {performance_test.logloss()}")
auc: 0.9052539122257957
MSE: 0.10335891103080022
R2: 0.44087059824545627
Gini: 0.8105078244515913
AIC: 5863.609924742784
LogLoss: 0.3219952095248656
In [39]:
# Gráfico ROC (Solo para clasificación binaria)
# ------------------------------------------------------------------------------
performance_test.plot(type='roc')

En el modelo anterior, se ha hecho una búsqueda del valor óptimo de lambda pero no de alpha (se ha empleado un valor fijo de alpha = 0.95). A continuación, se repite el ajuste del modelo, pero esta vez, comparando también distintos valores de alpha. Para estimar la capacidad predictiva de cada modelo se emplea validación cruzada con 10 particiones.

Nota: con el número de observaciones disponible, emplear validación simple sería suficiente y los resultados se obtendrían mucho más rápido.

En primer lugar se define un diccionario con los valores de los hiperparámetros que se desea explorar y que se pasa a través del argumento hyper_params.

In [40]:
# Búsqueda de hiperparámetros
# ------------------------------------------------------------------------------

from h2o.grid.grid_search import H2OGridSearch
# Valores de alpha que se van a comparar.
hiperparametros = {"alpha" : [0, 0.1, 0.5, 0.95, 1]}

Por defecto, H2OGridSearch hace una búsqueda cartesiana extricta analizando todas las posibles combinaciones de hiperparámetros definidas en el diccionario. En caso de querer otro tipo de búsqueda, se puede definir con un diccionario que se pasa al argumento search_criteria. Un ejemplo es:

criteria = {"strategy": "RandomDiscrete", "max_runtime_secs": 600,
            "max_models": 100, "stopping_metric": "AUTO",
            "stopping_tolerance": 0.00001, "stopping_rounds": 5,
            "seed": 123456}

El siguiente paso es crear el objeto H2OGridSearch en el que se especifica el tipo de modelo (con los parámetros fijdos), los hiperparámetros, la estrategia de búsqueda (por defecto cartesiana) y el id del grid creado.

In [41]:
glm_grid = H2OGridSearch(
                model=H2OGeneralizedLinearEstimator(
                        family            = "binomial",
                        link              = "logit",
                        standardize       = True,
                        balance_classes   = False,
                        ignore_const_cols = True,
                        # Se especifica que hacer con observaciones incompletas
                        missing_values_handling = "Skip",
                        # Se hace una búsqueda del hiperparámetro lamba.
                        lambda_search     = True,
                        # Selección automática del solver adecuado.
                        solver            = "AUTO",
                        # Validación cruzada de 10 folds para estimar el error
                        # del modelo.
                        seed              = 123,
                        nfolds            = 10,
                        # Reparto estratificado de las observaciones en la creación
                        # de las particiones.
                        fold_assignment   = "Stratified",
                        keep_cross_validation_predictions = False
                       ),
                hyper_params    = hiperparametros,
                search_criteria = {'strategy': "Cartesian"},
                grid_id         = 'glm_grid'
              )

Por último se realiza el entrenamiento.

In [42]:
glm_grid.train(
    y = var_respuesta,
    x = predictores,
    training_frame = datos_train_h2o,
)
glm Grid Build progress: |████████████████████████████████████████████████| 100%

Una vez entrenado el grid, con .get_grid() se muestran los resultados obtenidos con cada combinación de hiperpárametros ordenados por una métrica concreta. Para mostrar información detallada de todos los modelos probados se utiliza .summary().

In [43]:
# Resultados grid search
# ------------------------------------------------------------------------------
glm_grid.get_grid(sort_by="auc", decreasing=True).sorted_metric_table()
Out[43]:
alpha model_ids auc
0 [0.1] glm_grid_model_2 0.9024185138254697
1 [0.0] glm_grid_model_1 0.9024169851229237
2 [0.5] glm_grid_model_3 0.9023997576643652
3 [0.95] glm_grid_model_4 0.9023935633616488
4 [1.0] glm_grid_model_5 0.9023925646093188
In [44]:
#glm_grid.get_grid(sort_by='auc',decreasing=True)

De entre los valores comparados, el mayor accuracy promedio de validación cruzada se obtiene con alpha = 0.0 (modelo ridge), aunque para este problema, la diferencia es mínima.

Para conocer con más detalle la distribución de accuracy obtenido por cada modelo en cada partición de la validación cruzada, se tiene que extraer la información de cada uno de los modelos.

In [45]:
# Comparación errores validacción cruzada
# ------------------------------------------------------------------------------

# Identificador de los modelos creados por validación cruzada.
id_modelos = glm_grid.model_ids

# Se crea un diccionario donde se almacenarán los resultados.
auc_xvalidacion = {}

# Metrica que se quiere estudiar
metrica = "auc"

# Se recorre cada modelo almacenado en el grid y se extraen la métrica (auc)
# obtenida en cada partición.
for id in id_modelos:
    modelo = h2o.get_model(id)
    metricas_xvalidacion_modelo = modelo.cross_validation_metrics_summary()\
                                  .as_data_frame().set_index('')
    auc_xvalidacion[modelo.summary()["regularization"][0]] = \
        list(metricas_xvalidacion_modelo.loc[metrica][2:])
    
auc_xvalidacion

# Se convierte la lista en dataframe.
auc_xvalidacion = pd.DataFrame(auc_xvalidacion,dtype="float")

fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(6, 3.8))
auc_xvalidacion.boxplot(vert=False, ax = ax);
ax.tick_params(axis='both', labelsize=7)
fig.suptitle('Comparación errores validacción cruzada', fontsize= 'large');

En este caso, los resultados de los 5 modelos son prácticamente idénticos.

Una vez identificado el mejor modelo, se extrae del objeto grid y se almacena por separado.

In [46]:
# Mejor modelo
# ------------------------------------------------------------------------------
id_mejor_modelo = glm_grid.get_grid(sort_by="auc", decreasing=True)\
                  .sorted_metric_table()['model_ids'][0]
mejor_glm_final = h2o.get_model(id_mejor_modelo)

Detalles sobre todos los hiperparámetros del modelo final:

In [47]:
# Se emplea el formato YAML para facilitar la lectura de la info
import yaml

no_mostrar = ["model_id", "training_frame", "validation_frame"]
hyperparam = {key:val for key, val in mejor_glm_final.params.items() if key not in no_mostrar} 

print(yaml.dump(hyperparam))
HGLM:
  actual: false
  default: false
alpha:
  actual:
  - 0.1
  default: null
balance_classes:
  actual: false
  default: false
beta_constraints:
  actual: null
  default: null
beta_epsilon:
  actual: 0.0001
  default: 0.0001
calc_like:
  actual: false
  default: false
class_sampling_factors:
  actual: null
  default: null
compute_p_values:
  actual: false
  default: false
custom_metric_func:
  actual: null
  default: null
early_stopping:
  actual: true
  default: true
export_checkpoints_dir:
  actual: null
  default: null
family:
  actual: binomial
  default: gaussian
fold_assignment:
  actual: Stratified
  default: AUTO
fold_column:
  actual: null
  default: null
gradient_epsilon:
  actual: 1.0000000000000002e-06
  default: -1.0
ignore_const_cols:
  actual: true
  default: true
ignored_columns:
  actual: null
  default: null
interaction_pairs:
  actual: null
  default: null
interactions:
  actual: null
  default: null
intercept:
  actual: true
  default: true
keep_cross_validation_fold_assignment:
  actual: false
  default: false
keep_cross_validation_models:
  actual: true
  default: true
keep_cross_validation_predictions:
  actual: false
  default: false
lambda:
  actual:
  - 1.4428483867788438
  - 1.3146697127541986
  - 1.1978780788546741
  - 1.0914618918195522
  - 0.9944994255453292
  - 0.9061508375351529
  - 0.8256508945848888
  - 0.7523023446991866
  - 0.6854698778282556
  - 0.6245746231161411
  - 0.569089134998296
  - 0.5185328247204272
  - 0.4724677977086143
  - 0.4304950607359964
  - 0.3922510660343154
  - 0.3574045624170656
  - 0.3256537261401839
  - 0.2967235466491687
  - 0.2703634445692195
  - 0.24634510130656737
  - 0.22446048146204134
  - 0.20452003092796328
  - 0.1863510350611481
  - 0.16979612271128988
  - 0.1547119031473427
  - 0.14096772407560543
  - 0.12844453999205646
  - 0.11703388106714835
  - 0.10663691363203527
  - 0.09716358412861464
  - 0.08853183910868773
  - 0.08066691452624497
  - 0.07350068816706597
  - 0.06697108960668896
  - 0.06102156258608811
  - 0.055600575148413864
  - 0.050661173293835154
  - 0.04616057428645526
  - 0.04205979609072036
  - 0.03832331972767624
  - 0.03491878162656458
  - 0.03181669330705549
  - 0.028990185964137924
  - 0.026414777743384483
  - 0.02406816169084029
  - 0.021930012540859273
  - 0.01998181066837658
  - 0.01820668168077589
  - 0.016589250259973147
  - 0.015115506988766729
  - 0.013772687007967632
  - 0.012549159453295778
  - 0.011434326714397728
  - 0.010418532643415601
  - 0.009492978918053754
  - 0.008649648834720093
  - 0.00788123787167469
  - 0.0071810904207568835
  - 0.00654314213969138
  - 0.005961867425656598
  - 0.005432231555156423
  - 0.004949647075653856
  - 0.004509934071251739
  - 0.004109283958260884
  - 0.003744226497070154
  - 0.0034115997345910854
  - 0.003108522616932885
  - 0.0028323700350919006
  - 0.002580750087513261
  - 0.002351483362583873
  - 0.00214258406161164
  - 0.0019522427987871812
  - 0.0017788109291495447
  - 0.0016207862688122534
  - 0.001476800083765113
  - 0.0013456052345550058
  - 0.0012260653741605683
  - 0.001117145105497912
  - 0.0010179010133064064
  - 0.0009274734927370145
  - 0.0008450793038663174
  - 0.0007700047876469928
  - 0.000701599684534556
  - 0.0006392715022502638
  - 0.0005824803838964395
  - 0.0005307344319742881
  - 0.000483585447803081
  - 0.0004406250494375983
  - 0.00040148113445908577
  - 0.0003658146570021139
  - 0.0003333166911014885
  - 0.00030370575492332744
  - 0.0002767253727040743
  - 0.00025214185328013036
  - 0.0002297422659667909
  - 0.00020933259625451897
  - 0.00019073606534807845
  - 0.00017379159899317924
  - 0.00015835243232834308
  - 0.00014428483867788458
  default: null
lambda_min_ratio:
  actual: 0.0001
  default: -1.0
lambda_search:
  actual: true
  default: false
link:
  actual: logit
  default: family_default
max_active_predictors:
  actual: 5000
  default: -1
max_after_balance_size:
  actual: 5.0
  default: 5.0
max_confusion_matrix_size:
  actual: 20
  default: 20
max_hit_ratio_k:
  actual: 0
  default: 0
max_iterations:
  actual: 1000
  default: -1
max_runtime_secs:
  actual: 0.0
  default: 0.0
missing_values_handling:
  actual: Skip
  default: MeanImputation
nfolds:
  actual: 10
  default: 0
nlambdas:
  actual: 100
  default: -1
non_negative:
  actual: false
  default: false
obj_reg:
  actual: 2.7595341906286218e-05
  default: -1.0
objective_epsilon:
  actual: 0.0001
  default: -1.0
offset_column:
  actual: null
  default: null
plug_values:
  actual: null
  default: null
prior:
  actual: -1.0
  default: -1.0
rand_family:
  actual: null
  default: null
rand_link:
  actual: null
  default: null
random_columns:
  actual: null
  default: null
remove_collinear_columns:
  actual: false
  default: false
response_column:
  actual: !!python/object/new:h2o.backend.connection.H2OResponse
    dictitems:
      __meta: !!python/object/new:h2o.backend.connection.H2OResponse
        dictitems:
          schema_name: ColSpecifierV3
          schema_type: VecSpecifier
          schema_version: 3
      column_name: salario
      is_member_of_frames: null
  default: null
score_each_iteration:
  actual: false
  default: false
seed:
  actual: 123
  default: -1
solver:
  actual: COORDINATE_DESCENT
  default: AUTO
standardize:
  actual: true
  default: true
startval:
  actual: null
  default: null
theta:
  actual: 1.0e-10
  default: 1.0e-10
tweedie_link_power:
  actual: 1.0
  default: 1.0
tweedie_variance_power:
  actual: 0.0
  default: 0.0
weights_column:
  actual: null
  default: null

Gradient Boosting Machine (GBM)

Introducción

Boosting Machine (BM) es un tipo de modelo obtenido al combinar (ensembling) múltiples modelos sencillos (de regresión o clasificación), también conocidos como weak learners. Está combinación se realiza de forma secuencial, de forma que cada nuevo modelo que se incorpora al conjunto intenta corregir los errores de los anteriores modelos. Como resultado de la combinación de múltiples modelos, Boosting Machine consigue aprender relaciones no lineales entre la variable respuesta y los predictores. Si bien los modelos combinados pueden ser muy variados, H2O, al igual que la mayoría de librerías, utiliza como weak learners modelos basados en árboles.

Gradient Boosting Machine (GBM) es una generalización del modelo de Boosting Machine que permite aplicar el método de descenso de gradiente para optimizar cualquier función de coste durante el ajuste del modelo.

El valor predicho por un modelo GBM es la agregación (normalmente la moda en problemas de clasificación y la media en problemas de regresión) de las predicciones de todos los modelos individuales que forman el ensemble.

Dado que el ajuste de los weak learners que forman un GBM se hace de forma secuencial (el árbol i se ajusta a partir de los resultados del árbol i-1), las posibilidades de paralelizar el algoritmo son limitadas. H2O consigue acelerar el proceso en cierta medida paralelizando el ajuste de los árboles individuales.

Para información más detallada sobre métodos boosting consultar Gradient Boosting con Python.

Detención temprana del entrenamiento (Early Stopping)

Una de las características de GBM es que, con el número suficiente de weak learners, el modelo final tiende a ajustarse perfectamente a los datos de entrenamiento causando overfitting. Este comportamiento implica que el analista tiene que encontrar el número adecuado de árboles y, para ello, suele tener que entrenar el modelo con cientos o miles de árboles hasta identificar el momento en el que empieza el overfitting. Esto suele ser poco eficiente en términos de tiempo, ya que, posiblemente, se estén ajustando muchos árboles innecesarios.

De nuevo, la experiencia de los creadores de H2O queda reflejada en su herramienta, esta vez incluyendo toda una serie de estrategias para detener el proceso de ajuste de un modelo GBM a partir del momento en el que este deja de mejorar. Por defecto, la métrica empleada se calcula con los datos de validación. Cuatro argumentos controlan la estrategia de parada:

  • stopping_metric: métrica empleada para cuantificar cuánto mejora el modelo.

  • score_tree_interval: número de árboles tras los que se evalúa el modelo. Por ejemplo, si el valor es 10, entonces, tras cada 10 nuevos árboles que se añaden al modelo, se calcula la stopping_metric. Si bien la evaluación del modelo puede hacerse tras cada nuevo árbol que se añade, esto puede ralentizar el entrenamiento.

  • stopping_tolerance: porcentaje mínimo de mejora entre dos mediciones consecutivas (score_tree_interval) por debajo del cual se considera que el modelo no ha mejorado.

  • stopping_rounds: número de mediciones consecutivas en las que no se debe superar el stopping_tolerance para que el algoritmo se detenga.

Algunos ejemplos de criterios de parada son:

  • Detener el entrenamiento de un modelo cuando el error de clasificación no se reduce más de un 1% durante dos mediciones consecutivas: stopping_rounds=1, stopping_tolerance=0.01 y stopping_metric="misclassification".

  • Detener el entrenamiento de un modelo cuando el logloss no se reduce nada durante 3 mediciones consecutivas: stopping_rounds=3, stopping_tolerance=0 y stopping_metric="logloss".

  • Detener el entrenamiento de un modelo cuando la media de AUC no aumenta más de un 0.1% durante cinco mediciones consecutivas: stopping_rounds=5, stopping_tolerance=0.001 y stopping_metric="AUC".

  • Para detener el modelo aunque la métrica converja, se tiene que deshabilitar este comportamiento de parada automática indicando stopping_rounds=0. En este caso, el algoritmo se detendrá cuando se alcance otra condición de parada, por ejemplo, el número máximo de árboles que pueden formar el modelo.

También es posible detener el entrenamiento del modelo cuando se supera un tiempo máximo especificado con el argumento max_runtimesecs > 0.

En la práctica, es conveniente establecer un número máximo de árboles muy elevado (potencialmente por encima de la cantidad necesaria) y activar la parada temprana.

Comportamiento estocástico

Stochastic Gradient Boosting es el resultado de emplear, en el entrenamiento de cada árbol, únicamente una selección aleatoria de observaciones y predictores del conjunto de entrenamiento. Esta modificación suele reducir el overfitting del modelo y mejorar su generalización. Se puede especificar el % de datos aleatorios seleccionados en cada ajuste con los argumentos:

  • sample_rate: porcentaje de observaciones aleatorias empleadas para el ajuste de cada árbol del ensemble.

  • sample_rate_per_class: cuando el set de datos con el que se trabaja está desbalanceado (el % de observaciones de cada grupo es muy distinto), al hacer una selección aleatoria de observaciones para el entrenamiento de los árboles, puede ocurrir que, en algunos árboles, la selección excluya a alguno de los grupos. Para evitarlo, con el argumento sample_rate_per_class se indica que la selección debe hacerse de forma estratificada, asegurando que todos los grupos queden representados.

  • col_sample_rate: porcentaje de predictores (columnas) seleccionados de forma aleatoria para el ajuste de cada árbol del ensemble.

Los tres parámetros deben tener un valor entre 0 y 1. Por defecto, el valor empleado es 1, es decir, se emplean todos los datos y no hay comportamiento estocástico.

Distribución y función de coste

Dependiendo el tipo de modelo (regresión, clasificación binaria, clasificación múltiple...) se emplea una distribución y una función de coste distinta. Indicando la distribución con el argumento distribution, H2O selecciona automáticamente la función de coste correspondiente.

Las distribuciones disponibles son: bernoulli, quasibinomial, multinomial, poisson, laplace, tweedie, gaussian, huber, gamma y quantile. De entre todas ellas, las más empleadas suelen ser: gaussian para regresión, bernoulli para clasificación binaria y multinomial para clasificación multiclase. Aunque menos frecuentes, también pueden resultar muy útiles la distribución huber, cuando se quiere que los errores penalicen más que su valor absoluto pero menos que el cuadrado, y la distribución quantile cuando en lugar de la media se quiere predecir uno de los cuantiles, por ejemplo la mediana.

Otros hiperparámetros

Además de los ya descritos (criterio de parada, comportamiento estocástico, distribución), el modelo GBM de H2O incorpora muchos otros parámetros e hiperparámetros con los que se puede controlar el comportamiento del algoritmo. A continuación se describen algunos de los que tienen más influencia en el modelo final. Se puede encontrar un listado completo en la documentación.

Complejidad de los árboles

  • ntrees: número de árboles que forman el ensemble.

  • max_depth: profundidad máxima que pueden tener los árboles (número máximo de nodos).

  • min_rows: número mínimo de observaciones que debe tener cada nodo.

  • min_split_improvement: reducción mínima en el error cuadrático que debe de conseguir la división de un nodo para que se lleve a cabo.

Velocidad de aprendizaje

  • learn_rate: determina cuanto influye cada árbol individual en el conjunto del ensemble. Cuanto más pequeño es el valor de learning rate, más lentamente aprende el modelo y más árboles se necesitan, sin embargo, al aprender poco a poco, se reduce el riesgo de overfitting. Suelen emplearse valores inferiores a 0.1.

  • learn_rate_annealing: emplear un valor de learn_rate muy pequeño puede conllevar que el modelo necesite varios miles de árboles y, por lo tanto, el entrenamiento tarde mucho tiempo. En lugar de utilizar un valor muy bajo desde el principio, se puede iniciar el proceso con un valor moderadamente alto e ir reduciéndolo tras cada nuevo árbol que se incorpora al modelo. El learn_rate_annealing determina el factor por el que se reduce el learn_rate en cada iteración. Para N árboles, GBM empieza con learn_rate y termina con learn_rate * learn_rate_annealing^N. Por ejemplo, en lugar de emplear earn_rate=0.01, se puede emplear un learn_rate=0.05 y un learn_rate_annealing=0.99. Con la segunda opción, el algoritmo converge mucho más rápido con aproximadamente la misma capacidad predictiva final.

Comportamiento monotónico

Los modelos GBM son capaces de aprender interacciones complejas entre los predictores y la variable respuesta. Esto puede convertirse en una desventaja cuando, debido al ruido de los datos, el modelo aprende comportamientos que no reflejan la realidad. Con el argumento monotone_constraints se puede forzar a que la relación de cada predictor (numérico) con la variable respuesta tenga siempre la misma dirección (positiva o negativa). Puede encontrarse una descripción más detallada en GBM monotónicos.

Para conseguir maximizar la capacidad predictiva de un modelo GBM, es imprescindible entender bien cómo influye cada uno de los parámetros e hiperparámetros. Algunos de ellos pueden encontrarse descritos con más detalle en el documento Boosting.

Modelo

A continuación, se ajusta un modelo GBM que prediga la variable respuesta salario. En esta ocasión, se emplea validación simple en lugar de validación cruzada, por lo que dividen de nuevo los datos, esta vez, en un conjunto de entrenamiento (60%), validación (20%) y test (20%).

In [48]:
# Reparto de los datos en train, validación y test
# ------------------------------------------------------------------------------
datos_train_h2o, datos_val_h2o, datos_test_h2o = datos_h2o.split_frame(
       ratios=[0.6, 0.20],
       destination_frames= ["datos_train_H2O", "datos_val_h2o", "datos_test_H2O"],
       seed = 123
)

Se entrena un modelo inicial en el que únicamente se especifican el número máximo de árboles, la complejidad de los mismos y la estrategia de parada temprana, el resto de valores se dejan por defecto.

In [49]:
# Ajuste del modelo y validación mediente 5-CV para estimar su error
# ------------------------------------------------------------------------------
from h2o.estimators.gbm import H2OGradientBoostingEstimator
modelo_gbm = H2OGradientBoostingEstimator(
                      # Tipo de distribución (clasificación binaria)
                      distribution = "bernoulli",
                      # Número de árboles.
                      ntrees = 500,
                      # Complejidad de los árboles
                      max_depth = 3,
                      min_rows = 10,
                      # Aprendizaje
                      learn_rate = 0.01,
                      # Detención temprana
                      score_tree_interval = 5,
                      stopping_rounds = 3,
                      stopping_metric = "AUC",
                      stopping_tolerance = 0.001,
                      model_id = "modelo_gbm",
                      seed = 123
)
In [50]:
modelo_gbm.train(
    # Variable respuesta y predictores.
    y = var_respuesta,
    x = predictores,
    # Datos de entrenamiento.
    training_frame = datos_train_h2o,
    # Datos de validación para estimar el error.
    validation_frame = datos_val_h2o
)
gbm Model Build progress: |███████████████████████████████████████████████| 100%
In [51]:
modelo_gbm.summary()
Model Summary: 
number_of_trees number_of_internal_trees model_size_in_bytes min_depth max_depth mean_depth min_leaves max_leaves mean_leaves
0 95.0 95.0 15091.0 3.0 3.0 3.0 8.0 8.0 8.0
Out[51]:

Métricas de validación

In [52]:
# Métricas de validación
# ------------------------------------------------------------------------------
#modelo_gbm.model_performance(valid=True)
print("auc: ", modelo_gbm.auc(valid=True))
print("MSE: ",modelo_gbm.mse(valid=True))
print("R2: ",modelo_gbm.r2(valid=True))
print("Gini: ",modelo_gbm.gini(valid=True))
print("LogLoss: ",modelo_gbm.logloss(valid=True))
auc:  0.8983081718883074
MSE:  0.12060498046621397
R2:  0.34803284487764896
Gini:  0.7966163437766147
LogLoss:  0.3929508713514315

Evolución del modelo

Tras ajustar un modelo GBM, es fundamental evaluar la como aprende el modelo a medida que se añaden nuevos árboles al ensemble. Una forma de hacerlo es representar la evolución de una métrica (en este caso el área bajo la curva roc, AUC) de entrenamiento y validación.

In [53]:
# H2O almacena las métricas de entrenamiento y test bajo el nombre de scoring.
# Los valores se encuentran almacenados dentro del modelo.
historial_scoring = modelo_gbm.scoring_history()
historial_scoring.head(5)
Out[53]:
timestamp duration number_of_trees training_rmse training_logloss training_auc training_pr_auc training_lift training_classification_error validation_rmse validation_logloss validation_auc validation_pr_auc validation_lift validation_classification_error
0 2020-10-20 08:56:08 0.007 sec 0.0 0.432896 0.562113 0.500000 0.249798 1.000000 0.750202 0.430127 0.556861 0.500000 0.245022 1.000000 0.754978
1 2020-10-20 08:56:08 0.402 sec 5.0 0.425079 0.544571 0.851188 0.708361 3.996064 0.275903 0.422083 0.538932 0.861584 0.719758 4.061643 0.153650
2 2020-10-20 08:56:09 0.684 sec 10.0 0.417868 0.529126 0.870436 0.733717 3.950908 0.172954 0.414663 0.523156 0.881485 0.744413 4.006379 0.165929
3 2020-10-20 08:56:09 0.841 sec 15.0 0.411196 0.515310 0.871976 0.735588 3.959439 0.179315 0.407769 0.508996 0.883495 0.745848 3.975941 0.171571
4 2020-10-20 08:56:09 0.910 sec 20.0 0.405023 0.502859 0.871987 0.735900 3.959439 0.179315 0.401384 0.496225 0.883507 0.746178 3.975941 0.171571

Tal y como se ha indicado en el argumento (score_tree_interval=5), cada 5 árboles se han recalculado las métricas de rendimiento en el conjunto de entrenamiento y validación. El argumento stopping_metric="AUC" indica que se emplee el área bajo la curva como criterio para decidir si el algoritmo debe detenerse de forma temprana, pero se calculan siempre todas (AUC, logloss, lift, rmse, classification_error).

In [54]:
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(6, 3.8))
historial_scoring.plot(
    x="number_of_trees",
    y=["validation_auc","training_auc"],
    kind='line',
    ax = ax
)
fig.suptitle('Comparación errores train y validación', fontsize= 'large');

En el gráfico puede observarse que no se han alcanzado los 500 árboles indicados en el modelo, esto es así porque, alcanzados los 95 árboles, el valor de AUC se ha estabilizado lo suficiente como para que se ejecute la detención temprana.

Importancia de los predictores

En los modelos GBM de H2O, se puede estudiar la influencia de los predictores cuantificando la reducción total de error cuadrático que ha conseguido cada predictor en el conjunto de todos los árboles que forman el modelo.

In [55]:
# Importancia predictores
# -----------------------------------------------------------------------------
imporatancia_predictores = modelo_gbm.varimp(use_pandas=True)
imporatancia_predictores
Out[55]:
variable relative_importance scaled_importance percentage
0 relationship 43854.789062 1.000000 0.511779
1 capital_gain 18809.373047 0.428901 0.219502
2 education_number 11191.689453 0.255199 0.130605
3 occupation 9717.506836 0.221584 0.113402
4 capital_loss 1100.636475 0.025097 0.012844
5 age 627.400879 0.014306 0.007322
6 hours_per_week 389.534515 0.008882 0.004546
7 workclass 0.000000 0.000000 0.000000
8 final_weight 0.000000 0.000000 0.000000
9 education 0.000000 0.000000 0.000000
10 marital_status 0.000000 0.000000 0.000000
11 race 0.000000 0.000000 0.000000
12 sex 0.000000 0.000000 0.000000
13 native_country 0.000000 0.000000 0.000000
In [56]:
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(6, 3.8))
imporatancia_predictores.plot.barh(x='variable', y='scaled_importance', ax=ax)
ax.invert_yaxis()
fig.suptitle('Importancia predictores', fontsize= 'large');

Predicciones y error

Una vez ajustado el modelo, se puede predecir nuevas observaciones y estimar su error promedio.

In [57]:
predicciones = modelo_gbm.predict(test_data = datos_test_h2o)
gbm prediction progress: |████████████████████████████████████████████████| 100%
In [58]:
predicciones.head(5)
predict <=50K >50K
>50K 0.2996120.700388
>50K 0.3645950.635405
<=50K 0.7598280.240172
<=50K 0.7393680.260632
<=50K 0.7598280.240172
Out[58]:

In [59]:
# Cálculo de las métricas para el conjunto de test
# ------------------------------------------------------------------------------
performance_test = modelo_glm.model_performance(test_data = datos_test_h2o)
In [60]:
# Métricas de test
# ------------------------------------------------------------------------------
print(f"auc: {performance_test.auc()}")
print(f"MSE: {performance_test.mse()}")
print(f"R2: {performance_test.r2()}")
print(f"Gini: {performance_test.gini()}")
print(f"AIC: {performance_test.aic()}")
print(f"LogLoss: {performance_test.logloss()}")
auc: 0.9052539122257957
MSE: 0.10335891103080022
R2: 0.44087059824545627
Gini: 0.8105078244515913
AIC: 5863.609924742784
LogLoss: 0.3219952095248656

Empleando los valores por defecto de los hiperparámetros, se ha conseguido un AUC de test de 0.893. A continuación, se intenta encontrar los valores óptimos de los hiperparámetros con los que se consiga mejorar aún más el AUC.

Dada la gran cantidad de hiperparámetros que tiene el algoritmo GBM y, aunque todos interaccionan entre sí, no es posible explorar todo el espacio de posibles valores a la vez. Una estrategia que suele funcionar bien es establecer primero los hiperparámetros más influyentes (número de árboles, learning rate, complejidad de los árboles, % de observaciones empleadas en cada ajuste).

  • Número de árboles (ntree): incorporar tantos árboles como sea necesario hasta que el error de validación se estabilice o empiece a aumentar. Dado que H2O incorpora estrategias de parada temprana, no hay problema en indicar un número elevado porque, de no ser necesario, no se ajustarán.

  • Tasa de aprendizaje (learning_rate): por lo general, valores pequeños (<0.01) dan lugar a en mejores modelos, pero requieren más árboles para aprender. Se recomienda reducir el valor tanto como permitan las limitaciones de tiempo/computación.

  • Profundidad de los árboles (max_depth): es la principal forma de controlar la complejidad de los árboles que forman el ensemble. Al tratarse de weak learners, la complejidad de los árboles debe ser baja, aunque depende mucho de los datos, el valor óptimo raramente está fuera del rango [1, 20].

  • Comportamiento estocástico (sample_rate, col_sample_rate): la finalidad de emplear solo un subconjunto de observaciones y/o columnas en el entrenamiento de cada árbol es conseguir mejor generalización, sin embargo, puede tener el resultado contrario cuando se dispone de muy pocos datos. El valor óptimo suele estar dentro del rango de [0.7-0.9]. En situaciones en las que la variable respuesta es cualitativa y los grupos están desbalanceados, es conveniente realizar una selección aleatoria estratificada, este comportamiento puede activarse con sample_rate_per_class.

In [61]:
# Búsqueda de hiperparámetros
# ------------------------------------------------------------------------------
from h2o.grid.grid_search import H2OGridSearch
# Hiperparámetros que se quieren comparar.
hiperparametros = {
                   "learn_rate":[0.001, 0.05, 0.1],
                   "max_depth":[5, 10, 15, 20],
                   "sample_rate":[0.8, 1]
                  }
In [62]:
gbm_grid = H2OGridSearch(
                model=H2OGradientBoostingEstimator(
                        distribution = "bernoulli",
                        # Número de árboles.
                        ntrees = 5000,
                        # Preprocesado.
                        ignore_const_cols = True,
                        # Validación cruzada de 10 folds para estimar el error
                        # del modelo.
                        seed = 123,
                        nfolds = 10,
                        # Reparto estratificado de las observaciones en la creación
                        # de las particiones.
                        fold_assignment = "Stratified",
                        keep_cross_validation_predictions = False,
                        # Detención temprana
                        score_tree_interval = 5,
                        stopping_rounds = 3,
                        stopping_metric = "AUC",
                        stopping_tolerance = 0.001,
                       ),
                hyper_params    = hiperparametros,
                search_criteria = {'strategy': "Cartesian"},
                grid_id         = 'gbm_grid'
              )
In [63]:
gbm_grid.train(
    y = var_respuesta,
    x = predictores,
    training_frame = datos_train_h2o,
)
gbm Grid Build progress: |████████████████████████████████████████████████| 100%

Se muestran los modelos ordenados de mayor a menor AUC.

In [64]:
# Resultados grid search
# ------------------------------------------------------------------------------
gbm_grid.get_grid(sort_by="auc", decreasing=True).sorted_metric_table()
Out[64]:
learn_rate max_depth sample_rate model_ids auc
0 0.1 5 1.0 gbm_grid_model_15 0.9230204166389421
1 0.1 5 0.8 gbm_grid_model_3 0.9229135381975297
2 0.05 5 0.8 gbm_grid_model_2 0.9214098026748119
3 0.1 10 0.8 gbm_grid_model_6 0.9212633605986855
4 0.05 5 1.0 gbm_grid_model_14 0.920807013603398
5 0.1 10 1.0 gbm_grid_model_18 0.9207734770298204
6 0.05 10 0.8 gbm_grid_model_5 0.920687446232555
7 0.05 10 1.0 gbm_grid_model_17 0.9200852162242628
8 0.05 15 0.8 gbm_grid_model_8 0.9163461407504816
9 0.05 15 1.0 gbm_grid_model_20 0.9156963218465364
10 0.1 15 0.8 gbm_grid_model_9 0.9155608698693226
11 0.1 15 1.0 gbm_grid_model_21 0.9149318986095241
12 0.1 20 0.8 gbm_grid_model_12 0.9126322952543492
13 0.1 20 1.0 gbm_grid_model_24 0.9108515565788904
14 0.05 20 0.8 gbm_grid_model_11 0.9106781496260091
15 0.05 20 1.0 gbm_grid_model_23 0.9103509389980908
16 0.001 10 0.8 gbm_grid_model_4 0.9054890700057832
17 0.001 15 0.8 gbm_grid_model_7 0.9041709650805856
18 0.001 20 0.8 gbm_grid_model_10 0.9010543973755457
19 0.001 10 1.0 gbm_grid_model_16 0.888799048447155
20 0.001 5 0.8 gbm_grid_model_1 0.8794753543911948
21 0.001 5 1.0 gbm_grid_model_13 0.8794195057887776
22 0.001 15 1.0 gbm_grid_model_19 0.877747272248072
23 0.001 20 1.0 gbm_grid_model_22 0.8681082377366655

Los 3 modelos con mayor AUC de validación tienen todos un learning rate de 0.1 o 0.5 y un max depth 5, el sample rate parece no influir de forma sustancial. Teniendo en cuenta estos resultados, pude concluirse que los valores óptimos son: learn_rate=0.1, max_depth=5 y sample_rate=0.8.

Se extrae del grid el mejor modelo.

In [65]:
# Mejor modelo
# ------------------------------------------------------------------------------
id_mejor_modelo = gbm_grid.get_grid(sort_by="auc", decreasing=True)\
                  .sorted_metric_table()['model_ids'][0]
mejor_gbm_final = h2o.get_model(id_mejor_modelo)
In [66]:
# Cálculo de la métrica AUC para el conjunto de test.
performance = mejor_gbm_final.model_performance(test_data = datos_test_h2o)
performance.auc()
Out[66]:
0.9273913948290959

Optimizando los hiperparámetros más influyentes, se ha conseguido mejorar el AUC de test de 0.893 a 0.927.

Por último, se intenta encontrar el valor óptimo del resto de hiperparámetros, esta vez mediante una búsqueda aleatoria.

In [67]:
# Hiperparámetros que se quieren optimizar mediante búsqueda aleatoria.
# Para realizar esta búsqueda se tiene que pasar un vector de posibles valores
# de cada hiperparámetro, entre los que se escoge aleatoriamente.
hiperparametros = { 
                   "min_rows" : list(range(5,50,10)),                           
                   "nbins" : [2**x for x in range(4, 10, 1)],                                                     
                   "nbins_cats" : [2**x for x in range(4, 10, 1)],
                   "min_split_improvement" : [0, 1e-8, 1e-6, 1e-4],
                   "histogram_type" : ["UniformAdaptive","QuantilesGlobal","RoundRobin"]
                   }

# Al ser una búsqueda aleatoria, hay que indicar criterios de parada.
search_criteria = {
                  "strategy" : "RandomDiscrete",      
                  # Tiempo máximo de búsqueda (5 minutos).
                  "max_runtime_secs" : 5*60,         
                  # Número máximo de modelos.
                  "max_models" : 100,                  
                  # Reproducible.
                  "seed" : 1234
                  }
In [68]:
gbm_grid_2 = H2OGridSearch(
                model=H2OGradientBoostingEstimator(
                        distribution = "bernoulli",
                        # Hiperparámetros fijados
                        ntrees = 5000,
                        learn_rate = 0.1, 
                        max_depth = 5,
                        sample_rate = 0.8, 
                        # Preprocesado.
                        ignore_const_cols = True,
                        # Validación cruzada de 10 folds para estimar el error
                        # del modelo.
                        seed = 123,
                        nfolds = 10,
                        # Reparto estratificado de las observaciones en la creación
                        # de las particiones.
                        fold_assignment = "Stratified",
                        keep_cross_validation_predictions = False,
                        # Detención temprana
                        score_tree_interval = 5,
                        stopping_rounds = 3,
                        stopping_metric = "AUC",
                        stopping_tolerance = 0.001,
                       ),
                # Hiperparámetros optimizados.
                hyper_params    = hiperparametros,
                # Tipo de búsqueda.
                search_criteria = search_criteria,
                grid_id = "gbm_grid_2"
              )
In [69]:
gbm_grid_2.train(
    y = var_respuesta,
    x = predictores,
    training_frame = datos_train_h2o,
)
gbm Grid Build progress: |████████████████████████████████████████████████| 100%
In [70]:
# Se muestran los modelos ordenados de mayor a menor AUC.
gbm_grid_2.get_grid(sort_by="auc", decreasing=True).sorted_metric_table()
Out[70]:
histogram_type min_rows min_split_improvement nbins nbins_cats model_ids auc
0 UniformAdaptive 5.0 1.0E-8 64 64 gbm_grid_2_model_16 0.9233108300928832
1 UniformAdaptive 5.0 1.0E-4 64 64 gbm_grid_2_model_7 0.9231388226012354
2 RoundRobin 15.0 1.0E-8 512 256 gbm_grid_2_model_14 0.9229711000579003
3 RoundRobin 5.0 1.0E-8 64 256 gbm_grid_2_model_9 0.9229464543913853
4 UniformAdaptive 5.0 1.0E-4 256 128 gbm_grid_2_model_15 0.9227909446721125
5 UniformAdaptive 25.0 1.0E-6 512 32 gbm_grid_2_model_13 0.9227572241487337
6 RoundRobin 15.0 0.0 64 512 gbm_grid_2_model_1 0.9225543708002095
7 UniformAdaptive 35.0 1.0E-4 128 32 gbm_grid_2_model_4 0.9224657178165613
8 UniformAdaptive 35.0 0.0 32 32 gbm_grid_2_model_5 0.9223677555302235
9 RoundRobin 35.0 0.0 512 256 gbm_grid_2_model_12 0.9223587852722662
10 UniformAdaptive 45.0 1.0E-8 32 64 gbm_grid_2_model_3 0.9220698037412879
11 UniformAdaptive 45.0 1.0E-6 512 64 gbm_grid_2_model_6 0.9220434772785493
12 QuantilesGlobal 15.0 1.0E-6 512 256 gbm_grid_2_model_17 0.921576475622222
13 QuantilesGlobal 15.0 1.0E-4 64 16 gbm_grid_2_model_10 0.9208226673708024
14 QuantilesGlobal 35.0 1.0E-4 256 64 gbm_grid_2_model_11 0.9205979095684886
15 QuantilesGlobal 45.0 0.0 64 128 gbm_grid_2_model_8 0.9202604229997097
16 QuantilesGlobal 15.0 1.0E-6 16 32 gbm_grid_2_model_2 0.9198846388036724
17 RoundRobin 5.0 1.0E-4 256 256 gbm_grid_2_model_18 0.9184626058476011
18 QuantilesGlobal 5.0 0.0 64 128 gbm_grid_2_model_19 0.9091498441808122
19 UniformAdaptive 5.0 0.0 256 512 gbm_grid_2_model_20 0.8900575175681439
In [71]:
id_mejor_modelo = gbm_grid_2.get_grid(sort_by="auc", decreasing=True)\
                  .sorted_metric_table()['model_ids'][0]
mejor_gbm_final = h2o.get_model(id_mejor_modelo)
In [72]:
# Cálculo de la métrica AUC para el conjunto de test.
performance = mejor_gbm_final.model_performance(test_data = datos_test_h2o)
performance.auc()
Out[72]:
0.9270610711708912

El AUC de test obtenido es prácticamente idéntico al del anterior modelo.

Deep Learning (Neural Networks)

Introducción

El término deep learning engloba a todo un conjunto de modelos basados en redes neuronales artificiales (artificial neural networks) que contienen múltiples capas intermedias (ocultas). En concreto, H2O incorpora redes neuronales de tipo Multi-layer - feedforward - neural networks. Estas redes se caracterizan por:

  • Pueden tener una o múltiples capas intermedias, también conocidas como capas ocultas. Son las capas de neuronas que hay entre la capa de entrada y la de salida.

  • La estructura es full conected, lo que significa que cada neurona está conectada con todas las neuronas de la capa siguiente.

  • El peso (participación de cada neurona en la red) se aprende mediante el proceso feed-forward. A grandes rasgos, la estrategia de aprendizaje consiste en un proceso iterativo. En cada iteración (época), la red predice las observaciones de entrenamiento, se calcula el error de estas predicciones comparando los resultados frente al valor real y se reajustan los pesos de las neuronas con el objetivo de minimizar el error (backpropagation y descenso de gradiente).

Estas redes son unas de las más empleadas, pero existen otros tipos, por ejemplo, las Convolutional Neural Networks (CNNs) son las que mejores resultados dan cuando se trabaja con imágenes.

Comprender los fundamentos de las redes neuronales y deep learning requiere de un estudio notable, puede encontrarse mucha de la información necesaria en el curso gratuito de Stanford Convolutional Neural Networks for Visual Recognition y en el libro Deep Learning An MIT Press book.

Flexibilidad de las redes neuronales

Los modelos basados en redes neuronales tienen la característica de ser potencialmente capaces de aprender cualquier función. La velocidad con la que aprenden está influenciada principalmente (aunque hay muchos otros hiperparámetros) por el número de observaciones, el número de neuronas y el número de iteraciones de aprendizaje (comúnmente llamadas epoch). A continuación, se muestra un ejemplo sencillo que ilustra este concepto.

Supóngase las siguientes observaciones generadas a partir de un modelo no lineal de espiral.

In [73]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# Número de observaciones por clase
N = 300
# Número de dimensiones
D = 2
# Número clases
K = 3

x_1 = np.empty(0)
x_2 = np.empty(0)
y   = np.empty(0)

# Simulación de los datos
for i in np.arange(K):
    np.random.seed(123)
    r = np.linspace(start=0, stop=1, num=N)
    t = np.linspace(start =  i*4, stop = (i+1)*4, num = N) + np.random.normal(size = N) * 0.35
    x_1 = np.hstack((x_1,r * np.sin(t)))
    x_2 = np.hstack((x_2, r* np.cos(t)))
    y   = np.hstack((y, np.repeat(i, N)))
    
datos_espiral = pd.DataFrame({"x_1" : x_1,
                              "x_2" : x_2,
                              "y" : y})
In [74]:
fig, ax = plt.subplots(figsize=(4, 4))
ax.scatter(x = datos_espiral["x_1"],
           y = datos_espiral["x_2"],
           c = datos_espiral["y"],
           cmap = "viridis"
  )
ax.axis('off');

Se comparan 3 redes neuronales: 10 neuronas y 50 epochs, 10 neuronas y 1000 epochs, 2 capas de 200 neuronas neuronas y 1000 epochs.

In [75]:
from h2o.estimators.deeplearning import H2ODeepLearningEstimator

datos_espiral_h2o = h2o.H2OFrame(datos_espiral)
datos_espiral_h2o["y"] = datos_espiral_h2o["y"].asfactor()

modelo_dl_10 = H2ODeepLearningEstimator(
                                 distribution = "multinomial",
                                 standardize  = True,
                                 activation   = "Rectifier",
                                 hidden       = [10],
                                 stopping_rounds = 0,
                                 epochs   = 50,
                                 seed     = 123,
                                 model_id = "modelo_dl_10"
            )

modelo_dl_10.train(
    x = ["x_1", "x_2"],
    y = "y",
    training_frame = datos_espiral_h2o
)


modelo_dl_10_1k = H2ODeepLearningEstimator(
                                 distribution = "multinomial",
                                 standardize  = True,
                                 activation   = "Rectifier",
                                 hidden       = [10],
                                 stopping_rounds = 0,
                                 epochs   = 1000,
                                 seed     = 123,
                                 model_id = "modelo_dl_10_1k"
                 )

modelo_dl_10_1k.train(
    x = ["x_1", "x_2"],
    y = "y",
    training_frame = datos_espiral_h2o
)

modelo_dl_2_200 = H2ODeepLearningEstimator(
                                 distribution = "multinomial",
                                 standardize  = True,
                                 activation   = "Rectifier",
                                 hidden       = [200, 200],
                                 stopping_rounds = 0,
                                 epochs   = 1000,
                                 seed     = 123,
                                 model_id = "modelo_dl_2_200"
                 )

modelo_dl_2_200.train(
    x = ["x_1", "x_2"],
    y = "y",
    training_frame = datos_espiral_h2o
)
Parse progress: |█████████████████████████████████████████████████████████| 100%
deeplearning Model Build progress: |██████████████████████████████████████| 100%
deeplearning Model Build progress: |██████████████████████████████████████| 100%
deeplearning Model Build progress: |██████████████████████████████████████| 100%
In [76]:
def expand_grid(x, y):
    xG, yG = np.meshgrid(x, y) # create the actual grid
    xG = xG.flatten() # make the grid 1d
    yG = yG.flatten() # same
    return pd.DataFrame({'x':xG, 'y':yG})

grid_predicciones = expand_grid(x = np.linspace(-1,1, num = 75),
                                y = np.linspace(-1,1, num = 75))
grid_predicciones.columns = ["x_1", "x_2"]
In [77]:
grid_predicciones_h2o = h2o.H2OFrame(grid_predicciones)
Parse progress: |█████████████████████████████████████████████████████████| 100%

Una vez entrenados los modelos, se representan sus fronteras de clasificación.

In [78]:
predicciones_10 = modelo_dl_10.predict(test_data = grid_predicciones_h2o)
grid_predicciones["y_10"] = predicciones_10["predict"].as_data_frame()

predicciones_10_1k = modelo_dl_10_1k.predict(test_data = grid_predicciones_h2o)
grid_predicciones["y_10_1k"] = predicciones_10_1k["predict"].as_data_frame()

predicciones_2_200 = modelo_dl_2_200.predict(test_data = grid_predicciones_h2o)
grid_predicciones["y_2_200"] = predicciones_2_200["predict"].as_data_frame()

display(grid_predicciones.head(5))
deeplearning prediction progress: |███████████████████████████████████████| 100%
deeplearning prediction progress: |███████████████████████████████████████| 100%
deeplearning prediction progress: |███████████████████████████████████████| 100%
x_1 x_2 y_10 y_10_1k y_2_200
0 -1.000000 -1.0 0 0 0
1 -0.972973 -1.0 0 0 0
2 -0.945946 -1.0 0 0 0
3 -0.918919 -1.0 0 0 0
4 -0.891892 -1.0 0 0 0
In [79]:
fig, axis = plt.subplots(nrows=1, ncols=3, figsize=(10.5, 3.5))
axis[0].scatter(
    x = grid_predicciones["x_1"],
    y = grid_predicciones["x_2"],
    c = grid_predicciones["y_10"],
    cmap = "viridis"
)
axis[0].set_title("1 capa oculta, 10 neuronas, 50 epochs", fontsize = 8)
axis[0].axis('off')

axis[1].scatter(
    x = grid_predicciones["x_1"],
    y = grid_predicciones["x_2"],
    c = grid_predicciones["y_10_1k"],
    cmap = "viridis"
)
axis[1].set_title("1 capa oculta, 10 neuronas, 1000 epochs", fontsize = 8)
axis[1].axis('off')

axis[2].scatter(
    x = grid_predicciones["x_1"],
    y = grid_predicciones["x_2"],
    c = grid_predicciones["y_2_200"],
    cmap = "viridis"
)
axis[2].set_title("2 capa oculta, 200 neuronas, 1000 epochs", fontsize = 8)
axis[2].axis('off');

fig.suptitle('Comparación redes neuronales', fontsize= 'large');

Este ejemplo debe servir como reflexión sobre la alta capacidad que tienen los modelos basados en redes para adaptarse a los datos de entrenamiento, por lo tanto, cuando la complejidad de los datos no es muy grande o se dispone de pocas observaciones, se debe tener muy presente el riesgo de overfitting.

Parámetros e hiperparámetros

Los modelos de Deep Learning ofrecidos por H2O tienen un número muy elevado de parámetros configurables (listado). Para la gran mayoría de casos, los valores por defecto dan buenos resultados, sin embargo, es conveniente conocer, al menos, los más influyentes.

Arquitectura

  • hidden: número de capas ocultas y número de neuronas por capa. La estructura se define mediante una lista de números enteros. Por ejemplo, una red con una única capa de 100 neuronas se crea con hidden = [100], y una red con dos capas ocultas de 10 neuronas cada una con hidden = [10, 10]. Las capas de entrada y salida se crean automáticamente. La capa de entrada tiene tantas neuronas como predictores (tras codificar las variables categóricas). La capa de salida tiene una única neurona en problemas de regresión, y tantas neuronas como clases en los problemas de clasificación.

Preprocesado

  • standardize: por defecto, se estandarizan los valores para que tengan media cero y varianza uno. Los modelos basados en redes no son independientes de la escala de los predictores, por lo que es fundamental estandarizarlos.

  • missing_values_handling: las redes neuronales no aceptan observaciones con valores ausentes. H2O ofrece dos opciones: excluirlos Skip o imputarlos con la media MeanImputation.

  • shuffle_training_data: mezclar aleatoriamente las observaciones antes de entrenar el modelo.

Aprendizaje

  • activation: función de activación de las neuronas de las capas intermedias (Tahn, Tahn with dropout, Rectifier, Rectifier with dropout, Maxout, Maxout with dropout). Las funciones Rectifier y Rectifier with dropout suelen dar buenos resultados para un abanico amplio de situaciones. La función de activación de la capa de salida se selecciona automáticamente dependiendo de si es un problema de regresión o clasificación.

  • loss: función de coste que se intenta minimizar durante el entrenamiento de la red. Esta es la forma en que se cuantifica el error que comete la red y en función de ello aprende, por lo tanto, es importante escoger la función de coste más adecuada para el problema en cuestión (esto es así para todos los algoritmos, no solo para redes). Por ejemplo, en problemas de regresión, las dos funciones más comunes son el error absoluto y el error cuadrático. Con el primero, el modelo resultante prioriza predecir bien la mayoría de observaciones, aunque para unas pocas se equivoque por mucho. Con el segundo, como los errores se elevan al cuadrado, el modelo resultante tiende a evitar grandes errores a cambio de aumentar un poco el error en la mayoría de observaciones.

  • epochs: número de iteraciones de aprendizaje durante el entrenamiento de la red. Encontrar el valor óptimo de épocas es muy importante ya que, si son muy pocas, la red puede no aprender lo suficiente, pero si son demasiadas, se produce overfiting. Normalmente, se puede tener una idea aproximada del valor correcto en función de la complejidad de la red (cuanto más compleja más pesos hay que aprender) y el número de observaciones de entrenamiento. Gracias al sistema de early stopping implementado en H2O, se puede indicar un número muy elevado de épocas sin riesgo de overfitting, ya que el entrenamiento se detendrá cuando la métrica de validación elegida no mejore. Aun así, siempre es recomendable representar la evolución del error de entrenamiento y validación en función del número de épocas.

  • adaptive_rate: si se indica esta opción (activada por defecto), se emplea el algoritmo (ADADELTA) como algoritmo de aprendizaje. Con él, se consigue que el learning rate se adapte automáticamente (reduciéndose) a medida que el entrenamiento avanza. Es recomendable probar en primer lugar esta opción, ya que consigue un balance entre velocidad de aprendizaje y resultados bastante bueno. Este algoritmo está controlado por dos parámetros:

    • rho: controla el ratio en el que se va reduciendo el learning rate en cada iteración de aprendizaje.

    • epsilon: corrector para evitar divisiones entre cero.

Si se desactiva el adaptive_rate, entonces, el usuario debe especificar el valor de learning rate empleado por la red.

  • rate: Cuanto menor sea este valor, más estable es el modelo final pero más tarda en aprender (llegar a convergencia).

  • nesterov_accelerated_gradient: activa el método de descenso de gradiente Nesterov Accelerated Gradient.

Regularización

  • input_dropout_ratio: porcentaje de dropout en la capa de entrada. Este tipo de regularización controla que ningún predictor influya en exceso en la red. Se recomiendan valores de 0.1 o 0.2.

  • hidden_dropout_ratios: porcentaje de dropout en las capas de intermedias. Solo es aplicable si se selecciona como función de activación TanhWithDropout, RectifierWithDropout, o MaxoutWithDropout. Por defecto, el valor es 0.5.

  • l1: penalización l1. Ver apartado de regularización GLM para más información.

  • l2: penalización l2. Ver apartado de regularización GLM para más información.

El siguiente listado contiene el valor por defecto de cada uno de los argumentos de la función h2o.deeplearning().

  • nfolds = 0
  • keep_cross_validation_predictions = False
  • keep_cross_validation_fold_assignment = False
  • fold_assignment = "AUTO", "Random", "Modulo", "Stratified"
  • fold_column = NULL
  • ignore_const_cols = True
  • score_each_iteration = False
  • weights_column = NULL
  • offset_column = NULL
  • balance_classes = False
  • class_sampling_factors = NULL
  • max_after_balance_size = 5
  • max_hit_ratio_k = 0
  • checkpoint = NULL
  • pretrained_autoencoder = NULL
  • overwrite_with_best_model = True
  • use_all_factor_levels = True
  • standardize = True
  • activation = "Tanh", "TanhWithDropout", "Rectifier", "RectifierWithDropout", "Maxout", "MaxoutWithDropout"
  • hidden = [200, 200]
  • epochs = 10
  • train_samples_per_iteration = -2
  • target_ratio_comm_to_comp = 0.05
  • seed = -1
  • adaptive_rate = True
  • rho = 0.99
  • epsilon = 1e-08
  • rate = 0.005
  • rate_annealing = 1e-06
  • rate_decay = 1
  • momentum_start = 0
  • momentum_ramp = 1e+06
  • momentum_stable = 0
  • nesterov_accelerated_gradient = True
  • input_dropout_ratio = 0
  • hidden_dropout_ratios = NULL
  • l1 = 0
  • l2 = 0
  • max_w2 = 3.4028235e+38
  • initial_weight_distribution = "UniformAdaptive", "Uniform", "Normal"
  • initial_weight_scale = 1
  • initial_weights = NULL
  • initial_biases = NULL
  • loss = "Automatic", "CrossEntropy", "Quadratic", "Huber", "Absolute", "Quantile"
  • distribution = "AUTO", "bernoulli", "multinomial", "gaussian", "poisson", "gamma", "tweedie", "laplace", "quantile", "huber"
  • quantile_alpha = 0.5
  • tweedie_power = 1.5
  • huber_alpha = 0.9
  • score_interval = 5
  • score_training_samples = 10000
  • score_validation_samples = 0
  • score_duty_cycle = 0.1
  • classification_stop = 0
  • regression_stop = 1e-06
  • stopping_rounds = 5
  • stopping_metric = "AUTO", "deviance", "logloss", "MSE", "RMSE", "MAE", "RMSLE", "AUC", "lift_top_group", "misclassification", "mean_per_class_error"
  • stopping_tolerance = 0
  • max_runtime_secs = 0
  • score_validation_sampling = "Uniform", "Stratified"
  • diagnostics = True
  • fast_mode = True
  • force_load_balance = True
  • variable_importances = True
  • replicate_training_data = True
  • single_node_mode = False
  • shuffle_training_data = False
  • missing_values_handling = "MeanImputation", "Skip"
  • quiet_mode = False
  • autoencoder = False
  • sparse = False
  • col_major = False
  • average_activation = 0
  • sparsity_beta = 0
  • max_categorical_features = 2147483647
  • reproducible = False
  • export_weights_and_biases = False
  • mini_batch_size = 1
  • categorical_encoding = "AUTO", "Enum", "OneHotInternal", "OneHotExplicit", "Binary", "Eigen", "LabelEncoder", "SortByResponse", "EnumLimited"
  • elastic_averaging = False
  • elastic_averaging_moving_rate = 0.9
  • elastic_averaging_regularization = 0.001
  • verbose = False

Importancia de los predictores

Para modelos de redes neuronales, una forma de cuantificar la importancia de los predictores es mediante el método Gedeon. Por defecto, esta opción está desactivada debido a que puede ser un proceso muy lento.

Modelo

In [80]:
from h2o.grid.grid_search import H2OGridSearch

# Hiperparámetros que se quieren comparar.
hiperparametros = {"hidden" : [[64], [128], [256], [512], [1024],
                               [64,64], [128,128], [256,256], [512, 512]]
                  }
                   
                   
dl_grid = H2OGridSearch(
                model=H2ODeepLearningEstimator(
                        activation = "RectifierWithDropout",
                        epochs = 500,
                        # Preprocesado.
                        standardize = True,
                        missing_values_handling = "Skip",
                        # Detención temprana.
                        stopping_rounds = 3,
                        stopping_metric = "AUC",
                        stopping_tolerance = 0.01,
                        # Regularización
                        l1 = 1e-5,
                        l2 = 1e-5,
                       ),
                hyper_params    = hiperparametros,
                search_criteria = {'strategy': "Cartesian"},
                grid_id         = 'grid_dl'
              )
In [81]:
dl_grid.train(
    # Variable respuesta y predictores.
    y = var_respuesta,
    x = predictores,
    # Datos de entrenamiento.
    training_frame = datos_train_h2o,
    shuffle_training_data = False,
    # Datos de validación.
    validation_frame = datos_val_h2o
)
deeplearning Grid Build progress: |███████████████████████████████████████| 100%
In [82]:
hiperparametros
Out[82]:
{'hidden': [[64],
  [128],
  [256],
  [512],
  [1024],
  [64, 64],
  [128, 128],
  [256, 256],
  [512, 512]]}

Se muestran los modelos ordenados de mayor a menor AUC.

In [83]:
dl_grid.get_grid(sort_by="auc", decreasing=True).sorted_metric_table()
Out[83]:
hidden model_ids auc
0 [256, 256] grid_dl_model_8 0.9156164678060841
1 [512, 512] grid_dl_model_9 0.9154743134968869
2 [64] grid_dl_model_1 0.915019637999322
3 [128, 128] grid_dl_model_7 0.9148857523214182
4 [256] grid_dl_model_3 0.9145502774125799
5 [128] grid_dl_model_2 0.9141037382482078
6 [512] grid_dl_model_4 0.9137826507578201
7 [64, 64] grid_dl_model_6 0.9136238268879353
8 [1024] grid_dl_model_5 0.9135063461745178

El modelo que consigue mayor AUC de validación es el que tiene una arquitectura de una única capa con 128 neuronas.

In [84]:
id_mejor_modelo = dl_grid.get_grid(sort_by="auc", decreasing=True).sorted_metric_table()['model_ids'][0]
mejor_dl_final = h2o.get_model(id_mejor_modelo)
In [85]:
# Cálculo de la métrica AUC para el conjunto de test.
performance = mejor_dl_final.model_performance(test_data = datos_test_h2o)
performance.auc()
Out[85]:
0.912259709394268

Detalles sobre todos los hiperparámetros del modelo final:

In [86]:
params_list = []
no_mostrar = ["model_id", "training_frame", "validation_frame"]

for key, value in mejor_dl_final.params.items():
    if key not in no_mostrar:
        params_list.append(str(key)+" = "+str(value['actual']))
params_list
Out[86]:
['nfolds = 0',
 'keep_cross_validation_models = True',
 'keep_cross_validation_predictions = False',
 'keep_cross_validation_fold_assignment = False',
 'fold_assignment = AUTO',
 'fold_column = None',
 "response_column = {'__meta': {'schema_version': 3, 'schema_name': 'ColSpecifierV3', 'schema_type': 'VecSpecifier'}, 'column_name': 'salario', 'is_member_of_frames': None}",
 'ignored_columns = None',
 'ignore_const_cols = True',
 'score_each_iteration = False',
 'weights_column = None',
 'offset_column = None',
 'balance_classes = False',
 'class_sampling_factors = None',
 'max_after_balance_size = 5.0',
 'max_confusion_matrix_size = 20',
 'max_hit_ratio_k = 0',
 'checkpoint = None',
 'pretrained_autoencoder = None',
 'overwrite_with_best_model = True',
 'use_all_factor_levels = True',
 'standardize = True',
 'activation = RectifierWithDropout',
 'hidden = [256, 256]',
 'epochs = 500.0',
 'train_samples_per_iteration = -2',
 'target_ratio_comm_to_comp = 0.05',
 'seed = -2452965180847888378',
 'adaptive_rate = True',
 'rho = 0.99',
 'epsilon = 1e-08',
 'rate = 0.005',
 'rate_annealing = 1e-06',
 'rate_decay = 1.0',
 'momentum_start = 0.0',
 'momentum_ramp = 1000000.0',
 'momentum_stable = 0.0',
 'nesterov_accelerated_gradient = True',
 'input_dropout_ratio = 0.0',
 'hidden_dropout_ratios = None',
 'l1 = 1e-05',
 'l2 = 1e-05',
 'max_w2 = 3.4028235e+38',
 'initial_weight_distribution = UniformAdaptive',
 'initial_weight_scale = 1.0',
 'initial_weights = None',
 'initial_biases = None',
 'loss = Automatic',
 'distribution = AUTO',
 'quantile_alpha = 0.5',
 'tweedie_power = 1.5',
 'huber_alpha = 0.9',
 'score_interval = 5.0',
 'score_training_samples = 10000',
 'score_validation_samples = 0',
 'score_duty_cycle = 0.1',
 'classification_stop = 0.0',
 'regression_stop = 1e-06',
 'stopping_rounds = 3',
 'stopping_metric = AUC',
 'stopping_tolerance = 0.01',
 'max_runtime_secs = 1.7976931348623157e+308',
 'score_validation_sampling = Uniform',
 'diagnostics = True',
 'fast_mode = True',
 'force_load_balance = True',
 'variable_importances = True',
 'replicate_training_data = True',
 'single_node_mode = False',
 'shuffle_training_data = False',
 'missing_values_handling = Skip',
 'quiet_mode = False',
 'autoencoder = False',
 'sparse = False',
 'col_major = False',
 'average_activation = 0.0',
 'sparsity_beta = 0.0',
 'max_categorical_features = 2147483647',
 'reproducible = False',
 'export_weights_and_biases = False',
 'mini_batch_size = 1',
 'categorical_encoding = AUTO',
 'elastic_averaging = False',
 'elastic_averaging_moving_rate = 0.9',
 'elastic_averaging_regularization = 0.001',
 'export_checkpoints_dir = None']

Interpretación de modelos

En muchas aplicaciones reales, conocer la importancia que tiene cada predictor dentro de un modelo no es suficiente, además, se necesita entender de qué forma influye cada uno en el valor de las predicciones.

En los modelos lineales, gracias a los coeficientes de regresión, es fácil conocer en qué dirección y magnitud influyen variaciones de un predictor en el valor predicho. Sin embargo, en modelos más complejos como GBM, Random Forest o redes neuronales, no es tan inmediato. Una estrategia para estos últimos son los gráficos PDP e ICE.

Gráficos PDP

Los gráficos PDP muestran cómo varían en promedio las predicciones a medida que se modifica el valor de un predictor, manteniendo constantes el resto. Pueden obtenerse con la función h2o.partialPlot() pasándole como argumentos un modelo (object), las observaciones con las que se ha entrenado el modelo (data) y el nombre de los predictores para los que se quiere obtener el modelo (cols).

Véase el resultado de calcular las gráficas PDP del predictor age en el modelo GBM ajustado anteriormente.

In [87]:
curva_pdp = mejor_gbm_final.partial_plot(
                data = datos_train_h2o,
                cols = ["age"],
                server=True,
                plot = True,
                figsize=(5, 3),
            )
curva_pdp
PartialDependencePlot progress: |█████████████████████████████████████████| 100%

PartialDependence: Partial Dependence Plot of model gbm_grid_2_model_16 on column 'age'.
age mean_response stddev_response std_error_mean_response
0 17.000000 0.115484 0.230286 0.001396
1 20.842105 0.115484 0.230286 0.001396
2 24.684211 0.148177 0.246346 0.001494
3 28.526316 0.189107 0.264386 0.001603
4 32.368421 0.226805 0.289465 0.001755
5 36.210526 0.247327 0.299920 0.001819
6 40.052632 0.274368 0.305391 0.001852
7 43.894737 0.285038 0.307763 0.001866
8 47.736842 0.296469 0.311510 0.001889
9 51.578947 0.294145 0.309822 0.001879
10 55.421053 0.289211 0.306408 0.001858
11 59.263158 0.289736 0.304598 0.001847
12 63.105263 0.262343 0.289679 0.001757
13 66.947368 0.225955 0.269632 0.001635
14 70.789474 0.228626 0.268813 0.001630
15 74.631579 0.227730 0.267821 0.001624
16 78.473684 0.230550 0.267261 0.001621
17 82.315789 0.229814 0.265337 0.001609
18 86.157895 0.244415 0.257288 0.001560
19 90.000000 0.237490 0.240676 0.001459
Out[87]:
[]

El gráfico PDP muestra que, la relación entre la edad y la probabilidad de ganar más de 50000 dólares anuales no es constante ni monotónica. En el intervalo de los 20-40 años, a medida que aumenta la edad, mayor la probabilidad promedio de que una persona supere ese salario. En el intervalo 40-60 se estabiliza y a partir de los 60 decae.

Stacked Ensembles

Introducción

A modo general, el término model ensembling hace referencia a la combinación de las predicciones de dos o más modelos distintos con el objetivo de mejorar las predicciones finales. Esta estrategia se basa en la asunción de que, distintos modelos entrenados independientemente, emplean distintos aspectos de los datos para realizar las predicciones, es decir, cada uno es capaz de identificar parte de la “verdad” pero no toda ella. Combinando la perspectiva de cada uno de ellos, se obtiene una descripción más detallada de la verdadera estructura subyacente en los datos. A modo de analogía, imagínese un grupo de estudiantes que se enfrentan a un examen multidisciplinal. Aunque todos obtengan aproximadamente la misma nota, cada uno de ellos habrá conseguido más puntos con las preguntas que tratan sobre las disciplinas en las que destacan. Si en lugar de hacer el examen de forma individual, lo hacen en grupo, cada uno podrá contribuir en los aspectos que más domina, y el resultado final será probablemente superior a cualquiera de los resultados individuales.

La clave para que el ensembling consiga mejorar los resultados es la diversidad de los modelos. Si todos los modelos combinados son similares entre ellos, no podrán compensarse unos a otros. Por esta razón, se tiene que intentar combinar modelos que sean lo mejor posible a nivel individual y lo más diferentes entre ellos.

Las formas más simples de combinar las predicciones de varios modelos es emplear la media para problemas de regresión y la moda para problemas de clasificación. Sin embargo existen otras aproximaciones más complejas capaces de conseguir mejores resultados:

  • Ponderar las agregaciones dando distinto peso a cada modelo, por ejemplo, en proporción al accuracy que han obtenido de forma individual.

  • Super Learner (stacked regression): emplear un nuevo modelo para que decida la mejor forma de combinar las predicciones de los otros modelos.

Algoritmo Super Learner

La implementación de Super Learner disponible en H2O sigue el siguiente algoritmo:

Definición del ensemble

1) Definir un listado con los algoritmos base (cada uno con los hiperparámetros pertinentes).

2) Definir el algoritmo de metalearning que defina como se entrena en modelo superior. Entre los metalearners disponibles están (GLM, GBM, Random Forest y Deep Learning).

Entrenamiento del ensemble

1) Entrenar cada uno de los algoritmos base con el conjunto de entrenamiento.

2) Realizar k-fold cross-validation con cada uno de los algoritmos base y almacenar las predicciones hechas en cada una de las k particiones.

3) Combinar las predicciones del paso 2 en una única matriz NxL (N = número de observaciones en el conjunto de entrenamiento, L = número de modelos base).

4) Entrenar el metalearning con la variable respuesta y la matriz NxL como predictores.

5) El Super learner final está formado por los modelos base y el modelo metalearning.

Predecir

1) Predecir la nueva observación con cada uno de los modelos base.

2) Emplear las predicciones de los modelos base como input del metalearner para obtener la predicción final.

Modelo

Se entrena un super learner empleando como algoritmos base un GLM y un GBM. Cada uno de los modelos base se entrena por separado empleando como hiperparámetros los valores óptimos identificados en los grid search de los apartados anteriores.

Es importante tener en cuenta que, en el entrenamiento de los modelos base, es necesario:

  • Todos los modelos deben de emplear el mismo número de particiones en la validación cruzada (mismo valor en el argumento nfolds.

  • Deben de almacenarse las predicciones hechas durante la validación cruzada (keep_cross_validation_predictions = True).

  • Todos los modelos deben ser entrenados sobre el mismo conjunto de entrenamiento. Las filas deben de ser las mismas aunque cada modelo puede utilizar distintos predictores (columnas).

In [88]:
from h2o.estimators.glm import H2OGeneralizedLinearEstimator
from h2o.estimators.gbm import H2OGradientBoostingEstimator
from h2o.estimators.stackedensemble import H2OStackedEnsembleEstimator

# Entrenamiento del modelo GLM
modelo_glm = H2OGeneralizedLinearEstimator(
                     family = "binomial",
                     link = "logit",
                     standardize = True,
                     balance_classes = False,
                     ignore_const_cols = True,
                     # Se especifica que hacer con observaciones incompletas
                     missing_values_handling = "Skip",
                     # Se hace una búsqueda del hiperparámetro lamba.
                     lambda_search = True,
                     # Selección automática del solver adecuado.
                     solver = "AUTO",
                     alpha = 0,
                     # Validación cruzada de 5 folds para estimar el error
                     # del modelo.
                     seed = 123,
                     nfolds = 5,
                     # Reparto estratificado de las observaciones en la creación
                     # de las particiones.
                     fold_assignment = "Stratified",
                     keep_cross_validation_predictions = True,
                     model_id = "modelo_binomial"
)

modelo_glm.train(
    y = var_respuesta,
    x = predictores,
    training_frame = datos_train_h2o
)


# Entrenamiento del modelo GBM
modelo_gbm = H2OGradientBoostingEstimator(
                      # Tipo de distribución (clasificación binaria)
                      distribution = "bernoulli",
                      # Número de árboles.
                      ntrees = 500,
                      # Complejidad de los árboles
                      max_depth = 5,
                      min_rows = 5,
                      # Aprendizaje
                      learn_rate = 0.1,
                      sample_rate = 1,
                      # Detención temprana
                      score_tree_interval = 5,
                      stopping_rounds = 3,
                      stopping_metric = "AUC",
                      stopping_tolerance = 0.001,
                      # Validación cruzada de 5 folds para estimar el error
                      # del modelo.
                      seed = 123,
                      nfolds = 5,
                      fold_assignment = "Stratified",
                      keep_cross_validation_predictions = True,
                      model_id = "modelo_gbm",
)

modelo_gbm.train(
    # Variable respuesta y predictores.
    y = var_respuesta,
    x = predictores,
    # Datos de entrenamiento.
    training_frame = datos_train_h2o
)
glm Model Build progress: |███████████████████████████████████████████████| 100%
gbm Model Build progress: |███████████████████████████████████████████████| 100%
In [89]:
# Entrenar el ensemble con los dos modelos anteriores.
modelo_superlearner = H2OStackedEnsembleEstimator(model_id="modelo_superlearner",
                                       base_models=[modelo_glm, modelo_gbm])
modelo_superlearner.train(
    x=predictores,
    y=var_respuesta,
    training_frame=datos_train_h2o
)
stackedensemble Model Build progress: |███████████████████████████████████| 100%
In [90]:
predicciones = modelo_superlearner.predict(test_data = datos_test_h2o)
predicciones.head(5)
stackedensemble prediction progress: |████████████████████████████████████| 100%
predict <=50K >50K
>50K 0.04129540.958705
>50K 0.04389050.95611
<=50K 0.92771 0.0722896
>50K 0.676282 0.323718
<=50K 0.939955 0.0600452
Out[90]:

In [91]:
# Cálculo de la métrica AUC para el conjunto de test.
performance = modelo_superlearner.model_performance(test_data = datos_test_h2o)
performance.auc()
Out[91]:
0.9271665991408301

Guardar-cargar un modelo H2O

Para guardar un modelo H2O ya entrenado se emplean la función h2o.save_model().

In [92]:
# Los modelos generados en un grid se almacenan por defecto con un model_id
# que identifica la posición que ocupa dentro del grid. Para almacenar el modelo 
# con un nombre más descriptivo, se sobreescribe su atributo model_id.
mejor_gbm_final.model_id = "modelo_glm"

# Se guarda el modelo en el directorio actual
h2o.save_model(model=mejor_gbm_final, path=os.getcwd(), force=True)
Out[92]:
'/home/ubuntu/environment/varios/machine_learning_con_H2O_python/modelo_glm'

Para cargar un modelo previamente guardado se emplea la función h2o.load_model().

In [93]:
modelo = h2o.load_model(os.path.join(os.getcwd(), "modelo_glm"))

Bibliografía

Nykodym, T., Kraljevic, T., Hussami, N., Rao, A., and Wang, A. (Nov 2017). Generalized Linear Modeling with H2O

Gradient Boosting Machine with H2O by Michal Malohlava & Arno Candel with assitance from Cliff Click, Hank Roark, & Viraj Parmar

Machine Learning with Python and H2O Pasha Stetsenko and Edited by: Angela Bartz

Dua, D. and Karra Taniskidou, E. (2017). UCI Machine Learning Repository [http://archive.ics.uci.edu/ml]. Irvine, CA: University of California, School of Information and Computer Science

Practical Machine Learning with H2O by Darren Cook

[Top 10 Deep Learning Tips and Tricks - Arno Candel](https://www.youtube.com/watch?v=LM255qs8Zsk)

¿Cómo citar este documento?

Machine learning con H2O y Python by Joaquín Amat Rodrigo, available under a Attribution 4.0 International (CC BY 4.0) at https://www.cienciadedatos.net/documentos/py04_machine_learning_con_h2o_y_python.html

Creative Commons Licence
This work by Joaquín Amat Rodrigo is licensed under a Creative Commons Attribution 4.0 International License.