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

HuggingFace發表於2023-02-22

在《使用 ? Transformers 進行機率時間序列預測》的第一部分裡,我們為大家介紹了傳統時間序列預測和基於 Transformers 的方法,也一步步準備好了訓練所需的資料集並定義了環境、模型、轉換和 InstanceSplitter。本篇內容將包含從資料載入器,到前向傳播、訓練、推理和展望未來發展等精彩內容。

建立 PyTorch 資料載入器

有了資料,下一步需要建立 PyTorch DataLoaders。它允許我們批次處理成對的 (輸入, 輸出) 資料,即 (past_values , future_values)。

from gluonts.itertools import Cyclic, IterableSlice, PseudoShuffled
from gluonts.torch.util import IterableDataset
from torch.utils.data import DataLoader

from typing import Iterable

def create_train_dataloader(
    config: PretrainedConfig,
    freq,
    data,
    batch_size: int,
    num_batches_per_epoch: int,
    shuffle_buffer_length: Optional[int] = None,
    **kwargs,
) -> Iterable:
    PREDICTION_INPUT_NAMES = [
        "static_categorical_features",
        "static_real_features",
        "past_time_features",
        "past_values",
        "past_observed_mask",
        "future_time_features",
        ]

    TRAINING_INPUT_NAMES = PREDICTION_INPUT_NAMES + [
        "future_values",
        "future_observed_mask",
        ]
    
    transformation = create_transformation(freq, config)
    transformed_data = transformation.apply(data, is_train=True)
    
    # we initialize a Training instance
    instance_splitter = create_instance_splitter(
        config, "train"
    ) + SelectFields(TRAINING_INPUT_NAMES)


    # the instance splitter will sample a window of 
    # context length + lags + prediction length (from the 366 possible transformed time series)
    # randomly from within the target time series and return an iterator.
    training_instances = instance_splitter.apply(
        Cyclic(transformed_data)
        if shuffle_buffer_length is None
        else PseudoShuffled(
            Cyclic(transformed_data), 
            shuffle_buffer_length=shuffle_buffer_length,
        )
    )

    # from the training instances iterator we now return a Dataloader which will 
    # continue to sample random windows for as long as it is called
    # to return batch_size of the appropriate tensors ready for training!
    return IterableSlice(
        iter(
            DataLoader(
                IterableDataset(training_instances),
                batch_size=batch_size,
                **kwargs,
            )
        ),
        num_batches_per_epoch,
    )
def create_test_dataloader(
    config: PretrainedConfig,
    freq,
    data,
    batch_size: int,
    **kwargs,
):
    PREDICTION_INPUT_NAMES = [
        "static_categorical_features",
        "static_real_features",
        "past_time_features",
        "past_values",
        "past_observed_mask",
        "future_time_features",
        ]
    
    transformation = create_transformation(freq, config)
    transformed_data = transformation.apply(data, is_train=False)
    
    # we create a Test Instance splitter which will sample the very last 
    # context window seen during training only for the encoder.
    instance_splitter = create_instance_splitter(
        config, "test"
    ) + SelectFields(PREDICTION_INPUT_NAMES)
    
    # we apply the transformations in test mode
    testing_instances = instance_splitter.apply(transformed_data, is_train=False)
    
    # This returns a Dataloader which will go over the dataset once.
    return DataLoader(IterableDataset(testing_instances), batch_size=batch_size, **kwargs)
train_dataloader = create_train_dataloader(
    config=config, 
    freq=freq, 
    data=train_dataset, 
    batch_size=256, 
    num_batches_per_epoch=100,
)

test_dataloader = create_test_dataloader(
    config=config, 
    freq=freq, 
    data=test_dataset,
    batch_size=64,
)

讓我們檢查第一批:

batch = next(iter(train_dataloader))
for k,v in batch.items():
  print(k,v.shape, v.type())

>>> static_categorical_features torch.Size([256, 1]) torch.LongTensor
    static_real_features torch.Size([256, 1]) torch.FloatTensor
    past_time_features torch.Size([256, 181, 2]) torch.FloatTensor
    past_values torch.Size([256, 181]) torch.FloatTensor
    past_observed_mask torch.Size([256, 181]) torch.FloatTensor
    future_time_features torch.Size([256, 24, 2]) torch.FloatTensor
    future_values torch.Size([256, 24]) torch.FloatTensor
    future_observed_mask torch.Size([256, 24]) torch.FloatTensor

可以看出,我們沒有將 input_idsattention_mask 提供給編碼器 (訓練 NLP 模型時也是這種情況),而是提供 past_values,以及 past_observed_maskpast_time_featuresstatic_categorical_featuresstatic_real_features 幾項資料。

