Time Series Modeling with Amazon Forecast and DeepAR on SageMaker - DeepAR on SageMaker


This notebook’s CI test result for us-west-2 is as follows. CI test results in other regions can be found at the end of the notebook.

This us-west-2 badge failed to load. Check your device’s internet connectivity, otherwise the service is currently unavailable


Introduction

Amazon offers customers a multitude of time series prediction services, including DeepAR on SageMaker and the fully managed service Amazon Forecast. Both services are similar in some aspects, yet differ in others. This notebook series aims to highlight the similarities and differences between both services by demonstrating how each service is used as well as describing the features each service offers. As a result, both notebooks in the series will use the same dataset. We will consider a real use case using the Beijing Multi-Site Air-Quality Data Set which features hourly air pollutants data from 12 air-quality monitoring sites from March 1st, 2013 to February 28th, 2017, and is featured in the [1] academic paper. This particular notebook will focus on DeepAR on SageMaker, and will: - Demonstrate how to train a DeepAR model on SageMaker - Create inferences from the DeepAR model

One feature of Amazon Forecast is that the service can be used without any code. However, this notebook will outline how to use the service within a notebook format. Before you start, please note that training an Amazon Forecast may take several hours; this particular notebook took approximately 6 hours 30 minutes to complete. Also, make sure that your SageMaker Execution Role has the following policies:

  • AmazonSageMakerFullAccess

For convenience, here is an overview of the structure of this notebook: 1. Introduction - Preparation 2. Data Preprocessing - Data Import - Data Visualization - Train/Test Split - Upload to S3 3. Model 7. Resource Cleanup 8. Next Steps

Preparation

[ ]:
!pip install seaborn --upgrade
[ ]:
import boto3
import os
import pandas as pd
import numpy as np
import json
import sagemaker
from datetime import datetime
from IPython.display import display

import matplotlib.pyplot as plt
import seaborn as sns
[ ]:
session = boto3.Session()
s3_client = session.client("s3")
sagemaker_session = sagemaker.Session()
region = session.region_name

All paths and resource names are defined below for a simple overview for where each resource will be located:

[ ]:
# Remove paths if notebook was run before
!rm -r data
!rm -r deepar
[ ]:
bucket = sagemaker.Session().default_bucket()
sagemaker_sample_bucket = f"sagemaker-example-files-prod-{region}"
version = datetime.now().strftime("_%Y_%m_%d_%H_%M_%S")

dirs = ["data", "deepar", "deepar/to_export"]

for dir_name in dirs:
    os.makedirs(dir_name)

dataset_s3_path = "datasets/timeseries/beijing_air_quality/PRSA2017_Data_20130301-20170228.zip"
dataset_save_path = "data/dataset.zip"  # path where the zipped dataset is imported to
dataset_path = "data/dataset"  # path where unzipped dataset is located
deepar_export_path = "deepar/to_export"
deepar_training_path = "{}/training.json".format(deepar_export_path)
deepar_test_path = "{}/test.json".format(deepar_export_path)
deepar_s3_training_path = "deepar/train.json"
deepar_s3_test_path = "deepar/test.json"
deepar_s3_output_path = "deepar/output"

Data Preprocessing

This section prepares the dataset for use in DeepAR on SageMaker. It will cover: - Target/Test dataset splitting - Target/Related time series splitting - S3 uploading

Data Import

This section will be demonstrating how to import data from an S3 bucket, but one can import their data whichever way is convenient. The data for this example will be imported from the sagemaker-example-files-prod-{region} S3 Bucket.

To communicate with S3 outside of our console, we’ll use the Boto3 python3 library. More functionality between Boto3 and S3 can be found here: Boto3 Amazon S3 Examples

This particular dataset decompresses into a single folder named PRSA_Data_20130301-20170228. It contains 12 csv files, each containing air quality data for a single location. Each DataFrame will contain the following columns: - No: row number - year: year of data in this row - month: month of data in this row - day: day of data in this row - hour: hour of data in this row - PM2.5: PM2.5 concentration (ug/m^3) - PM10: PM10 concentration (ug/m^3) - SO2: SO2 concentration (ug/m^3) - NO2: NO2 concentration (ug/m^3) - CO: CO concentration (ug/m^3) - O3: O3 concentration (ug/m^3) - TEMP: temperature (degree Celsius) - PRES: pressure (hPa) - DEWP: dew point temperature (degree Celsius) - RAIN: precipitation (mm) - wd: wind direction - WSPM: wind speed (m/s) - station: name of the air-quality monitoring site

