Pular para o conteúdo principal

Introdução ao machine learning em Python

Neste tutorial, você vai conhecer o mundo do machine learning (ML) com Python. Para entender ML na prática, usaremos o algoritmo K-Nearest Neighbor (KNN) em Python.
· 14 min leer

Explore com IA

Você vai implementar KNN no famoso conjunto de dados Iris.

Observação: vale a pena fazer o curso Machine Learning with Python ou, para entender o contexto da evolução do ML e muito mais, ler este post.

Introdução

Machine learning surgiu da ciência da computação e estuda principalmente o design de algoritmos que aprendem com a experiência. Para aprender, eles precisam de dados com certos atributos, a partir dos quais os algoritmos buscam padrões preditivos relevantes. De forma geral, as tarefas de ML podem ser categorizadas em aprendizagem de conceitos, clusterização, modelagem preditiva etc. O objetivo final dos algoritmos de ML é tomar decisões corretas sem intervenção humana. Prever preços de ações ou o clima são alguns exemplos de aplicação.

Existem vários algoritmos de machine learning, como árvores de decisão, Naive Bayes, random forest, support vector machine, K-nearest neighbor, k-means clustering, entre outros.

Do conjunto de algoritmos de ML, o que você vai usar hoje é o k-nearest neighbor.

Agora, o que exatamente é o algoritmo K-Nearest Neighbor? Vamos descobrir!

O que é o k-nearest neighbor?

O KNN, ou k-nearest neighbor, é um algoritmo de aprendizagem supervisionada — “supervisionada” porque ele usa os rótulos de classe dos dados de treinamento durante a fase de aprendizagem. É um algoritmo baseado em instâncias: novos pontos são classificados com base em instâncias rotuladas já armazenadas. KNN pode ser usado tanto para classificação quanto para regressão; porém, é mais comum em tarefas de classificação.

O k no KNN é uma variável crucial, também chamada de hiperparâmetro, que ajuda a classificar um ponto de dados com precisão. Em termos práticos, k é o número de vizinhos mais próximos dos quais você quer “pegar o voto” ao classificar um novo ponto.

visualization of knn visualization of knn

Figura 1. Visualização do KNN Fonte

Perceba que, à medida que o valor de k aumenta de 1 para 7, a fronteira de decisão entre duas classes se torna mais suave.

Como essa “mágica” acontece? Como todo novo ponto é classificado com base nos pontos armazenados?

Vamos entender rapidamente, passo a passo:

  • Primeiro, você carrega todos os dados e define o valor de k;
  • Depois, calcula a distância entre os pontos armazenados e o novo ponto que deseja classificar, usando métricas como distância Manhattan (L1), distância Euclidiana (L2), similaridade do cosseno, distância Bhattacharyya, distância Chebyshev, etc.;
  • Em seguida, ordena os valores de distância em ordem crescente ou decrescente e determina os k vizinhos mais próximos (maiores ou menores, conforme a métrica);
  • Coleta os rótulos desses k vizinhos e usa uma votação majoritária ou ponderada para classificar o novo ponto. O rótulo atribuído é o da classe com maior pontuação;
  • Por fim, retorna a classe prevista para a nova instância.

A previsão pode ser de dois tipos: classificação, na qual é atribuído um rótulo de classe, ou regressão, na qual é atribuído um valor numérico. Diferente da classificação, na regressão costuma-se atribuir ao novo ponto a média dos valores dos k vizinhos.

Desvantagens do KNN: primeiro, a complexidade de buscar os vizinhos mais próximos para cada novo ponto. Segundo, escolher o valor de k pode ser trabalhoso. Por fim, nem sempre é claro qual métrica de distância usar.

Chega de teoria, certo? Vamos carregar, analisar e entender os dados que você vai usar neste tutorial.

Carregando os dados do Iris

O conjunto Iris tem 150 amostras de três classes: Iris-Setosa, Iris-Versicolor e Iris-Virginica. Quatro atributos usados para identificar cada uma das três classes são sepal-length, sepal-width, petal-length e petal-width.

Fique à vontade para usar outro dataset público ou um conjunto de dados próprio.

Sklearn é uma biblioteca de machine learning em Python amplamente usada em tarefas de data science. Ela oferece diversos algoritmos de classificação, regressão e clusterização, incluindo support vector machines, random forests, gradient boosting, k-means, KNN etc. Dentro do sklearn há o módulo datasets com vários conjuntos prontos para uso, incluindo o Iris. É bem intuitivo. Vamos carregar o iris.

