上篇 | 使用 ? Transformers 進行機率時間序列預測

HuggingFace發表於2023-02-22

介紹

時間序列預測是一個重要的科學和商業問題,因此最近透過使用基於深度學習 而不是經典方法的模型也湧現出諸多創新。ARIMA 等經典方法與新穎的深度學習方法之間的一個重要區別如下。

機率預測

通常,經典方法針對資料集中的每個時間序列單獨擬合。這些通常被稱為“單一”或“區域性”方法。然而,當處理某些應用程式的大量時間序列時,在所有可用時間序列上訓練一個“全域性”模型是有益的,這使模型能夠從許多不同的來源學習潛在的表示。

一些經典方法是點值的 (point-valued)(意思是每個時間步只輸出一個值),並且透過最小化關於基本事實資料的 L2 或 L1 型別的損失來訓練模型。然而,由於預測經常用於實際決策流程中,甚至在迴圈中有人的干預,讓模型同時也提供預測的不確定性更加有益。這也稱為“機率預測”,而不是“點預測”。這需要對可以取樣的機率分佈進行建模。

所以簡而言之,我們希望訓練全域性機率模型,而不是訓練區域性點預測模型。深度學習非常適合這一點,因為神經網路可以從幾個相關的時間序列中學習表示,並對資料的不確定性進行建模。

在機率設定中學習某些選定引數分佈的未來引數很常見,例如高斯分佈 (Gaussian) 或 Student-T,或者學習條件分位數函式 (conditional quantile function),或使用適應時間序列設定的共型預測 (Conformal Prediction) 框架。方法的選擇不會影響到建模,因此通常可以將其視為另一個超引數。透過採用經驗均值或中值,人們總是可以將機率模型轉變為點預測模型。

時間序列 Transformer

正如人們所想象的那樣,在對本來就連續的時間序列資料建模方面,研究人員提出了使用迴圈神經網路 (RNN) (如 LSTM 或 GRU) 或卷積網路 (CNN) 的模型,或利用最近興起的基於 Transformer 的訓練方法,都很自然地適合時間序列預測場景。

在這篇博文中,我們將利用傳統 vanilla Transformer (參考 Vaswani 等 2017 年發表的論文) 進行單變數機率預測 (univariate probabilistic forecasting) 任務 (即預測每個時間序列的一維分佈) 。 由於 Encoder-Decoder Transformer 很好地封裝了幾個歸納偏差,所以它成為了我們預測的自然選擇。

首先,使用 Encoder-Decoder 架構在推理時很有幫助。通常對於一些記錄的資料,我們希望提前預知未來的一些預測步驟。可以認為這個過程類似於文字生成任務,即給定上下文,取樣下一個詞元 (token) 並將其傳回解碼器 (也稱為“自迴歸生成”) 。類似地,我們也可以在給定某種分佈型別的情況下,從中抽樣以提供預測,直到我們期望的預測範圍。這被稱為貪婪取樣 (Greedy Sampling)/搜尋,此處 有一篇關於 NLP 場景預測的精彩博文。

其次,Transformer 幫助我們訓練可能包含成千上萬個時間點的時間序列資料。由於注意力機制的時間和記憶體限制,一次性將 所有 時間序列的完整歷史輸入模型或許不太可行。因此,在為隨機梯度下降 (SGD) 構建批次時,可以考慮適當的上下文視窗大小,並從訓練資料中對該視窗和後續預測長度大小的視窗進行取樣。可以將調整過大小的上下文視窗傳遞給編碼器、預測視窗傳遞給 causal-masked 解碼器。這樣一來,解碼器在學習下一個值時只能檢視之前的時間步。這相當於人們訓練用於機器翻譯的 vanilla Transformer 的過程,稱為“教師強制 (Teacher Forcing)”。

Transformers 相對於其他架構的另一個好處是,我們可以將缺失值 (這在時間序列場景中很常見) 作為編碼器或解碼器的額外掩蔽值 (mask),並且仍然可以在不訴諸於填充或插補的情況下進行訓練。這相當於 Transformers 庫中 BERT 和 GPT-2 等模型的 attention_mask,在注意力矩陣 (attention matrix) 的計算中不包括填充詞元。

