Часть 1: Еженедельный набор данных

TLDR: в этой статье мы рассмотрим несколько различных реализаций ThymeBoost, чтобы увидеть, насколько он конкурентоспособен в соревновании M4. Основными конкурентами являются модель ES-RNN (которая победила) и модель Theta, которая показала лучшие результаты. После набора данных Weekly некоторые реализации ThymeBoost выделялись.

Если вам нужно познакомиться с ThymeBoost, ознакомьтесь с этой статьей.

Таймбуст GitHub

Введение

Соревнование М4 стало еще одним участником М-соревнований, которые периодически проводились в течение последних 40 лет. Их искали для сравнительного анализа методов прогнозирования временных рядов на протяжении десятилетий, и соревнование M4 не стало исключением. Состоящий из 100 тысяч временных рядов, охватывающих несколько доменов и частот, он обеспечивает достойный эталон. Хотя некоторые частоты недостаточно представлены, что может благоприятствовать определенным методам. Обсуждая бенчмаркинг, я дам вам знать, что это не предназначено для бенчмаркинга ThymeBoost, вместо этого это обзор функций ThymeBoost. Я возьму любые знания, чтобы применить их в методе прогнозирования «автоподбор». В отличие от ARIMA или других традиционных методов, нет руководств по методу декомпозиции усиленных временных рядов, и есть несколько вопросов, на которые я надеюсь найти ответ, например:

  1. Какие трендовые методы хорошо работают при форсировании?
  2. Какие методы хорошо работают в качестве «генераторов» во фреймворке?
  3. А как насчет скорости обучения компонентов, они что-то делают?

Имея в виду эти вопросы, давайте углубимся.

Данные

Все наборы данных с открытым исходным кодом и живут на M-соревнованиях github. Он разделен на стандартные разбиения поезда и теста, поэтому мы будем использовать csv поезда для подбора и тестирования только для оценки с использованием SMAPE. Давайте продолжим и импортируем данные вместе с ThymeBoost, если вы еще не установили его, возьмите его из pip.

pip install ThymeBoost

Есть частые обновления, поэтому, даже если вы уже установили его ранее, вы, вероятно, захотите обновить его!

Теперь, когда у нас есть ThymeBoost и наборы данных, давайте приступим к кодированию.

import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm
import pandas as pd
from ThymeBoost  import ThymeBoost as tb
train_df = pd.read_csv(r'm4-weekly-train.csv')
test_df = pd.read_csv(r'm4-weekly-test.csv')
train_df.index = train_df['V1']
train_df = train_df.drop('V1', axis = 1)
test_df.index = test_df['V1']
test_df = test_df.drop('V1', axis = 1)

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

def smape(A, F):
    return 100/len(A) * np.sum(2 * np.abs(F - A) / (np.abs(A) +       np.abs(F)))

Для нашего эксперимента мы возьмем среднее значение по всем временным рядам для сравнения с другими моделями. Для проверки работоспособности мы также получим «наивный» средний SMAPE, чтобы убедиться, что то, что мы делаем, соответствует тому, что было сделано на соревнованиях. С учетом сказанного, мы просто будем перебирать фрейм данных, лениво подгонять и прогнозировать. Код можно было бы оптимизировать, не выполняя цикл for, но это сойдет!

Реализация №1: простая

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

trend_estimator=['linear', 'ses']

Это, как правило, хорошо, и у него очень «тета-эй» вид. Первый раунд (не раунд инициализации) соответствует простой линии тренда. Далее следует простое экспоненциальное сглаживание. Функция полной подгонки будет выглядеть так:

output = boosted_model.fit(y.values,
                                seasonal_period=[0, 52],
                                trend_estimator=['linear', 'ses'],
                                seasonal_estimator='fourier',
                                fit_type='global',
                                global_cost='mse',
                                seasonality_lr=.9,
                                additive=False,
                                )

Еще одна интересная часть этой настройки — переменная «генератор», передаваемая для «сезонный_период». Это работает так же, как и Trend_estimator, где в первом раунде используется отсутствиесезонности, затем используется период 52, затем возвращается к отсутствию и так далее. Параметр «добавка», являющийся ложным, по сути, просто означает, что журнал берется, и, наконец, есть небольшая регуляризация сезонности с передачей 0,9.

Как упоминалось ранее, эта реализация будет использоваться для каждой серии в простом цикле. Давайте продолжим и запустим его:

