{"cells":[{"metadata":{},"cell_type":"markdown","source":"1. Import de Librerias"},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load in \n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nimport os #File access\nimport time #measuring training time \n#Matplotlib and seaborn imports for graph plotting and image display \nimport matplotlib.pyplot as plt\nimport matplotlib.image as mpimg\nimport seaborn as sb\nimport itertools\n#Sklearn imports\nfrom sklearn.model_selection import train_test_split #Splitting dataset into train/dev sets\nfrom sklearn.metrics import confusion_matrix #Confusion matrix \n\n#Keras imports\nfrom keras.utils.np_utils import to_categorical # convert to one-hot-encoding\nfrom keras.models import Sequential #To create sequential layers in a network\nfrom keras.layers import Dense, Dropout, Flatten, Conv2D, MaxPool2D #Importing Dense (FC), Convolutional and Pooling Layers. \nfrom keras.layers.advanced_activations import LeakyReLU #Importing leaky relu\n#Also imports Dropout regularizations and Flatten for FC Layers. \nfrom keras.optimizers import RMSprop, Adam #Optimizers\nfrom keras.preprocessing.image import ImageDataGenerator #Data augmentation\n#from keras.callbacks import ReduceLROnPlateau\n\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"2. Preparación de la data"},{"metadata":{},"cell_type":"markdown","source":"2.1 Importamos los sets de entrenamiento y validación, y observamos como esta estructurado el dataset. "},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":true},"cell_type":"code","source":"#Checking directory structure\nos.listdir('../input')\n#Importing train and test datasets\ntrain = pd.read_csv(\"../input/train.csv\")\ntest = pd.read_csv(\"../input/test.csv\")","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"2.2 Separamos las predicciones de la data, revisamos si existe algun valor NULL. Se debe tener en cuenta que el test set no cuenta con los labels. Al revisar los nulls, se debe tener en cuenta que el train set cuenta con 785 y el test set con 784. Por lo tanto, utilizar sum() para ver los valores nulls daría una lista muy larga. Por esto, utilizamos any().describe(), que da una versión resumida de todas las filas (Ejemplos en el dataset) y las columnas (Valores de los ejemplos) donde se lista:\n\nCount, que es el total de valores contados por ejemplo.\nunique, que es el total de valores únicos.\ntop, que establece el valor mas repetido.\nfreq, que establece cuantas veces dicho valor se repite.\n\nCuando observamos los valores obtenidos, podemos ver que tanto en el set de entrenamiento como en el set de validación solo se presenta el valor false. Este valor false hace referencia a isnull(), que retorna True o False dependiendo de si existe algun valor NULL o NaN en alguna \"casilla\" de los sets de entrenamiento. Por lo tanto, podemos confirmar que no hace falta arreglar la data. "},{"metadata":{"trusted":true},"cell_type":"code","source":"#Getting the labels\nY_train = train[\"label\"]\n#Dropping the labels for X\nX_train = train.drop(\"label\", axis=1)\n\n#Checking for NULL Values\nprint(train.isnull().any().describe())\nprint(\"===============================\")\nprint(test.isnull().any().describe())","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"2.3 Analizamos la distribución de los 10 dígitos posibles en nuestros datasets. Podemos observar que todos los dígitos tienen una frecuencia parecida, por lo que no tendremos problemas de bias hacía cierto dígito. Con los shapes, podemos ver que tenemos 42000 ejemplos de entrenamiento y 28000 de validación. "},{"metadata":{"trusted":true},"cell_type":"code","source":"#Total examples\nprint(X_train.shape)\nprint(test.shape)\n#Value counts\nsb.countplot(Y_train)\nY_train.value_counts()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"2.4 Normalización de la data: Dividimos nuestra data entre 255 (Máximo valor posible) para obtener valores entre 0 y 1. Luego, hacemos reshape de cada uno de los ejemplos de los sets a matrices de 28x28x1 (Tamaño original de las imagenes). El 1 al final se debe a que manejamos imágenes en escala de grises, y se encuentra en este posición ya que usaremos el estandar \"channel last\" de keras, en vez de \"channel first\". "},{"metadata":{"trusted":true},"cell_type":"code","source":"\n#Making a copy of X_train to compare normalized and non-normalized images\nX_train_example = X_train\nX_train_example = X_train_example.values.reshape(-1,28,28,1)\n\n#Normalization \nX_train = X_train / 255.0\ntest = test / 255.0\nX_train = X_train.values.reshape(-1, 28, 28, 1)\ntest = test.values.reshape(-1, 28, 28, 1)\n\n#Comparison: We will compare how normalized images look compared to non-normalized. \nfig, comparison = plt.subplots(1, 2)\ncomparison[0].imshow(X_train[3][:,:,0]) \ncomparison[1].imshow(X_train_example[3][:,:,0]) ","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"2.5 One hot Encoding: Y_Train está compuesto por 42000 valores que van desde 0 a 9. Utilizando la función to_cateogrical de Keras, le hacemos One Hot Encoding a esto. Esto generará 10 columnas, una por cada valor posible de Y. Estas columnas tendrá un valor 1 y los demás valores 0, el 1 refiriéndose al valor real del label (El número en la imagen es un 0, es un 1, es un 2, etc.). Esto lo hacemos para utilizar la función de costo \"categorical_crossentropy\" en nuestra red neuronal: Al utilizar como última función de activación una función softmax, nuestra red producirá como output final 10 valores (Números de 0 al 9), cada uno representando la probabilidad de que la imagen analizada sea dicho número. Esta probabilidad debe ser comparada con el \"ground truth\" para calcular la pérdida de la red y, con esto, actualizarla para mejorar su rendimiento. Por lo tanto, será necesario tener los labels en one-hot encoding, para poder comparar la probabilidad calculada por la red de que la imagen sea un número vs el verdadero número que la imagen representa. "},{"metadata":{"trusted":true},"cell_type":"code","source":"print(\"Previous shape of Y_train:\")\nprint(Y_train.shape)\nY_train = to_categorical(Y_train, num_classes = 10)\nprint(\"New shape of Y_train:\")\nprint(Y_train.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"2.6 División del set de entrenamiento en un dev set. Este set estará compuesto por 10% de nuestro set de entrenamiento. Se probo con 15 y 20%, pero se encontro que 10% era suficiente. "},{"metadata":{"trusted":true},"cell_type":"code","source":"X_train, X_dev, Y_train, Y_dev = train_test_split(X_train, Y_train, test_size = 0.1)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"2.6 Data augmentation: Se decidió utilizar data augmentation tras realizar pruebas con la red de 10 epochs (Una con data augmentation y otra sin data augmentation, mas detalles en el informe). "},{"metadata":{"trusted":true},"cell_type":"code","source":"datagen = ImageDataGenerator(\n        rotation_range=10,  # rotates images by 10 deegres\n        zoom_range = 0.10, # Randomly zoom image \n        width_shift_range=0.1,  # shifts images horizontally by a fraction of total width\n        height_shift_range=0.1,  # same as above but vertically\n        horizontal_flip=False,  # flip images\n        vertical_flip=False)  #  flip images\n\n#Note that we specifically use the False value for flips (Even though this is the default value provided by the function). This is to emphasize the fact that doing\n#data augmentation by flipping numbers can bring problems, souch as mistaking a 6 for a 9, a 7 losing a shape that makes it a 7 for a human, etc. \n\n#Other thing to note is the usage of shifts. Even though filters in convolutional networks are very good at detecting shifts and data augmentation through shifts\n#does not give the network that much \"new learning material\" we still use it to experiment. \n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"2.7 Comparación imagen del dataset e imagen de data augmentation. Haremos algunos ejemplos de data augmentation para entender como el ImageDataGenerator puede modificar nuestra data. "},{"metadata":{"trusted":true},"cell_type":"code","source":"fig, comparison = plt.subplots(3, 2, sharex=True,sharey=True)\n\n#EXAMPLE ONE: Slightly rotate images\ncomparison[0,0].imshow(X_train[0][:,:,0])\ntransform_dictionary = {\"theta\": 10}\ngenerated_image = datagen.apply_transform(X_train[0], transform_dictionary)\ncomparison[0,1].imshow(generated_image[:,:,0], cmap='Greys')\n\n#EXAMPLE TWO: Apply zoom in x \ntransform_dictionary = {\n    \"zx\": 0.8,\n}\ncomparison[1,0].imshow(X_train[1][:,:,0])\ngenerated_image = datagen.apply_transform(X_train[1], transform_dictionary)\ncomparison[1,1].imshow(generated_image[:,:,0], cmap='Greys')\n#EXAMPLE THREE: Apply translation\ntransform_dictionary = {\n    \"tx\": 4,\n}\ncomparison[2,0].imshow(X_train[2][:,:,0])\ngenerated_image = datagen.apply_transform(X_train[2], transform_dictionary)\ncomparison[2,1].imshow(generated_image[:,:,0], cmap='Greys')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"3. Creación de la red convolucional: \nLa arquitectura final decidida fue de dos series de capas convolución-convolución-pooling y luego dos capas FC (Fully Connected). Para la entrada a las capas FC, se realiza \"aplanamiento\" previo de las imagenes a vectores. Como función de activación en todas las capas (menos la de salida), se decidió utilizar leaky ReLU. En las capas convolucionales, se utilizan filtros 3x3 y convoluciones \"same\", lo que implica que a los inputs de dichas capas se les agrega padding de tal manera que sus outputs sean del mismo tamaño del input. En las capas de pooling, se utilizan maxpools 2x2 (Esto, por lo tanto, toma el mayor número de una matriz 2x2) con padding \"valid\", por lo que no se agrega padding y solo se verifica que la operación sea válida. Se optó por 64 filtros en las primeras dos capas convolucionales y 128 en las otras dos. Se llego a este número despues de probar números muchos mas bajos, que no lograban accuracies muy altos en nuestra red, y muchos mas altos, que creaban \"overfitting\". Se jugó con dropout y números mas bajos hasta que se llego a dichos números. En las capas FC, se optó por una capa de 256 neuronas para análisis y posteriormente una de 10 para producir un output de 10 columnas mediante la función de activación softmax, que establece las distintas probabilidades de que una imagen pertenezca a una clase (Que en este caso, son nuestros 10 dígitos).   \n"},{"metadata":{"trusted":true},"cell_type":"code","source":"\nmodel = Sequential() \n#MODEL ONE    \n#First conv-conv-pool layer\nmodel.add(Conv2D(filters=64, kernel_size=(3,3), padding=\"same\", input_shape=(28, 28, 1)))\nmodel.add(LeakyReLU(0.1))\nmodel.add(Conv2D(filters=64, kernel_size=(3,3), padding=\"same\"))\nmodel.add(LeakyReLU(0.1))\nmodel.add(MaxPool2D(pool_size=(2, 2), strides=None, padding='valid', data_format=None))\nmodel.add(Dropout(0.25))\n#Second conv-conv-pool layer\nmodel.add(Conv2D(filters=128, kernel_size=(3,3), padding=\"same\"))\nmodel.add(LeakyReLU(0.1))\nmodel.add(Conv2D(filters=128, kernel_size=(3,3), padding=\"same\"))\nmodel.add(LeakyReLU(0.1))\nmodel.add(MaxPool2D(pool_size=(2, 2), strides=None, padding='valid', data_format=None))\nmodel.add(Dropout(0.25))\n#Flatten\nmodel.add(Flatten())\n#First FC layer \nmodel.add(Dense(256))\nmodel.add(LeakyReLU(0.1))\nmodel.add(Dropout(0.5))\n#Second FC layer \nmodel.add(Dense(10, activation=\"softmax\"))\n\n\n'''\nmodel2 = Sequential() \n#MODEL TWO    \n#First conv-conv-pool layer\nmodel2.add(Conv2D(filters=16, kernel_size=(3,3), padding=\"same\", input_shape=(28, 28, 1)))\nmodel2.add(LeakyReLU(0.1))\nmodel2.add(Conv2D(filters=32, kernel_size=(3,3), padding=\"same\"))\nmodel2.add(LeakyReLU(0.1))\nmodel2.add(MaxPool2D(pool_size=(2, 2), strides=None, padding='valid', data_format=None))\nmodel2.add(Dropout(0.25))\n#Second conv-conv-pool layer\nmodel2.add(Conv2D(filters=48, kernel_size=(3,3), padding=\"same\"))\nmodel2.add(LeakyReLU(0.1))\nmodel2.add(Conv2D(filters=64, kernel_size=(3,3), padding=\"same\"))\nmodel2.add(LeakyReLU(0.1))\nmodel2.add(MaxPool2D(pool_size=(2, 2), strides=None, padding='valid', data_format=None))\nmodel2.add(Dropout(0.25))\n#Flatten\nmodel2.add(Flatten())\n#First FC layer \nmodel2.add(Dense(256))\nmodel2.add(LeakyReLU(0.1))\nmodel2.add(Dropout(0.5))\n#Second FC layer \nmodel2.add(Dense(10))\nmodel2.add(LeakyReLU(0.1))\nmodel.add(Dense(10, activation=\"softmax\"))\n'''\n    ","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"3.1 Se estableció el algoritmo Adam con sus valores default y un learning rate de 0.001, debido a su sencillez de implementación. Como métrica, nos enfocamos en el accuracy. "},{"metadata":{"trusted":true},"cell_type":"code","source":"#optimizer = RMSprop(lr=0.001, rho=0.9, epsilon=1e-08, decay=0.0)\noptimizer = Adam(lr=0.001)\nmodel.compile(\n    loss='categorical_crossentropy',  \n    optimizer=optimizer,\n    metrics=['accuracy']  \n)\n'''\nmodel2.compile(\n        loss='sparse_categorical_crossentropy',  \n    optimizer=optimizer, \n    metrics=['accuracy']  \n)\n'''\n\nmodel.summary()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"4. Entrenamiento del modelo\nSe entrenó el modelo utilizando batches de 64 (Se escogío este número debido a que es relativamente pequeño y es múltiplo de 2, permitiendonos agilizar el aprendizaje). Se escogieron 30 epochs ya que lograban el mejor score. "},{"metadata":{"trusted":true},"cell_type":"code","source":"#Set and labels: X_train, X_dev, Y_train, Y_dev\n#Parameters\nepochs = 30\nbatch_size = 64\n#With data augmentation\nstart = time.time()\nhistory = model.fit_generator(datagen.flow(X_train,Y_train, batch_size=batch_size),\n                    validation_data = (X_dev, Y_dev),\n                    epochs = epochs,\n                    steps_per_epoch=X_train.shape[0] // batch_size\n                   )\n#We save the model on a history variable in order to evaluate the model later on. \nend = time.time()\nprint(\"Total time training was:\")\nprint(end - start)\n#Without data augmentation \n'''\nmodel.fit(X_train, Y_train, \n          batch_size=batch_size,\n          validation_data = (X_dev, Y_dev),\n          epochs = epochs,\n          #steps_per_epoch=X_train.shape[0] // batch_size\n         )\n'''","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"5. Evaluación del modelo \nPara la evaluación del modelo, utilizaremos el objeto history, devuelta por los métodos fit (y fit_generator) de Keras. Este objeto tiene varia data pertinente para la evaluación de nuestro modelo. Se puede controlar que contiene este objeto con los metrics pasados en el método compile. En nuestro caso, contamos con acc y val_acc, que representan la precisión de sus respectivos sets, y loss y val_loss, que representan la pérdida de sus respectivos sets. "},{"metadata":{"trusted":true},"cell_type":"code","source":"print(history.history.keys())","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"5.1 Utilizamos matplotlib para crear unos gráficos que demuestren el progreso de nuestro modelo. "},{"metadata":{"trusted":true},"cell_type":"code","source":"#Accuracy plot \nplt.plot(history.history['acc'], color=\"blue\", label=\"Precisión de entrenamiento\") #Paint blue line showing progressing train accuracy values\nplt.plot(history.history['val_acc'], color=\"red\", label=\"Precisión de dev\") #Paint red line showing progressing val accuracy values\nplt.legend(loc='best') #Place the legend where it doesn't overlap with the lines\nplt.title('Accuracy vs epoch') #Set graph title\nplt.ylabel('Precisión') #Set y label in graph\nplt.xlabel('epoch') #Set x label in graph\nplt.show()\n#Loss plot\nplt.plot(history.history['loss'], color='blue', label=\"Pérdida de entrenamiento\")\nplt.plot(history.history['val_loss'], color='red', label=\"Pérdida de dev\")\nplt.legend(loc='best')\nplt.title('Pérdida vs epoch')\nplt.ylabel('Pérdida')\nplt.xlabel('epoch')\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"5.2 Confusion Matrix: Utilizada para entender mejor los errores cometidos por nuestra red. Esta es una matriz donde la diagonal indica la cantidad de veces donde nuestras predicciones concordaron con el valor real del label de la imagen para dicho número. Por oto lado, cualquier número afuera de esa diagonal representa la cantidad de veces que dicho dígito fue confundido por otro. Tomada de https://www.kaggle.com/yassineghouzam/introduction-to-cnn-keras-0-997-top-6"},{"metadata":{"trusted":true},"cell_type":"code","source":"Y_dev_pred = model.predict(X_dev)\nY_dev_pred_value = np.argmax(Y_dev_pred, axis=1)\nY_dev_truth = np.argmax(Y_dev,axis = 1) \nconfusion_mtx = confusion_matrix(Y_dev_truth, Y_dev_pred_value) \n\ndef plot_confusion_matrix(cm, classes,\n                          title='Confusion matrix',\n                          cmap=plt.cm.Blues):\n    \"\"\"\n    This function prints and plots the confusion matrix.\n    Normalization can be applied by setting `normalize=True`.\n    \"\"\"\n    plt.imshow(cm, interpolation='nearest', cmap=cmap)\n    plt.title(title)\n    plt.colorbar()\n    tick_marks = np.arange(len(classes))\n    plt.xticks(tick_marks, classes, rotation=45)\n    plt.yticks(tick_marks, classes)\n    \n    thresh = cm.max() / 2.\n    for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):\n        plt.text(j, i, cm[i, j],\n                 horizontalalignment=\"center\",\n                 color=\"white\" if cm[i, j] > thresh else \"black\")\n\n    plt.tight_layout()\n    plt.ylabel('True label')\n    plt.xlabel('Predicted label')\n    \nplot_confusion_matrix(confusion_mtx, classes = range(10)) ","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"5.3 Comparación de errores cometidos por la red (Función tomada de https://www.kaggle.com/yassineghouzam/introduction-to-cnn-keras-0-997-top-6). Se realiza este analisis para familiarizarnos un poco más con los errores cometidos por nuestra red. "},{"metadata":{"trusted":true},"cell_type":"code","source":"errors = (Y_dev_pred_value - Y_dev_truth != 0) #We compare true labels and predictions arrays. This will return true where the labels mismatch.\nY_pred_classes_errors = Y_dev_pred_value[errors] #Using python properties, we can pass a boolean array as an index for an array. This will return an array where all the values are true. \nY_truth_errors = Y_dev_truth[errors]\nprint(\"Valores predecidos\")\nprint(Y_pred_classes_errors)\nprint(\"Valores reales\")\nprint(Y_truth_errors)\nX_value_errors = X_dev[errors] #We extract the X values where there were errors. \n#We have a total of 25 errors. Let's display some of them. \n\nprint(X_value_errors.shape)\ni = 0\n\nfor number in range(4):\n    rows = 2\n    columns = 3 \n\n    fig, ax = plt.subplots(rows, columns, sharex=True,sharey=True)\n    plt.subplots_adjust(hspace = 0.4)\n    for row in range(rows):\n        for column in range(columns):\n            ax[row, column].imshow(X_value_errors[i].reshape(28,28), cmap='Greys')\n            ax[row,column].set_title(\"Predicted label :{}\\nTrue label :{}\".format(Y_pred_classes_errors[i],Y_truth_errors[i]))\n            i += 1\n\n\n#Y_pred_errors = Y_pred[errors]\n#Y_true_errors = Y_true[errors]\n#X_val_errors = X_val[errors]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"6. Predicción y envío de test set: \nTomando en cuenta que solo puede haber un número por imagen, utilizamos la función np.argmax con axis=1. Esto analizará cada fila de las predicciones (Donde cada fila es una imagen), que contiene 10 columnas, donde cada columna representa los dígitos del 0 al 9 y en cada celda se encuentra la probabilidad de que la imagen sea dicho dígito. De esa fila, se toma la celda con el mayor valor utilizando la función argmax, que retorna el índice del mayor valor, que en este caso concuerda con su dígito. Debido a que cada imagen solo puede contener un dígito, solo hace falta tomar el valor mas probable. "},{"metadata":{"trusted":true},"cell_type":"code","source":"predictions = model.predict(test)\nresults = np.argmax(predictions, axis = 1)\nprint(results)\nresults = pd.Series(results, name=\"Label\")\n\nsubmission = pd.concat([pd.Series(range(1,28001),name = \"ImageId\"),results],axis = 1)\nsubmission.to_csv(\"output.csv\",index=False)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"7. Algunos kernels utilizados como inspiración:\n* https://www.kaggle.com/yassineghouzam/introduction-to-cnn-keras-0-997-top-6\n* https://www.kaggle.com/anirudhchak/keras-cnn-for-digit-recognition"}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.6.4","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat":4,"nbformat_minor":1}