import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from time import time
%autosave 300
%matplotlib notebook
# Supress unnecessary warnings so that presentation looks clean
import warnings
warnings.filterwarnings('ignore')
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score
def test_model(clf, X_test, y_test):
print('\nClassification Report:')
test_time = time()
y_pred = clf.predict(X_test)
print('Prediciton time: {:.3f}s'.format(time() - test_time))
print('\n', classification_report(y_test, y_pred))
print('\nAccuracy: {:.3f}'.format(accuracy_score(y_test, y_pred)))
print('\nConfusion Matrix:')
print('\n', confusion_matrix(y_test, y_pred),'\n\n')
def train_model(model, parameters, scores, X_train, X_test, y_train, y_test, cv=5, name='model'):
clfs = []
for score in scores:
print('Training {} for {}'.format(name, score))
train_time = time()
clf = GridSearchCV(model, parameters, cv=cv, scoring=score)
clf.fit(X_train, y_train)
print('Finished Traininig in {:.3f}s'.format(time() - train_time))
print('Best parameters found:')
print(clf.best_params_)
test_model(clf, X_test, y_test)
clfs.append((score, clf.best_params_, clf))
return clfs
Foi testado o algoritmo de árvore de decisão para todo o dataset e ao final conseguiu um resultado de 93% de acurácia média.
data = pd.read_csv('covtype.data', names=label, index_col=None)
from sklearn.model_selection import train_test_split
x_train, x_test , y_train, y_test = train_test_split(data.iloc[:,:-1], data.iloc[:,-1], test_size=0.3,random_state=40)
model = DecisionTreeClassifier()
model.fit(x_train,y_train)
prediction = model.predict(data.iloc[:,:-1])
print(model.score(x_test,y_test))
from sklearn.metrics import confusion_matrix
confu = confusion_matrix(data.iloc[:,-1], prediction)
print(confu)
Extra-Trees é um algoritmo de floresta de árvores de decisão, que significa árvores extremamente randomizadas (extremely randomized trees). Foi proposta por Geurts Pierre, et al, em 2006 para classificações e regressões supervisionadas.
Esse método consiste em randomizar fortemente tanto atrivutos e escolhas de pontos de poda para nós da árvore. Onde o a decisão de um ponto ótimo de poda é responsável, em grande parte, pela variância na árvore inferida. Portanto, esse método em vez de utilizar cópias da amostra de aprendizado tenta achar o ponto ótimo de poda para cada uma das K caracteristicas escolhidas randomicamente em cada nó, onde seleciona um ponto de poda aleatório. Dessa forma, esse método aumenta a acurácia ao custo de diminuir um pouco a precisão do modelo.
Esse método randômico é bom para contextos onde problemas são categorizados por um grande número de caracteristicas númericas continuas. Onde o principal objetivo do algoritmo é a eficiência computacional, além da acurácia, de forma a otimizar o aprendizado em grandes volumes de dados.
Classes 1 e 2 representam 40% do dataset e 60% para as classes restantes (3-7).
dataset = pd.read_csv('143k_std.csv')
from sklearn.model_selection import train_test_split
target_col = dataset.shape[1]-1
X = dataset.iloc[:, :target_col]
y = list(map(int, dataset.iloc[:, target_col].values))
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.4)
from sklearn.ensemble import ExtraTreesClassifier
parameters = [
{'n_estimators': [target_col, 100], 'max_features':[0.75,0.8,0.85,0.9], 'n_jobs':[-1]}
]
scores = ['f1_macro', 'f1_micro','accuracy']
et_clfs = train_model(ExtraTreesClassifier(),
parameters,
scores,
X_train,
X_test,
y_train,
y_test,
name='ExtraTrees'
)
Visto o resultado de 97%, podemos ter certo ceticismo, já que na literatura podemos verificar apenas o resultado de 71%.
Sendo assim, verificamos a quantidade e a distribuição das classes no conjunto de dados para avaliação. Visto que, dependendo da distribuição das classes podemos experenciar diferentes resultados.
Pode-se notar que o conjunto de dados para validação está distribuido de forma satisfatória, porém por desencargo de consciência podemos testar o modelo para as amostras retiradas na fase de modelagem.
def plot_multbar(bars, title='', xlabel='', ylabel=''):
fig, ax = plt.subplots()
ind = np.arange(1,len(bars)+1)
plt.bar(ind, bars)
ax.set_xticks(ind)
ax.set_title('{}'.format(title))
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
plt.show()
target_sums = [y_test.count(i) for i in range(1, 8)]
print(target_sums)
plot_multbar(target_sums, 'Distribuição de Classes - Avaliação', 'Classes', 'Qnt')
Esse teste é realizado tendo em mente que as duas classes são as que mais se sobrepõem dentro do domÃnio.
from sklearn.preprocessing import StandardScaler
from sklearn.externals import joblib
ssc = joblib.load('stdscaler.pkl')
def pre_process(dataset):
try:
dataset = dataset.drop(['Soil7','Soil8','Soil15'], axis=1)
except Exception as e:
print(e)
y = dataset.loc[:, 'Target'].values
X = dataset.iloc[:, :-1].values
X = ssc.transform(X[:, 0:10])
X = np.concatenate((X, dataset.iloc[:, 10:-1].values), axis=1)
return X, y
dataset_val = pd.read_csv('143k_val.csv')
X_val, y_val = pre_process(dataset_val)
X_val = np.concatenate((X_val, X_test), axis=0)
y_val = np.concatenate((y_val, y_test), axis=0)
for s, p, clf in et_clfs:
print('\nTesting ET - {}'.format(p))
test_model(clf, X_val, y_val)
target_sums = [list(y_val).count(i) for i in range(1, 8)]
print(target_sums)
plot_multbar(target_sums, 'Distribuição de Classes - Avaliação', 'Classes', 'Qnt')
Com os resultados da matriz de confusão e os resultados das métricas, podemos notar que as classes 1 e 2 estão sobrepostas, sendo difÃcil classificá-las. Porém, mesmo com essa dificuldade conseguiu-se um resultado de 86% de acurácia ao final, uma melhora de 15% do artigo original.
Esse dataset tenta melhorar a acurácia para as classes 1 e 2, aumentando o número de amostras dessas classes para treinamento, totalizando 50% paras as duas primeiras classes e 50% para as demais.
dataset_50 = pd.read_csv('50-50_std.csv')
from sklearn.model_selection import train_test_split
target_col = dataset_50.shape[1]-1
X = dataset_50.iloc[:, :target_col]
y = list(map(int, dataset_50.iloc[:, target_col].values))
X_train_50, X_test_50, y_train_50, y_test_50 = train_test_split(X, y, test_size=0.4)
from sklearn.ensemble import ExtraTreesClassifier
parameters = [
{'n_estimators': [target_col, 100], 'max_features':[0.75,0.8,0.85,0.9], 'n_jobs':[-1]}
]
scores = ['f1_macro', 'f1_micro','accuracy']
et_clfs_50 = train_model(ExtraTreesClassifier(),
parameters,
scores,
X_train_50,
X_test_50,
y_train_50,
y_test_50,
name='ExtraTrees'
)
dataset_val_50 = pd.read_csv('50-50_val.csv')
X_val_50, y_val_50 = pre_process(dataset_val)
X_val_50 = np.concatenate((X_val_50, X_test_50), axis=0)
y_val_50 = np.concatenate((y_val_50, y_test_50), axis=0)
for s, p, clf in et_clfs_50:
print('\nTesting ET 50-50 - {}'.format(p))
test_model(clf, X_val_50, y_val_50)
Podemos notar com o tal teste, que um número maior de amostras nas classes 1 e 2 melhorou o dicernimento do modelo quanto essas classes. Porém, ainda podemos utilizar mais amostras para o treinamento e talvez tal treinamento possa melhorar ainda mais os resultados.
Foi criado um modelo para cada dataset, onde cada um foi manipulado de forma que as duas primeiras classes tivessem mais ou menos amostras. Para a validação de cada modelo foi utilizada o restante das amostras não treinadas.
Dataset | % Classes 1 e 2 | % Demais Classes |
---|---|---|
143k | 40% | 60% |
50-50 | 50% | 50% |
60-40 | 60% | 40% |
70-30 | 70% | 30% |
All | 85% | 15% |
datasets_names = ['143k', '50-50', '60-40', '70-30', 'All']
datasets_train = []
datasets_test = []
for name in datasets_names:
datasets_train.append(pd.read_csv('{}_std.csv'.format(name)))
datasets_test.append(pd.read_csv('{}_val.csv'.format(name)))
from sklearn.model_selection import train_test_split
from sklearn.ensemble import ExtraTreesClassifier
parameters = [
{'n_estimators': [target_col, 100], 'max_features':[0.75,0.8,0.85,0.9], 'n_jobs':[-1]}
]
scores = ['f1_macro', 'f1_micro','accuracy']
for d_train, d_test, name in zip(datasets_train, datasets_test, datasets_names):
print('\nTrain for dataset: {}'.format(name))
target_col = d_train.shape[1]-1
X = d_train.iloc[:, :target_col]
y = list(map(int, d_train.iloc[:, target_col].values))
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.4)
X_val, y_val = pre_process(d_test)
X_test = np.concatenate((X_val, X_test), axis=0)
y_test = np.concatenate((y_val, y_test), axis=0)
clfs = train_model(ExtraTreesClassifier(),
parameters,
scores,
X_train,
X_test,
y_train,
y_test,
name='ExtraTrees - {}'.format(name)
)
Após executar o treinamento para todos os datasets gerados, encontramos o melhor resultado, de 95.6% de acurácia, para o dataset completo. Porém, para o dataset onde as duas primeiras classes representam 70% do total, foi encontrado um resultado de 95.5%. Visto que o último é avaliado com um conjunto de avaliação significantemente maior, é mais seguro o utilizarmos como modelo final.
Portanto, utilizamos desse modelo para realizar ajustes finos e tentar melhorar o resultado final.
parameters = [
{'n_estimators': [100, 150, 200], 'max_features':[0.85,0.9,0.95], 'n_jobs':[-1]}
]
scores = ['f1_macro', 'f1_micro','accuracy']
d_train = datasets_train[3] # 70-30
d_test = datasets_test[3]
name = datasets_names[3]
print('\nTrain for dataset: {}'.format(name))
target_col = d_train.shape[1]-1
X = d_train.iloc[:, :target_col]
y = list(map(int, d_train.iloc[:, target_col].values))
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.4)
X_val, y_val = pre_process(d_test)
X_test = np.concatenate((X_val, X_test), axis=0)
y_test = np.concatenate((y_val, y_test), axis=0)
clfs = train_model(ExtraTreesClassifier(),
parameters,
scores,
X_train,
X_test,
y_train,
y_test,
name='ExtraTrees - {}'.format(name)
)
Vimos que apesar do aumento do n_estimators não houve um aumento significativo no resultado final.
Sendo assim, isso levanta uma dúvida: "Será que existe um n_estimators menor que mantenha o resultado de 95%?"
parameters = [
{'n_estimators': [40, 45, target_col], 'max_features':[0.85,0.9,0.95,1], 'n_jobs':[-1]}
]
scores = ['f1_macro', 'f1_micro','accuracy']
d_train = datasets_train[3] # 70-30
d_test = datasets_test[3]
name = datasets_names[3]
print('\nTrain for dataset: {}'.format(name))
target_col = d_train.shape[1]-1
X = d_train.iloc[:, :target_col]
y = list(map(int, d_train.iloc[:, target_col].values))
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.4)
X_val, y_val = pre_process(d_test)
X_test = np.concatenate((X_val, X_test), axis=0)
y_test = np.concatenate((y_val, y_test), axis=0)
clfs_min_nest = train_model(ExtraTreesClassifier(),
parameters,
scores,
X_train,
X_test,
y_train,
y_test,
name='ExtraTrees - {}'.format(name)
)
Após o treinamento utilizando o GridSearch podemos visualizar todos os treinamentos e avaliar a diferença entre a categoria mean_test_score
, que é o resultado final de treinamento. Podemos ver que apenas para a nona tentativa o resultado é discrepante. Todo o restante é coerente com os restantes dos treinamentos.
pd.DataFrame(clfs_min_nest[1][2].cv_results_)
Visto que o ajuste fino, para incremento e decremento no número de estimadores não causou mudanças significativas nas métricas (mudança de +-0.001% para acurácia), foi escolhido o modelo com base no tempo de treinamento e no tamanho final do modelo gerado. Para o modelo com >100 estimadores, um modelo final serializado tem um tamanho de >100MB, onde do outro lado, um modelo com o mesmo número de features, ou menos, para estimadores tem seu tamanho em torno de 40MB.
Para o modelo final foi selecionado o algoritmo Extra Trees, com os parâmetros: {'max_features': 0.95, 'n_estimators': 51, 'n_jobs': -1}
, treinado sobre o dataset com proporções de 70-30. Tal modelo alcancou uma acurácia de 95.4%, F1 score, recall e precisão de 95%.
from sklearn.externals import joblib
parameters = [
{'n_estimators': [51], 'max_features':[0.95], 'n_jobs':[-1]}
]
scores = ['f1_micro']
d_train = datasets_train[3] # 70-30
d_test = datasets_test[3]
name = datasets_names[3]
print('\nTrain for dataset: {}'.format(name))
target_col = d_train.shape[1]-1
X = d_train.iloc[:, :target_col]
y = list(map(int, d_train.iloc[:, target_col].values))
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.4)
X_val, y_val = pre_process(d_test)
X_test = np.concatenate((X_val, X_test), axis=0)
y_test = np.concatenate((y_val, y_test), axis=0)
final = train_model(ExtraTreesClassifier(),
parameters,
scores,
X_train,
X_test,
y_train,
y_test,
name='ExtraTrees - {}'.format(name)
)
joblib.dump(final[0][2].best_estimator_,'ExtraTree5195.pkl', compress=3)
Por fim podemos plotar os gráficos ROC para melhor ver a performance do modelo classificador.
def find_optimal_cutoff(false_pos_rate, true_pos_rate, threshold):
i = np.arange(len(true_pos_rate))
roc = pd.DataFrame({
'fpr':false_pos_rate,
'tpr':true_pos_rate,
'tf' : pd.Series(true_pos_rate-(1-false_pos_rate), index=i)
})
roc_t = roc.iloc[(roc.tf).abs().argsort()[0]]
return roc_t, roc_t['fpr'], roc_t['tpr']
from sklearn.metrics import roc_curve, auc
%matplotlib inline
y_pred = final[0][2].predict_proba(X_test)
for index, label in enumerate(range(1, 8)):
y_pred_i = y_pred[:, index]
false_positive_rate, true_positive_rate, thresholds = roc_curve(y_test, y_pred_i, pos_label=label)
roc_auc = auc(false_positive_rate, true_positive_rate)
optimal_threshold = find_optimal_cutoff(false_positive_rate, true_positive_rate, thresholds)
plt.figure(figsize=(20,10))
plt.title('Receiver Operating Characteristic (ROC)\nClasse {}'.format(label), fontsize=18)
plt.plot(false_positive_rate, true_positive_rate,
color='darkorange',
lw=2,
label='Curva ROC (Area = {:.4f})'.format(roc_auc))
plt.plot(optimal_threshold[1], optimal_threshold[2], 'b*', ms=15, label='Cutoff otimo')
plt.plot([0,1],[0,1], color='navy', lw=2, linestyle='--')
plt.xlim([-0.1,1.2])
plt.ylim([-0.1,1.2])
plt.ylabel('Taxa de Verdadeiro Positivo (Sensitivity)', fontsize=16)
plt.xlabel('Taxa de Falso Positivo (Specificity)', fontsize=16)
plt.legend(loc="lower right", fontsize=16)
plt.show()