{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "8c2bf2b8",
   "metadata": {},
   "source": [
    "# XII.2 - Travaux pratiques - Prédiction de séries temporelles\n",
    "\n",
    "L'objectif de cette séance de travaux pratiques est de mettre en œuvre\n",
    "un réseau de neurones récurrent pour prédire les variations d'une série\n",
    "temporelle. En particulier, nous entraînerons un modèle de régression\n",
    "qui pourra servir soit à régresser des données manquantes, soit à\n",
    "prévoir les valeurs futures de la série. L'illustration se fera sur un\n",
    "jeu de données météorologiques.\n",
    "\n",
    "## Création d'un jeu d'entraînement et de validation\n",
    "\n",
    "Nous allons nous entraîner sur un jeu de données météorologiques\n",
    "disponible à l'adresse suivante :\n",
    "https://cedric.cnam.fr/vertigo/Cours/ml2/docs/meteo_dataset.csv.\n",
    "\n",
    "Pour commencer, téléchargeons ce jeu de données."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4e05ba2a",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import urllib.request\n",
    "\n",
    "if not os.path.exists('meteo_dataset.csv'):\n",
    "    urllib.request.urlretrieve('https://cedric.cnam.fr/vertigo/Cours/ml2/docs/meteo_dataset.csv', 'meteo_dataset.csv')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d01d0c2f",
   "metadata": {},
   "source": [
    "Il s'agit d'un jeu de données au format CSV, que nous pouvons\n",
    "visualiser, par exemple, avec la bibliothèque `pandas`\n",
    "(https://pandas.pydata.org/) en utilisant la méthode\n",
    "`pandas.read_csv`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "784eb14e",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "data = pd.read_csv('meteo_dataset.csv')\n",
    "data"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9adf8dda",
   "metadata": {},
   "source": [
    "Comme nous pouvons le voir sur la sortie ci-dessus, le jeu de données\n",
    "`meteo` est une série de $N = 420551$ observations. Chaque\n",
    "observation est décrite par un ensemble de variables, des grandeurs\n",
    "physiques issues d'un système météorologique. Formellement, nous\n",
    "travaillons sur une séquence de vecteurs\n",
    "$(\\mathbf u_n)_{0\\leq n \\leq N-1} \\in \\mathbb R^{7}$. Les\n",
    "différentes composantes de ces vecteurs correspondent aux paramètres\n",
    "physiques mesurés : $\\mathcal P = \\{$pression, température,\n",
    "saturation, déficit de pression de vapeur, humidité, densité, vitesse\n",
    "du vent$\\}$.\n",
    "\n",
    "Nous pouvons observer les variations de certains de ces paramètres au\n",
    "cours du temps pour l'ensemble du jeu de données, en traçant leur\n",
    "évolution au cours du temps avec `matplotlib` :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f6dbfe37",
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "\n",
    "fig = plt.figure(figsize=(20,7))\n",
    "# Évolution de la pression\n",
    "ax1 = fig.add_subplot(221)\n",
    "plt.plot(data['Pressure'])\n",
    "ax1.title.set_text('Pression')\n",
    "# Évolution de la température\n",
    "ax2 = fig.add_subplot(222)\n",
    "plt.plot(data['Temperature'])\n",
    "ax2.title.set_text('Température')\n",
    "# Évolution de la saturation\n",
    "ax3 = fig.add_subplot(223)\n",
    "plt.plot(data['Saturation vapor pressure'])\n",
    "ax3.title.set_text('Saturation')\n",
    "# Évolution du déficit de pression de vapeur\n",
    "ax4 = fig.add_subplot(224)\n",
    "plt.plot(data['Vapor pressure deficit'])\n",
    "ax4.title.set_text('Déficit de pression de vapeur')\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a45d1acd",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Commenter la forme des courbes. Que peut-on dire d'après l'aspect global des données ? Parmi les quatre paramètres que nous avons affiché, lesquels sont susceptibles d'être plus difficiles à apprendre que les autres ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cc95cec5",
   "metadata": {},
   "source": [
    "### Division du jeu de données en apprentissage et validation\n",
    "\n",
    "Comme d'habitude, nous allons séparer les données en un ensemble\n",
    "d'apprentissage et un ensemble de validation. L'ensemble de validation\n",
    "nous servira à l'évaluation du modèle. Dans notre cas, nous choisissons\n",
    "arbitrairement d'utiliser 80% des données pour l'apprentissage et les\n",
    "20% restants pour la validation."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "05768d0e",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Quelles précautions doit-on prendre dans la division de ce jeu de données en ensemble de *train* et de *test* ? Pourquoi ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1f555bb1",
   "metadata": {},
   "outputs": [],
   "source": [
    "split_fraction = 0.80\n",
    "split_idx = int(split_fraction * len(data))\n",
    "\n",
    "# Train: on conserve les 80% premières lignes\n",
    "train_data = data.loc[0:split_idx-1].values\n",
    "# Test: on conserve les 20% dernières lignes\n",
    "val_data = data.loc[split_idx:].values\n",
    "\n",
    "print(f\"Jeu d'apprentissage : {train_data.shape}, jeu d'évaluation : {val_data.shape}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6f3874c4",
   "metadata": {},
   "source": [
    "Dans un premier temps, nous allons nous intéresser à la prédiction de la\n",
    "température à partir des six autres paramètres de la série temporelle.\n",
    "Il s'agit donc d'une configuration *many-to-many*, c'est-à-dire que le\n",
    "modèle décisionnel consiste à prédire une séquence $\\mathbf y_t$\n",
    "(ici, la température) à partir d'une séquence de variables explicatives\n",
    "$\\mathbf x_t$ (les autres paramètres)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "65cef084",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Dans cette configuration, quelles sont les données ? Quelles sont les étiquettes ? Quelles sont leurs dimensions respectives ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3be0b960",
   "metadata": {},
   "source": [
    "Afin d'entraîner notre réseau de neurones par descente de gradient, nous\n",
    "allons devoir subdiviser le jeu de données en *batch* de séquences. En PyTorch, il n'existe pas\n",
    "d'équivalent direct de la fonction `timeseries_dataset_from_array` de Keras : nous allons donc\n",
    "écrire nous-mêmes un petit `Dataset` qui, à partir du tableau de la série temporelle, découpe des\n",
    "fenêtres glissantes de longueur `sequence_length`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "41ed7b69",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "from torch.utils.data import Dataset, DataLoader\n",
    "\n",
    "# Graine aléatoire, pour la reproductibilité (ordre des batches, initialisation du GRU)\n",
    "torch.manual_seed(42)\n",
    "\n",
    "class TimeseriesDataset(Dataset):\n",
    "    \"\"\" Convertit un tableau (N, d) en fenêtres glissantes de longueur `sequence_length`,\n",
    "        échantillonnées tous les `sampling_rate` pas de temps, pour prédire à chaque pas de\n",
    "        temps de la fenêtre la valeur correspondante de `targets` (many-to-many). \"\"\"\n",
    "    def __init__(self, data, targets, sequence_length, sampling_rate):\n",
    "        self.data = torch.as_tensor(data, dtype=torch.float32)\n",
    "        self.targets = torch.as_tensor(targets, dtype=torch.float32)\n",
    "        self.sequence_length = sequence_length\n",
    "        self.sampling_rate = sampling_rate\n",
    "        self.window_span = sequence_length * sampling_rate\n",
    "\n",
    "    def __len__(self):\n",
    "        return len(self.data) - self.window_span + 1\n",
    "\n",
    "    def __getitem__(self, idx):\n",
    "        x = self.data[idx: idx + self.window_span: self.sampling_rate]\n",
    "        y = self.targets[idx: idx + self.window_span: self.sampling_rate]\n",
    "        return x, y"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "292867e2",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "En vous aidant du code ci-dessus, expliquer à quoi correspondent les paramètres `sequence_length` et `sampling_rate`.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "26142c22",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Compléter le code suivant pour créer un jeu de données d'entraînement au format attendu par PyTorch (à l'aide de la classe `TimeseriesDataset` définie ci-dessus). Répéter pour le jeu de données d'évaluation. On se fixera des batch de taille $256$, des séquences de longueur $120$ éléments et, pour réduire le temps de calcul, nous allons sous-échantillonner les données d'un facteur $6$.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "969642c1",
   "metadata": {},
   "outputs": [],
   "source": [
    "sampling_rate = 6 # Fréquence d'échantillonnage\n",
    "sequence_length = 120  # Longueur de la séquence\n",
    "batch_size = 256 # Taille de batch\n",
    "\n",
    "# Création du jeu d'entraînement\n",
    "\n",
    "dataset_train = TimeseriesDataset(\n",
    "    data=train_data[:, (0,2,3,4,5,6)], # sélectionne toutes les colonnes sauf la température\n",
    "    targets=train_data[:, 1],           # sélectionne la colonne de température\n",
    "    # À compléter\n",
    ")\n",
    "train_loader = DataLoader(dataset_train, batch_size=batch_size, shuffle=True)\n",
    "\n",
    "# Création du jeu d'évaluation\n",
    "\n",
    "dataset_val = ... # À compléter\n",
    "val_loader = ... # À compléter"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "73d2fcaa",
   "metadata": {},
   "source": [
    "## Entraînement du réseau récurrent\n",
    "\n",
    "Maintenant que le jeu de données est mis en forme, nous pouvons définir\n",
    "le réseau récurrent que nous allons utiliser pour prédire la température\n",
    "à partir des autres paramètres météorologiques. Pour ce TP, nous allons\n",
    "implémenter un réseau récurrent simple constitué d'une *Gated Recurrent\n",
    "Unit* (GRU) et de deux couches linéaires."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5d3650b4",
   "metadata": {},
   "outputs": [],
   "source": [
    "from torch import nn"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "18d48455",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Quelles sont les dimensions des vecteurs en entrée et en sortie du réseau ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f0089d1b",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Compléter le code ci-dessous qui réalise la construction du modèle, sous la forme d'une classe\n",
    "héritant de `nn.Module`. On choisira d'utiliser une dimension égale à celle des variables\n",
    "d'entrée pour la couche cachée du GRU. On rappelle que `nn.GRU` renvoie, pour une séquence\n",
    "d'entrée, la séquence complète des états cachés (la sortie du GRU à chaque pas de temps),\n",
    "de dimensions `(batch, sequence_length, hidden_size)`.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6c9e1ae4",
   "metadata": {},
   "outputs": [],
   "source": [
    "class GRUModel(nn.Module):\n",
    "    def __init__(self, input_dim):\n",
    "        super().__init__()\n",
    "        # À compléter\n",
    "        ...\n",
    "\n",
    "    def forward(self, x):\n",
    "        # À compléter\n",
    "        ...\n",
    "\n",
    "dimension = train_data.shape[1] - 1\n",
    "model = GRUModel(dimension)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a13816c3",
   "metadata": {},
   "source": [
    "<div class=\"admonition note\">\n",
    "\n",
    "**Note**\n",
    "\n",
    "`batch_first=True` indique à `nn.GRU` que la dimension du batch vient en premier dans les tenseurs\n",
    "d'entrée et de sortie (forme `(batch, séquence, features)`), comme c'est le cas pour la plupart des\n",
    "autres couches PyTorch. Par défaut, `nn.GRU` attend au contraire la séquence en premier\n",
    "(`(séquence, batch, features)`), pour des raisons historiques de performance.\n",
    "\n",
    "</div>\n",
    "\n",
    "Concernant l'optimisation, nous allons utiliser le solveur `Adam` pour\n",
    "la descente de gradient. On fixe le pas d'apprentissage à $0,001$\n",
    "pour l'instant. On utilisera l'erreur quadratique moyenne (`nn.MSELoss`) comme fonction de coût,\n",
    "adaptée à un problème de régression :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fdecb1c3",
   "metadata": {},
   "outputs": [],
   "source": [
    "learning_rate = 0.001\n",
    "criterion = nn.MSELoss()\n",
    "optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0f353fd3",
   "metadata": {},
   "source": [
    "Comme pour les séances précédentes, nous devons cette fois écrire nous-mêmes la boucle\n",
    "d'apprentissage. Nous allons entraîner ce modèle pendant 5 époques, et suivre au passage l'erreur de\n",
    "validation :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6b91d41d",
   "metadata": {},
   "outputs": [],
   "source": [
    "epochs = 5\n",
    "\n",
    "for epoch in range(epochs):\n",
    "    model.train()\n",
    "    for x, y in train_loader:\n",
    "        y_pred = model(x).squeeze(-1)\n",
    "        loss = criterion(y_pred, y)\n",
    "        optimizer.zero_grad()\n",
    "        loss.backward()\n",
    "        optimizer.step()\n",
    "\n",
    "    model.eval()\n",
    "    with torch.no_grad():\n",
    "        val_losses = [criterion(model(x).squeeze(-1), y).item() for x, y in val_loader]\n",
    "    print(f\"Epoch {epoch}: loss (train) = {loss.item():.4f}, loss (val) = {sum(val_losses)/len(val_losses):.4f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7c9ae356",
   "metadata": {},
   "source": [
    "<div class=\"admonition note\">\n",
    "\n",
    "**Note**\n",
    "\n",
    "`model.train()` et `model.eval()` jouent, pour un modèle PyTorch personnalisé, le même rôle que\n",
    "nous avons déjà vu : ils permettent aux couches sensibles au mode d'exécution (*dropout*, *batch\n",
    "normalization*...) de se comporter différemment en apprentissage et en évaluation. Notre GRU n'en\n",
    "utilise pas, mais c'est une bonne habitude à prendre systématiquement.\n",
    "\n",
    "</div>\n",
    "\n",
    "L'affichage de l'erreur de validation à chaque époque nous permet de vérifier que nous ne sommes pas en\n",
    "régime de sur-apprentissage. Pour une analyse plus qualitative, nous pouvons visualiser à présent les\n",
    "résultats des prédictions sur les 5 premiers éléments du jeu de validation. La fonction ci-dessous\n",
    "`show_plot` permet de visualiser sur un même graphique la prédiction et la vérité terrain."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "055c7cf7",
   "metadata": {},
   "outputs": [],
   "source": [
    "def show_plot(predicted_data, true_data, title=None):\n",
    "    plt.title(title)\n",
    "    plt.plot(predicted_data, \"rx\", label=\"Température prédite\")\n",
    "    plt.plot(true_data, \".-\", label=\"Vraie température\")\n",
    "    plt.legend()\n",
    "    plt.xlabel(\"Pas de temps\")\n",
    "    plt.show()\n",
    "    return"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3d7ff8b8",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "À l'aide du modèle entraîné, calculer les prédictions de température pour les trois premières séquences `x` du jeu de données de validation. Utiliser la fonction `show_plot` ci-dessus pour afficher les graphiques correspondants. On pensera à repasser le modèle en mode évaluation (`model.eval()`) et à désactiver le calcul des gradients.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0b045d6a",
   "metadata": {},
   "outputs": [],
   "source": [
    "model.eval()\n",
    "with torch.no_grad():\n",
    "    for x, y in list(val_loader)[:1]:\n",
    "        # À compléter, sur les 3 premières séquences du batch\n",
    "        ..."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5d8df420",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Que constatez-vous concernant les performances du modèle ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "06a68df6",
   "metadata": {},
   "source": [
    "Bien que simple, notre modèle récurrent semble à même de prédire avec\n",
    "une haute précision la température à partir des autres paramètres\n",
    "météorologiques. Même si nous avons utilisé l'erreur quadratique moyenne\n",
    "pour l'optimisation du modèle, nous pouvons calculer l'erreur absolue\n",
    "moyenne, qui est souvent plus facile à interpréter :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e3c8a380",
   "metadata": {},
   "outputs": [],
   "source": [
    "mean_absolute_error = nn.L1Loss()\n",
    "error = 0\n",
    "\n",
    "model.eval()\n",
    "with torch.no_grad():\n",
    "    for x, y in val_loader:\n",
    "        y_pred = model(x).squeeze(-1)\n",
    "        error += mean_absolute_error(y_pred, y).item()\n",
    "\n",
    "print(f\"Erreur absolue moyenne : {error/len(val_loader):.5f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d66e784a",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "*À faire chez vous pour approfondir, si vous avez le temps.* Refaire l'expérience mais en prédisant la pression à partir des autres paramètres. Les conclusions sont-elles identiques ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8ead27b0",
   "metadata": {},
   "source": [
    "## Prédiction de la température future\n",
    "\n",
    "Nous avons vu précédemment comment prédire une séquence de valeurs à\n",
    "partir d'une autre séquence. Pour aller plus loin, nous allons à présent\n",
    "chercher non plus à reconstruire température à partir des autres\n",
    "paramètres, mais à prédire la température *future*, c'est-à-dire à\n",
    "$t+1$, à partir de toutes les variables météorologiques jusqu'au\n",
    "temps $t$.\n",
    "\n",
    "Comme le but est de prédire la température future, nous allons diviser\n",
    "nos données en séquences de $M$ éléments. Chacune aura pour\n",
    "étiquette associée la température $T$ instants plus tard.\n",
    "Formellement, notre modèle prendra en entrée une séquence décrite par\n",
    "$\\mathbf X_n = \\{\\mathbf u_{n}, \\cdots, \\mathbf u_{n+M}\\} \\in \\mathbb R^{M\\times 7}$\n",
    "et prédira en sortie un scalaire unique\n",
    "$y_n = \\mathbf u_{n+M+T+1,\\text{Temperature}} \\in \\mathbb R$. Il\n",
    "s'agit donc désormais d'un problème *many-to-one*.\n",
    "\n",
    "Nous utiliserons une séquence de $M = 720$ observations passées\n",
    "pour prédire la température à $T = 6$ instants plus tard. Les séquences\n",
    "d'entrée démarreront donc à l'indice $0$ tandis que la première\n",
    "étiquette correspondra à l'indice $720+6=726$. Ici encore, nous\n",
    "allons sous-échantillonner la séquence d'origine par un facteur\n",
    "$6$, ce qui nous donnera en entrée des séquences de longueur\n",
    "$720/6=120$, comme précédemment. Avec ce sous-échantillonnage,\n",
    "cela revient donc à faire la prédiction $t+1$ à partir des\n",
    "$120$ valeurs précédentes.\n",
    "\n",
    "Cette fois, chaque séquence d'entrée est associée à une *unique* étiquette (et non plus une séquence\n",
    "d'étiquettes) : nous avons besoin d'un second petit `Dataset`, adapté à ce cas *many-to-one*."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a0908184",
   "metadata": {},
   "outputs": [],
   "source": [
    "class FutureTimeseriesDataset(Dataset):\n",
    "    \"\"\" Fenêtres glissantes de longueur `sequence_length` (échantillonnées tous les\n",
    "        `sampling_rate` pas de temps), chacune associée à une unique étiquette (celle de\n",
    "        `targets` à `future_step` pas de temps après la fin de la fenêtre). \"\"\"\n",
    "    def __init__(self, data, targets, sequence_length, sampling_rate, future_step):\n",
    "        self.data = torch.as_tensor(data, dtype=torch.float32)\n",
    "        self.targets = torch.as_tensor(targets, dtype=torch.float32)\n",
    "        self.sequence_length = sequence_length\n",
    "        self.sampling_rate = sampling_rate\n",
    "        self.window_span = sequence_length * sampling_rate\n",
    "        self.future_step = future_step\n",
    "\n",
    "    def __len__(self):\n",
    "        return len(self.data) - self.window_span - self.future_step\n",
    "\n",
    "    def __getitem__(self, idx):\n",
    "        x = self.data[idx: idx + self.window_span: self.sampling_rate]\n",
    "        y = self.targets[idx + self.window_span + self.future_step]\n",
    "        return x, y"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "84892f98",
   "metadata": {},
   "outputs": [],
   "source": [
    "past_length = 720\n",
    "future_step = 6\n",
    "step_size = 6 # Facteur d'échantillonnage\n",
    "sequence_length = int(past_length / step_size)\n",
    "batch_size = 256\n",
    "\n",
    "dataset_train = FutureTimeseriesDataset(\n",
    "    data=train_data,\n",
    "    targets=train_data[:, 1],\n",
    "    sequence_length=sequence_length,\n",
    "    sampling_rate=step_size,\n",
    "    future_step=future_step,\n",
    ")\n",
    "train_loader = DataLoader(dataset_train, batch_size=batch_size, shuffle=True)\n",
    "\n",
    "dataset_val = FutureTimeseriesDataset(\n",
    "    data=val_data,\n",
    "    targets=val_data[:, 1],\n",
    "    sequence_length=sequence_length,\n",
    "    sampling_rate=step_size,\n",
    "    future_step=future_step,\n",
    ")\n",
    "val_loader = DataLoader(dataset_val, batch_size=batch_size, shuffle=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7cbb8ce3",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Quelle est la taille des éléments en entrée et en sortie du réseau ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1ba952dd",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Compléter le code ci-dessous qui gère la définition du modèle. Cette fois, seul le *dernier* état\n",
    "caché du GRU nous intéresse (puisque l'on prédit un scalaire unique, pas une séquence) : on\n",
    "pourra utiliser le second élément renvoyé par `self.gru(x)`, de dimensions\n",
    "`(1, batch, hidden_size)` (à ne pas confondre avec la séquence complète des états cachés).\n",
    "On utilisera une couche cachée de dimension 32. Ne pas\n",
    "oublier de créer un optimiseur `Adam` avec un pas\n",
    "d'apprentissage de $0,001$. On utilisera l'erreur quadratique\n",
    "moyenne (*MSE*) comme fonction de coût.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9be68155",
   "metadata": {},
   "outputs": [],
   "source": [
    "class GRUFutureModel(nn.Module):\n",
    "    def __init__(self, input_dim, hidden_dim):\n",
    "        super().__init__()\n",
    "        # À compléter\n",
    "        ...\n",
    "\n",
    "    def forward(self, x):\n",
    "        # À compléter\n",
    "        ...\n",
    "\n",
    "input_dim = train_data.shape[1]\n",
    "model = GRUFutureModel(input_dim, hidden_dim=32)\n",
    "criterion = ... # À compléter\n",
    "optimizer = ... # À compléter"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5692ffc8",
   "metadata": {},
   "source": [
    "À nouveau, nous pouvons entraîner le modèle sur le jeu de données à\n",
    "l'aide de la boucle d'apprentissage habituelle :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ab099f53",
   "metadata": {},
   "outputs": [],
   "source": [
    "epochs = 5\n",
    "\n",
    "for epoch in range(epochs):\n",
    "    model.train()\n",
    "    for x, y in train_loader:\n",
    "        y_pred = model(x)\n",
    "        loss = criterion(y_pred, y)\n",
    "        optimizer.zero_grad()\n",
    "        loss.backward()\n",
    "        optimizer.step()\n",
    "\n",
    "    model.eval()\n",
    "    with torch.no_grad():\n",
    "        val_losses = [criterion(model(x), y).item() for x, y in val_loader]\n",
    "    print(f\"Epoch {epoch}: loss (train) = {loss.item():.4f}, loss (val) = {sum(val_losses)/len(val_losses):.4f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5885b6b0",
   "metadata": {},
   "source": [
    "Pour terminer, nous pouvons désormais visualiser les résultats en\n",
    "prédiction de notre modèle sur les trois premières séquences du jeu de\n",
    "validation :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b94076f1",
   "metadata": {},
   "outputs": [],
   "source": [
    "def show_plot(sequence, prediction, ground_truth, delta=future_step/step_size, title=None):\n",
    "    plt.title(title)\n",
    "    plt.plot(sequence.flatten(), \".-\", label=\"Température passée\")\n",
    "    plt.plot(len(sequence) + delta, prediction, \"go\", label=\"Prédiction\")\n",
    "    plt.plot(len(sequence) + delta, ground_truth, \"rx\", label=\"Vraie température\")\n",
    "    plt.legend()\n",
    "    plt.xlim(-1, len(sequence) + delta + 3)\n",
    "    plt.xlabel(\"Pas de temps\")\n",
    "    plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "809ab045",
   "metadata": {},
   "outputs": [],
   "source": [
    "model.eval()\n",
    "with torch.no_grad():\n",
    "    x, y = next(iter(val_loader))\n",
    "    y_pred = model(x)\n",
    "    for i in range(5):\n",
    "        show_plot(x[i][:, 1].numpy(), y_pred[i].item(), y[i].item(), title=\"Prédiction de la température\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "97ce6e57",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Qu'observez-vous qualitativement dans les prédictions de notre modèle ? Que pouvez-vous en conclure sur la difficulté de la tâche de prédiction par rapport à la tâche de régression précédemment considérée ?\n",
    "\n",
    "</div>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
