Tabla de Contenidos

12 - Modelos clasificación 2: kNN

El algoritmo kNN (k-nearest neighbors) es un algoritmo simple de ML supervisado que puede ser usado tanto para clasificación como para regresión.

El algoritmo predice los valores en función de los elementos más cercanos. El valor predicho será la clase con mayor frecuencia de las instancias cercanas (en caso de clasificación) o un valor calculado a partir de dichas instancias en el caso de regresión (media o media ponderada).

Por ejemplo, supongamos que tenemos un conjunto de datos con una característica y una variable objetivo. Esta variable objetivo tendrá sólo dos posibles valores, con lo que estamos ante un problema de clasificación.

Supongamos que la distribución de los datos es como la siguiente imagen:

Si queremos predecir una instancia nueva (punto verde), nos fijaremos en los datos que están más cerca de ella:

Una cuestión sería elegir el número (K) de muestras cercanas en las que nos deberíamos fijar. Si elegimos k = 1, el modelo asignará a la nueva muestra la clase B. Si asignamos k = 3, el modelo asignará la clase A.

¿Qué pasaría si eligiéramos k = 2 en el ejemplo anterior? En ese caso, habría un empate entre ambas clases. La solución sería sumar 1 a nuestra k, con lo que quedaría k = 3. En caso de clasificación binaria, con elegir una k impar nos aseguramos que nunca habrá empate. En caso de empate entre varias clases en clasificación multiclase, deberíamos ir sumando 1 a nuestra k hasta que las clases desempaten.

Medidas de distancia

Otra cuestión es elegir una medida de distancia entre las instancias. Existen varias, entre las que destacan:

La fórmula de la distancia de Minkowski es la siguiente:

$\displaystyle Minkowski = (\sum_{i=1}^{n}|{x_{i} - y_{i}}|^p)^\frac{1}{p} $

Cuando p=2 obtenemos la distancia Euclídea y si p=1 obtenemos la distancia Manhattan. De todos modos, aunque los valores más típicos de la distancia Minkowski suelen ser 1 y 2, también se le podría asignar otros valores.

Programando kNN desde 0

Vamos a programar un algoritmo kNN que use la distancia Euclídea.

import pandas as pd
import numpy as np
from scipy import stats

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

from sklearn.metrics import confusion_matrix

# Configuración warnings
# ==============================================================================
import warnings
warnings.filterwarnings('ignore')

Creamos una función que calcule la distancia Euclídea entre dos puntos:

def euclidean_distance(a, b):
    return np.sqrt(np.sum((a - b)**2))

Creamos la función que encontrará los k vecinos más cercanos:

def nearest_neighbors(a, X_train, y_train, k):
    distances = X_train.apply(lambda x: euclidean_distance(a, x), axis = 1).sort_values()
    indexes = distances[0:k].index
    results = y_train[indexes]
    result = stats.mode(results, axis=None)
    #stats.mode devuelve un array con dos elementos. El primero es un array con los valores de la moda (puede ser más de uno) y el segundo el número de elementos
    return result[0][0]

El algoritmo anterior es sencillo. Primero calculamos la distancia del punto pasado (a) con el resto de instancias del conjunto de datos. Ordenamos los valores de menor a mayor y nos quedamos con los k primeros. Por último, devolvemos la moda (el valor más repetido) de esos mismos índices de los valores que contienen nuestras variables objetivos.

Vamos a probar el algoritmo con el conjunto de datos iris.

iris = load_iris()

df = pd.DataFrame(data = iris.data, columns = iris.feature_names)
df['target'] = iris.target

X = df.drop(columns = ['target'], axis = 1)
y = df['target']

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)

Elegimos la última instancia para hacer una prueba:

a = X_test.iloc[-1]
y_real = y_test.iloc[-1]

y_predict = nearest_neighbors(a, X_train, y_train, 51)

print("Predicción:", y_predict)
print("Real:", y_real)

