Este documento é o acompanhamento da 3ª edição do livro Machine Learning with R de Brett Lantz .
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.
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.
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
wbdcjá é 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.
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
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.
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.
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.
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%.