<jupyter_start><jupyter_text>Clasificación de Imágenes MNISTEl [MNIST](http://yann.lecun.com/exdb/mnist/) es una base de datos considerada como el "Hello World" del problema de clasificación de imágenes. Consta de 60000 ejemplos de *training* y 10000 ejemplos de *testing*. El problema es detectar y asignar un número digital a una imágen que contiene un número escrito a mano.En este notebook se implementa una red neuronal simple que es bastante eficáz para este problema.<jupyter_code># Se incluyen las bibliotecas necesarias
%tensorflow_version 2.x
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
plt.style.use('default')<jupyter_output><empty_output><jupyter_text>Descargar y explorar el *dataset* Al ser tan famoso, MNIST es un *dataset* que viene incluido en la instalación de TensorFlow, y particularmente [`keras`](https://keras.io/), un API que permite a los programadores contruir redes neuronales con agilidad, pues ya cuenta con unidades frecuentemente usadas y permite crear arquitecturas de red de forma ordenada y fácil.Primero que nada, cargamos el *dataset* a la memoria. Keras ya provee una partición conveniente en *training* y *testing*<jupyter_code>(train_images, train_labels), (test_images, test_labels) = tf.keras.datasets.mnist.load_data()<jupyter_output><empty_output><jupyter_text>Podemos explorar algunas dimensiones del *dataset*<jupyter_code>print('imágenes de training:', train_images.shape[0])
print('imágenes de testing:', test_images.shape[0])
print('resolución de cada imagen:', train_images[0].shape)<jupyter_output><empty_output><jupyter_text>Y visualizar algunos ejemplos<jupyter_code>plt.figure(figsize=(5,5))
for i in range(9):
    plt.subplot(3,3,i+1)
    plt.xticks([])
    plt.yticks([])
    plt.imshow(train_images[i], cmap='gray_r')
    plt.xlabel(train_labels[i])
    plt.grid(False)
plt.show()<jupyter_output><empty_output><jupyter_text>PreprocesamientoSi bien es claro que las imágenes que tenemos son correctas, cuando estamos lidiando con redes neuronales es importante tener en cuenta el *rango* de los valores que se pasan a las neuronas artificiales. Por lo general, es bueno **estandarizar** los datos a un rango entre 0 a 1, pues muchas de las funciones de activación funcionan mejor en ese rango. Vamos a ver el rango actual de una de las imágenes.<jupyter_code>print(f'rango de valores en una imagen: ({np.min(train_images[0])}, {np.max(train_images[0])})')<jupyter_output><empty_output><jupyter_text>el rango actual va de 0 a 255. Este rango no es adecuado para una función de activación como la sigmoide, ya que se saturaría muy rápidamente, y la calidad de nuestras predicciones se verá afectada. Por esto, normalizamos las imagens de nuestro *dataset* de tal manera que esten entre 0 y 1<jupyter_code>train_images = train_images/255.0
test_images = test_images/255.0<jupyter_output><empty_output><jupyter_text>volvemos a verificar que el rango sea el deseado<jupyter_code>print(f'rango de valores en una imagen: ({np.min(train_images[0])}, {np.max(train_images[0])})')<jupyter_output><empty_output><jupyter_text>No es necesario normalizar los *labels* en este caso, ya que al ser un problema de clasificación, basta que se cuente con la misma cantidad de valores diferentes con que se cuenta en el programa.  Diseño del modeloVamos a crear una red neuronal simple de 3 *layers* (o capas)<jupyter_code>model = tf.keras.Sequential([tf.keras.layers.Flatten(input_shape=(28,28)),
                             tf.keras.layers.Dense(100, activation='relu'),
                             tf.keras.layers.Dense(10, activation='softmax')])<jupyter_output><empty_output><jupyter_text>nuestro modelo se arma de la siguiente manera:**Sequential**: indica que nuestro modelo es una secuencia de *layers* conectados unos a otros.**Flatten**: es un tipo de *layer*, cuya función es "aplanar" los valores de entrada. En este caso, indicamos que se recibirá una matriz de dos dimensiones (28x28), y el resultante será un vector unidimensional de 784 valores.**Dense**: es el tipo de *layer* que vamos a utilizar. Agrega la cantidad de neuronas indicadas, que estan "densamente" o "totalmente" conectadas a las neuronas del *layer* anterior.**relu** indica que la función que se va a usar es un rectificador lineal, que implementa la siguiente función:```pythondef relu(x):    return max(x, 0)```esta es la función que cada neurona artificial de este *layer* va a ejecutar a la combinación lineal de sus valores de entrada.Finalmente **softmax** es una función de activación cuya función es asegurar que los valores del output de todas las neuronas de este *layer* sumen 1, y mantengan su proporción relativa. En términos simples, distribuye los outputs reflejando una probabilidad para cada neurona del *layer*.  Entrenamiento del modelocon el modelo definido, es hora de entrenar el modelo con los datos del *training set*. Para esto, es necesario utilizar la función `model.compile` para definir una función de optimización que guiará el aprendizaje de los coeficientes de la red. Además, se debe definir una función de costo que será minimizada por al función de optimización.En este caso, utilizaremos `adam`, un popular optimizador. La función de costo será `sparse_categorical_crossentropy`, una función de costo optimizada para problemas de clasificación con muchos *labels*.Finalmente, agregamos una métrica para medir calidad del modelo: `accuracy` (precisión)Una vez definidos estos parámetros, se utiliza la función `model.fit` para entrenar el modelo usando los datos. Aquí, `epoch` (épocas) se refiere a la cantidad de veces que el modelo será expuesto a los ejemplos.<jupyter_code>model.compile(optimizer = 'adam',
              loss = 'sparse_categorical_crossentropy',
              metrics=['accuracy'])

model.fit(train_images, train_labels, epochs=5)<jupyter_output><empty_output><jupyter_text>Evaluación del modeloCon el modelo entrenado, es hora de evaluarlo utilizando ejemplos que el modelo no ha visto (el *testing set*). Veremos que los resultados serán diferentes para datos no vistos por el modelo.<jupyter_code>model.evaluate(test_images, test_labels)<jupyter_output><empty_output><jupyter_text>La diferencia entre los resultados de *training* y *testing* se debe a algo conocido como *overfitting*. Este es un problema complejo, pero existen técnicas prácticas para minimizar el *overfitting* que veremos más adelante.Por el momento, analicemos los casos en que nuestro modelo no reconoce correctamente a los números. Primero, definamos una función que nos permitirá entender mejor el *output* de nuestro modelo.<jupyter_code>def ver_imagen(test_dataset, test_labels, predicciones, indice):
    plt.grid(False)
    plt.xticks([])
    plt.yticks([])
    plt.imshow(test_dataset[indice], cmap='gray_r')
    prediccion = np.argmax(predicciones[indice])
    col = 'green' if prediccion == test_labels[indice] else 'red'
    plt.xlabel(f'pred: {prediccion} ({100*np.max(predicciones[indice]):2.0f}%), val: {test_labels[indice]}',
               color=col)

def ver_prediccion(test_labels, predicciones, indice):
    plt.grid(False)
    plt.yticks([])
    plt.xticks(range(10))
    bars = plt.bar(range(10), predicciones[indice], color='gray')
    prediccion = np.argmax(predicciones[indice])
    bars[prediccion].set_color('red')
    bars[test_labels[indice]].set_color('green')

# obtenemos las predicciones para todos los ejemplos del testing set
predicciones = model.predict(test_images)
# la predicción seleccionada corresponde al índice de la lista que contiene el mayor valor
labels_pred = np.argmax(predicciones, axis=1)

# como sabemos los labels reales, es fácil identificar los valores que nuestro modelo no predice correctamente
# simplemente identificamos los ejemplos en que nuestro modelo difiere del valor real
errores = np.where(labels_pred != test_labels)[0]

plt.figure(figsize=(12, 6))
for i in range(9):
    plt.subplot(3, 6, 2*i+1)
    ver_imagen(test_images, test_labels, predicciones, errores[i])
    plt.subplot(3, 6, 2*i+2)
    ver_prediccion(test_labels, predicciones, errores[i])
plt.show()<jupyter_output><empty_output><jupyter_text>Puesta en producción del modeloEl modelo que entrenamos más arriba puede ser mejorado considerablemente (estás invitado a usar este notebook para modificar el modelo y probar como mejorar el modelo!). Por el momento, pretendamos que estamos contentos con la precisión de nuestro modelo, y veamos como podemos usarlo "en la vida real"Vamos a usar una [herramienta online](https://mnist-demo.herokuapp.com/) para dibujar números con el mouse y ver como nuestro modelo clasifica dichas imágenes.**Nota:** Este código es específico para Google Colab, y si quiere probarlo en su computadora local, debe cambiar el código que accede a las imágenes<jupyter_code>from google.colab import files
from keras.preprocessing import image

# subimos las imágenes a nuestra instancia de Google Colab
uploaded = files.upload()
cant_imgs = len(uploaded.keys())
plt.figure(figsize=(4,2*cant_imgs))
for i, fn in enumerate(uploaded.keys()):
 
    # prediciendo imágenes subidas
    directorio = '/content/' + fn
    imagen = image.load_img(directorio, target_size=(28, 28), color_mode='rgba')
    x = image.img_to_array(imagen)
    x = np.expand_dims(x, axis=0)
    x = x/255.0 #  normalizamos el array para nuestro modelo
    images = np.vstack([x[:,:,:,3]])
    prediccion = model.predict(images, batch_size=10)
    valor = np.argmax(prediccion[0])
    plt.subplot(cant_imgs, 2, 2*i+1)
    plt.xticks([])
    plt.yticks([])
    plt.imshow(images[0],cmap='gray_r')
    plt.xlabel(f'pred: {valor} ({100*np.max(prediccion[0]):2.0f}%)')
    plt.subplot(cant_imgs, 2, 2*i+2)
    plt.xticks(range(10))
    plt.yticks(range(10))
    plt.bar(range(10), prediccion[0], color='gray')
    #print(np.argmax(classes[0]))
plt.show()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.0924539753186324, 'fraction_numerical': 0.012037224357677524, 'mean_word_length': 5.386950904392765, 'pattern_counts': {'":': 0, '<': 49, '<?xml version=': 0, '>': 49, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '12', 'dir': 'jupyter-structured-clean-dedup', 'id': '289909', 'license': 'MIT', 'n_tokens_mistral': 3380, 'n_tokens_neox': 3312, 'n_words': 1082, 'path': 'D1_2_MNIST.ipynb', 'repo_name': 'lcmencia/penguin-tf-workshop', 'size': '9886'}