Прогнозирование временных рядов с использованием LSTM с PyTorch в Python

Данные временных рядов, как следует из названия, представляют собой тип данных, который изменяется со временем. Например, температура в 24-часовом периоде времени, цена на различные продукты в месяц, курс акций конкретной компании за год. Расширенные модели глубокого обучения, такие как сети долгосрочной краткосрочной памяти [https://en.wikipedia.org/wiki/Long_short-term_memory] (LSTM), способны фиксировать закономерности в данных временных рядов и, следовательно, могут использоваться для создания прогнозы относительно будущей тенденции

Данные временных рядов, как следует из названия, представляют собой тип данных, который изменяется со временем. Например, температура в 24-часовом периоде времени, цена на различные продукты в месяц, курс акций конкретной компании за год. Усовершенствованные модели глубокого обучения, такие как сети долгосрочной краткосрочной памяти (LSTM), способны фиксировать закономерности в данных временных рядов и, следовательно, могут использоваться для прогнозирования будущей тенденции данных. В этой статье вы увидите, как использовать алгоритм LSTM для прогнозирования будущего с использованием данных временных рядов.

В одной из своих предыдущих статей я объяснил, как выполнять анализ временных рядов с использованием LSTM в библиотеке Keras , чтобы предсказать будущие цены на акции. В этой статье мы будем использовать библиотеку PyTorch , которая является одной из наиболее часто используемых библиотек 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() 

Выход:

руководитель набора данных орейсах{.ezlazyload}

В наборе данных есть три столбца: year , month и passengers . В passengers указано общее количество путешествующих пассажиров за указанный месяц. Построим форму нашего набора данных:

 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']) 

Выход:

построение ежемесячной повторяемостипассажиров{.ezlazyload}

Полученные данные показывают, что с годами среднее количество пассажиров, путешествующих по воздуху, увеличилось. Количество пассажиров, путешествующих в течение года, колеблется, что имеет смысл, потому что во время летних или зимних каникул количество путешествующих пассажиров увеличивается по сравнению с другими частями года.

Предварительная обработка данных

Типы столбцов в нашем наборе данных - это object , как показано в следующем коде:

 flight_data.columns 

Выход:

 Index(['year', 'month', 'passengers'], dtype='object') 

Первый шаг предварительной обработки - изменить тип столбца passengers float .

 all_data = flight_data['passengers'].values.astype(float) 

Теперь, если вы распечатаете all_data numpy all_data, вы должны увидеть следующие значения плавающего типа:

 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_inout_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_inout_seq , вы увидите, что он содержит 120 элементов. Это связано с тем, что, хотя обучающий набор содержит 132 элемента, длина последовательности равна 12, что означает, что первая последовательность состоит из первых 12 элементов, а 13-й элемент является меткой для первой последовательности. Точно так же вторая последовательность начинается со второго элемента и заканчивается 13-м элементом, тогда как 14-й элемент является меткой для второй последовательности и так далее.

Теперь напечатаем первые 5 элементов списка train_inout_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 принимает три параметра:

  1. input_size : соответствует количеству функций на входе. Хотя длина нашей последовательности равна 12, для каждого месяца у нас есть только 1 значение, то есть общее количество пассажиров, поэтому размер ввода будет 1.
  2. hidden_layer_size : указывает количество скрытых слоев вместе с количеством нейронов в каждом слое. У нас будет один слой из 100 нейронов.
  3. 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 - это скрытые состояния и состояния ячеек на текущем временном шаге вместе с выходными данными. Выходные lstm слоя linear слою. Прогнозируемое количество пассажиров сохраняется в последнем элементе predictions , который возвращается вызывающей функции.

Следующим шагом является создание объекта класса LSTM() , определение функции потерь и оптимизатора. Поскольку мы решаем задачу классификации, мы будем использовать потерю кросс-энтропии . Для функции оптимизатора мы будем использовать оптимизатор adam .

 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 

Вы можете получить разные значения, поскольку по умолчанию веса инициализируются случайным образом в нейронной сети PyTorch.

Прогнозы

Теперь, когда наша модель обучена, мы можем начинать делать прогнозы. Поскольку наш тестовый набор содержит данные о пассажирах за последние 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 данных 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, который мы использовали для нормализации нашего набора данных.

 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() 

Выход:

отображение общего количествапассажиров{.ezlazyload}

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

Чтобы лучше видеть результат, мы можем построить график фактического и прогнозируемого количества пассажиров за последние 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() 

Выход:

построение прогнозируемого количествапассажиров{.ezlazyload}

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

Заключение

LSTM - один из наиболее широко используемых алгоритмов для решения задач последовательности. В этой статье мы увидели, как делать прогнозы на будущее, используя данные временных рядов с LSTM. Вы также увидели, как реализовать LSTM с библиотекой PyTorch, а затем как построить график предсказанных результатов против фактических значений, чтобы увидеть, насколько хорошо работает обученный алгоритм.

comments powered by Disqus