MAT281

Laboratorio Aplicaciones de la Matemática en la Ingeniería

Clasificación de dígitos con k Nearest Neighbors

INSTRUCCIONES

  • Anoten su nombre y rol en la celda siguiente.
  • Desarrollen los problemas de manera secuencial.
  • Guarden constantemente con Ctr-S para evitar sorpresas.
  • Reemplacen en las celdas de código donde diga #FIX_ME por el código correspondiente.
  • Ejecuten cada celda de código utilizando Ctr-Enter

In [1]:
# Configuracion para recargar módulos y librerías 
%reload_ext autoreload
%autoreload 2

%matplotlib inline

from IPython.display import Image as ShowImage

from IPython.core.display import HTML

HTML(open("style/mat281.css", "r").read())


Out[1]:

In [ ]:
from mat281_code.lab import greetings
alumno_1 = ("Sebastian Flores", "2004001-7")
alumno_2 = ("Maria Jose Vargas", "2004007-8")

HTML(greetings(alumno_1, alumno_2))

Observación

Este laboratorio utiliza la librería sklearn (oficialmente llamada scikit learn), de la cual utilizaremos el método de k Nearest Neighbors.

Problema: clasificación de dígitos

En este laboratorio realizaremos el trabajo de reconocer un dígito a partir de una imagen.

El repositorio con los datos se encuentra en el siguiente link, pero los datos ya han sido incluídos en el directorio data/.

Contenido

El laboratorio consiste de 4 secciones:

  1. Exploración de los datos.
  2. Entrenando el modelo kNN.
  3. Estimación del error de predicción de dígitos utilizando kNN.
  4. Aplicación a dígitos propios.

1. Exploración de los datos

Los datos se encuentran en 2 archivos, data/optdigits.train y data/optdigits.test. Como su nombre lo indica, el set data/optdigits.train contiene los ejemplos que deben ser usados para entrenar el modelo, mientras que el set data/optdigits.test se utilizará para obtener una estimación del error de predicción.

Ambos archivos comparten el mismo formato: cada línea contiene 65 valores. Los 64 primeros corresponden a la representación de la imagen en escala de grises (0-blanco, 255-negro), y el valor 65 corresponde al dígito de la imágene (0-9).

1.1 Cargando los datos

Para cargar los datos, utilizamos np.loadtxt con los parámetros extra delimiter (para indicar que el separador será en esta ocasión una coma) y con el dype np.int8 (para que su representación en memoria sea la mínima posible, 8 bits en vez de 32/64 bits para un float).


In [ ]:
import numpy as np

XY_tv = np.loadtxt("data/optdigits.train", delimiter=",", dtype=np.int8)
print XY_tv
X_tv = XY_tv[:,:64]
Y_tv = XY_tv[:, 64]

print X_tv.shape
print Y_tv.shape
print X_tv[0,:]
print X_tv[0,:].reshape(8,8)
print Y_tv[0]

1.2 Visualizando los datos

Para visualizar los datos utilizaremos el método imshow de pyplot. Resulta necesario convertir el arreglo desde las dimensiones (1,64) a (8,8) para que la imagen sea cuadrada y pueda distinguirse el dígito. Superpondremos además el label correspondiente al dígito, mediante el método text. Realizaremos lo anterior para los primeros 25 datos del archivo.


In [ ]:
from matplotlib import pyplot as plt

# Well plot the first nx*ny examples
nx, ny = 5, 5
fig, ax = plt.subplots(nx, ny, figsize=(12,12))
for i in range(nx):
    for j in range(ny):
        index = j+ny*i
        data  = X_tv[index,:].reshape(8,8)
        label = Y_tv[index]
        ax[i][j].imshow(data, interpolation='nearest', cmap=plt.get_cmap('gray_r'))
        ax[i][j].text(7, 0, str(int(label)), horizontalalignment='center',
                verticalalignment='center', fontsize=10, color='blue')
        ax[i][j].get_xaxis().set_visible(False)
        ax[i][j].get_yaxis().set_visible(False)
plt.show()

2. Entrenando el modelo

2.1 Entrenamiento trivial

Entrenaremos el modelo con 1 vecino y verificaremos el error de predicción en el set de entrenamiento.


In [ ]:
from sklearn.neighbors import KNeighborsClassifier

