Всем привет! Меня зовут Дмитрий Поляков, я работаю аналитиком данных в команде ad-hoc аналитики X5 Tech. В этой статье мы хотели бы рассмотреть задачу прогнозирования, которая является чрезвычайно важной задачей в ритейле. Точные прогнозы позволяют оптимально планировать объёмы товаров и запасы, распределять бюджет, устанавливать бизнес-цели и решать множество других задач. В X5 применяются десятки моделей прогнозирования, каждая из которых помогает решать конкретные задачи.

Наша ad-hoc команда занимается решением самых разнообразных задач. Во время работы над одной из них мы столкнулись с необходимостью использования прогнозов РТО (розничного товарооборота) и трафика (количества чеков) в магазинах сетей “Пятёрочка” и “Перекрёсток”, сформированных существующей моделью. Однако применение этих прогнозов в исходном виде оказалось невозможным, поэтому потребовалась их адаптация под специфику нашей задачи. Учитывая ограниченные ресурсы и сжатые сроки, мы сосредоточились на поиске простого и эффективного подхода. Так возникла идея создания модели, которая опирается на существующие прогнозы, сохраняя при этом использование всех признаков текущей модели. В процессе работы мы обнаружили, что данный подход не только успешно решает нашу ad-hoc задачу, но и в целом обладает потенциалом для повышения точности прогнозирования.

После проведения серии экспериментов в качестве основной модели была выбрана Temporal Fusion Transformer (TFT) — одна из самых современных моделей для прогнозирования временных рядов, разработанная в Google в 2019 году. В этой статье мы детально рассмотрим основные преимущества и архитектурные особенности TFT, наш подход к использованию этой модели в задаче прогнозирования спроса, и как нам удалось увеличить точность прогнозов в среднем на 7%, затратив при этом минимальные усилия.

Также эта статья будет полезна и тем, кто хочет глубже понять принципы работы TFT, изучить её применение в библиотеке Darts и решить задачу прогнозирования для множества многомерных временных рядов.

Temporal Fusion Transformer

Особенности и преимущества TFT

Модель TFT была выбрана благодаря множеству преимуществ перед другими архитектурами. Выделим некоторые из них (пускай даже очевидные):

  1. Учёт нелинейных зависимостей

Данные временных рядов часто содержат сложные взаимодействия, множественные сезонные колебания и разнообразные мультифакторные зависимости. TFT, как и другие нейросетевые модели, способна улавливать сложные взаимосвязи. Наиболее сильно этому способствует использование механизма внимания и рекуррентных слоёв, что позволяет эффективно извлекать как короткие, так и долгосрочные зависимости в данных.

  1. Автоматическое извлечение признаков

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

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

  1. Использование дополнительных данных и внешней информации

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

TFT может работать с тремя видами дополнительных данных, которые будем называть ковариатами:

  • Ковариаты прошлого – это временные ряды с известными значениями до момента прогнозирования.

  • Ковариаты будущего – это временные ряды, значения которых известны не только в прошлом, но и на весь горизонт прогнозирования.

  • Статические ковариаты – постоянные признаки временных рядов, не изменяющиеся во времени. TFT умеет работать как с категориальными, так и с непрерывными признаками.

  1. Работа с многомерными временными рядами

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

TFT поддерживает одномерные и многомерные временные ряды, адаптируясь к различным данным.

  1. Масштабируемость для обработки множества временных рядов

В ритейле (и не только) редко задача ограничивается одним временным рядом, часто прогноз необходимо выполнить для тысяч объектов. Классические модели, такие как ARIMA или даже LSTM, обычно плохо масштабируются для работы с множеством временных рядов и требуют отдельной модели для каждого ряда, что приводит к значительным вычислительным затратам.

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

  1. Multi-horizon forecasting

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

  1. Интерпретируемость результатов

Бизнесу зачастую важно не только получать точные прогнозы, но и понимать, как модель принимает решения. Однако чем сложнее модель, тем труднее объяснить её поведение. Нейронные сети и другие сложные модели часто называют «чёрным ящиком» из-за их низкой интерпретируемости. В некоторых задачах объяснимость модели даже важнее её точности, так как прозрачные модели укрепляют доверие к прогнозам и поддерживают принятие обоснованных решений.

Несмотря на свою сложную архитектуру, TFT предоставляет возможности для интерпретации. Механизм Variable Selection Network позволяет оценивать значимость и вклад различных признаков в итоговый прогноз. Дополнительно улучшенная архитектура Multi-Head Attention позволяет выделять важные временные точки, наиболее сильно влияющие на предсказание.

Архитектура TFT

