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 ?
Correction
La température, la saturation et le déficit de pression de vapeur ont des évolutions périodiques très marquées, avec une cohérence temporelle élevée. La courbe de température est notamment assez peu bruitée. À l’inverse, la courbe de pression est bruitée, avec des outliers importants et ne semble pas exhiber de périodicité évidente. Ce paramètre risque donc d’être nettement plus difficile à prédire.
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 ?
Correction
Comme il s’agit de séries temporelles, les observations ne sont pas i.i.d. (indépendantes et identiquement distribuées). En effet, l’observation \(x_t\) au pas de temps \(t\) sera très certainement corrélée avec l’observation \(x_{t+1}\) au pas de temps suivant. Il ne faut donc pas réaliser une division aléatoire avec un tirage uniforme : les données de test seraient très similaires aux données d’entraînement et notre estimation de l’erreur de généralisation sera bien trop optimiste.
Une solution consiste à entraîner sur les premières valeurs de la série temporelle, puis de s’évaluer sur les dernières valeurs, en créant un jeu d’apprentissage et un jeu de test qui sont temporellement disjoints.
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 ?
Correction
Le modèle prendra en entrée une séquence de \(M\) éléments correspondant à tous les paramètres sauf la température pour \(M\) acquisitions et prédira pour chaque pas de temps la température associée. Formellement, le modèle prendra en entrée une séquence \(\mathbf X_n = [\mathbf x_{n},\cdots,\mathbf x_{n+M-1}] \in \mathbb R^{M\times 6}\) telle que \(\forall\, n \in \{0,\cdots , N-1\}, p \in \mathcal P\setminus \{\text{Temperature}\}, \mathbf x_{n,p} = \mathbf u_{n,p}\). En sortie, le modèle prédit la séquence \(\mathbf Y_n = [ y_{n},\cdots, y_{n+M-1}] \in \mathbb R^{M\times 1}\) telle que \(\forall\, n \in \{0,\cdots , N-1\}, y_{n} = \mathbf u_{n,\text{Temperature}}\).
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
.
Correction
sequence_length
correspond à la taille de la fenêtre de
la série temporelle, c’est-à-dire la longueur maximale de la séquence
considérée. sampling_rate
définit le taux d’échantillonnage et
permet de réduire la dimension de la série en considérant par exemple
qu’une valeur sur deux.
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\).
Correction
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)],
targets=None,
sequence_length=sequence_length,
sampling_rate=sampling_rate,
batch_size=batch_size,
)
y_train = tf.keras.utils.timeseries_dataset_from_array(
data=train_data[:, 1],
targets=None,
sequence_length=sequence_length,
sampling_rate=sampling_rate,
batch_size=batch_size,
)
dataset_train = tf.data.Dataset.zip((X_train, y_train))
# Création du jeu d'évaluation
X_val = tf.keras.utils.timeseries_dataset_from_array(
data=val_data[:, (0,2,3,4,5,6)],
targets=None,
sequence_length=sequence_length,
sampling_rate=sampling_rate,
batch_size=batch_size,
)
y_val = tf.keras.utils.timeseries_dataset_from_array(
data=val_data[:, 1],
targets=None,
sequence_length=sequence_length,
sampling_rate=sampling_rate,
batch_size=batch_size,
)
dataset_val = tf.data.Dataset.zip((X_val, y_val))
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).
Correction
dimension = train_data.shape[1]-1
inputs = keras.layers.Input(shape=(sequence_length, dimension))
gru_out = keras.layers.GRU(dimension, return_sequences=True)(inputs)
dense_out = keras.layers.Dense(dimension, activation='relu')(gru_out)
outputs = keras.layers.Dense(1)(dense_out)
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.
Correction
for x, y in dataset_val.take(3):
show_plot(y[0].numpy(), model.predict(x)[0], title="Prédiction synchrone")
Question
Que constatez-vous concernant les performances du modèle ?
Correction
Les prédictions de température sont très proches de la vérité terrain. Les performances en train et en test sont bonnes et à peu près équivalentes, ce qui signifie a priori qu’il n’y a pas de surapprentrissage.
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,
)
Correction
Comme précédemment, nous allons entraîner un réseau récurrent constitué d’une couche récurrente utilisant la cellule GRU et de deux couches linéaires séparées par une activation ReLU.
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.
Correction
learning_rate = 0.001
input_dim = train_data.shape[1]
hidden_dim = 32
inputs = keras.layers.Input(shape=(sequence_length, input_dim))
gru_out = keras.layers.GRU(hidden_dim)(inputs)
dense_out = keras.layers.Dense(hidden_dim, activation='relu')(gru_out)
outputs = keras.layers.Dense(1)(dense_out)
model = keras.Model(inputs=inputs, outputs=outputs)
model.compile(optimizer=keras.optimizers.Adam(learning_rate=learning_rate), loss="mse")
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 ?
Correction
Les prédictions sont moins souvent correctes que précédemment. Il y a plus de grandes erreurs de prédictions entre la température prédite et la température future réelle. Ce n’est pas inattendu car l’on pouvait s’attendre à ce que la prédiction des valeurs futures soit un problème plus difficile que la reconstruction de la température à un pas de temps donné en connaissant toutes les autres variables.