Штрафы L1 и переставленные оценки характеристик

В прогнозном моделировании результаты хороши ровно настолько, насколько хороши входные данные, добавленные в модель. Во временных рядах основная проблема выбора входных данных модели заключается в том, как лучше всего представить историю ряда, чтобы предсказать его будущее. Мы можем разбить ряд на его основные компоненты: тренд, сезонность и остаток, но со сложными наборами данных это может быть трудно сделать точно правильно. Возьмем, к примеру, набор данных о солнечных пятнах, доступный на Kaggle с лицензией Public Domain. Он измеряет ежемесячно наблюдаемые солнечные пятна с 1700-х годов и при линейном разложении на вышеупомянутые компоненты выглядит следующим образом:

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

Один из вариантов — добавить все, что, по нашему мнению, может иметь значение, а затем, используя регуляризацию L1 или метод, аналогичный рекурсивному исключению признаков, мы сужаем переменные только до наиболее важного подмножества. Именно это и сделано в этом примере, который вы можете найти на Github или Read the Docs. Мы будем использовать пакет scalecast для анализа временных рядов. Пожалуйста, поставьте ему звезду на GitHub, если он покажется вам интересным. Пакет ELI5 также используется для оценки важности функций и scikit-learn для моделирования.

Подготовка и добавление переменных

Сначала установите пакет масштабирования:

pip install scalecast

Затем мы создаем объект Forecaster, который является оболочкой модели и контейнером результатов:

df = pd.read_csv(
  'Sunspots.csv',
  index_col=0,
  names=['Date','Target'],
  header=0,
)
f = Forecaster(y=df['Target'],current_dates=df['Date'])

Условия авторегрессии

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

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

f.add_ar_terms(120)     # lags 1-120
f.add_AR_terms((20,12)) # yearly lags up to 20 years

Сезонные циклы

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

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

f.add_seasonal_regressors('month','quarter',raw=False,sincos=True)
# 12-month cycles from 12 to 288 months    
for i in np.arange(12,289,12):
    f.add_cycle(i)

Тенденции

Что касается тенденций, мы можем сделать это простым. Мы добавляем переменную года, а также переменную под названием «t», которая будет считать от 1 до длины ряда для каждого наблюдения, иначе известную как временной тренд.

f.add_time_trend()
f.add_seasonal_regressors('year')

После добавления всех этих переменных у нас осталось 184 предиктора. Размер набора данных составляет 3265. Иногда хорошим числом переменных, к которым нужно стремиться, является квадратный корень из размера набора данных (около 57 в данном случае), но для этого нет жесткого и быстрого правила. В любом случае, маловероятно, что все добавленные нами функции помогут делать прогнозы (и трудно точно сказать, какие из них будут, а какие — нет). При прочих равных предпочтительнее более экономные модели, модели с меньшим количеством входных данных.

Устранить переменные

Устранить с помощью регуляризации L1

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

Где x — переменная, p — общее количество переменных, w — коэффициент данной переменной и M — общее количество количество наблюдений. С точки зрения непрофессионала, вес каждого коэффициента изменяется на коэффициент альфа, который устанавливается в соответствии с тем, что лучше всего оптимизирует модель для прогнозирования. В рамках этой структуры наименее важные переменные в линейной модели упорядочиваются, а их коэффициенты получают значение 0. Мы запускаем эту модель в нашем анализе и проверяем, какие переменные получают коэффициентный вес 0; переменные, которые не становятся нашим сокращенным подмножеством. Параметр альфа задается при поиске по сетке на проверочном срезе данных, и все входные данные масштабируются по умолчанию, что необходимо при использовании этого метода.

f.reduce_Xvars(
    method = 'l1',
    overwrite = False,
    dynamic_testing = False,
)
lasso_reduced_vars = f.reduced_Xvars[:]

В нашем наборе данных это уменьшает количество переменных со 184 до 34. Это довольно резкое падение!

Устранение с помощью важности функции перестановки