На рисунке представлена архитектура Temporal Fusion Transformer. Введём некоторые обозначения: пусть t – дата прогнозирования, k – некоторое конечное окно рассматриваемых значений назад (размерность энкодера), \tau – количество прогнозируемых значений вперёд (размерность декодера). Тогда модель в качестве входных данных принимает:

  • Статические ковариаты: s \in \mathbb{R}^{m_s}, где m_s – количество таких ковариат.

  • Ковариаты прошлого: z_{t-k:t} \in \mathbb{R}^{m_z}, где m_z – количество таких ковариат.

  • Ковариаты будущего: x_{t-k:t+\tau} \in \mathbb{R}^{m_x}, где m_x – количество таких ковариат.

Целевые переменные обрабатываются подобно ковариатам прошлого: y_{t-k:t} \in \mathbb{R}^{m_y}, где m_y – кол-во компонент прогнозируемого временного ряда.

В оригинальной статье TFT обучается с помощью квантильной регрессии, что позволяет выполнять вероятностные прогнозы, но, конечно, модель можно настроить и для детерминированных прогнозов. Более подробно обсудим это дальше.

Таким образом, модель имеет следующий вид:

\hat{y}(q, t, \tau) = f_q(\tau, y_{t-k:t}, z_{t-k:t}, x_{t-k:t+\tau},s)

где q – это прогнозируемый q-ый квантиль.

Рассмотрим более подробно основные компоненты архитектуры.

Gated Residual Network

Блок Gated Residual Network (GRN) встречается во многих местах архитектуры TFT и выполняет несколько важных функций.

Основная цель GRN — определить, требуется ли добавление нелинейности к входным данным. В случае, если более простое, линейное преобразование достаточно для представления данных, блок GRN автоматически регулируется, чтобы пропустить ненужные усложнения. Это достигается за счёт функции активации ELU (Exponential Linear Unit):

f(x, \alpha) = \begin{cases} x   & \quad \text{if } x \geq 0 \\  \alpha (e^x - 1)    & \quad \text{if } x < 0 \end{cases}

Если значения на выходе первого полносвязного слоя малы, то ELU работает почти в линейном режиме, выдавая практически константные значения. Это приводит к линейному поведению всего блока, позволяя модели адаптироваться под разные типы данных и сохранять простоту там, где это возможно. Если же данные требуют более сложного преобразования, то ELU добавляет нелинейность, необходимую для выявления скрытых паттернов.

Ещё одна важная цель GRN — повысить гибкость модели и контролировать поток информации, что достигается благодаря механизмам Gating на основе функции активации GLU (Gated Linear Unit):

GLU(x) = \sigma (W \cdot x + b) \otimes (V \cdot x + c)

Гейты позволяют динамически подавлять менее значимые признаки, настраивая сложность модели в зависимости от особенностей входных данных. В случаях, когда выход GLU близок к нулю, блок GRN пропускает входные данные напрямую через Residual Connection, минимизируя любые ненужные преобразования.

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

Variable Selection Network

Блок Variable Selection Network (VSN) является первым блоком архитектуры, поэтому сначала здесь важно описать, как кодируются и поступают входные данные. Непрерывные числовые признаки преобразуются в векторы с помощью линейных преобразований, а категориальные признаки представляются в виде плотных векторов с использованием Entity Embeddings.

После кодирования для каждого типа входных данных (статические ковариаты, известные значения прошлого и будущего) обучается отдельная сеть VSN. Это позволяет учитывать специфику каждого признака и адаптироваться к их различным ролям в прогнозировании.

Ключевым компонентом каждого VSN является уже знакомый блок GRN, причём для обработки каждого признака используется отдельный GRN, параметры которого в рамках каждого признака одинаковы для всех временных шагов. Это позволяет обеспечивать согласованность обработки данных на всём временном интервале.

Таким образом, VSN служит первым уровнем обработки данных и выполняет два ключевых этапа:

  • Отбор значимых признаков, которые оказывают наибольшее влияние на прогноз.

  • Подавление менее релевантных признаков, что уменьшает шум и повышает общую эффективность модели.

Static Covariate Encoders

В блоках GRN и VSN вы уже могли заметить, что опционально в них используются так называемые External Context Vectors. В архитектуре TFT статическая информация о временном ряде играет важную роль, и ее интеграция осуществляется с помощью блока Static Covariate Encoders. Он и формирует эти внешние контекстные вектора (external context vectors), которые передаются в другие части модели. Они помогают адаптировать обработку временных данных в зависимости от уникальных характеристик временного ряда.

Static Covariate Encoders состоит из четырёх отдельных GRN, каждый из которых генерирует один из следующих контекстных векторов:

  • c_s – контекст для блоков VSN, которые обрабатывают ковариаты прошлого и будущего.

  • c_c, c_h – контексты для LSTM энкодера, влияющие на моделирование временных зависимостей.

  • c_e – контекст для блока Static Enrichment, где статическая информация используется для дополнения динамических признаков.

