Идея использования нейронной сети (NN) для прогнозирования движения цены акций на рынке стара, как нейронные сети. Интуитивно кажется трудным предсказать будущее движение цены, глядя только на ее прошлое. Существует множество руководств о том, как предсказать ценовой тренд или его силу, что упрощает задачу. Я решил попытаться предсказать средневзвешенную цену с помощью LSTM, потому что это кажется сложным и интересным.

В этом сообщении в блоге я собираюсь обучить нейронную сеть с долгосрочной краткосрочной памятью (LSTM) с PyTorch на торговых данных биткойнов и использовать их для прогнозирования цены невидимых торговых данных. У меня были некоторые трудности с поиском промежуточных руководств с повторяющимся примером обучения LSTM для прогнозирования временных рядов, поэтому я собрал Блокнот Jupyter, чтобы помочь вам начать работу.

Вот несколько ссылок, которые могут вас заинтересовать:

- Complete your Python analyses 10x faster with Mito [Product]
- Free skill tests for Data Scientists & ML Engineers [Test]
- All New Self-Driving Car Engineer Nanodegree [Course]

Хотели бы вы прочитать больше таких статей? Если да, вы можете поддержать меня, нажав на любую ссылку выше. Некоторые из них являются партнерскими ссылками, но вам не нужно ничего покупать.

Загрузка необходимых зависимостей

Давайте импортируем библиотеки, которые мы собираемся использовать для обработки данных, визуализации, обучения модели и т. Д. Мы собираемся обучить LSTM с помощью библиотеки PyTorch.

%matplotlib inline
import glob
import matplotlib
import numpy as np
import pandas as pd
import sklearn
import torch

Загрузка данных

Мы собираемся проанализировать торговые данные XBTUSD от BitMex. Ежедневные файлы общедоступны для скачивания. Я не стал писать код для автоматической загрузки данных, я просто щелкнул пару раз, чтобы загрузить файлы.

Давайте составим список всех файлов, прочитаем их в DataFrame pandas и отфильтруем торговые данные по символу XBTUSD. Важно отсортировать DataFrame по метке времени, поскольку существует несколько ежедневных файлов, чтобы они не перепутались.

files = sorted(glob.glob('data/*.csv.gz'))
df = pd.concat(map(pd.read_csv, files))
df = df[df.symbol == 'XBTUSD']
df.timestamp = pd.to_datetime(df.timestamp.str.replace('D', 'T')) # covert to timestamp type
df = df.sort_values('timestamp')
df.set_index('timestamp', inplace=True) # set index to timestamp
df.head()

Каждая строка представляет сделку:

  • отметка времени с точностью до микросекунд,
  • символ торгуемого контракта,
  • сторона сделки, покупка или продажа,
  • размер представляет собой количество контрактов (количество торгуемых долларов США),
  • цена контракта,
  • tickDirection описывает увеличение / уменьшение цены с момента предыдущей транзакции,
  • trdMatchID - уникальный идентификатор сделки,
  • GrossValue - это количество обмененных сатоши,
  • homeNotional - это количество XBT в сделке,
  • foreignNotional - это сумма сделки в долларах США.

Мы собираемся использовать 3 столбца: timestamp, price и foreignNotional.

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

Рассчитаем Средневзвешенную цену по объему (VWAP) с интервалом в 1 минуту. Представление данных, в котором мы группируем сделки по заранее заданному временному интервалу, называется временными барами. Это лучший способ представления торговых данных для моделирования? По словам Лопеса де Прадо, сделки на рынке не распределяются равномерно во времени. Бывают периоды с высокой активностью, например. прямо перед истечением будущих контрактов, и группировка данных в заранее определенные интервалы времени приведет к избыточной выборке данных в одних временных барах и заниженной выборке в других. Финансовое машинное обучение, часть 0: планки - это красивое резюме 2-й главы книги Лопеса де Прадо Достижения в области финансового машинного обучения. Индикаторы времени могут быть не лучшим представлением данных, но мы собираемся их использовать в любом случае.

df_vwap = df.groupby(pd.Grouper(freq="1Min")).apply(
    lambda row: pd.np.sum(row.price * row.foreignNotional) / pd.np.sum(row.foreignNotional))

На графике показаны временные шкалы с VWAP с 1 августа по 17 сентября 2019 г. Мы собираемся использовать первую часть данных для обучающего набора, промежуточную часть для набора для проверки и последнюю часть данных для набор тестов (вертикальные линии - разделители). Мы можем наблюдать волатильность в VWAP, где цена достигает своих максимумов в первой половине августа и минимумов в конце августа. Высокие и низкие значения фиксируются в обучающем наборе, что важно, поскольку модель, скорее всего, не будет хорошо работать на невидимых интервалах VWAP.

Масштабирование данных

Чтобы модель LSTM могла быстрее сходиться, важно масштабировать данные. Возможно, что большие значения во входных данных замедляют обучение. Мы собираемся использовать StandardScaler из библиотеки sklearn для масштабирования данных. Масштабатор подходит для обучающего набора и используется для преобразования невидимых торговых данных при проверке и наборе тестов. Если бы мы поместили скаляр на все данные, модель была бы избыточной и достигла бы хороших результатов на этих данных, но производительность на реальных данных пострадала бы.

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
train_arr = scaler.fit_transform(df_train)
val_arr = scaler.transform(df_val)
test_arr = scaler.transform(df_test)

Преобразование данных

После масштабирования нам нужно преобразовать данные в формат, подходящий для моделирования с помощью LSTM. Мы преобразуем длинную последовательность данных во множество более коротких последовательностей (по 100 временных полос на последовательность), которые сдвигаются на одну временную шкалу.