seasonality = 52
smapes = []
naive_smape = []
j = tqdm(range(len(train_df)))
for row in j:
    y = train_df.iloc[row, :].dropna()
    y = y.iloc[-(3*seasonality):]
    y_test = test_df.iloc[row, :].dropna()
    j.set_description(f'{np.mean(smapes)}, {np.mean(naive_smape)}')
    boosted_model = tb.ThymeBoost(verbose=0,
                                  normalize_seasonality=True)
    output = boosted_model.fit(y.values,
                                seasonal_period=[0, 52],
                                trend_estimator=['linear', 'ses'],
                                seasonal_estimator='fourier',
                                fit_type='global',
                                global_cost='mse',
                                seasonality_lr=.9,
                                additive=False,
                                )
    predicted_output = boosted_model.predict(output,
                                             len(y_test),
                                             trend_penalty=True)
    smapes.append(smape(y_test.values, predicted_output['predictions'].clip(lower=0)))
    naive_smape.append(smape(y_test.values, np.tile(y.iloc[-1], len(y_test))))
print(f'Weekly {np.mean(smapes)}')
print(f'Naive {np.mean(naive_smape)}')

И вывод:

Weekly 7.780101701503696
Naive 9.161286913982

Среднее значение SMAPE составляет 7,78, что на самом деле является улучшением по сравнению с ES-RNN и Theta:

Мы можем получить еще больший выигрыш, слегка изменив сезонный_период с передачей:

seasonal_period=[0, 52, 52]

Это немного странно, так что определенно не тот параметр, который может быть широко полезен.

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

Реализация № 2: Ансамбль

Что-то еще, что мы можем сделать, это усреднить несколько прогнозов. Это можно сделать в ThymeBoost, передав все в виде списка методу ансамбля. Давайте подробнее рассмотрим, что мы будем пробовать:

trend_estimator=[['linear', 'ses'],
                 ['linear', 'damped_des'],
                 ['linear', 'des']]
output = boosted_model.ensemble(y.values,
                                seasonal_period=[[0, 52]],
                                global_cost=['mse'],
                                fit_type=['global'],
                                additive=[False],
                                seasonality_lr=[.9],
                                seasonal_estimator=['fourier']
                                )

Обратите внимание, как все передается в виде списка! Чтобы использовать переменную «генератор» с этим методом, как мы делали ранее с методом подгонки, теперь мы должны передать ее как список списков. В этом случае trend_estimator состоит из 3 «генераторных» переменных. Здесь мы обращаем внимание на то, что раньше работало хорошо, а также на метод «тестирования» из конкурса M4. Этот метод представлял собой усреднение простого, двойного и демпфированного двойного экспоненциального сглаживания.

Давайте запустим полный блок кода для этого цикла и посмотрим, как он работает:

smapes = []
naive_smape = []
j = tqdm(range(len(train_df)))
seasonality = 52
trend_estimator=[['linear', 'ses'],
                 ['linear', 'damped_des'],
                 ['linear', 'des']]
for row in j:
    y = train_df.iloc[row, :].dropna()
    y = y.iloc[-(3*seasonality):]
    y_test = test_df.iloc[row, :].dropna()
    j.set_description(f'{np.mean(smapes)}, {np.mean(naive_smape)}')
    boosted_model = tb.ThymeBoost(verbose=0,
                                  normalize_seasonality=True)
    output = boosted_model.ensemble(y.values,
                                    seasonal_period=[[0, 52]],
                                    trend_estimator=trend_estimator,
                                    global_cost=['mse'],
                                    fit_type=['global'],
                                    additive=[False],
                                    seasonality_lr=[.9],
                                    seasonal_estimator=['fourier']
                                    )
    predicted_output = boosted_model.predict(output,
                                             len(y_test),
                                             trend_penalty=True)
    smapes.append(smape(y_test.values, predicted_output['predictions'].clip(lower=0)))
    naive_smape.append(smape(y_test.values, np.tile(y.iloc[-1], len(y_test))))
print(f'Weekly {np.mean(smapes)}')
print(f'Naive {np.mean(naive_smape)}')

И вывод:

Weekly 7.660902293272987
Naive 9.161286913982

Еще большее улучшение по сравнению с ES-RNN и Theta, хотя такая сборка увеличивает время вычислений.

Текущие результаты:

Реализация №3: Оптимизация