Такое добавление статической информации в разные компоненты модели позволяет эффективно использовать уникальные характеристики временного ряда на всех уровнях архитектуры, подчёркивая роль статических ковариат в прогнозировании.

LSTM Encoder-Decoder Layer

Для эффективного улавливания последовательных зависимостей во временных рядах в архитектуре TFT используется LSTM энкодер-декодер. Несмотря на наличие в модели механизма внимания, авторы выбрали именно LSTM, а не стандартную архитектуру трансформера с позиционным кодированием. Этот выбор обусловлен способностью LSTM учитывать не только временные зависимости, но и влияние статических ковариат, что особенно важно для временных рядов, где глобальные (независящие от времени) особенности могут существенно влиять на динамику.

Для использования статической информации скрытое состояние LSTM энкодера инициализируется значением контекстного вектора c_h, а начальное состояние ячейки — значением c_c, которые были предварительно получены на предыдущем шаге с помощью Static Covariate Encoders. Это позволяет LSTM с самого начала учитывать ключевые особенности временного ряда, обеспечивая более точную обработку временных зависимостей.

Interpretable Multi-Head Attention

В модели TFT применяется механизм внимания, позволяющий учитывать долгосрочные зависимости и фокусироваться на наиболее значимых временных точках. Для захвата различных зависимостей используется Multi-Head Attention, но с некоторыми модификациями, направленными на повышение интерпретируемости результатов.

В классической реализации Multi-Head Attention каждая голова внимания использует свои матрицы запросов Q, ключей K и значений V. Но использование отдельных матриц значений V для каждой головы затрудняет интерпретацию вклада каждой головы в итоговый результат. Поэтому авторы TFT предложили использовать единую матрицу V для всех голов. Это делает возможным суммирование результатов внимания по всем головам и позволяет более прозрачно анализировать их вклад в итоговый прогноз.

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

Функция потерь для обучения TFT

Модель Temporal Fusion Transformer адаптирована для обучения с использованием квантильной регрессии, что позволяет ей предсказывать не просто одно значение, а определённые квантили распределения целевой переменной. Это расширяет возможности модели, позволяя учитывать различные вероятные исходы, такие как оптимистичный, медианный или пессимистичный сценарий. Вероятностные прогнозы позволяют строить доверительные интервалы для предсказаний, что особенно полезно в задачах, где важно учитывать риски.

Хотя использование квантильной регрессии предоставляет значительные преимущества, это не является единственным вариантом. Модель может быть переведена в детерминированный режим для предсказания единственного значения, используя для этого стандартные функции потерь, такие как MAE или MSE.

Кроме того, анализ различий между прогнозами для различных квантилей может служить инструментом интерпретации уверенности модели. Если прогнозы для разных квантилей сильно различаются, то это указывает на высокую неопределённость. Фактически, ширина доверительного интервала это и отражает.

Обучение модели осуществляется путем минимизации суммарной взвешенной ошибки по всем рассматриваемым квантилям. В оригинальной статье авторы используют квантили \mathcal{Q} = {0.1, 0.5, 0.9} для сравнения с бенчмарками. Однако на практике мы будем использовать большее количество квантилей для повышения точности прогнозирования.

В общем виде функцию ошибки можно записать следующим образом:

 \mathcal{L} = \sum_{y_t \in \mathcal{\omega}} \sum_{q \in \mathcal{Q}} \sum_{\tau = 1}^{\tau_{\text{max}}} \frac{\max( q \cdot (y - \hat{y}), (1-q) \cdot (\hat{y} - y) )}{M \tau_{\text{max}}}

где:

  • \mathcal{\omega} – набор тренировочных данных, состоящий из M элементов

  • y и \hat{y} – фактические и предсказанные значения соответственно

  • q – целевой квантиль

Практическая часть

Перейдём к описанию процесса прогнозирования. В качестве примера мы подробно разберём весь пайплайн построения прогноза для магазинов сети “Перекрёсток”.

Для построения модели использовалась библиотека Darts. Прогнозирование выполнялось одной моделью, которая одновременно предсказывала два целевых показателя – РТО и трафик, поэтому исторические данные продаж рассматривались как единый многомерный временной ряд. Поскольку динамика продаж в магазинах сетей “Пятёрочка” и “Перекрёсток” существенно различается, то для каждой сети обучалась отдельная модель.

Подготовка признаков

Признаки магазинов

Первым очевидным статическим признаком (ковариатой) является идентификатор магазина. В дополнение к нему мы добавили два категориальных признака и один числовой – это географические характеристики магазина и некоторые числовые показатели непосредственно самого магазина.

