Avatar foto de Flávio Coutinho

TP2 - Boosting com o CoutoBoost

Aluno
Flávio Coutinho
Turma
Aprendizado de Máquina, UFMG, 01/21
Objetivo
Implementar um processo de boosting para o problema de classificar o resultado de jogos da velha.
Link
https://fegemo.github.io/ml-boosting/

Conteúdo deste notebook:

Este notebook é a implementação do trabalho prático da disciplina de Aprendizado de Máquina cursada na UFMG em 2021/1 ministrada pelo prof. Adriano Veloso.

O trabalho contempla o conteúdo de Boosting e envolve a implementação desse processo para o problema de classificar o resultado de partidas de jogo da velha dada uma situação de tabuleiro.

Descrição dos dados

O arquivo tic-tac-toe.data descreve configurações de tabuleiro do jogo da velha com rótulos referentes ao resultado sob a perspectiva do jogador x. Por exemplo:

São 958 configurações de tabuleiro rotuladas em "positive", se o jogador x ganhou a partida, ou "negative", se perdeu.

As características são 9, uma para cada posição do tabuleiro, podendo assumir os valores x, o ou b - neste caso, a célula não foi preenchida.

Como pode ser observado, o conjunto de dados não está balanceado visto que quantidade de exemplos rotulados "positive" representa 65% da entrada. Sendo assim, é importante analisar a classificação não apenas com erro e acurácia, mas também olhar para precisão e revocação e outras medidas mais apropriadas a essa situação.

Implementação

Foi implementado um algoritmo similar ao AdaBoost, porém seguindo algumas restrições:

O algoritmo foi registrado em cartório como CoutoBoost e ele foi implementado seguindo a interface de estimadores do sklearn de forma a viabilizar a interoperabilidade de outros recursos do pacote sklearn com ele, como validação cruzada, métricas de desempenho etc.

O código foi escrito no módulo couto_boost.py e seus trechos mais relevantes estão descritos a seguir. Primeiramente, uma descrição em alto nível dos métodos de criação da instância de classificador:

class CoutoBoostClassifier(BaseEstimator, ClassifierMixin):

    def __init__(self, iterations=108):
        self.iterations = iterations # qtde de iterações


    # métodos PRIVADOS
    def _create_stumps(self, X):
        # gera e retorna lista com todas as possibildidade de stump

O parâmetro iterations define a quantidade de iterações do Boosting e é o único hiperparâmetro disponível.

A seguir, estão os métodos referentes ao treinamento do classificador:

class CoutoBoostClassifier(BaseEstimator, ClassifierMixin):

    def fit(self, X, y):
        # 1. cria os stumps que podem ser usados (de acordo com iterations)
        # 2. inicializa pesos dos exemplos
        # 3. repete pelo número de iterations:
        # 3.1. seleciona melhor stump: self._pick_best_stump(...)
        # 3.2. calcula importância dele e seu erro, atualizando pesos: self._update_weights(...)
        # 3.3. se erro chegou a zero, sai antes de concluir o loop

    # métodos PRIVADOS
    def _pick_best_stump(self, X, y):
        # 1. percorre stumps e pega o que menos errou
        # percorre os exemplos verificando se este stump errou cada um
        # se stump errou este exemplo, soma seu peso ao erro do stump
        # e coloca o exemplo na lista mistaken_sample_ids
        # se o stump tiver sido melhor que os anteriores, guarda ele
        # retorna o melhor stump, seu erro e lista de exemplos errados

    def _update_weights(self, error, mistaken_sample_ids):
        # 1. encontra valor de alpha dado o error
        # 2. acha novos pesos desta iteração (sem normalizar ainda)
        # 3. normaliza os pesos
        return

Além desses métodos, a classe CoutoBoostClassifier do módulo couto_boost.py também possui métodos para classificação:

class CoutoBoostClassifier(BaseEstimator, ClassifierMixin):

    def decision_function(self, X, iterations_to_consider=None):
        # 1. invoca todos os stumps
        # 2. multiplica o resultado de cada um pelo alfa
        # 3. soma tudo e retorna um número (não necessariamente -1, 1)

    def predict(self, X, iterations_to_consider=None):
        # 1. delega para self.decision_function
        # 2. se > 0, retorna +1. Senão, -1

    def predict_proba(self, X):
        # 1. delega para self.decision_function
        # 2. retorna softmax desse resultado

O método predict_proba(X) é necessário para utilizarmos a validação cruzada do sklearn.

Experimentos

Após a implementação, os dados foram preparados para a condução de experimentos. Foi usada a implementação AdaBoostClassifier do pacote sklearn como baseline. Neste passo, apenas testes simples foram feitos com o AdaBoost e o CoutoBoost, que foram medidos quanto a sua acurácia/erro. Outras análises foram feitas na seção seguinte.

