Como Utilizar Bootstrap Aggregation Para Classificação

Giovane Sylvestrin
Datarisk.io
Published in
19 min readSep 21, 2021

O Bootstrap Aggregation, mais conhecido pela abreviação “bagging”, faz parte do conjunto de classes principais dos métodos de Ensemble Learning -algoritmos de aprendizagem por agrupamento. Esses algoritmos, como regra geral, buscam por meio da combinação de diferentes modelos treinados para uma mesma tarefa, alcançar um modelo agrupado mais complexo e robusto.

Os três grandes grupos de abordagens de Ensemble são:

  • Bagging: se refere ao ajuste de diversas árvores de decisão treinadas em diferentes porções de amostras de um mesmo dataset, onde a saída estimada é calculada a partir da média das previsões individuais dos modelos;
  • Stacking: se refere ao ajuste de diferentes tipos de modelos, treinados nos mesmos dados, e o emprego de um outro modelo que possui a tarefa de aprender como melhor realizar as combinações das previsões individuais;
  • Boosting: se refere a adição de diferentes modelos sequencialmente, onde cada um deles, na “linha produtiva”, fica responsável por corrigir as previsões feitas pelos modelos anteriores, gerando uma média ponderada das previsões.

Neste artigo, discutiremos um pouco mais sobre a abordagem de bagging, principalmente aplicada a problemas de classificação binária, considerando o emprego do algoritmo em datasets balanceados e desbalanceados. Ao final, o leitor será capaz de implementar a abordagem utilizando as ferramentas da biblioteca Sckit-Learn. Além disso, terá as noções iniciais sobre o porquê do uso do bagging, para que tipos de modelos é mais recomendado, uma primeira exploração dos hiperparâmetros envolvidos e seus efeitos. Todo o desenvolvimento apresentado a seguir é disponibilizado neste repositório.

O que é Bagging?

Bagging (Bootstrap Aggregation) é um algoritmo de Ensemble de Machine Learning, essencialmente um ensemble de modelos de árvores de decisão, apesar de suportar outros tipos de técnicas para a combinação de modelos. É baseado na ideia de amostragem bootstrap, que é uma amostragem com reposição de um dataset (permite que uma amostra seja escolhida novamente em outro processo). Um exemplo do uso da amostragem bootstrap seria a tarefa de estimar a média da população a partir de um pequeno conjunto de dados. Nesta situação, poderíamos ter múltiplas amostras de bootstrap retiradas do conjunto de dados e, com as médias individuais de cada amostra, realizar o cálculo da média final como estimativa da população total.

No simples exemplo anterior, além da amostragem boostrap verificamos que cada amostragem da população serviu para estimar a população final e essa combinação “bootstrap + agregação” de informação de cada amostra na média final é o bagging. Formalmente o bagging parte desta abordagem, onde múltiplas amostras bootstrap são retiradas do dataset de treino, ajustando um modelo de árvore de decisão em cada uma delas. As previsões das árvores de decisão são combinadas e possibilitam, em geral, um modelo combinado com maior precisão e robustez.

As árvores de decisão são utilizadas porque tendem a um “super ajuste” nos dados em que são treinados, caracterizadas como modelos com alta variância (overfitting). Em teoria, cada árvore se ajusta ao conjunto de dados que lhe é passado, permitindo que cada uma delas apresente particularidades de acordo com a característica dos dados. A eficácia do método de bagging reside na suposição de que as árvores tenham entre si baixa correlação entre as predições e, desta forma, menores erros na predição combinada. A técnica de bagging é apropriada justamente para modelos com alta variância, como também é o caso do algoritmo KNN.

A partir do exposto, podemos definir a estrutura básica do bagging em três grandes partes:

  • Diferentes conjunto de dados para treinamento: criação de conjuntos de treinos diferentes utilizando a amostragem bootstrap;
  • Modelos de alta variância: treinamento de modelos de alta variância, como árvores de decisão, em cada conjunto de amostra dos dados;
  • Predição média: emprego de medida estatística para combinar as predições individuais: média para tarefas de regressão, moda para tarefas de classificação.
Figura 1: Bootstrap Aggregation.

Alguns exemplos de métodos descendentes do bagging são algoritmos de árvores conhecidos, como é o caso do: Random Forest Ensemble e Extra Trees Ensemble.

