{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "a4b9b9f7",
   "metadata": {},
   "source": [
    "# XI.2 - Travaux pratiques\n",
    "\n",
    "## Fonction d'activation ReLU\n",
    "\n",
    "*Pour approfondir la séance précédente, si vous avez le temps.*\n",
    "\n",
    "Dans un premier temps, nous pouvons comparer la fonction d'activation\n",
    "sigmoide utilisée dans les TP précédents et la fonction d'activation\n",
    "ReLU (*Rectifed Linear Unit*, `nn.ReLU()` sous PyTorch)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "72ef9511",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "-  Reprendre le perceptron à une couche cachée du TP précédent en\n",
    "   utilisant une non-linéarité ReLU. Observer le nombre d'itérations\n",
    "   nécessaires pour atteindre la convergence du modèle.\n",
    "-  Reprendre le réseau convolutif du TP précédent en utilisant des\n",
    "   non-linéarités ReLU pour les couches convolutives. Observer le nombre\n",
    "   d'itérations nécessaires pour atteindre la convergence du modèle.\n",
    "\n",
    "Conclure sur l'intérêt de la fonction d'activation ReLU.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fe649c2f",
   "metadata": {},
   "source": [
    "## *Transfer Learning* et *fine-tuning* sur EuroSAT\n",
    "\n",
    "Pour aller plus loin, nous allons nous intéresser aux propriétés de «\n",
    "transfert » des réseaux convolutifs profonds pré-entraînés sur des bases\n",
    "large échelle comme ImageNet. Nous allons nous intéresser au jeu de\n",
    "données [EuroSAT](https://github.com/phelber/EuroSAT), qui est une\n",
    "base d'images contenant :\n",
    "\n",
    "- 10 classes (bâtiments industriels, bâtiments\n",
    "  résidentiels, cultures saisonnières, cultures permanentes, rivières,\n",
    "  lacs & mers, végétation herbacée, routes/autoroutes, pâturages et\n",
    "  forêts).\n",
    "- Environ 27000 images au total. Il\n",
    "  n'y a pas d'ensemble de test prédéfini, nous allons donc la diviser en\n",
    "  deux.\n",
    "- Les images sont de taille $64\\times64$. Les images que\n",
    "  nous allons utiliser sont en couleur (RGB) mais il existe une version\n",
    "  multispectrale utilisant 13 longueurs d'onde différentes.\n",
    "\n",
    "Ce jeu de données est géré nativement par `torchvision` via la classe `torchvision.datasets.EuroSAT`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e32610eb",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "from torchvision.datasets import EuroSAT\n",
    "from torchvision.models import resnet50, ResNet50_Weights\n",
    "\n",
    "# Graine aléatoire, pour la reproductibilité (tirage des exemples visualisés,\n",
    "# découpage train/test, initialisation des couches réentraînées, ordre des batches)\n",
    "torch.manual_seed(42)\n",
    "\n",
    "# weights.transforms() renvoie le pré-traitement (redimensionnement, normalisation)\n",
    "# attendu par le modèle pré-entraîné que nous utiliserons plus bas\n",
    "weights = ResNet50_Weights.DEFAULT\n",
    "preprocess = weights.transforms()\n",
    "\n",
    "dataset = EuroSAT(root='data', download=True)\n",
    "print(f\"{len(dataset)} images, {len(dataset.classes)} classes : {dataset.classes}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ac692e32",
   "metadata": {},
   "source": [
    "Nous pouvons commencer par visualiser quelques images de ce jeu de\n",
    "données. Si c'est la première fois que vous exécutez cette cellule de\n",
    "code, le téléchargement du jeu de données EuroSAT peut prendre quelques\n",
    "minutes (≈90Mo)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "aa8d1b75",
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import torch\n",
    "\n",
    "indices = torch.randperm(len(dataset))[:10]\n",
    "\n",
    "fig = plt.figure(figsize=(18, 6))\n",
    "for i, idx in enumerate(indices):\n",
    "    image, label = dataset[idx]\n",
    "    fig.add_subplot(1, 10, i + 1)\n",
    "    plt.imshow(image)\n",
    "    plt.axis(\"off\")\n",
    "    plt.title(dataset.classes[label])\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "29a9ca8b",
   "metadata": {},
   "source": [
    "Avec des « petits » volumes de données (de l'ordre de quelques milliers\n",
    "d'images), il est impossible d'entraîner des réseaux de neurones avec\n",
    "autant de paramètres que ceux utilisés pour ImageNet sans devoir se\n",
    "confronter au problème du sur-apprentissage. Toutefois, une solution\n",
    "généralement assez efficace consiste à appliquer de l'apprentissage par\n",
    "transfert. C'est une approche commune et peu coûteuse, qui permet\n",
    "d'exploiter des modèles « sur étagère » sans les ré-entraîner en les\n",
    "utilisant comme extracteur de caractéristiques.\n",
    "\n",
    "### Chargement du modèle ResNet-50 avec PyTorch\n",
    "\n",
    "Pour cette séance, nous allons nous appuyer sur une architecture de\n",
    "réseau convolutif qui obtient des performances plus que satisfaisantes\n",
    "sur ImageNet : les réseaux de la famille des ResNet. La compétition\n",
    "ILSVRC en 2015 (classification d'images naturelles sur les 1000 classes\n",
    "de ImageNet) a été remportée par un modèle type ResNet.\n",
    "\n",
    "Nous allons repartir d'un modèle ResNet-50 dont l'architecture détaillée\n",
    "peut être trouvée dans le [code source de torchvision](https://github.com/pytorch/vision/blob/main/torchvision/models/resnet.py).\n",
    "Comme il s'agit d'un modèle assez répandu, il est directement implémenté\n",
    "dans `torchvision.models` et il n'est pas nécessaire de le redéfinir :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "15eed29d",
   "metadata": {},
   "outputs": [],
   "source": [
    "model = resnet50(weights=weights)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "172b5abf",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "À partir de la [documentation de torchvision](https://pytorch.org/vision/stable/models/generated/torchvision.models.resnet50.html), déterminer l'utilité du paramètre `weights`.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2c487dc9",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(model)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5a2fb5e3",
   "metadata": {},
   "source": [
    "### Extraction de *deep features*\n",
    "\n",
    "Une première solution pour surmonter le manque d'exemples\n",
    "d'apprentissage est de se servir des réseaux pré-entraînés sur ImageNet\n",
    "pour extraire des descripteurs d'image. En effet, la représentation\n",
    "apprise par les dernières couches du réseau contiennent normalement\n",
    "l'information utile à leur classification. Cette connaissance devrait\n",
    "pouvoir se transférer sur un autre jeu de données.\n",
    "\n",
    "Nous allons ainsi appliquer le réseau ResNet50 et extraire, pour chaque\n",
    "image de notre nouveau jeu de donnée, les activations de\n",
    "l'avant-dernière couche, c'est-à-dire celle se trouvant juste avant la\n",
    "couche linéaire `model.fc` qui réalise la classification dans les 1000 classes\n",
    "d'ImageNet."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "09c78a53",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "D'après l'affichage de `model` ci-dessus, quelle est la couche qui précède `model.fc` ? Quelle sera la taille du vecteur de descripteurs qu'elle produit ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d11027b2",
   "metadata": {},
   "source": [
    "L'application du réseau sur chaque image de la base produira donc un\n",
    "vecteur de taille $d=2048$, que l'on appelle aussi *deep feature* («\n",
    "caractéristique profonde »). Le modèle que nous allons utiliser est donc\n",
    "notre ResNet50 mais pour lequel nous supprimons la dernière couche. Sous PyTorch, la façon la plus\n",
    "simple de faire cela est de remplacer la couche `fc` par une couche « identité », qui ne fait rien\n",
    "d'autre que renvoyer son entrée telle quelle :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f1ad34dc",
   "metadata": {},
   "outputs": [],
   "source": [
    "from torch import nn\n",
    "\n",
    "model.fc = nn.Identity()\n",
    "model.eval()  # on n'utilise le modèle qu'en inférence pour cette partie"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4f8f60d0",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Pourquoi appelle-t-on `model.eval()` ici, plutôt que `model.train()` ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1b7346af",
   "metadata": {},
   "source": [
    "Ensuite, nous pouvons créer les deux jeux de données d'entraînement et\n",
    "d'évaluation de EuroSAT. Comme il n'y a pas de *split* fourni avec le\n",
    "jeu de données, nous allons en créer un, en utilisant `torch.utils.data.random_split`\n",
    "(un découpage aléatoire, contrairement à un découpage par indice, évite que le jeu d'apprentissage\n",
    "ou de test ne se retrouve avec uniquement certaines classes, celles-ci étant rangées par dossier dans\n",
    "le jeu de données d'origine) :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "27a01234",
   "metadata": {},
   "outputs": [],
   "source": [
    "from torch.utils.data import DataLoader, Subset, random_split\n",
    "\n",
    "dataset = EuroSAT(root='data', transform=preprocess)\n",
    "\n",
    "generator = torch.Generator().manual_seed(0)\n",
    "n_train = len(dataset) // 2\n",
    "n_test = len(dataset) - n_train\n",
    "train_dataset, test_dataset = random_split(dataset, [n_train, n_test], generator=generator)\n",
    "\n",
    "# On se limite à 640 exemples de chaque côté (comme 20 batches de 32), pour\n",
    "# limiter le temps de calcul sur cette séance\n",
    "batch_size = 32\n",
    "train_dataset = Subset(train_dataset, range(20 * batch_size))\n",
    "test_dataset = Subset(test_dataset, range(20 * batch_size))\n",
    "\n",
    "train_loader = DataLoader(train_dataset, batch_size=batch_size)\n",
    "test_loader = DataLoader(test_dataset, batch_size=batch_size)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "319d8fbe",
   "metadata": {},
   "source": [
    "Remarquons que nous ne conservons que 640 exemples de chaque côté\n",
    "(apprentissage et évaluation). Le jeu de\n",
    "données EuroSAT contient bien plus d'images mais cet échantillon sera\n",
    "suffisant pour notre séance de TP. Par ailleurs, cela va nous permettre\n",
    "de mesurer l'intérêt du *transfer learning* lorsque l'on n'a accès qu'à\n",
    "peu de données."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "22292ed2",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Compléter le code ci-dessous et itérer sur `train_loader` pour extraire séquentiellement les\n",
    "*deep features* sur le jeu de données. En itérant, le *DataLoader* renvoie à chaque itération un\n",
    "couple `(images, labels)` correspondant à un batch.\n",
    "\n",
    "On pensera à englober le calcul dans un bloc `with torch.no_grad():`, pour éviter de calculer\n",
    "inutilement les gradients (nous n'entraînons pas le modèle dans cette partie).\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3d2e6e08",
   "metadata": {},
   "outputs": [],
   "source": [
    "from tqdm.auto import tqdm\n",
    "\n",
    "d_size = 2048\n",
    "X_train = torch.zeros(len(train_dataset), d_size)\n",
    "y_train = torch.zeros(len(train_dataset), dtype=torch.long)\n",
    "\n",
    "# À compléter\n",
    "..."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7729f59b",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Faire cette même opération sur `test_loader`.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5f666ec2",
   "metadata": {},
   "source": [
    "Le temps d'extraction des caractéristiques peut être relativement long\n",
    "sur CPU (plusieurs minutes pour traiter l'intégralité du jeu de données\n",
    "par batch de 32 images). L'utilisation d'un GPU accélère\n",
    "considérablement le calcul. Pour éviter de recalculer les *deep\n",
    "features*, nous allons les sauvegarder à l'aide de PyTorch :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e0d32b90",
   "metadata": {},
   "outputs": [],
   "source": [
    "torch.save(\n",
    "    {\"X_train\": X_train, \"y_train\": y_train, \"X_test\": X_test, \"y_test\": y_test},\n",
    "    \"deepfeatures_resnet50_eurosat.pt\",\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ce83b492",
   "metadata": {},
   "source": [
    "### Transfert de ImageNet vers EuroSAT\n",
    "\n",
    "Vous pouvez recharger les *deep features* sauvegardées à l'étape précédente de la façon suivante :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "abf11e4f",
   "metadata": {},
   "outputs": [],
   "source": [
    "features = torch.load(\"deepfeatures_resnet50_eurosat.pt\")\n",
    "X_train = features[\"X_train\"]\n",
    "y_train = features[\"y_train\"]\n",
    "X_test = features[\"X_test\"]\n",
    "y_test = features[\"y_test\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8cf66395",
   "metadata": {},
   "source": [
    "Dans tous les cas, nous allons définir notre nouveau jeu de données qui\n",
    "prend comme observations les *deep features* calculées précédemment, et\n",
    "comme étiquettes celles du jeu de données EuroSAT.\n",
    "\n",
    "Dans cette partie, nous allons considérer les *deep features* comme les\n",
    "données d'entrée."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "803cde37",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "En utilisant `scikit-learn`, réaliser une visualisation t-SNE des\n",
    "*deep features* ainsi obtenues. On réalisera une projection dans\n",
    "un espace de dimension 2, puis on affichera les *features* projetées\n",
    "en les colorant en fonction de leur classe.\n",
    "\n",
    "Qu'observe-t-on? Que peut-on dire des représentations intermédiaires\n",
    "des images ainsi obtenues, par rapport aux images de départ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "87ecd361",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "À l'aide de `scikit-learn` toujours, entraîner une SVM linéaire\n",
    "simple qui va prédire les étiquettes de classes à partir des *deep\n",
    "features*. On apprendra la SVM sur le jeu de test `(X_train, y_train)`.\n",
    "\n",
    "Évaluer le taux de bonne classification (*accuracy*) de ce classifieur\n",
    "SVM sur le jeu de test `(X_test, y_test)` à l'aide de la méthode\n",
    "`.score()`.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2da8a7e3",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.svm import SVC\n",
    "\n",
    "# À compléter\n",
    "..."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "321da720",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "*pour approfondir, si vous avez le temps*\n",
    "\n",
    "Compléter le code ci-dessous et implémenter un réseau simple\n",
    "(percepton à une seule couche) avec PyTorch. Entraîner ce modèle et\n",
    "évaluer ses performances en classification sur EuroSAT à partir des\n",
    "descripteurs extraits ci-dessus. Que constatez-vous ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a0b362b6",
   "metadata": {},
   "outputs": [],
   "source": [
    "from torch.utils.data import TensorDataset, DataLoader\n",
    "\n",
    "features_train_loader = DataLoader(TensorDataset(X_train, y_train), batch_size=batch_size, shuffle=True)\n",
    "\n",
    "# À compléter en définissant et en entraînant un modèle de perceptron à une couche"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b6cbb0e6",
   "metadata": {},
   "source": [
    "## Fine-tuning\n",
    "\n",
    "Pour terminer ce TP, nous allons non plus simplement utiliser le modèle\n",
    "ResNet-50 préentraîné comme extracteur de caractéristiques « sur étagère\n",
    "», mais nous allons le spécialiser sur le jeu de données EuroSAT. Plus\n",
    "précisément, nous allons entraîner un ResNet-50 dont les paramètres\n",
    "seront initialisées à partir des poids appris sur la base ImageNet, puis\n",
    "nous allons entraîner pendant quelques itérations ce modèle sur EuroSAT\n",
    "pour spécialiser ses représentations internes sur la nouvelle base de\n",
    "données. Cela devrait notamment améliorer encore plus ses performances.\n",
    "\n",
    "<div class=\"admonition note\">\n",
    "\n",
    "**Note**\n",
    "\n",
    "L'exécution des apprentissages sur une carte graphique (GPU) est fortement recommandée pour cette partie.\n",
    "\n",
    "</div>\n",
    "\n",
    "Commençons par recharger le modèle depuis torchvision en l'initialisant à partir\n",
    "des poids appris sur ImageNet :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fa9cb55a",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Rechargement du modèle ResNet50 préentraîné sur ImageNet\n",
    "model = resnet50(weights=weights)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a8921635",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Retirer la dernière couche du modèle (la couche entièrement connectée `fc` qui renvoie les prédictions finales) et la remplacer par une nouvelle couche de taille adaptée. On remarquera que le modèle pré-entraîné renvoie 1000 probabilités (une pour chaque classe de ImageNet), tandis que le modèle que l'on souhaite *fine-tune* doit avoir 10 classes en sortie (10 classes dans EuroSAT). On utilisera `model.fc.in_features` pour récupérer la taille d'entrée de l'ancienne couche `fc`, sans avoir à l'écrire en dur.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "800672fa",
   "metadata": {},
   "source": [
    "Par défaut, tous les paramètres d'un modèle fraîchement créé (ou rechargé) sous PyTorch sont *apprenables*\n",
    "(`requires_grad=True`) : nous allons donc, sans rien avoir de plus à faire, entraîner l'intégralité du\n",
    "réseau, et pas seulement la couche `fc` que l'on vient d'ajouter."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "62a36673",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Si nous avions au contraire fixé `param.requires_grad = False` pour les paramètres de toutes les couches sauf `model.fc`, dans quel mode d'apprentissage serions-nous ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f4a8ce74",
   "metadata": {},
   "source": [
    "Nous pouvons finalement définir la fonction de coût et l'optimiseur comme d'habitude :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e85ae23a",
   "metadata": {},
   "outputs": [],
   "source": [
    "criterion = nn.CrossEntropyLoss()\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "27d67a2b",
   "metadata": {},
   "source": [
    "Comme tout le jeu de données EuroSAT risque de ne pas rentrer en\n",
    "mémoire, il est préférable de charger les images à la volée plutôt que de toutes les garder en mémoire.\n",
    "C'est exactement le rôle du couple `Dataset`/`DataLoader` que nous utilisons depuis le début de la\n",
    "séance : `EuroSAT(root='data', transform=preprocess)` ne charge une image depuis le disque, la\n",
    "redimensionne et la normalise que lorsqu'elle est effectivement demandée par le `DataLoader`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "52e71781",
   "metadata": {},
   "outputs": [],
   "source": [
    "train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d418a0f",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Que fait l'argument `transform=preprocess` passé à `EuroSAT` au tout début de la séance ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fa63afdd",
   "metadata": {},
   "source": [
    "Finalement, on peut démarrer le *fine-tuning* en écrivant la boucle d'apprentissage habituelle."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d0bf72be",
   "metadata": {},
   "outputs": [],
   "source": [
    "n_epochs = 10\n",
    "\n",
    "for epoch in range(n_epochs):\n",
    "    for images, labels in train_loader:\n",
    "        outputs = model(images)\n",
    "        loss = criterion(outputs, labels)\n",
    "        optimizer.zero_grad()\n",
    "        loss.backward()\n",
    "        optimizer.step()\n",
    "    print(f\"Epoch {epoch}: loss = {loss.item():.4f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e063f8db",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Évaluer les performances finales du modèle sur le jeu de test de EuroSAT (`test_loader`). Commenter par rapport à l'approche précédente de *transfer learning*.\n",
    "\n",
    "</div>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