Некоторые параметры, которые также могли бы считаться статическими, рассматривались как ковариаты будущего, поскольку их значения хоть и редко, но периодически могут изменяться и существенно влиять на уровень продаж. Исторические значения этих признаков известны, а для будущих периодов использовались последние актуальные данные. В результате были созданы 3 временных ряда, являющихся ковариатами будущего. Но в общем случае такими признаками могут быть, например: температура, расстояние до конкурента, флаг проведения крупных промоакций.

Временные признаки

Другими очевидными ковариатами будущего являются признаки, вычисляемые из даты: день, месяц, год, день недели и день года. Поскольку большинство временных признаков цикличны, модели нужно показать, например, что за 12-м месяцем следует 1-й. Один из распространённых методов для этого – представление через синус и косинус, что позволяет сделать переходы между последовательными значениями равномерными. Признак года, наоборот, изменяется монотонно, поэтому его логично нормализовать в диапазон от 0 до 1. Эти признаки могут быть автоматически сгенерированы при создании модели, поэтому их настройка будет рассмотрена позже.

Визуально такие преобразования временных признаков на примере синуса можно представить следующим образом:

Праздничные дни

Особое внимание было уделено праздничным дням. Мы учитывали не только официальные праздники России, но и расширяли их продолжительность на основе исторических данных о динамике продаж. В некоторых регионах учитывались также локальные праздники, что позволяло повысить точность модели. Праздничные дни мы знаем заранее, поэтому они тоже, безусловно, являются ковариатами будущего.

Прогнозы существующей модели

И самые значимые ковариаты будущего — это уже выполненные прогнозы другой моделью. Так как для их создания необходимо указать не только будущие значения, но и исторические, то их формирование усложняется по нескольким причинам. Во-первых, поскольку прогнозы обновляются еженедельно, то они частично накладываются друг на друга, что может приводить к различному распределению ошибки на исторических и будущих данных. Во-вторых, качество прогнозов со временем улучшалось, поэтому уменьшающаяся ошибка также только запутает модель.

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

  • Сопоставили все исторические прогнозы с фактическими данными и рассчитали метрику MAPE.

  • Вычислили стандартное отклонение ошибки прогнозов.

  • Сгенерировали исторические значения прогнозов на основе усечённого нормального распределения, где средним значением служило фактическое значение прогноза, а границы определялись ошибкой MAPE на уровне 0.99 квантиля. Стандартное отклонение было вычислено на предыдущем шаге.

Объединение сгенерированных исторических значений с последними актуальными прогнозами позволило сформировать полные временные ряды ковариат будущего.

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

Датафрейм, включающий ковариаты будущего:

Формирование временных рядов

Основным объектом для работы с временными рядами в библиотеке Darts является darts.Timeseries. Фактически этот объект представляет собой трёхмерный массив размерности (time_index, dimensions, samples), который также может содержать дополнительную информацию о статических ковариатах. Рассмотрим основные характеристики:

  • time_index – временная метка, которая задаётся с помощью pandas.DateTimeIndex или pandas.RangeIndex. Он должен быть строго монотонным, без пропусков и с заданной частотой.

  • dimensions – определяет количество измерений временного ряда. Ряд может быть как одномерным (например, продажи одного товара в одном магазине), так и многомерным (например, одновременное прогнозирование РТО и трафика).

  • samples – указывает на тип временного ряда. Если значение равно 1, ряд считается детерминированным. Если больше 1, ряд является вероятностным и содержит несколько выборок для каждого момента времени.

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

Код формирования временных рядов

from darts import TimeSeries


ts_target_list = TimeSeries.from_group_dataframe(
    df=df_target,
    group_cols="id",
    time_col="date",
    value_cols=["rto", "traffic"],
    static_cols=["cat_feature_1", "cat_feature_1", "num_feature_1"],
)

ts_future_covs_list = TimeSeries.from_group_dataframe(
    df=df_future_covs,
    group_cols="id",
    time_col="date",
    value_cols=["rto_forecast", "traffic_forecast",
                "feature_1", "feature_2", "feature_3", "feature_4"],
)

Посмотрим на целевой временной ряд случайного магазина. Обратим внимание, что ряд является многомерным (component: 2), а также что он содержит статические ковариаты.

Кроме того, заметим, что у временного ряда есть атрибут "иерархия", который мы не будем использовать в данном случае. Однако стоит отметить, что Darts поддерживает работу с иерархическими временными рядами.

Иерархические временные ряды – это набор временных рядов, которые имеют структуру и взаимосвязаны между собой на разных уровнях. Примером иерархии может быть всё тот же ритейл. Например, если мы прогнозируем суммарные продажи для всей компании, то при этом также нужно учитывать прогнозы на более низких уровнях: например, на уровне регионов и отдельных магазинов.

