Este documento é o acompanhamento da 3ª edição do livro Machine Learning with R de Brett Lantz .

k-NN

O algoritmo de classificação k-NN, do inglês, “k vizinhos mais próximos” consiste em encontrar uma quantidade k de observações mais próximas a observação em análise.
Algoritmos baseados em k-NN são considerados preguiçosos, pelo motivo de, tecnicamente falando, não resultar em abstrações. Os processos de abstrações e generalização são ignorados.
Sob a definição estrita de aprendizado, modelos preguiçosos não aprendem de fato, o que permite que a fase de treinamento - que não está treinando nada de fato - destes modelos seja breve.

Coleta de Dados

O dataset utilizado será o do Breast Cancer Wisconsin do UCI Machine Learning Repository. Esse dataset foi doado pelos pesquisadores da Universidade de Winsconsin. Os valores representam as características de células nuclei, presentes na imagem digitalresultantes de aspiração por agulha fina em mamas.
Esse conjunto de dados inclui 569 registros de biopsias de cancer, com 32 variáveis. Uma das variáveis é o id, outra o diagnóstico e o restante são 30 variáveis numéricas de medidas. O diagnóstico é codificado como “M” para indicar maligno e “B” benigno.

wbcd <- read.csv("C:/Users/PICHAU/OneDrive/Documentos/k-NN/breast+cancer+wisconsin+diagnostic/wisc_bc_data.csv", stringsAsFactors = F)
str(wbcd)
## 'data.frame':    569 obs. of  32 variables:
##  $ id                     : int  842302 842517 84300903 84348301 84358402 843786 844359 84458202 844981 84501001 ...
##  $ diagnosis              : chr  "M" "M" "M" "M" ...
##  $ radius_mean            : num  18 20.6 19.7 11.4 20.3 ...
##  $ texture_mean           : num  10.4 17.8 21.2 20.4 14.3 ...
##  $ perimeter_mean         : num  122.8 132.9 130 77.6 135.1 ...
##  $ area_mean              : num  1001 1326 1203 386 1297 ...
##  $ smoothness_mean        : num  0.1184 0.0847 0.1096 0.1425 0.1003 ...
##  $ compactness_mean       : num  0.2776 0.0786 0.1599 0.2839 0.1328 ...
##  $ concavity_mean         : num  0.3001 0.0869 0.1974 0.2414 0.198 ...
##  $ concave.points_mean    : num  0.1471 0.0702 0.1279 0.1052 0.1043 ...
##  $ symmetry_mean          : num  0.242 0.181 0.207 0.26 0.181 ...
##  $ fractal_dimension_mean : num  0.0787 0.0567 0.06 0.0974 0.0588 ...
##  $ radius_se              : num  1.095 0.543 0.746 0.496 0.757 ...
##  $ texture_se             : num  0.905 0.734 0.787 1.156 0.781 ...
##  $ perimeter_se           : num  8.59 3.4 4.58 3.44 5.44 ...
##  $ area_se                : num  153.4 74.1 94 27.2 94.4 ...
##  $ smoothness_se          : num  0.0064 0.00522 0.00615 0.00911 0.01149 ...
##  $ compactness_se         : num  0.049 0.0131 0.0401 0.0746 0.0246 ...
##  $ concavity_se           : num  0.0537 0.0186 0.0383 0.0566 0.0569 ...
##  $ concave.points_se      : num  0.0159 0.0134 0.0206 0.0187 0.0188 ...
##  $ symmetry_se            : num  0.03 0.0139 0.0225 0.0596 0.0176 ...
##  $ fractal_dimension_se   : num  0.00619 0.00353 0.00457 0.00921 0.00511 ...
##  $ radius_worst           : num  25.4 25 23.6 14.9 22.5 ...
##  $ texture_worst          : num  17.3 23.4 25.5 26.5 16.7 ...
##  $ perimeter_worst        : num  184.6 158.8 152.5 98.9 152.2 ...
##  $ area_worst             : num  2019 1956 1709 568 1575 ...
##  $ smoothness_worst       : num  0.162 0.124 0.144 0.21 0.137 ...
##  $ compactness_worst      : num  0.666 0.187 0.424 0.866 0.205 ...
##  $ concavity_worst        : num  0.712 0.242 0.45 0.687 0.4 ...
##  $ concave.points_worst   : num  0.265 0.186 0.243 0.258 0.163 ...
##  $ symmetry_worst         : num  0.46 0.275 0.361 0.664 0.236 ...
##  $ fractal_dimension_worst: num  0.1189 0.089 0.0876 0.173 0.0768 ...

