FastAI Machine Learning (Lección 5-segunda parte)

Christian Tutivén Gálvez
Saturdays.AI
Published in
11 min readMay 2, 2019

En este articulo aprenderemos como interpretar las predicciones de nuestros árboles, como saber si nuestro dataset es aleatorio o temporal y obtendremos nuestro modelo final. Este resultado nos pondría en el primer puesto del concurso de Kaggle.

Interpretes de árboles

¿Qué hace el intérprete de árboles y cómo lo hace? Comencemos con la salida del intérprete de árbol. Aquí hay un solo árbol:

La raíz del árbol es antes de que haya alguna división. Entonces 10.189 es el logaritmo del precio promedio de registro de todas las opciones en nuestro conjunto de capacitación. Luego, si voy a Coupler_System ≤ 0.5, obtenemos un promedio de 10.345 (para el subconjunto de 16815 ejemplos). Fuera de las personas con Coupler_System ≤0.5, tomamos el subconjunto donde Enclosure ≤ 2.0 y el logaritmo del precio de registro promedio es 9.955. Luego el paso final es ModelID ≤ 4573.0 y eso nos da 10.226.

Si pasamos estos datos a una tabla de excel, y calculamos el cambio de cada uno de los valores del logaritmo de precios promedios, obtenemos la siguiente figura.

Entonces podemos calcular el cambio en el precio promedio de registro por cada criterio adicional. Podemos dibujar eso como lo que se llama waterfall plot. Waterfall plots son uno de los gráficos más útiles que se conocen y, por extraño que parezca, no hay nada en Python para hacerlos. Esta es una de esas cosas donde existe esa desconexión entre el mundo de la consultoría de gestión y los negocios donde todo el mundo usa waterfall plots todo el tiempo y la academia que no tiene idea de qué son estas cosas. Cada vez que se tiene un punto de partida, una serie de cambios y un punto de finalización, los gráficos en cascada son casi siempre la mejor manera de mostrarlos.

Con Excel 2016, está incorporado. Simplemente has clic en Insertar gráfico de cascada y ahí está. Si quieres ser un héroe, crea un paquete de gráficos en cascada para matplotlib, ponlo en pip y todos te amaran. Estos son realmente muy fáciles de construir. Básicamente, haces un gráfico de columnas apiladas donde la parte inferior es todo blanco. Puedes hacerlo, pero si puedes envolver eso, coloca los puntos en los lugares correctos y coloréalos muy bien, eso sería totalmente increíble. Creo que todos tienen las habilidades para hacerlo y serían una gran cosa para su cartera, te animas??.

Entonces, si solo estuviéramos haciendo un árbol de decisiones y alguien pregunta “¿por qué la predicción de esta subasta en particular fue este precio en particular?”, Así es como puede responder: “porque estas tres cosas tuvieron estos tres impactos”. Para un bosque aleatorio, podríamos hacer eso en todos los árboles.

from treeinterpreter import treeinterpreter as tidf_train, df_valid = split_vals(df_raw[df_keep.columns], n_trn)row = X_valid.values[None,0]; rowarray([[4364751, 2300944, 665, 172, 1.0, 1999, 3726.0, 3, 3232, 1111, 0, 63, 0, 5, 17, 35, 4, 4, 0, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2, 19, 29, 3, 2, 1, 0, 0, 0, 0, 0, 2010, 9, 37,
16, 3, 259, False, False, False, False, False, False, 7912, False, False]], dtype=object)
prediction, bias, contributions = ti.predict(m, row)

Entonces, cuando uses treeinterpreter.predict (como en el ejemplo mostrado arriba) con un modelo de bosque aleatorio para un ejemplo especifico de precio de la subasta (en este caso es la fila de índice cero), te dice:

  • prediction: igual a la predicción de bosque aleatorio.
  • bias: esto va a ser siempre el mismo: es el precio de venta promedio para cada una de las muestras aleatorias en el árbol
  • contributions: el total de todas las contribuciones para cada vez que vemos que esa columna específica aparece en un árbol.
prediction[0], bias[0](9.1909688098736275, 10.10606580677884)

El valor de la contribución de cada entidad en el conjunto de datos para esta primera fila:

