Предсказание временного ряда с помощью 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 шт):

Автор решения: CrazyElf

Кажется, вы не совсем понимаете в чём смысл таких моделей. А смысл именно в том, что по нескольким предыдущим значениям предсказывается следующее значение. И не важно какая у этих значений дата, индекс или что. Из данных есть только N предыдущих значений, такая вот модель. В этом суть моделей типа RNN, LSTM и прочих подобных. В принципе, на этом же основаны модели предсказания временных рядов семейства ARIMA (хотя я в них не силён и могу тут ошибаться). Да - такие модели не учитывают сезонность и прочие факторы, которые можно было бы извлечь из даты, поэтому в реальной жизни их используют в основном для кратковременных предсказаний. Ну, либо сначала вычитают из данных сезонный фактор, тренд и т.п., а потом уже применяют такие модели.

  1. Почему такой способ? Потому что это удобно. Например, вы берёте 5 предыдущих значений и по ним вычисляете текущее значение. У вас получается обычная табличка с данными, вы можете применять любые модели, которые работают с табличными данными, начиная с линейной регрессии и заканчивая ансамблями (например, XGBoost). Модели, которые работают именно с временными рядами специфичные - их меньше, с ними нужно уметь обращаться, проверять их работу нужно особо тщательно. А тут у вас обычные табличные данные и обычные модели, с которыми все умеют работать, которые легко настраивать, интерпретировать. Удобно.

  2. Почему делаются такие преобразования данных (дифференцирование, скалирование)? И теория и практика показывают, что при таком варианте работы с данными (предсказание по сдвинутым данным) лучше получается предсказывать изменение данных, чем их абсолютную величину. Да это и в целом касается предсказания следующего значения в задаче на временные ряды. Поэтому и берётся разница между соседними значениями, а не сами значения. Ну и ещё нормализация значений делается.

P.S. По итогам обсуждения в комментариях должен напомнить известный факт. Обычно лучшее предсказание следующего шага временного ряда, будь это курс акций или что-то ещё более-менее непрерывное - это просто предыдущее значение ряда. У вас может получиться прекрасная метрика на вашем предсказании, но не обманывайтесь, а сравните, какой скор получит модель, которая предсказывает следующее значение равное предыдущему. Не исключено, что такая модель даст скор лучше, чем ваша более сложная модель.

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

→ Ссылка
Автор решения: Aribama
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()}')
→ Ссылка