A variável id, que é apenas uma identificadora, está como numérica e não nos trará nenhuma informação útil tampouco será necessário utilizá-la para fins de diferenciação entre observações. Portanto, a eliminaremos.

wbcd <- wbcd[-1]

A próxima variável, diagnosis, é de interesse particular, uma vez que esperamos prevê-la. A função table() indica que 357 observações são begninas contra 212 malignas.

table(wbcd$diagnosis)
## 
##   B   M 
## 357 212

Em R, diversos modelos clusterização requerem que a variável de interesse seja do tipo factor, então a ‘diagnosis’ será recodificada e aproveitaremos a oportunidade para mudar “B” e “M” para uma etiqueta mais informativa.

wbcd$diagnosis <- factor(wbcd$diagnosis, 
                         levels = c("B", "M"),
                         labels = c("Benigno", "Maligno"))
# Visulização do Resultado
round(prop.table(table(wbcd$diagnosis))*100, digits = 1)
## 
## Benigno Maligno 
##    62.7    37.3

As demais variáveis são todas numéricas, então, por motivos apenas ilustrativos do uso do k-NN, usaremos apenas 3 explorarmos o conjunto dos dados.

summary(wbcd[c("radius_mean", "area_mean", "smoothness_mean")])
##   radius_mean       area_mean      smoothness_mean  
##  Min.   : 6.981   Min.   : 143.5   Min.   :0.05263  
##  1st Qu.:11.700   1st Qu.: 420.3   1st Qu.:0.08637  
##  Median :13.370   Median : 551.1   Median :0.09587  
##  Mean   :14.127   Mean   : 654.9   Mean   :0.09636  
##  3rd Qu.:15.780   3rd Qu.: 782.7   3rd Qu.:0.10530  
##  Max.   :28.110   Max.   :2501.0   Max.   :0.16340

As três variáveis estão em escalas de valor muito discrepantes, e isso provavelmente se repete entre boa parte das colunas. Para um melhor desempenho do modelo de classificação devemos colocá-las dentro de uma mesma escala de valor.
Para tanto, usaremos a técnica de normalização dos dados, que consiste em redimensionar os valores para uma escala que varia de 0 a 1. A equação da normalização está descrita a seguir:
\[ x_{\text{norm}} = \frac{(x - x_{\min})}{(x_{\max} - x_{\min})} \]

# Criando uma função que executa a quação de normalização
normalize <- function(x) {
  return((x-min(x)) / (max(x) - min(x)))
}

# Aplicando a função nas variáveis numéricas
wbcd_n <- as.data.frame(lapply(wbcd[2:31], normalize))

# Verificação do funcionamento
summary(wbcd_n$area_mean)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##  0.0000  0.1174  0.1729  0.2169  0.2711  1.0000

Como esperado, a variável area_mean, que antes continha valores entre 143.5 e 2051, agora está normalizado: os valores foram redimensionados para variarem entre 0 e 1.

Preparação dos Dados: Criando datasets de treino e teste

Apesar de todas as observações estarem etiquetadas com o resultado da biopsia, não é interessante prever o que já se sabe.
Assim sendo, precisamos particionar na base de dados, simulando um cenário no qual se sabe o resultado e outro no qual se quer descobrir o diagnóstico. Para isso, devemos realizar uma divisão, respectivamente, em base de treino e base de teste.

wbcd_treino <- wbcd_n[1:469,]
wbcd_teste <- wbcd_n[470:569,]