A seguir vamos explorar a construção de um modelo combinado utilizando a biblioteca Scikit-learn, com o Bagging Scikit-Learn API. Abordaremos a tarefa de classificação binária considerando datasets balanceados e desbalanceados. Para os dados desbalanceados usaremos a biblioteca Imbalanced-Learn.

Ferramentas Necessárias

Para demonstrar o uso do bagging faremos uso das seguintes bibliotecas (pandas, numpy, sklearn, imblearn, matplotlib e seaborn) e imports associados:

import pandas as pd
import numpy as np
from numpy import arange
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import RepeatedStratifiedKFold
from sklearn.model_selection import RandomizedSearchCV
from sklearn.ensemble import BaggingClassifier
from imblearn.ensemble import BalancedBaggingClassifier
from sklearn import metrics
from sklearn.metrics import confusion_matrix
from sklearn.metrics import plot_confusion_matrix
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score
from sklearn.metrics import f1_score
from numpy import mean
from numpy import std
import matplotlib.pyplot as plt
import seaborn as sns

Classificação com Dataset Balanceado

Nossa primeira implementação do método de bagging é para uma tarefa de classificação binária, cujas classes de saída estão balanceadas (quantidades semelhantes de amostras). Como o objetivo deste artigo é introduzir ao método de ensemble, faremos uso da ferramenta make_classification para gerar um dataset randômico para um problema de classificação binária, contendo um total de 10.000 amostras, com 30 features. Nas linhas de código abaixo realizamos esse processo e, por conveniência de visualização, criamos um dataframe contendo os dados, features e target — Figura 2.

# Gerando um dataset randômico, com entradas X e saída binária y
X, y = make_classification(n_samples=10000, n_features=30,
n_informative=25, n_redundant=5,
random_state=42)
# Passando X e y de numpy.ndarray para dataframe
dataset = pd.DataFrame(X)
dataset['y'] = y
dataset
Figura 2: Dataset gerado para a classificação.

A distribuição das classes de saída para este dataset é aproximadamente igual, conforme a contagem utilizando value_counts.

dataset['y'].value_counts(True)

A primeira etapa realizada é o particionamento do dataset gerado nos conjuntos de treino - validação e teste - com 7.000, 1.500, e 1.500 amostras respectivamente.

X_train, X_, y_train, y_ = train_test_split(X, y, random_state=42,
test_size=0.3)
X_val, X_test, y_val, y_test = train_test_split(X_, y_,
random_state=42,
test_size=0.5)
print(X_train.shape)
print(X_val.shape)
print(X_test.shape)

Para a implementação do bagging sãonecessários modelos de alta variância, em geral árvores de decisão. Aqui iniciaremos realizando a implementação de um modelo de árvore de decisão default utilizando a biblioteca do Scikit-Learn, sem considerar ainda o processo de amostragem bootstrap, apenas um procedimento normal para criação de um modelo.

Entretanto, antes de começar a modelagem propriamente dita, utilizamos todo o conjunto de dados, e através do cross-validation, realizamos uma análise dos desempenhos deste modelo inicial em diferentes porções. Um dos fundamentos do bagging é reduzir a variância nos modelos. Para demostrarmos isso mais adiante, utilizamos o código abaixo. Nele observamos que o modelo de árvore comum obtém uma métrica de AUC médio de 0,807, que variou segundo um desvio padrão de 0.013. Quando realizarmos a implementação do bagging, esperemos que esses valores apontem para uma maior precisão e menor variância.

# Definição de um modelo de árvore de decisão default
model = DecisionTreeClassifier(random_state=42)
# Definição do cross-validation usando RepeatedStratifiiedKFold
cv = RepeatedStratifiedKFold(n_splits=20, n_repeats=3,
random_state=42)
# Verificando as métricas obtidas em diferentes partições e o desvio padrão
n_scores = cross_val_score(model, X, y, scoring='roc_auc', cv=cv,
n_jobs=-1, error_score='raise')
print(f'AUC, desvio padrão: {mean(n_scores)}, {std(n_scores)}')

Voltando agora ao processo de construção do modelo, fazemos o ajuste do modelo de árvore de decisão de acordo com os dados de treinamento, podemos ver que a alta variância do modelo está presente quando comparamos as métricas de AUC de treino, validação e teste, com uma diferença considerável, indicando a condição de overfitting.

