Clasificación II¶
Ya hemos hablado que los problemas de clasificación son de los más solicitados en el mundo real, por lo que en esta clase veremos otro algoritmo de clasificación, pero en esta ocasión, uno no paramaétrico.
K Nearest Neighbours¶
El algoritmo k Nearest Neighbors (kNN) es un método no paramétrico: una vez que el parámetro \(k\) se ha fijado, no se busca obtener ningún parámetro adicional.
Sean los puntos \(x^{(i)} = (x^{(i)}_1, ..., x^{(i)}_n)\) de etiqueta \(y^{(i)}\) conocida, para \(i=1, ..., m\).
El problema de clasificación consiste en encontrar la etiqueta de un nuevo punto \(x=(x_1, ..., x_m)\) para el cual no conocemos la etiqueta.
La etiqueta de un punto se obtiene de la siguiente forma:
Para \(k=1\), 1NN asigna a \(x\) la etiqueta de su vecino más cercano.
Para \(k\) genérico, kNN asigna a \(x\) la etiqueta más popular de los k vecinos más cercanos.
La idea es la siguiente:
El modelo subyacente a kNN es el conjunto de entrenamiento completo. A diferencia de otros métodos que efectivamente generalizan y resumen la información (como regresión logística, por ejemplo), cuando se necesita realizar una predicción, el algoritmo kNN mira todos los datos y selecciona los k datos más cercanos, para regresar la etiqueta más popular/más común. Los datos no se resumen en parámetros, sino que siempre deben mantenerse en memoria. Es un método por tanto que no escala bien con un gran número de datos.
En caso de empate, existen diversas maneras de desempatar:
Elegir la etiqueta del vecino más cercano (problema: no garantiza solución).
Elegir la etiqueta de menor valor (problema: arbitrario).
Elegir la etiqueta que se obtendría con \(k+1\) o \(k-1\) (problema: no garantiza solución, aumenta tiempo de cálculo).
La cercanía o similaridad entre los datos se mide de diversas maneras, pero en general depende del tipo de datos y del contexto.
Para datos reales, puede utilizarse cualquier distancia, siendo la distancia euclidiana la más utilizada. También es posible ponderar unas componentes más que otras. Resulta conveniente normalizar para poder utilizar la noción de distancia más naturalmente.
Para datos categóricos o binarios, suele utilizarse la distancia de Hamming.
A continuación, una implementación de from scratch en numpy:
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
def knn_search(X, k, x):
""" find K nearest neighbours of data among D """
# Distancia euclidiana
d = np.linalg.norm(X - x, axis=1)
# Ordenar por cercania
idx = np.argsort(d)
# Regresar los k mas cercanos
id_closest = idx[:k]
return id_closest, d[id_closest].max()
def knn(X,Y,k,x):
# Obtener los k mas cercanos
k_closest, dmax = knn_search(X, k, x)
# Obtener las etiquetas
Y_closest = Y[k_closest]
# Obtener la mas popular
counts = np.bincount(Y_closest.flatten())
# Regresar la mas popular (cualquiera, si hay empate)
return np.argmax(counts), k_closest, dmax
def plot_knn(X, Y, k, x):
y_pred, neig_idx, dmax = knn(X, Y, k, x)
# plotting the data and the input point
fig = plt.figure(figsize=(8, 8))
plt.plot(x[0, 0], x[0, 1], 'ok', ms=16)
m_ob = Y[:, 0] == 0
plt.plot(X[m_ob, 0], X[m_ob, 1], 'ob', ms=8)
m_sr = Y[:,0] == 1
plt.plot(X[m_sr, 0], X[m_sr, 1], 'sr', ms=8)
# highlighting the neighbours
plt.plot(X[neig_idx, 0], X[neig_idx, 1], 'o', markerfacecolor='None', markersize=24, markeredgewidth=1)
# Plot a circle
x_circle = dmax * np.cos(np.linspace(0, 2*np.pi, 360)) + x[0, 0]
y_circle = dmax * np.sin(np.linspace(0, 2*np.pi, 360)) + x[0, 1]
plt.plot(x_circle, y_circle, 'k', alpha=0.25)
plt.show();
# Print result
if y_pred==0:
print("Prediccion realizada para etiqueta del punto = {} (circulo azul)".format(y_pred))
else:
print("Prediccion realizada para etiqueta del punto = {} (cuadrado rojo)".format(y_pred))
Puedes ejecutar varias veces el código anterior, variando el número de vecinos k
para ver cómo afecta el algoritmo.
k = 5 # hyper-parameter
N = 50
np.random.seed(42)
X = np.random.rand(N, 2) # random dataset
Y = np.array(np.random.rand(N) < 0.4, dtype=int).reshape(N, 1) # random dataset
x = np.random.rand(1, 2) # query point
# performing the search
plot_knn(X, Y, k, x)
Prediccion realizada para etiqueta del punto = 1 (cuadrado rojo)
Aplicación¶
import pandas as pd
from sklearn import datasets
Utilizaremos un dataset de dígitos escritos a mano para clasificar según el valor de los píxeles
# print(digits_dict["DESCR"]) # Descomenta la línea si quieren más info del dataset
digits_X, digits_y = datasets.load_digits(return_X_y=True, as_frame=True)
digits = pd.concat([digits_X, digits_y], axis=1)
digits.head()
pixel_0_0 | pixel_0_1 | pixel_0_2 | pixel_0_3 | pixel_0_4 | pixel_0_5 | pixel_0_6 | pixel_0_7 | pixel_1_0 | pixel_1_1 | ... | pixel_6_7 | pixel_7_0 | pixel_7_1 | pixel_7_2 | pixel_7_3 | pixel_7_4 | pixel_7_5 | pixel_7_6 | pixel_7_7 | target | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0.0 | 0.0 | 5.0 | 13.0 | 9.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 6.0 | 13.0 | 10.0 | 0.0 | 0.0 | 0.0 | 0 |
1 | 0.0 | 0.0 | 0.0 | 12.0 | 13.0 | 5.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 11.0 | 16.0 | 10.0 | 0.0 | 0.0 | 1 |
2 | 0.0 | 0.0 | 0.0 | 4.0 | 15.0 | 12.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 3.0 | 11.0 | 16.0 | 9.0 | 0.0 | 2 |
3 | 0.0 | 0.0 | 7.0 | 15.0 | 13.0 | 1.0 | 0.0 | 0.0 | 0.0 | 8.0 | ... | 0.0 | 0.0 | 0.0 | 7.0 | 13.0 | 13.0 | 9.0 | 0.0 | 0.0 | 3 |
4 | 0.0 | 0.0 | 0.0 | 1.0 | 11.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 2.0 | 16.0 | 4.0 | 0.0 | 0.0 | 4 |
5 rows × 65 columns
Para hacerlo un poco más gráfico, veamos la imagen del primer número
digit_images = datasets.load_digits().images
plt.imshow(digit_images[0], cmap=plt.cm.gray_r, interpolation='nearest');
Como (casi) siempre, Scikit-learn nos ofrece los algoritmos en la misma sintaxis a la que ya nos hemos ido acostumbrando.
from sklearn.neighbors import KNeighborsClassifier
k = 5
neigh = KNeighborsClassifier(n_neighbors=k)
neigh.fit(digits_X, digits_y)
KNeighborsClassifier()
¿Qué pasó? Veamos algún registro en particular
i = 0
digits_X.iloc[[i], :]
pixel_0_0 | pixel_0_1 | pixel_0_2 | pixel_0_3 | pixel_0_4 | pixel_0_5 | pixel_0_6 | pixel_0_7 | pixel_1_0 | pixel_1_1 | ... | pixel_6_6 | pixel_6_7 | pixel_7_0 | pixel_7_1 | pixel_7_2 | pixel_7_3 | pixel_7_4 | pixel_7_5 | pixel_7_6 | pixel_7_7 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0.0 | 0.0 | 5.0 | 13.0 | 9.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 6.0 | 13.0 | 10.0 | 0.0 | 0.0 | 0.0 |
1 rows × 64 columns
Y su respectiva predicción
neigh.predict(digits_X.iloc[[i], :])
array([0])
neigh.predict_proba(digits_X.iloc[[i], :])
array([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.]])
¡No tiene ninguna gracia! Porque el elemento más cercano es él mismo! Adelantemos un poco de las clases siguientes
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(digits_X, digits_y, test_size=0.25, random_state=42)
Hagamos el ajuste solo con datos de entrenamiento
neigh_tt = KNeighborsClassifier(n_neighbors=k)
neigh_tt.fit(X_train, y_train)
KNeighborsClassifier()
i_test = 22
neigh_tt.predict(X_test.iloc[[i_test], :])
array([9])
neigh_tt.predict_proba(X_test.iloc[[i_test], :])
array([[0. , 0. , 0. , 0. , 0. , 0.2, 0. , 0. , 0. , 0.8]])
plt.imshow(X_test.iloc[[i_test], :].to_numpy().reshape(8, 8), cmap=plt.cm.gray_r, interpolation='nearest');