В библиотеке Darts для работы с иерархическими временными рядами есть три подхода для согласования прогнозов:

  • Bottom-up – прогнозирование на низком уровне иерархии, а затем агрегирование прогнозов для получения прогнозов для более высоких уровней.

  • Top-down – прогнозирование на высоком уровне иерархии, а затем распределение прогнозов на более низкие уровни.

  • Minimum Trace Reconciliation – прогнозирование на всех уровнях иерархии, а затем минимизация ошибок между прогнозами на разных уровнях с учётом всей иерархической структуры.

Также посмотрим и на случайный временной ряд ковариат будущего:

Нормализация временных рядов

Важным этапом подготовки данных перед обучением большинства моделей является стандартизация и нормализация. Она необходима по нескольким причинам:

  • Многие модели машинного обучения, особенно нейронные сети, достигают более быстрой и эффективной сходимости.

  • Приведение признаков различной природы к одному масштабу предотвращает доминирование признаков с большими значениями над остальными.

  • Помогает уменьшить проблемы "взрывающихся" и "затухающих" градиентов, что делает процесс обучения нейронных сетей более стабильным.

В библиотеке Darts для нормализации и стандартизации временных рядов доступен класс Scaler, который поддерживает любой объект, предоставляющий метод fit_transform (например, любой scaler из scikit-learn). Это позволяет легко применять стандартизацию как для каждого временного ряда по отдельности, так и глобально — ко всем рядам одновременно.

Целевые переменные (РТО и трафик) каждого временного ряда индивидуально стандартизируются (с использованием StandardScaler из sklearn). Этот же метод применяется к ковариатам будущего, отвечающим за прогнозные значения, чтобы обеспечить единообразие между ними.

Для оставшихся ковариат будущего (кроме имеющихся прогнозов) используется нормализация в диапазон от 0 до 1. Причём, эти параметры обрабатываются глобально, то есть нормализация происходит по всем временным рядам, чтобы учитывать общий диапазон значений.

Преобразование статических ковариат также выполняется глобально, обеспечивая единый масштаб для всех временных рядов. При этом непрерывные числовые признаки нормализуем в диапазон от 0 до 1. А категориальные признаки, включая идентификатор магазина, кодируем порядковыми числами с помощью OrdinalEncoder.

Код нормализации временных рядов
from darts.dataprocessing.transformers import Scaler, StaticCovariatesTransformer
from sklearn.preprocessing import MinMaxScaler, StandardScaler, OrdinalEncoder


target_series_scaler = Scaler(
    scaler=StandardScaler(), global_fit=False
)
target_static_scaler = StaticCovariatesTransformer(
    transformer_num=MinMaxScaler(feature_range=(0, 1)),
    cols_cat=["id", "cat_feature_1", "cat_feature_2"],
    cols_num=["num_feature_1"],
)
ts_target_list = target_series_scaler.fit_transform(ts_target_list)
ts_target_list = target_static_scaler.fit_transform(ts_target_list)

# components:
# "rto_forecast", "traffic_forecast", "feature_1", "feature_2", "feature_3", "feature_4"
future_covs_standard_scaler = Scaler(
    scaler=StandardScaler(), global_fit=False
)
ts_future_covs_list = future_covs_standard_scaler.fit_transform(
    ts_future_covs_list,
    component_mask=[True, True, False, False, False, False],
)
future_covs_min_max_scaler = Scaler(
    scaler=MinMaxScaler(feature_range=(0, 1)), global_fit=True
)
ts_future_covs_list = future_covs_min_max_scaler.fit_transform(
    ts_future_covs_list,
    component_mask=[False, False, True, True, True, True]
)

Посмотрим на один из целевых временных рядов после преобразования:

Разделение временных рядов на train/val/test

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

Если длина энкодера окажется слишком короткой, модель может не уловить важные долгосрочные зависимости. С другой стороны, значительное увеличение длины может привести к тому, что модель начнёт учитывать нерелевантные или устаревшие данные, что негативно скажется на качестве прогноза.

Самый простой, но вычислительно затратный подход для выбора оптимальной длины энкодера — это перебор значений по сетке. Однако для выбора можно также проанализировать сезонные и трендовые зависимости во временном ряде:

  • Если данные подвержены сезонным колебаниям, то длина энкодера должна охватывать как минимум один полный цикл этих колебаний.

  • Если в данных наблюдаются долгосрочные тренды, то длина энкодера также должна быть выбрана с учётом этого периода.

Для визуального анализа можно построить график функции автокорреляции (ACF) для переменных РТО и трафик, усреднив данные по всем магазинам для упрощения и наглядности.