# Ajuste do modelo aos dados de treinamento
model.fit(X_train, y_train)
# Métricas de desempenho nos conjuntos de validação e teste
prob_train = model.predict_proba(X_train)[:, 1]
prob_val = model.predict_proba(X_val)[:, 1]
prob_test = model.predict_proba(X_test)[:, 1]
print(f"AUC train: {metrics.roc_auc_score(y_train, prob_train)}")
print(f"AUC val: {metrics.roc_auc_score(y_val, prob_val)}")
print(f"AUC test: {metrics.roc_auc_score(y_test, prob_test)}")

Por ser um problema de classificação, é interessante também analisar a matriz de confusão para os dados de validação e teste, bem como outras métricas como precisão, recall, e f1-score, implementados conforme a seguir. Essas métricas e análise da matriz de confusão são ainda mais importantes na análise dos resultados quando a target é desbalanceada, pois consegue mostrar se o modelo de fato está generalizando. Os resultados são apresentados na Figura 3.

# Função para plot da matriz de confusão
def matriz_confusao(confusion):
group_names = ['TN','FP','FN','TP']
group_counts = ["{0:0.0f}".format(value) for value in confusion.flatten()]
labels = [f"{v1}\n{v2}" for v1, v2 in zip(group_names,group_counts)]
labels = np.asarray(labels).reshape(2,2)
plt.figure(figsize=(8,6))
cmap = sns.diverging_palette(250, 210, l=55, as_cmap=True)
sns.heatmap(confusion, annot=labels, fmt='', cmap=cmap, cbar=False, linecolor='black', linewidths=1.0)
plt.ylabel('True label')
plt.xlabel('Predicted label')
plt.rcParams.update({'font.size': 18})
return plt.show()
# Matrizes de confusão e métricas para validação e teste com ponto de corte = 0,5
pred_val = (model.predict_proba(X_val)[:,1] >= 0.5).astype(bool)
confusion_val = confusion_matrix(y_val, pred_val)
print(f"Matriz de confusão validação:\n")
matriz_confusao(confusion_val)
precision_val = precision_score(y_val, pred_val, average='binary')
recall_val = recall_score(y_val, pred_val, average='binary')
f1_val = f1_score(y_val, pred_val, average='binary')
print(f"Precisão: {precision_val}")
print(f"Recall: {recall_val}")
print(f"f1: {f1_val}")
print('\n---------------------------------------------------------\n')pred_test = (model.predict_proba(X_test)[:,1] >= 0.5).astype(bool)
confusion_test = confusion_matrix(y_test, pred_test)
print(f"Matriz de confusão teste:\n")
matriz_confusao(confusion_test)
precision_test = precision_score(y_test, pred_test, average='binary')
recall_test = recall_score(y_test, pred_test, average='binary')
f1_test = f1_score(y_test, pred_test, average='binary')
print(f"Precisão: {precision_test}")
print(f"Recall: {recall_test}")
print(f"f1: {f1_test}")
Figura 3: Matriz de confusão e métricas de desempenho para o modelo default.

Podemos realizar um ajuste dos hiperparâmetros desse modelo de árvore de decisão e verificar seu desempenho. Para isso utilizamos RandomizedSearchCV variando os parâmetros conforme as linhas de código a seguir:

# Parâmetros para tunning
params = {'criterion' :['gini', 'entropy'],
'splitter': ['best', 'random'],
'max_features': ['auto', 'sqrt', 'log2'],
'ccp_alpha': [0, 0.1, 0.01, 0.001],
'max_depth' : range(2, 102, 2),
'min_samples_leaf': range(2, 102, 2),
}
# Definição do cross-validation
cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=42)
# Busca dos parâmetros usando RandomizedSearchCV
random_search = RandomizedSearchCV(model, params, n_iter=5000, scoring='roc_auc', verbose=1, n_jobs=-1, cv=cv, random_state=42)
result = random_search.fit(X_train, y_train)
print(f"Best params: {result.best_params_}")
print(f"Best score: {result.best_score_}")

Ajustando esse modelo de acordo com os hiperparâmetros encontrados temos um aumento da generalização do modelo, com um considerável incremento no AUC das partições de validação e treino.

