Comparação de modelos de Decision Tree de Regressão e Classificação

Author

João F. Quentino

Essa atividade se trata sobre comparar a acurácia dos modelos Cart e M5 em um dataset de classificação, e comparar o algoritmo CART e C5.0 em um dataset de regressão. Os datasets escolhidos foram um conjunto de vinhos envolvendo sua origem, e outro conjunto sobre acidentes de trânsito envolvendo pedestres na república tcheca.

Procedimento padrão:

  • Leitura dos dados;
  • Exclusão de NA (neste caso: ‘?’);
  • Get_dummies devido a presença de variáveis categóricas;
  • Seleção do alvo/target (income) e das features. X e y, respectivamente;
  • Separando os dados em conjuntos de treino e teste (train_test_split do sklearn).
Code
import pandas as pd
import numpy as np
from sklearn.tree import DecisionTreeClassifier, ExtraTreeClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, accuracy_score

# Lendo o dataset
data = pd.read_csv('data/pedestrian.csv')
# Removendo as linhas com valores faltantes NA
data = data.dropna()

# REmovendo as duas primeiras colunas: Unnamed e Id
data = data.drop(['Unnamed: 0', 'id'], axis=1)
# Separando os dados em features e target
data_target = data['pedestrian_condition']
data_features = data.drop('pedestrian_condition', axis=1)

# Convertendo variáveis categóricas em dummies
data_features = pd.get_dummies(data_features)
# features names 
feature_names = data_features.columns.tolist()

# Dividindo os dados em treino e teste
X_train, X_test, y_train, y_test = train_test_split(data_features, data_target, test_size=0.3, random_state=1)

Agora que estabelecemos a base para os modelos, iremos utilizar a Random Search para acharmos os melhores hiperparâmetros.

Code
from sklearn.model_selection import RandomizedSearchCV
# Definindo os hiperparâmetros e as distribuições para a busca aleatória
param_dist = {"max_depth": [None] + list(np.arange(2, 20)),
              "min_samples_split": np.arange(2, 20),
              "min_samples_leaf": np.arange(1, 20),
              "criterion": ["gini", "entropy"]}

# Inicializando o classificador de árvore de decisão
tree = DecisionTreeClassifier()

# Inicializando a busca aleatória
random_search = RandomizedSearchCV(tree, 
                                   param_distributions=param_dist, n_iter=100, cv=3, random_state=0, n_jobs=-1)

# Executando a busca aleatória
random_search.fit(X_train, y_train)
# Imprimindo os melhores hiperparâmetros encontrados
print(random_search.best_params_)
{'min_samples_split': 19, 'min_samples_leaf': 15, 'max_depth': 10, 'criterion': 'entropy'}

Padronizando as features

Code
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
scaler.fit(X_train) # calcula a média e o desvio padrão para cada coluna
 
X_train = scaler.transform(X_train) # subtrai a média e divide pelo desvio padrão
X_test = scaler.transform(X_test) # subtrai a média e divide pelo desvio padrão

Fazendo o modelo CART

Code
def train_model_cart(height):
    model = DecisionTreeClassifier(criterion='entropy',
    max_depth=height, min_samples_split=14, min_samples_leaf=16,random_state=0) #  cria o modelo
    model.fit(X_train, y_train) # treina o modelo
    return model

Fazendo o modelo M5

Code
def train_model_m5():
    model = ExtraTreeClassifier(min_samples_leaf=10, min_samples_split=11) #  cria o modelo, na M5 a altura pode ser ilimitada
    model.fit(X_train, y_train) # treina o modelo
    return model

Executando o modelo CART

Code
# Acurácia para diferentes alturas da árvore CART
print('---------------------- CART --------------------------')
for height in range(1, 20): # testa diferentes alturas para a árvore
    model = train_model_cart(height)
    y_pred = model.predict(X_test) # faz a predição

    print('--------------------------------------------------')
    print(f'Altura - {height}\n')
    print(f'Acurácia: {accuracy_score(y_test, y_pred)}') # acurácia

