关于人工智能:上篇-使用-🤗-Transformers-进行概率时间序列预测

介绍

工夫序列预测是一个重要的迷信和商业问题,因而最近通过应用基于深度学习 而不是经典办法的模型也涌现出诸多翻新。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 (阿东)

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理