k = 1
kNN = KNeighborsClassifier(n_neighbors=k)
kNN.fit(X_tv, Y_tv)
Y_pred = kNN.predict(X_tv)
n_errors = sum(Y_pred!=Y_tv)
print "Hay %d errores de un total de %d ejemplos de entrenamiento" %(n_errors, len(Y_tv))

[10%] Desafío 1

¿Porqué el error de entrenamiento es 0 en el modelo?

RESPONDA AQUI

2.2 Buscando el valor de k más apropiado

A partir del análisis del punto anterior, nos damos cuenta de la necesidad de:

  1. Calcular el error en un set distinto al utilizado para entrenar.
  2. Calcular el mejor valor de vecinos para el algoritmo.

[20%] Desafío 2

Complete el código entregado a continuación, de modo que se calcule el error de predicción (en porcentaje) de kNN, para k entre 1 y 10 (ambos incluidos). Realice una división en set de entrenamiento (75%) y de validación (25%), y calcule el valor promedio y desviación estándar del error de predicción (en porcentaje), tomando al menos 20 repeticiones para cada valor de k.

OBS: Ejecución de la celda debería tomar alrededor de 5 minutos.


In [ ]:
from sklearn.neighbors import KNeighborsClassifier
from sklearn.cross_validation import train_test_split

template = "k={0:,d}: {1:.1f} +- {2:.1f} errores de clasificación de un total de {3:,d} puntos"
# Fitting the model
mean_error_for_k = []
std_error_for_k = []
k_range = ##FIX ME##
for k in k_range:
    errors_k = []
    for i in ##FIX ME##:
        kNN = ##FIX ME##
        X_train, X_valid, Y_train, Y_valid = ##FIX ME##
        kNN.fit(X_train, Y_train)
        # Predicting values
        Y_valid_pred = kNN.predict(X_valid)
        # Count the errors
        n_errors = ##FIX ME## 
        # Add them to vector
        errors_k.append(100.*n_errors/len(Y_valid))

    errors = np.array(errors_k)
    print template.format(k, errors.mean(), errors.std(), len(Y_valid))
    mean_error_for_k.append(errors.mean())
    std_error_for_k.append(errors.std())

2.3 Visualizado el error de predicción

Podemos visualizar los datos anteriores utilizando el siguiente código, que requiere que sd_error_for k y mean_error_for_k hayan sido apropiadamente definidos.


In [ ]:
mean = np.array(mean_error_for_k)
std = np.array(std_error_for_k)
plt.figure(figsize=(12,8))
plt.plot(k_range, mean - std, "k:")
plt.plot(k_range, mean , "r.-")
plt.plot(k_range, mean + std, "k:")
plt.xlabel("Numero de vecinos k")
plt.ylabel("Error de clasificacion")
plt.show()

[10%] Desafío 3

¿Qué patrón se observa en los datos? ¿Qué valor de $k$ elegirá para el algoritmo?

RESPONDA AQUI

2.4 Entrenando con todos los datos

A partir de lo anterior, se fija el número de vecinos $k$ y se procede a entrenar el modelo con todos los datos.


In [ ]:
from sklearn.neighbors import KNeighborsClassifier
from sklearn.cross_validation import train_test_split
import numpy as np

k = 2
kNN = KNeighborsClassifier(n_neighbors=k)
kNN.fit(X_tv, Y_tv)

2.5 Predicción en testing dataset

Ahora que el modelo kNN ha sido completamente entrenado, calcularemos el error de predicción en un set de datos completamente nuevo: el set de testing.

[15%] Desafío 4

Complete el código a continuación, para cargar los datos del set de entrenamiento y realizar una predicción de los dígitos de cada imagen. No cambie los nombres de las variables.


In [ ]:
# Cargando el archivo data/optdigits.tes
XY_test = ##FIX ME##
X_test = ##FIX ME##
Y_test = ##FIX ME##
# Predicción de etiquetas
Y_pred = ##FIX ME##

2.6 Visualización de etiquetas correctas

Puesto que tenemos las etiquetas verdaderas en el set de entrenamiento, podemos visualizar que números han sido correctamente etiquetados. Ejecute el código a continuación.


In [ ]:
from matplotlib import pyplot as plt