ACF показывает, насколько текущее значение временного ряда зависит от значений на предыдущих временных точках. Из графика мы видим значимые лаги через каждые 7 дней, что говорит о чёткой недельной сезонности, а также можем заметить слабо выраженную годовую сезонность. Для метрики РТО видно, что автокорреляция остаётся значимой примерно до 100-го лага. С учётом этих наблюдений, мы примем длину энкодера равной 100 дням (input). Эксперименты подтвердили, что увеличение длины энкодера выше 100 дней снижает качество модели. Горизонт прогнозирования (длина декодера) при этом зафиксирован в самой задаче и равен 40 дням (output).

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

Окно длиной input + output, завершающееся до даты прогнозирования на тестовом периоде, является валидационным. Чтобы получить несколько валидационных выборок, можно сформировать несколько окон с шагом в один день. Тренировочный набор данных формируется аналогично, с учётом того, что он не должен включать будущую информацию из валидационного периода. Такое разбиение следует повторить для всех временных рядов, используемых для прогнозирования.

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

Наиболее наглядно этот подход можно визуализировать следующим образом:

Разделить временные ряды на несколько периодов не составит труда, так как на объектах darts.Timeseries работают срезы.

Код разделения временных рядов на периоды:

Пусть мы зафиксировали начало и конец каждого периода: тестовый (test_min_dt и test_max_dt), валидационный (val_min_dt и val_max_dt) и тренировочный (train_min_dt и train_max_dt), а также размер окна window_len.

from typing import List
from darts import TimeSeries


def series_splitter(series_list: List[TimeSeries]):
    train: List[TimeSeries] = []
    val: List[TimeSeries] = []
    test: List[TimeSeries] = []

    for series in tqdm(series_list):
        test_series = series[test_min_dt : test_max_dt]
        test.append(test_series)

        val_series = series[val_min_dt : val_max_dt]
        if len(val_series) >= window_len:
            val.append(val_series)
        
        train_series = series[train_min_dt : train_max_dt]
        if len(train_series) >= window_len:
            train.append(train_series)

    return train, val, test


ts_target_train, ts_target_val, ts_target_test = series_splitter(ts_target_list)
ts_future_covs_train, ts_future_covs_val, ts_future_covs_test = series_splitter(ts_future_covs_list)

Создание и обучение модели TFT

Выполним последние подготовительные действия перед началом обучения модели. Нам необходимо задать размер эмбеддингов для категориальных статических ковариат. Определим словарь, где для каждого признака укажем количество уникальных элементов. В этом случае размер эмбеддингов будет рассчитан автоматически следующим образом: \min{(round(1.6 \cdot n^{0.56}), 100)}. Но его можно указать и самостоятельно, передав вторым параметром в кортеже вида: (количество уникальных значений, размер эмбеддинга).

static_cat_embedding_sizes = {
    "id": X,
    "cat_feature_1": X,
    "cat_feature_2": (X, Y),
}

В начале на этапе формирования признаков и ковариат мы говорили, что признаки дат можно будет задать более простым образом. При инициализации модели можно задать любое количество разнообразных ковариат прошлого и будущего. Кроме того, все сгенерированные признаки можно также нормализовать, передав общий scaler, но для примера значение года мы нормализуем самостоятельно. Через ключ cyclic зададим все циклические признаки времени:

def encode_year(idx):
    return (idx.year - 2022) / (2024 - 2022)

  
encoders = {
    "cyclic": {"future": ["day", "day_of_year", "day_of_week", "month"]},
    "custom": {"future": [encode_year]},
}
Инициализируем модель:
import torch
import torchmetrics
from darts.models import TFTModel
from darts.utils.likelihood_models import QuantileRegression
from pytorch_lightning.callbacks.early_stopping import EarlyStopping
from pytorch_lightning.callbacks.lr_monitor import LearningRateMonitor


tft_model = TFTModel(
    model_name="model_example",

    input_chunk_length=100, # размерность энкодера
    output_chunk_length=40, # размерность декодера
    batch_size=1024,
    n_epochs=10,

    # используем статические ковариаты
    use_static_covariates=True,
    # размеры эмбеддингов для категориальных признаков
    categorical_embedding_sizes=static_cat_embedding_sizes,
    # признаки дат
    add_encoders=encoders,

    hidden_size=64,
    lstm_layers=2,
    num_attention_heads=4,
    full_attention=True,
    hidden_continuous_size=16,
    
    work_dir="./logs",
    save_checkpoints=True,
    log_tensorboard=True,
    show_warnings=True,

    loss_fn=None,
    likelihood=QuantileRegression(
        quantiles=[
            0.01, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5,
            0.6, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99,
        ]
    ),
    
    torch_metrics=torchmetrics.WeightedMeanAbsolutePercentageError(),
    
    optimizer_cls=torch.optim.Adam,
    optimizer_kwargs=adam_kwargs,
    
    lr_scheduler_cls=torch.optim.lr_scheduler.ReduceLROnPlateau,
    lr_scheduler_kwargs=lr_scheduler_kwargs,
    
    pl_trainer_kwargs={
        "log_every_n_steps": 10,
        "callbacks": [
            EarlyStopping(**early_stopping_kwargs),
            LearningRateMonitor(logging_interval="epoch"),
        ]
    },
)

