Алгоритм прогнозирования DeepAR

По сей день прогнозирование остается одним из самых ценных приложений машинного обучения. Например, мы могли бы использовать модель для прогнозирования спроса на продукт. Затем эту информацию можно использовать для оптимизации цепочки поставок. Другими словами, мы могли бы гарантировать, что у нас будет достаточно запасов, чтобы удовлетворить спрос. Таким образом, высвобождая капитал, который можно направить на другие средства.

Предположим, что мы работаем над сайтом электронной коммерции, который продает множество товаров. Раньше мы в конечном итоге строили модель для каждого отдельного временного ряда. У этого подхода есть две основные проблемы.

  1. Даже при автоматизации поддержка моделей для тысяч временных рядов усложняет процесс, что требует большого количества человеко-часов.
  2. Мы не фиксируем отношения между различными временными рядами. Например, спрос на один продукт (например, хот-доги) повлияет на спрос на его заменитель (заменители) (например, гамбургеры).

Здесь на сцену выходит DeepAR. DeepAR — это рекуррентная нейронная сеть на основе LSTM, которая обучается на исторических данных ВСЕХ временных рядов в наборе данных. Обучаясь на нескольких временных рядах одновременно, модель DeepAR изучает сложное, зависящее от группы поведение между временными рядами, что часто приводит к более высокой производительности, чем стандартные методы ARIMA и ETS.

Есть несколько проблем, которые могут возникнуть в результате использования одной модели для нескольких временных рядов. Наиболее заметными из которых являются:

  • Единицы времени в разных временных рядах могут различаться (например, часы, дни, месяцы).
  • Начальные точки t = 1 могут относиться к разным абсолютным моментам времени для разных временных рядов. Например, временной ряд продукта а мог начаться в марте 1996 г., тогда как временной ряд продукта b мог начаться в феврале 1998 г. (когда он был впервые выпущен).

Существуют также проблемы, характерные для моделирования временных рядов в целом. Конкретно:

  • Наличие пропущенных значений. В контексте прогнозирования спроса товар может отсутствовать на складе в определенное время, и в этом случае спрос на товар невозможно отследить. Отсутствие явного учета таких отсутствующих наблюдений (например, при допущении, что наблюдаемые продажи соответствуют спросу, даже когда товара нет на складе), в лучшем случае может привести к систематической недооценке прогноза.
  • Наличие аномалий/выбросов. Качество этого подхода, основанного на прогнозировании, зависит от частоты таких аномалий в обучающих данных и лучше всего работает, если известные аномалии удаляются или маскируются.

К счастью для нас, библиотека прогнозирования PyTorch предоставляет класс набора данных временных рядов, который позаботится о предыдущих проблемах (например, отсутствующие значения, несколько длин истории, выбросы) для нас автоматически под капотом.

pip install pytorch-forecasting

Для начала откройте блокнот Jupyter и импортируйте следующие библиотеки:

import pandas as pd
import torch
import pytorch_lightning as pl
import matplotlib.pyplot as plt
from pytorch_forecasting import Baseline, DeepAR, TimeSeriesDataSet
from pytorch_lightning.callbacks import EarlyStopping
from pytorch_forecasting.metrics import SMAPE, MultivariateNormalDistributionLoss

Мы скачиваем и загружаем набор данных из Kaggle, содержащий данные о продажах на уровне товаров в разных магазинах.

train_df = pd.read_csv('demand-forecasting-kernels-only/train.csv')

Набор данных имеет 4 столбца: дата, номер магазина, номер товара и объем продаж.

train_df.head()

Библиотека ожидает, что цель будет иметь тип float. Таким образом, мы формируем столбец продаж соответствующим образом. Кроме того, столбцы store и item должны интерпретироваться как категориальные переменные (т. е. строки), а не целые числа.

train_df['date'] = pd.to_datetime(train_df['date'], errors='coerce')
train_df[['store', 'item']] = train_df[['store', 'item']].astype(str)
train_df['sales'] = pd.to_numeric(train_df['sales'], downcast='float')

Нам нужно добавить временной индекс, который увеличивается на единицу для каждого дополнительного временного шага. Сначала мы вычисляем минимальную дату, а затем используем ее для получения относительного количества дней, прошедших с начала набора данных.

