Когда одной ARIMA мало. Прогнозирование временных рядов нейросетями
Как сказал один мудрый человек, доклады по временным рядам бывают двух типов. Первый — предсказание *** (попробуйте угадать в комментариях, о чем шла речь), а второй — ориентированные на практическую пользу. Эта статья точно не относится к первому типу, а, надеемся, что ко второму. Но об этом уже судить не нам, а вам в комментариях.
Привет, Хабр! Меня зовут Никита, я работаю в Мегафоне аналитиком больших данных. В этой статье я хочу поговорить про временные ряды, а если конкретнее, про использование нейросетей для их прогнозирования.
На опыте решения реальной задачи я убедился, что сегодня существует немало архитектур, показывающих достаточно высокую точность прогнозирования и при этом нетрудных в использовании. Однако, по сравнению с классическими методами прогнозирования, они не так популярны.
Поэтому сейчас мы разберемся как работают архитектуры DeepAR и NBEATS, как применить их на реальных данных и (главное) когда стоит это делать.
Для полного погружения в тему переходите в репозиторий. Код можно запустить в Colab c помощью всего одной кнопки, параллельно с прочтением статьи.
Теоретическая часть
Обзор задачи
DeepAR
NBEATS
Практическая часть
Датасет, фреймворк
Подготовка данных
Обучение модели
Инференс
Итоги
Свяжем машинное обучение с реальным миром инфраструктуры.
Чтобы не заскучать, мы не будем обсуждать идеи вне контекста их применения. Мы возьмем реальную бизнес задачу, и на примере ее решения покажем, чем хороши в ней нейросети.
Итак, как вы уже, возможно, догадались, мы сотовый оператор, и поэтому у нашего BigData подразделения есть немало задач, напрямую связанных с работой сотовой сети.
Не смотря на то, что существуют разные архитектуры сети сотовой связи, основным элементом в каждой является базовая станция, которая обеспечивает подключение пользователя по беспроводным каналам. В местности с высокой плотностью застройки и большим количеством абонентов, одна базовая станция может обслуживать территорию радиусом в несколько сотен метров, а за городом — территорию радиусом в несколько километров. Выгляните в окно, и наверняка, увидите на одной из крыш соседних домов небольшую вышку — это и есть базовая станция.
Про соты
В основе сетей мобильной связи лежит иерархическая структура, которая обеспечивает эффективную и надежную передачу данных. На самом высоком уровне находятся макросоты (большие зоны покрытия, охватывающие города или регионы), внутри которых существуют более мелкие зоны покрытия, называемые микросотами, и еще более мелкие — пикосоты или фемтосоты. Поскольку именно на эти объекты инфраструктуры ложится основная нагрузка от пользователей, понятно, что они должны быть всегда в строю. А это требует от оператора поддерживать постоянный цикл эксплуатации, ремонта, обновления и мониторинга базовых станций.
Окей, элемент важный, понятно. А при чем тут временные ряды и машинное обучение?
Как я уже писал выше, состояние базовых станций и нагрузка на них (передаваемый объем трафика) мониторится оператором. Поэтому для каждой базовой станции у нас есть история нагрузки на нее. Значит, для каждой базовой станции мы можем составить временной ряд, где для момента в прошлом у нас будет известно значение нагрузки, и чисто теоретически, мы можем попробовать прогнозировать нагрузку наперед.
Какую выгоду мы можем получить от точного прогнозирования нагрузки?
Мы можем более эффективно планировать и распределять сетевые ресурсы в будущем, а значит, улучшать качество связи.
Сократить расходы на оптимизацию инфраструктуры и эксплуатацию оборудования.
Использовать полученные прогнозы в качестве признаков для других задач.
Почему бы не сделать авторегрессию на каждый временной ряд?
Давайте сперва обсудим причины из-за которых мы рассматриваем использование нейросетей в задачах прогнозирования временных рядов. Для этого рассмотрим некоторые недостатки классических методов прогнозирования, которые мы можем успешно компенсировать с помощью нейросетей:
Первым методом, который часто используется, является метод авторегрессии (AR). Его основная слабость заключается в том, что для каждого временного ряда приходится строить отдельную модель, и эти модели не учитывают информацию из других временных рядов.
Также, мы можем извлечь из исследуемых временных рядов признаки и использовать для прогнозирования алгоритмы классического машинного обучения (например, градиентный бустинг). Однако, тогда мы сталкиваемся с тем, что качество предсказаний напрямую зависит от качества созданных нами признаков. Создание признаков может быть трудоемким и неочевидным процессом.
С вышеуказанными недостатками справляются рекуррентные нейронные сети — мы можем обучать их на многих временных рядах и нам не обязательно использовать дополнительные признаки.
Тем более, за последние годы появилось много архитектур способных эффективно решать задачу прогнозирования временных рядов, поэтому в любом случае будет полезно хотя бы знать об их существовании.
Конечно, если в вашем проекте удается достичь необходимой точности и классическими методами — это отлично, нейросети не нужны в каждой задаче)
DeepAR
Первая модель, которой мы коснемся — DeepAR (deep auto regression). Эта модель состоит из нескольких рекуррентных блоков (как правило используются ячейки LSTM или GRU). При этом, тип ячейки, размер скрытого слоя и количество рекуррентных ячеек — это настраиваемые гиперпараметры модели.
Модель называется авторегрессионной, потому что на каждом временном шаге мы передаем в ячейку не только значение скрытого слоя с предыдущего шага (как в обычных рекуррентных сетях), но и предыдущее значение временного ряда/прогноз с предыдущего шага (во время обучения/прогноза на тесте).
Вторая важная идея, на которой построена модель — мы учим ее предсказывать не действительное значение временного ряда, а параметры распределения значений временного ряда таким образом, чтобы максимизировать правдоподобие полученной выборки. В качестве прогноза мы берем среднее значение полученного распределения.
За счет того, что модель возвращает при прогнозе параметры распределения, мы можем осуществлять вероятностный прогноз — то есть возвращать интервалы, в которые с заданной точностью попадает будущее значение временного ряда. Такая возможность может быть очень полезной во многих задачах.
Стоит также отметить, что DeepAR позволяет использовать дополнительные признаки при прогнозировании и обучаться на многих временных рядах одновременно.
NBEATS
В отличие от DeepAR в данной архитектуре используются не рекуррентные слои, а полносвязные.
Сеть получает на вход временной ряд и пропускает его через несколько полносвязных слоев. Затем выход с последнего слоя направляется по двум путям: backcast (моделирует прошлые значения) и forecast (моделирует будущие значения). Эта часть называется блоком сети.
Теперь смотрим на картинку снизу, часть слева. Выход backcast (левая стрелка от блока) вычитается из входного вектора перед следующим блоком, а выход forecast прибавляется к вектору прогноза от предыдущего блока. Такая архитектура позволяет backcast отфильтровать только необходимую информацию из входных данных, а forecast скорректировать прогноз и уменьшить ошибку (сильно напоминает идею градиентного бустинга, не правда ли?). Такая последовательность блоков называется стеком. Выходы со всех стеков также суммируются, и мы получаем глобальный прогноз.
Такая архитектура имеет ряд преимуществ, а именно: быстрее обучается, поскольку нет рекуррентных блоков, а также обладает возможностью тонкой настройки путей backast и forecast, что позволяет достичь очень высокой точности прогнозирования.
У архитектуры NBEATS есть еще одно важное преимущество. Модель способна выделить тренд и сезонную компоненту из каждого исследуемого временного ряда, что повышает интерпретируемость прогнозов. Это особенно важно при непосредственном анализе временных рядов, чтобы заметить изменения в поведении трафика, возможно, обосновать их причины.
Практическая часть
Выбранный датасет
Чтобы практическая часть была воспроизводима и полезна, нам нужен репрезентативный датасет по нашей задаче. Такой существует — Telecom Italia BigDataChallenge. В нем представлены данные по нагрузке базовых станций за два месяца, при этом также доступны и дополнительные данные по потреблению электроэнергии, активности в социальных сетях и т.д.
Данные представлены реальным телекоммуникационным оператором, и единственная операция, которая была произведена с ними, это масштабирование (чтобы не показывать реальные объемы потребления, каждое значение в датасете было умножено на коэффициент, который не указывается). Также, количество временных рядов, соответствующих трафику равняется 10000, что тоже довольно близко к реальным масштабам.
Чтобы получше понять данные, с которыми мы работаем, смотрите ноутбук с EDA.
Выбранный фреймворк
Для прогнозирования временных рядов сегодня существует много фреймворков, и, в большинстве случаев они содержат: классы и методы для подготовки временных рядов к обучению, EDA и реализованные архитектуры моделей.
Есть к примеру ETNA, GluonTS, DART. Рекомендую с ними ознакомиться!)
В рамках нашей практической части мы будем использовать библиотеку pytorch forecasting, она также содержит внутри все нужные нам модели и обладает достаточно гибким интерфейсом. Также, будет понятна всем, кто когда-либо сталкивался с pytorch.
Далее действие происходит в ноутбуках DeepAR и NBEATS. Можете начать с любого и потом перейти ко второму.
Этап 1. Подготовка данных.
Как правило, временные ряды изначально представлены в виде таблиц, но для обучения сети обычный pandas DataFrame не подойдет.
Сейчас наши данные выглядят следующим образом:
square_id | time_idx | … | internet |
47 | 1 | … | 34.2 |
Для каждой базовой станции (обозначенной отдельным square_id) есть значения интернета для временных шагов от 1 до N, где N — длительность обучающей выборки, измеряемая в временных шагах (в нашем случае это часы).
А мы хотим получить из каждого временного ряда пары последовательностей длины T1 и T2, где T1 — это длина последовательности, которую сеть будет получать на вход, а T2 — это длина последовательности, которую мы хотим спрогнозировать. (Параметры T1 и T2 мы можем задать сами, или у нас может быть бизнес требование, что например прогнозировать меньше, чем на два дня вперед не имеет смысла.)
Подробнее про Т1 и Т2
Понятно, что при увеличении T1 мы даем модели больше информации, и это может положительно влиять на качество прогноза, но важно помнить, что это происходит не всегда. К примеру, если в прошлом произошло событие, которое сильно изменило поведение временного ряда, данные из прошлого могут быть лишь дополнительным шумом. Однако, увеличение T1 гарантированно увеличивает сложность модели, что несет в себе негативные последствия. Во-первых, увеличивается число параметров, и сеть обучается дольше, а во-вторых, увеличение числа параметров может привести к переобучению.
Увеличение T2 в свою очередь может сильно повысить сложность решаемой задачи, поскольку неопределенность со временем увеличивается, а автокорреляция ряда падает.
Для обоих параметров нужно выбирать корректную длину временного шага. К примеру, если нужно прогнозировать временной ряд на неделю вперед, и мы делаем прогноз по часам, T2 = 24×7 = 168, что достаточно много, и при высокой дисперсии временного ряда будет сложной задачей. Если это возможно, лучше агрегировать значения по дням, и тогда мы получим задачу, где T2 = 7. Если в качестве агрегации использовать усреднение, то полученный временной ряд будет сильно менее шумен, и задача станет проще.
Чтобы подобрать оптимальные T1 и T2 можно использовать следующие лайфхаки:
Посмотреть на полную и частную АКФ ряда, там мы увидим за какой промежуток времени наблюдается наибольшая взаимосвязь между прошлыми и будущими значениями ряда, и подобрать наилучшее T1 около полученного числа,
В качестве T2 взять минимально возможную длину прогнозирования, удовлетворяющую бизнес требованиям.
Так как же нам перевести DataFrame в нужный формат?
Вышеупомянутые библиотеки позволяют упростить эту задачу, и сделать все буквально через один метод. Посмотрим на примере torch.forecasting. На основе нашей таблицы мы инициализировали класс TimeSeriesDataset и получили то, что нам нужно:
context_length = 60
prediction_length = 24
training = TimeSeriesDataSet(train,
time_idx="time_idx",
target="internet",
categorical_encoders={
"square_id": NaNLabelEncoder()...
group_ids=["square_id"],
static_categoricals=[ "square_id" ],
time_varying_unknown_reals=["internet"],
max_encoder_length=context_length,
max_prediction_length=prediction_length,
allow_missing_timesteps=False)
Но, как видно, указали еще много различных параметров, давайте разберем основное.
time_idx — колонка с индексом временного шага у каждого ряда,
target — колонка с переменной которую мы прогнозируем
group_ids — колонка, по которой мы отличим один ряд от другого, обязательный параметр в задаче, где у нас много временных рядов
context/prediction_length — наши T1 и T2.
Есть также дополнительные параметры, которые могут различаться в зависимости от решаемой задачи и используемой модели, все они описаны в документации.
Пользуясь объектом training можем создать датасет для валидации:
validation = TimeSeriesDataSet.from_dataset(training,
df,
min_prediction_idx=train_cutoff + 1)
А потом в одну строчку и создать dataloader«ы :
batch_size = 128
train_dataloader = training.to_dataloader(train=True,
batch_size=batch_size,
num_workers=4,
batch_sampler="synchronized"
)
val_dataloader = validation.to_dataloader(train=False,
batch_size=batch_size,
num_workers=4,
batch_sampler="synchronized")
Этап 2. Обучение модели
Прелесть класса TimeSeriesDataset состоит в том, что при его создании мы задали и часть параметров модели, и теперь сами сетки можно инициализировать с параметрами, указанными при создании датасета:
net = DeepAR.from_dataset(
training,
learning_rate=3e-2,
hidden_size=40,
rnn_layers=5,
loss=MultivariateNormalDistributionLoss(rank=25),
optimizer="Adam"
)
net = NBeats.from_dataset(
training,
learning_rate=1e-3,
widths=[32, 512],
backcast_loss_ratio=1.0,
)
Сейчас мы не указываем параметры, зависящие от входных данных, такие как context_length и prediction_length. Важно отметить, что доступные параметры для разных моделей различаются, и к примеру, подать датасет с категориальными фичами в NBEATS не получится, поэтому параметры с которыми мы инициализируем TimeSeriesDataset будут различаться.
Когда у нас есть модель и датасет, можно приступить к обучению. Поскольку библиотека не сильно отходит от обычного torch, можно написать стандартный цикл обучения, или же воспользоватся обертками для него, такими как Lightning (сами авторы torch forecasting рекомендуют его использовать):
logger = TensorBoardLogger('...', name='...')
early_stop_callback = EarlyStopping(monitor="val_loss"...)
trainer = pl.Trainer(max_epochs=50, accelerator="cpu",
enable_model_summary=True,
gradient_clip_val=0.1, callbacks=[early_stop_callback],
limit_train_batches=200, limit_val_batches=100,
enable_checkpointing=True,
logger=logger)
trainer.fit(net,
train_dataloaders=train_dataloader,
val_dataloaders=subset_data_loader)
Lightning позволяет не писать с нуля цикл обучения для модели, а воспользоваться для этого классом Trainer и также использовать tensorboard. Это удобный инструмент, который позволит посмотреть на то как менялись значения лоссов и метрик во время обучения на тренировочной и валидационной выборках.
Этап 3. Прогноз.
Чтобы довести пайплайн до конца, нам нужно предусмотреть и как модель будет работать с реальными данными. Предположим, что мы получаем их в схожем с обучающей выборкой формате — эксель таблица со значениями трафика за последний context_length временных шагов.
Первое, что нам нужно сделать — это подготовить данные для создания TimeSeriesDataset. К сожалению, TimeSeriesDataset не позволяет создать датасет длины меньше, чем context_length + prediction length (то есть обязательно должна быть тестовая часть с ответами). Поэтому давайте добавим к нашим временным рядам prediction_length нулей:
test_df = df[df.time_idx > last_idx - context_length]
start_idx = test_df.time_idx.max() + 1
for square_id in test_df.square_id.unique():
data = []
for d in range(config['prediction_length']):
data.append({"square_id": square_id,
"time_idx": start_idx + d,
"internet": 0})
data = pd.DataFrame(data)
test_df = pd.concat([test_df, data])
Теперь можем создать TimeSeriesDataSet, но важно сделать это через метод from_dataset, так как в датасете, который мы создавали для обучения, сохранена вся информация о правильном препроцессинге временных рядов (скейлинг, удаление тренда и т.д.)
infer_dataset = TimeSeriesDataSet.from_dataset(dataset,
test_df)
infer_loader = infer_dataset.to_dataloader(...)
Когда у нас есть dataloader с временными рядами для прогноза, нам нужно лишь сделать predict и привести данные к необходимому формату:
preds = net.predict(infer_loader, ...)
x, y = next(iter(infer_loader))
encoder = infer_dataset.categorical_encoders['square_id']
ids = encoder.inverse_transform(x['decoder_cat'])
time_steps = list(range(1, infer_dataset.max_prediction_length+1))
result = pd.DataFrame(columns=['square_id', 'internet', 'time_idx'])
for base in tqdm(range(test_df.square_id.nunique())):
base_id = ids[base]
forecast = pd.DataFrame(
{'internet': preds[base].numpy().tolist(),
'square_id': base_id.tolist(),
'time_idx': time_steps})
result = pd.concat([result, forecast], axis=0)
return result
Подводя итоги
Обе модели смогли показать качество сильно выше бейзлайна при прогнозировании нагрузки на базовые станции.
Про валидацию
Для оценки производительности разработанных моделей были проведены тренировка и валидация на отдельных наборах данных. Однако, стоит отметить, что данный туториал не включает использование кросс-валидации и других методов валидации, которые могли бы обеспечить более детальную и объективную оценку предложенных алгоритмов. Это решение было принято с целью сокращения объема туториала и упрощения его понимания для начинающих в области временных рядов.
(Если вы еще не открыли ноутбуки, переходите и смотрите результаты, там много красивых графиков)
Рассуждая над тем, какую модель стоит использовать для своей задачи можно опираться на следующие их преимущества:
NBEATS обучается быстрее, позволяет интерпретировать прогнозы (что вполне может быть требованием от заказчика) и при отсутствии дополнительных признаков, вероятно будет точнее DeepAR.
В случае когда у вас есть дополнительные признаки, или же когда нужно не только спрогнозировать значения временного ряда, но и оценить вероятный диапазон их изменений — на помощь придет DeepAR.
А еще было бы очень интересно услышать о вашем опыте использования нейросетей при прогнозировании временных рядов! Какие архитектуры использовали, заняло ли это больше времени по сравнению с классическими подходами, и, главное, стоило ли того?)