Recherche de site Web

Modèles d'apprentissage profond pour la prévision de séries chronologiques univariées


Les réseaux neuronaux d’apprentissage profond sont capables d’apprendre et d’extraire automatiquement des fonctionnalités à partir de données brutes.

Cette fonctionnalité des réseaux de neurones peut être utilisée pour les problèmes de prévision de séries chronologiques, où des modèles peuvent être développés directement sur les observations brutes sans qu'il soit directement nécessaire de mettre à l'échelle les données à l'aide de la normalisation et de la standardisation ou de rendre les données stationnaires par différenciation.

De manière impressionnante, les modèles simples de réseaux neuronaux d'apprentissage en profondeur sont capables de faire des prévisions habiles par rapport aux modèles naïfs et aux modèles SARIMA optimisés sur des problèmes de prévision de séries chronologiques univariées comportant à la fois des composantes de tendance et des composantes saisonnières sans prétraitement.

Dans ce didacticiel, vous découvrirez comment développer une suite de modèles d'apprentissage profond pour la prévision de séries chronologiques univariées.

Après avoir terminé ce tutoriel, vous saurez :

  • Comment développer un harnais de test robuste en utilisant la validation progressive pour évaluer les performances des modèles de réseaux neuronaux.
  • Comment développer et évaluer des réseaux de neurones Perceptron multicouches et convolutifs simples pour la prévision de séries chronologiques.
  • Comment développer et évaluer des modèles de réseaux neuronaux LSTM, CNN-LSTM et ConvLSTM pour la prévision de séries chronologiques.

Démarrez votre projet avec mon nouveau livre Deep Learning for Time Series Forecasting, comprenant des tutoriels pas à pas et les fichiers code source Python pour tous les exemples.

Commençons.

  • Mise à jour en avril 2019 : mise à jour du lien vers l'ensemble de données.

Présentation du didacticiel

Ce didacticiel est divisé en cinq parties ; ils sont:

  1. Description du problème
  2. Harnais de test d'évaluation du modèle
  3. Modèle Perceptron multicouche
  4. Modèle de réseau neuronal convolutif
  5. Modèles de réseaux neuronaux récurrents

Description du problème

L'ensemble de données « Ventes mensuelles de voitures » résume les ventes mensuelles de voitures au Québec, Canada entre 1960 et 1968.

Téléchargez l'ensemble de données directement à partir d'ici :

  • ventes-de-voitures-mensuelles.csv

Enregistrez le fichier sous le nom de fichier « monthly-car-sales.csv » dans votre répertoire de travail actuel.

Nous pouvons charger cet ensemble de données sous forme de série Pandas en utilisant la fonction read_csv().

# load
series = read_csv('monthly-car-sales.csv', header=0, index_col=0)

Une fois chargé, nous pouvons résumer la forme de l'ensemble de données afin de déterminer le nombre d'observations.

# summarize shape
print(series.shape)

Nous pouvons ensuite créer un tracé linéaire de la série pour avoir une idée de la structure de la série.

# plot
pyplot.plot(series)
pyplot.show()

Nous pouvons relier tout cela ensemble ; l'exemple complet est répertorié ci-dessous.

# load and plot dataset
from pandas import read_csv
from matplotlib import pyplot
# load
series = read_csv('monthly-car-sales.csv', header=0, index_col=0)
# summarize shape
print(series.shape)
# plot
pyplot.plot(series)
pyplot.show()

L’exécution de l’exemple imprime d’abord la forme de l’ensemble de données.

(108, 1)

L'ensemble de données est mensuel et compte neuf ans, soit 108 observations. Lors de nos tests, nous utiliserons l'année dernière, soit 12 observations, comme ensemble de tests.

Un tracé linéaire est créé. L'ensemble de données a une tendance évidente et une composante saisonnière. La durée de la composante saisonnière pourrait être de six mois ou de 12 mois.

D'après des expériences antérieures, nous savons qu'un modèle naïf peut atteindre une erreur quadratique moyenne, ou RMSE, de 1 841,155 en prenant la médiane des observations des trois années précédentes pour le mois prédit ; Par exemple:

yhat = median(-12, -24, -36)

Où les indices négatifs font référence aux observations de la série par rapport à la fin des données historiques pour le mois prédit.

D'après des expériences antérieures, nous savons qu'un modèle SARIMA peut atteindre un RMSE de 1551,842 avec la configuration de SARIMA(0, 0, 0),(1, 1, 0),12 où aucun élément n'est spécifié pour la tendance et une différence saisonnière. avec une période de 12 est calculé et un modèle AR d'une saison est utilisé.

Les performances du modèle naïf fournissent une borne inférieure sur un modèle considéré comme compétent. Tout modèle qui atteint une performance prédictive inférieure à 1841,155 sur les 12 derniers mois est compétent.

Les performances du modèle SARIMA fournissent une mesure d'un bon modèle sur le problème. Tout modèle qui atteint une performance prédictive inférieure à 1551,842 sur les 12 derniers mois doit être adopté par rapport à un modèle SARIMA.

Maintenant que nous avons défini notre problème et nos attentes en matière de compétences du modèle, nous pouvons envisager de définir le harnais de test.

Harnais de test d'évaluation du modèle

Dans cette section, nous développerons un ensemble de tests pour développer et évaluer différents types de modèles de réseaux neuronaux pour la prévision de séries chronologiques univariées.

Cette section est divisée en parties suivantes :

  1. Répartition Train-Test
  2. Les séries comme apprentissage supervisé
  3. Validation progressive
  4. Répéter l'évaluation
  5. Résumer les performances
  6. Exemple travaillé

Répartition Train-Test

La première étape consiste à diviser la série chargée en ensembles de train et de test.

Nous utiliserons les huit premières années (96 observations) pour la formation et les 12 dernières pour l'ensemble de tests.

La fonction train_test_split() ci-dessous divisera la série en prenant les observations brutes et le nombre d'observations à utiliser dans l'ensemble de test comme arguments.

# split a univariate dataset into train/test sets
def train_test_split(data, n_test):
	return data[:-n_test], data[-n_test:]

Les séries comme apprentissage supervisé

Ensuite, nous devons être capables de présenter la série univariée d’observations comme un problème d’apprentissage supervisé afin de pouvoir entraîner des modèles de réseaux neuronaux.

Un cadrage d'apprentissage supervisé d'une série signifie que les données doivent être divisées en plusieurs exemples à partir desquels le modèle apprend et généralise.

Chaque échantillon doit avoir à la fois une composante d’entrée et une composante de sortie.

La composante d'entrée sera un certain nombre d'observations antérieures, telles que trois ans ou 36 pas de temps.

La composante de production sera le total des ventes du mois prochain car nous souhaitons développer un modèle pour faire des prévisions en une étape.

