KNN: Classificar Olhando pros Vizinhos
Aula 4a: KNN, o modelo mais "sem modelo" que existe. Não tem coeficiente pra ajustar, não tem gradiente, não tem equação normal. É só uma régua.
O dataset: vinhos, 3 castas
Dataset novo: load_wine, 178 garrafas, 13 medidas químicas (teor alcoólico, acidez, magnésio, e por aí vai) e 3 classes, a casta da uva. O professor escolhe duas colunas pra poder plotar num plano: flavanoids (índice 6) e color_intensity (índice 9).
from sklearn.datasets import load_wine
X, y = load_wine(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
feats = [6, 9]
A ideia: não existe treino, só memória
Todo modelo que eu já vi até aqui (regressão, o classificador limiarizado) ajusta parâmetros: acha um w que minimiza algum erro. KNN não faz nada disso. O "treino" dele é literalmente guardar os dados:
class K1NN(BaseEstimator, ClassifierMixin):
def fit(self, X, y):
self.X = X
self.y = y
return self
def distance_(self, x):
return np.sum((self.X - x)**2, axis=1)**0.5
def predict(self, X):
y_pred = np.empty((X.shape[0],))
for i, x in enumerate(X):
distances = self.distance_(x)
min_idx = np.argmin(distances)
y_pred[i] = self.y[min_idx]
return y_pred
Pra classificar um vinho novo, ele calcula a distância euclidiana até todo vinho do treino, acha o mais parecido, e copia a classe dele. Chama isso de K1NN porque olha só pro vizinho mais próximo, K=1.
Saída: acurácia de 0.97 no teste. Idêntica ao
KNeighborsClassifier(n_neighbors=1)do scikit-learn, rodado nos mesmos dados.
O Bishop trata isso como um caso particular de um resultado mais geral (capítulo 2.5.2): se você desenha uma esfera em volta de um ponto novo até ela conter exatamente vizinhos, e olha qual classe é maioria entre eles, a probabilidade posterior de cada classe é simplesmente (a fração dos vizinhos que pertence àquela classe). Classificar pela classe mais comum entre os vizinhos mais próximos é aplicar Bayes nesse resultado. é só o caso mais extremo: a "esfera" cresce até tocar um único ponto, e você copia a classe dele sem votação nenhuma.
Interativo: mexendo no K de verdade
Em vez de rodar célula por célula pra cada K como o notebook faz, dá pra ver a fronteira de decisão mudando ao vivo. Isso é reconstrução minha em cima dos 142 vinhos reais do treino (as mesmas duas colunas, flavanoids e color_intensity), rodando o mesmo algoritmo de voto por maioria:
Clica em cada valor de K e repara: com K=1, as regiões coloridas têm um monte de ilhazinhas isoladas, cada uma abraçando um único ponto de treino, a fronteira fica toda picotada. Com K maior, as ilhas somem e as regiões viram blocos mais lisos e contínuos.
K é um parâmetro de suavização, não "quanto maior melhor"
O professor testa K=1, 3, 5, 13 nos dados de teste de verdade:
| K | Acurácia |
|---|---|
| 1 | 0.97 |
| 3 | 0.92 |
| 5 | 0.92 |
| 13 | 0.89 |
Contra-intuitivo à primeira vista: eu esperaria que olhar mais vizinhos desse um resultado mais "confiável", mas aqui a acurácia só piora conforme K cresce. O Bishop chama K exatamente disso, um parâmetro de suavização: K pequeno deixa a fronteira de decisão bem apertada em volta dos dados de treino (baixo viés, mas sensível a cada ponto individual, inclusive ruído), enquanto K grande borra a fronteira, misturando vizinhos de regiões diferentes na votação (mais viés, menos sensível a ruído). Nesse dataset específico, com só 142 pontos de treino espalhados em 3 classes, aumentar K começa a puxar vizinhos de outra casta pra dentro do voto rápido demais, então o ponto ótimo aqui está bem perto de K=1.
Isso não quer dizer "sempre use K=1". O Bishop cita um resultado interessante: no limite de infinitos dados de treino, o classificador de vizinho mais próximo (K=1) nunca comete mais que o dobro do erro do classificador ótimo teoricamente possível, uma garantia surpreendentemente boa pra um método tão simples. Mas com pouco dado (como aqui, 142 pontos), K=1 pode estar só "decorando" o treino, e a escolha certa de K é uma pergunta empírica, não uma regra fixa. Vou voltar nisso com mais rigor (validação, não só "testei e vi que K=1 ganhou") no próximo post.
Um lembrete rápido: distância também pede normalização
Eu já vi isso no post anterior: como o KNN decide por distância, uma variável de escala grande domina a conta sozinha. Aqui não é diferente, e o próximo post (bônus) mostra o tamanho real do estrago e o jeito certo de evitar, com Pipeline.
Fechando
| O que eu já sabia | O que essa aula assentou |
|---|---|
| Regressão e classificação limiarizada ajustam parâmetros | KNN não ajusta nada, "treinar" é só guardar os dados e comparar distância na hora de prever |
| Bayes conecta probabilidade condicional e prior | A regra de votar pela classe mais comum entre os K vizinhos é uma aplicação direta de Bayes numa estimativa de densidade local |
| Hiperparâmetro é algo que eu escolho, não que o modelo aprende | K é o exemplo mais direto disso: nem grande nem pequeno demais, o valor certo depende dos dados que eu tenho |
Aplicação Prática
Uso o mesmo dataset de vinhos, agora com as 13 variáveis (não só as 2 que dava pra plotar), pra ver o efeito da normalização na prática, uma coisa que eu só descrevi em palavras até aqui.
model_raw = KNeighborsClassifier(n_neighbors=k).fit(X_train, y_train)
model_norm = KNeighborsClassifier(n_neighbors=k).fit(X_train_normalizado, y_train)
| K | Sem normalizar | Normalizado |
|---|---|---|
| 1 | 0.7778 | 0.9444 |
| 3 | 0.8056 | 0.9444 |
| 5 | 0.7222 | 0.9444 |
Sem normalizar, a acurácia varia bastante e nunca passa de 0.81 (13 variáveis em escalas bem diferentes, tipo proline que vai até quase 1700 contra hue que fica abaixo de 2, então a distância é decidida quase só por proline). Normalizado, a acurácia bate 0.9444 em qualquer um dos três K, um salto enorme, e olha que interessante: depois de normalizar, a escolha de K deixa de importar tanto, os três dão exatamente o mesmo resultado nesse teste. Faz sentido: quando cada variável realmente contribui pra distância (em vez de uma única variável dominar tudo), a vizinhança fica mais "certa" logo de cara, e sobra menos trabalho pro ajuste fino de K resolver.