# A maior acurácia foi de 0.75, ALTURA 7
---------------------- CART --------------------------
--------------------------------------------------
Altura - 1

Acurácia: 0.7372084928646014
--------------------------------------------------
Altura - 2

Acurácia: 0.7199791159067177
--------------------------------------------------
Altura - 3

Acurácia: 0.7252001392272885
--------------------------------------------------
Altura - 4

Acurácia: 0.734075878872259
--------------------------------------------------
Altura - 5

Acurácia: 0.7408632091890011
--------------------------------------------------
Altura - 6

Acurácia: 0.7436477549599722
--------------------------------------------------
Altura - 7

Acurácia: 0.7537417333797425
--------------------------------------------------
Altura - 8

Acurácia: 0.7457361642882004
--------------------------------------------------
Altura - 9

Acurácia: 0.7467803689523147
--------------------------------------------------
Altura - 10

Acurácia: 0.7462582666202575
--------------------------------------------------
Altura - 11

Acurácia: 0.7483466759484859
--------------------------------------------------
Altura - 12

Acurácia: 0.7473024712843718
--------------------------------------------------
Altura - 13

Acurácia: 0.7473024712843718
--------------------------------------------------
Altura - 14

Acurácia: 0.7474765053950574
--------------------------------------------------
Altura - 15

Acurácia: 0.7473024712843718
--------------------------------------------------
Altura - 16

Acurácia: 0.7473024712843718
--------------------------------------------------
Altura - 17

Acurácia: 0.7473024712843718
--------------------------------------------------
Altura - 18

Acurácia: 0.7473024712843718
--------------------------------------------------
Altura - 19

Acurácia: 0.7473024712843718

Exibindo a Árvore do modelo CART (abra a imagem em uma nova guia)

Code
# Exibindo a árvore de decisão
from IPython.display import Image
from sklearn.tree import export_graphviz
import pydotplus

model = train_model_cart(7) # treina o modelo com altura 11
classes_names = [str(i) for i in model.classes_]

dot_data = export_graphviz(model, filled=True, feature_names=feature_names, class_names=classes_names, rounded=True, special_characters=True) # cria o gráfico
graph = pydotplus.graph_from_dot_data(dot_data) 

Image(graph.create_png()) # exibe o gráfico
graph.write_png('cart-pedestrian.png') # salva o gráfico
Image('cart-pedestrian.png') 

Executando o modelo M5

Code
print('----------------------- M5 ---------------------------')
for epocs in range(1, 21):
    model = train_model_m5()
    y_pred = model.predict(X_test) # faz a predição

    print('--------------------------------------------------')
    print(f'Teste {epocs}\n')
    print(f'Acurácia: {accuracy_score(y_test, y_pred)}') # acurácia

# A acurácia em alguns casos (randômicos) a árvore M5 é melhor em comparação com a árvore CART
# A maior acurácia foi de 0.759, com 20 testes reproduzidos várias vezes.
----------------------- M5 ---------------------------
--------------------------------------------------
Teste 1

Acurácia: 0.7476505395057431
--------------------------------------------------
Teste 2

Acurácia: 0.7535676992690568
--------------------------------------------------
Teste 3

Acurácia: 0.7438217890706579
--------------------------------------------------
Teste 4

Acurácia: 0.7521754263835712
--------------------------------------------------
Teste 5

Acurácia: 0.7483466759484859
--------------------------------------------------
Teste 6

Acurácia: 0.7460842325095719
--------------------------------------------------
Teste 7

Acurácia: 0.757396449704142
--------------------------------------------------
Teste 8

Acurácia: 0.7502610511660286
--------------------------------------------------
Teste 9

Acurácia: 0.7483466759484859
--------------------------------------------------
Teste 10

Acurácia: 0.731813435433345
--------------------------------------------------
Teste 11

Acurácia: 0.7547859380438566
--------------------------------------------------
Teste 12

Acurácia: 0.7432996867386008
--------------------------------------------------
Teste 13

Acurácia: 0.7368604246432301
--------------------------------------------------
Teste 14