Nous pouvons implémenter cela en utilisant la fonction shift() sur le DataFrame pandas. Cela nous permet de décaler une colonne vers le bas (vers l'avant dans le temps) ou vers l'arrière (en arrière dans le temps). Nous pouvons prendre la série comme une colonne de données, puis créer plusieurs copies de la colonne, décalées vers l'avant ou vers l'arrière dans le temps afin de créer les échantillons avec les éléments d'entrée et de sortie dont nous avons besoin.

Lorsqu'une série est décalée vers le bas, des valeurs NaN sont introduites car nous n'avons pas de valeurs au-delà du début de la série.

Par exemple, la série définie sous forme de colonne :

(t)
1
2
3
4

Peut être décalé et inséré au préalable en colonne :

(t-1),		(t)
Nan,		1
1,			2
2,			3
3,			4
4			NaN

Nous pouvons voir que sur la deuxième ligne, la valeur 1 est fournie en entrée en tant qu'observation au pas de temps précédent, et 2 est la valeur suivante de la série qui peut être prédite, ou apprise par le modèle à prédire lorsque 1 est présenté en entrée.

Les lignes avec des valeurs NaN peuvent être supprimées.

La fonction series_to_supervised() ci-dessous implémente ce comportement, vous permettant de spécifier le nombre d'observations de retard à utiliser dans l'entrée et le nombre à utiliser dans la sortie pour chaque échantillon. Cela supprimera également les lignes qui ont des valeurs NaN car elles ne peuvent pas être utilisées pour entraîner ou tester un modèle.

# transform list into supervised learning format
def series_to_supervised(data, n_in=1, n_out=1):
	df = DataFrame(data)
	cols = list()
	# input sequence (t-n, ... t-1)
	for i in range(n_in, 0, -1):
		cols.append(df.shift(i))
	# forecast sequence (t, t+1, ... t+n)
	for i in range(0, n_out):
		cols.append(df.shift(-i))
	# put it all together
	agg = concat(cols, axis=1)
	# drop rows with NaN values
	agg.dropna(inplace=True)
	return agg.values

Validation progressive

Les modèles de prévision de séries chronologiques peuvent être évalués sur un ensemble de tests à l'aide d'une validation progressive.

La validation progressive est une approche dans laquelle le modèle effectue une prévision pour chaque observation de l'ensemble de données de test, une par une. Une fois que chaque prévision a été effectuée pour un intervalle de temps dans l'ensemble de données de test, la véritable observation de la prévision est ajoutée à l'ensemble de données de test et mise à la disposition du modèle.

Des modèles plus simples peuvent être réajustés avec l'observation avant de faire la prédiction ultérieure. Les modèles plus complexes, tels que les réseaux neuronaux, ne sont pas réaménagés étant donné leur coût de calcul beaucoup plus élevé.

Néanmoins, la véritable observation pour le pas de temps peut ensuite être utilisée comme entrée pour faire la prédiction sur le pas de temps suivant.

Tout d’abord, l’ensemble de données est divisé en ensembles d’entraînement et de test. Nous appellerons la fonction train_test_split() pour effectuer cette division et transmettre le nombre prédéfini d'observations à utiliser comme données de test.

Un modèle sera ajusté une fois sur l'ensemble de données d'entraînement pour une configuration donnée.

Nous définirons une fonction générique model_fit() pour effectuer cette opération qui peut être renseignée pour le type donné de réseau neuronal qui pourrait nous intéresser plus tard. La fonction prend l'ensemble de données d'entraînement et la configuration du modèle et renvoie le modèle d'ajustement prêt à faire des prédictions.

# fit a model
def model_fit(train, config):
	return None

Chaque pas de temps de l'ensemble de données de test est énuméré. Une prédiction est effectuée à l'aide du modèle d'ajustement.

Encore une fois, nous définirons une fonction générique nommée model_predict() qui prend le modèle d'ajustement, l'historique et la configuration du modèle et effectue une seule prédiction en une seule étape.

# forecast with a pre-fit model
def model_predict(model, history, config):
	return 0.0

La prédiction est ajoutée à une liste de prédictions et la véritable observation de l'ensemble de test est ajoutée à une liste d'observations qui a été amorcée avec toutes les observations de l'ensemble de données d'entraînement. Cette liste est construite à chaque étape de la validation progressive, permettant au modèle de faire une prédiction en une étape en utilisant l'historique le plus récent.

Toutes les prédictions peuvent ensuite être comparées aux valeurs réelles de l'ensemble de test et une mesure d'erreur calculée.

Nous calculerons l’erreur quadratique moyenne, ou RMSE, entre les prédictions et les valeurs vraies.

Le RMSE est calculé comme la racine carrée de la moyenne des carrés des différences entre les prévisions et les valeurs réelles. measure_rmse() implémente ceci ci-dessous en utilisant la fonction scikit-learn Mean_squared_error() pour calculer d'abord l'erreur quadratique moyenne, ou MSE, avant de calculer la racine carrée.

# root mean squared error or rmse
def measure_rmse(actual, predicted):
	return sqrt(mean_squared_error(actual, predicted))

La fonction walk_forward_validation() complète qui relie tout cela est répertoriée ci-dessous.

Il prend l'ensemble de données, le nombre d'observations à utiliser comme ensemble de test et la configuration du modèle, et renvoie le RMSE pour les performances du modèle sur l'ensemble de test.

# walk-forward validation for univariate data
def walk_forward_validation(data, n_test, cfg):
	predictions = list()
	# split dataset
	train, test = train_test_split(data, n_test)
	# fit model
	model = model_fit(train, cfg)
	# seed history with training dataset
	history = [x for x in train]
	# step over each time-step in the test set
	for i in range(len(test)):
		# fit model and make forecast for history
		yhat = model_predict(model, history, cfg)
		# store forecast in list of predictions
		predictions.append(yhat)
		# add actual observation to history for the next loop
		history.append(test[i])
	# estimate prediction error
	error = measure_rmse(test, predictions)
	print(' > %.3f' % error)
	return error

Répéter l'évaluation

Les modèles de réseaux neuronaux sont stochastiques.

Cela signifie que, étant donné la même configuration de modèle et le même ensemble de données d'entraînement, un ensemble interne différent de poids résultera à chaque fois que le modèle est entraîné, ce qui aura à son tour des performances différentes.

C'est un avantage, permettant au modèle d'être adaptatif et de trouver des configurations très performantes pour des problèmes complexes.

C'est également un problème lors de l'évaluation des performances d'un modèle et du choix d'un modèle final à utiliser pour faire des prédictions.

Pour aborder l'évaluation du modèle, nous évaluerons une configuration de modèle plusieurs fois via une validation progressive et signalerons l'erreur comme l'erreur moyenne pour chaque évaluation.

Cela n'est pas toujours possible pour les grands réseaux de neurones et n'a de sens que pour les petits réseaux qui peuvent être installés en quelques minutes ou heures.

La fonction repeat_evaluate() ci-dessous implémente cela et permet de spécifier le nombre de répétitions en tant que paramètre facultatif dont la valeur par défaut est 30 et renvoie une liste des scores de performances du modèle : dans ce cas, les valeurs RMSE.

# repeat evaluation of a config
def repeat_evaluate(data, config, n_test, n_repeats=30):
	# fit and evaluate the model n times
	scores = [walk_forward_validation(data, n_test, config) for _ in range(n_repeats)]
	return scores

Résumer les performances

Enfin, nous devons résumer les performances d'un modèle à partir de plusieurs répétitions.

Nous résumerons d’abord les performances à l’aide de statistiques récapitulatives, notamment la moyenne et l’écart type.

Nous tracerons également la distribution des scores de performance du modèle à l'aide d'un diagramme en boîte et en moustaches pour aider à avoir une idée de la répartition des performances.

La fonction summarize_scores() ci-dessous implémente cela, en prenant le nom du modèle qui a été évalué et la liste des scores de chaque évaluation répétée, en imprimant le résumé et en affichant un graphique.

# summarize model performance
def summarize_scores(name, scores):
	# print a summary
	scores_m, score_std = mean(scores), std(scores)
	print('%s: %.3f RMSE (+/- %.3f)' % (name, scores_m, score_std))
	# box and whisker plot
	pyplot.boxplot(scores)
	pyplot.show()

Exemple travaillé

Maintenant que nous avons défini les éléments du harnais de test, nous pouvons les relier tous ensemble et définir un modèle de persistance simple.

Plus précisément, nous calculerons la médiane d'un sous-ensemble d'observations antérieures par rapport au temps à prévoir.

Nous n'avons pas besoin d'ajuster un modèle, donc la fonction model_fit() sera implémentée pour renvoyer simplement Aucun.

# fit a model
def model_fit(train, config):
	return None

Nous utiliserons la configuration pour définir une liste de décalages d'index dans les observations précédentes par rapport au temps à prévoir qui sera utilisé comme prédiction. Par exemple, 12 utilisera l'observation d'il y a 12 mois (-12) par rapport à l'heure à prévoir.

# define config
config = [12, 24, 36]

La fonction model_predict() peut être implémentée pour utiliser cette configuration pour collecter les observations, puis renvoyer la médiane de ces observations.

# forecast with a pre-fit model
def model_predict(model, history, config):
	values = list()
	for offset in config:
		values.append(history[-offset])
	return median(values)

L'exemple complet d'utilisation du framework avec un modèle de persistance simple est répertorié ci-dessous.

# persistence
from math import sqrt
from numpy import mean
from numpy import std
from pandas import DataFrame
from pandas import concat
from pandas import read_csv
from sklearn.metrics import mean_squared_error
from matplotlib import pyplot

# split a univariate dataset into train/test sets
def train_test_split(data, n_test):
	return data[:-n_test], data[-n_test:]

# transform list into supervised learning format
def series_to_supervised(data, n_in=1, n_out=1):
	df = DataFrame(data)
	cols = list()
	# input sequence (t-n, ... t-1)
	for i in range(n_in, 0, -1):
		cols.append(df.shift(i))
	# forecast sequence (t, t+1, ... t+n)
	for i in range(0, n_out):
		cols.append(df.shift(-i))
	# put it all together
	agg = concat(cols, axis=1)
	# drop rows with NaN values
	agg.dropna(inplace=True)
	return agg.values

# root mean squared error or rmse
def measure_rmse(actual, predicted):
	return sqrt(mean_squared_error(actual, predicted))

# difference dataset
def difference(data, interval):
	return [data[i] - data[i - interval] for i in range(interval, len(data))]

# fit a model
def model_fit(train, config):
	return None

# forecast with a pre-fit model
def model_predict(model, history, config):
	values = list()
	for offset in config:
		values.append(history[-offset])
	return median(values)

# walk-forward validation for univariate data
def walk_forward_validation(data, n_test, cfg):
	predictions = list()
	# split dataset
	train, test = train_test_split(data, n_test)
	# fit model
	model = model_fit(train, cfg)
	# seed history with training dataset
	history = [x for x in train]
	# step over each time-step in the test set
	for i in range(len(test)):
		# fit model and make forecast for history
		yhat = model_predict(model, history, cfg)
		# store forecast in list of predictions
		predictions.append(yhat)
		# add actual observation to history for the next loop
		history.append(test[i])
	# estimate prediction error
	error = measure_rmse(test, predictions)
	print(' > %.3f' % error)
	return error

# repeat evaluation of a config
def repeat_evaluate(data, config, n_test, n_repeats=30):
	# fit and evaluate the model n times
	scores = [walk_forward_validation(data, n_test, config) for _ in range(n_repeats)]
	return scores

# summarize model performance
def summarize_scores(name, scores):
	# print a summary
	scores_m, score_std = mean(scores), std(scores)
	print('%s: %.3f RMSE (+/- %.3f)' % (name, scores_m, score_std))
	# box and whisker plot
	pyplot.boxplot(scores)
	pyplot.show()

series = read_csv('monthly-car-sales.csv', header=0, index_col=0)
data = series.values
# data split
n_test = 12
# define config
config = [12, 24, 36]
# grid search
scores = repeat_evaluate(data, config, n_test)
# summarize scores
summarize_scores('persistence', scores)

L'exécution de l'exemple imprime le RMSE du modèle évalué à l'aide de la validation progressive sur les 12 derniers mois de données.

Le modèle est évalué 30 fois, mais comme le modèle ne comporte aucun élément stochastique, le score est le même à chaque fois.

 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
 > 1841.156
persistence: 1841.156 RMSE (+/- 0.000)

Nous pouvons voir que le RMSE du modèle est de 1841, fournissant une limite inférieure de performance par laquelle nous pouvons évaluer si un modèle est compétent ou non sur le problème.

Maintenant que nous disposons d’un harnais de test robuste, nous pouvons l’utiliser pour évaluer une suite de modèles de réseaux neuronaux.

Modèle Perceptron multicouche

Le premier réseau que nous évaluerons est un Perceptron multicouche, ou MLP en abrégé.

Il s’agit d’un modèle simple de réseau neuronal à action directe qui doit être évalué avant d’envisager des modèles plus élaborés.

Les MLP peuvent être utilisés pour la prévision de séries chronologiques en prenant plusieurs observations à des pas de temps antérieurs, appelés observations de décalage, et en les utilisant comme caractéristiques d'entrée et en prédisant un ou plusieurs pas de temps à partir de ces observations.

C'est exactement le cadre du problème fourni par la fonction series_to_supervised() dans la section précédente.

L'ensemble de données de formation est donc une liste d'échantillons, où chaque échantillon contient un certain nombre d'observations datant des mois précédant l'heure prévue, et la prévision porte sur le mois suivant dans la séquence. Par exemple:

X, 							y
month1, month2, month3,		month4
month2, month3, month4,		month5
month3, month4, month5,		month6
...

Le modèle tentera de généraliser sur ces échantillons, de sorte que lorsqu'un nouvel échantillon est fourni au-delà de ce qui est connu par le modèle, il puisse prédire quelque chose d'utile ; Par exemple:

X, 							y
month4, month5, month6,		???

Nous allons implémenter un MLP simple en utilisant la bibliothèque d'apprentissage profond Keras.

Le modèle aura une couche d'entrée avec un certain nombre d'observations antérieures. Cela peut être spécifié en utilisant l'argument input_dim lorsque nous définissons la première couche cachée. Le modèle aura une seule couche cachée avec un certain nombre de nœuds, puis une seule couche de sortie.

Nous utiliserons la fonction d'activation linéaire rectifiée sur la couche cachée car elle fonctionne bien. Nous utiliserons une fonction d'activation linéaire (la valeur par défaut) sur la couche de sortie car nous prédisons une valeur continue.

La fonction de perte pour le réseau sera la perte d'erreur quadratique moyenne, ou MSE, et nous utiliserons la version efficace d'Adam de descente de gradient stochastique pour entraîner le réseau.

# define model
model = Sequential()
model.add(Dense(n_nodes, activation='relu', input_dim=n_input))
model.add(Dense(1))
model.compile(loss='mse', optimizer='adam')

Le modèle sera adapté à un certain nombre d'époques de formation (expositions aux données de formation) et la taille du lot peut être spécifiée pour définir la fréquence à laquelle les poids sont mis à jour au sein de chaque époque.

La fonction model_fit() permettant d'ajuster un modèle MLP sur l'ensemble de données d'entraînement est répertoriée ci-dessous.

La fonction s'attend à ce que la configuration soit une liste avec les hyperparamètres de configuration suivants :

  • n_input : nombre d'observations de décalage à utiliser comme entrée dans le modèle.
  • n_nodes : le nombre de nœuds à utiliser dans la couche cachée.
  • n_epochs : nombre de fois où exposer le modèle à l'ensemble de données d'entraînement complet.
  • n_batch : nombre d'échantillons au sein d'une époque après lequel les poids sont mis à jour.
# fit a model
def model_fit(train, config):
	# unpack config
	n_input, n_nodes, n_epochs, n_batch = config
	# prepare data
	data = series_to_supervised(train, n_in=n_input)
	train_x, train_y = data[:, :-1], data[:, -1]
	# define model
	model = Sequential()
	model.add(Dense(n_nodes, activation='relu', input_dim=n_input))
	model.add(Dense(1))
	model.compile(loss='mse', optimizer='adam')
	# fit
	model.fit(train_x, train_y, epochs=n_epochs, batch_size=n_batch, verbose=0)
	return model

Faire une prédiction avec un modèle MLP adapté est aussi simple que d'appeler la fonction predict() et de transmettre un échantillon des valeurs d'entrée requises pour effectuer la prédiction.

yhat = model.predict(x_input, verbose=0)

Afin de faire une prédiction au-delà de la limite des données connues, cela nécessite que les n dernières observations connues soient prises sous forme de tableau et utilisées comme entrée.

La fonction predict() attend un ou plusieurs échantillons d'entrées lors d'une prédiction, donc fournir un seul échantillon nécessite que le tableau ait la forme [1, n_input], où n_input est le nombre de pas de temps que le modèle attend en entrée.

De même, la fonction predict() renvoie un tableau de prédictions, une pour chaque échantillon fourni en entrée. Dans le cas d’une prédiction, il y aura un tableau avec une seule valeur.

La fonction model_predict() ci-dessous implémente ce comportement, en prenant le modèle, les observations antérieures et la configuration du modèle comme arguments, en formulant un échantillon d'entrée et en effectuant une prédiction en une étape qui est ensuite renvoyée.

# forecast with a pre-fit model
def model_predict(model, history, config):
	# unpack config
	n_input, _, _, _ = config
	# prepare data
	x_input = array(history[-n_input:]).reshape(1, n_input)
	# forecast
	yhat = model.predict(x_input, verbose=0)
	return yhat[0]

Nous disposons désormais de tout ce dont nous avons besoin pour évaluer un modèle MLP sur l’ensemble de données mensuelles de ventes de voitures.

Une simple recherche de grille d'hyperparamètres du modèle a été effectuée et la configuration ci-dessous a été choisie. Ce n’est peut-être pas une configuration optimale, mais c’est la meilleure qui ait été trouvée.

  • n_input : 24 (par exemple 24 mois)
  • n_nodes : 500
  • n_époques : 100
  • n_batch : 100

Cette configuration peut être définie sous forme de liste :

# define config
config = [24, 500, 100, 100]

Notez que lorsque les données d'entraînement sont présentées comme un problème d'apprentissage supervisé, seuls 72 échantillons peuvent être utilisés pour entraîner le modèle.

L’utilisation d’une taille de lot de 72 ou plus signifie que le modèle est entraîné à l’aide d’une descente de gradient par lots au lieu d’une descente de gradient par mini-lots. Ceci est souvent utilisé pour de petits ensembles de données et signifie que les mises à jour de poids et les calculs de gradient sont effectués à la fin de chaque époque, au lieu de plusieurs fois au cours de chaque époque.

L’exemple de code complet est répertorié ci-dessous.

# evaluate mlp
from math import sqrt
from numpy import array
from numpy import mean
from numpy import std
from pandas import DataFrame
from pandas import concat
from pandas import read_csv
from sklearn.metrics import mean_squared_error
from keras.models import Sequential
from keras.layers import Dense
from matplotlib import pyplot

# split a univariate dataset into train/test sets
def train_test_split(data, n_test):
	return data[:-n_test], data[-n_test:]

# transform list into supervised learning format
def series_to_supervised(data, n_in=1, n_out=1):
	df = DataFrame(data)
	cols = list()
	# input sequence (t-n, ... t-1)
	for i in range(n_in, 0, -1):
		cols.append(df.shift(i))
	# forecast sequence (t, t+1, ... t+n)
	for i in range(0, n_out):
		cols.append(df.shift(-i))
	# put it all together
	agg = concat(cols, axis=1)
	# drop rows with NaN values
	agg.dropna(inplace=True)
	return agg.values

# root mean squared error or rmse
def measure_rmse(actual, predicted):
	return sqrt(mean_squared_error(actual, predicted))

# fit a model
def model_fit(train, config):
	# unpack config
	n_input, n_nodes, n_epochs, n_batch = config
	# prepare data
	data = series_to_supervised(train, n_in=n_input)
	train_x, train_y = data[:, :-1], data[:, -1]
	# define model
	model = Sequential()
	model.add(Dense(n_nodes, activation='relu', input_dim=n_input))
	model.add(Dense(1))
	model.compile(loss='mse', optimizer='adam')
	# fit
	model.fit(train_x, train_y, epochs=n_epochs, batch_size=n_batch, verbose=0)
	return model

# forecast with a pre-fit model
def model_predict(model, history, config):
	# unpack config
	n_input, _, _, _ = config
	# prepare data
	x_input = array(history[-n_input:]).reshape(1, n_input)
	# forecast
	yhat = model.predict(x_input, verbose=0)
	return yhat[0]

# walk-forward validation for univariate data
def walk_forward_validation(data, n_test, cfg):
	predictions = list()
	# split dataset
	train, test = train_test_split(data, n_test)
	# fit model
	model = model_fit(train, cfg)
	# seed history with training dataset
	history = [x for x in train]
	# step over each time-step in the test set
	for i in range(len(test)):
		# fit model and make forecast for history
		yhat = model_predict(model, history, cfg)
		# store forecast in list of predictions
		predictions.append(yhat)
		# add actual observation to history for the next loop
		history.append(test[i])
	# estimate prediction error
	error = measure_rmse(test, predictions)
	print(' > %.3f' % error)
	return error

# repeat evaluation of a config
def repeat_evaluate(data, config, n_test, n_repeats=30):
	# fit and evaluate the model n times
	scores = [walk_forward_validation(data, n_test, config) for _ in range(n_repeats)]
	return scores

# summarize model performance
def summarize_scores(name, scores):
	# print a summary
	scores_m, score_std = mean(scores), std(scores)
	print('%s: %.3f RMSE (+/- %.3f)' % (name, scores_m, score_std))
	# box and whisker plot
	pyplot.boxplot(scores)
	pyplot.show()

series = read_csv('monthly-car-sales.csv', header=0, index_col=0)
data = series.values
# data split
n_test = 12
# define config
config = [24, 500, 100, 100]
# grid search
scores = repeat_evaluate(data, config, n_test)
# summarize scores
summarize_scores('mlp', scores)

L’exécution de l’exemple imprime le RMSE pour chacune des 30 évaluations répétées du modèle.

Remarque : Vos résultats peuvent varier en raison de la nature stochastique de l'algorithme ou de la procédure d'évaluation, ou des différences de précision numérique. Pensez à exécuter l’exemple plusieurs fois et comparez le résultat moyen.

À la fin de l’analyse, la moyenne et l’écart type RMSE sont rapportés pour environ 1 526 ventes.

Nous pouvons constater qu'en moyenne, la configuration choisie a de meilleures performances que le modèle naïf (1841.155) et le modèle SARIMA (1551.842).

C’est impressionnant étant donné que le modèle fonctionnait directement sur les données brutes, sans mise à l’échelle ou sans rendre les données stationnaires.

 > 1629.203
 > 1642.219
 > 1472.483
 > 1662.055
 > 1452.480
 > 1465.535
 > 1116.253
 > 1682.667
 > 1642.626
 > 1700.183
 > 1444.481
 > 1673.217
 > 1602.342
 > 1655.895
 > 1319.387
 > 1591.972
 > 1592.574
 > 1361.607
 > 1450.348
 > 1314.529
 > 1549.505
 > 1569.750
 > 1427.897
 > 1478.926
 > 1474.990
 > 1458.993
 > 1643.383
 > 1457.925
 > 1558.934
 > 1708.278
mlp: 1526.688 RMSE (+/- 134.789)

Un diagramme en boîte et moustaches des scores RMSE est créé pour résumer la répartition des performances du modèle.

Cela permet de comprendre la répartition des scores. Nous pouvons constater que même si les performances moyennes du modèle sont impressionnantes, l’écart est important. L'écart type est d'un peu plus de 134 ventes, ce qui signifie qu'un modèle dans le pire des cas, avec une erreur de 2 ou 3 écarts types par rapport à l'erreur moyenne, peut être pire que le modèle naïf.

L’un des défis liés à l’utilisation du modèle MLP consiste à exploiter les compétences les plus élevées et à minimiser la variance du modèle sur plusieurs exécutions.

Ce problème s'applique généralement aux réseaux de neurones. Il existe de nombreuses stratégies que vous pouvez utiliser, mais la plus simple consiste peut-être simplement à former plusieurs modèles finaux sur toutes les données disponibles et à les utiliser dans un ensemble lors de la réalisation de prédictions, par ex. la prédiction est la moyenne de 10 à 30 modèles.

Modèle de réseau neuronal convolutif

Les réseaux de neurones convolutifs, ou CNN, sont un type de réseau neuronal développé pour les données d'images bidimensionnelles, bien qu'ils puissent être utilisés pour des données unidimensionnelles telles que des séquences de texte et des séries chronologiques.

Lorsqu'il opère sur des données unidimensionnelles, le CNN lit une séquence d'observations décalées et apprend à extraire les caractéristiques pertinentes pour faire une prédiction.

Nous définirons un CNN avec deux couches convolutives pour extraire les fonctionnalités des séquences d'entrée. Chacun aura un nombre configurable de filtres et une taille de noyau et utilisera la fonction d'activation linéaire rectifiée. Le nombre de filtres détermine le nombre de champs parallèles sur lesquels les entrées pondérées sont lues et projetées. La taille du noyau définit le nombre de pas de temps lus dans chaque instantané au fur et à mesure que le réseau lit la séquence d'entrée.

model.add(Conv1D(filters=n_filters, kernel_size=n_kernel, activation='relu', input_shape=(n_input, 1)))
model.add(Conv1D(filters=n_filters, kernel_size=n_kernel, activation='relu'))

Une couche de pooling maximale est utilisée après les couches convolutives pour distiller les caractéristiques d'entrée pondérées dans celles qui sont les plus saillantes, réduisant ainsi la taille d'entrée de 1/4. Les entrées regroupées sont aplaties en un long vecteur avant d'être interprétées et utilisées pour effectuer une prédiction en une étape.

model.add(MaxPooling1D(pool_size=2))
model.add(Flatten())
model.add(Dense(1))

Le modèle CNN s'attend à ce que les données d'entrée se présentent sous la forme de plusieurs échantillons, chaque échantillon comportant plusieurs pas de temps d'entrée, identiques à ceux du MLP de la section précédente.

Une différence est que le CNN peut prendre en charge plusieurs caractéristiques ou types d'observations à chaque pas de temps, qui sont interprétés comme des canaux d'une image. Nous n'avons qu'une seule fonctionnalité à chaque pas de temps, donc la forme tridimensionnelle requise des données d'entrée sera [n_samples, n_input, 1].

train_x = train_x.reshape((train_x.shape[0], train_x.shape[1], 1))

La fonction model_fit() permettant d'ajuster le modèle CNN sur l'ensemble de données d'entraînement est répertoriée ci-dessous.

Le modèle prend les cinq paramètres de configuration suivants sous forme de liste :

  • n_input : nombre d'observations de décalage à utiliser comme entrée dans le modèle.
  • n_filters : nombre de filtres parallèles.
  • n_kernel : nombre de pas de temps pris en compte dans chaque lecture de la séquence d'entrée.
  • n_epochs : nombre de fois où exposer le modèle à l'ensemble de données d'entraînement.
  • n_batch : nombre d'échantillons au sein d'une époque après lequel les poids sont mis à jour.
# fit a model
def model_fit(train, config):
	# unpack config
	n_input, n_filters, n_kernel, n_epochs, n_batch = config
	# prepare data
	data = series_to_supervised(train, n_in=n_input)
	train_x, train_y = data[:, :-1], data[:, -1]
	train_x = train_x.reshape((train_x.shape[0], train_x.shape[1], 1))
	# define model
	model = Sequential()
	model.add(Conv1D(filters=n_filters, kernel_size=n_kernel, activation='relu', input_shape=(n_input, 1)))
	model.add(Conv1D(filters=n_filters, kernel_size=n_kernel, activation='relu'))
	model.add(MaxPooling1D(pool_size=2))
	model.add(Flatten())
	model.add(Dense(1))
	model.compile(loss='mse', optimizer='adam')
	# fit
	model.fit(train_x, train_y, epochs=n_epochs, batch_size=n_batch, verbose=0)
	return model

Faire une prédiction avec le modèle fit CNN revient beaucoup à faire une prédiction avec le modèle fit MLP dans la section précédente.

La seule différence réside dans l'exigence que nous spécifions le nombre d'entités observées à chaque pas de temps, qui dans ce cas est 1. Par conséquent, lors d'une seule prédiction en une seule étape, la forme du tableau d'entrée doit être :

[1, n_input, 1]

La fonction model_predict() ci-dessous implémente ce comportement.

# forecast with a pre-fit model
def model_predict(model, history, config):
	# unpack config
	n_input, _, _, _, _ = config
	# prepare data
	x_input = array(history[-n_input:]).reshape((1, n_input, 1))
	# forecast
	yhat = model.predict(x_input, verbose=0)
	return yhat[0]

Une simple recherche de grille d'hyperparamètres du modèle a été effectuée et la configuration ci-dessous a été choisie. Ce n’est pas une configuration optimale, mais c’est la meilleure qui ait été trouvée.

La configuration choisie est la suivante :

  • n_input : 36 (par exemple 3 ans ou 3 * 12)
  • n_filters : 256
  • n_kernel : 3
  • n_époques : 100
  • n_batch : 100 (par exemple, descente de gradient par lots)

Cela peut être spécifié sous forme de liste comme suit :

# define config
config = [36, 256, 3, 100, 100]

En reliant tout cela ensemble, l’exemple complet est répertorié ci-dessous.

# evaluate cnn
from math import sqrt
from numpy import array
from numpy import mean
from numpy import std
from pandas import DataFrame
from pandas import concat
from pandas import read_csv
from sklearn.metrics import mean_squared_error
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers.convolutional import Conv1D
from keras.layers.convolutional import MaxPooling1D
from matplotlib import pyplot

# split a univariate dataset into train/test sets
def train_test_split(data, n_test):
	return data[:-n_test], data[-n_test:]

# transform list into supervised learning format
def series_to_supervised(data, n_in=1, n_out=1):
	df = DataFrame(data)
	cols = list()
	# input sequence (t-n, ... t-1)
	for i in range(n_in, 0, -1):
		cols.append(df.shift(i))
	# forecast sequence (t, t+1, ... t+n)
	for i in range(0, n_out):
		cols.append(df.shift(-i))
	# put it all together
	agg = concat(cols, axis=1)
	# drop rows with NaN values
	agg.dropna(inplace=True)
	return agg.values

# root mean squared error or rmse
def measure_rmse(actual, predicted):
	return sqrt(mean_squared_error(actual, predicted))

# fit a model
def model_fit(train, config):
	# unpack config
	n_input, n_filters, n_kernel, n_epochs, n_batch = config
	# prepare data
	data = series_to_supervised(train, n_in=n_input)
	train_x, train_y = data[:, :-1], data[:, -1]
	train_x = train_x.reshape((train_x.shape[0], train_x.shape[1], 1))
	# define model
	model = Sequential()
	model.add(Conv1D(filters=n_filters, kernel_size=n_kernel, activation='relu', input_shape=(n_input, 1)))
	model.add(Conv1D(filters=n_filters, kernel_size=n_kernel, activation='relu'))
	model.add(MaxPooling1D(pool_size=2))
	model.add(Flatten())
	model.add(Dense(1))
	model.compile(loss='mse', optimizer='adam')
	# fit
	model.fit(train_x, train_y, epochs=n_epochs, batch_size=n_batch, verbose=0)
	return model

# forecast with a pre-fit model
def model_predict(model, history, config):
	# unpack config
	n_input, _, _, _, _ = config
	# prepare data
	x_input = array(history[-n_input:]).reshape((1, n_input, 1))
	# forecast
	yhat = model.predict(x_input, verbose=0)
	return yhat[0]

# walk-forward validation for univariate data
def walk_forward_validation(data, n_test, cfg):
	predictions = list()
	# split dataset
	train, test = train_test_split(data, n_test)
	# fit model
	model = model_fit(train, cfg)
	# seed history with training dataset
	history = [x for x in train]
	# step over each time-step in the test set
	for i in range(len(test)):
		# fit model and make forecast for history
		yhat = model_predict(model, history, cfg)
		# store forecast in list of predictions
		predictions.append(yhat)
		# add actual observation to history for the next loop
		history.append(test[i])
	# estimate prediction error
	error = measure_rmse(test, predictions)
	print(' > %.3f' % error)
	return error

# repeat evaluation of a config
def repeat_evaluate(data, config, n_test, n_repeats=30):
	# fit and evaluate the model n times
	scores = [walk_forward_validation(data, n_test, config) for _ in range(n_repeats)]
	return scores

# summarize model performance
def summarize_scores(name, scores):
	# print a summary
	scores_m, score_std = mean(scores), std(scores)
	print('%s: %.3f RMSE (+/- %.3f)' % (name, scores_m, score_std))
	# box and whisker plot
	pyplot.boxplot(scores)
	pyplot.show()

series = read_csv('monthly-car-sales.csv', header=0, index_col=0)
data = series.values
# data split
n_test = 12
# define config
config = [36, 256, 3, 100, 100]
# grid search
scores = repeat_evaluate(data, config, n_test)
# summarize scores
summarize_scores('cnn', scores)

L’exécution de l’exemple imprime d’abord le RMSE pour chaque évaluation répétée du modèle.

Remarque : Vos résultats peuvent varier en raison de la nature stochastique de l'algorithme ou de la procédure d'évaluation, ou des différences de précision numérique. Pensez à exécuter l’exemple plusieurs fois et comparez le résultat moyen.

À la fin de l'analyse, nous pouvons voir qu'en effet le modèle est habile, atteignant un RMSE moyen de 1 524,067, ce qui est meilleur que le modèle naïf, le modèle SARIMA et même le modèle MLP de la section précédente.

C’est impressionnant étant donné que le modèle fonctionnait directement sur les données brutes, sans mise à l’échelle ou sans rendre les données stationnaires.

L'écart type du score est important, soit environ 57 ventes, mais il représente 1/3 de la taille de l'écart observé avec le modèle MLP dans la section précédente. Nous sommes certains que dans un scénario défavorable (3 écarts-types), le RMSE du modèle restera inférieur (meilleur que) aux performances du modèle naïf.

> 1551.031
> 1495.743
> 1449.408
> 1526.017
> 1466.118
> 1566.535
> 1649.204
> 1455.782
> 1574.214
> 1541.790
> 1489.140
> 1506.035
> 1513.197
> 1530.714
> 1511.328
> 1471.518
> 1555.596
> 1552.026
> 1531.727
> 1472.978
> 1620.242
> 1424.153
> 1456.393
> 1581.114
> 1539.286
> 1489.795
> 1652.620
> 1537.349
> 1443.777
> 1567.179
cnn: 1524.067 RMSE (+/- 57.148)

Un diagramme en boîte et en moustaches des scores est créé pour aider à comprendre la propagation des erreurs au fil des exécutions.

Nous pouvons voir que l'écart semble être biaisé vers des valeurs d'erreur plus élevées, comme on pouvait s'y attendre, bien que la moustache supérieure du graphique (dans ce cas, l'erreur la plus importante qui ne soit pas des valeurs aberrantes) soit toujours limitée à un RMSE de 1 650 ventes. .

