Merlion Framework или как упростить работу с временными рядами
Привет, Хабр!
Сегодня с вами участник профессионального сообщества NTA Марков Никита.
Введение
В нашем мире время является незаметным четвертым измерением (или временным рядом), по оси которого, можно упорядочивать разные события.
Временной ряд — это набор данных, описывающих изменения какой‑либо переменной во времени.
Задача анализа и предсказания временных рядов остаётся актуальной для бизнеса так как для успешного планирования возникает необходимость прогнозирования, например, следующих показателей:
спрос на определенные продукты/услуги;
нагрузка на контактный центр или сервера компании;
количество новых пользователей/загрузок для приложения.
Наиболее популярные подходы для предсказания временных рядов делятся на:
o ARIMA (AutoRegressive Integrated Moving Average);
o ETS (Error, Trend, Seasonal);
o Prophet;
o Exponential Smoother.
o Vector AutoRegressive;
o DeepAR;
o Transfromer;
o RandomForestForecaster.
Так какой же метод выбрать для решения задачи?
Преимущество статистических методов заключается в относительной простоте моделей, что делает их более интерпретируемыми и помогает исследовать зависимости между переменными. Однако модели машинного обучения гораздо лучше справляются с описанием динамики более сложных нелинейных процессов.
Почему бы просто не попробовать каждый из этих методов?
Для этого придётся устанавливать много библиотек и готовить по‑разному данные для обучения и предсказания каждой модели.
Есть ли какая‑то одна библиотека, которая облегчит задачу предсказания временных рядов?
Да, такая библиотека уже существует и называется Merlion Framework.
Краткий обзор библиотеки Merlion
Merlion — это библиотека для анализа временных рядов, написанная на языке Python. Она предоставляет комплексную платформу машинного обучения, которая включает:
загрузку и преобразование данных в удобный формат хранения временных рядов;
построение и обучение моделей для предсказания и детекции аномалий;
простые в использовании ансамбли;
распределенные вычисления с иcпользованием Spark;
последующую обработку выходных данных модели для уменьшения ошибок и повышения интерпретируемости результатов;
построение пайплайнов для оценки производительности модели.
Цель этой библиотеки — предоставить инженерам и исследователям универсальное решение для быстрой разработки моделей и их сравнения.
С полным списком возможностей можно ознакомиться взглянув на таблицу сравнения с другими библиотеками для анализа временных рядов, взятую из github‑репозитория библиотеки. Там же есть и инструкция по установке.
Сравнительный анализ похожих библиотек по возможностям
Сравнительный анализ похожих библиотек по возможностям
По набору возможностей c Merlion могут конкурировать kats и darts.
Kats предоставляет схожий функционал, но не поддерживает модели архитектуры Transformer, как и возможность использования экзогенных параметров в предсказателях (переменных, которые не объясняются другими переменными в модели, например, погода).
Darts предоставляет богатый выбор моделей, возможности для оценки предсказаний, проигрывая разве что в отсутствии функциональности для распознавания change points.
Архитектура и основные структуры данных
Merlion состоит из следующих основных пакетов:
utils — содержит модули, предоставляющие структуру данных для работы с временными рядами;
transform — предоставляет функциональность для преобразования временных рядов;
models — содержит реализации готовых моделей для предсказания и поиска аномалий;
evaluate — хранит метрики для оценки модели;
post_process — даёт возможность обработать спрогнозированный временной ряд;
spark — связующий модуль для работы с Apache Spark.
Архитектура Merlion
Начну работу с библиотекой сразу с практики. Для примера возьму простой датасет с kaggle. Он содержит информацию о траффике пешеходов и велосипедистов, двигающихся в направлении юга или севера мимо одного и того же светофора в Сиэтле. Информация обновляется с интервалом один час.
#прочитаем csv в Pandas
df = pd.read_csv("burke-gilman-trail-north-of-ne-70th-st-bike-and-ped-counter.csv")
#преобразуем текст в дату
df["Date"] = pd.to_datetime(df["Date"])
#установим индекс по времени, т.к. это требование метода UnivariateTimeSeries.from_pd()
df = df.set_index("Date")
df
Пример данных из датасета
В чем проблема датасетов временных рядов?
Для того, чтобы «скормить» такой датасет на обучение какой‑нибудь крутой
нейросетке, его нужно подготовить. Допустим я хочу по двум предыдущим значениям
предсказать третье, тогда необходимо пройтись окном размера два по нашей
выборке и нарезать её на тренировочные образцы, содержащие по два
элемента на вход модели (X) и один элемент как ожидаемое прогнозное значение (y).
Иллюстрация «скользящего окна» для подготовки данных
Обычно на этом этапе возникает путаница с датами и размерами. К тому же если в результате экспериментов захочу предсказывать не по двум предыдущим значениям, а по трём, придётся заново подготавливать данные и подгонять датасет под ожидаемые моделью размеры.
Merlion решает эту проблему просто, предоставляя свою структуру для хранения временных рядов, которая заточена под работу с интерфейсом предсказательных моделей.
Создаю временные ряды
UnivariateTimeSeries наследуется от pd.Series и является основным строительным блоком в Merlion. Это специальная структура данных для хранения одномерного временного ряда и работы с ним.
Взаимосвязь модулей для хранения временных рядов
Как создать?
UnivariateTimeSeries легко инициализировать из pd.Series или pd.Dataframe, в котором индекс единственной колонки (все‑таки создаем одномерный временной ряд) должен указывать время наблюдения.
from merlion.utils import UnivariateTimeSeries
uts = UnivariateTimeSeries.from_pd(df["BGT North of NE 70th Total"])
uts.head()
Что ещё может UnivariateTimeSeries?
Помимо всех унаследованных стандартных функций pd.Series, UnivariateTimeSeries предоставляет следующие фишки:
#предположим что мы хотим поделить выборку на тренировочную и тестовую по времени
train, test = uts.bisect("2017-01-06 00:00:00", t_in_left=False)
train.tail()
test.head()
uts.window("2014-01-01 00:00:00", "2014-01-01 06:00:00"):
Bisect и window облегчают выборку по времени, заменяя более громоздкие конструкции из pandas:
# pandas way
selected_rows = df.query("'2014-01-01 00:00:00' <= Date <= '2014-01-01 06:00:00'")
selected_rows = df.loc[(df['Date'] >= '2014-01-01 00:00:00') & (df['Date'] <= '2014-01-01 06:00:00')]
selected_rows = df[df['Date'].between('2014-01-01 00:00:00', '2014-01-01 06:00:00')]
А как хранить многомерный временной ряд?
TimeSeries предоставляет функционал для работы с многомерными временными рядами и объединяет в себе несколько UnivariateTimeSeries.
Создать такую структуру можно из готового pd.Dataframe (как с множеством колонок, так и с единственной). Однако возникает ситуация, когда есть несколько одномерных временных рядов с разной частотой наблюдений, которые надо как‑то объединить.
Для этого существует конструктор, принимающий словарь из UnivariateTimeSeries.
uts_total = UnivariateTimeSeries.from_pd(df["BGT North of NE 70th Total"])
uts_ped_south = UnivariateTimeSeries.from_pd(df.iloc[1::2]["Ped South"])
uts_bike_south = UnivariateTimeSeries.from_pd(df["Bike North"]).bisect("2014-01-01 04:00:00")[1]
ts = TimeSeries(univariates=OrderedDict([("total", uts_total.copy()),
("ped_south", uts_ped_south.copy()),
("bike_south", uts_bike_south.copy())]))
ts
Функция Align поможет выровнять одномерные ряды, предоставляя разработчику выбор стратегий объединения наблюдений и заполнения пропусков.
from merlion.utils.resample import *
ts.align(remove_non_overlapping=True, # начинаем наблюдения с момента пересечения всех параметров
missing_value_policy = MissingValuePolicy.FFill) #заполняем пропущенные значения предыдущими
ts = ts.align(alignment_policy=AlignPolicy.InnerJoin) #делаем inner join для всех параметров
Работа с моделями для предсказания
Merlion содержит богатый набор моделей под общим интерфейсом ModelBase, который пригодится для непосредственной работы с моделью:
сохранение весов модели в файл;
загрузка и инициализация из файла;
получение информации о последнем обучении;
получение информации о конфигурации модели.
Модели Merlion используются для двух типов задач временных рядов: обнаружение аномалий и предсказание.
В посте я рассмотрю задачу предсказания и, соответственно, буду работать с моделями под интерфейсом ForecasterBase, который предоставляет следующие основные функции:
train — обучить модель на входных данных;
forecast — получить предсказание;
plot_forecast — изобразить на графике сравнение между предсказанным временным рядом и тестовыми данными;
и др.
Чтобы создать Forecaster с какой‑либо моделью внутри, необходимо создать соответствующий экземпляр Config, который будет передан в конструктор.
from merlion.models.forecast.arima import Arima, ArimaConfig
forecast_steps = 48
config1 = ArimaConfig(max_forecast_steps=48, order=[20, 1, 5])
model1 = Arima(config1)
В данном случае ArimaConfig принимает параметры, влияющие на обучение модели. А экземпляр класса Arima предоставляет методы для запуска процесса обучения и построения прогноза.
#обучим модель
train_pred, train_err = model1.train(train, train_config={"enforce_stationarity": True,
"enforce_invertibility": True})
#визуализируем предсказания на обучающей выборке
train.univariates["BGT North of NE 70th Total"].plot(figsize=(20,7))
train_pred.univariates["BGT North of NE 70th Total"].plot(figsize=(20,7))
Предсказание модели на обучающей выборке. График синего цвета отображает обучающую выборку, оранжевого — полученные предсказания.
#получим отметки времени из тестовой выборки
time_stamps = test.time_stamps
#построим прогноз
forecast1, stderr1 = model1.forecast(time_stamps=time_stamps[:forecast_steps])
#рассчитаем метрику MAE
smape1 = ForecastMetric.MAE.value(ground_truth=test[:forecast_steps], predict=forecast1)
print(f"{type(model1).__name__} MAE is {smape1:.3f}")
#визуализируем предсказние на тестовой выборке, параметр #plot_forecast_uncertainty отвечает за отображение межквартильного диапазона
fig, ax = model1.plot_forecast(time_series=test[:forecast_steps], plot_forecast_uncertainty=True)
plt.show()
Метрика MAE модели Arima равна 9.617.
График предсказания модели Arima на тестовой выборке. Синяя область визуализирует межквартильный диапазон (interquartile range).
Если захочу взять принципиально другой метод для предсказания, например, градиентный бустинг, я также легко смогу это сделать, просто инициализировав другую модель. Остальной код, который в последующем можно обернуть в функцию, останется без изменений, в этом и прелесть интерфейсов.
from merlion.models.forecast.trees import LGBMForecasterConfig
from merlion.models.forecast.trees import LGBMForecaster
modelConfig = LGBMForecasterConfig(ts_encoding="h", max_forecast_steps=48, n_past=48, n_estimators=150)
model2 = LGBMForecaster(modelConfig)
forecast2, stderr2 = model2.train(train)
#визуализируем предсказания на обучающей выборке
train.univariates["BGT North of NE 70th Total"].plot(figsize=(20,7))
train_pred.univariates["BGT North of NE 70th Total"].plot(figsize=(20,7))
#получим отметки времени из тестовой выборки
time_stamps = test.time_stamps
#построим прогноз
forecast2, stderr2 = model2.forecast(time_stamps=time_stamps[:forecast_steps])
#рассчитаем метрику MAE
smape2 = ForecastMetric.MAE.value(ground_truth=test, predict=forecast2)
print(f"{type(model2).__name__} MAE is {smape2:.3f}")
#визуализируем предсказние на тестовой выборке
fig, ax = model2.plot_forecast(time_series=test[:forecast_steps], plot_forecast_uncertainty=True)
plt.show ()
Метрика MAE модели градиентного бустинга равна 10.501.
Предсказание на тестовой выборке
Что делать, если я хочу использовать модель, реализации которой нет в библиотеке?
Архитектура Merlion располагает возможностями к добавлению собственных моделей.
Схема зависимости DeepForecaster от nn.Module (базовый модуль для построения нейронных систем во фреймворке PyTotch)
Как видно, DeepForecaster является предком torch.nn.Module, что даёт возможность реализовывать нейронные сети в PyTorch и оборачивать их в удобную обертку DeepForecaster.
Ансамблирование
Ансамблирование — это метод в машинном обучении, цель которого объединить разные модели, обученные для решения одной и той же задачи. Полученные предсказания каждой модели обычно усредняются, и такая комбинированная оценка часто оказывается лучше предсказания каждой модели по отдельности.
Merlion позаботился об ансамблировании за пользователей и предоставляет два варианта как можно скомбинировать модели:
ensemble — агрегирует предсказания каждой из моделей (медиана, среднее, максимум, средневзвешенное);
selector — выбирает лучшую модель, основываясь на заданной метрике.
ForecasterEnsemble наследуется от класса ForecasterBase, что даёт возможность использовать его точно также, как и обычные модели выше.
from merlion.evaluate.forecast import ForecastMetric
from merlion.models.ensemble.combine import Mean, Median, ModelSelector
from merlion.models.ensemble.forecast import ForecasterEnsemble, ForecasterEnsembleConfig
ensemble_config = ForecasterEnsembleConfig(
combiner=Median(), models=[model1, model2])
ensemble = ForecasterEnsemble(config=ensemble_config)
selector_config = ForecasterEnsembleConfig(
combiner=ModelSelector(metric=ForecastMetric.MAE))
selector = ForecasterEnsemble(
config=selector_config, models=[model1, model2])
forecast_sel, stderr_sel = ensemble.train(train)
#визуализируем предсказания на обучающей выборке
train.univariates["BGT North of NE 70th Total"].plot(figsize=(20,7))
forecast_sel.univariates["BGT North of NE 70th Total"].plot(figsize=(20,7))
#получим отметки времени из тестовой выборки
time_stamps = test.time_stamps
#построим прогноз
forecast_sel, stderr_sel = ensemble.forecast(time_stamps=time_stamps[:forecast_steps])
#рассчитаем метрику MAE
smape1 = ForecastMetric.MAE.value(ground_truth=test[:forecast_steps], predict=forecast_sel)
print(f"{type(ensemble).__name__} MAE is {smape1:.3f}")
#визуализируем предсказние на тестовой выборке
fig, ax = ensemble.plot_forecast(time_series=test[:forecast_steps], plot_forecast_uncertainty=True)
plt.show()
Метрика MAE ансамбля равна 9.105.
Результаты предсказаний на тестовой выборке ансамбля selector
Как видно, простое ансамблирование двух моделей Arima и LGBM помогло снизить MAE с 9.617 и 10.501 до 9.105.
Оценка
Расчёт метрик качества предсказаний средствами фреймворка.
Класс ForecastMetric содержит следующие метрики:
Mean Absolute Error (MAE);
Mean Absolute Ranged Relative Error (MARRE);
Root Mean Squared Error (RMSE);
symmetric Mean Absolute Percentage Error (sMAPE);
Root Mean Square Percent Error;
Mean Absolute Scaled Error (MASE);
Mean Scaled Interval Score (MSIS).
Выше я уже оценивал точность прогноза, но делал это только на части тренировочной выборки. Это не даёт уверенности в том, что при дообучении модели на новых данных, точность не упадёт. Поэтому в Merlion был реализован класс ForecastEvaluator, позволяющий проводить Rolling Cross Validation.
Это проверка, суть которой в постоянном расширении обучающей выборки за счёт старой тестовой, что позволяет убедиться, что модель не просто обучилась единожды подгонять прогноз под тестовую выборку, но и способна выдерживать дальнейшие обучения без сильной потери в точности уже на новых данных.
from merlion.evaluate.forecast import ForecastEvaluator, ForecastEvaluatorConfig, ForecastMetric
def create_evaluator(model):
model.reset()
evaluator = ForecastEvaluator(
model=model, config=ForecastEvaluatorConfig(
horizon="48h", retrain_freq="24h")
)
return evaluator
ensemble_evaluator = create_evaluator(model1)
ensemble_train_result, ensemble_test_result = ensemble_evaluator.get_predict(train_vals=train,
test_vals=test[:240])
mae = ensemble_evaluator.evaluate(
ground_truth=test[:240],
predict=ensemble_test_result,
metric=ForecastMetric.MAE)
print(f"{type(ensemble).__name__} MAE: {mae:.3f}")
Метрика MAE ансамбля при кросс‑валидации равна 16.197.
В ForecastEvaluatorConfig я задал частоту повторного обучения 24 часа, также можно задать и другие параметры, например, ограничить максимальный размер тренировочной выборки (например, если не захочу проверять на совсем старых значениях) или задать как часто будут делаться предсказания для проверки (что влияет на количество тренировок и конечно же на общее время выполнения).
Получается, я не единожды обучил на тренировочной выборке и проверил, а разбил тренировочную на несколько частей и оценивали каждый раз как меняются метрики, что позволило собрать больше данных о результате обучения. В данном случае такая проверка показала, что в результате обучения на порционно подаваемых данных, которые разделены по принципу Rolling Cross Validation, метрика MAE возрастает c 9.105 до 16.197. Из чего можно сделать вывод о снижении предсказательной способности.
Вывод
В посте был проведён лишь поверхностный анализ возможностей Merlion, полностью описать которые сложно даже за серия постов. Я убедился, что данное решение помогает упростить работу с временными рядами и предсказательными моделями. Становится неважно, работаю с нейронной сетью или простой ARIMA моделью, Merlion даёт возможность быстро попробовать разные варианты, при этом экономя драгоценное время на подготовку данных. Библиотека развивается в упрощении работы с временными рядами, например, появлением графического интерфейса со встроенными no‑code autoML решениями. Набор возможностей перекрывает все остальные фреймворки. Из минусов — библиотека пока что требует уверенные знания Python т.к. потребуется нередко залезать в исходные коды (советую сразу работать в IDE), чтобы понять, что могло пойти не так, т.к. сообщения об ошибках иногда не наталкивают на скорейший путь к исправлению.
Также могу отметить разве что редкое упоминание в интернете, что я и пытаюсь исправить написанием поста.
Благодарю за прочтение, код из примеров вы сможете найти на github.