Sitemap
Data Hackers

Blog oficial da comunidade Data Hackers

Conheça os principais conceitos de Machine Learning — 2/2

--

Detalhamos o processo de Regressão Linear — mas desta vez, utilizaremos a poderosa biblioteca scikit-learn em um caso de Classificação.

*Este artigo é uma continuação direta. Acompanhe a primeira parte.

A poderosa biblioteca

Finalmente, abordaremos assuntos mais interessantes — mas antes, falaremos sobre o uso de múltiplas características, detalhe este abstraído.

Geralmente, diversas características são usadas para a construção de um modelo de Aprendizado de Máquina — e até o momento detalhamos apenas a construção de um algoritmo de Regressão Linear Simples.

Então — diferentemente do passado, ao invés de explicar o processo da Regressão Logística Múltipla, deixaremos o trabalho para a biblioteca scikit-learn, oferecendo uma plataforma robusta para a maioria dos problemas.

Engenharia de Características

Em seguida, temos o resultado das transformações. Mas afinal, o que foi feito? Como dito anteriormente, precisamos transformar as características em algo que o algoritmo entenda — ou seja, números, mas não apenas isso.

# bibliotecas.
from scipy import stats
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from category_encoders import TargetEncoder
from sklearn.preprocessing import OrdinalEncoder
from sklearn.impute import KNNImputer

# recuperando dados.
train = pd.read_csv('train.csv')

# preenchendo a coluna com a mediana.
train.fillna({'Age': train['Age'].median()}, inplace = True)

# Criando uma nova coluna. Os elementos da coluna serão colocados em uma 'caixa' com limites.
train['age_bin'] = pd.cut(x = train['Age'],
bins = [train['Age'].min() - 1, 1, 8, 18, 65, train['Age'].max() + 1],
labels = [1, 2, 3, 4, 5]) #[1, 8, 18, 45, 65]
# extraindo os títulos sociais de cada passageiro.
names = []

for _ in train['Name']:
if "Mr." in _ :
names.append("Mr")
elif "Mrs." in _:
names.append("Mrs")
elif "Miss." in _:
names.append("Miss")
elif "Master." in _:
names.append("Master")
else:
names.append("Unknown")

names = pd.Series(names, name = "Social")

# criando uma coluna com os títulos.
train['Social'] = names

# Instanciado o transformador. Ele transforma colunas categoricas em inteiros.
enc_encoder = OrdinalEncoder()

# Encaixando/transformando a coluna. Cada elemento único será tranformado em inteiro.
train['Social'] = enc_encoder.fit_transform(np.asarray(train['Social']).reshape(-1,1))

# transformando a coluna com condicionais.
train['Sex'] = np.where(train['Sex'] == 'male', 0, 1)

# preenchendo a coluna com o valor indicado.
train.fillna({'Fare': 7.500}, inplace = True)
train['Fare'] = train['Fare'].astype(int)

# Criando uma nova coluna. Os elementos da coluna serão colocados em uma 'caixa' com limites.
train['fare_bin'] = pd.cut(x = train['Fare'],
bins = [- 1, 10, 20, 50, 100, train['Fare'].max() + 1],
labels = [1, 2, 3, 4, 5] )

# Transformando a coluna com condicionais.Neste caso, caso a coluna seja nula, será atribuido o valor 1.
train['cabin_isnull'] = np.where(train['Cabin'].isnull(),1,0)

# criando uma coluna relacional.
train['Social_sex_interaction'] = train['Social'] * train['Sex']

# removendo colunas desnecessárias/manipuladas.
train = train.drop(['Name', 'SibSp', 'Parch', 'Ticket', 'Fare', 'Age', 'Embarked', 'Cabin', 'PassengerId'], axis = 1)

# observando resultado.
train

Como podemos ver, o valor pago pela passagem (representado pela coluna Fare) foi transformado em uma categoria com limites pré-estabelecidos. Neste caso, a coluna contém 90 elementos distintos — e para o nosso modelo, essa dispersão de valores pode inferir dele em compreender os padrões contidos nos dados.