idxs = np.argsort(contributions[0])[o for o in zip(df_keep.columns[idxs], df_valid.iloc[0][idxs], contributions[0][idxs])][('ProductSize', 'Mini', -0.54680742853695008),
('age', 11, -0.12507089451852943),
('fiProductClassDesc',
'Hydraulic Excavator, Track - 3.0 to 4.0 Metric Tons',
-0.11143111128570773),
('fiModelDesc', 'KX1212', -0.065155113754146801),
('fiSecondaryDesc', nan, -0.055237427792181749),
('Enclosure', 'EROPS', -0.050467175593900217),
('fiModelDescriptor', nan, -0.042354676935508852),
('saleElapsed', 7912, -0.019642242073500914),
('saleDay', 16, -0.012812993479652724),
('Tire_Size', nan, -0.0029687660942271598),
('SalesID', 4364751, -0.0010443985823001434),
('saleDayofyear', 259, -0.00086540581130196688),
('Drive_System', nan, 0.0015385818526195915),
('Hydraulics', 'Standard', 0.0022411701338458821),
('state', 'Ohio', 0.0037587658190299409),
('ProductGroupDesc', 'Track Excavators', 0.0067688906745931197),
('ProductGroup', 'TEX', 0.014654732626326661),
('MachineID', 2300944, 0.015578052196894499),
('Hydraulics_Flow', nan, 0.028973749866174004),
('ModelID', 665, 0.038307429579276284),
('Coupler_System', nan, 0.052509808150765114),
('YearMade', 1999, 0.071829996446492878)]

Así que una pequeña pieza de equipo industrial (ProductSize Mini) significaba que era menos costoso. Si se lo hizo hace poco (YearMade) significaba que era más caro, si era antiguo (age) lo hace más económico. Así que esto no te ayudará mucho con Kaggle, donde solo necesitas predicciones. Pero te va a ayudar mucho en un entorno de producción o incluso en la preproducción. Entonces, algo que un buen administrador debería hacer si le dices que aquí hay un modelo de aprendizaje automático que creo que deberíamos usar, es tomar un ejemplo de clientes reales o subastas reales y verificar si tu modelo parece intuitivo. Por ejemplo, si dices que mi predicción es que mucha gente realmente va a disfrutar de una mala película, y es como “wow, esa fue una película muy mala”, entonces volverán a ti y te dirán “explica por qué tu modelo me está diciendo que me va a gustar esta película porque odio esa película ”. Luego puedes volver atrás y decir bien, es porque te gusta esta película y porque tienes este rango de edad y género, y en promedio, a la gente le gustó esa película.

Esta es una técnica casi totalmente desconocida y esta biblioteca en particular también es casi totalmente desconocida. Así que es una gran oportunidad para mostrar algo que muchas personas no saben. Es totalmente crítico en mi opinión, pero rara vez se hace.

Extrapolation

El último paso que vamos a hacer antes de intentar construir nuestro propio bosque aleatorio es lidiar con este complicado problema de extrapolación. Entonces, en este caso, si observamos la precisión de nuestros árboles recientes, todavía tenemos una gran diferencia entre nuestro puntaje de validación y nuestro puntaje de entrenamiento.

Además, en este caso, la diferencia entre el OOB (0.89420) y la validación (0.89319) es bastante cercana. Aquí está el modelo más reciente:

Muy a menudo verás que hay una gran diferencia entre tu puntuación de validación y tu puntuación OOB, y quiero mostrarte cómo enfrentarías eso en particular porque sabemos que la puntuación OOB debería ser un poco peor porque usa menos árboles. Así que me da la sensación de que deberíamos hacerlo un poco mejor. La forma en que deberíamos poder hacerlo un poco mejor es manejar el componente de tiempo un poco mejor.

Aquí está el problema con los bosques aleatorios cuando se trata de extrapolación. Cuando tienes un conjunto de datos con cuatro años de datos de ventas, creas tu árbol y dice que está en alguna tienda en particular y en algún artículo en particular, este es el precio promedio. Y en realidad nos dice el precio promedio de todo el conjunto de entrenamiento que podría ser bastante antiguo. Entonces, cuando quieras dar un paso adelante hacia lo que será el precio el próximo mes, nunca se verá el próximo mes. En cualquier otro lugar con un modelo lineal, puedes encontrar una relación entre el tiempo y el precio donde, a pesar de que contamos con mucha información, cuando vaya y prediga algo en el futuro, puede extrapolar eso. Pero un bosque aleatorio no puede hacer eso. Así que hay algunas maneras de lidiar con esto, pero una manera simple es tratar de evitar el uso de variables de tiempo como predictores, si es que hay algo más que podríamos usar que nos de una mejor y fuerte relación de cómo en realidad funcionaria en el futuro.

