sábado, 19 de diciembre de 2020

Seleccionar y entrenar un modelo de Machine Learning

Ya tenemos enmarcado nuestro problema y nuestros datos adaptados para los algoritmos de Machine Learning.  Ahora sólo falta alimentar nuestro modelo con estos datos.
Vamos a comenzar con el modelo más sencillo, una regresión lineal. Para ello utilizaremos la función LinearRegression de Scikit-Learn y los datasets adaptados en el post anterior datos_preparados y datos_num_tr

from sklearn.linear_model import LinearRegression
lin_reg = LinearRegression()
lin_reg.fit(datos_preparados, datos_num_tr)

Ahora intentaremos entrenar el modelo con unas pocas instancias de nuestro dataset

# aquí pasamos el pipeline sobre unas pocas instancias de entrenamiento
algunos_datos = datos.iloc[:5]
algunos_datos_num = datos_num.iloc[:5]
algunos_datos_preparados= full_pipeline.transform(algunos_datos)
print("Predicciones:", lin_reg.predict(algunos_datos_preparados))

con esto obtenemos una matriz de predicciones


Ahora vamos a probar  el error cuadrático sobre todo el conjunto de datos

from sklearn.metrics import mean_squared_error
datos_predicciones = lin_reg.predict(datos_preparados)
lin_mse = mean_squared_error(datos_num_tr, datos_predicciones)
lin_rmse = np.sqrt(lin_mse)
lin_rmse



En nuestro caso devuelve un error minúsculo, pero eso es porque estamos comparando los datos con ellos mismos, en un caso real debería devolvernos errores mucho más altos.
A continuación vamos a entrenar un modelo de árbol de decisión DecisionTreeRegressor, es un modelo bastante potente para encontrar relaciones no lineales complejas en nuestros datos.

from sklearn.tree import DecisionTreeRegressor
tree_reg = DecisionTreeRegressor(random_state=42)
tree_reg.fit(datos_preparados, datos_num_tr)

Una vez entrenado vamos a evaluarlo con  nuestro set de entrenamiento

datos_predicciones = tree_reg.predict(datos_preparados)
tree_mse = mean_squared_error(datos_num_tr, datos_predicciones)
tree_rmse = np.sqrt(tree_mse)
tree_rmse



Nos devuelve 0 ¿significa esto que nuestro modelo es perfecto? En realidad nos indica que es un modelo bastante bueno. Pero hemos utilizado los mismos datos de entrenamiento y de validación, esa es la verdadera razón de que el error sea cero. Lo ideal es entrenar con unos datos y validarlo con un set de datos diferente.

Mejores evaluaciones con validación cruzada

Una forma de dividir nuestro set de datos para entrenamiento y validación es utilizar la función train_test_split() para partir nuestro set de datos. Aunque no es difícil nos llevará algo de trabajo adicional.
Una buena alternativa es la característica K-fold cross-validation de Scikit-Learn. El código que presentamos a continuación, divide nuestro set de datos en 10 trozos de forma aleatoria y evalúa el modelo de árbol de decisión 10 veces tomando un trozo diferente cada vez como entrenamiento y los otros 9 trozos como evaluación.

from sklearn.model_selection import cross_val_score
scores = cross_val_score(tree_reg, datos_preparados, datos_num_tr,
                         scoring="neg_mean_squared_error", cv=10)
tree_rmse_scores = np.sqrt(-scores)

def display_scores(scores):
    print("Scores:", scores)
    print("Mean:", scores.mean())
    print("Standard deviation:", scores.std())

display_scores(tree_rmse_scores)


Esto nos devuelve la mediana y la desviación estándar. En este caso los datos son un poco extraños, 1.1634 en realidad significa 0,00011634 % con una variación de ±0,00001603  lo lógico sería que devolviera datos por ejemplo del tipo 78564.432 para representar 78,564432 % con una desviación estádard de  2546.43 es decir ±2,5464 %
Vamos al realizar el mismo ejemplo con regresión lineal