Essas variações podem ser interpretadas como sendo mais importantes do que realmente são, e ao categorizar estes valores, tornamos os padrões mais claros.

Dependemos da linha na qual o elemento está inserido para definir se é o valor pago pelo passageiro (único), ou o valor pago pela família (agrupado),

Para observarmos isso, a coluna foi agrupada com o resultado verdadeiro, significando que, além de estar demonstrando os limites que estabelecemos, ela também mostra se o passageiro em questão sobreviveu.

Em seguida, temos o agrupamento dos sobreviventes com sua respectiva classe de passagem — coluna esta que não foi manipulada. Note que os gráficos são semelhantes, mas não necessariamente pela manipulação, e sim pelo que representam.

Dessa maneira, estamos informando ao modelo que não há diferença entre um passageiro que pagou 5 e um que pagou 6. Mas — certamente, há diferença entre um que pagou 4 a um que pagou 40 — pois estes são de diferentes categorias, e devem ser representados como tal.

Além disso, reforçando que há padrões entre as características que definem a sobrevivência de cada passageiro, e assim, precisamos expô-las. Agora, o mesmo caso sem a categorização.

Os padrões serão mais fáceis de serem compreendidos no primeiro caso.

Treinamento, Validação e Teste

Desta vez, ficaremos atentos ao conjunto de dados — mais precisamente, na razão pela qual estão separados.

# info do conjunto.
train.info()
class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Survived 891 non-null int64
1 Pclass 891 non-null int64
2 Sex 891 non-null int32
3 age_bin 891 non-null category
4 Social 891 non-null float64
5 fare_bin 891 non-null category
6 cabin_isnull 891 non-null int32
7 Social_sex_interaction 891 non-null float64
dtypes: category(2), float64(2), int32(2), int64(2)
memory usage: 37.1 KB
# info do conjunto.
test.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 418 entries, 0 to 417
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Pclass 418 non-null int64
1 Sex 418 non-null int32
2 age_bin 418 non-null category
3 Social 418 non-null float64
4 fare_bin 418 non-null category
5 cabin_isnull 418 non-null int32
6 Social_sex_interaction 418 non-null float64
dtypes: category(2), float64(2), int32(2), int64(1)
memory usage: 14.4 KB

E essa razão é simples. Aprenderíamos tão bem as correlações — caso utilizássemos todos os dados para o Treinamento, que não conseguiríamos prever de forma correta dados que viessem de fora do Treinamento.

E o pior acontece quando não utilizamos um número satisfatório — já que as correlações entre as características ainda podem não ter sido compreendidas pelo algoritmo.

Como vimos, essas correlações habilitam o modelo a prever de forma correta. Então, não teremos um bom desempenho em aprender as correlações e, após isso, ao prevê-las.

O Kaggle sabe disso — mas, reforçando a situação, precisamos encontrar uma forma a que o modelo aprenda as correlações, mas não se adapte a elas. Assim, precisamos dividir o conjunto, e com os produtos dessa divisão, utilizaremos elas— em etapas, para a construção do modelo.

O modelo será construído junto aos dados provenientes da divisão que denominamos de Treinamento. Após isso, faremos ajustes no modelo — tunando os hiperparâmetros, com intuito de melhorar o desempenho das predições.

Depois, realizaremos predições em dados que não foram vistos, com o nosso modelo ajustado pela Validação.

Como sabemos o resultado verdadeiro, podemos quantificar a performance do modelo com dados que não foram vistos no treinamento. Neste caso, prever a etapa que denominamos de Testes, comparando com o resultado verdadeiro.

Em problemas maiores, a Validação é necessária para quantificar a qualidade destes ajustes. Contudo, neste caso específico, não podemos dispor de uma parte dos dados — já que não temos muitos deles para começar.

Por isso, utilizaremos menos etapas — treinando o modelo com uma parte do primeiro arquivo — e futuramente, utilizaremos o próprio modelo para prever os dados do segundo.