Modèles de réseaux neuronaux récurrents

Les réseaux de neurones récurrents, ou RNN, sont les types de réseaux de neurones qui utilisent une sortie du réseau d'une étape précédente comme entrée pour tenter d'apprendre automatiquement à travers les données de séquence.

Le réseau Long Short-Term Memory, ou LSTM, est un type de RNN dont la mise en œuvre résout les difficultés générales liées à la formation des RNN sur des données de séquence qui aboutissent à un modèle stable. Il y parvient en apprenant les poids des portes internes qui contrôlent les connexions récurrentes au sein de chaque nœud.

Bien que développés pour les données de séquence, les LSTM ne se sont pas révélés efficaces sur les problèmes de prévision de séries chronologiques où le résultat est fonction d'observations récentes, par ex. un problème de prévision de type autorégressif, tel que l'ensemble de données sur les ventes de voitures.

Néanmoins, nous pouvons développer des modèles LSTM pour les problèmes autorégressifs et les utiliser comme point de comparaison avec d'autres modèles de réseaux neuronaux.

Dans cette section, nous explorerons trois variantes du modèle LSTM pour la prévision de séries chronologiques univariées ; ils sont:

  • LSTM : le réseau LSTM tel quel.
  • CNN-LSTM : un réseau CNN qui apprend les caractéristiques d'entrée et un LSTM qui les interprète.
  • ConvLSTM : une combinaison de CNN et de LSTM dans laquelle les unités LSTM lisent les données d'entrée à l'aide du processus convolutif d'un CNN.

