Предсказание временного ряда с помощью lstm
Я начал изучение изучение библиотеки keras для того, чтобы предсказывать временные ряды (в особенности с помощью lstm).
В качестве туториала я выбрал статью Джейсона Броунли (https://machinelearningmastery.com/time-series-forecasting-long-short-term-memory-network-python/). По ней у меня есть пару вопросов.
В начале параграфа LSTM Data Preparation появляется от части непонятный для меня механизм действий.
Transform Time Series to Supervised Learning - предполагает, что мы переходим к машинному обучению с учителем, что логично. Но делается это следующим образом:
def timeseries_to_supervised(data, lag=1):
df = DataFrame(data)
columns = [df.shift(i) for i in range(1, lag+1)]
columns.append(df)
df = concat(columns, axis=1)
df.fillna(0, inplace=True)
return df
0 0
0 0.000000 266.000000
1 266.000000 145.899994
2 145.899994 183.100006
3 183.100006 119.300003
4 119.300003 180.300003
То есть наш временной ряд разбивается на Х и y. Где в качестве Х у нас значения временного ряда, только сдвинутые на один шаг вперед, а самое первое значение, которое становится Nan, мы перевоплощаем в 0. В качестве y выступают обычные (не сдвинутые) значения нашего временного ряда
1 вопрос: В статье не указывается, почему именно этим образом мы формируем Х и y. Почему не легче использовать даты или индексы наших данных? Почему именно этот способ используется (не только в этой статье) для формирования обучения с учителем?
Ну и собственно 2 вопрос, вытекающий из первого: В главе Complete LSTM Example используется функция для предсказания:
# make a one-step forecast
def forecast_lstm(model, batch_size, X):
X = X.reshape(1, 1, len(X))
yhat = model.predict(X, batch_size=batch_size)
return yhat[0,0]
И еще полный цикл предсказания с используемой функцией выше
# walk-forward validation on the test data
predictions = list()
for i in range(len(test_scaled)):
# make one-step forecast
X, y = test_scaled[i, 0:-1], test_scaled[i, -1]
yhat = forecast_lstm(lstm_model, 1, X)
# invert scaling
yhat = invert_scale(scaler, X, yhat)
# invert differencing
yhat = inverse_difference(raw_values, yhat, len(test_scaled)+1-i)
# store forecast
predictions.append(yhat)
expected = raw_values[len(train) + i + 1]
print('Month=%d, Predicted=%f, Expected=%f' % (i+1, yhat, expected))
Как так получается, что модель предсказывает по сдвинутым значениям ряда? Буду очень благодарен за объяснение.
Ответы (2 шт):
Кажется, вы не совсем понимаете в чём смысл таких моделей. А смысл именно в том, что по нескольким предыдущим значениям предсказывается следующее значение. И не важно какая у этих значений дата, индекс или что. Из данных есть только N предыдущих значений, такая вот модель. В этом суть моделей типа RNN, LSTM и прочих подобных. В принципе, на этом же основаны модели предсказания временных рядов семейства ARIMA (хотя я в них не силён и могу тут ошибаться). Да - такие модели не учитывают сезонность и прочие факторы, которые можно было бы извлечь из даты, поэтому в реальной жизни их используют в основном для кратковременных предсказаний. Ну, либо сначала вычитают из данных сезонный фактор, тренд и т.п., а потом уже применяют такие модели.
Почему такой способ? Потому что это удобно. Например, вы берёте 5 предыдущих значений и по ним вычисляете текущее значение. У вас получается обычная табличка с данными, вы можете применять любые модели, которые работают с табличными данными, начиная с линейной регрессии и заканчивая ансамблями (например,
XGBoost). Модели, которые работают именно с временными рядами специфичные - их меньше, с ними нужно уметь обращаться, проверять их работу нужно особо тщательно. А тут у вас обычные табличные данные и обычные модели, с которыми все умеют работать, которые легко настраивать, интерпретировать. Удобно.Почему делаются такие преобразования данных (дифференцирование, скалирование)? И теория и практика показывают, что при таком варианте работы с данными (предсказание по сдвинутым данным) лучше получается предсказывать изменение данных, чем их абсолютную величину. Да это и в целом касается предсказания следующего значения в задаче на временные ряды. Поэтому и берётся разница между соседними значениями, а не сами значения. Ну и ещё нормализация значений делается.
P.S. По итогам обсуждения в комментариях должен напомнить известный факт. Обычно лучшее предсказание следующего шага временного ряда, будь это курс акций или что-то ещё более-менее непрерывное - это просто предыдущее значение ряда. У вас может получиться прекрасная метрика на вашем предсказании, но не обманывайтесь, а сравните, какой скор получит модель, которая предсказывает следующее значение равное предыдущему. Не исключено, что такая модель даст скор лучше, чем ваша более сложная модель.
А если вы попытаетесь предсказывать не следующий шаг ряда, а сразу через несколько шагов вперёд, то обнаружите, что с каждым таким шагом прогноз всё больше расходится с реальностью и сделать с этим по большому счёту почти ничего нельзя. Неопределённость неизбежно нарастает, поскольку обычно предсказываете вы всё-таки довольно случайный процесс.
import torch
import torch.nn as nn
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
class LSTMModel(nn.Module):
def __init__(self, input_size, hidden_layer_size, output_size):
super(LSTMModel, self).__init__()
self.lstm = nn.LSTM(input_size, hidden_layer_size, batch_first=True)
self.linear = nn.Linear(hidden_layer_size, output_size)
def forward(self, x):
lstm_out, _ = self.lstm(x)
predictions = self.linear(lstm_out[:, -1, :])
return predictions
X_train_tensor = torch.tensor(X_train, dtype=torch.float32).to(device)
y_train_tensor = torch.tensor(y_train, dtype=torch.float32).to(device)
model = LSTMModel(input_size=1, hidden_layer_size=5, output_size=1)
print(model)
learning_rate = 0.001
num_epochs = 100
Определение потерь и оптимизатора
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
Обучение модели
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
epochs = 10
X_train_tensor = torch.tensor(X_train, dtype=torch.float32)
y_train_tensor = torch.tensor(y_train, dtype=torch.float32)
for epoch in range(epochs):
model.train()
optimizer.zero_grad()
y_pred = model(X_train_tensor)
loss = criterion(y_pred, y_train_tensor)
loss.backward()
optimizer.step()
if (epoch + 1) % 10 == 0:
print(f'Epoch {epoch + 1}/{epochs}, Loss: {loss.item()}')
Или можно так:
class LSTMModel(nn.Module):
def __init__(self, input_size, hidden_layer_size, output_size):
super(LSTMModel, self).__init__()
self.lstm = nn.LSTM(input_size, hidden_layer_size, batch_first=True)
self.linear = nn.Linear(hidden_layer_size, output_size)
def forward(self, x):
lstm_out, _ = self.lstm(x)
predictions = self.linear(lstm_out[:, -1, :])
return predictions
model = LSTMModel(input_size=1, hidden_layer_size=50, output_size=1)
print(model)
Обучение модели:
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
epochs = 100
X_train_tensor = torch.tensor(X_train, dtype=torch.float32)
y_train_tensor = torch.tensor(y_train, dtype=torch.float32)
for epoch in range(epochs):
model.train()
optimizer.zero_grad()
y_pred = model(X_train_tensor)
loss = criterion(y_pred, y_train_tensor)
loss.backward()
optimizer.step()
if (epoch + 1) % 10 == 0:
print(f'Epoch {epoch + 1}/{epochs}, Loss: {loss.item()}')