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

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

В этом сообщении в блоге LSTM применяется к почасовым данным закрытия ETH-USD за несколько лет, полученным с Coinbase Data Marketplace; однако этот набор данных можно заменить любым набором данных о продукте по вашему выбору. Код для этого сообщения в блоге полностью написан на Python с использованием нескольких пакетов искусственного интеллекта/машинного обучения (AI/ML). Каждый раздел кода предоставляется и описывается в следующих разделах. Кроме того, весь приведенный ниже код доступен на общедоступном GitHub Coinbase Samples.

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

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

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

Помимо стандартного импорта обработки данных pandas и numpy, импортируется несколько сторонних пакетов для машинного обучения. sckit-learn, простая и эффективная библиотека прогнозного анализа, используется для масштабирования функций. Matplotlib используется для построения графиков для анализа модели. Наконец, для построения и обучения этой модели нейронной сети используется библиотека глубокого обучения Keras.

import pandas as pd
import numpy as np
import datetime as dt
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler
from keras.models import Sequential
from keras.callbacks import EarlyStopping
from keras.layers import Dense, Dropout
from keras.regularizers import l2
from keras.layers import LSTM

Эта следующая строка получает текущее время и форматирует его в строку. Позже это используется для названий сюжетов.

current_time = dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S')

Данные считываются из файла CSV, и все строки со значениями Not a Number (NaN) удаляются. Необработанный набор данных, используемый в этой модели, охватывает период с 1 января 2017 г. по 4 апреля 2023 г., однако для повышения производительности модели и учета динамики рынка первоначальные 25 000 значений были исключены. Эта корректировка выравнивает начальную точку модели по 8 ноября 2019 года. При тестировании включение полного периода времени привело к более плохим прогностическим результатам. В зависимости от длины вашего набора данных, это исключение данных, вероятно, потребует тестирования и настройки.

Создается новый столбец с именем ‘date’, созданный путем преобразования столбца меток времени данных (предоставленного во времени Unix) в дату и время, которые будут использоваться позже при создании графиков. Наконец, индекс DataFrame сбрасывается.

df = pd.read_csv('eth_ohlcv.csv')  
df.dropna(inplace=True)
print(len(df))
df = df.iloc[25000:]  
df['date'] = pd.to_datetime(df['timestamp'], unit='us')  df.reset_index(drop=True, inplace=True)
df.reset_index(drop=True, inplace=True)

В этой модели основное внимание уделяется прогнозированию почасовой цены закрытия Ethereum, поэтому столбец ‘close’ DataFrame изолирован в новом DataFrame. Однако при необходимости другие столбцы, такие как ‘buyVolume’, ‘sellVolume’ и остальные компоненты OHLCV, могут быть включены в качестве дополнительных функций, если производительность модели, основанная исключительно на цене закрытия, недостаточна.

close_data = df[['close']]
print(close_data.shape)

Затем новый DataFrame разбивается на обучающий набор (первые 75% данных) и тестовый набор (оставшиеся 25%). Очень важно разделить этот набор данных на обучающий набор и тестовый набор, чтобы убедиться, что модель протестирована на невидимых данных. Это сводит к минимуму переобучение, которое происходит, когда модель изучает сложности и шум обучающих данных до такой степени, что это негативно влияет на обобщение модели на новые, невидимые данные. Обратите внимание, что при тестировании было установлено, что 75% — это хороший размер для обучающей выборки, однако эти проценты являются гибкими.

train_size = int(len(close_data) * 0.75)
train_data = close_data[:train_size]
test_data = close_data[train_size:]
print(train_data.shape)
print(test_data.shape)

Затем данные преобразуются в массивы numpy, а затем нормализуются до значений от 0 до 1 с использованием MinMaxScaler. Этот масштабатор имеет решающее значение в моделях LSTM, поскольку он гарантирует, что все входные функции находятся в одинаковом масштабе, что может помочь модели быстрее сходиться во время обучения. Это масштабирование не позволяет функциям с большими значениями доминировать в процессе обучения и гарантирует, что все функции вносят пропорциональный вклад в выходные данные модели.

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

Функция fit_transform() применяется к train_data для вычисления минимального и максимального значений обучающих данных, а затем используется для масштабирования обучающих данных. Далее функция transform() используется на test_data. Важно отметить, что он применяет те же параметры масштабирования, которые были рассчитаны на основе обучающих данных. Другими словами, он использует те же минимальные и максимальные значения, которые ранее были рассчитаны из train_data для масштабирования test_data.

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

train_data = np.array(train_data)
test_data = np.array(test_data)

scaler = MinMaxScaler(feature_range=(0, 1))
train_data_scaled = scaler.fit_transform(train_data)
test_data_scaled = scaler.transform(test_data)
print('train_data: ', train_data_scaled.shape)
print('test_data: ', test_data_scaled.shape)

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