解碼器的輸入包括 future_valuesfuture_observed_maskfuture_time_featuresfuture_values 可以看作等同於 NLP 訓練中的 decoder_input_ids

我們可以參考 Time Series Transformer 文件 以獲得對它們中每一個的詳細解釋。

前向傳播

讓我們對剛剛建立的批次執行一次前向傳播:

# perform forward pass
outputs = model(
    past_values=batch["past_values"],
    past_time_features=batch["past_time_features"],
    past_observed_mask=batch["past_observed_mask"],
    static_categorical_features=batch["static_categorical_features"],
    static_real_features=batch["static_real_features"],
    future_values=batch["future_values"],
    future_time_features=batch["future_time_features"],
    future_observed_mask=batch["future_observed_mask"],
    output_hidden_states=True
)
print("Loss:", outputs.loss.item())

>>> Loss: 9.141253471374512

目前,該模型返回了損失值。這是由於解碼器會自動將 future_values 向右移動一個位置以獲得標籤。這允許計算預測結果和標籤值之間的誤差。

另請注意,解碼器使用 Causal Mask 來避免預測未來,因為它需要預測的值在 future_values 張量中。

訓練模型

是時候訓練模型了!我們將使用標準的 PyTorch 訓練迴圈。

這裡我們用到了 ? Accelerate 庫,它會自動將模型、最佳化器和資料載入器放置在適當的 device 上。

from accelerate import Accelerator
from torch.optim import Adam

accelerator = Accelerator()
device = accelerator.device

model.to(device)
optimizer = Adam(model.parameters(), lr=1e-3)
 
model, optimizer, train_dataloader = accelerator.prepare(
    model, optimizer, train_dataloader, 
)

for epoch in range(40):
    model.train()
    for batch in train_dataloader:
        optimizer.zero_grad()
        outputs = model(
            static_categorical_features=batch["static_categorical_features"].to(device),
            static_real_features=batch["static_real_features"].to(device),
            past_time_features=batch["past_time_features"].to(device),
            past_values=batch["past_values"].to(device),
            future_time_features=batch["future_time_features"].to(device),
            future_values=batch["future_values"].to(device),
            past_observed_mask=batch["past_observed_mask"].to(device),
            future_observed_mask=batch["future_observed_mask"].to(device),
        )
        loss = outputs.loss

        # Backpropagation
        accelerator.backward(loss)
        optimizer.step()

        print(loss.item())

推理

在推理時,建議使用 generate() 方法進行自迴歸生成,類似於 NLP 模型。

預測的過程會從測試例項取樣器中獲得資料。取樣器會將資料集的每個時間序列的最後 context_length 那麼長時間的資料取樣出來,然後輸入模型。請注意,這裡需要把提前已知的 future_time_features 傳遞給解碼器。

該模型將從預測分佈中自迴歸取樣一定數量的值,並將它們傳回解碼器最終得到預測輸出:

model.eval()

forecasts = []

for batch in test_dataloader:
    outputs = model.generate(
        static_categorical_features=batch["static_categorical_features"].to(device),
        static_real_features=batch["static_real_features"].to(device),
        past_time_features=batch["past_time_features"].to(device),
        past_values=batch["past_values"].to(device),
        future_time_features=batch["future_time_features"].to(device),
        past_observed_mask=batch["past_observed_mask"].to(device),
    )
    forecasts.append(outputs.sequences.cpu().numpy())

該模型輸出一個表示結構的張量 (batch_size, number of samples, prediction length)。

下面的輸出說明: 對於大小為 64 的批次中的每個示例,我們將獲得接下來 24 個月內的 100 個可能的值:

forecasts[0].shape

>>> (64, 100, 24)

我們將垂直堆疊它們,以獲得測試資料集中所有時間序列的預測:

forecasts = np.vstack(forecasts)
print(forecasts.shape)

>>> (366, 100, 24)

我們可以根據測試集中存在的樣本值,根據真實情況評估生成的預測。這裡我們使用資料集中的每個時間序列的 MASEsMAPE 指標 (metrics) 來評估:

from evaluate import load
from gluonts.time_feature import get_seasonality

mase_metric = load("evaluate-metric/mase")
smape_metric = load("evaluate-metric/smape")

forecast_median = np.median(forecasts, 1)

mase_metrics = []
smape_metrics = []
for item_id, ts in enumerate(test_dataset):
    training_data = ts["target"][:-prediction_length]
    ground_truth = ts["target"][-prediction_length:]
    mase = mase_metric.compute(
        predictions=forecast_median[item_id], 
        references=np.array(ground_truth), 
        training=np.array(training_data), 
        periodicity=get_seasonality(freq))
    mase_metrics.append(mase["mase"])
    
    smape = smape_metric.compute(
        predictions=forecast_median[item_id], 
        references=np.array(ground_truth), 
    )
    smape_metrics.append(smape["smape"])
