Tsfresh: автоматически генерируем признаки из временных рядов | OTUS

Tsfresh: автоматически генерируем признаки из временных рядов

ML_Deep_23.1-5020-61eb0c.png

Продолжаем работать с временными рядами. В прошлой статье мы посмотрели, как использовать мощь глубокого обучения для прогнозирования временных рядов при помощи библиотеки GluonTS от Amazon. На сей раз вернёмся к "обычному" машинному обучению, где признаки по-прежнему нужно генерировать перед построением моделей (а не модели услужливо строят удобные для себя признаки).

Time Series FeatuRe Extraction based on Scalable Hypothesis tests

tsfresh-20219-0a8200.png

Автоматизируем рутину

При работе с временными рядами могут возникнуть две ситуации: 1. Мы хотим спрогнозировать временной ряд, основываясь на его предыдущих значениях. 2. Мы хотим использовать временной ряд как признак объекта.

И в первом, и во втором случае мы могли бы самостоятельно придумать различного рода эвристики и признаки, извлечь их из временного ряда и обучить на них модель. Типичными признаками могут быть предыдущие значения ряда, минимальные/максимальные значения в пределах некоторого окна, стандартное отклонение и среднее, и так далее. Признаков можно придумывать бесконечно много и бесконечно долго. Но что, если эту операцию автоматизировать? Для этих целей и создавалась замечательная библиотека tsfresh. В этой статье рассмотрим её использование для второго случая.

tsfresh — а вы и признаки за меня придумывать будете?

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

Давайте разберём всё на интересном примере — распознавании активности человека по данным акселерометра с мобильного телефона:

# импортируем необходимые функции из библиотеки
from tsfresh.examples.har_dataset import download_har_dataset, load_har_dataset, load_har_classes
from tsfresh import extract_features, extract_relevant_features, select_features
from tsfresh.utilities.dataframe_functions import impute
from tsfresh.feature_extraction import settings

# для построения моделей воспользуемся sklearn
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

import pandas as pd
import numpy as np

import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns

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

download_har_dataset()
data = load_har_dataset()
y = load_har_classes()
print(data.shape)
data.head()
Out[]: (7352, 128)

data-20219-d380d0.png

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

example-20219-a32707.png

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

X_train, X_test, y_train, y_test = train_test_split(data, y, test_size=.2)


cl = DecisionTreeClassifier()
cl.fit(X_train, y_train)
print(classification_report(y_test, cl.predict(X_test)))
Out[]:
              precision    recall  f1-score   support

           1       0.68      0.69      0.68       260
           2       0.56      0.59      0.57       210
           3       0.78      0.68      0.73       199
           4       0.31      0.37      0.33       242
           5       0.34      0.33      0.34       266
           6       0.49      0.44      0.46       294

   micro avg       0.51      0.51      0.51      1471
   macro avg       0.53      0.52      0.52      1471
weighted avg       0.51      0.51      0.51      1471

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

Раз бейзлайн на сырых признаках есть — настало время извлечь что-то поинтереснее!

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

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

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

settings_minimal = settings.MinimalFCParameters()
settings_minimal
Out[]:
{
    'sum_values': None,
    'median': None,
    'mean': None,
    'length': None,
    'standard_deviation': None,
    'variance': None,
    'maximum': None,
    'minimum': None
}

Теперь посмотрим, как можно скомбинировать несколько разных вариантов извлечения признаков. Добавим к текущему минимальному набору еще один — TimeBasedFCParameters. Этот метод посчитает по нашему ряду линейный тренд, возьмет оттуда значение коэффициента R2, intercept, slope, стандартную ошибку и p-value и всё это сделает новыми признаками для объекта. При помощи простого совмещения двух словарей с правилами мы получаем уже чуть более интересный набор признаков, который по-прежнему будет достаточно быстро считаться даже на больших датасетах.

settings_time = settings.TimeBasedFCParameters()
settings_time.update(settings_minimal)
settings_time
Out[]:
{'linear_trend_timewise': [{'attr': 'pvalue'},
  {'attr': 'rvalue'},
  {'attr': 'intercept'},
  {'attr': 'slope'},
  {'attr': 'stderr'}],
 'sum_values': None,
 'median': None,
 'mean': None,
 'length': None,
 'standard_deviation': None,
 'variance': None,
 'maximum': None,
 'minimum': None}