Функция create_dataset принимает набор данных и параметр time_step. Этот параметр определяет длину входной последовательности для модели LSTM — в данном случае для него установлено значение 168 или одна неделя почасовых данных, что означает, что значения предыдущих 168 временных шагов используются в качестве входных функций, а следующие 168 временных шагов являются мишенями.

Функция инициализирует два пустых списка, dataX и dataY. Цикл for перебирает набор данных, создавая последовательность из предыдущего time_step количества точек данных (a) и следующего количества точек данных time_step (b). Затем к dataX добавляется a, а к dataY — b. Этот процесс повторяется для всей длины набора данных, создавая набор последовательностей, которые будут использоваться для обучения модели LSTM.

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

LSTM ожидает, что данные будут в определенном формате, как правило, в виде трехмерного массива. При использовании Keras входная форма должна быть в форме [samples, time steps, features]. Количество выборок — это длина набора данных, временные шаги — это количество зависящих от времени шагов, представленных в одной точке данных, а функции — это количество функций модели. Необходимо изменить форму X_train и X_test, чтобы они имели три измерения, при этом операция изменения формы не меняет данные, а только структуру данных.

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

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

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

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

def reshape_data(X):
    return X.reshape(X.shape[0], X.shape[1], 1)

def generate_dataset(dataset, time_step):
    X, y = create_dataset(dataset, time_step)
    X = reshape_data(X)
    return X, y

def create_dataset(dataset, time_step):
    dataX, dataY = [], []
    for i in range(len(dataset)-2*time_step-1):
        a = dataset[i:(i+time_step), 0]
        dataX.append(a)
        b = dataset[(i+time_step):(i+2*time_step), 0]
        dataY.append(b)
    return np.array(dataX), np.array(dataY)

time_step = 168 
neurons = 50  
dropout_rate = 0.2 

X_train, y_train = generate_dataset(train_data_scaled, time_step)
X_test, y_test = generate_dataset(test_data_scaled, time_step)

print('X_train: ', X_train.shape)
print('X_test: ', X_test.shape)

II. Обучение модели

Модель построена с использованием Sequential API от Keras. Слой LSTM добавляется в качестве первого слоя с отсевом и регуляризацией L2 для предотвращения переобучения. Последний слой представляет собой плотный слой с тем же количеством нейронов, что и значение time_step. При компиляции модели используется оптимизатор Adam, известный своей эффективностью в моделях глубокого обучения. Каждое из этих решений объясняется ниже.

Регулирование L2 препятствует изучению сложных или чрезмерно специфичных моделей, чтобы предотвратить переоснащение. Он делает это, добавляя штраф к функции потерь, пропорциональный квадрату величины весов (таким образом, «2» в L2).

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

Адам, аббревиатура от Adaptive Moment Estimation, представляет собой алгоритм оптимизации, который регулирует скорость обучения для каждого веса в модели индивидуально. Это позволяет модели более эффективно перемещаться по весовому пространству.

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