# Mostrar los datos correctos
mask = (Y_pred==Y_test)
X_aux = X_test[mask]
Y_aux_true = Y_test[mask]
Y_aux_pred = Y_pred[mask]

# We'll plot the first 100 examples, randomly choosen
nx, ny = 5, 5
fig, ax = plt.subplots(nx, ny, figsize=(12,12))
for i in range(nx):
    for j in range(ny):
        index = j+ny*i
        data  = X_aux[index,:].reshape(8,8)
        label_pred = str(int(Y_aux_pred[index]))
        label_true = str(int(Y_aux_true[index]))
        ax[i][j].imshow(data, interpolation='nearest', cmap=plt.get_cmap('gray_r'))
        ax[i][j].text(0, 0, label_pred, horizontalalignment='center',
                verticalalignment='center', fontsize=10, color='green')
        ax[i][j].text(7, 0, label_true, horizontalalignment='center',
                verticalalignment='center', fontsize=10, color='blue')
        ax[i][j].get_xaxis().set_visible(False)
        ax[i][j].get_yaxis().set_visible(False)
plt.show()

2.7 Visualización de etiquetas incorrectas

Más interesante que el gráfico anterior, resulta considerar los casos donde los dígitos han sido incorrectamente etiquetados.

[15%] Desafio 5

Modifique el código anteriormente provisto para que muestre los dígitos incorrectamente etiquetados, cambiando apropiadamente la máscara. Cambie también el color de la etiqueta desde verde a rojo, para indicar una mala etiquetación.


In [ ]:
##FIX ME##

2.8 Análisis del error

Después de la exploración visual de los resultados, queremos obtener el error de predicción real del modelo.

[10%] Desafío 6

Complete el código, obteniendo el error de clasificación para cada dígito. ¿Existen dígitos más fáciles o difíciles de clasificar?

RESPONDER AQUI


In [ ]:
# Error global
mask = (Y_pred!=Y_test)
error_prediccion = ##FIX ME##
print "Error de predicción total de %.1f " %error_prediccion

for digito in range(0,10):
    mask_digito = ##FIX ME##
    Y_test_digito = Y_test[mask_digito] 
    Y_pred_digito = Y_pred[mask_digito]
    error_prediccion = 100.*sum((Y_pred_digito!=Y_test_digito)) / len(Y_pred_digito)
    print "Error de predicción para digito %d de %.1f " %(digito, error_prediccion)

2.9 Análisis del error (cont. de)

El siguiente código muestra el error de clasificación, permitiendo verificar que números son confundibles


In [ ]:
from sklearn.metrics import confusion_matrix as cm
cm = cm(Y_test, Y_pred)
print cm

In [ ]:
# As in http://scikit-learn.org/stable/auto_examples/model_selection/plot_confusion_matrix.html
def plot_confusion_matrix(cm, title='Confusion matrix', cmap=plt.cm.hot):
    plt.figure(figsize=(10,10))
    plt.imshow(cm, interpolation='nearest', cmap=cmap)
    plt.title(title)
    plt.colorbar()
    tick_marks = np.arange(10)
    plt.xticks(tick_marks, tick_marks)
    plt.yticks(tick_marks, tick_marks)
    plt.tight_layout()
    plt.ylabel('True label')
    plt.xlabel('Predicted label')
    plt.show()
    return None

# Compute confusion matrix
plt.figure()
plot_confusion_matrix(cm)

# Normalize the confusion matrix by row (i.e by the number of samples
# in each class)
cm_normalized = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
plot_confusion_matrix(cm_normalized, title='Normalized confusion matrix')

A partir de lo anterior, vemos observamos que los mayores errores son:

  • El 2 puede clasificarse erróneamente como 1 (pero no viceversa).
  • El 7 puede clasificarse erróneamente como 9 (pero no viceversa).
  • El 8 puede clasificarse erróneamente como 1 (pero no viceversa).
  • El 9 puede clasificarse erróneamente como 3 (pero no viceversa).

3. Digitos propios

Para ver que tan bien resulta la predicción en sus propios dígitos, usted ha escrito los dígitos en un papel y le ha sacado una fotografía.

Después de regular la luminosidad, recortarlos apropiadamente y escalarlos a 8x8 pixeles, ha obtenido las siguientes imágenes:


3.1 Cargando datos de digitos