# Modelo com os melhores parâmetros
model_dtt = DecisionTreeClassifier(**result.best_params_, random_state=42).fit(X_train, y_train)
# Métricas de desempenho nos conjuntos de validação e teste
prob_train = model_dtt.predict_proba(X_train)[:, 1]
prob_val = model_dtt.predict_proba(X_val)[:, 1]
prob_test = model_dtt.predict_proba(X_test)[:, 1]
print(f"Acur. train: {metrics.roc_auc_score(y_train, prob_train)}")
print(f"Acur. val: {metrics.roc_auc_score(y_val, prob_val)}")
print(f"Acur. test: {metrics.roc_auc_score(y_test, prob_test)}")

De forma semelhante, analisamos as matrizes de confusão e métricas de desempenho. Nota-se que apesar do incremento no AUC, os resultados são semelhantes.

# Matrizes de confusão e métricas para validação e teste com ponto de corte = 0,5
pred_val = (model_dtt.predict_proba(X_val)[:,1] >= 0.5).astype(bool)
confusion_val = confusion_matrix(y_val, pred_val)
print(f"Matriz de confusão validação:\n")
matriz_confusao(confusion_val)
precision_val = precision_score(y_val, pred_val, average='binary')
recall_val = recall_score(y_val, pred_val, average='binary')
f1_val = f1_score(y_val, pred_val, average='binary')
print(f"Precisão: {precision_val}")
print(f"Recall: {recall_val}")
print(f"f1: {f1_val}")
print('\n---------------------------------------------------------\n')pred_test = (model_dtt.predict_proba(X_test)[:,1] >= 0.5).astype(bool)
confusion_test = confusion_matrix(y_test, pred_test)
print(f"Matriz de confusão teste:\n")
matriz_confusao(confusion_test)
precision_test = precision_score(y_test, pred_test, average='binary')
recall_test = recall_score(y_test, pred_test, average='binary')
f1_test = f1_score(y_test, pred_test, average='binary')
print(f"Precisão: {precision_test}")
print(f"Recall: {recall_test}")
print(f"f1: {f1_test}")
Figura 4: Matriz de confusão e métricas de desempenho para o modelo com ajuste de hiperparâmetros.

Para a implementação do método de bagging fazemos uso do BaggingClassifier, que facilita a construção do modelo ensemble. Antes de ajustar o modelo em si, lembre-se do nosso pequeno teste proposto em avaliar o aumento do desempenho e redução da variância que o bagging deveria cumprir. Agora é a hora de realizarmos essa comprovação. Repetimos o processo de cross-validation para analisar o desempenho do bagging. É possível verificar um incremento considerável no desempenho, com grande aumento do AUC médio (0,81 para 0,95) e diminuição da variância (0.019 para 0,008), significando que os desempenhos variaram menos nas diferentes porções dos dados utilizados pelo cross-validation.

# Definindo modelo com bagging considerando como estimator o modelo default
model_bg = BaggingClassifier(model)
# Definição do cross-validation usando RepeatedStratifiiedKFold
cv = RepeatedStratifiedKFold(n_splits=20, n_repeats=3, random_state=42)
n_scores = cross_val_score(model_bg, X, y, scoring='roc_auc', cv=cv, n_jobs=-1, error_score='raise')
# Verificando as métricas obtidas em diferentes partições e o desvio padrão
print(f'AUC, std: {mean(n_scores)}, {std(n_scores)}')

Voltemos agora aos trilhos da modelagem, realizando o ajuste do modelo bagging aos dados de treino selecionados e avaliando o AUC. Como estimador selecionamos o modelo de árvore de decisão com os parâmetros ajustados. Verificamos o grande aumento na performance da classificação. Além disso, os resultados para os conjuntos de validação e teste indicam que o modelo não sofre mais de overfitting.

# Definindo modelo com bagging considerando como estimator o modelo com tunning
model_bg = BaggingClassifier(model_dtt)
# Ajuste do modelo bagging
model_bg.fit(X_train, y_train)
# Métricas de desempenho nos conjuntos de validação e teste
prob_train = model_bg.predict_proba(X_train)[:, 1]
prob_val = model_bg.predict_proba(X_val)[:, 1]
prob_test = model_bg.predict_proba(X_test)[:, 1]
print(f"AUC train: {metrics.roc_auc_score(y_train, prob_train)}")
print(f"AUC val: {metrics.roc_auc_score(y_val, prob_val)}")
print(f"AUC test: {metrics.roc_auc_score(y_test, prob_test)}")