Acurácia: 0.742603550295858
--------------------------------------------------
Teste 15

Acurácia: 0.7518273581621998
--------------------------------------------------
Teste 16

Acurácia: 0.7387747998607727
--------------------------------------------------
Teste 17

Acurácia: 0.7532196310476853
--------------------------------------------------
Teste 18

Acurácia: 0.7460842325095719
--------------------------------------------------
Teste 19

Acurácia: 0.7537417333797425
--------------------------------------------------
Teste 20

Acurácia: 0.7523494604942569

Exibindo a árvore M5 (abra a imagem em uma nova guia)

Code
model = train_model_m5() 
classes_names = [str(i) for i in model.classes_]

dot_data = export_graphviz(model, filled=True, feature_names=feature_names, class_names=classes_names, rounded=True, special_characters=True) # cria o gráfico
graph = pydotplus.graph_from_dot_data(dot_data) 

Image(graph.create_png()) # exibe o gráfico
graph.write_png('m5-pedestrian.png') # salva o gráfico
Image('m5-pedestrian.png') 

Considerações entre M5 e CART:

Os dois modelos obtiveram uma acurácia praticamente idêntica, mas isso se deve pela procura dos melhores hiperparâmetros para o modelo do CART, como o modelo M5 funciona basicamente atraveś de ‘descent gradient’, em alguns cenários ele vai obter uma acurácia superior (+1%), em outros casos vai se manter perto de outras alturas !=7 do modelo CART.

Comparação entre CART e C5.0 no conjunto wine.data

Realizar o mesmo procedimento do CART realizado no dataset anterior

Code
import numpy as np
import pandas as pd
import matplotlib.pyplot as plot
import seaborn as sns
import pydotplus
     
dataset = pd.read_csv('data/wine.data', header=None)

dataset.columns = ['label',
                   'alcohol',
                   'malic_acid',
                   'ash',
                   'alcalinity_of_ash',
                   'magnesium',
                   'total_phenols',
                   'flavanoids',
                   'nonflavanoid_phenols', 
                   'proanthocyanins', 
                   'color_intensity', 
                   'hue',
                   'OD280/OD315',
                   'proline']

# Divisão treino-teste

from sklearn.model_selection import train_test_split

X = dataset.values[:, 1:]
y = dataset.values[:, 0] # a primeira coluna do dataset indica a origem do vinho
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=0)

# Feature Scaling

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
scaler.fit(X_train) # calcula a média e o desvio padrão para cada coluna
 
X_train = scaler.transform(X_train) # subtrai a média e divide pelo desvio padrão
X_test = scaler.transform(X_test) # subtrai a média e divide pelo desvio padrão

# Treinamento do modelo

from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score

def train_model(height):
    model = DecisionTreeClassifier(criterion='entropy', max_depth=height, random_state=0) #  cria o modelo
    model.fit(X_train, y_train) # treina o modelo
    return model

Acurácia de CART

Code
for height in range(1, 21): # testa diferentes alturas para a árvore
    model = train_model(height)
    y_pred = model.predict(X_test) # faz a predição

    print('--------------------------------------------------')
    print(f'Altura - {height}\n')
    print(f'Acurácia: {accuracy_score(y_test, y_pred)}') # acurácia
--------------------------------------------------
Altura - 1

Acurácia: 0.5555555555555556
--------------------------------------------------
Altura - 2

Acurácia: 0.9444444444444444
--------------------------------------------------
Altura - 3

Acurácia: 0.9444444444444444
--------------------------------------------------
Altura - 4

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 5

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 6

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 7

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 8

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 9

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 10

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 11

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 12

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 13

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 14

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 15

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 16

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 17

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 18

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 19

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 20

Acurácia: 0.9166666666666666

Visualização da árvore

Code
# Visualização da árvore de decisão

from IPython.display import Image
from sklearn.tree import export_graphviz

model = train_model(8) # treina o modelo com altura 3