print(f"MASE: {np.mean(mase_metrics)}")

>>> MASE: 1.361636922541396

print(f"sMAPE: {np.mean(smape_metrics)}")

>>> sMAPE: 0.17457818831512306

我們還可以單獨繪製資料集中每個時間序列的結果指標,並觀察到其中少數時間序列對最終測試指標的影響很大:

plt.scatter(mase_metrics, smape_metrics, alpha=0.3)
plt.xlabel("MASE")
plt.ylabel("sMAPE")
plt.show()

為了根據基本事實測試資料繪製任何時間序列的預測,我們定義了以下輔助繪圖函式:

import matplotlib.dates as mdates

def plot(ts_index):
    fig, ax = plt.subplots()

    index = pd.period_range(
        start=test_dataset[ts_index][FieldName.START],
        periods=len(test_dataset[ts_index][FieldName.TARGET]),
        freq=freq,
    ).to_timestamp()

    # Major ticks every half year, minor ticks every month,
    ax.xaxis.set_major_locator(mdates.MonthLocator(bymonth=(1, 7)))
    ax.xaxis.set_minor_locator(mdates.MonthLocator())

    ax.plot(
        index[-2*prediction_length:], 
        test_dataset[ts_index]["target"][-2*prediction_length:],
        label="actual",
    )

    plt.plot(
        index[-prediction_length:], 
        np.median(forecasts[ts_index], axis=0),
        label="median",
    )
    
    plt.fill_between(
        index[-prediction_length:],
        forecasts[ts_index].mean(0) - forecasts[ts_index].std(axis=0), 
        forecasts[ts_index].mean(0) + forecasts[ts_index].std(axis=0), 
        alpha=0.3, 
        interpolate=True,
        label="+/- 1-std",
    )
    plt.legend()
    plt.show()

例如:

plot(334)

我們如何與其他模型進行比較? Monash Time Series Repository 有一個測試集 MASE 指標的比較表。我們可以將自己的結果新增到其中作比較:

DatasetSESThetaTBATSETS(DHR-)ARIMAPRCatBoostFFNNDeepARN-BEATSWaveNetTransformer (Our)
Tourism Monthly3.3061.6491.7511.5261.5891.6781.6991.5821.4091.5741.4821.361

請注意,我們的模型擊敗了所有已知的其他模型 (另請參見相應 論文 中的表 2) ,並且我們沒有做任何超引數最佳化。我們僅僅花了 40 個完整訓練調參週期來訓練 Transformer。

當然,我們應該謙虛。從歷史發展的角度來看,現在認為神經網路解決時間序列預測問題是正途,就好比當年的論文得出了 “你需要的就是 XGBoost” 的結論。我們只是很好奇,想看看神經網路能帶我們走多遠,以及 Transformer 是否會在這個領域發揮作用。這個特定的資料集似乎表明它絕對值得探索。

下一步

我們鼓勵讀者嘗試我們的 Jupyter Notebook 和來自 Hugging Face Hub 的其他時間序列資料集,並替換適當的頻率和預測長度引數。對於您的資料集,需要將它們轉換為 GluonTS 的慣用格式,在他們的 文件 裡有非常清晰的說明。我們還準備了一個示例 Notebook,向您展示如何將資料集轉換為 ? Hugging Face 資料集格式。

正如時間序列研究人員所知,人們對“將基於 Transformer 的模型應用於時間序列”問題很感興趣。傳統 vanilla Transformer 只是眾多基於注意力 (Attention) 的模型之一,因此需要向庫中補充更多模型。

目前沒有什麼能妨礙我們繼續探索對多變數時間序列 (multivariate time series) 進行建模,但是為此需要使用多變數分佈頭 (multivariate distribution head) 來例項化模型。目前已經支援了對角獨立分佈 (diagonal independent distributions),後續會增加其他多元分佈支援。請繼續關注未來的部落格文章以及其中的教程。

路線圖上的另一件事是時間序列分類。這需要將帶有分類頭的時間序列模型新增到庫中,例如用於異常檢測這類任務。

當前的模型會假設日期時間和時間序列值都存在,但在現實中這可能不能完全滿足。例如 WOODS 給出的神經科學資料集。因此,我們還需要對當前模型進行泛化,使某些輸入在整個流水線中可選。

最後,NLP/CV 領域從大型預訓練模型 中獲益匪淺,但據我們所知,時間序列領域並非如此。基於 Transformer 的模型似乎是這一研究方向的必然之選,我們迫不及待地想看看研究人員和從業者會發現哪些突破!


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

譯者、排版: zhongdongy (阿東)

相關文章