As matrizes de confusão e medidas de precisão, recall e f1-score também refletem o ganho de desempenho com o uso do método de ensemble.

Figura 5: Matriz de confusão e métricas de desempenho para o modelo bagging.

Podemos tentar melhorar esses resultados ajustando os hiperparâmetros do modelo bagging. Existem diversos parâmetros na implementação do bagging que podem ser ajustados. Mas antes disso vamos visualizar como alguns deles alteram seu comportamento na predição.

O primeiro parâmetro que analisamos é o número de árvores utilizadas, um dos principais parâmetros do método. Em geral esse número aumenta até que o desempenho do modelo se estabilize. Embora possa parecer que o incremento do número de árvores possa levar a um overfitting, o uso do bagging tende a apresentar uma certa imunidade em virtude da natureza estocástica de aprendizagem.

O parâmetro n_estimator padrão é de 100 árvores. No código abaixo variamos este parâmetro até 5.000. O gráfico da Figura 6 demonstra que conforme aumentamos o número de árvores alcançamos melhores desempenhos no AUC e uma estabilidade na variação.

# Função para obter uma lista de modelos com diferentes qtds de arvores
def get_models():
models = dict()
# Define o número de árvores a serem consideradas no modelo
n_trees = [10, 50, 100, 500, 500, 1000, 5000]
for n in n_trees:
models[str(n)] = BaggingClassifier(model, n_estimators=n)
return models
# Função para avaliar um modelo usando cross-validation
def evaluate_model(model, X, y):
# define the evaluation procedure
cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1)
scores = cross_val_score(model, X, y, scoring='roc_auc', cv=cv, n_jobs=-1)
return scores
models = get_models()# Listas para armazenar os resultados
results = []
names = []
for name, model in models.items():
scores = evaluate_model(model, X, y)
results.append(scores)
names.append(name)
print(f'{name, mean(scores), std(scores)}')

# Visualização da avaliação dos modelos variando o número de árvores
plt.figure(figsize=(8,6))
sns.boxplot(data=results)
plt.xticks(range(6), names)
plt.ylabel('AUC')
plt.xlabel('Número de árvores')
Figura 6: Variação de desempenho de acordo com o número de árvores.

Outro parâmetro importante é o tamanho da amostra de bootstrap. Por padrão, esse tamanho é definido como o mesmo número de exemplos dos dados originais. Usar um conjunto de dados menor tende a aumentar a variação das árvores de decisão em cada amostragem bootstrap e resultar em melhor desempenho geral.

No código abaixo variou-se o tamanho da amostra de 10% a 100%, através do parâmetro max_samples. Na Figura 7 nota-se que o desempenho melhora conforme se aumenta o tamanho da amostra (percentual), e a variância oscila mais, tendo alguns resultados interessantes com menores fatias dos dados (50%).

# Função para obter uma lista de modelos com diferentes qtds de amostras
def get_models():
models = dict()
# Varia a quantidade de amostras de 10% a 100% dos exemplos
n_samples = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
for i in n_samples:
models[f'{i}'] = BaggingClassifier(max_samples=i)
return models
# Função para avaliar um modelo usando cross-validation
def evaluate_model(model, X, y):
# define the evaluation procedure
cv = RepeatedStratifiedKFold(n_splits=5, n_repeats=3, random_state=1)
scores = cross_val_score(model, X, y, scoring='roc_auc', cv=cv, n_jobs=-1)
return scores
models = get_models()# Listas para armazenar os resultados
results = []
names = []
for name, model in models.items():
scores = evaluate_model(model, X, y)
results.append(scores)
names.append(name)
print(f'{name, mean(scores), std(scores)}')

# Visualização da avaliação dos modelos variando o número de árvores
plt.figure(figsize=(8,6))
sns.boxplot(data=results)
plt.xticks(range(10), names)
plt.ylabel('AUC')
plt.xlabel('Número de amostras')
Figura 7: Variação de desempenho de acordo com o tamanho da amostro.

Para o ajuste dos hiperparâmetros no modelo bagging, variamos além da quantidade de árvores (n_estimators) e quantidade de amostras (max_samples), os parâmetros bootstrap e warm_start. O bootstrap indica se as amostras são retiradas com (True) ou sem (False) repetição da amostra. O warm_start diz respeito a reutilização de resultados anteriores para ajuste e adição de mais estimadores em conjunto (caso True). No código abaixo, obtemos os melhores parâmetros dentro das combinações randômicas realizadas.