feature_names = ['alcohol',
                 'malic_acid',
                 'ash',
                 'alcalinity_of_ash', 
                 'magnesium', 
                 'total_phenols', 
                 'flavanoids', 
                 'nonflavanoid_phenols', 
                 'proanthocyanins', 
                 'color_intensity', 
                 'hue',
                 'OD280/OD315',
                 'proline']

classes_names = ['%.f' % i for i in model.classes_]

dot_data = export_graphviz(model, filled=True, feature_names=feature_names, class_names=classes_names, rounded=True, special_characters=True) # cria o gráfico
graph = pydotplus.graph_from_dot_data(dot_data) # cria o gráfico

Image(graph.create_png()) # exibe o gráfico
graph.write_png('classification-tree.png') # salva o gráfico
Image('classification-tree.png')

Modelo C5.0

Code
def train_model_c5(height):
    # A árvore C5 se diferencia da CART pela escolha dos intervalos de corte
    clf = DecisionTreeClassifier(criterion='entropy', splitter='best', max_depth=height, min_samples_split=2, 
                                min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features=None, 
                                random_state=None, max_leaf_nodes=None, min_impurity_decrease=0.0, 
                                class_weight=None)
    clf.fit(X_train, y_train)
    return clf

Testando o modelo

Code
for height in range(1, 21): # testa diferentes alturas para a árvore
    model = train_model_c5(height)
    y_pred = model.predict(X_test) # faz a predição

    print('--------------------------------------------------')
    print(f'Altura - {height}\n')
    print(f'Acurácia: {accuracy_score(y_test, y_pred)}') # acurácia
--------------------------------------------------
Altura - 1

Acurácia: 0.5555555555555556
--------------------------------------------------
Altura - 2

Acurácia: 0.9444444444444444
--------------------------------------------------
Altura - 3

Acurácia: 0.9444444444444444
--------------------------------------------------
Altura - 4

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 5

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 6

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 7

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 8

Acurácia: 0.9444444444444444
--------------------------------------------------
Altura - 9

Acurácia: 0.9444444444444444
--------------------------------------------------
Altura - 10

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 11

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 12

Acurácia: 0.9444444444444444
--------------------------------------------------
Altura - 13

Acurácia: 0.9444444444444444
--------------------------------------------------
Altura - 14

Acurácia: 0.9444444444444444
--------------------------------------------------
Altura - 15

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 16

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 17

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 18

Acurácia: 0.9444444444444444
--------------------------------------------------
Altura - 19

Acurácia: 0.9166666666666666
--------------------------------------------------
Altura - 20

Acurácia: 0.9444444444444444

A maior acuráciafoi 0.94444… , que foi obtida tanto em alturas menores e alturas maiores.

Exibindo a árvore

Code
classes_names = [str(i) for i in model.classes_]
model = train_model_c5(8) # treina o modelo com altura 3
dot_data = export_graphviz(model, filled=True, feature_names=feature_names, class_names=classes_names, rounded=True, special_characters=True) # cria o gráfico
graph = pydotplus.graph_from_dot_data(dot_data) # cria o gráfico

Image(graph.create_png()) # exibe o gráfico
graph.write_png('regression-tree-c5.png') # salva o gráfico
Image('regression-tree-c5.png')

Conclusão:

O algoritmo c5 funcionou de uma maneira peculiar, tanto em alturas altas (15, 18, 19, 20) quanto em alturas baixas (2, 3) ele obteve uma maior acurácia: 94.4%. Isso se deve pelo fato do intervalo de corte (parâmetro splitter) ser do tipo ‘best’, fazendo com que o algoritmo faça de tudo o possível para a entropia diminuir, independentemente da altura. É um algoritmo mais usual para o usuário, diferente do CART que é necessário você colocar hiperparâmetros que o programador provavelmente só irá descobrir qual é o melhor pela tentativa e erro. Além de que o desempenho no dataset de vinhos ter sido inferior a partir da altura 4, com uma acurácia reduzida em mais de 3%. O CART e C5.0 teve o mesmo desempenho em alturas menores, mas o C5.0 conseguiu ter um desempenho alto em algumas alturas maiores.