from torch.autograd import Variable
def transform_data(arr, seq_len):
    x, y = [], []
    for i in range(len(arr) - seq_len):
        x_i = arr[i : i + seq_len]
        y_i = arr[i + 1 : i + seq_len + 1]
        x.append(x_i)
        y.append(y_i)
    x_arr = np.array(x).reshape(-1, seq_len)
    y_arr = np.array(y).reshape(-1, seq_len)
    x_var = Variable(torch.from_numpy(x_arr).float())
    y_var = Variable(torch.from_numpy(y_arr).float())
    return x_var, y_var
seq_len = 100
x_train, y_train = transform_data(train_arr, seq_len)
x_val, y_val = transform_data(val_arr, seq_len)
x_test, y_test = transform_data(test_arr, seq_len)

График ниже показывает первую и вторую последовательность в обучающей выборке. Длина обеих последовательностей составляет 100 временных баров. Мы можем заметить, что цель обеих последовательностей почти такая же, как и функция, различия в первом и последнем временном баре.

Как LSTM использует последовательность на этапе обучения?

Давайте сосредоточимся на первой последовательности. Модель использует функцию шкалы времени с индексом 0 и пытается предсказать цель шкалы времени с индексом 1. Затем она использует функцию шкалы времени с индексом 1 и пытается предсказать цель шкалы времени. при индексе 2 и т. д. Признак 2-й последовательности сдвигается на одноразовую полосу из признака 1-й последовательности, признак 3-й последовательности сдвигается на одноразовую полосу из 2-й последовательности и т. д. С помощью этой процедуры, мы получаем много более коротких последовательностей, которые сдвинуты на одну шкалу времени.

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

Нейронная сеть с долговременной краткосрочной памятью

Нейронная сеть с долговременной краткосрочной памятью - это разновидность рекуррентной нейронной сети (RNN). RNN используют предыдущие временные события для информирования более поздних. Например, чтобы классифицировать, какое событие происходит в фильме, модели необходимо использовать информацию о предыдущих событиях. RNN работают хорошо, если проблема требует только свежей информации для выполнения текущей задачи. Если проблема требует долгосрочных зависимостей, RNN будет изо всех сил пытаться ее смоделировать. LSTM был разработан для изучения долгосрочных зависимостей. Он запоминает информацию надолго. LSTM был представлен S Hochreiter, J Schmidhuber в 1997 году. Чтобы узнать больше о LSTM, прочтите отличный пост в блоге о колах, который предлагает хорошее объяснение.

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

import torch.nn as nn
import torch.optim as optim

class Model(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(Model, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.lstm = nn.LSTMCell(self.input_size, self.hidden_size)
        self.linear = nn.Linear(self.hidden_size, self.output_size)
    def forward(self, input, future=0, y=None):
        outputs = []
        # reset the state of LSTM
        # the state is kept till the end of the sequence
        h_t = torch.zeros(input.size(0), self.hidden_size, dtype=torch.float32)
        c_t = torch.zeros(input.size(0), self.hidden_size, dtype=torch.float32)
        for i, input_t in enumerate(input.chunk(input.size(1), dim=1)):
            h_t, c_t = self.lstm(input_t, (h_t, c_t))
            output = self.linear(h_t)
            outputs += [output]
        for i in range(future):
            if y is not None and random.random() > 0.5:
                output = y[:, [i]]  # teacher forcing
            h_t, c_t = self.lstm(output, (h_t, c_t))
            output = self.linear(h_t)
            outputs += [output]
        outputs = torch.stack(outputs, 1).squeeze(2)
        return outputs

Обучение LSTM

Мы обучаем LSTM с 21 скрытым юнитом. Используется меньшее количество единиц, чтобы маловероятно, что LSTM точно запомнит последовательность. Мы используем функцию потерь среднеквадратической ошибки и оптимизатор Адама. Скорость обучения установлена ​​на 0,001 и уменьшается каждые 5 эпох. Мы обучаем модель со 100 последовательностями в партии для 15 эпох. Из приведенного ниже графика мы можем наблюдать, что потери при обучении и проверке сходятся после шестой эпохи.

Оценим модель на тестовом наборе. Для параметра future установлено значение 5, что означает, что модель выводит VWAP там, где, по ее мнению, он будет в следующие 5 временных полос (5 ​​минут в нашем примере). Это должно сделать изменение цены видимым за несколько временных баров до того, как оно произойдет.

На графике ниже мы видим, что прогнозируемые значения близко соответствуют фактическим значениям VWAP, что на первый взгляд кажется отличным. Но для параметра future было установлено значение 5, что означает, что оранжевая линия должна реагировать до того, как произойдет всплеск, вместо того, чтобы закрывать его.

Когда мы увеличиваем масштаб пиков (один в начале, а другой в конце временного ряда). Мы можем заметить, что прогнозируемые значения имитируют фактические значения. Когда фактическое значение меняет направление, следует прогнозируемое значение, что нам не очень помогает. То же самое происходит, когда мы увеличиваем будущий параметр (как будто он не влияет на прогнозируемую линию).

Давайте сгенерируем 1000 временных полос для первой тестовой последовательности с моделью и сравним прогнозируемый, сгенерированный и фактический VWAP. Мы можем заметить, что, хотя модель выводит прогнозируемые значения, они близки к фактическим значениям. Но когда он начинает генерировать значения, выходной сигнал почти напоминает синусоидальную волну. По истечении определенного периода значения сходятся к 9600.

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

Учитель заставляет

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

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

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

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

Давайте сгенерируем 1000 временных полос для первой тестовой последовательности с помощью модели, обученной с помощью учителя.

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

Заключение

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

использованная литература

Прежде чем ты уйдешь

Следуйте за мной в Twitter, где я регулярно пишу твиты о Data Science и машинном обучении.