介紹
時間序列預測是一個重要的科學和商業問題,因此最近透過使用基於深度學習 而不是經典方法的模型也湧現出諸多創新。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
})
})
每個示例都包含一些鍵,其中 start
和 target
是最重要的鍵。讓我們看一下資料集中的第一個時間序列:
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
這裡我們使用 datasets
的 set_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
,因為我們將新增MonthOfYear
和Age
特徵; - 靜態類別型特徵的數量: 在我們的例子中,這將只是
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 (阿東)