====== 12 - Modelos clasificación 5: Ensamblaje y Random Forest ======
A menudo, se obtienen predicciones mejores si sumamos las predicciones de un grupo de predictores (clasificadores o regresores). Un grupo de predictores se denomina **ensamble** y la técnica de juntarlos **ensamblaje**.
En este tema vamos a ver varios de los métodos de ensamblaje más populares.
===== Clasificadores de votación =====
Vamos a crear varios clasificadores (árbol de decisión, Knn y regresión logística) para el conjunto de datos //cancer// de //Sklearn//:
import pandas as pd
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import VotingClassifier
from sklearn.metrics import accuracy_score
# Configuración warnings
# ==============================================================================
import warnings
warnings.filterwarnings('ignore')
cancer = load_breast_cancer()
df = pd.DataFrame(data = cancer.data, columns = cancer.feature_names)
df['target'] = cancer.target
X = df.drop(columns = ['target'], axis = 1)
y = df['target']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=25)
tree_clf = DecisionTreeClassifier(max_depth=5)
knn_clf = KNeighborsClassifier(n_neighbors=5, weights='uniform')
log_clf = LogisticRegression()
Vemos la exactitud de cada uno de ellos:
for clf in (tree_clf, knn_clf, log_clf):
clf.fit(X_train, y_train)
y_pred=clf.predict(X_test)
print(clf.__class__.__name__, float("{0:.4f}".format(accuracy_score(y_test, y_pred))))
DecisionTreeClassifier 0.9357
KNeighborsClassifier 0.924
LogisticRegression 0.924
Para crear un clasificador de votación, usamos la clase [[https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.VotingClassifier.html|VotingClassifier]] de //Sklearn//. Para crear el clasificador, le indicamos los estimadores que vamos a usar (en nuestro caso, los 3 clasificadores que hemos creado) mediante el parámetro **estimators** y el tipo de votación (ya veremos más adelante que tipos hay):
voting_clf_hard = VotingClassifier(
estimators=[('dt', tree_clf), ('knn', knn_clf), ('lr', log_clf)],
voting = 'hard'
)
voting_clf_hard.fit(X_train, y_train)
Mostramos ahora la exactitud de los 4 clasificadores:
for clf in (tree_clf, knn_clf, log_clf, voting_clf_soft):
clf.fit(X_train, y_train)
y_pred=clf.predict(X_test)
print(clf.__class__.__name__, float("{0:.4f}".format(accuracy_score(y_test, y_pred))))
DecisionTreeClassifier 0.9357
KNeighborsClassifier 0.924
LogisticRegression 0.924
VotingClassifier 0.9415
Nuestro clasificador de votación consigue una exactitud mayor que el mejor clasificador del ensamble. De hecho, aunque un clasificador sea débil (el resultado es sólo un poco mejor que una suposición aleatoria), el ensamble puede ser un clasificador fuerte siempre y cuando haya un número suficiente de aprendices débiles y sean lo bastante diversos.
El clasificador de votación que hemos creado suma las predicciones de cada clasificador y predice la clase que obtiene más votos. Esto se llama **hard voting**.
Si todos los clasificadores del ensamble pueden estimar probabilidades de clase (tienen el método //predict_proba()//), podemos indicar a //Sklearn// que prediga la clase con la probabilidad de clase más alta, promediada sobre todos los clasificadores individuales. Esto se denomina **soft voting**. A menudo, consigue mejores resultados que el //hard voting//, ya que da más peso a los votos que son más seguros. Lo único que tenemos que hacer es sustituir //voting="hard"// por //voting="soft"// cuando creamos el clasificador de votación (y asegurarte que todos los clasificadores individuales pueden hacer predicciones de clase).
===== Bagging y pasting =====
Hemos visto como crear un clasificador como un conjunto de clasificadores diferentes. Otra forma es crear un clasificador como un conjunto de clasificadores iguales, pero entrenados con datos aleatorios distintos del conjunto de entrenamiento. Cuando se realiza un muestreo con reemplazo, este método se llama **bagging**. Cuando es sin reemplazo se denomina **pasting**.
Para ver como funciona, vamos a usar la clase [[https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.BaggingClassifier.html|BaggingClassifier]] de //Sklearn//. A la hora de crear el clasificador, definiremos los siguientes hiperparámetros:
* **base_estimator**: El clasificador individual que vamos a utilizar.
* **n_estimators**: Número de clasificadores individuales que vamos a crear.
* **max_samples**: Número de instancias de entrenamiento con las que vamos a entrenar cada clasificador (hay que tener en cuenta que cada conjunto de instancias será diferente en cada clasificador individual). Puede configurarse también como un //float// que va de 0.0 a 1.0, en cuyo caso indica la proporción de instancias de entrenamiento a muestrear.
* **bootstrap**: Indica si vamos a usar reemplazo (//bagging//) a la hora de seleccionar el conjunto de instancias o no (//pasting//).
* **n_jobs**: Número de núcleos de CPU que debe utilizar para el entrenamiento y las predicciones (-1 indica que utilice todos los núcleos disponibles)
from sklearn.ensemble import BaggingClassifier
bag_clf = BaggingClassifier(
base_estimator=DecisionTreeClassifier(), n_estimators=500,
max_samples=100, bootstrap=True, n_jobs=-1
)
bag_clf.fit(X_train, y_train)
En este caso, hemos creado un ensamble de 500 clasificadores de árboles de decisión. Cada uno se entrena con 100 instancias de entrenamiento, seleccionadas aleatoriamente del conjunto de entrenamiento con reemplazo. Si quisiéramos usar //pasting// en lugar de //bagging//, sólo deberíamos modificar el hiperparámetro //bootstrap = False//:
from sklearn.ensemble import BaggingClassifier
bag_clf = BaggingClassifier(
base_estimator=DecisionTreeClassifier(), n_estimators=500,
max_samples=100, bootstrap=False, n_jobs=-1
)
bag_clf.fit(X_train, y_train)
==== Evaluación fuera de la bolsa ====
Si usamos la técnica de //bagging//, al haber reemplazo en la selección de instancias habrá algunas que no se seleccionen nunca (aproximadamente un 37%). Esas instancias se denominan //fuera de la bolsa// (**Out Of Bag**), y podemos utilizarlas para la validación de nuestro modelo sin necesidad de crear un conjunto de validación aparte. Para ello, debemos establecer **oob_score = True** a la hora de crear el clasificador //bagging// (obviamente, esto no lo podemos hacer si usamos un clasificador //pasting//, ya que no hay reemplazo en la selección de instancias):
bag_clf_3 = BaggingClassifier(
base_estimator=DecisionTreeClassifier(), n_estimators=500,
max_samples=100, bootstrap=True, n_jobs=-1, oob_score=True
)
bag_clf_3.fit(X_train, y_train)
Ahora podemos ver la estimación del modelo con mediante la variable **oob_score_**:
bag_clf_3.oob_score_
0.9597989949748744
La clase //BaggingClassifier// soporta también el submuestreo de características. Es decir, se puede entrenar cada clasificador individual con características diferentes. El muestreo está controlado por dos hiperparámetros: **max_features** y **bootstrap_features**, que funcionan igual que //max_samples// y //bootstrap//, pero para el muestreo de características. Esta técnica es especialmente útil cuando tenemos entradas de alta dimensión (por ejemplo, imágenes). \\ \\
Muestrear tanto características como instancias se denomina **parches aleatorios**. Si muestreamos sólo características y mantenemos todas las instancias de entrenamiento (con //bootstrap=False// y //max_samples=1.0//) se llama **método de subespacios aleatorios**.
===== Random forest =====
Un **random forest** es un ensamble formado por un conjunto de //árboles de decisión//, entrenado, por lo general, mediante //bagging//, habitualmente con //max_samples// igual al tamaño del conjunto de entrenamiento. Además, introduce una aleatoriedad extra cuando hace crecer los árboles: en lugar de buscar la mejor característica cuando divide un nodo, busca la mejor característica de un subconjunto aleatorio.
Se puede utilizar un //BaggingClassifier// con los parámetros anteriores para crear un //random forest//. Para hacer que la elección de las características sea sobre un conjunto aleatorio, podemos usar el hiperparámetro //splitter = "random"//:
bag_clf = BaggingClassifier(
DecisionTreeClassifier(splitter="random", max_leaf_nodes=16),
n_estimators=500, max_samples=1.0, bootstrap=True, n_jobs=-1
)
Aunque el modelo anterior funcionaría de forma similar a un //random forest//, podemos usar la clase [[https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html|RandomForestClassifier]], que es más conveniente y está optimizada para los árboles de decisión:
rnd_clf = RandomForestClassifier(n_estimators = 500, max_leaf_nodes=16, n_jobs=-1)
rnd_clf.fit(X_train, y_train)
y_pred_rf = rnd_clf.predict(X_test)
print(rnd_clf.__class__.__name__, float("{0:.4f}".format(accuracy_score(y_test, y_pred_rf))))
RandomForestClassifier 0.9532
También existe la clase [[https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestRegressor.html|RandomForestRegressor]] para tareas de regresión
//Random forest// es uno de los algoritmos de //machine learning// más potentes en la actualidad.
==== Extra-Trees ====
Como hemos visto, los //random forest// utilizan un subconjunto aleatorio de características a la hora de seleccionar las características para dividir un nodo. Podemos hacer que los //árboles de decisión// sean todavía más aleatorios al utilizar también umbrales aleatorios para cada característica, en vez de buscar los mejores umbrales posibles.
Este tipo de //random forest// se llaman **ensamble de bosques extremadamente aleatorios** (**Extra-Tress, Extremely randomized trees**).
Los //Extra-trees// se entrenan con mucha más rapidez que los //random forest// corrientes, ya que la tarea de encontrar el mejor umbral posible para cada característica es una de las tareas más costosas.
Podemos utilizar la clase [[https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.ExtraTreesClassifier.html|ExtraTreesClassifier]] de //sklearn// para crear un clasificador de este tipo:
ext_clf = ExtraTreesClassifier(n_estimators = 500, max_leaf_nodes=16, n_jobs=-1)
ext_clf.fit(X_train, y_train)
No hay forma de saber, a priori, cuál de los dos modelos tendrá un rendimiento mejor (//random forest// o //Extra-trees//). Por lo general, la única forma de saberlo es probar los dos y comprobarlos mediante validación cruzada.
==== Importancia de las características ====
Una cualidad bastante útil de los //random forest// es que hacer muy fácil medir la importancia relativa de cada característica. //Sklearn// calcula esta puntuación (escalando los resultados de manera que la suma sea igual a 1) de forma automática después del entrenamiento. Para ver los resultados, usamos la variable **feature_importances_**:
iris = load_iris()
rnd_clf = RandomForestClassifier(n_estimators=500, random_state=42)
rnd_clf.fit(iris["data"], iris["target"])
for name, score in zip(iris["feature_names"], rnd_clf.feature_importances_):
print(name, score)
sepal length (cm) 0.11249225099876375
sepal width (cm) 0.02311928828251033
petal length (cm) 0.4410304643639577
petal width (cm) 0.4233579963547682
===== Boosting =====
Los métodos de ensamble que hemos visto hasta ahora entrenan los modelos individuales de forma independiente. Existe otra técnica donde estos modelos están ensamblados de manera secuencial, de forma que cada uno de ellos intenta corregir a su predecesor. Ésto se conoce como **Boosting**.
Existen muchos métodos de //boosting//, entre los cuales, los más populares son **AdaBoost** y **Gradient Boosting**.
==== AdaBoost ====
En este método, cada predictor intenta corregir a su predecesor prestando más atención a las instancias de entrenamiento que el predecesor ha subajustado.
Por ejemplo, el algoritmo entrena primero un clasificador base y hace predicciones con el conjunto de entrenamiento. El algoritmo, después, incrementa el peso relativo de las instancias mal clasificadas y entrena un segundo predictor utilizando esos pesos actualizados, y así sucesivamente.
==== Gradient Boosting ====
En este caso, los predictores se entrenan con los errores residuales cometidos por el predictor anterior.