FastAI Machine Learning (Lección 4)

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

Continuamos analizando nuestros datos usando el modelo de predicción Random Forest. Puedes leer la lección 3 aquí.

Codificación One-Hot

Recordemos la lección 1 de FastAI Machine Learning, donde nos encontramos con el problema de que teníamos muchas variables categóricas, las cuales muchos modelos no aceptan. Usando proc_df , convertimos las variables categóricas en columnas numéricas. Por ejemplo, tenemos una variable UsageBand , que tiene tres niveles: “Alto”, “Bajo” y “Medio”. Reemplazamos estas categorías con números 0, 1 y 2.

Si UsageBand tuviera filas con un valor llamado unknow, con qué número se podría reemplazar, respetando el orden?. ¿Seguramente debe haber otra forma de manejar esto?

En lugar de convertir estas categorías en números, podemos crear columnas separadas para cada categoría. La columna UsageBand se puede reemplazar con tres columnas:

  • UsoBand_low
  • UsoBand_medium
  • UsoBand_high

Cada uno de estos tiene 1s y 0s como los valores. Esto se llama una codificación one-hot.

¿Qué pasa cuando hay más de 3 categorías? ¿Qué pasa si tenemos más de 10? Tomemos un ejemplo para entender esto.

Supongamos que tenemos una columna ‘ zip_code ‘ en el conjunto de datos que tiene un valor único para cada fila. El uso de la codificación one-hot aquí no será beneficioso para el modelo y terminará aumentando el tiempo de ejecución (un escenario de pérdida-pérdida).

Usando proc_df en fastai, podemos realizar una codificación instantánea pasando un parámetro max_n_cat . Aquí, hemos establecido el max_n_cat = 7 , lo que significa que las variables que tienen niveles más de 7 (como el zip_code) no se codificarán, mientras que todas las demás variables se codificarán en un solo lugar.

df_trn2, y_trn, nas = proc_df(df_raw, 'SalePrice', max_n_cat=7)
X_train, X_valid = split_vals(df_trn2, n_trn)
m = RandomForestRegressor(n_estimators=40, min_samples_leaf=3, max_features=0.6, n_jobs=-1, oob_score=True)
m.fit(X_train, y_train)
print_score(m)

Esto puede ser útil para determinar si un nivel particular en una columna particular es importante o no. Ya que hemos separado cada nivel para las variables categóricas, podremos verlas también en el gráfico de características importantes. De nuevo FastAI nos facilita la vida y tiene una función a la medida.

fi = rf_feat_importance(m, df_trn2)
plot_fi(fi[:25]);

Anteriormente, YearMade era la función más importante en el conjunto de datos, pero Enclosure_EROPS w AC tiene una importancia más importante en esta nueva tabla. Interesante verdad?

Eliminar características redundantes

Hasta ahora, hemos entendido que tener una gran cantidad de características puede afectar el rendimiento del modelo y también dificultar la interpretación de los resultados. En esta sección, veremos cómo podemos identificar las características redundantes y eliminarlas de los datos.

Usaremos de clustering jerárquico (dendrograma), para identificar variables similares. En esta técnica, observamos cada objeto e identificamos cuál de ellos es el más cercano en términos de características. Estas variables son reemplazadas por su punto medio. Para entenderlo mejor, echemos un vistazo al diagrama de cluster de nuestro conjunto de datos.

Primero importamos

from scipy.cluster import hierarchy as hccorr = np.round(scipy.stats.spearmanr(df_keep).correlation, 4)
corr_condensed = hc.distance.squareform(1-corr)
z = hc.linkage(corr_condensed, method=’average’)
fig = plt.figure(figsize=(16,10))
dendrogram = hc.dendrogram(z, labels=df_keep.columns, orientation=’left’, leaf_font_size=16)
plt.show()

Podemos ver que las variables SaleYear y SaleElapsed son muy similares entre sí y tienden a representar lo mismo. De manera similar, Grouser_Tracks , Hydraulics_Flow y Coupler_System están altamente correlacionados. Lo mismo sucede con ProductGroup y ProductGroupDesc y finalmente fiBaseModel con fiModelDesc . Eliminaremos cada una de estas funciones una por una y veremos cómo afecta el rendimiento del modelo.

Primero, definimos una función para calcular la puntuación de fuera de bolsa (OOB) (para evitar repetir las mismas líneas de código):

def get_oob(df):
m = RandomForestRegressor(n_estimators=30, min_samples_leaf=5, max_features=0.6, n_jobs=-1, oob_score=True)
x, _ = split_vals(df, n_trn)
m.fit(x, y_train)
return m.oob_score_