Entonces, en este caso, lo que se debe hacer es averiguar cuál es la diferencia entre nuestro conjunto de validación y nuestro conjunto de entrenamiento. Si comprendo la diferencia entre nuestro conjunto de validación y nuestro conjunto de entrenamiento, eso me dice cuáles son los predictores que tienen un fuerte componente temporal y, por lo tanto, pueden ser irrelevantes cuando llegue al período de tiempo futuro.

Para averiguar qué variables dependen del tiempo, crearemos un modelo de bosque aleatorio que intente predecir si una fila en particular está en el conjunto de validación o no. Luego veremos qué variable tiene la mayor contribución para hacer una predicción exitosa.

Definiendo la variable objetivo “is valid”:

df_ext = df_keep.copy()
df_ext['is_valid'] = 1
df_ext.is_valid[:n_trn] = 0
x, y, nas = proc_df(df_ext, 'is_valid')

m = RandomForestClassifier(n_estimators=40, min_samples_leaf=3, max_features=0.5, n_jobs=-1, oob_score=True)
m.fit(x, y);
m.oob_score_
0.99998753505765037

Este es un gran truco en Kaggle porque a menudo no te dirán si el conjunto de pruebas es una muestra aleatoria o no. Así que puedes poner el conjunto de prueba y el conjunto de entrenamiento, crear una nueva columna llamada is_test y ver si puedes predecirlo. Si puede, no tiene una muestra aleatoria, lo que significa que debe averiguar cómo crear un conjunto de validación a partir de ella.

El modelo es capaz de separar los conjuntos de entrenamiento y de validación con un valor de R² 0.99998, por lo que no tengo una muestra aleatoria.

Calculamos las variables más importante

fi = rf_feat_importance(m, x)
fi[:10]

Entonces, si observo la importancia de las características, lo más importante es SalesID. Así que esto es realmente interesante. Nos dice muy claramente que SalesID no es un identificador aleatorio, pero probablemente es algo que se configura consecutivamente a medida que pasa el tiempo. SaleElapsed fue el número de días desde la primera fecha en nuestro conjunto de datos, así que no es sorprendente que también sea un buen predictor. Es interesante MachineID: claramente, cada máquina está siendo etiquetada con algún identificador consecutivo también, y luego hay una gran disminución de importancia, por lo que nos detendremos aquí.

Tomemos ahora los tres primeros y luego podemos ver sus valores tanto desde el conjunto de entrenamiento como desde el conjunto de validación.

feats=['SalesID', 'saleElapsed', 'MachineID']
(X_train[feats]/1000).describe()
(X_valid[feats]/1000).describe()

Podemos ver, por ejemplo, que SalesID en promedio es de 1,8 millones en el conjunto de capacitación y 5,8 millones en el conjunto de validación (observa que el valor está dividido por 1000). Así que puedes confirmar que son muy diferentes.

Así que vamos a eliminarlos.

x.drop(feats, axis=1, inplace=True)m = RandomForestClassifier(n_estimators=40, min_samples_leaf=3, max_features=0.5, n_jobs=-1, oob_score=True)
m.fit(x, y);
m.oob_score_
0.9789018385789966

Entonces, después de eliminarlos, veamos si puedo predecir si algo es del conjunto de validación. Todavía puedo con R² de 0,978.

fi = rf_feat_importance(m, x)
fi[:10]

Aunque estas variables son obviamente dependientes del tiempo, también pueden ser importantes para hacer las predicciones. Antes de eliminar estas variables, debemos comprobar cómo afectan la puntuación OOB. La puntuación inicial OOB en una muestra se calcula para la comparación:

set_rf_samples(50000)
feats=['SalesID', 'saleElapsed', 'MachineID', 'age', 'YearMade', 'saleDayofyear']
X_train, X_valid = split_vals(df_keep, n_trn)
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.21136509778791376, 0.2493668921196425, 0.90909393040946562, 0.88894821098056087, 0.89255408392415925]

Eliminando cada una de las variables de una en una:

for f in feats:

df_subs = df_keep.drop(f, axis=1)
X_train, X_valid = split_vals(df_subs, n_trn)
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(f)
print_score(m)
SalesID
0.20918653475938534, 0.2459966629213187, 0.9053273181678706, 0.89192968797265737, 0.89245205174299469]

saleElapsed
[0.2194124612957369, 0.2546442621643524, 0.90358104739129086, 0.8841980790762114, 0.88681881032219145]

