diff --git a/TP6/TP6_ex2_correc.ipynb b/TP6/TP6_ex2_correc.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..ee608e53109f72315fe42cf342825b8e89f03f4c
--- /dev/null
+++ b/TP6/TP6_ex2_correc.ipynb
@@ -0,0 +1,747 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Introduction à l'apprentissage automatique - TP6 exercice 2\n",
+    "\n",
+    "### Classification d'images au XXIème siècle: _CNN_ et _transfer learning_\n",
+    "\n",
+    "<br>\n",
+    "\n",
+    "__Remarque préliminaire__: il s'agit plus d'un tutoriel que d'un exercice. Vous n'avez pas de code à écrire, mais passez y suffisamment de temps et assurez-vous de bien comprendre.\n",
+    "\n",
+    "<br>\n",
+    "\n",
+    "Dans cet exercice, nous mettons en oeuvre des réseaux de neurones convolutifs. Nous allons utiliser la bibliothèque [Tensorflow](https://www.tensorflow.org/) par l'intermédiaire de l'API Keras qui simplifie la manipulation des réseaux. De nombreuses ressources pédagogiques sont disponibles sur la page web de Tensorflow: elles pourront vous être utiles en stage, pour un projet, etc.\n",
+    "\n",
+    "Commencez par prendre connaissance des pages suivantes:\n",
+    "* [Page wikipedia Keras](https://fr.wikipedia.org/wiki/Keras)\n",
+    "* [Page wikipedia Tensorflow](https://fr.wikipedia.org/wiki/TensorFlow)\n",
+    "\n",
+    "\n",
+    "<br>\n",
+    "\n",
+    "Installez Tensorflow et Keras si ce n'est déjà fait: voir les instructions sur la page Arche du cours. \n",
+    "\n",
+    "\n",
+    "<br>\n",
+    "\n",
+    "Notre exercice est partiellement une adaptation simplifiée de [cette page](https://blog.keras.io/building-powerful-image-classification-models-using-very-little-data.html) du blog de François Chollet, papa de Keras. Lisez cette page __à la fin du TP__, notamment ce qui concerne la génération d'images synthétiques pour augmenter la base d'apprentissage, que nous n'abordons pas dans cet exercice par souci de simplicité. Par ailleurs, le problème __chiens et chats__ du blog ne concerne que deux classes alors que le nôtre est multiclasses (7 classes à identifier).\n",
+    "\n",
+    "Nous allons donc adapter les réseaux décrits sur la page du blog de la manière suivante:\n",
+    "- il faut 7 neurones sur la couche de sortie, qui doit être de type `softmax` (dans le cas biclasse, il faut un seul neurone de sortie, de type `sigmoid`, comme expliqué en cours)\n",
+    "- le _loss_ est de type `categorical_crossentropy` et non `binary_crossentropy`, comme dans le cours également."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 0. Préparatifs\n",
+    "\n",
+    "On commence par charger quelques bibliothèques et définir la fonction `display_test` (comme dans l'exercice précédent)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numpy as np\n",
+    "import os\n",
+    "import matplotlib.pyplot as plt\n",
+    "from sklearn import model_selection\n",
+    "\n",
+    "%matplotlib inline\n",
+    "\n",
+    "def display_test(X_image_test,Y_test,Y_test_pred,txt):\n",
+    "    # affiche les résultats de classification pour 120 images aléatoires\n",
+    "    # paramètres: \n",
+    "    # X_image_test: tableau des images de test \n",
+    "    # Y_test: classes d'appartenance réelles\n",
+    "    # Y_test_pred: classes prédites\n",
+    "    # txt: légende de la figure\n",
+    "    alea=np.random.choice(len(X_image_test),size=120,replace=False)\n",
+    "    plt.figure(figsize=[18,12])    \n",
+    "    for n in range(120):\n",
+    "        plt.subplot(12,10,n+1,xticks=[],yticks=[])\n",
+    "        plt.imshow(X_image_test[alea[n]],cmap='gray')\n",
+    "        if Y_test_pred[alea[n]]==Y_test[alea[n]]:\n",
+    "            plt.text(0.1,0.1,str(Y_test_pred[alea[n]])+' / '+str(Y_test[alea[n]]),fontsize=8,bbox=dict(facecolor='white', alpha=1))\n",
+    "        else:\n",
+    "            plt.text(0.1,0.1,str(Y_test_pred[alea[n]])+' / '+str(Y_test[alea[n]]),fontsize=8,bbox=dict(facecolor='red', alpha=1))\n",
+    "    plt.suptitle('prediction '+txt)\n",
+    "    plt.show();"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "On charge à présent les données. Cette fois, nos classifieurs vont admettre directement les images en entrée, et pas des descripteurs comme les histogrammes de l'exercice 1. Comme les réseaux de neurones utilisés exigent des images de taille identique en entrée, il va falloir redimensionner les images de la base de données avant de les stocker dans les tableaux `X_train` et `X_test`. On choisit une taille de redimensionnement de $150\\times 100$ pixels (150 colonnes et 100 lignes), ce qui a l'avantage de ne pas trop déformer la majorité des images (l'\"aspect ratio\" est globalement conservé).\n",
+    "\n",
+    "Notez le nombre d'images dans les bases d'entraînement et de test."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from PIL import Image  # on utilise la bibliothèque de manipulation d'image PIL pour redimensionner les images\n",
+    "\n",
+    "# modifiez ici le chemin d'accès à vos données:\n",
+    "path=\"./Caltech256_small/\" \n",
+    "\n",
+    "X=[]\n",
+    "Y=[]\n",
+    "X_image=[]\n",
+    "X_train=[]\n",
+    "X_test=[]\n",
+    "\n",
+    "y=0  # numéro de classe\n",
+    "\n",
+    "for directory in os.listdir(path):\n",
+    "    count = 0 # indice de l'image traitée dans le répertoire courant\n",
+    "    print(\"%s  - classe: %d\" % (directory,y))\n",
+    "    for file in os.listdir(path+directory):\n",
+    "        img = plt.imread(path+directory+\"/\"+file)\n",
+    "        if (len(img.shape)==2):\n",
+    "            img=np.repeat(img[:,:,np.newaxis],3,axis=2)        \n",
+    "        X_image.append(img)\n",
+    "        Y.append(y)\n",
+    "        count = count+1\n",
+    "    y = y+1\n",
+    "    \n",
+    "X_image_train, X_image_test, Y_train, Y_test = model_selection.train_test_split(X_image, Y, test_size=0.2, random_state=1)\n",
+    "\n",
+    "for i in range(len(X_image_train)):\n",
+    "    img=X_image_train[i]\n",
+    "    imgresize=np.array(Image.fromarray(img).resize((150,100)))/256  # images redimensionnées, et canaux normalisés entre 0 et 1\n",
+    "    X_train.append(imgresize)\n",
+    "\n",
+    "for i in range(len(X_image_test)):\n",
+    "    img=X_image_test[i]\n",
+    "    imgresize=np.array(Image.fromarray(img).resize((150,100)))/256  # images redimensionnées, et canaux normalisés entre 0 et 1\n",
+    "    X_test.append(imgresize)\n",
+    "\n",
+    "\n",
+    "# tensorflow demande que les données soient représentées comme un tableau numpy:    \n",
+    "X_train=np.asarray(X_train)\n",
+    "Y_train=np.asarray(Y_train)\n",
+    "X_test=np.asarray(X_test)\n",
+    "Y_test=np.asarray(Y_test)\n",
+    "\n",
+    "print(\"base d'apprentissage: %d images\" %X_train.shape[0])\n",
+    "print(\"base de test: %d images\" %X_test.shape[0])\n",
+    "print(\"\\ndimension des tableaux:\")\n",
+    "print(\"X_train\")\n",
+    "print(X_train.shape)\n",
+    "print(\"Y_train\")\n",
+    "print(Y_train.shape)\n",
+    "print(\"X_test\")\n",
+    "print(X_test.shape)\n",
+    "print(\"Y_test\")\n",
+    "print(Y_test.shape)\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "n_image=200\n",
+    "plt.figure()\n",
+    "plt.imshow(X_image_train[n_image]);\n",
+    "plt.title(\"Un exemple d'image\");\n",
+    "plt.figure()\n",
+    "plt.imshow(X_train[n_image].reshape(100,150,3))\n",
+    "plt.title(\"Sa représentation dans la base d'apprentissage (100x150)\");"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<br>\n",
+    "\n",
+    "## 1. Classification par un réseau convolutif\n",
+    "\n",
+    "Nous allons tester le premier réseau étudié dans le blog de F. Chollet, inspiré des réseaux convolutifs comme __[LeNet5](http://yann.lecun.com/exdb/lenet/index.html)__ proposés par __[Yann Le Cun](https://fr.wikipedia.org/wiki/Yann_Le_Cun)__ dans les années 1990.\n",
+    "\n",
+    "Le réseau est formé des couches suivantes.\n",
+    "\n",
+    "Partie \"définition de descripteurs\" ( _features_ ):\n",
+    "- entrée: un image couleur (trois canaux: rouge, vert, bleu) de taille $100\\times 150$ pixels\n",
+    "- une couche convolutive de 32 filtres de couverture spatiale $3\\times3$ pixels (ce sont donc des filtres $3\\times 3\\times 3$ car ils agissent sur les trois canaux), chaque neurone ayant une activation ReLU.\n",
+    "- une couche `MaxPooling` réduisant d'un facteur 2 la largeur et hauteur des sorties (mais pas la profondeur)\n",
+    "- une couche convolutive de 32 filtres $3\\times3$ (ce sont donc des filtres $3\\times 3\\times 32$ car ils agissent sur toute la profondeur de la couche précédente), activation ReLU\n",
+    "- une couche `MaxPooling` \n",
+    "- une couche convolutive de 64 filtres $3\\times3$, activation ReLU\n",
+    "- une couche `MaxPooling` \n",
+    "\n",
+    "Partie \"classification\", qui ressemble au perceptron multi-couches de l'exercice précédent:\n",
+    "- pour commencer on \"applatit\" la couche finale de la partie précédente, qui est vue comme une colonne de neurones.\n",
+    "- une couche \"fully connected\" de 64 neurones, activation ReLU, et dropout de 0.5. Cela signifie que pendant l'apprentissage, pour chaque batch traité on élimine aléatoirement 50% des connexions. Cela a pour effet d'éviter que le réseau s'adapte trop bien aux données d'apprentissage, et donc d'éviter le surapprentissage, comme expliqué dans le polycopié. Naturellement, lorsque le réseau est utilisé après apprentissage pour faire une prédiction, il n'y a pas de _dropout_. \n",
+    "- une couche de sortie SoftMax à 7 neurones (car nous devons discriminer 7 classes)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from keras.models import Sequential\n",
+    "from keras.layers import Conv2D, MaxPooling2D\n",
+    "from keras.layers import Activation, Dropout, Flatten, Dense\n",
+    "from keras.utils import plot_model\n",
+    "\n",
+    "model = Sequential() # le réseau est défini couche après couche dans ce qui suit\n",
+    "\n",
+    "# partie \"features\"\n",
+    "\n",
+    "model.add(Conv2D(32, (3, 3), input_shape=(100, 150, 3)))   # 100 lignes et 150 colonnes (notation matricielle)\n",
+    "model.add(Activation('relu'))\n",
+    "model.add(MaxPooling2D(pool_size=(2, 2)))\n",
+    "\n",
+    "model.add(Conv2D(32, (3, 3)))\n",
+    "model.add(Activation('relu'))\n",
+    "model.add(MaxPooling2D(pool_size=(2, 2)))\n",
+    "\n",
+    "model.add(Conv2D(64, (3, 3)))\n",
+    "model.add(Activation('relu'))\n",
+    "model.add(MaxPooling2D(pool_size=(2, 2)))\n",
+    "\n",
+    "# à ce stade, on sort des caractéristiques 3D "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# partie \"classifier\"\n",
+    "\n",
+    "model.add(Flatten())  #  Ceci transforme les caractéristique 3D en une \"colonne\" de neurones d'entrée, comme dans les MLP classiques\n",
+    "model.add(Dense(64))\n",
+    "model.add(Activation('relu'))\n",
+    "model.add(Dropout(0.5))\n",
+    "model.add(Dense(7))  \n",
+    "model.add(Activation('softmax'))  "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# on précise à présent le loss à optimiser (ici, \"sparse\" car les \"y\" sont codés comme des entiers et pas par \"one-hot-encoding\")\n",
+    "# ainsi qu'une métrique à afficher (score de classification) et un optimiseur (rmsprop adapte le taux d'apprentissage automatiquement)\n",
+    "\n",
+    "model.compile(loss='sparse_categorical_crossentropy', metrics=['accuracy'], optimizer='rmsprop')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "scrolled": false
+   },
+   "outputs": [],
+   "source": [
+    "# résumé du modèle précédemment défini:\n",
+    "model.summary()\n",
+    "\n",
+    "# affichage graphique\n",
+    "plot_model(model,  show_shapes=True)\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Ce modèle nécessite l'apprentissage de 725 479 paramètres, qui sont dans la partie \"fully connected\" pour la très grande majorité. Par exemple, la seconde couche de convolution nécessite l'estimation de 9248 paramètres. En effet, elle effectue 32 convolutions de taille $3 \\times 3 \\times \\text{(épaisseur de la sortie de la couche précédente)}$, soit: $(9\\times 32+1)\\times 32 = 9248$ paramètres (la couche précédente effectue 32 convolutions, et +1 car il faut ajouter le terme de biais).\n",
+    "\n",
+    "Remarquez que les couches convolutives \"rognent\" les bords (de manière à ce que le noyau de convolution \"ne dépasse pas\" des bords du domaine où il est appliqué). Cela explique pourquoi la première couche part d'images de taille $100\\times 150$ et aboutit à un résultat de taille $98\\times 148$.\n",
+    "\n",
+    "\n",
+    "<br>\n",
+    "\n",
+    "L'apprentissage proprement dit se lance dans la cellule suivante. Notez la taille des batches utilisés ainsi que le nombre d'epochs. Les données de validation ne sont pas utilisées pour l'apprentissage (à la fin de chaque epoch), seulement à titre indicatif pour surveiller un surapprentissage potentiel, comme expliqué en cours. Constatez la décroissance du _loss_ au cours de l'apprentissage. L'_accuracy_ est calculée également mais n'intervient pas dans l'apprentissage: il s'agit de la proportion de prédictions correctes dans la base d'apprentissage et dans la base de test/validation."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "model.fit(X_train,Y_train,batch_size=16,epochs=15,validation_data=(X_test,Y_test))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Constatez que ce réseau convolutif présente de meilleures performance que le meilleur modèle de l'exercice 1 (si ce n'est pas le cas, vous n'avez pas eu de chance: relancez l'apprentissage...). Le modèle présente sans doute un peu de surapprentissage: `accuracy` sur la base d'apprentissage est assez sensiblement supérieur à `val_accuracy` (sur la base test).\n",
+    "\n",
+    "Vérifiez le rôle du _dropout_: commentez la ligne `model.add(Dropout(0.5))` dans ce qui précède, et relancez la construction (à partir de la cellule où figure `model = Sequential()`) puis l'apprentissage du réseau (attention: il ne suffit pas de relancer le `fit` car on continuerait alors l'apprentissage à partir des paramètres déjà estimés). Vous allez voir le réseau apprendre \"quasiment par coeur\" la base d'apprentissage (_accuracy_ à 0.98-0.99) mais une accuracy qui finit par décroître sur la base de test. N'oubliez pas de revenir au réseau appris avec _dropout_ avant de continuer.\n",
+    "\n",
+    "_Remarque_: il ne faut pas perdre de vue que notre base de données est assez petite. On pourrait utiliser un réseau plus simple avec des performances similaires.\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<br>\n",
+    "\n",
+    "La cellule suivante permet de calculer les prédictions du réseau (`predict`) sur la base test. Il s'agit des probabilités _a posteriori_ de chaque classe, on assigne donc chaque observation à la classe de probabilité maximale, selon le principe MAP. "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "scrolled": false
+   },
+   "outputs": [],
+   "source": [
+    "from sklearn import metrics\n",
+    "\n",
+    "y_proba = model.predict(X_test)\n",
+    "#print(y_proba) # les proba a posteriori de chaque classe\n",
+    "Y_test_pred=y_proba.argmax(axis=1)  # classe prédite: celle de probabilité maximale\n",
+    "\n",
+    "print(\"classes prédites:\")\n",
+    "print(Y_test_pred)\n",
+    "print(\"vraies classes:\")\n",
+    "print(Y_test)\n",
+    "print(\"\\nscore de classification: %.3f\\n\" %metrics.accuracy_score(Y_test,Y_test_pred))\n",
+    "\n",
+    "print(\"matrice de confusion\")\n",
+    "print(metrics.confusion_matrix(Y_test,Y_test_pred))\n",
+    "\n",
+    "display_test(X_image_test,Y_test,Y_test_pred,'CNN')"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Les classes binoculars (0), gorilla (1), horses (2), ainsi que airplanes (5) restent les plus difficiles à discriminer, comme dans l'exercice 1."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<br>\n",
+    "\n",
+    "## 2. Génération aléatoire d'exemples adversariaux\n",
+    "\n",
+    "<br> \n",
+    "\n",
+    "La cellule suivante prend une image-test, et change aléatoirement la valeur (R,V,B) de pixels par un triplet dont les composantes varient entre $0$ et $M$, jusqu'à ce que l'image ne soit plus classée dans la bonne catégorie...  "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "scrolled": false
+   },
+   "outputs": [],
+   "source": [
+    "adv_example=np.zeros([1, 100, 150, 3])\n",
+    "adv_example[0,:,:,:]=X_test[100]  # vous pouvez expérimenter avec d'autres images\n",
+    "init_pred=model.predict(adv_example).argmax()  # classe prédite pour l'image initiale\n",
+    "\n",
+    "plt.figure(figsize=[12,8])\n",
+    "plt.imshow(adv_example[0])\n",
+    "plt.title(\"classe prédite pour l'exemple de test considéré: %d\" % init_pred)\n",
+    "\n",
+    "M=10\n",
+    "count=0\n",
+    "for c in range(1000):\n",
+    "    count=count+1\n",
+    "    adv_example[0,np.random.randint(low=0,high=100),np.random.randint(low=0,high=150),:] = M*(np.random.random_sample((3,)))\n",
+    "    adv_pred=model.predict(adv_example, verbose=0).argmax()\n",
+    "    if (adv_pred!=init_pred):\n",
+    "        break\n",
+    "\n",
+    "plt.figure(figsize=[12,8])\n",
+    "plt.imshow(adv_example[0])\n",
+    "plt.title(\"classe prédite pour l'exemple de test considéré: %d - %d pixels modifiés\" % (adv_pred, count));\n",
+    "           "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Quelques remarques...\n",
+    "\n",
+    "L'objectif de cette expérience très simple est d'illustrer que la perception humaine n'a rien à voir avec la classification algorithmique.\n",
+    "\n",
+    "Relancez plusieurs fois la cellule: vous voyez que la classe affectée à notre exemple adversarial change, et que quelques pixels suffisent à tromper le modèle. A quelques pixels près, un avion est reconnu comme un cheval...\n",
+    "\n",
+    "En fait on triche un peu ici: on autorise des modifications des valeurs des pixels entre 0 et M=10, alors que les composantes RVB des \"vraies\" images varient entre 0 et 1.\n",
+    "\n",
+    "Dans les exemples présentés en cours (issus d'articles de recherche récents), les perturbations adversariales ne sont pas générées aléatoirement, mais en fonction du modèle de classification, et de manière à atteindre une classe prédeterminée."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<br>\n",
+    "\n",
+    "## 3. Apprentissage par transfert\n",
+    "\n",
+    "<br>\n",
+    "\n",
+    "L'apprentissage est limité par la taille de la base d'apprentissage: nous avons dans chaque catégorie de l'ordre de 200 images de taille $150\\times100\\times3$, pour apprendre plus de 700000 paramètres.\n",
+    "\n",
+    "Une manière de surmonter cette difficulté est de réutiliser un réseau complexe dont les paramètres auront été appris sur une très grande base de données, par des chercheurs ou entreprises disposant de grandes ressources de calcul. On parle d'apprentissage _par transfert_.\n",
+    "\n",
+    "Nous allons adapter le réseau VGG16, décrit dans le polycopié. VGG16 a été construit pour un problème de classification à 1000 classes, et ses paramètres ont été appris sur 14 millions d'images.\n",
+    "\n",
+    "L'idée est d'utiliser uniquement la partie de VGG16 construisant des descripteurs (on ne modifie pas les paramètres de cette partie), puis d'utiliser ces descripteurs en entrée d'un classifieur permettant de discriminer 7 classes. Seuls les paramètres de la partie \"classifieur\" seront appris sur nos données. On peut aussi dire qu'on remplace la partie \"features\" du modèle CNN précédent par la partie \"features\" de VGG16 dont on ne modifie pas les poids. Remarquons au passage que nous pourrions utiliser un autre classifieur qu'un réseau de neurones (les curieux pourront aller voir la dernière section du carnet).\n",
+    "\n",
+    "Bien entendu, tout cela a une chance raisonnable de fonctionner si les descripteurs fournis par VGG16 sont bien adaptés à nos images, donc si les images d'apprentissage de VGG16 ressemblent aux nôtres. Les classes que nous cherchons à identifier ne sont pas présentes dans VGG16 (je n'ai pas vérifié mais ce ne sont pas les mêmes images): ce n'est pas un problème puisque nous entraînerons la partie \"classifieur\" sur nos données.\n",
+    "\n",
+    "Voir plus bas en cas de capacités de calcul, de bande passante réseau, ou d'espace disque limitées."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "scrolled": false
+   },
+   "outputs": [],
+   "source": [
+    "from tensorflow.keras import applications\n",
+    "\n",
+    "# on charge VGG16 dont les paramètres ont été appris sur le dataset Imagenet, \n",
+    "# sans la couche \"top\" de classification (on utilise donc uniquement la partie \"features\")\n",
+    "modelVGG = applications.VGG16(include_top=False, weights='imagenet')\n",
+    "\n",
+    "modelVGG.summary()\n",
+    "\n",
+    "plot_model(modelVGG,  show_shapes=True)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "On remarque que si on considère uniquement la partie convolutive du réseau (le modèle précédent), la dimension des images en entrée peut être arbitraire (d'où les `None` dans le tableau ci-dessus). En effet, les paramètres sont ici les coefficients des noyaux de convolution, et les convolutions peuvent s'appliquer sur des images de taille arbitraire. La seule contrainte est que les images en entrée doivent avoir trois canaux (les noyaux de convolutions de la première couche sont de taille $3 \\times 3 \\times 3$). Comme nos images en entrée ont toute la même définition, les _features_ en sortie auront aussi tous la même dimension.\n",
+    "\n",
+    "On peut donc calculer les caractéristiques ( _features_ ) de nos images train et test à l'aide de `modelVGG`:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# peut prendre quelques minutes\n",
+    "\n",
+    "features_train = modelVGG.predict(X_train)\n",
+    "np.save('features_train.npy',features_train) \n",
+    "print(features_train.shape)\n",
+    "\n",
+    "features_test = modelVGG.predict(X_test)\n",
+    "np.save('features_test.npy',features_test)\n",
+    "print(features_test.shape)\n",
+    "\n",
+    "# vous pouvez aussi continuer en chargeant les variables sans les calculer: \n",
+    "# il suffit de commenter les lignes précédentes et décommenter les lignes suivantes (voir explications ci-dessous)\n",
+    "#features_train = np.load('features_train.npy')\n",
+    "#features_test = np.load('features_test.npy')"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "La base d'apprentissage est formée de 1052 observations, celle de test de 263 observations. La sortie de la partie \"convolutive\" de VGG16 sort des blocs de $3\\times 4\\times 512$ valeurs (512 convolutions dans la dernière couche convolutive, qui sortent chacunes $3\\times 4$ valeurs pour nos entrées de taille $100\\times150$).\n",
+    "\n",
+    "Au cas où votre connexion (ou votre espace disque) ne vous permet pas de charger VGG16, ou si votre processeur n'est pas assez rapide, vous pouvez charger les features calculés pour vous, à ces liens ou sur Arche:\n",
+    "- [features pour la base d'apprentissage](https://members.loria.fr/FSur/enseignement/apprauto/features_train.npy)\n",
+    "- [features pour la base de test](https://members.loria.fr/FSur/enseignement/apprauto/features_test.npy)\n",
+    "\n",
+    "à stocker dans la variable correspondante par: \n",
+    "\n",
+    "```\n",
+    "features_train = np.load('features_train.npy')\n",
+    "\n",
+    "features_test = np.load('features_test.npy')\n",
+    "```\n",
+    "\n",
+    "__Attention__: ces features sont calculés sur des images chargées dans l'ordre de numérotation des répertoires correspondant aux classes (classe 0: binoculars; 1: gorilla, 2: horses, etc.), et séparées de la même manière par `train_test_split`. Sur certains OS, les images ne sont pas chargées dans le même ordre, ou la séparation aléatoire entre bases d'apprentissage et de test diffère (même en précisant la _seed_ du générateur aléatoire): il est alors impossible d'utiliser ces fichiers."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Ensuite, on définit le réseau classifieur qui admettra en entrée les caractéristiques précédentes (de dimension  3x4x512=6144 ici) et calculera les probabilités a posteriori. \n",
+    "\n",
+    "Ce réseau est constitué:\n",
+    "* d'une couche d'entrée où on \"applatit\" les _features_ (ce sont donc des vecteurs de dimension 6144), \n",
+    "* d'une simple couche cachée de 256 neurones (notez le dropout), \n",
+    "* et de 7 neurones de sortie (autant que de catégories). \n",
+    "\n",
+    "N'hésitez pas à expérimenter avec d'autre valeurs que 256 neurones dans la couche cachée. "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "model_classif = Sequential()\n",
+    "model_classif.add(Flatten(input_shape=features_train.shape[1:]))\n",
+    "model_classif.add(Dense(256, activation='relu'))\n",
+    "model_classif.add(Dropout(0.5))\n",
+    "model_classif.add(Dense(7, activation='softmax'))  \n",
+    "\n",
+    "model_classif.compile(optimizer='rmsprop',\n",
+    "              loss='sparse_categorical_crossentropy',  \n",
+    "              metrics=['accuracy'])\n",
+    "\n",
+    "model_classif.summary()\n",
+    "\n",
+    "plot_model(model_classif, show_shapes=True)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "On entraîne à présent notre classifieur sur les _features_ de VGG16. Notez que l'apprentissage est très rapide."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "scrolled": false
+   },
+   "outputs": [],
+   "source": [
+    "model_classif.fit(features_train, Y_train, epochs=10, batch_size=16, validation_data=(features_test, Y_test))\n",
+    "# model.save_weights('model_classif.h5')  # (pour sauvegarder le réseau entraîné)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "On visualise le résultat final:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "y_proba = model_classif.predict(features_test)\n",
+    "#print(y_proba)\n",
+    "Y_test_pred = y_proba.argmax(axis=1)\n",
+    "\n",
+    "print(\"classes prédites:\")\n",
+    "print(Y_test_pred)\n",
+    "\n",
+    "print(\"score de classification: %.3f\" %metrics.accuracy_score(Y_test,Y_test_pred))\n",
+    "\n",
+    "print(\"matrice de confusion\")\n",
+    "print(metrics.confusion_matrix(Y_test,Y_test_pred))\n",
+    "\n",
+    "display_test(X_image_test,Y_test,Y_test_pred,'with pretrained network')"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Le taux de classifiations correctes est à présent supérieur à 95%. Certaines classes ne donnent pas d'erreur.\n",
+    "\n",
+    "<br>\n",
+    "\n",
+    "Au passage, notons que notre génération aléatoire d'exemples adversariaux fonctionne toujours, comme on peut le constater à l'aide de la cellule suivante. Il faut néanmoins modifier un plus grand nombre de pixels."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "scrolled": false
+   },
+   "outputs": [],
+   "source": [
+    "adv_example=np.zeros([1, 100, 150, 3])\n",
+    "adv_example[0,:,:,:]=X_test[100]\n",
+    "init_pred=model_classif.predict(modelVGG.predict(adv_example)).argmax()\n",
+    "\n",
+    "plt.figure(figsize=[12,8])\n",
+    "plt.imshow(adv_example[0])\n",
+    "plt.title(\"classe prédite pour l'exemple de test considéré: %d\" % init_pred)\n",
+    "\n",
+    "M=10\n",
+    "count=0\n",
+    "for c in range(1000):\n",
+    "    count=count+1\n",
+    "    adv_example[0,np.random.randint(low=0,high=100),np.random.randint(low=0,high=150),:] = M*(np.random.randint((3,)))\n",
+    "    adv_pred=model_classif.predict(modelVGG.predict(adv_example,verbose=0),verbose=0).argmax(axis=1)\n",
+    "    if (adv_pred!=init_pred):\n",
+    "        break\n",
+    "\n",
+    "plt.figure(figsize=[12,8])\n",
+    "plt.imshow(adv_example[0])\n",
+    "plt.title(\"classe prédite pour l'exemple de test considéré: %d - %d pixels modifiés\" % (adv_pred, count));"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<br>\n",
+    "\n",
+    "## 4. Et si on utilisait un autre classifieur qu'un MLP sur les descripteurs calculés par VGG16? \n",
+    "\n",
+    "<br>\n",
+    "\n",
+    "Dans notre expérience d'apprentissage par transfert, on utilise des descripteurs calculés par la partie \"convolutive\" de VGG16, puis on entraîne un perceptron multicouche (MLP) classifieur de manière à discriminer nos sept classes. Comme nous l'avons suggéré plus haut, on pourrait envisager d'utiliser autre chose qu'un MLP. Nous allons tester ce que donnerait la SVM RBF.\n",
+    "\n",
+    "<br>\n",
+    "\n",
+    "En sortie de VGG16, les descripteurs sont des tableaux multidimensionnels de taille $3\\times 4\\times 512$, que l'on commence par transformer en des vecteurs de dimension 6144 dans la cellule suivante:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "X_train=[]\n",
+    "X_test=[]\n",
+    "for i in range(len(features_train)):\n",
+    "    X_train.append(features_train[i].flatten())\n",
+    "for i in range(len(features_test)):\n",
+    "    X_test.append(features_test[i].flatten())\n",
+    "print(\"\\n%d observations dans la base d'apprentissage, chacune est décrite par un vecteur de dimension %d\" % (len(X_train), len(X_train[0])) )\n",
+    "print(\"\\n%d observations dans la base de test, chacune est décrite par un vecteur de dimension %d\" % (len(X_test), len(X_test[0])) )\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Ensuite on cherche la valeur optimale pour l'hyperparamètre $C$:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from sklearn import svm, model_selection, metrics\n",
+    "\n",
+    "C_range=10**np.arange(-3,3.5,1)\n",
+    "parameters = {'C':C_range}\n",
+    "\n",
+    "SVM = svm.SVC(kernel='rbf')\n",
+    "parameters = { 'C':C_range }\n",
+    "gridsearch=model_selection.GridSearchCV(SVM, parameters, cv=5, n_jobs=-1)\n",
+    "gridsearch.fit(X_train,Y_train)\n",
+    "print(\"Meilleur paramètre pour SVM_rbf:\")\n",
+    "print(gridsearch.best_params_)\n",
+    "\n",
+    "scores = gridsearch.cv_results_['mean_test_score']\n",
+    "plt.figure(figsize=[7,7])\n",
+    "plt.semilogx(C_range,scores)\n",
+    "plt.grid()\n",
+    "plt.xlabel('C')\n",
+    "plt.ylabel(\"score\")\n",
+    "plt.show();"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Enfin, on procède à l'apprentissage et on visualise le résultat:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "SVM_r = svm.SVC(kernel='rbf',C=10)\n",
+    "SVM_r.fit(X_train,Y_train)\n",
+    "print(\"score SVM rbf : %.3f\" % SVM_r.score(X_test, Y_test) )\n",
+    "Y_test_pred=SVM_r.predict(X_test)\n",
+    "print(metrics.confusion_matrix(Y_test,Y_test_pred))\n",
+    "display_test(X_image_test,Y_test,Y_test_pred,\"SVM rbf\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "On obtient un taux de classifications correctes (au moins) aussi bon! \n",
+    "\n",
+    "L'enseignement principal est que les performances de l'apprentissage profond (en particulier des CNN) vient du calcul \"automatique\" de descripteurs bien adaptés. En effet, on voit dans cette expérience particulière que si on remplace la partie classifieur par une SVM on obtient également d'excellents résultats. \n",
+    "\n",
+    "Lorsqu'on entraîne \"de zéro\" (sans _transfer learning_), il n'est pas possible de procéder ainsi: il faut tout faire avec un réseau de neurones pour entraîner le modèle (images en entrée, probabilité des classes en sortie) à l'aide de la rétropropagation."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.10.8"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/TP6/TP6_ex3_correc.ipynb b/TP6/TP6_ex3_correc.ipynb
deleted file mode 100644
index cc651501d42d5b44ecf4af39d523e70b94872481..0000000000000000000000000000000000000000
--- a/TP6/TP6_ex3_correc.ipynb
+++ /dev/null
@@ -1,223 +0,0 @@
-{
- "cells": [
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "# Introduction à l'apprentissage automatique - TP6 exercice 3 - <font color=red> CORRECTION </font>\n",
-    "\n",
-    "### CNN pour Fashion-MNIST\n",
-    "\n",
-    "\n",
-    "<br>\n",
-    "\n",
-    "Dans cet exercice nous allons mettre en oeuvre un réseau de neurones convolutif pour la base d'images Fashion-MNIST.\n",
-    "\n",
-    "<br>\n",
-    "\n",
-    "La cellule suivante importe les données et les sépare en base d'apprentissage et en base de test. Les niveaux de gris sont normalisés entre 0 et 1."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "import numpy as np\n",
-    "import matplotlib.pyplot as plt\n",
-    "import time\n",
-    "from sklearn import datasets, metrics, model_selection\n",
-    "%matplotlib inline \n",
-    "\n",
-    "size_images=(28,28)\n",
-    "X_fashion, y_fashion = datasets.fetch_openml(data_id=40996, return_X_y=True, as_frame=False)\n",
-    "X_fashion=X_fashion/255.  # normalisation des niveaux de gris entre 0 et 1\n",
-    "\n",
-    "for i in range(10):\n",
-    "    n=np.sum(y_fashion==str(i))\n",
-    "    print(\"nombre d'observations dans la classe %d: %d\" %(i,n))\n",
-    "\n",
-    "n_samples = len(X_fashion)\n",
-    "print(\"nombre total d'observations (apprentissage + test): %d\" % n_samples)\n",
-    "\n",
-    "n_features = len(X_fashion[0])\n",
-    "print(\"nombre de caractéristiques par observation: %d\" % n_features)\n",
-    "\n",
-    "X_train, X_test, y_train, y_test = model_selection.train_test_split(X_fashion, y_fashion, test_size=0.2, random_state=1)\n",
-    "\n",
-    "print(\"nombre d'observations dans la base d'apprentissage: %d\" %len(X_train))\n",
-    "print(\"nombre d'observations dans la base de test: %d\" %len(X_test))\n"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "On définit la fonction d'affichage habituelle:"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "def affichage_150_images(X_test,y_test,y_pred):\n",
-    "    plt.figure(figsize=[15,12])   \n",
-    "    for n in range(150):\n",
-    "        plt.subplot(10,15,n+1,xticks=[],yticks=[])\n",
-    "        plt.imshow(np.reshape(X_test[n,:],size_images),cmap='gray_r')\n",
-    "        if y_pred[n]==y_test[n]:\n",
-    "            plt.text(0.1,0.1,str(y_pred[n])+' / '+str(y_test[n]),fontsize=8,bbox=dict(facecolor='white', alpha=1))    \n",
-    "        else:\n",
-    "            plt.text(0.1,0.1,str(y_pred[n])+' / '+str(y_test[n]),fontsize=8,bbox=dict(facecolor='red', alpha=1))    \n",
-    "    plt.suptitle('classe predite / classe réelle')\n",
-    "    plt.show();\n"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "Les CNN de Tensorflow exigent des bases de données sous la forme de tableaux numpy de dimension 4, de taille $(N,li,co,ch)$ où\n",
-    "- $N$ est le nombre d'observations\n",
-    "- $li$ est le nombre de lignes de chaque image\n",
-    "- $co$ est le nombre de colonnes de chaque image\n",
-    "- $ch$ est le nombre de canaux de chaque image (1 ici, car les images sont en noir-et-blanc)\n",
-    "\n",
-    "On reformate donc les tableaux `X_train` et `X_test`. Par ailleurs, `y_train` et `y_test` doivent être de type entier."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "X_train = X_train.reshape((len(X_train),)+size_images+(1,))\n",
-    "X_test = X_test.reshape((len(X_test),)+size_images+(1,))\n",
-    "y_train = y_train.astype(int)\n",
-    "y_test = y_test.astype(int)\n"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "\n",
-    "<br>\n",
-    "\n",
-    "__Travail à faire__:\n",
-    "\n",
-    "en vous inspirant de la première partie de l'exercice 2, construisez un CNN permettant de classifier Fashion-MNIST.\n",
-    "\n",
-    "<br>\n"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "from keras.models import Sequential\n",
-    "from keras.layers import Conv2D, MaxPooling2D\n",
-    "from keras.layers import Activation, Dropout, Flatten, Dense\n",
-    "\n",
-    "model = Sequential() \n",
-    "\n",
-    "# on part de la même architecture que dans l'exercice 2, mais on peut aussi innover:\n",
-    "# n'hésitez pas à m'envoyer vos résultats si vous améliorez le taux de classification sur la base test\n",
-    "# (à peu près 90% de succès ici) en introduisant une autre architecture\n",
-    "\n",
-    "model.add(Conv2D(32, (3, 3), input_shape=(28, 28, 1)))   \n",
-    "model.add(Activation('relu'))\n",
-    "model.add(MaxPooling2D(pool_size=(2, 2)))\n",
-    "\n",
-    "# on obtient de meilleurs résultats sans les deux couches suviantes:\n",
-    "#model.add(Conv2D(32, (3, 3)))\n",
-    "#model.add(Activation('relu'))\n",
-    "#model.add(MaxPooling2D(pool_size=(2, 2)))\n",
-    "\n",
-    "#model.add(Conv2D(64, (3, 3)))\n",
-    "#model.add(Activation('relu'))\n",
-    "#model.add(MaxPooling2D(pool_size=(2, 2)))\n",
-    "\n",
-    "model.add(Flatten())  # this converts our 3D feature maps to 1D feature vectors\n",
-    "model.add(Dense(64))  \n",
-    "model.add(Activation('relu'))\n",
-    "model.add(Dropout(0.2))  # semble meilleur que 0.5\n",
-    "model.add(Dense(10))  \n",
-    "model.add(Activation('softmax'))  \n",
-    "\n",
-    "model.compile(loss='sparse_categorical_crossentropy', metrics=['accuracy'], optimizer='rmsprop')\n",
-    "\n",
-    "model.summary()"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "scrolled": false
-   },
-   "outputs": [],
-   "source": [
-    "model.fit(X_train,y_train,batch_size=16,epochs=5,validation_data=(X_test,y_test))"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "scrolled": false
-   },
-   "outputs": [],
-   "source": [
-    "y_proba = model.predict(X_test)\n",
-    "#print(y_proba) # les proba a posteriori de chaque classe\n",
-    "Y_test_pred=y_proba.argmax(axis=1)\n",
-    "\n",
-    "print(\"classes prédites:\")\n",
-    "print(Y_test_pred)\n",
-    "\n",
-    "\n",
-    "print(\"score de classification: %.3f\" %metrics.accuracy_score(y_test,Y_test_pred))\n",
-    "\n",
-    "print(\"matrice de confusion\")\n",
-    "print(metrics.confusion_matrix(y_test,Y_test_pred))\n",
-    "\n",
-    "affichage_150_images(X_test,y_test,Y_test_pred)\n"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": []
-  }
- ],
- "metadata": {
-  "kernelspec": {
-   "display_name": "Python 3 (ipykernel)",
-   "language": "python",
-   "name": "python3"
-  },
-  "language_info": {
-   "codemirror_mode": {
-    "name": "ipython",
-    "version": 3
-   },
-   "file_extension": ".py",
-   "mimetype": "text/x-python",
-   "name": "python",
-   "nbconvert_exporter": "python",
-   "pygments_lexer": "ipython3",
-   "version": "3.8.12"
-  }
- },
- "nbformat": 4,
- "nbformat_minor": 4
-}