A continuación se muestra el puntaje original de OOB antes de eliminar cualquier función:

get_oob(df_keep)
0.89019425494301454

Ahora eliminamos una variable a la vez y calcularemos la puntuación:

for c in (‘saleYear’, ‘saleElapsed’, ‘fiModelDesc’, ‘fiBaseModel’, ‘Grouser_Tracks’, ‘Coupler_System’):
print(c, get_oob(df_keep.drop(c, axis=1)))

Esto no ha afectado en gran medida la puntuación OOB, lo que quiere decir que eliminar una de estas variables redundantes no afecta en mucho a nuestro modelo. Ahora eliminemos una variable de cada par y verifiquemos la puntuación general:

to_drop = [‘saleYear’, ‘fiBaseModel’, ‘Grouser_Tracks’]
get_oob(df_keep.drop(to_drop, axis=1))
0.8886129948554602

La puntuación ha cambiado de 0.8901 a 0.8885. Utilizaremos estas características seleccionadas en el conjunto de datos completo y veremos cómo funciona nuestro modelo:

df_keep.drop(to_drop, axis=1, inplace=True)
X_train, X_valid = split_vals(df_keep, n_trn)
reset_rf_samples()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)

Obtenemos

[0.12615142089579687, 0.22781819082173235, 0.96677727309424211, 0.90731173105384466, 0.9084359846323049]

Una vez que estas variables se eliminan del marco de datos original, el puntaje del modelo resulta ser 0.907 en el conjunto de validación, con un modelo más pequeño y simple.

Dependencia parcial

Existe otra técnica que tiene el potencial de ayudarnos a comprender mejor los datos. Esta técnica se llama Dependencia parcial y se usa para averiguar cómo se relacionan las características con la variable objetivo.

from pdpbox import pdp
from plotnine import *
set_rf_samples(50000)df_trn2, y_trn, nas = proc_df(df_raw, 'SalePrice', max_n_cat=7)
X_train, X_valid = split_vals(df_trn2, n_trn)
m = RandomForestRegressor(n_estimators=40, min_samples_leaf=3, max_features=0.6, n_jobs=-1)
m.fit(X_train, y_train);
plot_fi(rf_feat_importance(m, df_trn2)[:10]);

Comparemos YearMade y SalePrice . Si creas un diagrama de dispersión para YearMade y SaleElapsed , notarías que algunos vehículos fueron creados en el año 1000, lo cual no es prácticamente posible.

df_raw.plot(‘YearMade’, ‘saleElapsed’, ‘scatter’, alpha=0.01, figsize=(10,8));

Estos podrían ser los valores que inicialmente faltaban y se han reemplazado con 1,000. Para mantener las cosas prácticas, nos centraremos en los valores superiores a 1930 para la variable YearMade.

x_all = get_sample(df_raw[df_raw.YearMade>1930], 500)

Creamos un gráfico utilizando el popular paquete ggplot .

import skmisc
ggplot(x_all, aes(‘YearMade’, ‘SalePrice’))+stat_smooth(se=True, method=’loess’)

Esta gráfica muestra que el precio de venta es mayor para los vehículos fabricados más recientemente, excepto por una caída entre 1991 y 1997. Podría haber varias razones para esta caída: la recesión, los vehículos preferidos por los clientes de menor precio o algún otro factor externo. Para entender esto, crearemos una gráfica que muestre la relación entre YearMade y SalePrice , dado que todos los demás valores de características son los mismos, es decir que si vendemos algo en 1990 versus 1980 y fue vendido exactamente lo mismo, la misma persona fue la que lo compro, la misma opción, etc. , entonces cuál sería la diferencia en precios. Para esto usamos el gráfico de dependencia parcial.

x = get_sample(X_train[X_train.YearMade>1930], 500)

def plot_pdp(feat, clusters=None, feat_name=None):
feat_name = feat_name or feat
p = pdp.pdp_isolate(m, x, x.columns, feat)
return pdp.pdp_plot(p, feat_name, plot_lines=True,
cluster=clusters is not None,
n_cluster_centers=clusters)
plot_pdp ('YearMade')

Esta trama se obtiene al fijar el Año Hecho para cada fila en 1960, luego en 1961, y así sucesivamente. En palabras simples, tomamos un conjunto de filas y calculamos el Precio de Venta para cada fila cuando YearMade es 1960. Luego, tomamos todo el conjunto de nuevo y calculamos el Precio de Venta al establecer YearMade en 1962. Repetimos esto varias veces, lo que resulta en las múltiples líneas azules Lo vemos en la trama anterior. La línea negra oscura representa el promedio. Esto confirma nuestra hipótesis de que el precio de venta aumenta para los vehículos fabricados más recientemente.

