LSTM и GRU для прогнозирования цен на акции Amazon

Проблема временного ряда

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

По определению временной ряд - это ряд точек данных, индексированных во временном порядке. Этот тип проблем важен, потому что существует множество задач прогнозирования, которые связаны с временной составляющей, и поиск взаимосвязи между данными и временем является ключом к анализу (например, прогноз погоды и прогноз землетрясений). Однако этими проблемами иногда пренебрегают, потому что моделирование этого отношения компонентов времени не так тривиально, как может показаться.

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

Среди нескольких способов, разработанных на протяжении многих лет для точного прогнозирования сложных и изменчивых колебаний цен на акции, нейронные сети, в частности RNN, показали значительное применение в этой области. Здесь мы собираемся построить две разные модели RNN - LSTM и GRU - с PyTorch, чтобы предсказать рыночную цену Amazon и сравнить их производительность с точки зрения времени и эффективности.

Рекуррентная нейронная сеть (RNN)

Рекуррентная нейронная сеть (RNN) - это тип искусственной нейронной сети, предназначенный для распознавания последовательных шаблонов данных для прогнозирования следующих сценариев. Эта архитектура особенно мощна из-за ее узловых соединений, позволяющих демонстрировать временное динамическое поведение. Еще одна важная особенность этой архитектуры - использование контуров обратной связи для обработки последовательности. Такая характеристика позволяет информации сохраняться, часто ее называют памятью. Такое поведение делает RNN отличными решениями для обработки естественного языка (NLP) и временных рядов. На основе этой структуры были разработаны архитектуры, получившие название Long short-term memory (LSTM) и Gated recurrent units (GRU).

Блок LSTM состоит из ячейки, входного элемента, выходного элемента и элемента забывания. Ячейка запоминает значения за произвольные интервалы времени, а три ворот регулируют поток информации в ячейку и из нее.

С другой стороны, у GRU меньше параметров, чем у LSTM, и у него отсутствует выходной вентиль. Обе структуры могут решить проблему «кратковременной памяти», преследующую обычные RNN, и эффективно сохранять долгосрочные зависимости в последовательных данных.

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

Реализация модели

Набор данных содержит исторические цены на акции (за последние 12 лет) 29 компаний, но я выбрал данные Amazon, потому что подумал, что это может быть интересно.

Мы собираемся спрогнозировать цену закрытия акции, а ниже показано поведение данных по годам.

Мы разрезаем фрейм данных, чтобы получить нужный столбец, и нормализовать данные.

from sklearn.preprocessing import MinMaxScaler
price = data[['Close']]
scaler = MinMaxScaler(feature_range=(-1, 1))
price['Close'] = scaler.fit_transform(price['Close'].values.reshape(-1,1))

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

def split_data(stock, lookback):
    data_raw = stock.to_numpy() # convert to numpy array
    data = []
    
    # create all possible sequences of length seq_len
    for index in range(len(data_raw) - lookback): 
        data.append(data_raw[index: index + lookback])
    
    data = np.array(data);
    test_set_size = int(np.round(0.2*data.shape[0]));
    train_set_size = data.shape[0] - (test_set_size);
    
    x_train = data[:train_set_size,:-1,:]
    y_train = data[:train_set_size,-1,:]
    
    x_test = data[train_set_size:,:-1]
    y_test = data[train_set_size:,-1,:]
    
    return [x_train, y_train, x_test, y_test]
lookback = 20 # choose sequence length
x_train, y_train, x_test, y_test = split_data(price, lookback)

Затем мы преобразуем их в тензоры, что является базовой структурой для построения модели PyTorch.

import torch
import torch.nn as nn
x_train = torch.from_numpy(x_train).type(torch.Tensor)
x_test = torch.from_numpy(x_test).type(torch.Tensor)
y_train_lstm = torch.from_numpy(y_train).type(torch.Tensor)
y_test_lstm = torch.from_numpy(y_test).type(torch.Tensor)
y_train_gru = torch.from_numpy(y_train).type(torch.Tensor)
y_test_gru = torch.from_numpy(y_test).type(torch.Tensor)

Мы определяем некоторые общие значения для обеих моделей относительно слоев.

input_dim = 1
hidden_dim = 32
num_layers = 2
output_dim = 1
num_epochs = 100

LSTM

class LSTM(nn.Module):
    def __init__(self, input_dim, hidden_dim, num_layers, output_dim):
        super(LSTM, self).__init__()
        self.hidden_dim = hidden_dim
        self.num_layers = num_layers
        
        self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers, batch_first=True)
        self.fc = nn.Linear(hidden_dim, output_dim)
    def forward(self, x):
        h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_dim).requires_grad_()
        c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_dim).requires_grad_()
        out, (hn, cn) = self.lstm(x, (h0.detach(), c0.detach()))
        out = self.fc(out[:, -1, :]) 
        return out

Создаем модель, задаем критерий и оптимизатор.

model = LSTM(input_dim=input_dim, hidden_dim=hidden_dim, output_dim=output_dim, num_layers=num_layers)
criterion = torch.nn.MSELoss(reduction='mean')
optimiser = torch.optim.Adam(model.parameters(), lr=0.01)

Наконец, мы обучаем модель более 100 эпох.

import time
hist = np.zeros(num_epochs)
start_time = time.time()
lstm = []
for t in range(num_epochs):
    y_train_pred = model(x_train)
    loss = criterion(y_train_pred, y_train_lstm)
    print("Epoch ", t, "MSE: ", loss.item())
    hist[t] = loss.item()
    optimiser.zero_grad()
    loss.backward()
    optimiser.step()
    
training_time = time.time()-start_time
print("Training time: {}".format(training_time))

Закончив обучение, мы можем применить прогноз.

Модель хорошо работает с обучающим набором, но плохо работает с тестовым набором. Модель, вероятно, переоснащена, особенно с учетом того, что после 40-й эпохи потери минимальны.

ГРУ

Код для реализации модели GRU очень похож.

class GRU(nn.Module):
    def __init__(self, input_dim, hidden_dim, num_layers, output_dim):
        super(GRU, self).__init__()
        self.hidden_dim = hidden_dim
        self.num_layers = num_layers
        
        self.gru = nn.GRU(input_dim, hidden_dim, num_layers, batch_first=True)
        self.fc = nn.Linear(hidden_dim, output_dim)

    def forward(self, x):
        h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_dim).requires_grad_()
        out, (hn) = self.gru(x, (h0.detach()))
        out = self.fc(out[:, -1, :]) 
        return out

Аналогичным образом создаем модель и настраиваем параметры.

model = GRU(input_dim=input_dim, hidden_dim=hidden_dim, output_dim=output_dim, num_layers=num_layers)
criterion = torch.nn.MSELoss(reduction='mean')
optimiser = torch.optim.Adam(model.parameters(), lr=0.01)

Шаг обучения точно такой же, и результаты, которых мы достигаем, тоже чем-то похожи.

Однако, когда дело доходит до прогноза, модель ГРУ явно более точна с точки зрения прогноза, как мы видим на следующем графике.

Заключение

Обе модели демонстрируют хорошие результаты на этапе обучения, но стагнируют около 40-й эпохи, что означает, что им не нужно заранее определять 100 эпох.

Как и ожидалось, нейронная сеть GRU превзошла LSTM с точки зрения точности, потому что она достигла более низкой среднеквадратичной ошибки (при обучении и, что наиболее важно, в тестовом наборе) и скорости, видно, что GRU потребовалось на 5 секунд меньше для завершения обучения, чем LSTM.

Вопросы, связанные с кодом - https://www.kaggle.com/rodsaldanha/stock-prediction-pytorch