FastAI Machine Learning (Lección 2)

Christian Tutivén Gálvez
Saturdays.AI
Published in
10 min readFeb 8, 2019

Ahora que sabemos cómo crear un modelo de bosque aleatorio (Random Forest) en un Jupyter Notebook usando FastAI, es igualmente importante comprender cómo funciona realmente este código. Hemos visto que Random Forest para ciertos DataSets funciona muy bien, sin muchos problemas pero aún no sabemos como realmente funciona, que podemos hacer si no funciona adecuadamente, sus ventajas y desventajas y qué parámetros podemos modificar, por lo que en esta lección veremos todo esto y luego como interpretar los resultados del modelo de Random Forest, no solo para obtener predicciones sino que para profundizar el conocimiento de nuestros datos.

Analizando R²

Primero analicemos el score que usamos para medir si nuestro modelo es bueno o no.

SSregression, es la suma de los cuadrados de (valor real — predicción).
SStotal, es la suma de los cuadrados de (valor real — promedio)
El valor de R² puede ser cualquier valor menor que 1. Si el cuadrado R² es negativo, significa que su modelo es peor que la predicción de la media.

Observamos en la primera lección que el modelo funciona extremadamente bien con los datos de entrenamiento (los puntos con los que se entreno) pero cae cuando se prueba en el conjunto de validación (el conjunto con el cual no fue entrenado el modelo). Ahora, entendamos cómo creamos el conjunto de validación y por qué es tan crucial.

Creando un conjunto de validación (Repaso de lección 1)

Crear un buen conjunto de validación es una de las tareas más importantes en el aprendizaje automático. El puntaje de validación es representativo de cómo se desempeña nuestro modelo en datos del mundo real o en los datos de prueba.

Hay que tener en cuenta que si hay un componente de tiempo involucrado, entonces las filas más recientes deben incluirse en el conjunto de validación. Esto es debido a que en el concurso se desea predecir precios en el futuro, no precios en determinados meses. Por lo tanto, nuestro conjunto de validación debe tener las mismas propiedades que el conjunto de prueba. El conjunto de prueba del concurso tiene 12,000 filas por lo que nuestro conjunto de validación será del mismo tamaño (las últimas 12,000 filas de los datos de entrenamiento).

def split_vals(a,n): return a[:n].copy(), a[n:].copy()

n_valid = 12000
n_trn = len(df)-n_valid
raw_train, raw_valid = split_vals(df_raw, n_trn)
X_train, X_valid = split_vals(df, n_trn)
y_train, y_valid = split_vals(y, n_trn)

Los puntos de datos de 0 a (longitud — 12000) se almacenan como el conjunto de entrenamiento (x_train, y_train).

Para ver las dimensiones de nuestro conjuntos ejecutamos

X_train.shape, y_train.shape, X_valid.shape

Se muestra

((389125, 66), (389125,), (12000,66))

Creamos nuestra función para poder trabajar con la métrica RMSLE

def rmse(x,y): return math.sqrt(((x-y)**2).mean())def print_score(m):
res = [rmse(m.predict(X_train), y_train),
rmse(m.predict(X_valid), y_valid),
m.score(X_train, y_train), m.score(X_valid,
y_valid)]
if hasattr(m, 'oob_score_'): res.append(m.oob_score_)
print(res)

El modelo se construye utilizando el conjunto de entrenamiento y su rendimiento se mide tanto en el conjunto de entrenamiento como en los conjuntos de validación. Volvemos a ejecutar el entrenamiento con Random Forest.

m = RandomForestRegressor(n_jobs=-1)
%time m.fit(X_train, y_train)
print_score(m)

Obteniendo la siguiente salida:

CPU times: user 1min 3s, sys: 356 ms, total: 1min 3s 
Wall time: 8.46 s
[0.09044244804386327, 0.2508166961122146, 0.98290459302099709, 0.86765316048270615]

Del código anterior, obtenemos los resultados:

  • RMSE en el conjunto de entrenamiento
  • RMSE en el conjunto de validación
  • R² en el conjunto de entrenamiento
  • R² en el conjunto de validación