Nesta etapa de experimentos iniciais, o conjunto de dados foi dividido estaticamente entre X_train e X_test (80/20%). Apenas nas próximas análises é que foi realizada a validação cruzada.

Preparação dos dados

O algoritmo CoutoBoostClassifier implementado é uma versão simplificada do AdaBoost que espera receber as entradas com características binárias e a saída $\in \{-1, +1\}$.

Baseline: AdaBoost

Para fins de comparação, vamos resolver o problema usando a implementação de AdaBoost do pacote sklearn.ensemble.

Treinamento do AdaBoost:

Teste do AdaBoost:

CoutoBoost

Similar aos algoritmos do pacote sklearn, o CoutoBoostClassifier tem em sua interface pública os métodos fit(X, y) e predict(X), sendo que este último pode receber um parâmetro iterations que indica a quantidade de iterações que selecionarão uma hipótese, das que foram treinadas, que serão usadas para a classificação. Se omitido, todas as hipóteses serão usadas uma vez.

Treinamento do CoutoBoost:

Teste inicial do CoutoBoost:

Com o classificador CoutoBoost implementado, vamos ver como sua acurácia se comporta de acordo com o número de iterações:

A evolução do erro ao longo das iterações do CoutoBoost (figura à esquerda) mostrou que a redução do erro possui oscilação local, mas que é sustentada, não sofrendo de underfitting, nem de overfitting, mesmo sem a utilização de regularizador.

Esse é o comportamento esperado para as técnicas de ensemble, visto que ao empregar diversos modelos fracos - neste caso, decision stumps, a capacidade de cada modelo isoladamente é muito baixa, reduzindo a chance de sobreajuste. Por outro lado, como vários desses modelos fracos são usados, o processo de boosting tampouco sofre de subajuste, dada uma quantidade suficiente de modelos fracos.

No caso do CoutoBoost/AdaBoost, a técnica de ensemble é o boosting aditivo, que emprega uma combinação linear desses modelos fracos considerando que a "opinião" da maioria deles converge para um modelo forte. Neste problema foi, de fato, possível observar esse comportamento.

Já a análise da matriz de confusão (figura à direita) é interessante em situações de conjunto de dados desbalanceado. No caso deste problema, ela possibilitou verificar alta precisão e alta revocação, visto que as taxas de falsos negativos e falsos positivos foram zero ou próximas dele.

Análises

Além dos experimentos iniciais que verificaram que o CoutoBoost converge e atinge erros tão baixos quando a implementação baseline do AdaBoost, também foram conduzidas uma avaliação cruzada e outra específica do processo de boosting, realçando os valores internos do algoritmo como a evolução dos erros e a importância dos stumps, bem como as características dos stumps escolhidos em cada iteração.

Análise Cruzada

Como a análise cruzada faz sua própria divisão entre conjunto de treinamento e teste, a divisão anterior foi desfeita, resultado em variáveis X e y contemplando todo o conjunto rotulado.

A seguir, a função recebe um classificar e o conjunto de dados e rótulos, faz 5-fold cross validation e plota tanto um gráfico ROC-AUC quanto uma curva precisão-revocação.

O gráfico das curvas ROC (à esquerda) mostra que a área sob as curvas é bem grande, aproximando 1 em todos os folds. Além disso, o desvio padrão desse valor ficou em $0.0032$, bem baixo, indicando um bom grau de certeza quanto ao bom resultado da relação entre verdadeiros e falsos positivos.

A curva precisão-revocação (à direita) também apresenta uma grande área sob ela, indicando um bom balanceamento entre precisão e revocação. Isso quer dizer que aumentos da precisão não atrapalham a revocação, nem vice-versa. Outro indicativo positivo é a precisão média, cujo valor foi $1.00$.

Processo de Boosting

Também foram avaliados aspectos internos do processo de boosting como a evolução erros e a importância dos stumps, bem como o uso de stumps e as características da entrada a que se referem.

O gráfico de erro e importância do melhor stump a cada iteração (à esquerda) mostra a variação desses valores para o melhor stump que foi selecionado em cada iteração. Nele, é possível observar que o erro começa baixo (~$0.3$), rapidamente aumenta nas primeiras iterações e continua aumentando porém com taxa bem pequena, sugerindo saturação. Já a importância do stump, como é calculada com base no erro, evolui de forma análoga porém inversamente proporcional.

Também foram analisdas características dos stumps escolhidos ao longo das gerações (à direita). É possível observar que há repetição alternada de um subconjunto de todos 108 stumps. O eixo Y exibe o índice do stump escolhido na iteração referente ao eixo X. As cores indicam qual rótulo o stump está avaliando e os marcadores o preenchimento da célula. É possível observar que foram usados bastante stumps do jogador x e do o, mas não muitos referentes a células vazias do tabuleiro.