Por fim, haverá uma divisão interna no Treinamento — mas ela não é considerada uma Validação.

# biblioteca.
from sklearn.model_selection import train_test_split

# Criando X & y. Como vimos, X representando as característica e y representando o resultado verdadeiro.
X = train.drop("Survived", axis = 1)
y = train["Survived"]

# divisão interna do primeiro arquivo.
X_train, X_test, y_train, y_test = train_test_split(X,y, train_size= 0.68, random_state = 42)

# checando cada divisão.
X_train.shape, X_test.shape, y_train.shape, y_test.shape
((605, 7), (286, 7), (605,), (286,))

Escolha de estimadores

No passado, detalhamos o processo de criação do algoritmo — mas ao utilizar a biblioteca, iremos instanciá-lo. Agora, estes são chamados de estimadores, e podemos criar instâncias deles — não precisando necessariamente saber qual deles irá sobressair ao outro.

Por exemplo, a Regressão Logística é passível a outliers — pontos de dados que desviam significativamente do resto do conjunto. Em contrapartida, ela requer poucos ajustes e tem uma ótima interpretabilidade.

Já o Random Forest é conhecido por sua robustez a estes desvios, além disso, ele nos dá a porcentagem de quanto cada característica influenciou para chegar naquele resultado, sendo útil para criar novas colunas pertinentes ao conjunto.

Como pode ser visto — na Engenharia de Características, há um relacionamento entre colunas, sendo o produto da multiplicação de duas outras características.

Segundo o estimador, estas foram extremamente importantes para o resultado final, e assim, podemos relacioná-las — caso não introduza ruído, e certamente, aumente o desempenho do modelo.

De qualquer forma, diferentes estimadores têm suas vantagens — mas com a biblioteca, estes serão mais fáceis de serem analisados, e possivelmente, substituídos.

Além disso, podemos escolher o melhor estimador baseado na própria biblioteca.

# importando estimadores.
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression

# instanciando estimadores.
clf = RandomForestClassifier(random_state= 42)
clf_2 = LogisticRegression(random_state = 42)

Encontrando os Hiperparâmetros

Como podemos ver, instanciamos dois estimadores em uma variável — que futuramente, será encaixada com o conjunto processado. Após disso, podemos ajustar o modelo — ou melhor, encontrar os melhores Hiperparâmetros*.

Hiperparâmetros são configurações dentro do estimadores encarregadas no processo de aprendizagem. Alguns Hiperparâmetros podem ser estabelecidos — não precisando ser encontrados.

Neste caso, o número de passageiros que não sobreviveram muito excede os que de fato sobreviveram.

Dessa forma, temos um desbalanceamento de classes — e para corrigir isso, podemos indicar ao modelo que dê mais atenção naquela com um número menor de exemplos. Em outras palavras, os passageiros que sobreviveram.

Contudo, procuraremos por estes.

# garantindo reprodutibilidade.
np.random.seed(42)

# encaixando os dados aos estimadores.
clf.fit(X_train, y_train); # Random Forest.
clf_2.fit(X_train, y_train); # Regressão Logística.

Após encaixar o conjunto no estimador, estamos prontos para prever. Contudo, uma recapitulação.

Treinamos o modelo com uma parte do primeiro arquivo — e assim, utilizaremos o modelo para prever o restante deste mesmo arquivo — divisão esta, que não foi vista no Treinamento.

Futuramente, queremos comparar o valor dessas predições com o resultado que sabemos que é real, mas como sabemos das limitações do conjunto, não utilizaremos a etapa de Validação.

Em seguida, devemos prever a divisão que não utilizamos no Treinamento — e logo após, extrair métricas.

# garantindo reprodutibilidade.
np.random.seed(42)

# biblioteca.
from sklearn.metrics import accuracy_score

# prevendo o resultado.
ypreds = clf.predict(X_test)
ypreds_2 = clf_2.predict(X_test)

# colocando numa variável o resultado da métrica.
resultado_random_forest = accuracy_score(ypreds, y_test)
resultado_regressão_logistica = accuracy_score(ypreds_2, y_test)