Para cargar los datos, necesitamos utilizar la librería PIL. Ilustraremos el funcionamiento con la imagen del dígito 0, que se encuentra en mis_digitos/0.jpg.


In [ ]:
from PIL import Image
import numpy as np

# Data
input_image = "mis_digitos/0.jpg"

# Read image
image = Image.open(input_image, 'r')
width, height = image.size
print "La imagen tiene %dx%d=%d pixeles" %(width, height, width*height)
X_rgb = np.array(image.getdata(), dtype=np.uint8) # x_i = (R_i, G_i, B_i)
#X_rgb = X_rgb.reshape(height,width,3)
print X_rgb[:,0] # Rojo
print X_rgb[:,1] # Verde
print X_rgb[:,2] # Azul

Nuestro algoritmo no utiliza los distintos niveles de rojo (R), verde (G) y azul (B), y solamente le importa la luminancia $L$ de la imagen: $$ L = (R + G + B)/3 $$ Esto es razonable, pues no nos interesa el color del lápiz que realizó el dígito, sino la forma de éste. Resulta además escalar para que la imagen tenga presente el valor máximo y mínimo (no depende de la iluminación de la imagen), es decir, que se cumpla $min(X) = 0$ y $max(X) = 255$.


In [ ]:
X = 255 - (X_rgb[:,0] + X_rgb[:,1] + X_rgb[:,2])/3.
digito = 300 * (X-X.min())/(X.max()-X.min())
digito[digito>255] = 255

Visualicemos la imagen obtenida


In [ ]:
data = digito.reshape(8,8)
plt.imshow(data, interpolation='nearest', cmap=plt.get_cmap('gray_r'))
plt.show()

Veamos que nos daría la predicción con kNN. Recuerde que tiene el modelo ya ha sido cargado en memoria y se encuenta en la variable kNN.


In [ ]:
label = kNN.predict(digito)
print label

3.2 Carga de datos

Realicemos ahora la predicción para todas las imágenes de manera simultánea. Para ello abriremos cada imagen, realizaremos los arreglos de luminancia y escalamiento, y almacenaremos el arreglo en una fila de una matriz.


In [ ]:
X_mis_digitos = np.zeros([10,64])
Y_mis_digitos = np.zeros(10)
for i in range(10):
    template = "mis_digitos/{0}.jpg"
    archivo = template.format(i)
    print "Abriendo archivo", archivo
    image = Image.open(archivo, 'r')
    X_rgb = np.array(image.getdata(), dtype=np.uint8) # x_i = (R_i, G_i, B_i)
    X = 255 - (X_rgb[:,0] + X_rgb[:,1] + X_rgb[:,2])/3.
    digito = 300 * (X-X.min())/(X.max()-X.min())
    digito[digito>255] = 255
    X_mis_digitos[i,:] = digito
    Y_mis_digitos[i] = i

# Realizar la predicción simultánea
Y_pred = kNN.predict(X_mis_digitos)

3.2 Visualización de resultados


In [ ]:
from matplotlib import pyplot as plt

X_aux = X_mis_digitos
Y_aux_true = Y_mis_digitos
Y_aux_pred = Y_pred

nx, ny = 2, 5
fig, ax = plt.subplots(nx, ny, figsize=(20,10))
for i in range(nx):
    for j in range(ny):
        index = j+ny*i
        data  = X_aux[index,:].reshape(8,8)
        label_pred = str(int(Y_aux_pred[index]))
        label_true = str(int(Y_aux_true[index]))
        if label_true == label_pred:
            color = "green"
        else:
            color = "red"
        ax[i][j].imshow(data, interpolation='nearest', cmap=plt.get_cmap('gray_r'))
        ax[i][j].text(0, 0, label_pred, horizontalalignment='center',
                verticalalignment='center', fontsize=10, color=color)
        ax[i][j].text(7, 0, label_true, horizontalalignment='center',
                verticalalignment='center', fontsize=10, color='blue')
        ax[i][j].get_xaxis().set_visible(False)
        ax[i][j].get_yaxis().set_visible(False)

plt.show()

[10%] Desafío 7

¿Qué error de predicción se obtiene en las imágenes de los dígitos? ¿Es esperable este resultado?

RESPONDA AQUI

[10%] Desafío 8

¿Cómo podría mejorarse el resultado anterior?

RESPONDA AQUI