LSTM

Le réseau neuronal LSTM peut être utilisé pour la prévision de séries chronologiques univariées.

En tant que RNN, il lira chaque pas de temps d'une séquence d'entrée, une étape à la fois. Le LSTM dispose d'une mémoire interne lui permettant d'accumuler l'état interne lors de la lecture des étapes d'une séquence d'entrée donnée.

À la fin de la séquence, chaque nœud d'une couche d'unités LSTM cachées produira une valeur unique. Ce vecteur de valeurs résume ce que le LSTM a appris ou extrait de la séquence d'entrée. Cela peut être interprété par une couche entièrement connectée avant qu’une prédiction finale ne soit faite.

# define model
model = Sequential()
model.add(LSTM(n_nodes, activation='relu', input_shape=(n_input, 1)))
model.add(Dense(n_nodes, activation='relu'))
model.add(Dense(1))
model.compile(loss='mse', optimizer='adam')

Comme le CNN, le LSTM peut prendre en charge plusieurs variables ou fonctionnalités à chaque pas de temps. Comme l'ensemble de données sur les ventes de voitures n'a qu'une seule valeur à chaque pas de temps, nous pouvons la fixer à 1, à la fois lors de la définition de l'entrée sur le réseau dans l'argument input_shape [n_input, 1] et lors de la définition de l'entrée du réseau dans l'argument input_shape [n_input, 1] forme des échantillons d’entrée.