由於傳統 vanilla Transformer 的平方運算和記憶體要求,Transformer 架構的一個缺點是上下文和預測視窗的大小受到限制。關於這一點,可以參閱 Tay 等人於 2020 年發表的調研報告 。此外,由於 Transformer 是一種強大的架構,與 其他方法 相比,它可能會過擬合或更容易學習虛假相關性。

? Transformers 庫帶有一個普通的機率時間序列 Transformer 模型,簡稱為 Time Series Transformer。在這篇文章後面的內容中,我們將展示如何在自定義資料集上訓練此類模型。

設定環境

首先,讓我們安裝必要的庫: ? Transformers、? Datasets、? Evaluate、? Accelerate 和 GluonTS

正如我們將展示的那樣,GluonTS 將用於轉換資料以建立特徵以及建立適當的訓練、驗證和測試批次。

!pip install -q transformers
!pip install -q datasets
!pip install -q evaluate
!pip install -q accelerate
!pip install -q gluonts ujson

載入資料集

在這篇博文中,我們將使用 Hugging Face Hub 上提供的 tourism_monthly 資料集。該資料集包含澳大利亞 366 個地區的每月旅遊流量。

此資料集是 Monash Time Series Forecasting 儲存庫的一部分,該儲存庫收納了是來自多個領域的時間序列資料集。它可以看作是時間序列預測的 GLUE 基準。

from datasets import load_dataset
dataset = load_dataset("monash_tsf", "tourism_monthly")

可以看出,資料集包含 3 個片段: 訓練、驗證和測試。

dataset
>>> DatasetDict({
        train: Dataset({
            features: ['start', 'target', 'feat_static_cat', 'feat_dynamic_real', 'item_id'],
            num_rows: 366
        })
        test: Dataset({
            features: ['start', 'target', 'feat_static_cat', 'feat_dynamic_real', 'item_id'],
            num_rows: 366
        })
        validation: Dataset({
            features: ['start', 'target', 'feat_static_cat', 'feat_dynamic_real', 'item_id'],
            num_rows: 366
        })
    })

每個示例都包含一些鍵,其中 starttarget 是最重要的鍵。讓我們看一下資料集中的第一個時間序列:

train_example = dataset['train'][0]
train_example.keys()

>>> dict_keys(['start', 'target', 'feat_static_cat', 'feat_dynamic_real', 'item_id'])

start 僅指示時間序列的開始 (型別為 datetime) ,而 target 包含時間序列的實際值。

start 將有助於將時間相關的特徵新增到時間序列值中,作為模型的額外輸入 (例如“一年中的月份”) 。因為我們已經知道資料的頻率是 每月,所以也能推算第二個值的時間戳為 1979-02-01,等等。

print(train_example['start'])
print(train_example['target'])

>>> 1979-01-01 00:00:00
    [1149.8699951171875, 1053.8001708984375, ..., 5772.876953125]

驗證集包含與訓練集相同的資料,只是資料時間範圍延長了 prediction_length 那麼多。這使我們能夠根據真實情況驗證模型的預測。

與驗證集相比,測試集還是比驗證集多包含 prediction_length 時間的資料 (或者使用比訓練集多出數個 prediction_length 時長資料的測試集,實現在多重滾動視窗上的測試任務)。

validation_example = dataset['validation'][0]
validation_example.keys()

>>> dict_keys(['start', 'target', 'feat_static_cat', 'feat_dynamic_real', 'item_id'])

驗證的初始值與相應的訓練示例完全相同:

print(validation_example['start'])
print(validation_example['target'])

>>> 1979-01-01 00:00:00
    [1149.8699951171875, 1053.8001708984375, ..., 5985.830078125]

但是,與訓練示例相比,此示例具有 prediction_length=24 個額外的資料。讓我們驗證一下。