min_date = train_df['date'].min()
train_df["time_idx"] = train_df["date"].map(lambda current_date: (current_date - min_date).days)

Мы будем использовать данные за последние 60 дней, чтобы сделать прогноз на следующие 20 дней.

max_encoder_length = 60 # days
max_prediction_length = 20 # 20 days
training_cutoff = train_df["time_idx"].max() - max_prediction_length

Мы создаем экземпляр класса TimeSeriesDataSet.

training = TimeSeriesDataSet(
    train_df[lambda x: x.time_idx <= training_cutoff],
    time_idx="time_idx",
    target="sales",
    group_ids=["store", "item"], # list of column names identifying a time series.
    max_encoder_length=max_encoder_length,
    max_prediction_length=max_prediction_length,
    static_categoricals=["store", "item"], # categorical variables that do not change over time (e.g. product categories)
    time_varying_unknown_reals=[
        "sales"
    ],
)

Мы откладываем часть данных для проверки.

validation = TimeSeriesDataSet.from_dataset(training, train_df, min_prediction_idx=training_cutoff + 1)

При обучении модели мы обычно хотим передавать образцы «мини-пакетами», перетасовывать данные в каждую эпоху, чтобы уменьшить переобучение модели, и использовать multiprocessing Python для ускорения поиска данных.

DataLoaders абстрагирует эту сложность для нас в простом API.

batch_size = 128
train_dataloader = training.to_dataloader(
    train=True, batch_size=batch_size, num_workers=0
)
val_dataloader = validation.to_dataloader(
    train=False, batch_size=batch_size, num_workers=0
)

Мы устанавливаем случайное начальное число, чтобы гарантировать воспроизводимость результатов.

pl.seed_everything(42)

Мы создаем экземпляр класса Trainer и говорим ему использовать GPU с индексом 0.

trainer = pl.Trainer(
    gpus=[0],
    gradient_clip_val=0.1,
)

Мы создаем нейронную сеть с 2 слоями и 30 узлами на каждом слое.

net = DeepAR.from_dataset(
    training,
    learning_rate=3e-2,
    hidden_size=30,
    rnn_layers=2,
    loss=MultivariateNormalDistributionLoss(rank=30)
)

Затем мы получаем рекомендуемую скорость обучения.

res = trainer.tuner.lr_find(
    net,
    train_dataloaders=train_dataloader,
    val_dataloaders=val_dataloader,
    min_lr=1e-5,
    max_lr=1e0,
    early_stop_threshold=100,
)
suggested learning rate: 0.7079457843841377

Полученный объект имеет функцию графика, которую можно использовать для визуализации оптимальной скорости обучения.

fig = res.plot(show=True, suggest=True)
fig.show()

При обучении модели мы хотим остановиться раньше, если потеря на проверочном наборе меньше 0,0001.

early_stop_callback = EarlyStopping(monitor="val_loss", min_delta=1e-4, patience=10, verbose=False, mode="min")

Мы определяем еще один экземпляр класса Trainer.

trainer = pl.Trainer(
    max_epochs=30,
    gpus=[0],
    enable_model_summary=True,
    gradient_clip_val=0.1,
    callbacks=[early_stop_callback],
    limit_train_batches=50,
    enable_checkpointing=True,
)

Мы определяем другую RNN, указав скорость обучения, которую мы вычислили ранее.

net = DeepAR.from_dataset(
    training,
    learning_rate=0.7,
    log_interval=10,
    log_val_interval=1,
    hidden_size=30,
    rnn_layers=2,
    loss=MultivariateNormalDistributionLoss(rank=30),
)

Наконец, мы обучаем модель.

trainer.fit(
    net,
    train_dataloaders=train_dataloader,
    val_dataloaders=val_dataloader,
)

Загружаем модель с лучшей производительностью с контрольной точки.

best_model_path = trainer.checkpoint_callback.best_model_path
best_model = DeepAR.load_from_checkpoint(best_model_path)

Мы используем модель для прогнозирования данных в наборе данных проверки и построения графика результатов для 2 временных рядов.

# Predict raw (unnormalized) values, return x = time step
raw_predictions, x = best_model.predict(val_dataloader, mode="raw", return_x=True)
for idx in range(2):
    best_model.plot_prediction(
        x,
        raw_predictions,
        idx=idx,
        add_loss_to_title=SMAPE()
    )