Обратим внимание на основные гиперпараметры модели. В нашей задаче по результатам экспериментов данная конфигурация показала наилучшие результаты:

  • hidden_size = 64 – основной гиперпараметр модели, задающий размерность векторов скрытого состояния;

  • lstm_layers = 2 – количество LSTM слоев;

  • num_attention_heads = 4 – количество голов внимания;

  • full_attention = True – режим полного внимания, при котором декодер обращает внимание также и на будущие значения;

  • hidden_continuous_size = 16 – размерность векторов, в которые преобразуются непрерывные признаки.

Мы планируем выполнять вероятностные прогнозы, но модель может работать и в детерминированном режиме, если при инициализации установить параметр likelihood = None, а в качестве функции потерь (loss_fn) использовать любую функцию потерь из библиотеки PyTorch.

Для оценки качества прогнозов используется WeightedMeanAbsolutePercentageError. Для контроля скорости обучения мы подключили lr_scheduler и для ранней остановки обучения EarlyStopping. Весь процесс обучения автоматически логируется в tensorboard.

Начнём обучение модели:
tft_model.fit(
    series=ts_target_train,
    future_covariates=ts_future_covs_train,
    val_series=ts_target_val,
    val_future_covariates=ts_future_covs_val,
    verbose=True,
)

Выполнение прогнозов

При вероятностном прогнозировании необходимо задать количество прогнозов (выборок), которые будут созданы для каждого временного ряда, что позволит оценить степень неопределённости и сформировать доверительные интервалы. Очевидно, что увеличение числа выборок позволяет повысить точность интервалов, но это также значительно увеличивает время вычислений.

Важно отметить, что мы обучали модель для прогноза на output шагов, но Darts предоставляет функционал, позволяющий расширить прогнозирование на большее количество шагов при необходимости. В этом случае модель работает рекурсивно, последовательно прогнозируя блоками по output значений, но при этом требуется обеспечить наличие достаточного количества ковариат будущего на всю длину прогноза.

Выполним прогноз и обратим все выполненные трансформации над данными:
tft_preds_scaled = tft_model.predict(
    n=output_len,
    series=ts_target_test,
    future_covariates=ts_future_covs_test,
    verbose=True,
    num_samples=500,
)
tft_preds = target_static_scaler.inverse_transform(tft_preds_scaled)
tft_preds = target_series_scaler.inverse_transform(tft_preds)

Для случайно выбранного временного ряда построим график усреднённого прогноза с 95% доверительным интервалом, и сравним результат с фактическими данными и ранее выполненными прогнозами.

С помощью встроенного метода TimeSeries.plot(central_quantile=0.5, low_quantile=0.05, high_quantile=0.95) можно легко построить медианный прогноз с 90% или любым другим доверительным интервалом.

Значение усредненного прогноза можно вычислить самостоятельно с помощью TimeSeries.mean(), а квантили, например, так: TimeSeries.quantile_timeseries(quantile=0.975). Получим:

Как видим, хоть модель и ошибается в некоторой степени, но доверительный интервал покрывает практически все фактические значения.

Качество прогнозов на тестовом периоде оценим с помощью метрик MAPE и WAPE. По понятным причинам мы не можем показать истинные значения получившихся метрик, но можем продемонстрировать, насколько они улучшились. Метрики подсчитываются в рамках каждого магазина отдельно, а затем результаты усредняются.

MAPE = \frac{1}{n} \sum_{t=1}^{n} |\frac{y_t - \hat{y}_t}{y_t}|WAPE = \frac{\sum_{t=1}^{n} |y_t - \hat{y}_t|}{\sum_{t=1}^{n} |y_t|}

Получим следующие результаты:

Показатель

Метрика

Улучшение, %

РТО

MAPE

11.34

WAPE

11.93

Трафик

MAPE

6.4

WAPE

5.95

Интерпретация результатов

В начале статьи мы выделили, что одним из ключевых преимуществ модели Temporal Fusion Transformer является её интерпретируемость. В библиотеке Darts предусмотрен отдельный интерпретатор для TFT, который автоматически извлекает необходимые веса и коэффициенты из модели. Для анализа необходимо передать один временной ряд.

from darts.explainability import TFTExplainer