Следующий набор — EfficientFCParameters. Здесь число различных признаков уже заметно выше, чем в предыдущих вариантах. Рассчитываются коэффициенты skewness и kurtosis, считается, сколько раз повторялось значение минимума и максимума, различные квантили, оконные статистики, автокорреляции и многое-многое другое. Efficient этот набор потому, что он по-прежнему относительно быстро считается и не требует значительных вычислительных затрат на построение признакового пространства.

settings_efficient = settings.EfficientFCParameters()
settings_efficient
Out[]:
{'variance_larger_than_standard_deviation': None,
 'has_duplicate_max': None,
 'has_duplicate_min': None,
 'has_duplicate': None,
 'sum_values': None,
 'abs_energy': None,
 'mean_abs_change': None,
 'mean_change': None,
 'mean_second_derivative_central': None,
 'median': None,
 'mean': None,
 'length': None,
 'standard_deviation': None,
 'variance': None,
 'skewness': None,
 'kurtosis': None,
 ...
}

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

settings_comprehensive = settings.ComprehensiveFCParameters()
len(settings_comprehensive)
Out[]: 64

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

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

data_long = pd.DataFrame({0: data.values.flatten(),
                          1: np.arange(data.shape[0]).repeat(data.shape[1])})
print(data_long.shape)
data_long.head()
Out[]: (941056, 2)

long-20219-ff1ea5.png

В результате наш датасет удлинился практически до одного миллиона строк.

Извлекаем признаки при помощи extract_features, указав в качестве параметров для извлечения наш эффективный список. Также укажем параметр для impute_function, передав туда функцию impute, импортированную выше из tsfresh.utilities.dataframe_functions. Эта функция автоматически заполнит все пропуски в получившемся датасете (если они там внезапно появятся)

X = extract_features(data_long, column_id=1, impute_function=impute, default_fc_parameters=settings_efficient)
print(X.shape)
Out[]: (7352, 788)

extraction-20219-fa0cf4.png

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

Попробуем теперь обучить модель на них!

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.2)
cl = DecisionTreeClassifier()
cl.fit(X_train, y_train)
print(classification_report(y_test, cl.predict(X_test)))
Out[]:

              precision    recall  f1-score   support

           1       0.91      0.90      0.90       245
           2       0.80      0.85      0.82       217
           3       0.92      0.86      0.89       214
           4       0.40      0.40      0.40       275
           5       0.49      0.46      0.48       257
           6       0.55      0.58      0.57       263

   micro avg       0.66      0.66      0.66      1471
   macro avg       0.68      0.68      0.68      1471
weighted avg       0.66      0.66      0.66      1471

Ура, действительно, признаки оказались полезными и качество предсказаний заметно подросло по всем классам.

Но скорее всего, многие из извлеченных признаков на самом деле не нужны для построения предсказаний и могут быть спокойно выброшены. Для этого в библиотеке есть метод select_features, который рассчитывает важность текущего признака для предсказания класса. После расчёта ненужные признаки отбрасываются по p-value, при этом в функцию зашита поправка Бенджамини-Иекутиели на множественное тестирование.

relevant_features = set()

for label in y.unique():
    # select_features работает с бинарной классификацией, поэтому переводим задачу
    # в бинарную для каждого класса и повторяем по всем классам
    y_train_binary = y_train == label
    X_train_filtered = select_features(X_train, y_train_binary)
    relevant_features = relevant_features.union(set(X_train_filtered.columns))

len(relevant_features)
Out[]: 352

В результате получили заметно меньшей признаковое пространство, попробуем снова построить модель.

X_train_filtered = X_train[list(relevant_features)]
X_test_filtered = X_test[list(relevant_features)]

cl = DecisionTreeClassifier()
cl.fit(X_train_filtered, y_train)
print(classification_report(y_test, cl.predict(X_test_filtered)))
Out[]:

              precision    recall  f1-score   support

           1       0.92      0.93      0.93       245
           2       0.83      0.87      0.85       217
           3       0.92      0.86      0.89       214
           4       0.41      0.40      0.41       275
           5       0.45      0.47      0.46       257
           6       0.58      0.57      0.57       263

   micro avg       0.67      0.67      0.67      1471
   macro avg       0.69      0.68      0.68      1471
weighted avg       0.67      0.67      0.67      1471

Отлично, отбросив мусорные признаки мы не только упростили модель, но еще и получили более высокое качество!

О других методах работы с временными рядами вы сможете узнать на занятиях "Анализ временных рядов" курса Machine Learning.

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
0 комментариев
Для комментирования необходимо авторизоваться
Популярное
Сегодня тут пусто