from sklearn.datasets import load_iris

O load_iris traz os dados e os rótulos de classe de cada amostra. Vamos extrair tudo rapidamente.

data = load_iris().data

A variável data será um array numpy de shape (150,4), com 150 amostras e quatro atributos cada. Cada classe tem 50 amostras.

data.shape
(150, 4)

Vamos extrair os rótulos de classe.

labels = load_iris().target
labels.shape
(150,)

Agora você vai combinar dados e rótulos usando a excelente biblioteca NumPy. O NumPy oferece suporte a arrays e matrizes multidimensionais, além de uma coleção extensa de funções matemáticas de alto nível para operar nesses arrays. Vamos importar:

import numpy as np

Como data é um array 2D, você também precisa reshapar labels para 2D.

labels = np.reshape(labels,(150,1))

Agora use a função concatenate do numpy, com axis=-1, para concatenar pela segunda dimensão.

data = np.concatenate([data,labels],axis=-1)
data.shape
(150, 5)

Em seguida, importe a biblioteca de análise de dados pandas, útil para organizar dados em formato tabular e fazer operações e manipulações. Em especial, ela oferece estruturas de dados e operações para trabalhar com tabelas numéricas e séries temporais.

Neste tutorial, você vai usar bastante o pandas.

import pandas as pd
names = ['sepal-length', 'sepal-width', 'petal-length', 'petal-width', 'species']
dataset = pd.DataFrame(data,columns=names)

Pronto: o data frame dataset agora tem os dados e os rótulos de classe que você precisa!

Antes de avançar, lembre que a variável labels tem rótulos numéricos. Vamos convertê-los para os nomes das flores (espécies).

Para isso, selecione apenas a coluna class e substitua os três valores numéricos pelas espécies correspondentes. Use inplace=True para modificar o data frame dataset diretamente.

dataset['species'].replace(0, 'Iris-setosa',inplace=True)
dataset['species'].replace(1, 'Iris-versicolor',inplace=True)
dataset['species'].replace(2, 'Iris-virginica',inplace=True)

Vamos imprimir as cinco primeiras linhas de dataset para ver o resultado:

dataset.head(5)
 sepal-lengthsepal-widthpetal-lengthpetal-widthspecies
05.13.51.40.2Iris-setosa
14.93.01.40.2Iris-setosa
24.73.21.30.2Iris-setosa
34.63.11.50.2Iris-setosa
45.03.61.40.2Iris-setosa

Analise seus dados

Vamos ver como as três flores ficam quando visualizadas e quão diferentes são entre si — não só em números, mas também na prática!

iris(Fonte)

Agora, vamos visualizar os dados carregados acima com um scatterplot para entender quanto uma variável influencia a outra — ou seja, qual é a correlação entre elas.

Você vai usar a biblioteca matplotlib para criar o scatterplot.

import matplotlib.pyplot as plt

Dica: quer aprender diferentes formas de visualizar dados em Python? Então confira o curso Introduction to data visualization with matplotlib.

plt.figure(4, figsize=(10, 8))

plt.scatter(data[:50, 0], data[:50, 1], c='r', label='Iris-setosa')

plt.scatter(data[50:100, 0], data[50:100, 1], c='g',label='Iris-versicolor')

plt.scatter(data[100:, 0], data[100:, 1], c='b',label='Iris-virginica')