Está claro que el modelo esta demasiado ajustado al conjunto de datos de entrenamiento. Aun así obtenemos un 0.2508 en el RMSE en el conjunto de validación, lo que nos pone en el top-25% del concurso de Kaggle.

Además se puede observar que nos tomo 8.46 segundos para entrenar el modelo. Como estamos usando varias CPUs en esta computadora, se muestra que el tiempo total es de 1min 3s. ¿Podemos reducir el tiempo de entrenamiento? ¡Si podemos!.

Lección 2

Desde aquí en adelante, todo es nuevo en la lección 2.

Acelerando las cosas

Una manera de acelerar el proceso es pasarle a la función “proc_df” el parámetro “subset”. Esto creara una muestra aleatoria de los datos.

df_trn, y_trn, nas = proc_df(df_raw, 'SalePrice', subset=30000, na_dict=nas)
X_train, _ = split_vals(df_trn, 20000)
y_train, _ = split_vals(y_trn, 20000)

Se ha creado un subconjunto de 30,000 muestras, de las cuales tomamos 20,000 para entrenar el modelo de Random Forest.

Entrenamos nuestro nuevo modelo

m = RandomForestRegressor(n_jobs=-1)
%time m.fit(X_train, y_train)
print_score(m)

obteniendo

CPU times: user 3.89 s, sys: 16 ms, total: 3.88 s 
Wall time: 621 ms
[0.11794430422595521, 0.28881319691543633, 0.970812018265333525, 0.8510357543674435]

Se puede observar que el tiempo se redujo a 621 ms. Muy bien verdad!!

Construyendo un árbol sencillo

Bosque aleatorio es un grupo de árboles que se llaman estimadores. El número de árboles en un modelo de bosque aleatorio está definido por el parámetro n_estimator. Primero veremos un solo árbol (n_estimator = 1) con una profundidad máxima de 3 (max_depth=3).

m = RandomForestRegressor(n_estimators=1, max_depth=3, bootstrap=False, n_jobs=-1)
m.fit(X_train, y_train)
print_score(m)

Pasando estos parámetros creamos un pequeño árbol, obteniendo los siguientes resultados:

[0.5214347423221267, 0.5813295482070254, 0.41033418186185866, 0.3964784664823804]

Podemos observar que nuestros R² empeoro, por lo que este no es un buen modelo, pero es un modelo que podemos dibujar.

Dibujando el árbol:

draw_tree(m.estimators_[0], df_trn, precision=3)

Un árbol consiste de una secuencia de decisiones binarias, como se puede ver en la figura. Cada bloque consta del número de ejemplos usados, el promedio del logaritmo de los precios y si construimos un modelo donde usamos el valor promedio todo el tiempo, cuanto sería el Error cuadrático medio (MSE). Es decir, muestra el modelo más básico que puede predecir el promedio.

Mirando la primera casilla, la primera división es en el valor de Coupler_Systems menor o igual a 0.5 o mayor a 0.5. Del mismo modo, la siguiente división es en el valor de Yearmade.

Para el primer cuadro (Coupler_Systems), se crea un modelo utilizando solo el valor promedio (10.189). Esto significa que todas las filas tienen un valor predicho de 10.189 y el MSE para estas predicciones es 0.459. En cambio, si hacemos una división y separamos las filas basadas en coupler_system <0.5, el MSE se reduce a 0.414 para muestras que satisfacen la condición (verdadero) y 0.109 para las muestras restantes.

Entonces, ¿cómo decidimos en qué variable dividir? La idea es dividir los datos en dos grupos que sean tan diferentes entre sí como sea posible. Esto se puede hacer al verificar cada posible punto de división para cada variable, y luego averiguar cuál da el MSE más bajo. Para hacer esto, podemos tomar un promedio ponderado de los dos valores de MSE después de la separación. La división se detiene cuando alcanza el valor max_depth preespecificado o cuando cada nodo hoja solo tiene un valor.

Vamos a ver qué pasa si creamos un árbol más grande.

m = RandomForestRegressor(n_estimators=1, bootstrap=False, n_jobs=-1)
m.fit(X_train, y_train)
print_score(m)