explainer = TFTExplainer(
    tft_model,
    background_series=ts_target_test[0],
    background_future_covariates=ts_future_covs_test[0]
)
explainability_result = explainer.explain()

Для выбранного магазина изучим, какие признаки вносят наибольший вклад в прогноз. Напомним о Variable Selection Network (VSN), который обрабатывает статические ковариаты, а также ковариаты прошлого и будущего. VSN-энкодер обрабатывает признаки целевого временного ряда, ковариаты прошлого и исторические значения ковариат будущего, а VSN-декодер работает только с будущими значениями ковариат будущего.

explainer.plot_variable_selection(explainability_result)

Можем заметить несколько интересных особенностей:

  • День недели, преобразованный с помощью синуса и косинуса, является топ-5 признаком, что подтверждает наличие выраженной недельной сезонности в данных, которую модель прекрасно распознала.

  • Среди статических ковариат, важность ID магазина не превышает 15%. Это говорит о том, что модель считает общие (глобальные) особенности временных рядов более значимыми, чем локальные характеристики, относящиеся к отдельным магазинам.

Далее обратим внимание на Multi-Head Attention, который был усовершенствован авторами TFT именно для интерпретации результатов. Существует несколько вариантов визуализации весов внимания, что позволяет лучше понять, на какие признаки модель обращает внимание на каждом шаге горизонта прогнозирования.

Отобразим внимание модели по каждому шагу горизонта прогнозирования:

explainer.plot_attention(explainability_result, plot_type="all")

Также внимание можно усреднить по всему горизонту прогнозирования:

explainer.plot_attention(explainability_result, plot_type="time")

Заметим, что для каждого шага наибольшее внимание модель обращает на 7, 14, 21 и т. д. дней назад.

И последний вариант визуализации – это тепловая карта:

explainer.plot_attention(explainability_result, plot_type="heatmap")

Проверка модели в реальных условиях

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

После успешного бэк тестирования мы тестировали модель в реальных условиях. Практически на протяжении 6 месяцев мы выполняли еженедельные прогнозы сразу после основной модели для всех магазинов обеих торговых сетей X5: “Пятёрочка” и “Перекрёсток”.

Результаты экспериментов показали, что модель улучшала качество прогнозирования на всём рассматриваемом периоде. В разные моменты времени метрики MAPE и WAPE улучшались от 2% до 12%, а среднее улучшение по обеим торговым сетям составило около 7%.

Заключение

В данной статье был представлен подход к прогнозированию временных рядов с использованием модели Temporal Fusion Transformer (TFT). Для построения модели использовались исключительно те же признаки, которые применялись в существующей системе прогнозирования. При этом в качестве ковариат учитывались выполненные ранее прогнозы, что позволило улучшить качество предсказаний без привлечения новых данных или дополнительных источников информации.

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

Ещё одной важной особенностью модели является её высокая интерпретируемость. Возможность анализировать влияние различных факторов на результат прогнозирования повышает доверие к модели и упрощает её использование в бизнесе. Кроме того, интерпретируемость помогает улучшать качество прогнозов за счёт корректировки входных данных и выбора релевантных признаков.

Результаты тестирования в реальных условиях продемонстрировали стабильное улучшение точности прогнозов в течение почти 6 месяцев. Среднее улучшение качества прогнозов составило 7%, что подтверждает надёжность и эффективность предложенной модели.

Таким образом, использование Temporal Fusion Transformer позволяет не только повысить точность прогнозирования спроса, но и предоставляет бизнесу новые инструменты для анализа и принятия обоснованных решений.

Комментарии (1)


  1. adeshere
    27.12.2024 13:49

    Хорошая статья и интересный подход к прогнозированию. Однако по опыту работы с геофизическими временными рядами мне кажется, что погрешность можно заметно уменьшить, если заранее выделить из сигналов наиболее яркие квазидетерминированные составляющие (разные виды сезонности, тренды, календарные эффекты и др.) и построить для каждой из них отдельную модель. Эти составляющие затем убираются из сигналов (декомпозиция). Потом по описанной в статье схеме строится модель для квазислучайных (остаточных) составляющих, в которых на первом шаге не удалось заметить какой-либо регулярности. Ну и затем все эти модели объединяются в общую комбинированную модель. Мы у себя делаем именно так.

    Правда, у нас задача прогноза не является первоочередной

    У нас обычно на первом месте стоит поиск:
    1) закономерностей в сигналах и
    2) взаимосвязей между разными наблюдаемыми

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

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

    Единственное, с поставленной задачей "минимизации затрат" наш подход не особо дружит. Но когда данных не так уж много, а их получение (геодинамический и геофизический мониторинг) стоит огромных усилий, то объем трудозатрат на обработку сигналов уже не особо критичен. Так как это лишь малый процент от усилий по получению этих данных.