plt.xlabel('Sepal length',fontsize=20)
plt.ylabel('Sepal width',fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.title('Sepal length vs. Sepal width',fontsize=20)
plt.legend(prop={'size': 18})
plt.show()
sepal length x width scatter plot

Pelo gráfico acima, fica claro que há alta correlação para as flores Iris setosa em relação ao comprimento e à largura da sépala. Por outro lado, há menor correlação entre Iris versicolor e Iris virginica. Os pontos de versicolor e virginica estão mais espalhados, enquanto os de setosa são mais densos.

Vamos também plotar o gráfico para petal-length e petal-width.

plt.figure(4, figsize=(8, 8))

plt.scatter(data[:50, 2], data[:50, 3], c='r', label='Iris-setosa')

plt.scatter(data[50:100, 2], data[50:100, 3], c='g',label='Iris-versicolor')

plt.scatter(data[100:, 2], data[100:, 3], c='b',label='Iris-virginica')
plt.xlabel('Petal length',fontsize=15)
plt.ylabel('Petal width',fontsize=15)
plt.xticks(fontsize=15)
plt.yticks(fontsize=15)
plt.title('Petal length vs. Petal width',fontsize=15)
plt.legend(prop={'size': 20})
plt.show()
petal length x width scatter plot

Também para petal-length e petal-width, o gráfico indica uma forte correlação para as flores setosa, que ficam densamente agrupadas.

Para reforçar como petal-length e petal-width se correlacionam, vamos plotar uma matriz de correlação.

dataset.iloc[:,2:].corr()
 petal-lengthpetal-width
petal-length1.0000000.962865
petal-width0.9628651.000000

A tabela acima mostra uma forte correlação de 0.96 entre petal-length e petal-width quando combinamos as três espécies.

Vamos analisar a correlação separadamente por espécie.

dataset.iloc[:50,:].corr() #setosa
 sepal-lengthsepal-widthpetal-lengthpetal-width
sepal-length1.0000000.7425470.2671760.278098
sepal-width0.7425471.0000000.1777000.232752
petal-length0.2671760.1777001.0000000.331630
petal-width0.2780980.2327520.3316301.000000
dataset.iloc[50:100,:].corr() #versicolor
 sepal-lengthsepal-widthpetal-lengthpetal-width
sepal-length1.0000000.5259110.7540490.546461
sepal-width0.5259111.0000000.5605220.663999
petal-length0.7540490.5605221.0000000.786668
petal-width0.5464610.6639990.7866681.000000
dataset.iloc[100:,:].corr() #virginica
 sepal-lengthsepal-widthpetal-lengthpetal-width
sepal-length1.0000000.4572280.8642250.281108
sepal-width0.4572281.0000000.4010450.537728
petal-length0.8642250.4010451.0000000.322108
petal-width0.2811080.5377280.3221081.000000

Pelas três tabelas, fica claro que a correlação entre petal-length e petal-width é de 0.33 para setosa e 0.32 para virginica, enquanto para versicolor é 0.78.

Agora, vamos visualizar a distribuição das features com histogramas:

fig = plt.figure(figsize = (8,8))
ax = fig.gca()
dataset.hist(ax=ax)
plt.show()
bar charts

petal-length, petal-width e sepal-length mostram distribuição unimodal, enquanto sepal-width se aproxima de uma distribuição Gaussiana. Tudo isso ajuda a escolher um algoritmo que funcione bem com essas distribuições.

Em seguida, vamos analisar se os quatro atributos estão na mesma escala — algo essencial em ML. O data frame do pandas tem a função describe, que retorna count, mean, max, min etc. em formato tabular.

dataset.describe()
 sepal-lengthsepal-widthpetal-lengthpetal-width
count150.000000150.000000150.000000150.000000
mean5.8433333.0573333.7580001.199333
std0.8280660.4358661.7652980.762238
min4.3000002.0000001.0000000.100000
25%5.1000002.8000001.6000000.300000
50%5.8000003.0000004.3500001.300000
75%6.4000003.3000005.1000001.800000
max7.9000004.4000006.9000002.500000

Perceba que os quatro atributos têm escala semelhante entre 0 e 8, em centímetros. Se quiser, você ainda pode reescalá-los para o intervalo entre 0 e 1.

Embora saibamos que há 50 amostras por classe (cerca de 33,3% do total), vamos confirmar:

print(dataset.groupby('species').size())
species
Iris-setosa        50
Iris-versicolor    50
Iris-virginica     50
dtype: int64

Pré-processando seus dados

Depois de carregar e analisar bem os dados, é hora de prepará-los para alimentar seu modelo de ML. Nesta etapa, você vai normalizar os dados e dividir em conjuntos de treino e teste.

Normalizando seus dados

Existem duas formas comuns de normalização:

  • Normalização por exemplo, em que você normaliza cada amostra individualmente;
  • Normalização por feature, em que você normaliza cada atributo de forma consistente em todas as amostras.

Por que e quando normalizar? E é necessário padronizar o Iris?

De modo geral, quase sempre é uma boa prática. Normalizar coloca tudo na mesma escala e faixa, o que é crucial quando os dados não são consistentes. Você pode checar inconsistências com describe(), que mostra max e min. Se uma feature tem faixas muito maiores que outra, normalizar ambas é importante.

Imagine X com faixa maior e Y com faixa menor: a influência de Y pode ser ofuscada pela de X. Nesses casos, normalize X e Y.

No Iris, a normalização não é necessária.

Vamos imprimir novamente describe() para ver por que não precisamos normalizar.

dataset.describe()
 sepal-lengthsepal-widthpetal-lengthpetal-width
count150.000000150.000000150.000000150.000000
mean5.8433333.0573333.7580001.199333
std0.8280660.4358661.7652980.762238
min4.3000002.0000001.0000000.100000
25%5.1000002.8000001.6000000.300000
50%5.8000003.0000004.3500001.300000
75%6.4000003.3000005.1000001.800000
max7.9000004.4000006.9000002.500000

O atributo sepal-length varia de 4,3 a 7,9; sepal-width, de 2 a 4,4; petal-length, de 1 a 6,9; e petal-width, de 0,1 a 2,5. Todas as features ficam entre 0,1 e 7,9 — aceitável. Logo, não é necessário normalizar o Iris.

Dividindo os dados

Este é outro ponto importante em ML: seu objetivo é treinar um modelo capaz de tomar decisões ou classificar dados em ambiente de teste sem intervenção humana. Antes de colocar o modelo em produção, garanta que ele generaliza bem nos dados de teste.

Para isso, você precisa de conjunto de treino e de teste. No Iris, com 150 amostras, vamos treinar em 80% e testar nos 20% restantes.

Em data science, você vai ouvir muito sobre overfitting: quando o modelo aprende muito bem os dados de treino, mas vai mal nos de teste. Dividir em treino e teste (ou validação) ajuda a detectar esse problema.

Para fazer o split, use a função train_test_split da biblioteca sklearn. Vamos dividir:

from sklearn.model_selection import train_test_split

train_data, test_data, train_label, test_label = train_test_split(dataset.iloc[:,:3], dataset.iloc[:,3], test_size=0.2, random_state=42) 

Note que random_state é uma semente. Se você mudar o número, a divisão muda. Mantendo o mesmo random_state, a divisão permanece igual em execuções repetidas.

Vamos imprimir o shape dos conjuntos de treino e teste e seus rótulos.

train_data.shape,train_label.shape,test_data.shape,test_label.shape
((120, 3), (120,), (30, 3), (30,))

Agora sim: é hora de alimentar o algoritmo k-nearest neighbor!

O modelo KNN

Depois de carregar, analisar e pré-processar os dados, vamos alimentar o modelo KNN. Para isso, use a função neighbors do sklearn, que tem a classe KNeigborsClassifier.

Comece importando o classificador.

from sklearn.neighbors import KNeighborsClassifier

Observação: o parâmetro k (n_neighbors) costuma ser ímpar para evitar empates na votação.

Para escolher o melhor k, vamos fazer um grid-search simples: treinar e testar o modelo com 10 valores diferentes de k e ficar com o que tiver melhor desempenho.

Vamos inicializar neighbors(k) com valores de 1 a 9 e duas matrizes de zeros do numpy, train_accuracy e test_accuracy, para armazenar as acurácias de treino e teste. Depois, vamos plotar um gráfico e escolher o melhor neighbor.

neighbors = np.arange(1,9)
train_accuracy =np.zeros(len(neighbors))
test_accuracy = np.zeros(len(neighbors))

No próximo trecho acontece a mágica: vamos enumerate sobre os nove valores de vizinhos e, para cada um, prever em treino e teste. Por fim, armazenar as acurácias em train_accuracy e test_accuracy.

for i,k in enumerate(neighbors):
    knn = KNeighborsClassifier(n_neighbors=k)

    #Fit the model
    knn.fit(train_data, train_label)

    #Compute accuracy on the training set
    train_accuracy[i] = knn.score(train_data, train_label)

    #Compute accuracy on the test set
    test_accuracy[i] = knn.score(test_data, test_label)

Agora vamos plotar as acurácias de treino e teste com matplotlib. Com o gráfico de accuracy vs. número de vizinhos, você escolhe o melhor valor de k.

plt.figure(figsize=(10,6))
plt.title('KNN accuracy with varying number of neighbors',fontsize=20)
plt.plot(neighbors, test_accuracy, label='Testing Accuracy')
plt.plot(neighbors, train_accuracy, label='Training accuracy')
plt.legend(prop={'size': 20})
plt.xlabel('Number of neighbors',fontsize=20)
plt.ylabel('Accuracy',fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.show()
knn accuracy chart

Olhando o gráfico, parece que com n_neighbors=3 o modelo tem o melhor desempenho. Vamos usar n_neighbors=3 e treinar novamente.

knn = KNeighborsClassifier(n_neighbors=3)

#Fit the model
knn.fit(train_data, train_label)

#Compute accuracy on the training set
train_accuracy = knn.score(train_data, train_label)

#Compute accuracy on the test set
test_accuracy = knn.score(test_data, test_label)

Avaliando seu modelo

No último trecho do tutorial, vamos avaliar o modelo nos dados de teste usando técnicas como confusion_matrix e classification_report.

Primeiro, verifique a acurácia no conjunto de teste.

test_accuracy
0.9666666666666667

Uau! Parece que o modelo classificou corretamente 96,66% dos dados de teste. Incrível, né? Com poucas linhas de código, você treinou um modelo que consegue dizer o nome da flor usando apenas quatro atributos, com 96,66% de acerto. Quem sabe ele até supera um humano nessa tarefa.

Matriz de confusão

A matriz de confusão descreve o desempenho do modelo no conjunto de teste, para o qual os rótulos verdadeiros são conhecidos.

O Scikit-learn fornece uma função que calcula a matriz de confusão para você.

prediction = knn.predict(test_data)

A função plot_confusion_matrix() abaixo foi adaptada desta fonte.

import itertools
def plot_confusion_matrix(cm, classes,
                          normalize=False,
                          title='Confusion matrix',
                          cmap=plt.cm.Blues):


    plt.imshow(cm, interpolation='nearest', cmap=cmap)
    plt.title(title)
    plt.colorbar()
    tick_marks = np.arange(len(classes))
    plt.xticks(tick_marks, classes, rotation=45)
    plt.yticks(tick_marks, classes)

    fmt = '.2f' if normalize else 'd'
    thresh = cm.max() / 2.
    for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
        plt.text(j, i, format(cm[i, j], fmt),
                 horizontalalignment="center",
                 color="white" if cm[i, j] > thresh else "black")

    plt.ylabel('True label',fontsize=30)
    plt.xlabel('Predicted label',fontsize=30)
    plt.tight_layout()
    plt.xticks(fontsize=18)
    plt.yticks(fontsize=18)
class_names = load_iris().target_names


# Compute confusion matrix
cnf_matrix = confusion_matrix(test_label, prediction)
np.set_printoptions(precision=2)

# Plot non-normalized confusion matrix
plt.figure(figsize=(10,8))
plot_confusion_matrix(cnf_matrix, classes=class_names)
plt.title('Confusion Matrix',fontsize=30)
plt.show()
confusion matrix

Pela confusion_matrix acima, dá para ver que o modelo classificou todas corretamente, exceto uma virginica, que foi prevista como versicolor.

Relatório de classificação

O classification report ajuda a identificar classes mal classificadas com mais detalhes, fornecendo precision, recall e F1 score por classe. Vamos usar o sklearn para visualizar o relatório.

from sklearn.metrics import classification_report
print(classification_report(test_label, prediction))
                 precision    recall  f1-score   support

    Iris-setosa       1.00      1.00      1.00        10
Iris-versicolor       0.90      1.00      0.95         9
 Iris-virginica       1.00      0.91      0.95        11

      micro avg       0.97      0.97      0.97        30
      macro avg       0.97      0.97      0.97        30
   weighted avg       0.97      0.97      0.97        30

Vá além!

Parabéns a quem chegou até aqui! Mas isso é só o começo — ainda tem muita estrada pela frente.

Este tutorial tratou dos fundamentos de machine learning e da implementação de um algoritmo, o KNN, em Python. O dataset Iris é pequeno e relativamente simples.

Se este conteúdo despertou seu interesse, experimente outros conjuntos de dados ou estude mais algoritmos de ML e aplique no Iris para observar o impacto na acurácia. Dessa forma, você aprende muito mais do que apenas a teoria!

Se você já praticou bastante com o que foi apresentado aqui e com outros algoritmos de ML, talvez seja hora de se aprofundar em Python e análise de dados.

Temas

Relacionado