model = Sequential() kernel_regularizer=l2(0.01)))
model.add(LSTM(neurons, input_shape=(None, 1), activation='tanh',
model.add(Dropout(dropout_rate))
model.add(Dense(time_step))
model.compile(loss='mean_absolute_error', optimizer='adam')

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

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

early_stopping = EarlyStopping(
    monitor='val_loss', patience=10, restore_best_weights=True)

history = model.fit(X_train, y_train, validation_data=(X_test, y_test),
                    epochs=100, batch_size=10, verbose=1, callbacks=[early_stopping])

model.save('trained_eth_hourly_lstm.keras')

# from keras.models import load_model
# model = load_model('trained_eth_hourly_lstm.keras)

Затем потери при обучении и проверке отображаются с течением времени, что дает четкое визуальное представление о том, как модель работала во время обучения.

loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(len(loss))
plt.plot(epochs, loss, 'r', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss {} / {} / {}  (Time: {})'.format(time_step,neurons,dropout_rate, current_time))
plt.legend(loc=0)
plt.show()

Результаты расчета функции потерь показаны выше. И потери при обучении, и при проверке дают представление об обучении модели.

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

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

Как упоминалось ранее, используется ранняя остановка, и обучение автоматически завершается через 33 эпохи. Это указывает на то, что производительность модели на данных проверки существенно не улучшилась после этой точки, поэтому дальнейшее обучение, вероятно, не приведет к существенным улучшениям.

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

III. Выводы и прогнозы

По завершении обучения акцент смещается на создание выводов с использованием обученной модели или на использование модели для составления прогнозов на основе невидимых данных. Эта модель используется для выводов как по обучающим данным (X_train), так и по тестовым данным (X_test) путем вызова метода model.predict() с наборами обучающих и тестовых данных в качестве входных данных.

Модель генерирует эти выводы в том же преобразованном пространстве, в котором проводилось обучение, что делает результаты несопоставимыми напрямую с исходными данными. Чтобы преодолеть это, к полученным результатам применяется обратное преобразование с использованием функции scaler.inverse_transform(). Эта операция преобразует предполагаемые значения обратно в масштаб исходных данных, что делает их непосредственно сопоставимыми с фактическими значениями.

После генерации и обратного преобразования выводов крайне важно оценить производительность модели. Тестовые потери рассчитываются с использованием метода model.evaluate(), который сравнивает предполагаемые результаты модели для тестовых данных (test_predict) с фактическими значениями (y_test).

train_predict = model.predict(X_train)
test_predict = model.predict(X_test)

train_predict = scaler.inverse_transform(train_predict)
test_predict = scaler.inverse_transform(test_predict)

test_loss = model.evaluate(X_test, y_test.reshape(y_test.shape[0], y_test.shape[1]))
print('Test Loss:', test_loss)

Вычисленная тестовая потеря, сгенерированная этой моделью, составляет 0,01823, что предполагает довольно низкую ошибку, указывающую на то, что прогнозы модели немного отклоняются в масштабированном диапазоне от 0 до 1.

Визуализация этих выводов вместе с фактическими данными обеспечивает более интуитивное понимание производительности модели.

Создается массив, заполненный значениями NaN, отражающий форму исходных данных (close_data). Затем значения NaN в позициях, соответствующих выводам модели, заменяются фактическими предполагаемыми значениями.

Этот процесс повторяется как для вывода обучающих, так и для тестовых данных, в результате чего получаются два массива: trainPredictPlot и testPredictPlot. Эти массивы вместе с исходными близкими значениями (close_data) затем наносятся на один и тот же график, чтобы предложить визуальное сравнение между выводами модели и фактическими значениями. Это визуальное представление позволяет оценить, насколько близко выводы модели совпадают с истинным трендом временного ряда.

trainPredictPlot = np.empty_like(close_data)
trainPredictPlot[:, :] = np.nan
trainPredictPlot[time_step:len(train_predict)+time_step, :] = train_predict[:,-1].reshape(-1,1)

testPredictPlot = np.full_like(close_data, np.nan)
testPredictPlot[len(train_data) + time_step : len(train_data) + time_step + len(test_predict), :] = test_predict[:,-1].reshape(-1,1)

current_time = dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S')

train_date = df['date'].iloc[time_step : time_step+len(train_predict)]
test_date = df['date'].iloc[len(train_predict) + 2*time_step + time_step : len(train_predict) + 2*time_step + time_step + len(test_predict)]

plt.figure(figsize=(12, 6))
plt.plot(df['date'], close_data, label='Original Close')
plt.plot(train_date, train_predict[:,-1], label='Training Predictions')
plt.plot(test_date, test_predict[:,-1], label='Test Predictions')
plt.xlabel('Time')
plt.ylabel('Close Value')
plt.title('Close Values vs. Predictions {} / {} / {}  (Time: {})'.format(time_step,neurons,dropout_rate, current_time))
plt.legend()
plt.show()

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

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

Чтобы визуализировать эти прогнозы на будущее, они наносятся на соответствующие будущие часы. Добавлен код для правильного построения этих прогнозов по точному часу, с которым они совпадают, учитывая, что эти временные метки не были предоставлены исходным набором данных. График обеспечивает визуальное представление цен, предсказанных моделью LSTM, на следующие time_step часов, предлагая потенциальную информацию о будущих тенденциях.

last_data = test_data_scaled[-time_step:]
last_data = last_data.reshape(1, time_step, 1)

predictions = model.predict(last_data)
predictions = predictions.reshape(-1, 1)
predictions = scaler.inverse_transform(predictions)

freq = 'H'
last_date = df['date'].iloc[-1]
future_dates = pd.date_range(start=last_date, periods=time_step+1, freq=freq)[1:]

plt.figure(figsize=(12, 6))
plt.plot(future_dates, predictions, label='Predictions')
plt.xlabel('Date')
plt.ylabel('Price')
plt.title('Future Price Predictions {} / {} / {}  (Time: {})'.format(time_step, neurons, dropout_rate, current_time))
plt.legend()
plt.show()

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

Coinbase’s Data Marketplace является образцовым источником таких надежных и чистых данных, что делает его идеальной отправной точкой для создания превосходных моделей машинного обучения. Более сложные типы данных, доступные на рынке, могут еще больше расширить эту модель. Например, использование Coinbase торговых данных на уровне тиков может обеспечить более детальное понимание при обучении на этих более крупных и сложных данных.

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

Отказ от ответственности: информация предоставляется только в информационных целях и не является инвестиционным советом. Это не рекомендация покупать или продавать цифровые активы или использовать определенную инвестиционную стратегию, коды или API. Coinbase не делает никаких заявлений о точности, пригодности или достоверности любой информации, представленной здесь.