freq = "1M"
prediction_length = 24

assert len(train_example['target']) + prediction_length == len(validation_example['target'])

讓我們視覺化一下:

import matplotlib.pyplot as plt

figure, axes = plt.subplots()
axes.plot(train_example['target'], color="blue") 
axes.plot(validation_example['target'], color="red", alpha=0.5)

plt.show()

下面拆分資料:

train_dataset = dataset["train"]
test_dataset = dataset["test"]

start 更新為 pd.Period

我們要做的第一件事是根據資料的 freq 值將每個時間序列的 start 特徵轉換為 pandas 的 Period 索引:

from functools import lru_cache

import pandas as pd
import numpy as np

@lru_cache(10_000)
def convert_to_pandas_period(date, freq):
    return pd.Period(date, freq)

def transform_start_field(batch, freq):
    batch["start"] = [convert_to_pandas_period(date, freq) for date in batch["start"]]
    return batch

這裡我們使用 datasetsset_transform 來實現:

from functools import partial

train_dataset.set_transform(partial(transform_start_field, freq=freq))
test_dataset.set_transform(partial(transform_start_field, freq=freq))

定義模型

接下來,讓我們例項化一個模型。該模型將從頭開始訓練,因此我們不使用 from_pretrained 方法,而是從 config 中隨機初始化模型。

我們為模型指定了幾個附加引數:

  • prediction_length (在我們的例子中是 24 個月) : 這是 Transformer 的解碼器將學習預測的範圍;
  • context_length: 如果未指定 context_length,模型會將 context_length (編碼器的輸入) 設定為等於 prediction_length;
  • 給定頻率的 lags(滯後): 這將決定模型“回頭看”的程度,也會作為附加特徵。例如對於 Daily 頻率,我們可能會考慮回顧 [1, 2, 7, 30, ...],也就是回顧 1、2……天的資料,而對於 Minute 資料,我們可能會考慮 [1, 30, 60, 60*24, ...]` 等;
  • 時間特徵的數量: 在我們的例子中設定為 2,因為我們將新增 MonthOfYearAge 特徵;
  • 靜態類別型特徵的數量: 在我們的例子中,這將只是 1,因為我們將新增一個“時間序列 ID”特徵;
  • 基數: 將每個靜態類別型特徵的值的數量構成一個列表,對於本例來說將是 [366],因為我們有 366 個不同的時間序列;
  • 嵌入維度: 每個靜態類別型特徵的嵌入維度,也是構成列表。例如 [3] 意味著模型將為每個 `366 時間序列 (區域) 學習大小為 3 的嵌入向量。

讓我們使用 GluonTS 為給定頻率 (“每月”) 提供的預設滯後值:

from gluonts.time_feature import get_lags_for_frequency

lags_sequence = get_lags_for_frequency(freq)
print(lags_sequence)

>>> [1, 2, 3, 4, 5, 6, 7, 11, 12, 13, 23, 24, 25, 35, 36, 37]

這意味著我們每個時間步將回顧長達 37 個月的資料,作為附加特徵。

我們還檢查 GluonTS 為我們提供的預設時間特徵:

from gluonts.time_feature import time_features_from_frequency_str

time_features = time_features_from_frequency_str(freq)
print(time_features)

>>> [<function month_of_year at 0x7fa496d0ca70>]

在這種情況下,只有一個特徵,即“一年中的月份”。這意味著對於每個時間步長,我們將新增月份作為標量值 (例如,如果時間戳為 "january",則為 1;如果時間戳為 "february",則為 2,等等) 。

我們現在準備好定義模型需要的所有內容了:

from transformers import TimeSeriesTransformerConfig, TimeSeriesTransformerForPrediction

config = TimeSeriesTransformerConfig(
    prediction_length=prediction_length,
    context_length=prediction_length*3, # context length
    lags_sequence=lags_sequence,
    num_time_features=len(time_features) + 1, # we'll add 2 time features ("month of year" and "age", see further)
    num_static_categorical_features=1, # we have a single static categorical feature, namely time series ID
    cardinality=[len(train_dataset)], # it has 366 possible values
    embedding_dimension=[2], # the model will learn an embedding of size 2 for each of the 366 possible values
    encoder_layers=4, 
    decoder_layers=4,
)

