{"cells":[{"metadata":{},"cell_type":"markdown","source":"## CNN Convolutional Neural Network pour Classification des Images\n\nPour cette deuxième partie du tutorial nous allons Réaliser un Réseau de Convolution mais cette fois-ci sur un jeu de données plus complexe que les chiffres du MNIST. En effet nous allons utiliser un dataset avec des **vrais images pour la reconnaissance Chien / Chat** (toujours en apprentissage supervisé)\n\nNous Utiliserons encore une fois les libraries de KERAS pour la création du réseau de Convolution.\n\nTous les principaux éléments qui composent ce réseau ont été abordés en partie 1, s'y reporter en cas de besoin.\n\n### Image Classification Références:\nhttps://www.kaggle.com/yassineghouzam/introduction-to-cnn-keras-0-997-top-6\nhttps://blog.keras.io/building-powerful-image-classification-models-using-very-little-data.html\nhttps://www.kaggle.com/stevenhurwitt/cats-vs-dogs-using-a-keras-convnet\nhttps://stanford.edu/~shervine/blog/keras-how-to-generate-data-on-the-fly\nhttps://github.com/shervinea/enzynet/blob/master/scripts/architecture/enzynet_uniform.py\nhttps://stanford.edu/~shervine/blog/evolution-image-classification-explained\nhttps://www.analyticsvidhya.com/blog/2016/10/tutorial-optimizing-neural-networks-using-keras-with-image-recognition-case-study/\nhttp://ruder.io/optimizing-gradient-descent/\nhttps://medium.com/@vijayabhaskar96/tutorial-on-keras-flow-from-dataframe-1fd4493d237c\n\ndataset : https://www.kaggle.com/c/dogs-vs-cats-redux-kernels-edition/data\n"},{"metadata":{},"cell_type":"markdown","source":"## Import des librairies nécessaire\n\nCi-dessous nous importons toutes les libraries nécessaires pour la création du réseau de la même manière que dans le précédent tutorial.\n\nNous allons également rajouter des librairies de Keras pour la gestion des callback (nous allons voir ce point plus en détail)"},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":true},"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\n\nfrom pathlib import Path\nimport matplotlib.pyplot as plt\n\nfrom sklearn.utils import class_weight as cw\nfrom keras.models import load_model\nfrom keras.preprocessing.image import ImageDataGenerator\nfrom keras.preprocessing import image\n\nfrom keras.models import Sequential\nfrom keras.layers import Conv2D, MaxPooling2D, BatchNormalization\nfrom keras.layers import Flatten, Dense, Dropout\n\n# Import des librairies pour la gestion des Callback\nfrom keras.callbacks import Callback, ReduceLROnPlateau, EarlyStopping","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Configuration du modèle\n\nAfin de variabiliser et de configurer plus facilement le réseau nous exposons ici les principaux paramètres configurables"},{"metadata":{"trusted":true},"cell_type":"code","source":"EPOCHS                  = 100   # Nombre d'epoch\nIMGSIZE                 = 96    # Taille des images\nBATCH_SIZE              = 32    # Pour le traitement par lot des images (optimisation de la decente de gradient)\nSTOPPING_PATIENCE       = 10    # Callback pour stopper si le modèle n'apprend plus\nVERBOSE                 = 0     # Niveau de verbosité\nMODEL_NAME              = 'cnn_80epochs_imgsize160'\nOPTIMIZER               = 'adam'\nTRAINING_DIR            = '../input/dogs-vs-cats-redux-kernels-edition/train'\nTEST_DIR                = '../input/dogs-vs-cats-redux-kernels-edition/test'\nTRAIN_MODEL             = True  # Entrainement du modele (True) ou chargement (False)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Préparation des Jeux de données\n\nCréation du dataframe des **données d'entrainement** contenant les id des images et leur labels.\nNous utilisons toujours le Dataframe de Panda pour construire nos tableaux de données.\n\nCette fois ci le jeu d'entrainement contient le nom du fichier ainsi que le label de l'image dans le nom du fichier (ie cat.100.jpg)\n\nNous allons donc créer un tableau avec la colonne id contenant le nom du fichier et la colonne label contenant la classe à prédire."},{"metadata":{"trusted":true},"cell_type":"code","source":"train_files = os.listdir(TRAINING_DIR)\ntrain_labels = []\n\nfor file in train_files:\n    train_labels.append(file.split(\".\")[0])\n    \ndf_train = pd.DataFrame({\"id\": train_files, \"label\": train_labels})\n\ndf_train.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Data Generators & Image Real Time Augmentation"},{"metadata":{},"cell_type":"markdown","source":"### Jeu d'entrainement \n**Augmentation des images** à la volée via les générateurs pour permettre de simuler une augmentation du nombre de données disponible pour l'entrainement du réseau. \n\nComme vu précédement c'est la classe de **Keras ImageDataGenerator** qui va permettre l'application de filtres et de transformations sur les images sources.\n\nIl faut également noter ici que nous faisons **split du jeu d'entrainement en deux sous parties** (les données d'entrainement et celles de validation)\nCeci est réalisé grâce à validation_split de ImageDataGenerator pour lequel on préciser un ratio."},{"metadata":{"trusted":true},"cell_type":"code","source":"# Augmentation d'images à la volée et split train / validation\ntrain_datagen =  \\\n        ImageDataGenerator(\n            rescale=1./255,\n            shear_range=0.1,\n            zoom_range=0.3,\n            rotation_range=10,\n            width_shift_range=0.1,\n            height_shift_range=0.1,\n            horizontal_flip=True,\n            vertical_flip=True,\n            validation_split=0.10)\n\n# Parcours du jeu d'entrainement (subset = 'training')\ntrain_generator = \\\n        train_datagen.flow_from_dataframe(\n            df_train,\n            TRAINING_DIR,\n            x_col='id',\n            y_col='label',\n            has_ext=True,\n            shuffle=True,\n            target_size=(IMGSIZE, IMGSIZE),\n            batch_size=BATCH_SIZE,\n            subset='training',\n            class_mode='categorical')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Jeu de Validation\nTraitement du jeu de **validation qui est une sous partie du jeu d'entrainement** (subset = 'validation'). \n\nA noter que pour le jeu de validation nous appliquons aussi l'augmentation des images à la volée (utilisation du train_datagen) , ce n'est pas une obligation."},{"metadata":{"trusted":true},"cell_type":"code","source":"valid_generator = \\\n        train_datagen.flow_from_dataframe(\n            df_train,\n            TRAINING_DIR,\n            x_col='id',\n            y_col='label',\n            has_ext=True,\n            shuffle=True,\n            target_size=(IMGSIZE, IMGSIZE),\n            batch_size=BATCH_SIZE,\n            subset='validation',\n            class_mode='categorical')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Jeu de Test\nLe jeu de test contient les images non classifiées sur lesquels nous devons faire les prédictions pour déterminer si l'image est un chien ou un chat.\n\nNous allons donc suivre le même processus que pour les données de l'entrainement sauf qu'ici : \n* Nous ne connaissons pas les labels des images (evidence)\n* Nous n'appliquons pas de transformations sur les images"},{"metadata":{"trusted":true},"cell_type":"code","source":"test_files = os.listdir(TEST_DIR)\ndf_test = pd.DataFrame({\"id\": test_files, 'label': 'nan'})","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# https://medium.com/@vijayabhaskar96/tutorial-on-keras-flow-from-dataframe-1fd4493d237c\n# Le ImageDataGenerator fait juste une normalisation des valeurs\ntest_datagen = ImageDataGenerator(rescale=1.0/255)\ntest_generator = test_datagen.flow_from_dataframe(\n    df_test, \n    TEST_DIR, \n    x_col='id',\n    y_col=None,       # None car nous ne connaissons pas les labels\n    has_ext=True, \n    target_size=(IMGSIZE, IMGSIZE), \n    class_mode=None,  # None pour le jeu de test\n    seed=42,\n    batch_size=1,     # batch_size = 1 sur le jeu de test\n    shuffle=False     # Pas de mélange sur le jeu de test\n)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Cette fonction permet de retourner le ratio entre chat vs chien (utile dans le cas ou une classe et proéminente sur les autres)\ndef get_weight(y):\n    class_weight_current =  cw.compute_class_weight('balanced', np.unique(y), y)\n    return class_weight_current\nclass_weights = get_weight(train_generator.classes)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Avec l'utilisation des generator il est nécessaire de maitriser les \"step_size\" : "},{"metadata":{"trusted":true},"cell_type":"code","source":"# Génération des STEPS_SIZE (comme nous utilisons des générateurs infinis)\nSTEP_SIZE_TRAIN = train_generator.n // train_generator.batch_size\nSTEP_SIZE_VALID = valid_generator.n // valid_generator.batch_size\nSTEP_SIZE_TEST  = test_generator.n  // test_generator.batch_size","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Callbacks Keras\n\nPetite nouveauté par rapport à la partie 1 : **L'utilisation des Callbacks**.\n\nLes callback permettent d'appliquer des traitements pendant l'entrainement du réseau.  Nous pouvons donc influer ou observer l'apprentissage en cours.\n\nDans notre cas nous allons en définir deux : un pour permettre **l'arrêt prématuré** de l'entrainement afin d'économiser les temps de calcul dans le cas ou le réseau ne progresse plus, et le deuxième pour influer sur un paramètre appelé le **Learning Rate** utilisé dans les calculs numériques de la decente de gradient."},{"metadata":{"trusted":true},"cell_type":"code","source":"# Permet de stopper l'apprentissage si il stagne\nEARLY_STOPPING = \\\n        EarlyStopping(\n            monitor='val_loss',\n            patience=STOPPING_PATIENCE,\n            verbose=VERBOSE,\n            mode='auto')\n\n\n# Reduit le LearningRate si stagnation\nLR_REDUCTION = \\\n        ReduceLROnPlateau(\n            monitor='val_acc',\n            patience=5,\n            verbose=VERBOSE,\n            factor=0.5,\n            min_lr=0.00001)\n\nCALLBACKS = [EARLY_STOPPING, LR_REDUCTION]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Architecture du CNN\n\nCi-dessous comme nous l'avons déjà vu nous définissons l'architecture du CNN puis de sa couche de classification (cf partie 1).\n\nComme nous ne prédisons que deux classes, **la couche de sortie sera composée de deux neurones**."},{"metadata":{"trusted":true},"cell_type":"code","source":"# Initialisation du modèle\nclassifier = Sequential()\n\n# Réalisation des couches de Convolution  / Pooling\n\n# ---- Conv / Pool N°1\nclassifier.add(Conv2D(filters=16,\n                      kernel_size=3,\n                      strides=1,\n                      padding='same',\n                      input_shape=(IMGSIZE, IMGSIZE, 3),\n                      activation='relu'))\n\nclassifier.add(BatchNormalization())\nclassifier.add(MaxPooling2D(pool_size=(2, 2), strides=2))\n\n# ---- Conv / Pool N°2\nclassifier.add(Conv2D(filters=16,\n                      kernel_size=3,\n                      strides=1,\n                      padding='same',\n                      activation='relu'))\n\nclassifier.add(BatchNormalization())\nclassifier.add(MaxPooling2D(pool_size=(2, 2), strides=2))\n\n# ---- Conv / Pool N°3\nclassifier.add(Conv2D(filters=32,\n                      kernel_size=3,\n                      strides=1,\n                      padding='same',\n                      activation='relu'))\n\nclassifier.add(BatchNormalization())\nclassifier.add(MaxPooling2D(pool_size=(2, 2), strides=2))\n\n# ---- Conv / Pool N°4\nclassifier.add(Conv2D(filters=32,\n                      kernel_size=3,\n                      strides=1,\n                      padding='same',\n                      activation='relu'))\n\nclassifier.add(BatchNormalization())\n\nclassifier.add(MaxPooling2D(pool_size=(2, 2), strides=2))\n\n\n# Fully Connected\n# Flattening : passage de matrices 3D vers un vecteur\nclassifier.add(Flatten())\nclassifier.add(Dense(512, activation='relu'))\nclassifier.add(Dropout(0.1))\n\n\n# Couche de sortie : classification => softmax sur le nombre de classe\nclassifier.add(\n    Dense(\n        units=2,\n        activation='softmax',\n        name='softmax'))\n\n# compilation du  model de classification\nclassifier.compile(\n    optimizer=OPTIMIZER,\n    loss='categorical_crossentropy',\n    metrics=['accuracy'])\n\n\nprint(\"Input Shape :{}\".format(classifier.get_input_shape_at(0)))\nclassifier.summary()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Entrainement du modèle\n\nIl est temps d'entrainer le modèle que nous avons crée avec les données que nous avons préparées."},{"metadata":{"trusted":true},"cell_type":"code","source":"def train_model():\n    # https://keras.io/models/sequential/#fit_generator\n    # Pour visualisation avec Tensorboard (console anaconda): \n    # tensorboard --logdir=/full_path_to_your_logs\n    history = classifier.fit_generator(\n        generator=train_generator,           # le générateur pour les données d'entrainement\n        steps_per_epoch=STEP_SIZE_TRAIN,     # le Step_size pour les données d'entrainement\n        validation_data=valid_generator,     # le générateur pour les données de validation\n        validation_steps=STEP_SIZE_VALID,    # le Step_size pour les données de validation\n        epochs=EPOCHS,                       # le nombre d'epoch sur l'ensemble du jeu de données\n        verbose=VERBOSE,                     # la verbosité\n        class_weight=class_weights,          # le ratio de répartition des classes chien/chat\n        callbacks=CALLBACKS)                 # la liste des fonctions de callback à appeler après chaque epoch\n    return history    ","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def plot_history(history):\n    # --------------------------------------\n    # Affichage des courbes accuracy et Loss\n    # --------------------------------------\n    plt.figure(1)\n    plt.subplot(211)\n    plt.plot(history.history['acc'])\n    plt.plot(history.history['val_acc'])\n    plt.title('model accuracy')\n    plt.ylabel('accuracy')\n    plt.xlabel('epoch')\n    plt.legend(['train', 'test'], loc='upper left')\n\n    plt.subplot(212)\n    plt.plot(history.history['loss'])\n    plt.plot(history.history['val_loss'])\n    plt.title('model loss')\n    plt.ylabel('loss')\n    plt.xlabel('epoch')\n    plt.legend(['train', 'test'], loc='upper left')\n    plt.show()     ","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Entrainement ou Chargement du modèle\n\n**Cette étape permet de charger un modèle déjà entrainé**.\n\nPour charger le modèle il faut d'abord avoir entrainé le réseau, commité le notebook et ensuite uploadé en zip le fichier contenant les poids et l'architecture du modèle. Il est également nécessaire de configurer la variable TRAIN_MODEL = False.\n\nA la suite de l'entrainement nous pouvons observer les courbes d'accuracy et de loss pour vérifier si le modèle apprend correctement"},{"metadata":{"trusted":true},"cell_type":"code","source":"if (TRAIN_MODEL):\n    print(\"Entrainement du modèle CNN\")\n    hist = train_model()     # Entrainement du modèle\n    plot_history(hist)       # Affichage de la courbe d'apprentissage\n    classifier.save(MODEL_NAME + '.h5')\nelse:\n    print(\"Chargement du modèle...\")\n    classifier = load_model('../input/weight/cnn/cnn.h5')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Evaluations du modèle\n\nNous procédons maintenant à l'évaluation de la performance du modèle sur le jeu de Validation. \nLa première valeur est le Loss et la seconde l'accuracy"},{"metadata":{"trusted":true},"cell_type":"code","source":"classifier.evaluate_generator(generator=valid_generator, steps=STEP_SIZE_TEST)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Prédictions"},{"metadata":{},"cell_type":"markdown","source":"### Génération des prédictions depuis le modèle\n\nLe modèle étant entrainé il est temps de générer les prédictions sur les images, nous obtenons en sortie deux probabilités fournies par la couche de sortie du modèle de classification."},{"metadata":{"trusted":true},"cell_type":"code","source":"# Le générateur doit être reseter avant utilisation pour les prédictions\ntest_generator.reset()\npred=classifier.predict_generator(test_generator, steps=STEP_SIZE_TEST, verbose=1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Visualisation du vecteur de probabilité des 5 premières lignes des prédictions\npred[0:5,:]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"predicted_class_indices=np.argmax(pred,axis=1)\nlabels = (train_generator.class_indices)\nlabels = dict((v,k) for k,v in labels.items())\npredictions = [labels[k] for k in predicted_class_indices]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Création d'un dataframe contenant les images et classes prédites\nfilenames=test_generator.filenames\nresults=pd.DataFrame({\"id\":filenames,\"label\":predictions})\nresults.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Mise en forme des prédictions\nMise en forme pour préparer le format attendu pour la soumission des résultats"},{"metadata":{"trusted":true},"cell_type":"code","source":"# copy du dataframe de resultat\nsoumission = results.copy()\n\n# suppression de l'extension du fichier et conversion de la colonne en int avec la méthode vectorielle str\nsoumission['id'] = soumission['id'].str[:-4].astype('int')\nsoumission.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Tri sur la colonne des id avec la methode sort_values du dataframe\nsoumission = soumission.sort_values(by=['id'])\nsoumission.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Remplacement du label 'cat' ou 'dog' par une valeur numérique : utilisation de la fonction replace\n# Rappel sur les classes : {0: \"Cat\", 1: \"Dog\"} \nsoumission.replace({'dog': 1, 'cat': 0}, inplace=True)\nsoumission.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Ecriture du fichier de soumission"},{"metadata":{"trusted":true},"cell_type":"code","source":"# conversion du Dataframe vers un fichier de sortie\n# This is saved in the same directory as your notebook\nfilename = 'results.csv'\nsoumission.to_csv(filename,index=False)\nprint('Fichier enregistré: ' + filename)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Affichage aléatoire des images prédites"},{"metadata":{"trusted":true},"cell_type":"code","source":"import random\n\nn = results.shape[0]\nf = list(np.arange(1,n))\n\nc = 20\nr =random.sample(f, c)\nnrows = 4\nncols = 5\nfig, ax = plt.subplots(nrows=nrows, ncols=ncols, figsize=(nrows*5, ncols*5))    \nfor i in range(c):\n    file = str(results['id'][r[i]])\n    path = TEST_DIR+\"/\"+file\n    img = plt.imread(path)\n    plt.subplot(4, 5, i+1)\n    plt.imshow(img, aspect='auto')\n    plt.xticks([])\n    plt.yticks([])\n    plt.title(str(results['id'][r[i]])+\"\\n\"+str(results['label'][r[i]]))\nplt.show()","execution_count":null,"outputs":[]}],"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}