Ahora podemos hacer un análisis de cluster, para ver los cinco más omunes comportamientos que tenemos.

plot_pdp(‘YearMade’, clusters=5)

Podemos que algunos tipos de vehículos llegan a un tiempo que los precios son más planos a lo largo del tiempo, otros que exactamente lo opuesto, etc. Quizás esta información no nos ayude a mejorar el modelo pero si nos sirve para entender el comportamiento real de los datos. Muchas veces queremos predecir cosas pero también es importante entender que podemos cambiar en la forma de que hacemos negocios, marketing, logística, etc. y ahí es que es importante entender como están relacionadas nuestras variables unas con otras.

Podemos hacer lo mismo en un gráfico de dependencia parcial (PDP Interaction Plot). Lo que deseamos ver es como saleElapsed y YearMade juntas impactan al precio.

## Line added to draw pdp_interact properly
!sed -i ‘251s/.* / inter_ax.clabel(c2, /g’ /usr/local/lib/python3.6/dist-packages/pdpbox/pdp_plot_utils.py
feats = [‘saleElapsed’, ‘YearMade’]
p = pdp.pdp_interact(m, x, x.columns, feats)
pdp.pdp_interact_plot(p, feats)

Podemos ver que los precios más altos son en los mas recientes YearMade y el menor SaleElapsed.

Realizando el mismo paso para las categorías en Enclosure (dado que Enclosure_EROPS w AC demostró ser una de las características más importantes), el gráfico resultante se ve así:

plot_pdp([‘Enclosure_EROPS w AC’, ‘Enclosure_EROPS’, ‘Enclosure_OROPS’], 5, ‘Enclosure’)

Enclosure_EROPS w AC parece tener un precio de venta más alto en comparación con las otras dos variables (que tienen valores casi iguales). Entonces, ¿qué en el mundo es EROPS? Es una estructura protectora antivuelco cerrada que puede ser con o sin AC. Y, obviamente, EROPS con un AC tendrá un precio de venta más alto.

Otro ejemplo:

df_raw.YearMade[df_raw.YearMade<1950] = 1950
df_keep[‘age’] = df_raw[‘age’] = df_raw.saleYear-df_raw.YearMade
X_train, X_valid = split_vals(df_keep, n_trn)
m = RandomForestRegressor(n_estimators=40, min_samples_leaf=3, max_features=0.6, n_jobs=-1)
m.fit(X_train, y_train)
plot_fi(rf_feat_importance(m, df_keep));

Podemos ver que con estas condiciones los años del equipo son la variable con mayor peso. No podemos asumir que esto nos ayudara a obtener un mejor modelo, tan solo es para explorar los datos.

Intérprete de árboles

Intérprete de árbol en otra técnica interesante que analiza cada fila individual en el conjunto de datos. Hemos visto hasta ahora cómo interpretar un modelo, y cómo cada característica (y los niveles en cada característica categórica) afectan las predicciones del modelo. Ahora usaremos este concepto de intérprete de árbol y visualizaremos las predicciones para una fila en particular.

Importemos la biblioteca del intérprete de árbol y evaluemos los resultados de la primera fila en el conjunto de validación.

from treeinterpreter import treeinterpreter as tidf_train, df_valid = split_vals(df_raw[df_keep.columns], n_trn)row = X_valid.values[None,0]; row

Obteniendo

array([[ 1999, 5, 0, 17, 665, 1284595200, 0, 1, 
3232, 4364751, 0, 2300944, 0, 4, 4, 0,
0, 16, 35, 259, 12, 11]])

Estos son los valores originales para la primera fila (y cada columna) en el conjunto de validación. Usando el intérprete de árbol, haremos predicciones para el mismo usando un modelo de bosque aleatorio. El intérprete de árboles da tres resultados: predicción, sesgo y contribución.

  • Las predicciones son los valores predichos por el modelo de bosque aleatorio
  • El sesgo es el valor promedio de la variable objetivo para el conjunto de datos completo
  • Las contribuciones son la cantidad en la cual el valor predicho fue cambiado por cada columna

El valor de Coupler_System <0.5 aumentó el valor de 10.189 a 10.345 y Enclosure menos de 0.2 redujo el valor de 10.345 a 9.955, y así sucesivamente. Así que las contribuciones representarán este cambio en los valores predichos.

Imprimiendo la predicción y el sesgo de la primera fila en nuestro conjunto de validación

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:

[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)]

Por ahora terminamos este análisis, pero recuerden que seguiremos con el resto de lecciones del curso de Machine Learning de FastAI. Espero les haya sido de gran ayuda.

--

--