Obteniendo

[6.526751786450488e-17, 0.5277160421601815, 1.0, 0.5026655229333343]

¡El resultado del set de entrenamiento se ve genial! Pero el conjunto de validación es peor que nuestro modelo original. Esta es la razón por la que necesitamos utilizar la acumulación de varios árboles para obtener resultados más generalizables.

Tenemos un modelo básico, un solo árbol, pero este no es un muy buen modelo. Necesitamos algo un poco más complejo que se construya sobre esta estructura. Para crear un bosque, utilizaremos una técnica estadística llamada bagging.

Bagging

En la técnica de bagging, creamos múltiples modelos, cada uno con predicciones que no están correlacionadas con la otra. Luego promediamos las predicciones de estos modelos. Random Forest es una técnica de bagging.

Si todos los árboles creados son similares entre sí y dan predicciones similares, entonces promediar estas predicciones no mejorará el rendimiento del modelo. En su lugar, podemos crear múltiples árboles en un subconjunto diferente de datos, de modo que incluso si estos árboles se adaptan, lo harán en un conjunto diferente de puntos. Estas muestras se toman con reemplazo.

En palabras simples, creamos múltiples modelos (con datos aleatorios) de bajo rendimiento y los promediamos para crear un buen modelo. Los modelos individuales deben ser lo más predictivos posible, pero juntos no deben estar correlacionados.

Ahora aumentaremos el número de estimadores en nuestro bosque aleatorio y veremos los resultados.

m = RandomForestRegressor(n_jobs=-1)
m.fit(X_train, y_train)
print_score(m)

Si no le damos un valor al parámetro n_estimator, se toma como 10 por defecto. Luego de ejecutar Random Forest, cada árbol se almacenó en el atributo “m.estimators_”.

preds = np.stack([t.predict(X_valid) for t in m.estimators_])

Lo que se creo fue una lista de todas las predicciones de cada uno de los arboles. Obtendremos predicciones de cada uno de los 10 árboles. Además, np.stack se utilizará para concatenar las predicciones una sobre la otra.

Para ver las dimensiones de nuestras predicciones

preds.shape

Podemos ver que las dimensiones de las predicciones son (10, 12000). Esto significa que tenemos 10 predicciones para cada fila en el conjunto de validación.

Ahora, para comparar los resultados de nuestro modelo con el conjunto de validación, aquí está la fila de predicciones, la media de las predicciones y el valor real del conjunto de validación.

preds[:,0], np.mean(preds[:,0]), y_valid[0]

El valor real es 9.10 pero ninguna de nuestras predicciones se acerca a este valor. Al tomar el promedio de todas nuestras predicciones obtenemos 9.07, que es una mejor predicción que cualquiera de los árboles individuales.

(array([ 9.21034,  8.9872 , 8.9872 , 8.9872 ,  8.9872 , 9.21034, 8.92266, 9.21034,  9.21034, 8.9872 ]),
9.0700003890739005,
9.1049798563183568)

Siempre es una buena idea visualizar tu modelo lo más posible. Aquí hay una gráfica que muestra la variación en el valor de R² a medida que aumenta el número de árboles.

plt.plot([metrics.r2_score(y_valid, np.mean(preds[:i+1], axis=0)) for i in range(10)]);

Como se esperaba, R² mejora a medida que aumenta el número de árboles. Puede experimentar con el valor n_estimator y ver cómo cambia el valor R² con cada iteración. Notarás que después de un cierto número de árboles, el valor de R² es plano.

Puntuación de fuera de bolsa (OOB)

Crear un conjunto de validación separado para un conjunto de datos pequeño puede ser potencialmente un problema, ya que resultará en un conjunto de entrenamiento aún más pequeño. En tales casos, podemos usar los puntos de datos (o muestras) en los que no se entrenó el árbol.

Para esto, establecemos el parámetro oob_score = True.

m = RandomForestRegressor(n_estimators=40, n_jobs=-1, oob_score=True)
m.fit(X_train, y_train)
print_score(m)

