Travaux pratiques - Prédiction de séries temporelles¶
L’objectif de cette séance de travaux pratiques est de mettre en œuvre un réseau de neurones récurrent pour prédire les variations d’une série temporelle. En particulier, nous entraînerons un modèle de régression qui pourra servire soit à régresser des données manquantes, soit à prévoir les valeurs futures de la série. L’illustration se fera sur un jeu de données météorologiques.
Création d’un jeu d’entraînement et de validation¶
Nous allons nous entraîner sur un jeu de données météorologiques disponible à l’adresse suivante : http://cedric.cnam.fr/~rambourc/meteo_dataset.csv.
Pour commencer, téléchargeons ce jeu de données.
!wget -nc http://cedric.cnam.fr/~rambourc/meteo_dataset.csv
Il s’agit d’un jeu de données au format CSV, que nous pouvons
visualiser, par exemple, avec la bibliothèque pandas
(https://pandas.pydata.org/) en utilisant la méthode
pandas.read_csv
.
import pandas as pd
data = pd.read_csv('meteo_dataset.csv')
data
Comme nous pouvons le voir sur la sortie ci-dessus, le jeu de données
meteo
est une série de \(N = 420551\) observations. Chaque
observation est décrite par un ensemble de variables, des grandeurs
physiques issues d’un système météorologique. Formellement, nous
travaillons sur une séquence de vecteurs
\((\mathbf u_n)_{0\leq n \leq N-1} \in \mathbb R^{7}\). Les
différentes composantes de ces vecteurs correspondent aux paramètres
physiques mesurés : \(\mathcal P = \{\)pression, température,
saturation, déficit de pression de vapeur, humidité, densité, vistesse
du vent\(\}\).
Nous pouvons observer les variations de certains de ces paramètres au
cours du temps pour l’ensemble du jeu de données, en traçant leur
évolution au cours du temps avec matplotlib
:
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(20,7))
# Évolution de la pression
ax1 = fig.add_subplot(221)
plt.plot(data['Pressure'])
ax1.title.set_text('Pression')
# Évolution de la température
ax2 = fig.add_subplot(222)
plt.plot(data['Temperature'])
ax2.title.set_text('Température')
# Évolution de la saturation
ax3 = fig.add_subplot(223)
plt.plot(data['Saturation vapor pressure'])
ax3.title.set_text('Saturation')
# Évolution du déficit de pression de vapeur
ax4 = fig.add_subplot(224)
plt.plot(data['Vapor pressure deficit'])
ax4.title.set_text('Déficit de pression de vapeur')
plt.show()
Question
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 ?
Division du jeu de données en apprentissage et validation¶
Comme d’habitude, nous allons séparer les données en un ensemble d’apprentissage et un ensemble de validation. L’ensemble de validation nous servira à l’évaluation du modèle. Dans notre cas, nous choisissons arbitrairement d’utiliser 80% des données pour l’apprentissage et les 20% restants pour la validation.
Question
Quelles précautions doit-on prendre dans la division de ce jeu de données en ensemble de train et de test ? Pourquoi ?
split_fraction = 0.80
split_idx =int(split_fraction * len(data))
# Train: on conserve les 80% premières lignes
train_data = data.loc[0:split_idx-1].values
# Test: on conserve les 20% dernières lignes
val_data = data.loc[split_idx:].values
print(f"Jeu d'apprentissage : {train_data.shape}, jeu d'évaluation : {val_data.shape}")
Dans un premier temps, nous allons nous intéresser à la prédiction de la température à partir des six autres paramètres de la série temporelle. Il s’agit donc d’une configuration many-to-many, c’est-à-dire que le modèle décisionnel consiste à prédire une séquence \(\mathbf y_t\) (ici, la température) à partir d’une séquence de variables explicatives \(\mathbf x_t\) (les autres paramètres).
Question
Dans cette configuration, quelles sont les données ? Quelles sont les étiquettes ? Quelles sont leurs dimensions respectives ?
Afin d’entraîner notre réseau de neurones par descente de gradient, nous
allons devoir subdiviser le jeu de données en batch. Cette opération
peut être réalisée automatiquement à l’aide de la fonction
timeseries_dataset_from_array()
de Keras.
Question
En vous aidant de la documentation de la fonction, expliquer
à quoi correspondent les paramètres sequence_length
et sampling_rate
.
Question
Compléter le code suivant pour créer un jeu de données d’entraînement au format attendu par Keras. 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\).
import tensorflow as tf
sampling_rate = 6 # Fréquence d'échantillonnage
sequence_length = 120 # Longueur de la séquence
batch_size = 256 # Taille de batch
# Création du jeu d'entraînement
X_train = tf.keras.utils.timeseries_dataset_from_array(
data=train_data[:, (0,2,3,4,5,6)], # sélectionne toutes les colonnes sauf la température
targets=None,
# TODO: compléter
)
y_train = tf.keras.utils.timeseries_dataset_from_array(
data=train_data[:, 1], # sélectionne la colonne de température
targets=None,
# TODO: compléter
)
dataset_train = tf.data.Dataset.zip(X_train, y_train)
# Création du jeu d'évaluation
X_val = ... # TODO: Compléter
y_val = ... # TODO: Compléter
dataset_val = ... # TODO: Compléter
Entraînement du réseau récurrent¶
Maintenant que le jeu de données est mis en forme, nous pouvons définir le réseau récurrent que nous allons utiliser pour prédire la température à partir des autres paramètres météorologiques. Pour ce TP, nous allons implémenter un réseau récurrent simple constitué d’une Gated Recurrent Unit (GRU) et de deux couches linéaires.
from tensorflow import keras
Question
Quelles sont les dimensions des vecteurs en entrée et en sortie du réseau ?
Question
Compléter le code ci-dessous qui réalise la construction du modèle. On choisira d’utiliser une dimension égale à celle des variables d’entrée pour la couche cachée du GRU. La fonction de perte utilisée pour la régression sera l’erreur quadratique moyenne (MSE).
dimension = train_data.shape[1] - 1
inputs = keras.layers.Input(shape=(sequence_length, dimension))
# TODO: à compléter
...
model = keras.Model(inputs=inputs, outputs=outputs)
Concernant l’optimisation, nous allons utiliser le solveur Adam
pour
la descente de gradient. On fixe le pas d’apprentissage à \(0,001\)
pour l’instant. Nous pouvons compiler le modèle avec Keras :
learning_rate = 0.001
model.compile(optimizer=keras.optimizers.Adam(learning_rate=learning_rate), loss="mse")
model.summary()
Comme d’habitude avec Keras, nous pouvons démarrer l’apprentissage du
modèle à l’aide de la méthode fit
sur notre jeu de données
d’apprentissage. Nous allons entraîner ce modèle pendant 5 époques :
epochs = 5
model.fit(
dataset_train,
epochs=epochs,
validation_data=dataset_val,
)
Keras nous affiche lors de l’apprentissage les scores de validation, ce
qui nous permet de vérifier que nous ne sommes pas en régime de
sur-apprentissage. Pour une analyse plus qualitative, nous pouvons
visualiser à présent les résultats des prédictions sur les 5 premiers
éléments du jeu de validation. La fonction ci-dessous show_plot
permet de visualiser sur un même graphique la prédiction et la vérité
terrain.
def show_plot(predicted_data, true_data, title=None):
plt.title(title)
plt.plot(predicted_data, "rx", label="Température prédite")
plt.plot(true_data, ".-", label="Vraie température")
plt.legend()
plt.xlabel("Pas de temps")
plt.show()
return
Question
À l’aide de la méthode predict
de Keras
(cf. documentation),
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.
for x, y in dataset_val.take(3):
# TODO: compléter
...
Question
Que constatez-vous concernant les performances du modèle ?
Bien que simple, notre modèle récurrent semble à même de prédire avec une haute précision la température à partir des autres paramètres météorologiques. Même si nous avons utilisé l’erreur quadratique moyenne pour l’optimisation du modèle, nous pouvons calculer l’erreur absolue moyenne, qui est souvent plus facile à interpréter :
mean_absolute_error = tf.keras.losses.MeanAbsoluteError()
error = 0
for x, y in dataset_val:
y_pred = model(x)
error += mean_absolute_error(y, y_pred).numpy()
print(f"Erreur absolue moyenne : {error/len(dataset_val):.5f}")
Question
À 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 ?
Prédiction de la température future¶
Nous avons vu précédemment comment prédire une séquence de valeurs à partir d’une autre séquence. Pour aller plus loin, nous allons à présent chercher non plus à reconstruire température à partir des autres paramètres, mais à prédire la température future, c’est-à-dire à \(t+1\), à partir de toutes les variables météorologiques jusqu’au temps \(t\).
Comme le but est de prédire la température future, nous allons diviser nos données en séquences de \(M\) éléments. Chacune aura pour étiquette associée la température \(T\) instants plus tard. Formellement, notre modèle prendra en entrée une séquence décrite par \(\mathbf X_n = \{\mathbf u_{n}, \cdots, \mathbf u_{n+M}\} \in \mathbb R^{M\times 7}\) et prédira en sortie un scalaire unique \(y_n = \mathbf u_{n+M+T+1,\text{Temperature}} \in \mathbb R\). Il s’agit donc désormais d’un problème many-to-one.
Nous utiliserons une séquence de \(M = 720\) observations passées prédire la température à \(T = 6\) instants plus tard. Les séquences d’entrée démarreront donc à l’indice \(0\) tandis que la première étiquette correspondra à l’indice \(720+6=726\). Ici encore, nous allons sous-échantillonner la séquence d’origine par un facteur \(6\), ce qui nous donnera en entrée des séquences de longueur \(720/6=120\), comme précédemment. Avec ce sous-échantillonnage, cela revient donc à faire la prédiction \(t+1\) à partir des \(120\) valeurs précédentes.
past_length = 720
future_step = 6
label_start = past_length + future_step # Début de la séquence des labels
label_end = label_start + split_idx # Fin de la séquence des labels
x_train = train_data
y_train = data[label_start:label_end][['Temperature']]
x_end = len(val_data) - past_length - future_step # Fin de la séquence des données d'entrées en validation
label_start = split_idx + past_length + future_step # Début de la séquence des labels en validation
x_val = val_data[:x_end]
y_val = data[label_start:][['Temperature']]
batch_size = 256
step_size = 6 # Facteur d'échantillonnage
sequence_length = int(past_length / step_size)
dataset_train = tf.keras.utils.timeseries_dataset_from_array(
x_train,
y_train,
sequence_length=sequence_length,
sampling_rate=step_size,
batch_size=batch_size,
)
dataset_val = keras.preprocessing.timeseries_dataset_from_array(
x_val,
y_val,
sequence_length=sequence_length,
sampling_rate=step_size,
batch_size=batch_size,
)
Question
Quelle est la taille des éléments en entrée et en sortie du réseau ?
Question
Compléter le code ci-dessous qui gère la définition du modèle
avec Keras. On utilisera une couche cachée de dimension 32. Ne pas
oublier de compiler le modèle avec un optimiseur Adam
avec un pas
d’apprentissage de \(0,001\). On utilisera l’erreur quadratique
moyenne (mse) comme fonction de coût.
input_dim = train_data.shape[1]
inputs = keras.layers.Input(shape=(sequence_length, input_dim))
# TODO: compléter
...
model = keras.Model(inputs=inputs, outputs=outputs)
model.compile(...) # TODO: compléter
model.summary()
À nouveau, nous pouvons entraîner le modèle sur le jeu de données à
l’aide de la méthode .fit()
du modèle Keras :
epochs = 5
history = model.fit(
dataset_train,
epochs=epochs,
validation_data=dataset_val,
)
Pour terminer, nous pouvons désormais visualisons les résultats en prédiction de notre modèle sur les trois premières séquences du jeu de validation :
def show_plot(sequence, prediction, ground_truth, delta=future_step/step_size, title=None):
plt.title(title)
plt.plot(sequence.flatten(), ".-", label="Température passée")
plt.plot(len(sequence) + delta, prediction, "go", label="Prédiction")
plt.plot(len(sequence) + delta, ground_truth, "rx", label="Vraie température")
plt.legend()
plt.xlim(-1, len(sequence) + delta + 3)
plt.xlabel("Pas de temps")
plt.show()
for x, y in dataset_val.take(5):
show_plot(x[0][:, 1].numpy(), model.predict(x)[0], y[0].numpy(), title="Prédiction de la température")
Question
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 ?