Citations

  • Dua, D. and Graff, C. (2019). UCI Machine Learning Repository [http://archive.ics.uci.edu/ml]. Irvine, CA: University of California, School of Information and Computer Science.

[ ]:
s3_client.download_file(sagemaker_sample_bucket, dataset_s3_path, dataset_save_path)
[ ]:
!unzip data/dataset.zip -d data && mv data/PRSA_Data_20130301-20170228 data/dataset
[ ]:
dataset = [
    pd.read_csv("{}/{}".format(dataset_path, file_name)) for file_name in os.listdir(dataset_path)
]

display(dataset[0])

Both SageMaker DeepAR and Amazon Forecast use datetime objects for their time series cataloging, so we’ll convert our year,month,day,hour columns into datetime column. Since we’ve represented these columns into our new datetime column, we can drop our year,month,day,hour columns from earlier. We can also drop the No column as our data is already in order.

[ ]:
for df in dataset:
    df.insert(0, "datetime", pd.to_datetime(df[["year", "month", "day", "hour"]]))
    df.drop(columns=["No", "year", "month", "day", "hour"], inplace=True)

display(dataset[0])

Data Visualization

For this example, we’ll use the temperature, or TEMP column, as our target variable to predict on. Let’s first take a look at what each of our time series looks like.

[ ]:
sns.set_style("dark")
fig, axes = plt.subplots(2, 3, figsize=(18, 10))
fig.suptitle("Target Values")

for i, axis in zip(range(len(dataset))[:6], axes.ravel()):
    sns.lineplot(data=dataset[i], x="datetime", y="TEMP", ax=axis)
    axis.set_title(dataset[i]["station"].iloc[0])
    axis.set_ylabel("Temperature (Celsius)")
fig.tight_layout()

Dataset Visual

Train/Test Split

Now we’ll demonstrate how to use this dataset in SageMaker DeepAR and predict.

SageMaker’s DeepAR expects input in a JSON format with these specific fields for each time series: - start - target - cat (optional) - dynamic_feat (optional)

Further information about the DeepAR input formatting can be found here: DeepAR Input/Output Interface.

SageMaker DeepAR recommends a prediction length of <=400 as large values decrease the algorithms accuracy and speed. Thus, let’s set the length of our test time series and prediction length to the last two weeks of our data, or 14*24 = 336 observations. Useful information about best practices for DeepAR can be found here: DeepAR Best Practices. Since we have missing values in our time series, we must account for these. Luckily, DeepAR accepts missing values as long as they’re "NaN" strings or encoded as null literals, as we will be exporting our time series to JSON to train the DeepAR model. One could also choose to replace all missing values with the mean of each time series.

[ ]:
prediction_length = 14 * 24  # 14 days

deepar_training = [
    {
        "start": str(df["datetime"].min()),
        "target": df["TEMP"].fillna("NaN").tolist()[:-prediction_length],
    }
    for df in dataset
]

deepar_test = [
    {"start": str(df["datetime"].min()), "target": df["TEMP"].fillna("NaN").tolist()}
    for df in dataset
]

Upload to S3

SageMaker DeepAR gets its data for training from S3, so we’ll use the previously defined Boto3 S3 Client to upload our JSON files to S3. However, uploading files through the AWS console is another option and does not require code.

Let’s define a function to export our dictionaries into JSON files to make our data properly input into SageMaker DeepAR:

[ ]:
def write_dicts_to_json(path, data):
    with open(path, "wb") as file_path:
        for ts in data:
            file_path.write(json.dumps(ts).encode("utf-8"))
            file_path.write("\n".encode("utf-8"))

Now we can export our dictionaries in a JSON format into the paths we defined earlier:

[ ]:
write_dicts_to_json(deepar_training_path, deepar_training)
write_dicts_to_json(deepar_test_path, deepar_test)
[ ]:
s3_client.upload_file(deepar_training_path, bucket, deepar_s3_training_path)
s3_client.upload_file(deepar_test_path, bucket, deepar_s3_test_path)

Model

Now that we’ve formatted our data properly, we can train our model. When initializing our estimator, we must specify an instance type. Available options as well as pricing can be viewed here: Available SageMaker Pricing. We also need to pass an Image URI to specify which algorithm we want to use, as well as pass required parameters to our Estimator. Further documentation on retrieving Image URIs and the sagemaker.estimator.Estimator class can be found here:

In this case, it was found that an ml.c5.2xlarge had the minimum amount of memory required for the training to complete, but one should use any instance type that fits their use case. In addition, using faster EC2 instances may in some cases be cheaper than using the minimum required as the model will take less time to train. Amazon SageMaker also offers discounted EC2 pricing if Amazon EC2 Spot instances are used, which is unused EC2 capacity in the AWS cloud. This can be toggled with the use_spot_instances parameter. Further information on Managed Spot Training can be found here: Model Managed Spot Training

[ ]:
image_uri = sagemaker.image_uris.retrieve("forecasting-deepar", region)
role = sagemaker.get_execution_role()

estimator = sagemaker.estimator.Estimator(
    sagemaker_session=sagemaker_session,
    image_uri=image_uri,
    role=role,
    instance_count=1,
    instance_type="ml.c5.2xlarge",
    base_job_name="DEMO-DeepAR",
    use_spot_instances=False,
    output_path="s3://{}/{}".format(bucket, deepar_s3_output_path),
)

Now we need to configure the DeepAR instance’s hyperparameters to our specific needs. There are four required hyperparameters that we must define, but there are 16 total tunable hyperparameters. All tunable hyperparameters and detailed descriptions can be found here: DeepAR Hyperparameters.

[ ]:
hyperparameters = {
    "epochs": "50",
    "time_freq": "H",
    "prediction_length": prediction_length,
    "context_length": prediction_length,
}

estimator.set_hyperparameters(**hyperparameters)

After setting the hyperparameters, we can train our model. One run of the training job took 1543 seconds, or approximately 25 minutes.

[ ]:
%%time
estimator.fit(
    inputs={
        "train": "s3://{}/{}".format(bucket, deepar_s3_training_path),
        "test": "s3://{}/{}".format(bucket, deepar_s3_test_path),
    }
)

Inference

After training our model, we must initialize an endpoint to call our model. This particular endpoint uses an ml.c5.large instance and took 3 minutes 2 seconds to initialize.

[ ]:
%%time
job_name = estimator.latest_training_job.name

endpoint_name = sagemaker_session.endpoint_from_job(
    job_name=job_name,
    initial_instance_count=1,
    instance_type="ml.c5.large",
    image_uri=image_uri,
    role=role,
)

Then, we can initialize a predictor from our endpoint to receive time series predictions:

[ ]:
from sagemaker.serializers import JSONSerializer

predictor = sagemaker.predictor.Predictor(
    endpoint_name=endpoint_name, sagemaker_session=sagemaker_session, serializer=JSONSerializer()
)

DeepAR requires our request be in a JSON request format as input to receive predictions. The following example is from the DeepAR JSON Request Formats documentation page where request definition is outlined:

{
    "instances": [
        {
            "start": "2009-11-01 00:00:00",
            "target": [4.0, 10.0, "NaN", 100.0, 113.0],
            "cat": [0, 1],
            "dynamic_feat": [[1.0, 1.1, 2.1, 0.5, 3.1, 4.1, 1.2, 5.0, ...]]
        },
        {
            "start": "2012-01-30",
            "target": [1.0],
            "cat": [2, 1],
            "dynamic_feat": [[2.0, 3.1, 4.5, 1.5, 1.8, 3.2, 0.1, 3.0, ...]]
        },
        {
            "start": "1999-01-30",
            "target": [2.0, 1.0],
            "cat": [1, 3],
            "dynamic_feat": [[1.0, 0.1, -2.5, 0.3, 2.0, -1.2, -0.1, -3.0, ...]]
        }
    ],
    "configuration": {
         "num_samples": 50,
         "output_types": ["mean", "quantiles", "samples"],
         "quantiles": ["0.5", "0.9"]
    }
}

Only types specified in the request will be present in the predictor’s response. Valid values for the output_types field are: "mean","quantiles", and "samples". Furthermore, the "cat" and/or "dynamic_feat" fields of each instance should be omitted if these fields were not used to train the model. Let’s define our request, where we’ll request predictions for the 0.1, 0.5, and 0.9 quantiles.

[ ]:
predictor_input = {
    "instances": deepar_training,
    "configuration": {"output_types": ["quantiles"], "quantiles": ["0.1", "0.5", "0.9"]},
}

Finally, we can obtain a prediction from our model for the prediction_length number of instances following our requested time series, and conforming to the time_freq (time frequency) specified in our hyperparameters. This prediction took approximately 8 seconds to receive a response.

[ ]:
%%time
prediction = predictor.predict(predictor_input)

Interpreting Results

The resulting prediction will come in a JSON format. The response is within a dictionary formatted like so: DeepAR JSON Response Formats. The following example is from the previously mentioned page:

{
    "predictions": [
        {
            "quantiles": {
                "0.9": [...],
                "0.5": [...]
            },
            "samples": [...],
            "mean": [...]
        },
        {
            "quantiles": {
                "0.9": [...],
                "0.5": [...]
            },
            "samples": [...],
            "mean": [...]
        },
        {
            "quantiles": {
                "0.9": [...],
                "0.5": [...]
            },
            "samples": [...],
            "mean": [...]
        }
    ]
}

Let’s define a method to help us decode the predictor’s JSON response and load it onto a DataFrame:

[ ]:
def prediction_to_df(response):
    data = json.loads(response)
    dataframes = []

    for ts in data["predictions"]:
        if "quantiles" in ts:
            # Since the quantiles response comes in a list within the dictionary, we will append the quantiles
            # dictionary of each time series to the mean and samples(if requested) of those respective time series
            ts.update(ts["quantiles"])
            ts.pop("quantiles")
        dataframes.append(pd.DataFrame(data=ts))

    return dataframes

Now that we’ve obtained our predictions(that came in a JSON format) and defined a method to decode these predictions, we can see our results in a pandas DataFrame format:

[ ]:
deepar_results = prediction_to_df(prediction)

display(deepar_results[0])

Let’s visualize our predictions after acquisition. We’ll plot our first station to see how we did. First, let’s append our target values to our results for convenient comparison. Then, we’ll plot all requested quantiles onto the same plot with the target values to see how DeepAR did.

[ ]:
df_results = []

for i in range(len(deepar_results)):
    temp = pd.concat(
        [
            dataset[i][["TEMP", "datetime", "station"]]
            .tail(prediction_length)
            .reset_index(drop=True),
            deepar_results[i],
        ],
        axis=1,
    )
    temp = temp.rename(columns={"TEMP": "target"})
    df_results.append(temp)
[ ]:
def plot_comparison(query):
    sns.set_style("dark")
    plt.figure(figsize=(18, 10))
    plt.plot(query["datetime"], query["0.1"], color="r", lw=1)
    plt.plot(query["datetime"], query["0.5"], color="orange", linestyle=":", lw=2)
    plt.plot(query["datetime"], query["0.9"], color="r", lw=1)
    plt.plot(query["datetime"], query["target"], color="b", lw=1)
    plt.fill_between(
        query["datetime"].tolist(),
        query["0.9"].tolist(),
        query["0.1"].tolist(),
        color="y",
        alpha=0.5,
    )
    plt.title(query["station"][0])
    plt.xlabel("Datetime")
    plt.ylabel("Temperature (Celsius)")

    plt.legend(["10% Quantile", "50% Quantile", "90% Quantile", "Target"])
    plt.show()
[ ]:
plot_comparison(df_results[0])

DeepAR Results

As we can see, the 0.1 and 0.9 quantiles create an 80% confidence interval for our predictions, which our target generally stays within. However, as mentioned in the DeepAR Best Practices, our confidence interval becomes less accurate towards the end due to our relatively high prediction_length value. To remediate this, lowering the frequency of data, such as changing 1min to 5min, or H to D (hourly to daily), is recommended.

Resource Cleanup

[ ]:
predictor.delete_model()
predictor.delete_endpoint()

Next Steps

This notebook illustrates the features offered by DeepAR on SageMaker, and is part of the Time Series Modeling with Amazon Forecast and DeepAR on SageMaker series. The notebook series aims to demonstrate how to use the Amazon Forecast and DeepAR on SageMaker time series modeling services as well as outline their features. Be sure to read the Amazon Forecast example, and view a top-level comparison of both services in the README.

Notebook CI Test Results

This notebook was tested in multiple regions. The test results are as follows, except for us-west-2 which is shown at the top of the notebook.

This us-east-1 badge failed to load. Check your device’s internet connectivity, otherwise the service is currently unavailable

This us-east-2 badge failed to load. Check your device’s internet connectivity, otherwise the service is currently unavailable

This us-west-1 badge failed to load. Check your device’s internet connectivity, otherwise the service is currently unavailable

This ca-central-1 badge failed to load. Check your device’s internet connectivity, otherwise the service is currently unavailable

This sa-east-1 badge failed to load. Check your device’s internet connectivity, otherwise the service is currently unavailable

This eu-west-1 badge failed to load. Check your device’s internet connectivity, otherwise the service is currently unavailable

This eu-west-2 badge failed to load. Check your device’s internet connectivity, otherwise the service is currently unavailable

This eu-west-3 badge failed to load. Check your device’s internet connectivity, otherwise the service is currently unavailable

This eu-central-1 badge failed to load. Check your device’s internet connectivity, otherwise the service is currently unavailable

This eu-north-1 badge failed to load. Check your device’s internet connectivity, otherwise the service is currently unavailable

This ap-southeast-1 badge failed to load. Check your device’s internet connectivity, otherwise the service is currently unavailable

This ap-southeast-2 badge failed to load. Check your device’s internet connectivity, otherwise the service is currently unavailable

This ap-northeast-1 badge failed to load. Check your device’s internet connectivity, otherwise the service is currently unavailable

This ap-northeast-2 badge failed to load. Check your device’s internet connectivity, otherwise the service is currently unavailable

This ap-south-1 badge failed to load. Check your device’s internet connectivity, otherwise the service is currently unavailable