До сих пор мы рассматривали простую конфигурацию ThymeBoost и более сложный подход к ансамблю с усреднением 3 выходных данных. Следующий шаг — выбрать, какой подход использовать, и, надеюсь, найти «оптимальный». Для этого воспользуемся методом оптимизации от ThymeBoost. Этот метод довольно прост, он принимает список стандартных параметров подгонки (так же, как ансамбль), но на этот раз он выберет один выход на основе точности удержания. Вы можете выбрать стратегию оптимизации «удерживания» или «перемотки», в которой прокрутка использует перекрестную проверку временных рядов. Это означает, что будет несколько раундов примерки и тестирования.

Давайте внимательно рассмотрим этот метод оптимизации, прежде чем мы запустим полный цикл:

trend_estimator=[['linear', 'ses'],
                 ['linear', 'damped_des'],
                 ['linear', 'des']]
output = boosted_model.optimize(y.values,
                            seasonal_period=[[0, 52], 0],
                            trend_estimator=[['linear', 'ses'],    boosted_model.combine(trend_estimator)],
                            seasonal_estimator=['fourier'],
                            fit_type=['global'],
                            global_cost=['mse'],
                            seasonality_lr=[.9, .1],
                            additive=[False],
                            optimization_steps=4,
                            optimization_strategy='rolling',
                            lag=28,
                            verbose=0,
                            )

Во-первых, для сезонного периода мы будем использовать то, что хорошо работало в прошлый раз [0, 52], а также 0, обозначающее полное отсутствие сезонности. Кроме того, мы попробуем легкую регуляризацию сезонности с помощью скорости обучения 0,9, а также тяжелую регуляризацию с 0,1. Самое интересное, что этот «комбинированный» метод добавлен в смесь. Это просто означает, что ThymeBoost будет использовать этот параметр как ансамбль при оптимизации. Помимо этих настроек, мы видим новые параметры, связанные с оптимизацией, которые указывают, как оценивать каждый метод. Здесь мы проведем 4 раунда подгонки и тестирования с общей задержкой последних 28 периодов (очевидно, вы должны изменить это в зависимости от частоты данных). Это означает, что каждый из 4 раундов будет тестироваться на 7 точках данных и прокручиваться через всю задержку задержки, равную 28.

После всего сказанного давайте посмотрим на полный код и вывод:

smapes = []
naive_smape = []
j = tqdm(range(len(train_df)))
seasonality = 52
trend_estimator=[['linear', 'ses'],
                 ['linear', 'damped_des'],
                 ['linear', 'des']]
for row in j:
    y = train_df.iloc[row, :].dropna()
    y = y.iloc[-(3*seasonality):]
    y_test = test_df.iloc[row, :].dropna()
    j.set_description(f'{np.mean(smapes)}, {np.mean(naive_smape)}')
    boosted_model = tb.ThymeBoost(verbose=0,
                                  normalize_seasonality=True)
    output = boosted_model.optimize(y.values,
                                seasonal_period=[[0, 52], 0],
                                trend_estimator=[['linear', 'ses'], boosted_model.combine(trend_estimator)],
                                seasonal_estimator=['fourier'],
                                fit_type=['global'],
                                global_cost=['mse'],
                                seasonality_lr=[.9, .1],
                                additive=[False],
                                optimization_steps=4,
                                optimization_strategy='rolling',
                                lag=28,
                                verbose=0,
                                )
    predicted_output = boosted_model.predict(output,
                                             len(y_test),
                                             trend_penalty=True)
    smapes.append(smape(y_test.values, predicted_output['predictions'].clip(lower=0)))
    naive_smape.append(smape(y_test.values, np.tile(y.iloc[-1], len(y_test))))
print(f'Weekly {np.mean(smapes)}')
print(f'Naive {np.mean(naive_smape)}')

И операторы печати:

Weekly 7.235682291017231
Naive 9.161286913982

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

Окончательные результаты для этого набора данных:

Мы определенно видим еще одно улучшение по сравнению с предыдущей реализацией ThymeBoost.

Заключение

Ясно, что во всей этой идее «усиления декомпозиции временных рядов» что-то есть. Основным узким местом является огромное пространство параметров. Одна из моих целей, когда мы просматриваем наборы данных M4, состоит в том, чтобы придумать содержательные тесты, которые можно попытаться использовать в методе автоподбора. Один необходимый тест на сезонность, чтобы ограничить пространство параметров, но я до сих пор не знаю, какой тип данных хорошо работает с каким типом оценок усиленного тренда. Какие особенности временного ряда коррелируют с повышенным сглаживанием? Как насчет усиленного ARIMA?

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

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

А пока, если вы обнаружите что-нибудь интересное при работе с ThymeBoost, обязательно дайте мне знать!