# manipulando o saída para melhor vizualização.
print(f"Random Forest : {resultado_random_forest * 100:.4f}%")
print(f"Regressao Logistica : {resultado_regressão_logistica * 100:.4f}%")
Random Forest           : 81.1189%
Regressao Logistica : 81.4685%
# parâmetros do matplotlib.
plt.style.use("seaborn-v0_8-darkgrid")
plt.rcParams["font.family"] = "serif"

# biblioteca.
from sklearn.metrics import confusion_matrix
import seaborn as sns

# Instanciando eixos.
fig, ((ax1,ax2)) = plt.subplots(nrows = 1, ncols = 2, figsize= (8,5))

# criando uma matriz de confusão.
matrix = confusion_matrix(y_test, ypreds) # Random Forest.
matrix_2 = confusion_matrix(y_test, ypreds_2) # Regressão Logística.

# criando descrições a serem colocados para matrix.
labels = [f"Verdadeiro Positivo\n{matrix[0][0]}",f"Falso Positivo\n{matrix[0][1]}",f"Falso Negativo\n{matrix[1][0]}",f"Verdadeiro Negativo\n{matrix[1][1]}"]
labels = np.asarray(labels).reshape(2,2)

# criando descrições a serem colocados para matrix_2.
labels_2 = [f"Verdadeiro Positivo\n{matrix_2[0][0]}",f"Falso Positivo\n{matrix_2[0][1]}",f"Falso Negativo\n{matrix_2[1][0]}",f"Verdadeiro Negativo\n{matrix_2[1][1]}"]
labels_2 = np.asarray(labels_2).reshape(2,2)

# criando um gráfico a partir da matrix.
sns.heatmap(matrix/np.sum(matrix), annot = labels,
fmt = '', cmap='GnBu', ax = ax1, cbar = False);

# criando um gráfico a partir da matrix_2.
sns.heatmap(matrix_2/np.sum(matrix_2), annot = labels_2,
fmt = '', cmap='GnBu', ax = ax2, cbar = False);

# título.
ax1.set_title("Random Forest")
ax2.set_title("Regressao Logistica")

Aliados com a Accuracy Score (métrica usada na competição), temos a Matriz de Confusão — tabela que visualizamos a performance de um modelo de Classificação.

Contudo, ela é diferente das métricas que usamos — mostrando exatamente a situação do modelo. Por exemplo, o Falso Positivo, são as vezes que o modelo previu o resultado como verdadeiro — mas de fato, ele era falso.

Neste caso, queremos as instâncias onde o modelo previu corretamente a classe, sendo a diagonal: Verdadeiro Positivo e Verdadeiro Negativo — que, após calculada, nos dá a própria Accuracy Score.

Com isso, podemos buscar estes ajustes aleatoriamente, mas também podemos verificar cada variação individualmente. Em ambos os casos, uma grade com ajustes precisa ser montada — e cabe ao tipo de busca encontrar pelos melhores ajustes.

Por fim — para aqueles utilizando serviços na nuvem, cautela. Estes são extremamente custosos para serem encontrados.

Tunando o modelo.

Por hora, utilizaremos o Random Forest. Assim, buscaremos os melhores ajustes deste— evitando a cara computação que comentamos.

# garantindo reprodutibilidade.
np.random.seed(42)

# bibliotecas.
from sklearn.model_selection import RandomizedSearchCV

# Grid com os ajustes que queremos buscar. Note que, há ajustes estabelecidos.
grid = { 'n_estimators': [100, 150, 200, 250, 350, 500],
'max_depth': [None, 7, 10, 15, 20],
'min_samples_split': [ 2, 6, 10],
'min_samples_leaf': [1, 2, 4, 6],
'max_features': ['sqrt', 'log2', None],
'criterion': ['gini'],
'class_weight': ['balanced'],
}

# instanciando estimador randomizado.
random_forest_randomizada = RandomizedSearchCV(
estimator=RandomForestClassifier(),
param_distributions = grid,
cv = 3,
n_iter = 1000,
random_state = 42,
verbose = 3,
scoring = "accuracy",
pre_dispatch = 4,
n_jobs = -1)