train_x = train_x.reshape((train_x.shape[0], train_x.shape[1], 1))

Contrairement au MLP et au CNN qui ne lisent pas les données de séquence une étape à la fois, le LSTM fonctionne mieux si les données sont stationnaires. Cela signifie que des opérations de différence sont effectuées pour supprimer la tendance et la structure saisonnière.

Dans le cas de l'ensemble de données sur les ventes de voitures, nous pouvons rendre les données stationnaires en effectuant un ajustement saisonnier, c'est-à-dire en soustrayant la valeur d'il y a un an de chaque observation.

adjusted = value - value[-12]

Cela peut être effectué systématiquement pour l’ensemble des données de formation. Cela signifie également que la première année d'observations doit être écartée car nous ne disposons d'aucune année de données antérieure avec laquelle les comparer.

La fonction difference() ci-dessous différenciera un ensemble de données fourni avec un décalage fourni, appelé ordre de différence, par ex. 12 pour un an ou des mois auparavant.

# difference dataset
def difference(data, interval):
	return [data[i] - data[i - interval] for i in range(interval, len(data))]

Nous pouvons faire de l'ordre de différence un hyperparamètre du modèle et effectuer l'opération uniquement si une valeur autre que zéro est fournie.

La fonction model_fit() pour ajuster un modèle LSTM est fournie ci-dessous.

Le modèle attend une liste de cinq hyperparamètres du modèle ; ils sont:

  • n_input : nombre d'observations de décalage à utiliser comme entrée dans le modèle.
  • n_nodes : nombre d'unités LSTM à utiliser dans la couche cachée.
  • n_epochs : nombre de fois où exposer le modèle à l'ensemble de données d'entraînement complet.
  • n_batch : nombre d'échantillons au sein d'une époque après lequel les poids sont mis à jour.
  • n_diff : l'ordre de différence ou 0 s'il n'est pas utilisé.
# fit a model
def model_fit(train, config):
	# unpack config
	n_input, n_nodes, n_epochs, n_batch, n_diff = config
	# prepare data
	if n_diff > 0:
		train = difference(train, n_diff)
	data = series_to_supervised(train, n_in=n_input)
	train_x, train_y = data[:, :-1], data[:, -1]
	train_x = train_x.reshape((train_x.shape[0], train_x.shape[1], 1))
	# define model
	model = Sequential()
	model.add(LSTM(n_nodes, activation='relu', input_shape=(n_input, 1)))
	model.add(Dense(n_nodes, activation='relu'))
	model.add(Dense(1))
	model.compile(loss='mse', optimizer='adam')
	# fit
	model.fit(train_x, train_y, epochs=n_epochs, batch_size=n_batch, verbose=0)
	return model

Faire une prédiction avec le modèle LSTM revient à faire une prédiction avec un modèle CNN.

Une seule entrée doit avoir la structure tridimensionnelle d'échantillons, de pas de temps et de fonctionnalités, qui dans ce cas nous n'avons qu'un seul échantillon et une seule fonctionnalité : [1, n_input, 1].

Si l'opération de différence a été effectuée, nous devons rajouter la valeur qui a été soustraite après que le modèle a fait une prévision. Nous devons également différencier les données historiques avant de formuler l’entrée unique utilisée pour faire une prédiction.

La fonction model_predict() ci-dessous implémente ce comportement.

# forecast with a pre-fit model
def model_predict(model, history, config):
	# unpack config
	n_input, _, _, _, n_diff = config
	# prepare data
	correction = 0.0
	if n_diff > 0:
		correction = history[-n_diff]
		history = difference(history, n_diff)
	x_input = array(history[-n_input:]).reshape((1, n_input, 1))
	# forecast
	yhat = model.predict(x_input, verbose=0)
	return correction + yhat[0]

Une simple recherche de grille d'hyperparamètres du modèle a été effectuée et la configuration ci-dessous a été choisie. Ce n’est pas une configuration optimale, mais c’est la meilleure qui ait été trouvée.