model = TimeSeriesTransformerForPrediction(config)

請注意,與 ? Transformers 庫中的其他模型類似,TimeSeriesTransformerModel 對應於沒有任何頂部前置頭的編碼器-解碼器 Transformer,而 TimeSeriesTransformerForPrediction 對應於頂部有一個分佈前置頭 (distribution head) 的 TimeSeriesTransformerModel。預設情況下,該模型使用 Student-t 分佈 (也可以自行配置):

model.config.distribution_output

>>> student_t

這是具體實現層面與用於 NLP 的 Transformers 的一個重要區別,其中頭部通常由一個固定的分類分佈組成,實現為 nn.Linear 層。

定義轉換

接下來,我們定義資料的轉換,尤其是需要基於樣本資料集或通用資料集來建立其中的時間特徵。

同樣,我們用到了 GluonTS 庫。這裡定義了一個 Chain (有點類似於影像訓練的 torchvision.transforms.Compose) 。它允許我們將多個轉換組合到一個流水線中。

from gluonts.time_feature import time_features_from_frequency_str, TimeFeature, get_lags_for_frequency
from gluonts.dataset.field_names import FieldName
from gluonts.transform import (
    AddAgeFeature,
    AddObservedValuesIndicator,
    AddTimeFeatures,
    AsNumpyArray,
    Chain,
    ExpectedNumInstanceSampler,
    InstanceSplitter,
    RemoveFields,
    SelectFields,
    SetField,
    TestSplitSampler,
    Transformation,
    ValidationSplitSampler,
    VstackFeatures,
    RenameFields,
)

下面的轉換程式碼帶有註釋供大家檢視具體的操作步驟。從全域性來說,我們將迭代資料集的各個時間序列並新增、刪除某些欄位或特徵:

from transformers import PretrainedConfig

def create_transformation(freq: str, config: PretrainedConfig) -> Transformation:
    remove_field_names = []
    if config.num_static_real_features == 0:
        remove_field_names.append(FieldName.FEAT_STATIC_REAL)
    if config.num_dynamic_real_features == 0:
        remove_field_names.append(FieldName.FEAT_DYNAMIC_REAL)

    # 類似 torchvision.transforms.Compose
    return Chain(
        # 步驟 1: 如果靜態或動態欄位沒有特殊宣告,則將它們移除
        [RemoveFields(field_names=remove_field_names)]
        # 步驟 2: 如果靜態特徵存在,就直接使用,否則新增一些虛擬值
        + (
            [SetField(output_field=FieldName.FEAT_STATIC_CAT, value=[0])]
            if not config.num_static_categorical_features > 0
            else []
        )
        + (
            [SetField(output_field=FieldName.FEAT_STATIC_REAL, value=[0.0])]
            if not config.num_static_real_features > 0
            else []
        )
        # 步驟 3: 將資料轉換為 NumPy 格式 (應該用不上)
        + [
            AsNumpyArray(
                field=FieldName.FEAT_STATIC_CAT,
                expected_ndim=1,
                dtype=int,
            ),
            AsNumpyArray(
                field=FieldName.FEAT_STATIC_REAL,
                expected_ndim=1,
            ),
            AsNumpyArray(
                field=FieldName.TARGET,
                # 接下來一行我們為時間維度的資料加上 1
                expected_ndim=1 if config.input_size==1 else 2,
            ),
            # 步驟 4: 目標值遇到 NaN 時,用 0 填充
            # 然後返回觀察值的掩蔽值
            # 存在觀察值時為 true,NaN 時為 false
            # 解碼器會使用這些掩蔽值 (遇到非觀察值時不會產生損失值)
            # 具體可以檢視 xxxForPrediction 模型的 loss_weights 說明
            AddObservedValuesIndicator(
                target_field=FieldName.TARGET,
                output_field=FieldName.OBSERVED_VALUES,
            ),
            # 步驟 5: 根據資料集的 freq 欄位新增暫存值
            # 也就是這裡的“一年中的月份”
            # 這些暫存值將作為定位編碼使用
            AddTimeFeatures(
                start_field=FieldName.START,
                target_field=FieldName.TARGET,
                output_field=FieldName.FEAT_TIME,
                time_features=time_features_from_frequency_str(freq),
                pred_length=config.prediction_length,
            ),
            # 步驟 6: 新增另一個暫存值 (一個單一數字)
            # 用於讓模型知道當前值在時間序列中的位置
            # 類似於一個步進計數器
            AddAgeFeature(
                target_field=FieldName.TARGET,
                output_field=FieldName.FEAT_AGE,
                pred_length=config.prediction_length,
                log_scale=True,
            ),
            # 步驟 7: 將所有暫存特徵值縱向堆疊
            VstackFeatures(
                output_field=FieldName.FEAT_TIME,
                input_fields=[FieldName.FEAT_TIME, FieldName.FEAT_AGE]
                + ([FieldName.FEAT_DYNAMIC_REAL] if config.num_dynamic_real_features > 0 else []),
            ),
            # 步驟 8: 建立欄位名和 Hugging Face 慣用欄位名之間的對映
            RenameFields(
                mapping={
                    FieldName.FEAT_STATIC_CAT: "static_categorical_features",
                    FieldName.FEAT_STATIC_REAL: "static_real_features",
                    FieldName.FEAT_TIME: "time_features",
                    FieldName.TARGET: "values",
                    FieldName.OBSERVED_VALUES: "observed_mask",
                }
            ),
        ]
    )