Importante: Ao dividir datasets para treino e teste, certifique-se de que os valores não estão seguindo nenhum tipo de organização, como ordem alfabética, numérica ou cronológica. O dataset wbdc já é ordenado aleatoriamente, por isso pôde-se separar os dados de forma consecutiva, sem a necessidade de aleatorização prévia.

Ao construir as bases de treino e teste, excluímos a variável de interesse. Para treinar o modelo k-NN, precisaremos guardar esses valores excluídos em um vetor de fatores, dividindo-os também entre treino e teste.

wbcd_treino_labels <- wbcd[1:469, 1]
wbcd_teste_labels <- wbcd[479:569, 1]

Essa divisão é necessária nas próximas etapas para avaliarmos o desempenho do modelo.

Treinamento do Modelo

Para realizar o treinamento do modelo precisaremos utilizar a biblioteca class, desenvolvida para classificação de dados.

library(class)

A função knn()do pacote class() provê as implementações do algoritmo k-NN. Para cada linha do dataset de treino, essa função irá identificar os k vizinhos mais próximos - Nearest Neighbors - utilizando a equação de distância Euclidiana, onde k é arbitrário. Como nossa base de treino possui 469 observações, tentaremos utilizar k = 21, número ímpar aproximado da raíz quadrada de 469. Números ímpares favorecem o aprendizado do k-NN, pois este modelo quando encontra nos últimos valores mais próximos um empate, faz uma escolha aleatória sobre qual será o vizinho escolhido.

wbcd_teste_pred <- knn(train = wbcd_treino, # Data frame com os dados de treino
                 test = wbcd_teste, # Data frame com os dados de teste
                 cl = wbcd_treino_labels, # vetor com as classes para cada linha
                 k = 21) # Quantidade desejada de vizinhos mais próximos

Avaliando a perfomance do modelo

Como o enunciado desta seção anuncia, o próximo passo é avaliar a perfomance do modelo, o quão bem ele predisse as classes comparando wbcd_teste_pred com wbcd_teste_labels. Para isso, usaremos a função CrossTable()do pacote gmodels.

library(gmodels)
## Warning: pacote 'gmodels' foi compilado no R versão 4.4.3
#wbcd_teste_labels e wbcd_teste_pred estão com tamanhos diferentes
#para a execução do CrossTable, é necessário que tenham a mesma dimensão
wbcd_teste_pred <- wbcd_teste_pred[1:91]

CrossTable(x = wbcd_teste_labels, y = wbcd_teste_pred,
           prop.chisq = F)
## 
##  
##    Cell Contents
## |-------------------------|
## |                       N |
## |           N / Row Total |
## |           N / Col Total |
## |         N / Table Total |
## |-------------------------|
## 
##  
## Total Observations in Table:  91 
## 
##  
##                   | wbcd_teste_pred 
## wbcd_teste_labels |   Benigno |   Maligno | Row Total | 
## ------------------|-----------|-----------|-----------|
##           Benigno |        56 |        12 |        68 | 
##                   |     0.824 |     0.176 |     0.747 | 
##                   |     0.737 |     0.800 |           | 
##                   |     0.615 |     0.132 |           | 
## ------------------|-----------|-----------|-----------|
##           Maligno |        20 |         3 |        23 | 
##                   |     0.870 |     0.130 |     0.253 | 
##                   |     0.263 |     0.200 |           | 
##                   |     0.220 |     0.033 |           | 
## ------------------|-----------|-----------|-----------|
##      Column Total |        76 |        15 |        91 | 
##                   |     0.835 |     0.165 |           | 
## ------------------|-----------|-----------|-----------|
## 
## 

A primeira célular da esquerda indica os casos de Verdadeiuros Negativos, esses 56 de 91 valores são os casos onde o tumor foi de fato benigno e o k-NN os identificou corretamente. A célula inferor da direita indica os Verdadeiros Negativos, ou seja, é a representação de quantos tumores malignos o k-NN classificou corretamente de acordo com a base de dados original (3/91).
As células na outra diagonal indicam os erros, ou seja, quantas classificações o modelo errou em prever. Das 91 observações, preveu erroneamente 12 benignos que na verdade eram malignos e 20 malignos que na verdade eram benignos, estes erros são denominados falsos positivos e falsos negativos, respectivamente.