lin_scores = cross_val_score(lin_reg, datos_preparados, datos_num_tr,
scoring="neg_mean_squared_error", cv=10)
lin_rmse_scores = np.sqrt(-lin_scores)
display_scores(lin_rmse_scores)


Parece que los datos de la regresión lineal son más bajos, por lo que su ejecución será mejor que la del árbol de decisión.
Ahora vamos a probar con RandomForestRegressor, que trabaja entrenando varios árboles de decisión con subsets de datos aleatorios de nuestro dataset. Construir un modelo con otros muchos se llama ensamblaje Ensemble Learning.

from sklearn.ensemble import RandomForestRegressor
forest_reg = RandomForestRegressor(n_estimators=100, random_state=42)
forest_reg.fit(datos_preparados, datos_num_tr)
datos_prediciones = forest_reg.predict(datos_preparados)
forest_mse = mean_squared_error(datos_num_tr, datos_prediciones)
forest_rmse = np.sqrt(forest_mse)
forest_rmse

from sklearn.model_selection import cross_val_score

forest_scores = cross_val_score(forest_reg, datos_preparados, datos_num_tr,scoring="neg_mean_squared_error", cv=10)
forest_rmse_scores = np.sqrt(-forest_scores)
display_scores(forest_rmse_scores)


En este caso estamos a mitad de camino entre la regresión lineal y el árbol de decisión. Para hacer un estudio completo deberíamos probar otros muchos modelos para varias categorías de algoritmos de Machine Learning como varios Support Vectors Machines con diferentes núcleos y posiblemente una red neuronal, pero sin perder demasiado tiempo retorciendo los parámetros. El objetivo es tener una pequeña lista de entre dos y cinco modelos prometedores (los que mejores resultados de salida han arrojado).
Podemos ver más datos de nuestro dataset con 

scores = cross_val_score(lin_reg, datos_preparados, datos_num_tr, scoring="neg_mean_squared_error", cv=10)
pd.Series(np.sqrt(-scores)).describe()



Es una buena práctica guardar los modelos de prueba por si tenemos que recurrir rápidamente a ellos, debemos asegurarnos de guardar los hiperparámetros utilizados para los entrenamientos, los resultados de las validaciones cruzadas y las predicciones. Esto nos permitirá realizar rápidas comparaciones con los modelos entre sí. 
Para grabar estos datos podemos utilizar el módulo pickle de Python o la librería joblib que es muy eficiente serializando grandes matrices de NumPy.

Afinar con el modelo

Ahora tenemos una pequeña lista de modelos prometedores, necesitamos ajustarlos, vamos a ver algunas técnicas para ello.

Matriz de Búsqueda 

Una forma de ajustar nuestro modelo sería probar a introducir diferentes hiperparámetros manualmente hasta encontrar la mejor combinación de hiperparámetros, pero sería un trabajo muy tedioso. En vez de esto, podemos utilizar la función de Scikit-Learn GridSearchcv, sólo necesitamos decirle que hiperparámetros queremos evaluar. En el siguiente ejemplo experimentaremos con las diferentes combinaciones para evaluar la función RandomForestRegressor.

from sklearn.model_selection import GridSearchCV
param_grid = [
    # intenta 12 (3×4) combinaciones de hiperparametros
    {'n_estimators': [3, 10, 30], 'max_features': [2, 4, 6, 8]},
    # intenta 6 (2×3) combinaciones con bootstrap puesto a false
    {'bootstrap': [False], 'n_estimators': [3, 10], 'max_features': [2, 3, 4]},
  ]
forest_reg = RandomForestRegressor(random_state=42)
# entrena a lo largo de 5 carpetas, esto es un total de (12+6)*5=90 rondas de entrenamiento
grid_search = GridSearchCV(forest_reg, param_grid, cv=5,
scoring='neg_mean_squared_error',
                           return_train_score=True)
grid_search.fit(datos_preparados, datos_num_tr)
grid_search.best_params_

En nuestro caso hemos obtenido de resultado como mejor combinación de hiperparámetros encontrada