定義 InstanceSplitter

對於訓練、驗證、測試步驟,接下來我們建立一個 InstanceSplitter,用於從資料集中對視窗進行取樣 (因為由於時間和記憶體限制,我們無法將整個歷史值傳遞給 Transformer)。

例項拆分器從資料中隨機取樣大小為 context_length 和後續大小為 prediction_length 的視窗,並將 past_future_ 鍵附加到各個視窗的任何臨時鍵。這確保了 values 被拆分為 past_values 和後續的 future_values 鍵,它們將分別用作編碼器和解碼器的輸入。同樣我們還需要修改 time_series_fields 引數中的所有鍵:

from gluonts.transform.sampler import InstanceSampler
from typing import Optional

def create_instance_splitter(config: PretrainedConfig, mode: str, train_sampler: Optional[InstanceSampler] = None,
    validation_sampler: Optional[InstanceSampler] = None,) -> Transformation:
    assert mode in ["train", "validation", "test"]

    instance_sampler = {
        "train": train_sampler or ExpectedNumInstanceSampler(
            num_instances=1.0, min_future=config.prediction_length
        ),
        "validation":  validation_sampler or ValidationSplitSampler(
            min_future=config.prediction_length
        ),
        "test": TestSplitSampler(),
    }[mode]

    return InstanceSplitter(
        target_field="values",
        is_pad_field=FieldName.IS_PAD,
        start_field=FieldName.START,
        forecast_start_field=FieldName.FORECAST_START,
        instance_sampler=instance_sampler,
        past_length=config.context_length + max(config.lags_sequence),
        future_length=config.prediction_length,
        time_series_fields=[
            "time_features",
            "observed_mask",
        ],
    )

以上是《使用 ? Transformers 進行機率時間序列預測》的第一部分,我們在這部分中為大家介紹了傳統時間序列預測和基於 Transformers 的方法,也一步步準備好了訓練所需的資料集並定義了環境、模型、轉換和 InstanceSplitter。在下一部分裡,我們會開始訓練,並採用視覺化的方法評估模型預測的效果。請關注我們的內容,不要錯過後續精彩。

英文原文: Probabilistic Time Series Forecasting with ? Transformers

譯者、排版: zhongdongy (阿東)

相關文章