params = {'n_estimators' :[10, 50, 100, 500, 1000],
'max_samples': arange(0.1, 1.1, 0.1),
'bootstrap': [False, True],
'warm_start': [False, True],
}
cv = RepeatedStratifiedKFold(n_splits=5, n_repeats=3, random_state=42)
random_search = RandomizedSearchCV(model_bg, params, n_iter=30, scoring='roc_auc', verbose=1, n_jobs=-1, cv=cv, random_state=42)
result = random_search.fit(X_train, y_train)
print(f"Best params: {result.best_params_}")
print(f"Best score: {result.best_score_}")

Utilizando os parâmetros de ajuste, construímos um novo modelo bagging e alcançamos um desempenho ainda similar ao modelo anterior, com alguma variação.

# Ajuste do modelo bagging tunning
model_bgt = BaggingClassifier(**result.best_params_, random_state=42).fit(X_train, y_train)
# Métricas de desempenho nos conjuntos de validação e teste
prob_train = model_bgt.predict_proba(X_train)[:, 1]
prob_val = model_bgt.predict_proba(X_val)[:, 1]
prob_test = model_bgt.predict_proba(X_test)[:, 1]
print(f"AUC train: {metrics.roc_auc_score(y_train, prob_train)}")
print(f"AUC val: {metrics.roc_auc_score(y_val, prob_val)}")
print(f"AUC test: {metrics.roc_auc_score(y_test, prob_test)}")
Figura 8: Matriz de confusão e métricas de desempenho para modelo bagging com hiperparâmetros ajustados.

Ao final, podemos evidenciar em comparação com o primeiro modelo ajustado, com apenas uma árvore de decisão, um incremento considerável no desempenho utilizando o método de bagging.

Classificação com Dataset Desbalanceado

Agora vamos repetir as etapas anteriores e utilizar o método de bagging para a construção de um modelo considerando um dataset com target desbalanceada. Utilizamos novamente a ferramenta make_classification para gerar um dataset randômico, com 10.000 exemplos e 30 features. Desta vez, inserimos um parâmetro (weight) para gerar desbalanceamento nas classes de saídas, de forma que uma classe possua 95% das amostras de saída e a classe minoritária apresente o restante, 5%.

# Gerando dataset desbalanceado com 95% y=0 e 5% y=1 
X, y = make_classification(n_samples=10000, n_features=30, n_informative=25, n_redundant=5, n_clusters_per_class=1, weights=[0.95], flip_y=0, random_state=42)
dataset = pd.DataFrame(X)
dataset['y'] = y
dataset['y'].value_counts(True)

Assim como fizemos para a tarefa de classificação anterior, iniciamos definindo um modelo de árvore de decisão default ajustado aos dados de treino.

model = DecisionTreeClassifier(random_state=42).fit(X_train, y_train)prob_train = model.predict_proba(X_train)[:, 1]
prob_val = model.predict_proba(X_val)[:, 1]
prob_test = model.predict_proba(X_test)[:, 1]
print(f"AUC train: {metrics.roc_auc_score(y_train, prob_train)}")
print(f"AUC val: {metrics.roc_auc_score(y_val, prob_val)}")
print(f"AUC test: {metrics.roc_auc_score(y_test, prob_test)}")

Notamos que existe um overfitting no modelo ajustado, com um AUC inferior para os conjuntos de validação e teste. Para problemas desbalanceados a inspeção de matriz de confusão é ainda mais importante, uma vez que, nosso modelo pode alcançar métricas de AUC altas sem necessariamente estar generalizando. Devido a pouca quantidade de exemplos da classe minoritária, o modelo pode apenas estar assumindo que todos os exemplos são da classe majoritária.

A matriz de confusão da Figura 9 demonstra que o modelo está acertando cerca da metade dos exemplos da classe minoritária. Em muitos casos reais de classes desbalanceadas, como a incidência de uma doença em uma população, é ainda mais importante a detecção dos casos positivos, mesmo que isto cause um aumento na taxa de falsos positivos. Portanto, métricas como recall e f1-score devem ser aumentadas.

Figura 9: Matriz de confusão para o algoritmo de árvore de decisão default para a classificação desbalanceada.

Novamente, realizamos o ajuste dos hiperparâmetros do algoritmo de árvore de decisão utilizando RandomizedSearchCV.