{'max_features': 2, 'n_estimators': 30}

Cuando no tenemos ni idea de qué valor de hiperparámetros debemos probar, podemos aproximarnos intentando potencias de 10 consecutivas (o un número más pequeño si queremos una búsqueda de grano más fino)  como hemos visto en este ejemplo con el hiperparámetro n_estimators.

El parámetro param_grid le dice a Scikit-Learn que evalúe 12 combinaciones de los hiperparámetros n_estimators y max_features. (De momento no nos vamos a preocupar por el significado de estos hiperparámetros). En la primera línea y 6 en la segunda.
La función grid search explorará 18 combinaciones de valores de los hiperparámetros para RandomForestRegressor y entrenará el modelo 5 veces. En total seguirá 90 rondas de entrenamiento, lo que podría llevar bastante tiempo, pero cuando termine tendremos una la salida del estilo.

{'max_features': x, 'n_estimators': y}

Donde x e y son los mejores valores que ha evaluado.
Para encontrar directamente los mejores valores podemos hacer

grid_search.best_estimator_

Podemos tener una visión de toda la matriz de resultados para ver como se ha realizado la prueba y que valores se han obtenido.

cvres = grid_search.cv_results_
for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
    print(np.sqrt(-mean_score), params)


Donde vemos que efectivamente, la salida más baja 0,9723 corresponde para los valores de x=2 e Y =30.

{'max_features': 2, 'n_estimators': 30}

Podemos visualizar una matriz con más información y mucho  más amigable con esta instrucción 

pd.DataFrame(grid_search.cv_results_)

Búsqueda aleatoria

La búsqueda anterior es útil si el rango de los posibles valores de los hiperparámetros es estrecho, en el caso de que estos valores se extiendan sobre un rango amplio, podría ser más interesante hacer una búsqueda aleatoria y utilizar RandomizedSearchcV, esta clase es utilizada igual que GridSearchCV pero en vez de evaluar todas las posibles conbinaciones, selecciona un valor aleatorio para cada hiperparámetro en cada iteración.

from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint

param_distribs = {
        'n_estimators': randint(low=1, high=200),
        'max_features': randint(low=1, high=8),
    }

forest_reg = RandomForestRegressor(random_state=42)
rnd_search = RandomizedSearchCV(forest_reg, param_distributions=param_distribs,
                                n_iter=10, cv=5, scoring='neg_mean_squared_error', random_state=42)
rnd_search.fit(datos_preparados, datos_num_tr)
cvres = rnd_search.cv_results_
for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
    print(np.sqrt(-mean_score), params)



En este caso vemos que ha hecho un rastreo aleatorio y ha encontrado los mejores valores alrededor de max_features =  3 y n_estimators = 150 esto nos permitiría por ejemplo, hacer una matriz de búsqueda alrededor de estos valores, para afinar aún más nuestra búsqueda.

Analizar los mejores modelos y sus errores

Si queremos tener una buena visión de nuestro problema e inspeccionar que modelos son los más adecuados, podemos encontrar para una función, por ejemplo RandomForestRegressor ¿cuales son los atributos que más adecuados para obtener predicciones precisas?

feature_importances = grid_search.best_estimator_.feature_importances_
feature_importances

Ahora vamos a mostrar esos resultados junto con sus correspondientes nombres de atributo

extra_attribs = ["a", "b", "c"]
cat_encoder = full_pipeline.named_transformers_["cat"]
cat_one_hot_attribs = list(cat_encoder.categories_[0])
attributes = num_attribs + extra_attribs + cat_one_hot_attribs
sorted(zip(feature_importances, attributes), reverse=True)



Aquí los atributos más adecuados son los que obtienen un número más alto, En nuestro caso particular, octubre será el mes con los datos más relevantes para encontrar patrones.  Esta información puede servirnos también para eliminar columnas que no nos aporten información relevante, en nuestro caso, el carácter anual (cálido, fresco, etc, parece no aportar mucha información adicional)