Данные временных рядов, как следует из названия, представляют собой тип данных, который изменяется со временем. Например, температура в 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
принимает три параметра:
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 -
это скрытые состояния и состояния ячеек на текущем временном шаге вместе
с выходными данными. Выходные 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, а затем как построить график предсказанных результатов против фактических значений, чтобы увидеть, насколько хорошо работает обученный алгоритм.