Clasificador KNN con scikit-learn: Tu primer proyecto de IA
La inteligencia artificial lleva existiendo décadas, pero ha sido recientemente cuando se ha vuelto una tecnología generalista. A partir de 2015, el desarrollo de redes convolucionales, unido a la creciente disponibilidad de datos, y el movimiento GPGPU trajeron un renacimiento al campo de la investigación en IA. Sin embargo, ha sido la popularización de aplicaciones de usuario final como ChatGPT o Midjourney, lo que realmente ha llevado la IA a las masas.
Empezar directamente con transformers, o con el proceso de difusión puede ser un poco desalentador, pero por suerte para los recién llegados, hay técnicas más sencillas pero potentes más adecuadas para los primerizos.
Uno de los algoritmos de IA más básicos es k-nearest neighbors (KNN). KNN es un algoritmo simple pero efectivo que puede ser utilizado para una variedad de tareas, incluyendo clasificación, regresión y detección de anomalías. Además, uno no tiene que implementar KNN desde cero, ya que existen librerías de Python como scikit-learn con buenas implementaciones.
En este post, aprenderás a construir un clasificador de dígitos manuscritos en Python. Utilizarás la librería scikit-learn para entrenar y ejecutar inferencia utilizando el algoritmo KNN, y aprenderás algunos de sus mecanismos internos. También aprenderás buenas prácticas, como la validación cruzada y la búsqueda de hiper parámetros. Al final de este tutorial, no solo entenderás el algoritmo KNN, sino que también serás capaz de utilizarlo en tus propios proyectos.
¿Cómo funciona el algoritmo KNN?
K Nearest Neighbors (KNN) es probablemente uno de los algoritmos de machine learning más simples. Los modelos de IA que llenan titulares hoy en día tardan semanas en entrenarse y suelen requerir centros de datos con muchos núcleos de cómputo y memoria, en cambio, KNN en su forma más sencilla no hace ningún cálculo al entrenarse, tan sólo guarda los datos de entrenamiento. Después, cuando necesitamos ejecutar el algoritmo sobre nuevos datos, KNN identifica las muestras de entrenamiento más cercanas al nuevo dato y las examina. Dependiendo de la información que extraigamos de esas muestras más cercanas, podemos ejecutar la clasificación, la regresión o la identificación de anomalías, pero la clasificación es, con diferencia, la aplicación más popular. En este sentido, el algoritmo KNN simplemente extrae las etiquetas de los vecinos más cercanos y devuelve la más común.
Por otra parte, si queremos utilizar KNN para la regresión o identificación de valores atípicos debemos ser un poco más cuidadosos. Para regresión, podemos obtener la distancia, o similitud, entre el nuevo dato y cada uno de los vecinos, y promediar sus características utilizando la similitud para ponderar. Para la identificación de anomalías, o valores atípicos, podemos establecer un umbral de distancia, y si no hay muestras de entrenamiento lo suficientemente cerca del nuevo dato, podemos asumir que se trata de una anomalía. Trabajar con KNN también puede complicarse cuando el conjunto de datos de entrenamiento es grande. En este caso, el proceso de encontrar los vecinos más cercanos a un dato nuevo puede ser bastante lento, ya que KNN tendría que comparar la nueva muestra con todas las muestras del conjunto de datos de entrenamiento. Esta estrategia suele denominarse búsqueda por fuerza bruta. En su lugar, se puede construir un árbol de búsqueda en el momento de entrenar KNN, de forma que la búsqueda de vecinos más cercanos se ejecute más rápidamente. Este árbol de búsqueda suele ser un KD-tree o un ball tree y, aunque estos nombres puedan parecer intimidatorios, lo único que hacen es analizar una única característica a la vez y dividir el conjunto de datos de entrenamiento en dos divisiones: en una de ellas se almacenan las muestras con un valor inferior a un umbral para la característica seleccionada y en la otra se almacenan las muestras con un valor superior; este proceso puede repetirse construyendo un árbol, subdividiendo cada vez utilizando otra característica. Entonces, a la hora de ejecutar la inferencia, sólo tenemos que seguir las divisiones del árbol fijándonos en las características correspondientes del nuevo dato y, cuando llegamos a un nodo hoja, realizamos una búsqueda bruta en un conjunto más pequeño de los datos de entrenamiento.
Por último, otra cuestión importante de KNN es la forma en que comparamos cada par de muestras, a la hora de buscar vecinos cercanos. La distancia más básica es la distancia euclidiana, o norma L2, que mide la distancia en línea recta entre dos puntos en el espacio de características. Otra opción común es la distancia Manhattan, también llamada norma L1 o distancia del taxista, que mide la distancia sumando las diferencias absolutas entre las características de los dos puntos. Finalmente, una última distancia común es la distancia de Minkowski, que tiene un parámetro "p" que controla el orden de la distancia, y puede imitar la distancia Manhattan para p = 1, y la distancia euclídea para p = 2.
Dataset UCI ML de dígitos manuscritos
Un algoritmo de IA serviría de poco sin datos para entrenarlo. En este ejemplo utilizaremos el dataset de dígitos manuscritos UCI ML, una colección de imágenes de dígitos escritos a mano, del 0 al 9, en su mayoría en blanco y negro, y en los que la imagen solo muestra el dígito que queremos indentificar.
Utilizaremos específicamente la versión incluida con scikit-learn, que es una copia del conjunto de test de dicho dataset. Esta versión contiene 1797 imágenes de un total de 13 personas, donde cada imagen es de 8x8 píxeles. Durante la adquisición, imágenes de 32 por 32 píxeles se redujeron dividiéndolas en bloques no solapados de 4 por 4 píxeles, y contando el número de píxeles blancos en cada bloque. El tamaño original de 32 píxeles, dividido por 4, da como resultado una imagen de 8 por 8 y, dado que cada bloque de la imagen original tiene 16 píxeles, la imagen reducida debería tener píxeles con valores comprendidos entre 0 y 16.
En lugar de descargar manualmente el dataset, podemos cargarlo utilizando el siguiente código:
from sklearn import datasetsdigits_datset = datasets.load_digits()
La variable digits_datset.data contiene los valores de los píxeles, y la variable digits_datset.target contiene las etiquetas de las imágenes. Las etiquetas son números del 0 al 9, correspondientes al dígito que se muestra en cada imagen.
Una vez que hemos cargado el dataset, podemos visualizarlo usando matplotlib de la siguiente manera:
import matplotlib.pyplot as pltfor i in range(10): plt.subplot(1, 10, i+1) plt.imshow(digits_datset.images[i], cmap='gray') plt.show()
Lo que generará el siguiente gráfico:
Además, dado que el conjunto de datos incluido en scikit-learn es sólo el conjunto de test, debemos subdividirlo en nuestros propios conjuntos de entrenamiento y test. Para ello, utilizaremos la función train_test_split del módulo model_selection, de la siguiente manera:
from sklearn.model_selection import train_test_split(train_data, test_data, train_labels, test_labels) = train_test_split(digits_datset.data, digits_datset.target, test_size=0.2, random_state=1337)
Por aclarar, el parámetro random_state es un valor semilla que controla cómo se barajarán los datos. Normalmente, los datos se barajan aleatoriamente, y cada llamada a la función barajará los datos de forma diferente, sin embargo, si pasamos un valor random_state, tendremos un comportamiento reproducible.
Entrenando y evaluando el modelo
En primer lugar, importamos la clase correspondiente y creamos una instancia. Utilizaremos 20 vecinos, la búsqueda KD-Tree y p = 2 para la distancia de Minkowski:
from sklearn.neighbors import KNeighborsClassifiermodel = KNeighborsClassifier(n_neighbors=20, algorithm='kd_tree', p=2)
Ahora, podemos entrenar el modelo utilizando el método .fit:
model.fit(train_data, train_labels)
Llegados a este punto tenemos un modelo entrenado, y podemos obtener el rendimiento en el conjunto de test con el siguiente código:
from sklearn.metrics import accuracy_scoretest_predictions = model.predict(test_data)accuracy = accuracy_score(test_labels, test_predictions)print(accuracy)
Después, también podemos guardar el modelo para utilizarlo en el futuro. Para ello, podemos utilizar el módulo pickle:
import picklewith open('model.pkl','wb') as f: pickle.dump(model, f)with open('model.pkl','rb') as f: loaded_model = pickle.load(f)
¿Cómo podemos averiguar los mejores parámetros?
En la sección anterior utilizamos 20 vecinos y p = 2 para la distancia de Minkowski. Sin embargo, no verificamos si estos eran los parámetros que maximizan el rendimiento del clasificador.
En este tipo de situaciones, podemos utilizar grid search. Esta técnica consiste en probar exhaustivamente todas las combinaciones de diferentes parámetros en un rango discreto, una cuadrícula, y seleccionar la combinación que obtenga el mejor rendimiento.
Para mejorar el proceso de búsqueda, una estrategia habitual es realizar grid search con validación cruzada. La validación cruzada es una técnica que divide los datos en varios subconjuntos, k. Una vez divididos, se ejecuta un bucle de entrenamiento y evaluación k veces, cada vez utilizando k-1 subconjuntos para el entrenamiento y el subconjunto restante para la evaluación. Por último, en lugar de calcular el rendimiento en un único conjunto de prueba, se calcula la precisión para cada subconjunto de validación y luego se hace la media.
Para hacer esto en Python, primero tenemos que importar la clase GridSearchCV, NumPy, y crear el modelo, el grid de parámetros, y el objeto de grid search:
from sklearn.model_selection import GridSearchCVimport numpy as npparameters = {'n_neighbors': np.arange(2, 20, 2), 'p': np.arange(1, 4, 1)}model = KNeighborsClassifier(algorithm='kd_tree')grid_search_model = GridSearchCV(model, parameters, scoring='accuracy', n_jobs=-1)
El parámetro n_jobs = -1 le dirá a scikit-learn que utilice todos nuestros núcleos de CPU, y así terminar más rápido.
Después podemos llamar a .fit
para ejecutar la búsqueda en la cuadrícula con validación cruzada, tardará algún tiempo en terminar:
grid_search_model.fit(digits_datset.data, digits_datset.target)
El objeto grid_search_model dividirá automáticamente los datos en conjuntos de entrenamiento y validación, y utilizará la técnica de validación cruzada para medir el rendimiento de cada combinación de parámetros. Una vez haya finalizado, podremos comprobar cuáles son los mejores parámetros y la mejor precisión:
print(grid_search_model.best_score_)print(grid_search_model.best_params_)
Por último, podemos evaluar el mejor modelo con el conjunto de test original para ver si hemos obtenido alguna mejora:
test_predictions = grid_search_model.predict(test_data)accuracy = accuracy_score(test_labels, test_predictions)print(accuracy)
Aunque es posible que obtengamos mejores resultados en el conjunto de pruebas original, la precisión de la validación cruzada es en la que más deberíamos confiar. Esto se debe a que esa precisión se evaluó en k subconjuntos.
Conclusión
En este post hemos aprendido cómo funciona el algoritmo KNN, un buen punto de partida para aquellos que buscan entrar en el campo del aprendizaje automático. Este algoritmo almacena las muestras de entrenamiento y analiza las nuevas comparándolas con los datos de entrenamiento. Para clasificar nuevos datos, asigna la etiqueta más común entre sus vecinos más cercanos.
A pesar de los buenos resultados obtenidos, el algoritmo KNN se queda corto en pruebas más exigentes, como por ejemplo con Imagenet. Las imágenes del conjunto de datos utilizado son muy pequeñas, 8 x 8 píxeles, están en blanco y negro y no contienen elementos que puedan confundir al algoritmo, mientras que las imágenes del mundo real son mucho más complejas.
Los siguientes pasos podrían pasar por aprender sobre los algoritmos de feature extraction, como HOG o SIFT, y clasificadores más complejos como SVM o Random Forest. Al aprender sobre extractores de características basados en filtros, empezarás a familiarizarte con la operación de convolución, que es una de las piedras angulares de las redes convolucionales. Por otro lado, al estudiar modelos basados en la optimización, como SVM, comprenderás mejor cómo los algoritmos de aprendizaje automático ajustan sus parámetros a los datos de entrenamiento.
The next steps could be reading about feature extraction algorithms for images like HOG or SIFT, and more complex classifiers like the SVM or Random Forest. By learning about filter based feature extractors, you will begin familiarizing yourself with the convolution operation, which is one of the cornerstones of convolutional networks. On the other hand, by looking at optimization based models like SVM you will get a better understanding of how machine learning algorithms fit the training data.
En resumen, aunque hemos conseguido buenos resultados, aún queda mucho camino por recorrer. Esto no es más que un primer paso en el campo de la IA, pero es un buen punto de partida.