Следующий метод исключения является более сложным и включает в себя важность признаков перестановки (PFI) из ELI5. Идея PFI состоит в том, чтобы обучать и оценивать любой оценщик scikit-learn, используя данные вне выборки, но с каждой итерацией оценки перемешивать значения одной функции, чтобы они были случайными. Это повторяется несколько раз для каждого входа в модели. Затем измеряется среднее изменение точности алгоритма при рандомизации одного входа, и функции, которые больше всего снижают точность модели, оцениваются выше в рейтинге важности функций. Это метод, не зависящий от конкретного алгоритма, переданного ему, что делает его обобщающим для многих классов моделей. Его недостатки заключаются в том, что он проверяет каждую переменную одну за другой, поэтому при оценке функций упускается взаимодействие между переменными. По той же причине две переменные, сильно коррелирующие друг с другом, могут получить низкие баллы важности, даже если они обе важны для модели. Особенно часто это происходит с временными рядами, поэтому Scalecast вносит коррективы в необработанные оценки, прежде чем решить, какая функция будет удалена следующей. См. документацию.

Включая PFI для исключения функций, мы используем следующий процесс:

  1. Обучите заданный класс модели с полным набором переменных и настройте его гиперпараметры, используя поиск по сетке на проверочном фрагменте данных вне выборки.
  2. Переобучите модель с оптимальными гиперпараметрами из шага 1 и отслеживайте ошибку на проверочном срезе данных.
  3. Оценка функций с помощью PFI
  4. Отбросьте функцию с наименьшей оценкой
  5. Повторяйте с шага 2, пока не будут удалены все переменные, кроме определенного количества.
  6. Выберите сокращенный набор переменных, который вернул наименьшую ошибку в данных проверки.

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

f.reduce_Xvars(
    method = "pfi",
    estimator = "mlr",
    keep_at_least = "sqrt",
    grid_search = True,
    dynamic_testing = False,
    overwrite=False,
)

Опять же, благодаря гибкости интеграции ELI5, в этом процессе можно использовать любую программу оценки scikit-learn. В приведенном выше коде используется множественная линейная регрессия, но мы также исследуем k-ближайших соседей, деревья с градиентным усилением и машину опорных векторов. Доступны многие другие модели, если мы решим их использовать. Мы можем построить результаты, изучив ошибку вне выборки, полученную после удаления каждой переменной:

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

the mlr model chose 146 variables
the knn model chose 98 variables
the gbt model chose 136 variables
the svr model chose 61 variables

Подтвердить выбор

Как обобщаются эти переменные подмножества?

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

Главный вывод из изучения этих результатов заключается в том, что, хотя сокращенный набор данных из 136 признаков с GBT показал лучшие результаты в проверочном наборе, уменьшенный набор переменных из модели k-ближайших соседей с 98 признаками показал лучшие результаты в тестовом наборе, показывая, что сокращенное подмножество переменных может быть получено из одного класса моделей и успешно интегрировано в другой. Мы можем сохранить этот сокращенный набор данных для будущих задач прогнозирования, если нам так будет угодно:

Xvars_df = f.export_Xvars_df()
Xvars_df.set_index("DATE",inplace=True)
reduced_dataset = Xvars_df[knn_reduced_vars]

Какие переменные чаще всего удалялись?

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

Наиболее часто удаляемые переменные:

  • четвертькосы (все 5 техник)
  • AR86 (все 5 техник)
  • AR46 (все 5 техник)

Каждый раз отбрасывалась одна, но не обе наши четвертьсезонные переменные, а также отставание 86-й и 46-й серий. Есть много других интересных выводов из этой части анализа. Полные результаты см. в блокноте, ссылка на который приведена в начале статьи.

Заключение

Выбор переменных является важной частью оптимизации модели временных рядов, но не всегда легко понять, какие переменные наиболее важны для включения в ту или иную модель. В этом примере мы добавили в модель больше переменных, чем считали необходимым, и исследовали сокращение переменных с помощью оценщика лассо и регуляризации L1, а также итеративное устранение признаков с использованием четырех классов модели scikit-learn и использование важности признаков перестановки. Посмотрите scalecast на Github и поставьте звезду, если вам это интересно!