La configuration choisie est la suivante :

  • n_input : 36 (soit 3 ans ou 3 * 12)
  • n_nodes : 50
  • n_époques : 100
  • n_batch : 100 (c'est-à-dire descente de gradient par lots)
  • n_diff : 12 (c'est-à-dire différence saisonnière)

Cela peut être spécifié sous forme de liste :

# define config
config = [36, 50, 100, 100, 12]

En reliant tout cela ensemble, l’exemple complet est répertorié ci-dessous.

# evaluate lstm
from math import sqrt
from numpy import array
from numpy import mean
from numpy import std
from pandas import DataFrame
from pandas import concat
from pandas import read_csv
from sklearn.metrics import mean_squared_error
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from matplotlib import pyplot

# split a univariate dataset into train/test sets
def train_test_split(data, n_test):
	return data[:-n_test], data[-n_test:]

# transform list into supervised learning format
def series_to_supervised(data, n_in=1, n_out=1):
	df = DataFrame(data)
	cols = list()
	# input sequence (t-n, ... t-1)
	for i in range(n_in, 0, -1):
		cols.append(df.shift(i))
	# forecast sequence (t, t+1, ... t+n)
	for i in range(0, n_out):
		cols.append(df.shift(-i))
	# put it all together
	agg = concat(cols, axis=1)
	# drop rows with NaN values
	agg.dropna(inplace=True)
	return agg.values

# root mean squared error or rmse
def measure_rmse(actual, predicted):
	return sqrt(mean_squared_error(actual, predicted))

# difference dataset
def difference(data, interval):
	return [data[i] - data[i - interval] for i in range(interval, len(data))]

# fit a model
def model_fit(train, config):
	# unpack config
	n_input, n_nodes, n_epochs, n_batch, n_diff = config
	# prepare data
	if n_diff > 0:
		train = difference(train, n_diff)
	data = series_to_supervised(train, n_in=n_input)
	train_x, train_y = data[:, :-1], data[:, -1]
	train_x = train_x.reshape((train_x.shape[0], train_x.shape[1], 1))
	# define model
	model = Sequential()
	model.add(LSTM(n_nodes, activation='relu', input_shape=(n_input, 1)))
	model.add(Dense(n_nodes, activation='relu'))
	model.add(Dense(1))
	model.compile(loss='mse', optimizer='adam')
	# fit
	model.fit(train_x, train_y, epochs=n_epochs, batch_size=n_batch, verbose=0)
	return model

# forecast with a pre-fit model
def model_predict(model, history, config):
	# unpack config
	n_input, _, _, _, n_diff = config
	# prepare data
	correction = 0.0
	if n_diff > 0:
		correction = history[-n_diff]
		history = difference(history, n_diff)
	x_input = array(history[-n_input:]).reshape((1, n_input, 1))
	# forecast
	yhat = model.predict(x_input, verbose=0)
	return correction + yhat[0]

# walk-forward validation for univariate data
def walk_forward_validation(data, n_test, cfg):
	predictions = list()
	# split dataset
	train, test = train_test_split(data, n_test)
	# fit model
	model = model_fit(train, cfg)
	# seed history with training dataset
	history = [x for x in train]
	# step over each time-step in the test set
	for i in range(len(test)):
		# fit model and make forecast for history
		yhat = model_predict(model, history, cfg)
		# store forecast in list of predictions
		predictions.append(yhat)
		# add actual observation to history for the next loop
		history.append(test[i])
	# estimate prediction error
	error = measure_rmse(test, predictions)
	print(' > %.3f' % error)
	return error

# repeat evaluation of a config
def repeat_evaluate(data, config, n_test, n_repeats=30):
	# fit and evaluate the model n times
	scores = [walk_forward_validation(data, n_test, config) for _ in range(n_repeats)]
	return scores

# summarize model performance
def summarize_scores(name, scores):
	# print a summary
	scores_m, score_std = mean(scores), std(scores)
	print('%s: %.3f RMSE (+/- %.3f)' % (name, scores_m, score_std))
	# box and whisker plot
	pyplot.boxplot(scores)
	pyplot.show()

series = read_csv('monthly-car-sales.csv', header=0, index_col=0)
data = series.values
# data split
n_test = 12
# define config
config = [36, 50, 100, 100, 12]
# grid search
scores = repeat_evaluate(data, config, n_test)
# summarize scores
summarize_scores('lstm', scores)

En exécutant l'exemple, nous pouvons voir le RMSE pour chaque évaluation répétée du modèle.

Remarque : Vos résultats peuvent varier en raison de la nature stochastique de l'algorithme ou de la procédure d'évaluation, ou des différences de précision numérique. Pensez à exécuter l’exemple plusieurs fois et comparez le résultat moyen.

À la fin de l’analyse, nous pouvons constater que le RMSE moyen est d’environ 2 109, ce qui est pire que le modèle naïf. Cela suggère que le modèle choisi n’est pas habile et qu’il s’agit du meilleur que l’on puisse trouver compte tenu des mêmes ressources utilisées pour trouver les configurations de modèle dans les sections précédentes.

Cela fournit une preuve supplémentaire (bien que faible) que les LSTM, au moins seuls, ne conviennent peut-être pas aux problèmes de prédiction de séquence de type autorégressif.

> 2129.480
> 2169.109
> 2078.290
> 2257.222
> 2014.911
> 2197.283
> 2028.176
> 2110.718
> 2100.388
> 2157.271
> 1940.103
> 2086.588
> 1986.696
> 2168.784
> 2188.813
> 2086.759
> 2128.095
> 2126.467
> 2077.463
> 2057.679
> 2209.818
> 2067.082
> 1983.346
> 2157.749
> 2145.071
> 2266.130
> 2105.043
> 2128.549
> 1952.002
> 2188.287
lstm: 2109.779 RMSE (+/- 81.373)

Un diagramme en boîte et en moustaches est également créé résumant la distribution des scores RMSE.

Même le scénario de base du modèle n’a pas atteint les performances d’un modèle naïf.

CNN LSTM

Nous avons vu que le modèle CNN est capable d'apprendre et d'extraire automatiquement des caractéristiques des données de séquence brutes sans mise à l'échelle ni différence.

Nous pouvons combiner cette capacité avec le LSTM où un modèle CNN est appliqué à des sous-séquences de données d'entrée, dont les résultats forment ensemble une série chronologique de caractéristiques extraites qui peuvent être interprétées par un modèle LSTM.

Cette combinaison d'un modèle CNN utilisée pour lire plusieurs sous-séquences au fil du temps par un LSTM est appelée modèle CNN-LSTM.

Le modèle exige que chaque séquence d'entrée, par ex. 36 mois, est divisé en plusieurs sous-séquences, chacune lue par le modèle CNN, par ex. 3 sous-séquence de 12 pas de temps. Il peut être judicieux de diviser les sous-séquences par années, mais ce n'est qu'une hypothèse, et d'autres divisions pourraient être utilisées, comme six sous-séquences de six pas de temps. Par conséquent, ce fractionnement est paramétré avec les paramètres n_seq et n_steps pour le nombre de sous-séquences et le nombre d'étapes par sous-séquence.

train_x = train_x.reshape((train_x.shape[0], n_seq, n_steps, 1))

Le nombre d'observations de décalage par échantillon est simplement (n_seq * n_steps).

Il s'agit désormais d'un tableau d'entrée à 4 dimensions avec les dimensions :

[samples, subsequences, timesteps, features]

Le même modèle CNN doit être appliqué à chaque sous-séquence d'entrée.

Nous pouvons y parvenir en encapsulant l'intégralité du modèle CNN dans un wrapper de couche TimeDistributed.

model = Sequential()
model.add(TimeDistributed(Conv1D(filters=n_filters, kernel_size=n_kernel, activation='relu', input_shape=(None,n_steps,1))))
model.add(TimeDistributed(Conv1D(filters=n_filters, kernel_size=n_kernel, activation='relu')))
model.add(TimeDistributed(MaxPooling1D(pool_size=2)))
model.add(TimeDistributed(Flatten()))

La sortie d’une application du sous-modèle CNN sera un vecteur. La sortie du sous-modèle pour chaque sous-séquence d'entrée sera une série chronologique d'interprétations qui peuvent être interprétées par un modèle LSTM. Cela peut être suivi d'une couche entièrement connectée pour interpréter les résultats du LSTM et enfin d'une couche de sortie pour effectuer des prédictions en une étape.

model.add(LSTM(n_nodes, activation='relu'))
model.add(Dense(n_nodes, activation='relu'))
model.add(Dense(1))

La fonction complète model_fit() est répertoriée ci-dessous.

Le modèle attend une liste de sept hyperparamètres ; ils sont:

  • n_seq : nombre de sous-séquences dans un échantillon.
  • n_steps : nombre de pas de temps dans chaque sous-séquence.
  • n_filters : nombre de filtres parallèles.
  • n_kernel : nombre de pas de temps pris en compte dans chaque lecture de la séquence d'entrée.
  • n_nodes : nombre d'unités LSTM à utiliser dans la couche cachée.
  • n_epochs : nombre de fois où exposer le modèle à l'ensemble de données d'entraînement complet.
  • n_batch : nombre d'échantillons au sein d'une époque après lequel les poids sont mis à jour.
# fit a model
def model_fit(train, config):
	# unpack config
	n_seq, n_steps, n_filters, n_kernel, n_nodes, n_epochs, n_batch = config
	n_input = n_seq * n_steps
	# prepare data
	data = series_to_supervised(train, n_in=n_input)
	train_x, train_y = data[:, :-1], data[:, -1]
	train_x = train_x.reshape((train_x.shape[0], n_seq, n_steps, 1))
	# define model
	model = Sequential()
	model.add(TimeDistributed(Conv1D(filters=n_filters, kernel_size=n_kernel, activation='relu', input_shape=(None,n_steps,1))))
	model.add(TimeDistributed(Conv1D(filters=n_filters, kernel_size=n_kernel, activation='relu')))
	model.add(TimeDistributed(MaxPooling1D(pool_size=2)))
	model.add(TimeDistributed(Flatten()))
	model.add(LSTM(n_nodes, activation='relu'))
	model.add(Dense(n_nodes, activation='relu'))
	model.add(Dense(1))
	model.compile(loss='mse', optimizer='adam')
	# fit
	model.fit(train_x, train_y, epochs=n_epochs, batch_size=n_batch, verbose=0)
	return model

Faire une prédiction avec le modèle d'ajustement est à peu près la même chose qu'avec le LSTM ou le CNN, mais avec en plus la division de chaque échantillon en sous-séquences avec un nombre de pas de temps donné.

# prepare data
x_input = array(history[-n_input:]).reshape((1, n_seq, n_steps, 1))

La fonction model_predict() mise à jour est répertoriée ci-dessous.

# forecast with a pre-fit model
def model_predict(model, history, config):
	# unpack config
	n_seq, n_steps, _, _, _, _, _ = config
	n_input = n_seq * n_steps
	# prepare data
	x_input = array(history[-n_input:]).reshape((1, n_seq, n_steps, 1))
	# forecast
	yhat = model.predict(x_input, verbose=0)
	return yhat[0]

Une simple recherche de grille d'hyperparamètres du modèle a été effectuée et la configuration ci-dessous a été choisie. Ce n’est peut-être pas une configuration optimale, mais c’est la meilleure qui ait été trouvée.

  • n_seq : 3 (soit 3 ans)
  • n_steps : 12 (soit 1 an de mois)
  • n_filters : 64
  • n_kernel : 3
  • n_nodes : 100
  • n_époques : 200
  • n_batch : 100 (c'est-à-dire descente de gradient par lots)

Nous pouvons définir la configuration sous forme de liste ; Par exemple:

# define config
config = [3, 12, 64, 3, 100, 200, 100]

L'exemple complet d'évaluation du modèle CNN-LSTM pour la prévision des ventes mensuelles univariées de voitures est répertorié ci-dessous.

# evaluate cnn lstm
from math import sqrt
from numpy import array
from numpy import mean
from numpy import std
from pandas import DataFrame
from pandas import concat
from pandas import read_csv
from sklearn.metrics import mean_squared_error
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from keras.layers import TimeDistributed
from keras.layers import Flatten
from keras.layers.convolutional import Conv1D
from keras.layers.convolutional import MaxPooling1D
from matplotlib import pyplot

# split a univariate dataset into train/test sets
def train_test_split(data, n_test):
	return data[:-n_test], data[-n_test:]

# transform list into supervised learning format
def series_to_supervised(data, n_in=1, n_out=1):
	df = DataFrame(data)
	cols = list()
	# input sequence (t-n, ... t-1)
	for i in range(n_in, 0, -1):
		cols.append(df.shift(i))
	# forecast sequence (t, t+1, ... t+n)
	for i in range(0, n_out):
		cols.append(df.shift(-i))
	# put it all together
	agg = concat(cols, axis=1)
	# drop rows with NaN values
	agg.dropna(inplace=True)
	return agg.values

# root mean squared error or rmse
def measure_rmse(actual, predicted):
	return sqrt(mean_squared_error(actual, predicted))

# fit a model
def model_fit(train, config):
	# unpack config
	n_seq, n_steps, n_filters, n_kernel, n_nodes, n_epochs, n_batch = config
	n_input = n_seq * n_steps
	# prepare data
	data = series_to_supervised(train, n_in=n_input)
	train_x, train_y = data[:, :-1], data[:, -1]
	train_x = train_x.reshape((train_x.shape[0], n_seq, n_steps, 1))
	# define model
	model = Sequential()
	model.add(TimeDistributed(Conv1D(filters=n_filters, kernel_size=n_kernel, activation='relu', input_shape=(None,n_steps,1))))
	model.add(TimeDistributed(Conv1D(filters=n_filters, kernel_size=n_kernel, activation='relu')))
	model.add(TimeDistributed(MaxPooling1D(pool_size=2)))
	model.add(TimeDistributed(Flatten()))
	model.add(LSTM(n_nodes, activation='relu'))
	model.add(Dense(n_nodes, activation='relu'))
	model.add(Dense(1))
	model.compile(loss='mse', optimizer='adam')
	# fit
	model.fit(train_x, train_y, epochs=n_epochs, batch_size=n_batch, verbose=0)
	return model

# forecast with a pre-fit model
def model_predict(model, history, config):
	# unpack config
	n_seq, n_steps, _, _, _, _, _ = config
	n_input = n_seq * n_steps
	# prepare data
	x_input = array(history[-n_input:]).reshape((1, n_seq, n_steps, 1))
	# forecast
	yhat = model.predict(x_input, verbose=0)
	return yhat[0]

# walk-forward validation for univariate data
def walk_forward_validation(data, n_test, cfg):
	predictions = list()
	# split dataset
	train, test = train_test_split(data, n_test)
	# fit model
	model = model_fit(train, cfg)
	# seed history with training dataset
	history = [x for x in train]
	# step over each time-step in the test set
	for i in range(len(test)):
		# fit model and make forecast for history
		yhat = model_predict(model, history, cfg)
		# store forecast in list of predictions
		predictions.append(yhat)
		# add actual observation to history for the next loop
		history.append(test[i])
	# estimate prediction error
	error = measure_rmse(test, predictions)
	print(' > %.3f' % error)
	return error

# repeat evaluation of a config
def repeat_evaluate(data, config, n_test, n_repeats=30):
	# fit and evaluate the model n times
	scores = [walk_forward_validation(data, n_test, config) for _ in range(n_repeats)]
	return scores

# summarize model performance
def summarize_scores(name, scores):
	# print a summary
	scores_m, score_std = mean(scores), std(scores)
	print('%s: %.3f RMSE (+/- %.3f)' % (name, scores_m, score_std))
	# box and whisker plot
	pyplot.boxplot(scores)
	pyplot.show()

series = read_csv('monthly-car-sales.csv', header=0, index_col=0)
data = series.values
# data split
n_test = 12
# define config
config = [3, 12, 64, 3, 100, 200, 100]
# grid search
scores = repeat_evaluate(data, config, n_test)
# summarize scores
summarize_scores('cnn-lstm', scores)

L’exécution de l’exemple imprime le RMSE pour chaque évaluation répétée du modèle.

Remarque : Vos résultats peuvent varier en raison de la nature stochastique de l'algorithme ou de la procédure d'évaluation, ou des différences de précision numérique. Pensez à exécuter l’exemple plusieurs fois et comparez le résultat moyen.

Le RMSE moyen final est indiqué à la fin d'environ 1 626, ce qui est inférieur au modèle naïf, mais toujours supérieur à un modèle SARIMA. L'écart type de ce score est également très important, ce qui suggère que la configuration choisie n'est peut-être pas aussi stable que le modèle CNN autonome.

 > 1543.533
 > 1421.895
 > 1467.927
 > 1441.125
 > 1750.995
 > 1321.498
 > 1571.657
 > 1845.298
 > 1621.589
 > 1425.065
 > 1675.232
 > 1807.288
 > 2922.295
 > 1391.861
 > 1626.655
 > 1633.177
 > 1667.572
 > 1577.285
 > 1590.235
 > 1557.385
 > 1784.982
 > 1664.839
 > 1741.729
 > 1437.992
 > 1772.076
 > 1289.794
 > 1685.976
 > 1498.123
 > 1618.627
 > 1448.361
cnn-lstm: 1626.735 RMSE (+/- 279.850)

Un diagramme en boîte et moustaches est également créé résumant la distribution des scores RMSE.

Le graphique montre une seule valeur aberrante, soit une très mauvaise performance juste en dessous de 3 000 ventes.

ConvLSTM

Il est possible d'effectuer une opération convolutive dans le cadre de la lecture de la séquence d'entrée au sein de chaque unité LSTM.

Cela signifie que plutôt que de lire une séquence étape par étape, le LSTM lirait un bloc ou une sous-séquence d'observations à la fois en utilisant un processus convolutif, comme un CNN.

Ceci est différent de la première lecture d'une extraction de caractéristiques avec un LSTM et de l'interprétation du résultat avec un LSTM ; il s'agit d'effectuer l'opération CNN à chaque pas de temps dans le cadre du LSTM.

Ce type de modèle est appelé Convolutional LSTM, ou ConvLSTM en abrégé. Il est fourni dans Keras sous la forme d'une couche appelée ConvLSTM2D pour les données 2D. Nous pouvons le configurer pour une utilisation avec des données de séquence 1D en supposant que nous avons une ligne avec plusieurs colonnes.

Comme avec le CNN-LSTM, les données d'entrée sont divisées en sous-séquences où chaque sous-séquence a un nombre fixe de pas de temps, même si nous devons également spécifier le nombre de lignes dans chaque sous-séquence, qui dans ce cas est fixé à 1.

train_x = train_x.reshape((train_x.shape[0], n_seq, 1, n_steps, 1))

La forme est à cinq dimensions, avec les dimensions :

[samples, subsequences, rows, columns, features]

Comme le CNN, la couche ConvLSTM permet de préciser le nombre de cartes de filtres et la taille du noyau utilisé lors de la lecture des séquences d'entrée.

model.add(ConvLSTM2D(filters=n_filters, kernel_size=(1,n_kernel), activation='relu', input_shape=(n_seq, 1, n_steps, 1)))

La sortie de la couche est une séquence de cartes de filtres qui doivent d'abord être aplaties avant de pouvoir être interprétées et suivies par une couche de sortie.

Le modèle attend une liste de sept hyperparamètres, les mêmes que le CNN-LSTM ; ils sont:

  • n_seq : nombre de sous-séquences dans un échantillon.
  • n_steps : nombre de pas de temps dans chaque sous-séquence.
  • n_filters : nombre de filtres parallèles.
  • n_kernel : nombre de pas de temps pris en compte dans chaque lecture de la séquence d'entrée.
  • n_nodes : nombre d'unités LSTM à utiliser dans la couche cachée.
  • n_epochs : nombre de fois où exposer le modèle à l'ensemble de données d'entraînement complet.
  • n_batch : nombre d'échantillons au sein d'une époque après lequel les poids sont mis à jour.

La fonction model_fit() qui implémente tout cela est répertoriée ci-dessous.

# fit a model
def model_fit(train, config):
	# unpack config
	n_seq, n_steps, n_filters, n_kernel, n_nodes, n_epochs, n_batch = config
	n_input = n_seq * n_steps
	# prepare data
	data = series_to_supervised(train, n_in=n_input)
	train_x, train_y = data[:, :-1], data[:, -1]
	train_x = train_x.reshape((train_x.shape[0], n_seq, 1, n_steps, 1))
	# define model
	model = Sequential()
	model.add(ConvLSTM2D(filters=n_filters, kernel_size=(1,n_kernel), activation='relu', input_shape=(n_seq, 1, n_steps, 1)))
	model.add(Flatten())
	model.add(Dense(n_nodes, activation='relu'))
	model.add(Dense(1))
	model.compile(loss='mse', optimizer='adam')
	# fit
	model.fit(train_x, train_y, epochs=n_epochs, batch_size=n_batch, verbose=0)
	return model

Une prédiction est faite avec le modèle d'ajustement de la même manière que le CNN-LSTM, mais avec la dimension de lignes supplémentaire que nous fixons à 1.

# prepare data
x_input = array(history[-n_input:]).reshape((1, n_seq, 1, n_steps, 1))

La fonction model_predict() permettant d'effectuer une seule prédiction en une seule étape est répertoriée ci-dessous.

# forecast with a pre-fit model
def model_predict(model, history, config):
	# unpack config
	n_seq, n_steps, _, _, _, _, _ = config
	n_input = n_seq * n_steps
	# prepare data
	x_input = array(history[-n_input:]).reshape((1, n_seq, 1, n_steps, 1))
	# forecast
	yhat = model.predict(x_input, verbose=0)
	return yhat[0]

Une simple recherche de grille d'hyperparamètres du modèle a été effectuée et la configuration ci-dessous a été choisie.

Ce n’est peut-être pas une configuration optimale, mais c’est la meilleure qui ait été trouvée.

  • n_seq : 3 (soit 3 ans)
  • n_steps : 12 (soit 1 an de mois)
  • n_filters : 256
  • n_kernel : 3
  • n_nodes : 200
  • n_époques : 200
  • n_batch : 100 (c'est-à-dire descente de gradient par lots)

Nous pouvons définir la configuration sous forme de liste ; Par exemple:

# define config
config = [3, 12, 256, 3, 200, 200, 100]

Nous pouvons relier tout cela ensemble. La liste complète des codes du modèle ConvLSTM évalué pour la prévision en une étape de l'ensemble de données mensuelles sur les ventes de voitures est répertoriée ci-dessous.

# evaluate convlstm
from math import sqrt
from numpy import array
from numpy import mean
from numpy import std
from pandas import DataFrame
from pandas import concat
from pandas import read_csv
from sklearn.metrics import mean_squared_error
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers import ConvLSTM2D
from matplotlib import pyplot

# split a univariate dataset into train/test sets
def train_test_split(data, n_test):
	return data[:-n_test], data[-n_test:]

# transform list into supervised learning format
def series_to_supervised(data, n_in=1, n_out=1):
	df = DataFrame(data)
	cols = list()
	# input sequence (t-n, ... t-1)
	for i in range(n_in, 0, -1):
		cols.append(df.shift(i))
	# forecast sequence (t, t+1, ... t+n)
	for i in range(0, n_out):
		cols.append(df.shift(-i))
	# put it all together
	agg = concat(cols, axis=1)
	# drop rows with NaN values
	agg.dropna(inplace=True)
	return agg.values

# root mean squared error or rmse
def measure_rmse(actual, predicted):
	return sqrt(mean_squared_error(actual, predicted))

# difference dataset
def difference(data, interval):
	return [data[i] - data[i - interval] for i in range(interval, len(data))]

# fit a model
def model_fit(train, config):
	# unpack config
	n_seq, n_steps, n_filters, n_kernel, n_nodes, n_epochs, n_batch = config
	n_input = n_seq * n_steps
	# prepare data
	data = series_to_supervised(train, n_in=n_input)
	train_x, train_y = data[:, :-1], data[:, -1]
	train_x = train_x.reshape((train_x.shape[0], n_seq, 1, n_steps, 1))
	# define model
	model = Sequential()
	model.add(ConvLSTM2D(filters=n_filters, kernel_size=(1,n_kernel), activation='relu', input_shape=(n_seq, 1, n_steps, 1)))
	model.add(Flatten())
	model.add(Dense(n_nodes, activation='relu'))
	model.add(Dense(1))
	model.compile(loss='mse', optimizer='adam')
	# fit
	model.fit(train_x, train_y, epochs=n_epochs, batch_size=n_batch, verbose=0)
	return model

# forecast with a pre-fit model
def model_predict(model, history, config):
	# unpack config
	n_seq, n_steps, _, _, _, _, _ = config
	n_input = n_seq * n_steps
	# prepare data
	x_input = array(history[-n_input:]).reshape((1, n_seq, 1, n_steps, 1))
	# forecast
	yhat = model.predict(x_input, verbose=0)
	return yhat[0]

# walk-forward validation for univariate data
def walk_forward_validation(data, n_test, cfg):
	predictions = list()
	# split dataset
	train, test = train_test_split(data, n_test)
	# fit model
	model = model_fit(train, cfg)
	# seed history with training dataset
	history = [x for x in train]
	# step over each time-step in the test set
	for i in range(len(test)):
		# fit model and make forecast for history
		yhat = model_predict(model, history, cfg)
		# store forecast in list of predictions
		predictions.append(yhat)
		# add actual observation to history for the next loop
		history.append(test[i])
	# estimate prediction error
	error = measure_rmse(test, predictions)
	print(' > %.3f' % error)
	return error

# repeat evaluation of a config
def repeat_evaluate(data, config, n_test, n_repeats=30):
	# fit and evaluate the model n times
	scores = [walk_forward_validation(data, n_test, config) for _ in range(n_repeats)]
	return scores

# summarize model performance
def summarize_scores(name, scores):
	# print a summary
	scores_m, score_std = mean(scores), std(scores)
	print('%s: %.3f RMSE (+/- %.3f)' % (name, scores_m, score_std))
	# box and whisker plot
	pyplot.boxplot(scores)
	pyplot.show()

series = read_csv('monthly-car-sales.csv', header=0, index_col=0)
data = series.values
# data split
n_test = 12
# define config
config = [3, 12, 256, 3, 200, 200, 100]
# grid search
scores = repeat_evaluate(data, config, n_test)
# summarize scores
summarize_scores('convlstm', scores)

L’exécution de l’exemple imprime le RMSE pour chaque évaluation répétée du modèle.

Remarque : Vos résultats peuvent varier en raison de la nature stochastique de l'algorithme ou de la procédure d'évaluation, ou des différences de précision numérique. Pensez à exécuter l’exemple plusieurs fois et comparez le résultat moyen.

Le RMSE moyen final est rapporté à la fin d'environ 1 660, ce qui est inférieur au modèle naïf, mais toujours supérieur à un modèle SARIMA.

C’est un résultat qui est peut-être à la hauteur du modèle CNN-LSTM. L'écart type de ce score est également très important, ce qui suggère que la configuration choisie n'est peut-être pas aussi stable que le modèle CNN autonome.

 > 1825.246
 > 1862.674
 > 1684.313
 > 1310.448
 > 2109.668
 > 1507.912
 > 1431.118
 > 1442.692
 > 1400.548
 > 1732.381
 > 1523.824
 > 1611.898
 > 1805.970
 > 1616.015
 > 1649.466
 > 1521.884
 > 2025.655
 > 1622.886
 > 2536.448
 > 1526.532
 > 1866.631
 > 1562.625
 > 1491.386
 > 1506.270
 > 1843.981
 > 1653.084
 > 1650.430
 > 1291.353
 > 1558.616
 > 1653.231
convlstm: 1660.840 RMSE (+/- 248.826)

Un diagramme en boîte et moustaches est également créé, résumant la distribution des scores RMSE.

Rallonges

Cette section répertorie quelques idées pour étendre le didacticiel que vous souhaiterez peut-être explorer.

  • Préparation des données. Découvrez si la préparation des données, telle que la normalisation, la standardisation et/ou la différenciation, peut répertorier les performances de l'un des modèles.
  • Hyperparamètres de recherche de grille. Implémentez une recherche de grille des hyperparamètres pour un modèle pour voir si vous pouvez améliorer davantage les performances.
  • Diagnostic de la courbe d'apprentissage. Créez un ajustement unique d'un modèle et examinez les courbes d'apprentissage sur les fractionnements d'entraînement et de validation de l'ensemble de données, puis utilisez les diagnostics des courbes d'apprentissage pour affiner davantage les hyperparamètres du modèle afin d'améliorer les performances du modèle.
  • Taille de l'historique. Explorez différentes quantités de données historiques (entrées de décalage) pour un modèle afin de voir si vous pouvez améliorer encore les performances du modèle.
  • Réduire la variance du modèle final. Explorez une ou plusieurs stratégies pour réduire la variance de l'un des modèles de réseau neuronal.
  • Mise à jour pendant Walk-Forward. Découvrez si le réaménagement ou la mise à jour d'un modèle de réseau neuronal dans le cadre d'une validation progressive peut améliorer encore les performances du modèle.
  • Plus de paramétrage. Envisagez d'ajouter d'autres paramétrages de modèle pour un modèle, comme l'utilisation de couches supplémentaires.

Si vous explorez l’une de ces extensions, j’aimerais le savoir.

Lectures complémentaires

Cette section fournit plus de ressources sur le sujet si vous souhaitez approfondir.

  • API pandas.DataFrame.shift
  • API sklearn.metrics.mean_squared_error
  • API matplotlib.pyplot.boxplot
  • API du modèle de séquence Keras

Résumé

Dans ce didacticiel, vous avez découvert comment développer une suite de modèles d'apprentissage profond pour la prévision de séries chronologiques univariées.

Concrètement, vous avez appris :

  • Comment développer un harnais de test robuste en utilisant la validation progressive pour évaluer les performances des modèles de réseaux neuronaux.
  • Comment développer et évaluer des réseaux de neurones Perceptron multicouches et convolutifs simples pour la prévision de séries chronologiques.
  • Comment développer et évaluer des modèles de réseaux neuronaux LSTM, CNN-LSTM et ConvLSTM pour la prévision de séries chronologiques.

Avez-vous des questions ?
Posez vos questions dans les commentaires ci-dessous et je ferai de mon mieux pour y répondre.

Articles connexes