Прогнозирование временных рядов с LSTM в Python
Данные временных рядов, как следует из названия, – это тип данных, который изменяется со временем. Например, температура в течение 24-часового периода времени, цена различных продуктов в течение месяца, цены акций конкретной компании в течение года. Продвинутые модели глубокого обучения, такие как Long Short Term Memory Networks (LSTM), способны улавливать закономерности в данных временных рядов и поэтому могут быть использованы для прогнозирования будущего тренда данных. В этой статье вы увидите, как использовать алгоритм LSTM для прогнозирования будущего с использованием данных временных рядов.
В одной из моих предыдущих статей я объяснил, как выполнить анализ временных рядов с использованием LSTM в библиотеке Keras для прогнозирования будущих цен акций. В этой статье мы будем использовать библиотеку Python , которая является одной из наиболее часто используемых библиотек Python для глубокого обучения.
Прежде чем продолжить, предполагается, что вы владеете языком программирования Python среднего уровня и установили библиотеку PyTorch. Кроме того, поможет знание основных концепций машинного обучения и концепций глубокого обучения. Если вы еще не установили PyTorch, вы можете сделать это с помощью следующей команды pip:
$ pip install pytorch
Набор данных и определение проблемы
Набор данных, который мы будем использовать, встроен в библиотеку Python Seaborn . Давайте сначала импортируем необходимые библиотеки, а затем импортируем набор данных:
import torch import torch.nn as nn import seaborn as sns import numpy as np import pandas as pd import matplotlib.pyplot as plt %matplotlib inline
Давайте распечатаем список всех наборов данных, встроенных в библиотеку Seaborn:
sns.get_dataset_names()
Выход:
['anscombe', 'attention', 'brain_networks', 'car_crashes', 'diamonds', 'dots', 'exercise', 'flights', 'fmri', 'gammas', 'iris', 'mpg', 'planets', 'tips', 'titanic']
Набор данных, который мы будем использовать, – это набор данных flights
. Давайте загрузим набор данных в наше приложение и посмотрим, как он выглядит:
flight_data = sns.load_dataset("flights") flight_data.head()
Выход:
Набор данных состоит из трех столбцов: год
, месяц
и пассажиры
. Столбец пассажиры
содержит общее количество путешествующих пассажиров за указанный месяц. Давайте построим график формы нашего набора данных:
flight_data.shape
Выход:
(144, 3)
Вы можете видеть, что в наборе данных есть 144 строки и 3 столбца, что означает, что набор данных содержит 12-летнюю историю путешествий пассажиров.
Задача состоит в том, чтобы предсказать количество пассажиров, путешествовавших за последние 12 месяцев, исходя из первых 132 месяцев. Помните, что у нас есть запись 144 месяцев, а это означает, что данные за первые 132 месяца будут использоваться для обучения нашей модели LSTM, в то время как производительность модели будет оцениваться с использованием значений за последние 12 месяцев.
Давайте построим график частоты поездок пассажиров в месяц. Следующий сценарий увеличивает размер участка по умолчанию:
fig_size = plt.rcParams["figure.figsize"] fig_size[0] = 15 fig_size[1] = 5 plt.rcParams["figure.figsize"] = fig_size
И этот следующий сценарий строит ежемесячную частоту числа пассажиров:
plt.title('Month vs Passenger') plt.ylabel('Total Passengers') plt.xlabel('Months') plt.grid(True) plt.autoscale(axis='x',tight=True) plt.plot(flight_data['passengers'])
Выход:
Вывод показывает, что с годами среднее количество пассажиров, путешествующих воздушным транспортом, увеличилось. Количество пассажиров, путешествующих в течение года, колеблется, что имеет смысл, потому что во время летних или зимних каникул количество путешествующих пассажиров увеличивается по сравнению с другими частями года.
Предварительная обработка данных
Типы столбцов в нашем наборе данных-это object
, как показано в следующем коде:
flight_data.columns
Выход:
Index(['year', 'month', 'passengers'], dtype='object')
Первым шагом предварительной обработки является изменение типа столбца passengers
на float
.
all_data = flight_data['passengers'].values.astype(float)
Теперь, если вы распечатаете массив all_data
numpy, вы увидите следующие значения плавающего типа:
print(all_data)
Выход:
[112. 118. 132. 129. 121. 135. 148. 148. 136. 119. 104. 118. 115. 126. 141. 135. 125. 149. 170. 170. 158. 133. 114. 140. 145. 150. 178. 163. 172. 178. 199. 199. 184. 162. 146. 166. 171. 180. 193. 181. 183. 218. 230. 242. 209. 191. 172. 194. 196. 196. 236. 235. 229. 243. 264. 272. 237. 211. 180. 201. 204. 188. 235. 227. 234. 264. 302. 293. 259. 229. 203. 229. 242. 233. 267. 269. 270. 315. 364. 347. 312. 274. 237. 278. 284. 277. 317. 313. 318. 374. 413. 405. 355. 306. 271. 306. 315. 301. 356. 348. 355. 422. 465. 467. 404. 347. 305. 336. 340. 318. 362. 348. 363. 435. 491. 505. 404. 359. 310. 337. 360. 342. 406. 396. 420. 472. 548. 559. 463. 407. 362. 405. 417. 391. 419. 461. 472. 535. 622. 606. 508. 461. 390. 432.]
Далее мы разделим наш набор данных на обучающие и тестовые наборы. Алгоритм LSTM будет обучаться на обучающем наборе. Затем модель будет использоваться для составления прогнозов на тестовом наборе. Прогнозы будут сравниваться с фактическими значениями в тестовом наборе для оценки производительности обученной модели.
Первые 132 записи будут использоваться для обучения модели, а последние 12 записей будут использоваться в качестве тестового набора. Следующий сценарий делит данные на обучающие и тестовые наборы.
test_data_size = 12 train_data = all_data[:-test_data_size] test_data = all_data[-test_data_size:]
Теперь давайте напечатаем длину тестовых и тренировочных наборов:
print(len(train_data)) print(len(test_data))
Выход:
132 12
Если вы сейчас распечатаете тестовые данные, то увидите, что они содержат последние 12 записей из массива all_data
numpy:
print(test_data)
Выход:
[417. 391. 419. 461. 472. 535. 622. 606. 508. 461. 390. 432.]
Наш набор данных на данный момент не нормализован. Общее число пассажиров в первые годы значительно меньше по сравнению с общим числом пассажиров в последующие годы. Очень важно нормализовать данные для предсказаний временных рядов. Мы выполним минимальное/максимальное масштабирование набора данных, которое нормализует данные в определенном диапазоне минимальных и максимальных значений. Мы будем использовать класс MinMaxScaler
из модуля sklearn.preprocessing
для масштабирования наших данных. Для получения более подробной информации о реализации шкалы min/max посетите эту ссылку .
Следующий код нормализует наши данные с помощью скалера min/max с минимальными и максимальными значениями -1 и 1 соответственно.
from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(-1, 1)) train_data_normalized = scaler.fit_transform(train_data .reshape(-1, 1))
Теперь давайте напечатаем первые 5 и последние 5 записей наших нормализованных данных о поездах.
print(train_data_normalized[:5]) print(train_data_normalized[-5:])
Выход:
[[-0.96483516] [-0.93846154] [-0.87692308] [-0.89010989] [-0.92527473]] [[1. ] [0.57802198] [0.33186813] [0.13406593] [0.32307692]]
Вы можете видеть, что значения набора данных теперь находятся в диапазоне от -1 до 1.
Здесь важно отметить, что нормализация данных применяется только к обучающим данным, а не к тестовым. Если нормализация применяется к тестовым данным, существует вероятность того, что некоторая информация будет просочена из обучающего набора в тестовый набор.
Следующим шагом является преобразование нашего набора данных в тензоры, поскольку модели PyTorch обучаются с использованием тензоров. Чтобы преобразовать набор данных в тензоры, мы можем просто передать наш набор данных конструктору объекта FloatTensor
, как показано ниже:
train_data_normalized = torch.FloatTensor(train_data_normalized).view(-1)
Последний этап предварительной обработки-преобразование обучающих данных в последовательности и соответствующие метки.
Вы можете использовать любую длину последовательности, и это зависит от знания предметной области. Однако в нашем наборе данных удобно использовать последовательность длиной 12, так как у нас есть ежемесячные данные, а в году 12 месяцев. Если бы у нас были ежедневные данные, лучшая длина последовательности была бы 365, то есть количество дней в году. Поэтому мы установим длину входной последовательности для обучения равной 12.
train_window = 12
Далее мы определим функцию с именем create_input_sequences
. Функция примет необработанные входные данные и вернет список кортежей. В каждом кортеже первый элемент будет содержать список из 12 пунктов, соответствующих количеству пассажиров, путешествующих за 12 месяцев, второй элемент кортежа будет содержать один пункт, т. е. количество пассажиров за 12+1-й месяц.
def create_inout_sequences(input_data, tw): inout_seq = [] L = len(input_data) for i in range(L-tw): train_seq = input_data[i:i+tw] train_label = input_data[i+tw:i+tw+1] inout_seq.append((train_seq ,train_label)) return inout_seq
Выполните следующий сценарий для создания последовательностей и соответствующих меток для обучения:
train_inout_seq = create_inout_sequences(train_data_normalized, train_window)
Если вы распечатаете длину списка train_in out_seq
, то увидите, что он содержит 120 элементов. Это происходит потому, что, хотя обучающий набор содержит 132 элемента, длина последовательности равна 12, что означает, что первая последовательность состоит из первых 12 элементов, а 13-й элемент является меткой для первой последовательности. Аналогично, вторая последовательность начинается со второго элемента и заканчивается на 13-м элементе, тогда как 14-й элемент является меткой для второй последовательности и так далее.
Теперь давайте напечатаем первые 5 элементов списка train_in out_seq
:
train_inout_seq[:5]
Выход:
[(tensor([-0.9648, -0.9385, -0.8769, -0.8901, -0.9253, -0.8637, -0.8066, -0.8066, -0.8593, -0.9341, -1.0000, -0.9385]), tensor([-0.9516])), (tensor([-0.9385, -0.8769, -0.8901, -0.9253, -0.8637, -0.8066, -0.8066, -0.8593, -0.9341, -1.0000, -0.9385, -0.9516]), tensor([-0.9033])), (tensor([-0.8769, -0.8901, -0.9253, -0.8637, -0.8066, -0.8066, -0.8593, -0.9341, -1.0000, -0.9385, -0.9516, -0.9033]), tensor([-0.8374])), (tensor([-0.8901, -0.9253, -0.8637, -0.8066, -0.8066, -0.8593, -0.9341, -1.0000, -0.9385, -0.9516, -0.9033, -0.8374]), tensor([-0.8637])), (tensor([-0.9253, -0.8637, -0.8066, -0.8066, -0.8593, -0.9341, -1.0000, -0.9385, -0.9516, -0.9033, -0.8374, -0.8637]), tensor([-0.9077]))]
Вы можете видеть, что каждый элемент является кортежем, где первый элемент состоит из 12 элементов последовательности, а второй элемент кортежа содержит соответствующую метку.
Создание модели LSTM
Мы предварительно обработали данные, теперь самое время обучить нашу модель. Мы определим класс LSTM
, который наследуется от класса nn.Module
библиотеки PyTorch. Посмотрите мою последнюю статью, чтобы увидеть как создать классификационную модель с помощью PyTorch . Эта статья поможет вам понять, что происходит в следующем коде.
class LSTM(nn.Module): def __init__(self, input_size=1, hidden_layer_size=100, output_size=1): super().__init__() self.hidden_layer_size = hidden_layer_size self.lstm = nn.LSTM(input_size, hidden_layer_size) self.linear = nn.Linear(hidden_layer_size, output_size) self.hidden_cell = (torch.zeros(1,1,self.hidden_layer_size), torch.zeros(1,1,self.hidden_layer_size)) def forward(self, input_seq): lstm_out, self.hidden_cell = self.lstm(input_seq.view(len(input_seq) ,1, -1), self.hidden_cell) predictions = self.linear(lstm_out.view(len(input_seq), -1)) return predictions[-1]
Позвольте мне кратко изложить то, что происходит в приведенном выше коде. Конструктор класса LSTM
принимает три параметра:
input_size
: Соответствует количеству объектов во входных данных. Хотя длина нашей последовательности равна 12, для каждого месяца у нас есть только 1 значение, то есть общее количество пассажиров, поэтому входной размер будет равен 1.hidden_layer_size
: Определяет количество скрытых слоев вместе с количеством нейронов в каждом слое. У нас будет один слой из 100 нейронов.output_size
: Количество элементов в выводе, так как мы хотим предсказать количество пассажиров на 1 месяц в будущем, размер вывода будет равен 1.
Далее в конструкторе мы создаем переменные hidden_layer_size
, lstm
, linear
и hidden_cell
. Алгоритм LSTM принимает три входа: предыдущее скрытое состояние, предыдущее состояние ячейки и текущий вход. Переменная hidden_cell
содержит предыдущее скрытое состояние и состояние ячейки. Переменные lstm
и linear
layer используются для создания слоев LSTM и linear.
Внутри метода forward
в качестве параметра передается input_seq
, который сначала передается через слой lstm
. Выход слоя lstm
– это скрытое состояние и состояние ячейки на текущем временном шаге, а также выход. Выходные данные из слоя lstm
передаются в слой linear
. Прогнозируемое количество пассажиров сохраняется в последнем элементе списка predictions
, который возвращается вызывающей функции.
Следующим шагом является создание объекта класса LSTM ()
, определение функции потерь и оптимизатора. Поскольку мы решаем задачу классификации, мы будем использовать кросс-энтропийные потери . Для функции оптимизатора мы будем использовать adam optimizer .
model = LSTM() loss_function = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
Давайте напечатаем нашу модель:
print(model)
Выход:
LSTM( (lstm): LSTM(1, 100) (linear): Linear(in_features=100, out_features=1, bias=True) )
Обучение модели
Мы будем тренировать нашу модель в течение 150 эпох. Вы можете попробовать с большим количеством эпох, если хотите. Потери будут печататься через каждые 25 эпох.
epochs = 150 for i in range(epochs): for seq, labels in train_inout_seq: optimizer.zero_grad() model.hidden_cell = (torch.zeros(1, 1, model.hidden_layer_size), torch.zeros(1, 1, model.hidden_layer_size)) y_pred = model(seq) single_loss = loss_function(y_pred, labels) single_loss.backward() optimizer.step() if i%25 == 1: print(f'epoch: {i:3} loss: {single_loss.item():10.8f}') print(f'epoch: {i:3} loss: {single_loss.item():10.10f}')
Выход:
epoch: 1 loss: 0.00517058 epoch: 26 loss: 0.00390285 epoch: 51 loss: 0.00473305 epoch: 76 loss: 0.00187001 epoch: 101 loss: 0.00000075 epoch: 126 loss: 0.00608046 epoch: 149 loss: 0.0004329932
Вы можете получить разные значения, так как по умолчанию веса инициализируются случайным образом в нейронной сети шага.
Делать Прогнозы
Теперь, когда наша модель обучена, мы можем начать делать прогнозы. Поскольку наш тестовый набор содержит данные о пассажирах за последние 12 месяцев, а наша модель обучена делать прогнозы, используя длину последовательности 12. Сначала мы отфильтруем последние 12 значений из обучающего набора:
fut_pred = 12 test_inputs = train_data_normalized[-train_window:].tolist() print(test_inputs)
Выход:
[0.12527473270893097, 0.04615384712815285, 0.3274725377559662, 0.2835164964199066, 0.3890109956264496, 0.6175824403762817, 0.9516483545303345, 1.0, 0.5780220031738281, 0.33186814188957214, 0.13406594097614288, 0.32307693362236023]
Вы можете сравнить приведенные выше значения с последними 12 значениями списка данных train_data_normalized
.
Первоначально элемент test_inputs
будет содержать 12 элементов. Внутри цикла for
эти 12 элементов будут использоваться для предсказания первого элемента из тестового набора, то есть элемента под номером 133. Затем значение прогноза будет добавлено в список test_inputs
. Во время второй итерации снова будут использованы последние 12 элементов в качестве входных данных, и будет сделано новое предсказание, которое затем снова будет добавлено в список test_inputs
. Цикл for
будет выполняться 12 раз, так как в тестовом наборе имеется 12 элементов. В конце цикла список test_inputs
будет содержать 24 элемента. Последние 12 пунктов будут прогнозируемыми значениями для тестового набора.
Для составления прогнозов используется следующий сценарий:
model.eval() for i in range(fut_pred): seq = torch.FloatTensor(test_inputs[-train_window:]) with torch.no_grad(): model.hidden = (torch.zeros(1, 1, model.hidden_layer_size), torch.zeros(1, 1, model.hidden_layer_size)) test_inputs.append(model(seq).item())
Если вы распечатаете длину списка test_inputs
, то увидите, что он содержит 24 элемента. Последние 12 прогнозируемых элементов могут быть напечатаны следующим образом:
test_inputs[fut_pred:]
Выход:
[0.4574652910232544, 0.9810629487037659, 1.279405951499939, 1.0621851682662964, 1.5830546617507935, 1.8899496793746948, 1.323508620262146, 1.8764172792434692, 2.1249167919158936, 1.7745600938796997, 1.7952896356582642, 1.977765679359436]
Уместно еще раз упомянуть, что вы можете получить различные значения в зависимости от весов, используемых для обучения LSTM.
Поскольку мы нормализовали набор данных для обучения, прогнозируемые значения также нормализуются. Нам нужно преобразовать нормализованные прогнозные значения в фактические прогнозные значения. Мы можем сделать это, передав нормализованные значения в метод inverse_transform
объекта min/max scaler, который мы использовали для нормализации нашего набора данных.
actual_predictions = scaler.inverse_transform(np.array(test_inputs[train_window:] ).reshape(-1, 1)) print(actual_predictions)
Выход:
[[435.57335371] [554.69182083] [622.56485397] [573.14712578] [691.64493555] [761.46355206] [632.59821111] [758.38493103] [814.91857016] [735.21242136] [739.92839211] [781.44169205]]
Теперь построим график предсказанных значений относительно фактических. Посмотрите на следующий код:
x = np.arange(132, 144, 1) print(x)
Выход:
[132 133 134 135 136 137 138 139 140 141 142 143]
В приведенном выше скрипте мы создаем список, содержащий числовые значения за последние 12 месяцев. Первый месяц имеет значение индекса 0, поэтому последний месяц будет иметь индекс 143.
В следующем сценарии мы построим график общего числа пассажиров за 144 месяца вместе с прогнозируемым числом пассажиров за последние 12 месяцев.
plt.title('Month vs Passenger') plt.ylabel('Total Passengers') plt.grid(True) plt.autoscale(axis='x', tight=True) plt.plot(flight_data['passengers']) plt.plot(x,actual_predictions) plt.show()
Выход:
Предсказания, сделанные нашим LSTM, изображены оранжевой линией. Вы можете видеть, что наш алгоритм не слишком точен, но все же он смог зафиксировать восходящий тренд для общего числа пассажиров, путешествующих за последние 12 месяцев, а также случайные колебания. Вы можете попробовать с большим количеством эпох и с большим количеством нейронов в мозге.
Чтобы лучше видеть результат, мы можем построить график фактического и прогнозируемого количества пассажиров за последние 12 месяцев следующим образом:
plt.title('Month vs Passenger') plt.ylabel('Total Passengers') plt.grid(True) plt.autoscale(axis='x', tight=True) plt.plot(flight_data['passengers'][-train_window:]) plt.plot(x,actual_predictions) plt.show()
Выход:
Опять же, прогнозы не очень точны, но алгоритм смог уловить тенденцию, что количество пассажиров в будущих месяцах должно быть выше, чем в предыдущие месяцы с периодическими колебаниями.
Вывод
LSTM-один из наиболее широко используемых алгоритмов для решения задач последовательности. В этой статье мы рассмотрели, как делать прогнозы на будущее, используя данные временных рядов с помощью LSTM. Вы также видели, как реализовать ITSM с помощью библиотеки PyTorch, а затем как построить прогнозируемые результаты по фактическим значениям, чтобы увидеть, насколько хорошо работает обученный алгоритм.