# Parâmetros para tunning
params = {'criterion' :['gini', 'entropy'],
'splitter': ['best', 'random'],
'max_features': ['auto', 'sqrt', 'log2'],
'ccp_alpha': [0, 0.1, 0.01, 0.001],
'max_depth' : range(2, 102, 2),
'min_samples_leaf': range(2, 102, 2),
}
# Definição do cross-validation
cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=42)
# Busca dos parâmetros usando RandomizedSearchCV
random_search = RandomizedSearchCV(model, params, n_iter=1000, scoring='roc_auc', verbose=1, n_jobs=-1, cv=cv, random_state=42)
result = random_search.fit(X_train, y_train)
print(f"Best params: {result.best_params_}")
print(f"Best score: {result.best_score_}")

Ajustamos, então, o modelo aos dados de treino e avaliamos seu desempenho. Percebemos que houve um considerável aumento no AUC para os conjuntos de validação e teste.

model_dtt = DecisionTreeClassifier(**result.best_params_, random_state=42).fit(X_train, y_train)prob_train = model_dtt.predict_proba(X_train)[:, 1]
prob_val = model_dtt.predict_proba(X_val)[:, 1]
prob_test = model_dtt.predict_proba(X_test)[:, 1]
print(f"AUC train: {metrics.roc_auc_score(y_train, prob_train)}")
print(f"AUC val: {metrics.roc_auc_score(y_val, prob_val)}")
print(f"AUC test: {metrics.roc_auc_score(y_test, prob_test)}")

Entretanto, na Figura 10, a matriz de confusão mostra que na verdade o modelo apenas começou a marcar ainda menos exemplos como da classe minoritária, prevendo ainda menos exemplos desta classe, com as métricas de precisão, recall e f1-score sendo reduzidas.

Figura 10: matriz de confusão para modelo de árvore de decisão com hiperparâmetros ajustados.

Vamos agora aplicar o método de bagging a este problema. Realizamos o mesmo processo do caso anterior, utilizando BaggingClassifier.

model_bg = BaggingClassifier(model_dtt).fit(X_train, y_train)prob_train = model_bg.predict_proba(X_train)[:, 1]
prob_val = model_bg.predict_proba(X_val)[:, 1]
prob_test = model_bg.predict_proba(X_test)[:, 1]
print(f"AUC train: {metrics.roc_auc_score(y_train, prob_train)}")
print(f"AUC val: {metrics.roc_auc_score(y_val, prob_val)}")
print(f"AUC test: {metrics.roc_auc_score(y_test, prob_test)}")

O desempenho aumentou muito com relação a AUC. Não existe mais indícios de um overfitting, tudo está resolvido … a não ser pela análise da matriz de confusão.

Figura 11: matriz de confusão para modelo de bagging.

Infelizmente a simples aplicação do bagging não resolveu nosso problema de classificação desbalanceada (piorou muito na verdade), chegamos praticamente ao caso extremo onde nosso modelo se converte apenas em um “repetidor” para a classe majoritária, que devido às proporções de casos, alcança um AUC extremamente elevado.

De fato, o método de bagging normal apesar de efetivo, conforme vimos no exemplo anterior, não é adequado para problemas desbalanceados. Entretanto, existem adaptações que podem ser realizadas para ainda assim utilizar este método e alcançar bons resultados. Uma delas é a realização do balanceamento do conjunto de treino.

A biblioteca Imbalanced-learn faz a implementação da ferramenta de bagging do Sckit-learn, adicionando uma etapa de balanceamento, conhecida por BalancedBaggingClassifier. Utiliza uma estratégia de random undersampling na classe majoritária em uma amostra de bootstrap para equilibrar as duas classes. Nas linhas a seguir implementamos o BalancedBaggingClassifier.

model_ib = BalancedBaggingClassifier(model_dtt).fit(X_train, y_train)prob_train = model_ib.predict_proba(X_train)[:, 1]
prob_val = model_ib.predict_proba(X_val)[:, 1]
prob_test = model_ib.predict_proba(X_test)[:, 1]
print(f"Acur. train: {metrics.roc_auc_score(y_train, prob_train)}")
print(f"Acur. val: {metrics.roc_auc_score(y_val, prob_val)}")
print(f"Acur. test: {metrics.roc_auc_score(y_test, prob_test)}")