MachineID
[0.206612984511148, 0.24446409479358033, 0.90312476862123559, 0.89327205732490311, 0.89501553584754967]

age
[0.21317740718919814, 0.2471719147150774, 0.90260198977488226, 0.89089460707372525, 0.89185129799503315]

YearMade
[0.21305398932040326, 0.2534570148977216, 0.90555219348567462, 0.88527538596974953, 0.89158854973045432]

saleDayofyear
[0.21320711524847227, 0.24629839782893828, 0.90881970943169987, 0.89166441133215968, 0.89272793857941679]

Y se puede ver que cuando elimino SalesID, mi puntuación aumenta. Esto es lo que esperábamos. Hemos eliminado una variable dependiente del tiempo, hubo otras variables que podrían encontrar relaciones similares sin la dependencia del tiempo. Así que eliminarlo causó que nuestra validación subiera. Ahora OOB no subió, porque esto es realmente un predictor estadísticamente útil, pero es dependiente del tiempo y tenemos un conjunto de validación dependiente del tiempo. Así que esto es realmente sutil pero puede ser realmente importante. Está tratando de encontrar las cosas que le dan una predicción generalizable a través del tiempo y aquí es cómo puedes verlo.

Debemos eliminar SalesID con seguridad, pero saleElapsed no mejoró, por lo que no queremos eliminarlo. MachineID mejoró: de 0.888 a 0.893, por lo que es un poco mejor. La edad mejoró un poco. YearMade empeoró, saleDayofyear un poco mejor.

reset_rf_samples()
df_subs = df_keep.drop(['SalesID', 'MachineID', 'saleDayofyear'],axis=1)
X_train, X_valid = split_vals(df_subs, n_trn)
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.1418970082803121, 0.21779153679471935, 0.96040441863389681, 0.91529091848161925, 0.90918594039522138]

Después de eliminar las variables dependientes del tiempo, la puntuación de validación (0.915) ahora es mejor que la puntuación OOB (0.909).

Ese fue un enfoque muy exitoso allí, y ahora podemos verificar la importancia de las características.

plot_fi(rf_feat_importance(m, X_train));

Ahora podemos jugar con otros parámetros como n_estimator en max_features. Para crear el modelo final, podemos aumentar el número de árboles a 160 y aquí están los resultados:

m = RandomForestRegressor(n_estimators=160, max_features=0.5, n_jobs=-1, oob_score=True)
%time m.fit(X_train, y_train)
print_score(m)
CPU times: user 6min 3s, sys: 2.75 s, total: 6min 6s
Wall time: 16.7 s
[0.08104912951128229, 0.2109679613161783, 0.9865755186304942, 0.92051576728916762, 0.9143700001430598]

Como puedes ver, hicimos todas nuestras interpretaciones, todos nuestros ajustes, básicamente con modelos / subconjuntos más pequeños y al final, ejecutamos todo el proceso. Y en realidad solo tomó 16 segundos y ahora el puntaje de validación es de 0.92 mientras que el RMSE mejora a 0.21. Ahora podemos comprobar eso contra Kaggle. Desafortunadamente, esta es una competencia más antigua y ya no se nos permite entrar para ver cómo habríamos ido. Entonces, lo mejor que podemos hacer es verificar si parece que podríamos haberlo hecho bien en función de su conjunto de validación, por lo que debería estar en el área correcta. Basado en eso, habríamos quedado primeros.

Creo que esta es una interesante serie de pasos. Así que puedes seguir la misma serie de pasos en tus proyectos Kaggle y, lo que es más importante, tus proyectos del mundo real. Uno de los desafíos es que una vez que abandones este entorno de aprendizaje, de repente te encuentres rodeado de personas que nunca tienen suficiente tiempo, siempre quieren que tengas prisa, siempre te dicen que hagas esto y luego que hagas eso. Debes encontrar el tiempo para alejarte y regresar, ya que este es un proceso de modelado real que puedes usar. Y da resultados de clase mundial, lo digo en serio. El chico que ganó esto, Leustagos, lamentablemente falleció, pero es el mejor competidor de Kaggle de todos los tiempos. Ganó, creo que docenas de competiciones, así que si podemos obtener un puntaje incluso mejor que él, entonces lo estamos haciendo muy bien.

Con esto concluimos nuestro modelo de Random Forest usando FastAI y podemos ver que con pocas líneas de código podemos tener un buen modelo en producción.

Espero que te haya gustado y sea de utilidad. Puedes dejar tus aplausos y recuerda, puedes dejar hasta 50 =)

--

--