Predicción: 1  Real: 1

En este caso, nuestro modelo acierta la clase.

Vamos a sacar la matriz de confusión para ver como funciona nuestro algoritmo:

def predict(X_test, X_train, y_train, k):
    return X_test.apply(lambda x: nearest_neighbors(x, X_train, y_train, k), axis = 1)
    
y_test_predict = predict(X_test, X_train, y_train, 5)

confusion_matrix(y_test, y_test_predict)

array([[14,  0,  0],
       [ 0, 16,  0],
       [ 0,  1, 14]])

El algoritmo funciona correctamente, además da bastante buen resultado (kNN funciona especialmente bien con el conjunto de datos iris).

kNN en problemas de regresión

Como hemos dicho antes, kNN también se puede usar para problemas de regresión. La diferencia con el modelo anterior es que, en este caso, no devolvemos el valor de clase más repetido (la moda), si no la media (también podría ser la media ponderada) de los valores reales de los k vecinos más cercanos. Para probarlo, vamos a utilizar el dataset heights, donde tenemos la altura de hijos y de padres. El objetivo es predecir la altura de los hijos en función de la altura de sus padres:

df = pd.read_csv('heights.csv')

X = df.drop(columns = ['Son'], axis = 1)
y = df['Son']

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)

Modificamos nuestro algoritmo para devolver la media:

# En este caso hacemos la predicción en base a la media
def nearest_neighbors_regression(a, X_train, y_train, k, distance = 0, p = None):
    distances = X_train.apply(lambda x: euclidean_distance(a, x), axis = 1).sort_values()
    indexes = distances[0:k].index
    results = y_train[indexes]
    result = np.mean(results, axis=None)
    #stats.mode devuelve un array con dos elementos. El primero es un array con los valores de la moda (puede ser más de uno) y el segundo el número de elementos
    return result

Probamos el algoritmo con el último valor del conjunto de test:

a = X_test.iloc[-1]
y_real = y_test.iloc[-1]

y_predict = nearest_neighbors_regression(a, X_train, y_train, 5)
print("Predicción:", y_predict)
print("Real:", y_real)

Predicción: 171.1452
Real: 179.832

Usando sklearn

Sklearn tiene dos modelos basados en kNN para problemas de clasificación y regresión: KNeighborsClassifier y KNeighborsRegressor. Veamos como utilizar este último sobre el dataset heights:

from sklearn.neighbors import KNeighborsRegressor

model_knn = KNeighborsRegressor(n_neighbors=5, weights='uniform')

model_knn.fit(X_train, y_train)

y_predict = model_knn.predict([a])
print("Predicción:", y_predict)
print("Real:", y_real)

Predicción: [173.2788]
Real: 179.832

Eligiendo el valor de k más adecuado

Una de las preguntas más importantes sobre el algoritmo kNN es cuántos vecinos deberíamos revisar. Como norma general, existen dos problemas principales a la hora de elegir la k:

Como norma general, la k se suele elegir mediante la siguiente fórmula:

$\displaystyle k = \sqrt{n}$

Aunque esta regla sea bastante sencilla y rápida, otra forma de elegir el número de k es iterando el modelo con diferentes ks. De esta forma, nos podemos asegurar que la k que elegimos es la que maximiza los resultados (al menos para ese dataset). Sin embargo, lo malo de obtener el número de k de esta forma es que es computacionalmente muy costoso, lo que hace que pierda uno de los valores del algoritmo kNN.

Para que el algoritmo kNN funcione adecuadamente debemos normalizar los datos sobre los que lo vamos a aplicar.

El motivo es que si tenemos dos variables con escalas muy diferentes, aquella variable con valores más altos será la que determine la distancia. Esto lleva a que, en la práctica, en vez de que el algoritmo tenga en cuenta ambas variables, se está centrando únicamente en una variable para hacer la predicción.

Ejercicios