Melhorando a perfomance do modelo

Tentaremos uma simples variações no nosso modelo de classificação. Iremos empregar um método alternativo no ajuste da escala das nossas variáveis numéricas. Outra técnica poderia ser utilizada aqui, tanto com padronizão Z-Score quanto com a normalização, que é variar o valor de k.

Transformação - Padronização Z-Score

Embopra a normalização seja comum para classificação através do k-NN, padronização Z-Score pode ser mais paorpriada, o único jeito de descobrir será testando. A padronização Z-Score é descrita pela seguinte equação: \[ z = \frac{x - \mu}{\sigma} \] Onde:
- z = valor padronizado;
- x= valor original;
- μ = média da variável;
- σ = desvio padrão da variável;

Uma vez que a padronização Z-Score não possui um mínimo ou máximo, valores extremos não tendem ao valor central. Vejamos se a padronização Z-Score melhor a acurácia preditiva do modelo.

#Padronização de todas as coluans numéricas
wbcd_z <- as.data.frame(scale(wbcd[-1])) 
summary(wbcd_z$area_mean)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
## -1.4532 -0.6666 -0.2949  0.0000  0.3632  5.2459

A média de uma padronização Z-Score deve ser sempre zero e a faixa de valores compacta.
Como já feito anteriormente, precisamos dividir essa base de dados em base de treino e teste, e classificar a instância de teste utilizando a função knn().

wbcd_treino <- wbcd_z[1:469,]
wbcd_teste <- wbcd_z[470:569,]
wbcd_treino_labels <- wbcd[1:469, 1]
wbcd_teste_labels <- wbcd[470:569, 1]
wbcd_teste_pred <- knn(train = wbcd_treino,
                       test = wbcd_teste,
                       cl = wbcd_treino_labels,
                       k = 21)
CrossTable(x = wbcd_teste_labels,
           y = wbcd_teste_pred,
           prop.chisq = F)
## 
##  
##    Cell Contents
## |-------------------------|
## |                       N |
## |           N / Row Total |
## |           N / Col Total |
## |         N / Table Total |
## |-------------------------|
## 
##  
## Total Observations in Table:  100 
## 
##  
##                   | wbcd_teste_pred 
## wbcd_teste_labels |   Benigno |   Maligno | Row Total | 
## ------------------|-----------|-----------|-----------|
##           Benigno |        77 |         0 |        77 | 
##                   |     1.000 |     0.000 |     0.770 | 
##                   |     0.975 |     0.000 |           | 
##                   |     0.770 |     0.000 |           | 
## ------------------|-----------|-----------|-----------|
##           Maligno |         2 |        21 |        23 | 
##                   |     0.087 |     0.913 |     0.230 | 
##                   |     0.025 |     1.000 |           | 
##                   |     0.020 |     0.210 |           | 
## ------------------|-----------|-----------|-----------|
##      Column Total |        79 |        21 |       100 | 
##                   |     0.790 |     0.210 |           | 
## ------------------|-----------|-----------|-----------|
## 
## 

Pudemos notar uma melhora no modelo. Na diagonal principal, que indica os acertos, tivemos um total de 98 acertos de 100 observações, enquanto que na diagonal de falha, obtivemos apenas 2 predições erradas.

Resumo

Aprendemos sobre classificação de dados utilziando o algoritmo k-NN. Diferentemente de vários classificadores, o k-nearest neighbor não aprende algo de fato, ele simplesmente guarda o verbatim do conjunto de dados - ou seja, ele decora os padrões dos dados sem criar regras ou correlações entre as variáveis.
Os dados não etiquetados são e casados ao dado mais similar a ele no conjunto de treinamento utilizando a função de distância, e só então é atribuída a sua classificação de acordo com o seus vizinhos mais próximos.

Mesmo o k-NN sendo um algoritmo simples, possui capacidade de lidar com tarefas extremamente complexas, como a identificação de células cancerosas, utilizando apenas algumas linhas de código R, onde conseguimos atingimos uma acurácia de 98%.