[0.10198464613020647, 0.2714485881623037, 0.9786192457999483, 0.86840992079038759, 0.84831537630038534]

Se puede observar que tenemos un nuevo valor al final, el cual es el oob_score. El oob_score es 0.84, que es similar al del conjunto de validación.

Veamos algunas otras técnicas interesantes mediante las cuales podemos mejorar nuestro modelo.

Submuestreo

Anteriormente, creamos un subconjunto de 30,000 filas y el conjunto de entrenamiento fue elegido al azar de este subconjunto. Como alternativa, por qué no usar el conjunto completo de entrenamiento y crear un subconjunto diferente cada vez para que el modelo sea entrenado en una gran parte de los datos. Suena lógico, verdad?

df_trn, y_trn, nas = proc_df(df_raw, 'SalePrice')
X_train, X_valid = split_vals(df_trn, n_trn)
y_train, y_valid = split_vals(y_trn, n_trn)
set_rf_samples(20000)

Usamos set_rf_sample para especificar el tamaño de la muestra. Vamos a comprobar si el rendimiento del modelo ha mejorado o no.

m = RandomForestRegressor(n_estimators=40, n_jobs=-1, oob_score=True)
m.fit(X_train, y_train)
print_score(m)

[0.2317315086850927, 0.26334275954117264, 0.89225792718146846, 0.87615150359885019, 0.88097587673696554]

Obtenemos una puntuación de validación de 0,876. Hasta ahora, hemos trabajado en un subconjunto de una muestra. Podemos ajustar este modelo en todo el conjunto de datos (¡pero llevará mucho tiempo ejecutarlo dependiendo de qué tan buenos sean sus recursos computacionales!).

Otros Hyper-parámetros para experimentar y sintonizar

Hoja de muestra mínima

Esto puede ser tratado como el criterio de parada para el árbol. El árbol deja de crecer (o dividirse) cuando el número de muestras en el nodo de hoja es menor que el especificado.

m = RandomForestRegressor(n_estimators=40, min_samples_leaf=3,n_jobs=-1, oob_score=True)
m.fit(X_train, y_train)
print_score(m)

[0.11595869956476182, 0.23427349924625201, 0.97209195463880227, 0.90198460308551043, 0.90843297242839738]

Aquí hemos especificado min_sample_leaf como 3. Esto significa que el número mínimo de muestras en el nodo debe ser 3 para cada división. Vemos que el R² ha mejorado para el conjunto de validación y se ha reducido en el conjunto de prueba, concluyendo que el modelo no se adapta demasiado a los datos de entrenamiento.

Característica máxima

Otro parámetro importante en el bosque aleatorio es max_features. Hemos discutido previamente que los árboles individuales deben estar lo menos correlacionados posible. Para el mismo, el bosque aleatorio utiliza un subconjunto de filas para entrenar cada árbol. Además, también podemos usar un subconjunto de columnas (características) en lugar de usar todas las características. Esto se logra ajustando el parámetro max_features.

m = RandomForestRegressor(n_estimators=40, min_samples_leaf=3, max_features=0.5, n_jobs=-1, oob_score=True) 
m.fit(X_train, y_train)
print_score(m)

[0.11926975747908228, 0.22869111042050522, 0.97026995966445684, 0.9066000722129437, 0.91144914977164715]

La configuración de max_features ha mejorado ligeramente la puntuación de validación. Aquí, max_features se establece en 0.5, lo que significa que se utiliza el 50% de las variables para cada división.

Pues ahora tenemos otra mejora a 0.906 en nuestro conjunto de validación y un R² de 0.22.

Los documentos de sklearn muestran un ejemplo de diferentes métodos de max_features con un número creciente de árboles; como puedes ver, usar un subconjunto de funciones en cada división requiere usar más árboles, pero resulta en mejores modelos:

Conclusiones

No podemos comparar nuestros resultados directamente con la competencia Kaggle, ya que usó un conjunto de validación diferente (y no podemos enviarnos a esta competencia ya que esta cerrada), pero al menos podemos ver que estamos obteniendo resultados similares a los ganadores basados en el conjunto de datos que tenemos.

--

--