# encaixando dados ao estimador.
random_forest_randomizada.fit(X_train, y_train)
# garantindo reprodutibilidade.
np.random.seed(42)

# prevendo o resultado com o estimador 'ajustado'.
ypreds_3 = random_forest_randomizada.predict(X_test);

# obetendo métricas.
resultado_random_forest_grid = accuracy_score(y_test, ypreds_3)

# adicionando o resultado obtivo com o que já tinhamos.
print(f"Random Forest : {resultado_random_forest * 100:.4f}%")
print(f"Regressão Logística : {resultado_regressão_logistica * 100:.4f}%")
print(f"Random Forest Ajustada : {resultado_random_forest_grid * 100:.4f}%")

Random Forest : 81.1189%
Regressão Logística : 81.4685%
Random Forest Ajustada : 82.8671%

Com isso, tivemos um aumento relativo no desempenho, e as coisas melhoraram— e muito, na Matriz de Confusão. Além disso, devemos recuperar os melhores hiperparâmetros. Como pode ser visto abaixo.

# recupeando ajustes.
random_forest_randomizada.best_params
{'n_estimators': 150,
'min_samples_split': 6,
'min_samples_leaf': 1,
'max_features': None,
'max_depth': 20,
'criterion': 'gini',
'class_weight': 'balanced'}

Submissão

A partir daqui, podemos trilhar diversos caminhos. Mas antes, uma nova recapitulação. Treinamos o modelo com uma divisão do primeiro arquivo, em seguida, ajustamos o modelo — resultando em melhores predições.

Agora, treinaremos o estimador com todo o arquivo, e submeteremos a previsão do segundo para o site. Lembrando que, devemos manipular os dados desta etapa, exatamente como fizemos na Engenharia de Características.

Contudo, um pequeno truque. Aumentaremos o número de acertos para classe desbalanceada utilizando o modelo para prever a probabilidade da classe— favorecendo aquela com mais exemplos.

Em outras palavras, queremos que acerte mais quando o passageiro não sobreviveu — já que, temos mais deles em nosso conjunto.

# garantindo reprodutibilidade.
np.random.seed(12)

# instanciando o estimador com Hiperparamêtros encontrados.
clf = RandomForestClassifier(n_estimators = 150,
min_samples_split = 6,
min_samples_leaf = 1,
max_features = None,
max_depth = 20,
criterion = 'gini',
class_weight = 'balanced')

# encaixando dados de todo o primeiro arquivo.
clf.fit(X,y)

# prevendo a probabilidade do resultado do segundo.
ypreds = clf.predict_proba(test)[:,1]

# manipulando as prediçoes de probabilidade.
ypreds_lim = []
for _ in ypreds:
if _ >= 0.68:
_ = 1
ypreds_lim.append(_)
else:
_ = 0
ypreds_lim.append(_)
# submetendo as predições para o Kaggle. 
submission = pd.DataFrame({'PassengerId': pd.read_csv('test.csv')['PassengerId'] , 'Survived': ypreds_lim})

# criando um arquivo CSV.
submission.to_csv('Submission.csv', index = False)

Por fim, este foi o resultado que conseguimos com um único estimador. Contudo, há mais que podemos fazer, mas certamente, no próximo artigo. De qualquer maneira, obrigado por terem acompanhado, e até mais.

Considerações finais.

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Obrigado por terem acompanhado até aqui. Certamente, foi divertido escrever este artigo, mas tenham cuidado com este único aspecto do Aprendizado de Máquina.

Como vocês podem ver, nosso resultado não foi tão bom quanto alguns podem ter imaginar, e sim, há mais que possamos fazer. Contudo, o problema deste competição específica é a falta de dados.

Assim, não fiquem desmotivados, caso tentarem por e o resultado ser baixo. O importante, literalmente, é tentar. E por fim, agora de vez, ficamos por aqui.

--

--

No responses yet