O desempenho do score de AUC é bem superior ao alcançado pelo primeiro modelo de árvore de decisão para todo o conjunto (~0,76 para ~0,96). Mas o principal ponto de análise é a matriz de confusão. Na Figura 12, vemos que o modelo acerta cerca de 90% dos casos positivos (recall), embora para isso exista um decréscimo da precisão, pois agora o modelo acaba classificando alguns casos da classe majoritária de forma errada.

Em geral, quando estamos trabalhando com este tipo de situação, podemos encarar essa diminuição na precisão como um ponto positivo. Se tivéssemos tratando de um problema para saber se uma pessoa tem ou não uma certa doença, estaríamos reduzindo o público com propensão da doença e exigindo um exame de comprovação em uma quantidade muito mais reduzida de pacientes. Em nosso caso fictício, contando as amostras de validação e teste, passaríamos a exigir um exame de comprovação para 442 pessoas, no lugar do público total de 3.000. E mesmo assim, considerando um corte de 0,5, alcançando 90% dos casos positivos da doença.

Figura 12: matriz de confusão para modelo utiizando BalancedBaggingClassifier.

Na tentativa de melhorar ainda mais nossos resultados, podemos realizar um ajuste dos hiperparâmetros seguindo os mesmos procedimentos realizados ao longo do artigo. Os parâmetros variados são os mesmos do bagging para a classificação balanceada.

params = {'n_estimators' :[10, 50, 100, 500, 1000],
'max_samples': arange(0.1, 1.1, 0.1),
'bootstrap': [False, True],
'warm_start': [False, True],
}
cv = RepeatedStratifiedKFold(n_splits=5, n_repeats=3, random_state=42)
random_search = RandomizedSearchCV(model_ib, params, n_iter=30, scoring='roc_auc', verbose=1, n_jobs=-1, cv=cv, random_state=42)
result = random_search.fit(X_train, y_train)
print(f"Best params: {result.best_params_}")
print(f"Best score: {result.best_score_}")

Ajustando o modelo para os parâmetros com melhor desempenho no RandomizedSearchCV, obtemos um modelo com AUC de cerca de 0,99 nos conjuntos de validação e teste.

model_ibt = BalancedBaggingClassifier(**result.best_params_, random_state=42).fit(X_train, y_train)prob_train = model_ibt.predict_proba(X_train)[:, 1]
prob_val = model_ibt.predict_proba(X_val)[:, 1]
prob_test = model_ibt.predict_proba(X_test)[:, 1]
print(f"AUC train: {metrics.roc_auc_score(y_train, prob_train)}")
print(f"AUC val: {metrics.roc_auc_score(y_val, prob_val)}")
print(f"AUC test: {metrics.roc_auc_score(y_test, prob_test)}")

A matriz de confusão e as métricas relacionadas (Figura 12) mostram um aumento na precisão, recall e f1-score médio dos dois conjuntos. Pensando no público com possibilidade de caso positivo, reduzimos de 442 para 235 e acertamos cerca de 93% dos casos positivos.

Figura 13: matriz de confusão para modelo utiizando BalancedBaggingClassifier com ajuste de hiperparâmetros.

Por fim, neste exemplo verificamos um crescente incremento de desempenho desde o modelo mais básico, bem como também a ineficiência da simples aplicação do bagging sem os ajustes de balanceamento presentes bo balanced bagging.

Conclusões

Neste artigo fizemos uma introdução a um dos mais conhecidos métodos de ensemble: o bootstrap aggregation (bagging). Através de dois exemplos para problemas de classificação binária, um com target balanceada e outro desbalanceado. Percorremos os passos desde a implementação de um modelo de árvore de decisão para todo o conjunto, passando por ajustes de hiperparâmetros e o uso deste modelo ajustado como modelo de alta variância no método de bagging. Demonstramos a efetividade do método em reduzir erros de variância no modelo (overfitting) e alcançar resultados satisfatórios em ambos os casos analisados.

O leitor poderá seguir explorando esta técnica de ensemble nas referências abaixo, bem como comparar com outras técnicas de ensemble e adequar a seu problema.

Diversos outros temas relacionados à Inteligência Artificial você pode conferir em nossos outros posts aqui, aproveite também e dê uma passada em nosso site e veja como solucionamos problemas do mundo real com Machine Learning.

Referências

--

--