diff --git a/TD/TD7/ML-TD7.ipynb b/TD/TD7/ML-TD7.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..b946c8f85331b20d7613342c9dbad4038a55934f
--- /dev/null
+++ b/TD/TD7/ML-TD7.ipynb
@@ -0,0 +1,735 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "7edf7168",
+   "metadata": {},
+   "source": [
+    "### **_Introduction au Machine Learning - Enise - Centrale Lyon_**\n",
+    "\n",
+    "2024-2025\n",
+    "\n",
+    "Emmanuel Dellandréa\t  \n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "4c69d182",
+   "metadata": {},
+   "source": [
+    "# TD7 – Convolutional Neural Networks"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "fa71eda4",
+   "metadata": {},
+   "source": [
+    "The objective of this tutorial is to use the PyTorch library for building, training, and evaluating CNN models."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "23f266da",
+   "metadata": {},
+   "source": [
+    "## Sequence 1: Training a CNN to classify CIFAR10 images\n",
+    "\n",
+    "The goal is to apply a Convolutional Neural Net (CNN) model on the CIFAR10 image dataset and test the accuracy of the model on the basis of image classification. \n",
+    "\n",
+    "Be sure to check the PyTorch tutorials and documentation when needed:\n",
+    "\n",
+    "https://pytorch.org/tutorials/\n",
+    "\n",
+    "https://pytorch.org/docs/stable/index.html\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "4ba1c82d",
+   "metadata": {},
+   "source": [
+    "You can test if GPU is available on your machine and thus train on it to speed up the process"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "6e18f2fd",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import torch\n",
+    "\n",
+    "# check if CUDA is available\n",
+    "train_on_gpu = torch.cuda.is_available()\n",
+    "\n",
+    "if not train_on_gpu:\n",
+    "    print(\"CUDA is not available.  Training on CPU ...\")\n",
+    "else:\n",
+    "    print(\"CUDA is available!  Training on GPU ...\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "5cf214eb",
+   "metadata": {},
+   "source": [
+    "Next we load the CIFAR10 dataset"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "462666a2",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import numpy as np\n",
+    "from torchvision import datasets, transforms\n",
+    "from torch.utils.data.sampler import SubsetRandomSampler\n",
+    "\n",
+    "# number of subprocesses to use for data loading\n",
+    "num_workers = 0\n",
+    "# how many samples per batch to load\n",
+    "batch_size = 20\n",
+    "# percentage of training set to use as validation\n",
+    "valid_size = 0.2\n",
+    "\n",
+    "# convert data to a normalized torch.FloatTensor\n",
+    "transform = transforms.Compose(\n",
+    "    [transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]\n",
+    ")\n",
+    "\n",
+    "# choose the training and test datasets\n",
+    "train_data = datasets.CIFAR10(\"data\", train=True, download=True, transform=transform)\n",
+    "test_data = datasets.CIFAR10(\"data\", train=False, download=True, transform=transform)\n",
+    "\n",
+    "# obtain training indices that will be used for validation\n",
+    "num_train = len(train_data)\n",
+    "indices = list(range(num_train))\n",
+    "np.random.shuffle(indices)\n",
+    "split = int(np.floor(valid_size * num_train))\n",
+    "train_idx, valid_idx = indices[split:], indices[:split]\n",
+    "\n",
+    "# define samplers for obtaining training and validation batches\n",
+    "train_sampler = SubsetRandomSampler(train_idx)\n",
+    "valid_sampler = SubsetRandomSampler(valid_idx)\n",
+    "\n",
+    "# prepare data loaders (combine dataset and sampler)\n",
+    "train_loader = torch.utils.data.DataLoader(\n",
+    "    train_data, batch_size=batch_size, sampler=train_sampler, num_workers=num_workers\n",
+    ")\n",
+    "valid_loader = torch.utils.data.DataLoader(\n",
+    "    train_data, batch_size=batch_size, sampler=valid_sampler, num_workers=num_workers\n",
+    ")\n",
+    "test_loader = torch.utils.data.DataLoader(\n",
+    "    test_data, batch_size=batch_size, num_workers=num_workers\n",
+    ")\n",
+    "\n",
+    "# specify the image classes\n",
+    "classes = [\n",
+    "    \"airplane\",\n",
+    "    \"automobile\",\n",
+    "    \"bird\",\n",
+    "    \"cat\",\n",
+    "    \"deer\",\n",
+    "    \"dog\",\n",
+    "    \"frog\",\n",
+    "    \"horse\",\n",
+    "    \"ship\",\n",
+    "    \"truck\",\n",
+    "]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "58ec3903",
+   "metadata": {},
+   "source": [
+    "CNN definition (this one is an example)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "317bf070",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import torch.nn as nn\n",
+    "import torch.nn.functional as F\n",
+    "\n",
+    "# define the CNN architecture\n",
+    "\n",
+    "\n",
+    "class Net(nn.Module):\n",
+    "    def __init__(self):\n",
+    "        super(Net, self).__init__()\n",
+    "        self.conv1 = nn.Conv2d(3, 6, 5)\n",
+    "        self.pool = nn.MaxPool2d(2, 2)\n",
+    "        self.conv2 = nn.Conv2d(6, 16, 5)\n",
+    "        self.fc1 = nn.Linear(16 * 5 * 5, 120)\n",
+    "        self.fc2 = nn.Linear(120, 84)\n",
+    "        self.fc3 = nn.Linear(84, 10)\n",
+    "\n",
+    "    def forward(self, x):\n",
+    "        x = self.pool(F.relu(self.conv1(x)))\n",
+    "        x = self.pool(F.relu(self.conv2(x)))\n",
+    "        x = x.view(-1, 16 * 5 * 5)\n",
+    "        x = F.relu(self.fc1(x))\n",
+    "        x = F.relu(self.fc2(x))\n",
+    "        x = self.fc3(x)\n",
+    "        return x\n",
+    "\n",
+    "\n",
+    "# create a complete CNN\n",
+    "model = Net()\n",
+    "print(model)\n",
+    "# move tensors to GPU if CUDA is available\n",
+    "if train_on_gpu:\n",
+    "    model.cuda()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a2dc4974",
+   "metadata": {},
+   "source": [
+    "Loss function and training using SGD (Stochastic Gradient Descent) optimizer"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "4b53f229",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import torch.optim as optim\n",
+    "\n",
+    "criterion = nn.CrossEntropyLoss()  # specify loss function\n",
+    "optimizer = optim.SGD(model.parameters(), lr=0.01)  # specify optimizer\n",
+    "\n",
+    "n_epochs = 30  # number of epochs to train the model\n",
+    "train_loss_list = []  # list to store loss to visualize\n",
+    "valid_loss_min = np.Inf  # track change in validation loss\n",
+    "\n",
+    "for epoch in range(n_epochs):\n",
+    "    # Keep track of training and validation loss\n",
+    "    train_loss = 0.0\n",
+    "    valid_loss = 0.0\n",
+    "\n",
+    "    # Train the model\n",
+    "    model.train()\n",
+    "    for data, target in train_loader:\n",
+    "        # Move tensors to GPU if CUDA is available\n",
+    "        if train_on_gpu:\n",
+    "            data, target = data.cuda(), target.cuda()\n",
+    "        # Clear the gradients of all optimized variables\n",
+    "        optimizer.zero_grad()\n",
+    "        # Forward pass: compute predicted outputs by passing inputs to the model\n",
+    "        output = model(data)\n",
+    "        # Calculate the batch loss\n",
+    "        loss = criterion(output, target)\n",
+    "        # Backward pass: compute gradient of the loss with respect to model parameters\n",
+    "        loss.backward()\n",
+    "        # Perform a single optimization step (parameter update)\n",
+    "        optimizer.step()\n",
+    "        # Update training loss\n",
+    "        train_loss += loss.item() * data.size(0)\n",
+    "\n",
+    "    # Validate the model\n",
+    "    model.eval()\n",
+    "    for data, target in valid_loader:\n",
+    "        # Move tensors to GPU if CUDA is available\n",
+    "        if train_on_gpu:\n",
+    "            data, target = data.cuda(), target.cuda()\n",
+    "        # Forward pass: compute predicted outputs by passing inputs to the model\n",
+    "        output = model(data)\n",
+    "        # Calculate the batch loss\n",
+    "        loss = criterion(output, target)\n",
+    "        # Update average validation loss\n",
+    "        valid_loss += loss.item() * data.size(0)\n",
+    "\n",
+    "    # Calculate average losses\n",
+    "    train_loss = train_loss / len(train_loader)\n",
+    "    valid_loss = valid_loss / len(valid_loader)\n",
+    "    train_loss_list.append(train_loss)\n",
+    "\n",
+    "    # Print training/validation statistics\n",
+    "    print(\n",
+    "        \"Epoch: {} \\tTraining Loss: {:.6f} \\tValidation Loss: {:.6f}\".format(\n",
+    "            epoch, train_loss, valid_loss\n",
+    "        )\n",
+    "    )\n",
+    "\n",
+    "    # Save model if validation loss has decreased\n",
+    "    if valid_loss <= valid_loss_min:\n",
+    "        print(\n",
+    "            \"Validation loss decreased ({:.6f} --> {:.6f}).  Saving model ...\".format(\n",
+    "                valid_loss_min, valid_loss\n",
+    "            )\n",
+    "        )\n",
+    "        torch.save(model.state_dict(), \"model_cifar.pt\")\n",
+    "        valid_loss_min = valid_loss"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "13e1df74",
+   "metadata": {},
+   "source": [
+    "Does overfit occur? If so, do an early stopping."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "d39df818",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import matplotlib.pyplot as plt\n",
+    "\n",
+    "plt.plot(range((len(train_loss_list))), train_loss_list)\n",
+    "plt.xlabel(\"Epoch\")\n",
+    "plt.ylabel(\"Loss\")\n",
+    "plt.title(\"Performance of Model 1\")\n",
+    "plt.show()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "11df8fd4",
+   "metadata": {},
+   "source": [
+    "Now loading the model with the lowest validation loss value\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "e93efdfc",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "model.load_state_dict(torch.load(\"./model_cifar.pt\"))\n",
+    "\n",
+    "# track test loss\n",
+    "test_loss = 0.0\n",
+    "class_correct = list(0.0 for i in range(10))\n",
+    "class_total = list(0.0 for i in range(10))\n",
+    "\n",
+    "model.eval()\n",
+    "# iterate over test data\n",
+    "for data, target in test_loader:\n",
+    "    # move tensors to GPU if CUDA is available\n",
+    "    if train_on_gpu:\n",
+    "        data, target = data.cuda(), target.cuda()\n",
+    "    # forward pass: compute predicted outputs by passing inputs to the model\n",
+    "    output = model(data)\n",
+    "    # calculate the batch loss\n",
+    "    loss = criterion(output, target)\n",
+    "    # update test loss\n",
+    "    test_loss += loss.item() * data.size(0)\n",
+    "    # convert output probabilities to predicted class\n",
+    "    _, pred = torch.max(output, 1)\n",
+    "    # compare predictions to true label\n",
+    "    correct_tensor = pred.eq(target.data.view_as(pred))\n",
+    "    correct = (\n",
+    "        np.squeeze(correct_tensor.numpy())\n",
+    "        if not train_on_gpu\n",
+    "        else np.squeeze(correct_tensor.cpu().numpy())\n",
+    "    )\n",
+    "    # calculate test accuracy for each object class\n",
+    "    for i in range(batch_size):\n",
+    "        label = target.data[i]\n",
+    "        class_correct[label] += correct[i].item()\n",
+    "        class_total[label] += 1\n",
+    "\n",
+    "# average test loss\n",
+    "test_loss = test_loss / len(test_loader)\n",
+    "print(\"Test Loss: {:.6f}\\n\".format(test_loss))\n",
+    "\n",
+    "for i in range(10):\n",
+    "    if class_total[i] > 0:\n",
+    "        print(\n",
+    "            \"Test Accuracy of %5s: %2d%% (%2d/%2d)\"\n",
+    "            % (\n",
+    "                classes[i],\n",
+    "                100 * class_correct[i] / class_total[i],\n",
+    "                np.sum(class_correct[i]),\n",
+    "                np.sum(class_total[i]),\n",
+    "            )\n",
+    "        )\n",
+    "    else:\n",
+    "        print(\"Test Accuracy of %5s: N/A (no training examples)\" % (classes[i]))\n",
+    "\n",
+    "print(\n",
+    "    \"\\nTest Accuracy (Overall): %2d%% (%2d/%2d)\"\n",
+    "    % (\n",
+    "        100.0 * np.sum(class_correct) / np.sum(class_total),\n",
+    "        np.sum(class_correct),\n",
+    "        np.sum(class_total),\n",
+    "    )\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "944991a2",
+   "metadata": {},
+   "source": [
+    "### Experiments:\n",
+    "\n",
+    "Build a new network with the following structure.\n",
+    "\n",
+    "- It has 3 convolutional layers of kernel size 3 and padding of 1.\n",
+    "- The first convolutional layer must output 16 channels, the second 32 and the third 64.\n",
+    "- At each convolutional layer output, we apply a ReLU activation then a MaxPool with kernel size of 2.\n",
+    "- Then, three fully connected layers, the first two being followed by a ReLU activation.\n",
+    "- The first fully connected layer will have an output size of 512.\n",
+    "- The second fully connected layer will have an output size of 64.\n",
+    "\n",
+    "Compare the results obtained with this new network to those obtained previously."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "201470f9",
+   "metadata": {},
+   "source": [
+    "## Sequence 2: Working with pre-trained models.\n",
+    "\n",
+    "PyTorch offers several pre-trained models https://pytorch.org/vision/0.8/models.html        \n",
+    "We will use ResNet50 trained on ImageNet dataset (https://www.image-net.org/index.php). Use the following code with the files `imagenet-simple-labels.json` that contains the imagenet labels and the image dog.png that we will use as test.\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "b4d13080",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import json\n",
+    "from PIL import Image\n",
+    "from torchvision import models\n",
+    "\n",
+    "# Choose an image to pass through the model\n",
+    "test_image = \"dog.png\"\n",
+    "\n",
+    "# Configure matplotlib for pretty inline plots\n",
+    "#%matplotlib inline\n",
+    "#%config InlineBackend.figure_format = 'retina'\n",
+    "\n",
+    "# Prepare the labels\n",
+    "with open(\"imagenet-simple-labels.json\") as f:\n",
+    "    labels = json.load(f)\n",
+    "\n",
+    "# First prepare the transformations: resize the image to what the model was trained on and convert it to a tensor\n",
+    "data_transform = transforms.Compose(\n",
+    "    [\n",
+    "        transforms.Resize((224, 224)),\n",
+    "        transforms.ToTensor(),\n",
+    "        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),\n",
+    "    ]\n",
+    ")\n",
+    "# Load the image\n",
+    "\n",
+    "image = Image.open(test_image)\n",
+    "plt.imshow(image), plt.xticks([]), plt.yticks([])\n",
+    "\n",
+    "# Now apply the transformation, expand the batch dimension, and send the image to the GPU\n",
+    "# image = data_transform(image).unsqueeze(0).cuda()\n",
+    "image = data_transform(image).unsqueeze(0)\n",
+    "\n",
+    "# Download the model if it's not there already. It will take a bit on the first run, after that it's fast\n",
+    "model = models.resnet50(pretrained=True)\n",
+    "# Send the model to the GPU\n",
+    "# model.cuda()\n",
+    "# Set layers such as dropout and batchnorm in evaluation mode\n",
+    "model.eval()\n",
+    "\n",
+    "# Get the 1000-dimensional model output\n",
+    "out = model(image)\n",
+    "# Find the predicted class\n",
+    "print(\"Predicted class is: {}\".format(labels[out.argmax()]))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "184cfceb",
+   "metadata": {},
+   "source": [
+    "### Experiments:\n",
+    "\n",
+    "Study the code and the results obtained. Possibly add other images downloaded from the internet.\n",
+    "\n",
+    "Experiment with other pre-trained CNN models.\n",
+    "\n",
+    "    \n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "5d57da4b",
+   "metadata": {},
+   "source": [
+    "## Sequence 3: Transfer Learning\n",
+    "    \n",
+    "    \n",
+    "For this work, we will use a pre-trained model (ResNet18) as a descriptor extractor and will refine the classification by training only the last fully connected layer of the network. Thus, the output layer of the pre-trained network will be replaced by a layer adapted to the new classes to be recognized which will be in our case ants and bees.\n",
+    "Download and unzip in your working directory the dataset available at the address :\n",
+    "    \n",
+    "https://download.pytorch.org/tutorial/hymenoptera_data.zip\n",
+    "    \n",
+    "Execute the following code in order to display some images of the dataset."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "be2d31f5",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import os\n",
+    "\n",
+    "import matplotlib.pyplot as plt\n",
+    "import numpy as np\n",
+    "import torch\n",
+    "import torchvision\n",
+    "from torchvision import datasets, transforms\n",
+    "import torch.nn as nn\n",
+    "import torch.optim as optim\n",
+    "from torch.optim import lr_scheduler\n",
+    "\n",
+    "# Data augmentation and normalization for training\n",
+    "# Just normalization for validation\n",
+    "data_transforms = {\n",
+    "    \"train\": transforms.Compose(\n",
+    "        [\n",
+    "            transforms.RandomResizedCrop(\n",
+    "                224\n",
+    "            ),  # ImageNet models were trained on 224x224 images\n",
+    "            transforms.RandomHorizontalFlip(),  # flip horizontally 50% of the time - increases train set variability\n",
+    "            transforms.ToTensor(),  # convert it to a PyTorch tensor\n",
+    "            transforms.Normalize(\n",
+    "                [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]\n",
+    "            ),  # ImageNet models expect this norm\n",
+    "        ]\n",
+    "    ),\n",
+    "    \"val\": transforms.Compose(\n",
+    "        [\n",
+    "            transforms.Resize(256),\n",
+    "            transforms.CenterCrop(224),\n",
+    "            transforms.ToTensor(),\n",
+    "            transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),\n",
+    "        ]\n",
+    "    ),\n",
+    "}\n",
+    "\n",
+    "data_dir = \"hymenoptera_data\"\n",
+    "# Create train and validation datasets and loaders\n",
+    "image_datasets = {\n",
+    "    x: datasets.ImageFolder(os.path.join(data_dir, x), data_transforms[x])\n",
+    "    for x in [\"train\", \"val\"]\n",
+    "}\n",
+    "dataloaders = {\n",
+    "    x: torch.utils.data.DataLoader(\n",
+    "        image_datasets[x], batch_size=4, shuffle=True, num_workers=0\n",
+    "    )\n",
+    "    for x in [\"train\", \"val\"]\n",
+    "}\n",
+    "dataset_sizes = {x: len(image_datasets[x]) for x in [\"train\", \"val\"]}\n",
+    "class_names = image_datasets[\"train\"].classes\n",
+    "device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n",
+    "\n",
+    "# Helper function for displaying images\n",
+    "def imshow(inp, title=None):\n",
+    "    \"\"\"Imshow for Tensor.\"\"\"\n",
+    "    inp = inp.numpy().transpose((1, 2, 0))\n",
+    "    mean = np.array([0.485, 0.456, 0.406])\n",
+    "    std = np.array([0.229, 0.224, 0.225])\n",
+    "\n",
+    "    # Un-normalize the images\n",
+    "    inp = std * inp + mean\n",
+    "    # Clip just in case\n",
+    "    inp = np.clip(inp, 0, 1)\n",
+    "    plt.imshow(inp)\n",
+    "    if title is not None:\n",
+    "        plt.title(title)\n",
+    "    plt.pause(0.001)  # pause a bit so that plots are updated\n",
+    "    plt.show()\n",
+    "\n",
+    "\n",
+    "# Get a batch of training data\n",
+    "inputs, classes = next(iter(dataloaders[\"train\"]))\n",
+    "\n",
+    "# Make a grid from batch\n",
+    "out = torchvision.utils.make_grid(inputs)\n",
+    "\n",
+    "imshow(out, title=[class_names[x] for x in classes])\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "bbd48800",
+   "metadata": {},
+   "source": [
+    "Now, execute the following code which uses a pre-trained model ResNet18 having replaced the output layer for the ants/bees classification and performs the model training by only changing the weights of this output layer."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "572d824c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import copy\n",
+    "import time\n",
+    "\n",
+    "\n",
+    "def train_model(model, criterion, optimizer, scheduler, num_epochs=25):\n",
+    "    since = time.time()\n",
+    "\n",
+    "    best_model_wts = copy.deepcopy(model.state_dict())\n",
+    "    best_acc = 0.0\n",
+    "\n",
+    "    epoch_time = []  # we'll keep track of the time needed for each epoch\n",
+    "\n",
+    "    for epoch in range(num_epochs):\n",
+    "        epoch_start = time.time()\n",
+    "        print(\"Epoch {}/{}\".format(epoch + 1, num_epochs))\n",
+    "        print(\"-\" * 10)\n",
+    "\n",
+    "        # Each epoch has a training and validation phase\n",
+    "        for phase in [\"train\", \"val\"]:\n",
+    "            if phase == \"train\":\n",
+    "                scheduler.step()\n",
+    "                model.train()  # Set model to training mode\n",
+    "            else:\n",
+    "                model.eval()  # Set model to evaluate mode\n",
+    "\n",
+    "            running_loss = 0.0\n",
+    "            running_corrects = 0\n",
+    "\n",
+    "            # Iterate over data.\n",
+    "            for inputs, labels in dataloaders[phase]:\n",
+    "                inputs = inputs.to(device)\n",
+    "                labels = labels.to(device)\n",
+    "\n",
+    "                # zero the parameter gradients\n",
+    "                optimizer.zero_grad()\n",
+    "\n",
+    "                # Forward\n",
+    "                # Track history if only in training phase\n",
+    "                with torch.set_grad_enabled(phase == \"train\"):\n",
+    "                    outputs = model(inputs)\n",
+    "                    _, preds = torch.max(outputs, 1)\n",
+    "                    loss = criterion(outputs, labels)\n",
+    "\n",
+    "                    # backward + optimize only if in training phase\n",
+    "                    if phase == \"train\":\n",
+    "                        loss.backward()\n",
+    "                        optimizer.step()\n",
+    "\n",
+    "                # Statistics\n",
+    "                running_loss += loss.item() * inputs.size(0)\n",
+    "                running_corrects += torch.sum(preds == labels.data)\n",
+    "\n",
+    "            epoch_loss = running_loss / dataset_sizes[phase]\n",
+    "            epoch_acc = running_corrects.double() / dataset_sizes[phase]\n",
+    "\n",
+    "            print(\"{} Loss: {:.4f} Acc: {:.4f}\".format(phase, epoch_loss, epoch_acc))\n",
+    "\n",
+    "            # Deep copy the model\n",
+    "            if phase == \"val\" and epoch_acc > best_acc:\n",
+    "                best_acc = epoch_acc\n",
+    "                best_model_wts = copy.deepcopy(model.state_dict())\n",
+    "\n",
+    "        # Add the epoch time\n",
+    "        t_epoch = time.time() - epoch_start\n",
+    "        epoch_time.append(t_epoch)\n",
+    "        print()\n",
+    "\n",
+    "    time_elapsed = time.time() - since\n",
+    "    print(\n",
+    "        \"Training complete in {:.0f}m {:.0f}s\".format(\n",
+    "            time_elapsed // 60, time_elapsed % 60\n",
+    "        )\n",
+    "    )\n",
+    "    print(\"Best val Acc: {:4f}\".format(best_acc))\n",
+    "\n",
+    "    # Load best model weights\n",
+    "    model.load_state_dict(best_model_wts)\n",
+    "    return model, epoch_time\n",
+    "\n",
+    "\n",
+    "# Download a pre-trained ResNet18 model and freeze its weights\n",
+    "model = torchvision.models.resnet18(pretrained=True)\n",
+    "for param in model.parameters():\n",
+    "    param.requires_grad = False\n",
+    "\n",
+    "# Replace the final fully connected layer\n",
+    "# Parameters of newly constructed modules have requires_grad=True by default\n",
+    "num_ftrs = model.fc.in_features\n",
+    "model.fc = nn.Linear(num_ftrs, 2)\n",
+    "# Send the model to the GPU\n",
+    "model = model.to(device)\n",
+    "# Set the loss function\n",
+    "criterion = nn.CrossEntropyLoss()\n",
+    "\n",
+    "# Observe that only the parameters of the final layer are being optimized\n",
+    "optimizer_conv = optim.SGD(model.fc.parameters(), lr=0.001, momentum=0.9)\n",
+    "exp_lr_scheduler = lr_scheduler.StepLR(optimizer_conv, step_size=7, gamma=0.1)\n",
+    "model, epoch_time = train_model(\n",
+    "    model, criterion, optimizer_conv, exp_lr_scheduler, num_epochs=10\n",
+    ")\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "bbd48800",
+   "metadata": {},
+   "source": [
+    "### Experiments:\n",
+    "\n",
+    "Study the code and the results obtained.\n",
+    "\n",
+    "Modify the code and add an \"eval_model\" function to allow\n",
+    "the evaluation of the model on a test set (different from the learning and validation sets used during the learning phase). Study the results obtained.\n",
+    "\n",
+    "Now modify the code to replace the current classification layer with a set of two layers using a \"relu\" activation function for the middle layer. Renew the experiments and study the results obtained.\n",
+    "\n",
+    "Experiment with other models and datasets."
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "base",
+   "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.9.20"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}