The GluonTS toolkit contains components and tools for building time series models using MXNet. The models that are currently included are forecasting models but the components also support other time series use cases, such as classification or anomaly detection.
The toolkit is not intended as a forecasting solution for businesses or end users but it rather targets scientists and engineers who want to tweak algorithms or build and experiment with their own models.
GluonTS contains:
- Components for building new models (likelihoods, feature processing pipelines, calendar features etc.)
- Data loading and processing
- A number of pre-built models
- Plotting and evaluation facilities
- Artificial and real datasets (only external datasets with blessed license)
# Third-party imports
%matplotlib inline
import mxnet as mx
from mxnet import gluon
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import json
GluonTS comes with a number of publicly available datasets.
from gluonts.dataset.repository.datasets import get_dataset, dataset_recipes
from gluonts.dataset.util import to_pandas
print(f"Available datasets: {list(dataset_recipes.keys())}")
An available dataset can be easily downloaded by its name. In this notebook we will use the "m4_hourly" dataset that contains a few hundred time series. If the dataset already exists locally it is not downloaded again by setting regenerate=False
.
dataset = get_dataset("m4_hourly", regenerate=False)
In general, the datasets provided by GluonTS are objects that consists of three main components:
dataset.train
is an iterable collection of data entries used for training.dataset.test
is an iterable collection of data entries used for inference. The test dataset is an extended version of the train dataset that contains a window in the end of each time series that was not seen during training. This window has length equal to the recommended prediction length.dataset.metadata
containts metadata of the dataset such as the frequency of the time series, a recommended prediction horizon, associated features, etc.
entry = next(iter(dataset.train))
train_series = to_pandas(entry)
train_series.plot()
plt.grid(which="both")
plt.legend(["train series"], loc="upper left")
plt.show()
entry = next(iter(dataset.test))
test_series = to_pandas(entry)
test_series.plot()
plt.axvline(train_series.index[-1], color='r') # end of train dataset
plt.grid(which="both")
plt.legend(["test series", "end of train series"], loc="upper left")
plt.show()
print(f"Length of forecasting window in test dataset: {len(test_series) - len(train_series)}")
print(f"Recommended prediction horizon: {dataset.metadata.prediction_length}")
print(f"Frequency of the time series: {dataset.metadata.time_granularity}")
At this point, it is important to emphasize that GluonTS does not require this specific format for a custom dataset that a user may have. The only requirements for a custom dataset are to be iterable and have a "target" and a "start" field. To make this more clear, assume the common case where a dataset is in the form of a numpy.array
and the index of the time series in a pandas.Timestamp
(possibly different for each time series):
N = 10 # number of time series
T = 100 # number of timesteps
prediction_length = 24
custom_dataset = np.random.normal(size=(N, T))
start = pd.Timestamp("01-01-2019", freq='1H') # can be different for each time series
Now, you can split your dataset and bring it in a GluonTS appropriate format with just two lines of code:
# train dataset: cut the last window of length "prediction_length", add "target" and "start" fields
train_ds = [{'target': x, 'start': start} for x in custom_dataset[:, :-prediction_length]]
# test dataset: use the whole dataset, add "target" and "start" fields
test_ds = [{'target': x, 'start': start} for x in custom_dataset]
As we already mentioned, GluonTS comes with a number of pre-built models that can be used directly with minor hyperparameter configurations. For starters we will use one of these predefined models to go through the whole pipeline of training a model, predicting, and evaluating the results.
GluonTS gives focus (but is not restricted) to probabilistic forecasting, i.e., forecasting the future distribution of the values and not the future values themselves (point estimates) of a time series. Having estimated the future distribution of each time step in the forecasting horizon, we can draw a sample from the distribution at each time step and thus create a "sample path" that can be seen as a possible realization of the future. In practice we draw multiple samples and create multiple sample paths which can be used for visualization, evaluation of the model, to derive statistics, etc.
In this example we will use a simple pre-built feedforward neural network estimator that takes as input a window of length context_length
and predicts the distribution of the values of the subsequent future window of length prediction_length
.
In general, each estimator (pre-built or custom) is configured by a number of hyperparameters that can be either common (but not binding) among all estimators (e.g., the prediction_length
) or specific for the particular estimator (e.g., number of layers for a neural network or the stride in a CNN).
Finally, each estimator is configured by a Trainer
, which defines how the model will be trained i.e., the number of epochs, the learning rate, etc.
from gluonts.model.simple_feedforward import SimpleFeedForwardEstimator
from gluonts.trainer import Trainer
estimator = SimpleFeedForwardEstimator(
num_hidden_dimensions=[10],
prediction_length=dataset.metadata.prediction_length,
context_length=100,
freq=dataset.metadata.time_granularity,
trainer=Trainer(ctx="cpu", epochs=5, learning_rate=1E-3, hybridize=True, num_batches_per_epoch=200,),
)
After specifing our estimator with all the necessary hyperparameters we can train it using our training dataset dataset.train
just by invoking the train
method of the estimator. The training returns a predictor that can be used to predict.
predictor = estimator.train(dataset.train)
Now we have a predictor in our hands. We can use it to predict the last window of the dataset.test
and evaluate how our model performs.
GluonTS comes with the make_evaluation_predictions
function that automates all this procedure. Roughly, this module performs the following steps:
- Removes the final window of length
prediction_length
of thedataset.test
that we want to predict - The estimator uses the remaining dataset to predict (in the form of sample paths) the "future" window that was just removed
- The module outputs a generator over the forecasted sample paths and a generator over the
dataset.test
from gluonts.evaluation.backtest import make_evaluation_predictions
forecast_it, ts_it = make_evaluation_predictions(
dataset=dataset.test, # test dataset
predictor=predictor, # predictor
num_eval_samples=100, # number of sample paths we want for evaluation
)
print(type(forecast_it))
print(type(ts_it))
First, we can convert these generators to lists to ease the subsequent computations.
forecasts = list(forecast_it)
tss = list(ts_it)
Now, let's see what do these lists contain under the hood. Let's start with the time series tss
that is simpler. Each item in the tss
list is just a pandas dataframe that contains the actual time series.
print(type(tss[0]))
tss[0].head()
The forecasts
list is a bit more complex. Each item in the forecasts
list is an object that contains all the sample paths in the form of numpy.ndarray
with dimension (num_samples, prediction_length)
, the start date of the forecast, the frequency of the time series, etc. We can access all these information by simply invoking the corresponding attribute of the forecast object.
print(type(forecasts[0]))
print(f"Number of sample paths: {forecasts[0].num_samples}")
print(f"Dimension of samples: {forecasts[0].samples.shape}")
print(f"Start date of the forecast window: {forecasts[0].start_date}")
print(f"Frequency of the time series: {forecasts[0].freq}")
Apart from retrieving basic information we can do some more complex calculations such as to compute the mean or a given quantile of the values of the forecasted window.
print(f"Mean of the future window:\n {forecasts[0].mean}")
print(f"0.5-quantile (median) of the future window:\n {forecasts[0].quantile(0.5)}")
Finally, each forecast object has a plot
method that can be parametrized to show the mean, prediction intervals, etc. The prediction intervals are plotted in different shades so they are distinct.
plot_length = 150
prediction_intervals = (50.0, 90.0)
legend = ["observations", "median prediction"] + [f"{k}% prediction interval" for k in prediction_intervals][::-1]
fig, ax = plt.subplots(1, 1, figsize=(10, 7))
tss[0][-plot_length:].plot(ax=ax) # plot the time series
forecasts[0].plot(prediction_intervals=prediction_intervals, color='g')
plt.grid(which="both")
plt.legend(legend, loc="upper left")
plt.show()
We can also evaluate the quality of our forecasts. GluonTS comes with an Evaluator
that returns aggregate error metrics as well as metrics per time series which can be used e.g., for scatter plots.
from gluonts.evaluation import Evaluator
evaluator = Evaluator(quantiles=[0.1, 0.5, 0.9])
agg_metrics, item_metrics = evaluator(iter(tss), iter(forecasts), num_series=len(dataset.test))
print(json.dumps(agg_metrics, indent=4))
item_metrics.head()
item_metrics.plot(x='MSIS', y='MASE', kind='scatter')
plt.grid(which="both")
plt.show()
For creating your own forecast model you need to:
- Define the training and prediction network
- Define a new estimator that specifies any data processing and uses the networks
The training and prediction networks can be arbitrarily complex but they should follow some basic rules:
- Both should have a
hybrid_forward
method that defines what should happen when the network is called - The trainng network's
hybrid_forward
should return a loss based on the prediction and the true values - The prediction network's
hybrid_forward
should return the predictions
For example, we can create a simple training network that defines a neural network which takes as an input the past values of the time series and outputs a future predicted window of length prediction_length
. It uses the L1 loss in the hybrid_forward
method to evaluate the error among the predictions and the true values of the time series. The corresponding prediction network should be identical to the training network in terms of architecture (we achieve this by inheriting the training network class), and its hybrid_forward
method outputs directly the predictions.
Note that this simple model does only point forecasts by construction, i.e., we train it to outputs directly the future values of the time series and not any probabilistic view of the future (to achieve this we should train a network to learn a probability distribution and then sample from it to create sample paths).
class MyTrainNetwork(gluon.HybridBlock):
def __init__(self, prediction_length, **kwargs):
super().__init__(**kwargs)
self.prediction_length = prediction_length
with self.name_scope():
# Set up a 3 layer neural network that directly predicts the target values
self.nn = mx.gluon.nn.HybridSequential()
self.nn.add(mx.gluon.nn.Dense(units=40, activation='relu'))
self.nn.add(mx.gluon.nn.Dense(units=40, activation='relu'))
self.nn.add(mx.gluon.nn.Dense(units=self.prediction_length, activation='softrelu'))
def hybrid_forward(self, F, past_target, future_target):
prediction = self.nn(past_target)
# calculate L1 loss with the future_target to learn the median
return (prediction - future_target).abs().mean(axis=-1)
class MyPredNetwork(MyTrainNetwork):
# The prediction network only receives past_target and returns predictions
def hybrid_forward(self, F, past_target):
prediction = self.nn(past_target)
return prediction.expand_dims(axis=1)
Now, we need to construct the estimator which should also follow some rules:
- It should include a
create_transformation
method that defines all the possible feature transformations and how the data is split during training - It should include a
create_training_network
method that returns the training network configured with any necessary hyperparameters - It should include a
create_predictor
method that creates the prediction network, and returns aPredictor
object
A Predictor
defines the predict
method of a given predictor. Roughly, this method takes the test dataset, it passes it through the prediction network and yields the predictions. You can think of the Predictor
object as a wrapper of the prediction network that defines its predict
method.
Earlier, we used the make_evaluation_predictions
to evaluate our predictor. Internally, the make_evaluation_predictions
function invokes the predict
method of the predictor to get the forecasts.
from gluonts.model.estimator import GluonEstimator
from gluonts.model.predictor import Predictor, RepresentableBlockPredictor
from gluonts.core.component import validated
from gluonts.support.util import copy_parameters
from gluonts.transform import ExpectedNumInstanceSampler, Transformation, InstanceSplitter, FieldName
from mxnet.gluon import HybridBlock
class MyEstimator(GluonEstimator):
@validated()
def __init__(
self,
freq: str,
context_length: int,
prediction_length: int,
trainer: Trainer = Trainer()
) -> None:
super().__init__(trainer=trainer)
self.context_length = context_length
self.prediction_length = prediction_length
self.freq = freq
def create_transformation(self):
# Feature transformation that the model uses for input.
# Here we use a transformation that randomly select training samples from all time series.
return InstanceSplitter(
target_field=FieldName.TARGET,
is_pad_field=FieldName.IS_PAD,
start_field=FieldName.START,
forecast_start_field=FieldName.FORECAST_START,
train_sampler=ExpectedNumInstanceSampler(num_instances=1),
past_length=self.context_length,
future_length=self.prediction_length,
)
def create_training_network(self) -> MyTrainNetwork:
return MyTrainNetwork(
prediction_length=self.prediction_length
)
def create_predictor(
self, transformation: Transformation, trained_network: HybridBlock
) -> Predictor:
prediction_network = MyPredNetwork(
prediction_length=self.prediction_length
)
copy_parameters(trained_network, prediction_network)
return RepresentableBlockPredictor(
input_transform=transformation,
prediction_net=prediction_network,
batch_size=self.trainer.batch_size,
freq=self.freq,
prediction_length=self.prediction_length,
ctx=self.trainer.ctx,
)
Now, we can repeat the same pipeline as in the case we had a pre-built model: train the predictor, create the forecasts and evaluate the results.
estimator = MyEstimator(
prediction_length=dataset.metadata.prediction_length,
context_length=200,
freq=dataset.metadata.time_granularity,
trainer=Trainer(ctx="cpu", epochs=5, learning_rate=1E-3, hybridize=True, num_batches_per_epoch=200,),
)
predictor = estimator.train(dataset.train)
forecast_it, ts_it = make_evaluation_predictions(
dataset=dataset.test,
predictor=predictor,
num_eval_samples=100
)
forecasts = list(forecast_it)
tss = list(ts_it)
plot_length = 150
prediction_intervals = (50.0, 90.0)
legend = ["observations", "median prediction"] + [f"{k}% prediction interval" for k in prediction_intervals][::-1]
fig, ax = plt.subplots(1, 1, figsize=(10, 7))
tss[0][-plot_length:].plot(ax=ax) # plot the time series
forecasts[0].plot(prediction_intervals=prediction_intervals, color='g')
plt.grid(which="both")
plt.legend(legend, loc="upper left")
plt.show()
We observe from the plot above that we cannot actually see any prediction intervals in the predictions. This is expected since the model that we defined does not do probabilistic forecasting but it just gives point estimates. By requiring 100 sample paths (defined in make_evaluation_predictions
) in such a network, we get 100 times the same output.
evaluator = Evaluator(quantiles=[0.1, 0.5, 0.9])
agg_metrics, item_metrics = evaluator(iter(tss), iter(forecasts), num_series=len(dataset.test))
print(json.dumps(agg_metrics, indent=4))
item_metrics.head(10)
item_metrics.plot(x='MSIS', y='MASE', kind='scatter')
plt.grid(which="both")
plt.show()