English 中文 Español Deutsch 日本語 Português
preview
Машинное обучение и Data Science (Часть 22): Автоэнкодеры для устранения шума и выявления сигналов в трейдинге

Машинное обучение и Data Science (Часть 22): Автоэнкодеры для устранения шума и выявления сигналов в трейдинге

MetaTrader 5Индикаторы |
948 0
Omega J Msigwa
Omega J Msigwa

Что такое Автоэнкодер?

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

Допустим, вы передали в автоэнкодер размытое изображение кошки. Это изображение будет сжато и распаковано обратно в исходное состояние, при этом будут потеряны некоторые из его шумных/размытых пикселей, и в итоге получится четкое изображение кошки.

Размытое и неразмытое изображение кошки

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

Эту статью будет понять проще, если у вас уже есть базовые знания об ONNX, PCA и нейронных сетях.

Автоэнкодер состоит из двух частей:

  1. Энкодер принимает входные данные и сжимает их в скрытое представление меньшей размерности, фиксируя основные характеристики.
  2. Декодер получает скрытое представление и пытается восстановить исходные входные данные как можно точнее.

Преимущества автоэнкодеров:

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


Из чего они сделаны?

Давайте разберем автоэнкодеры и посмотрим, из чего они состоят и что делает их особенными.

В основе автоэнкодера лежит искусственная нейронная сеть, состоящая из трех частей.

  1. Энкодер
  2. Вектор эмбеддинга/скрытый слой
  3. Декодер

Архитектура простого автоэнкодера

Левая часть нейронной сети называется энкодером. Его задача — преобразовать исходные входные данные в представление меньшей размерности.

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

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

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

Фотография дома и дом

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

Ошибка построения — разница между попыткой воссоздания и исходными входными данными.

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

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

Python:

class Autoencoder(Model):
  def __init__(self, input_dim, latent_dim, hidden_dims=[]):
    super(Autoencoder, self).__init__()

    self.encoder = tf.keras.Sequential()
    # Add hidden layers to the encoder (if any)
    for dim in hidden_dims:
      self.encoder.add(layers.Dense(dim, activation='relu'))
      self.encoder.add(layers.Dropout(0.5))

    # Define the latent layer
    self.encoder.add(layers.Dense(latent_dim, activation='relu'))

    # Decoder ( mirrored structure )
    self.decoder = tf.keras.Sequential()
    # Add hidden layers to the decoder (in reverse order)
    for dim in hidden_dims[::-1]:
      self.decoder.add(layers.Dense(dim, activation='relu'))
      self.decoder.add(layers.Dropout(0.5))

    # Define the output layer
    self.decoder.add(layers.Dense(input_dim, activation='sigmoid'))  #the output layer with dimensions matching the original input data

  def call(self, x):
    encoded = self.encoder(x)
    decoded = self.decoder(encoded)
    return decoded

Вызов класса Autoencoder:

Python:

input_dim = dataset.shape[1]  # number of columns in the data
latent_dim = 5  # Dimension of latent layer
hidden_dims = [12, 10]

autoencoder = Autoencoder(input_dim, latent_dim, hidden_dims)

Ниже представлена архитектура автоэнкодера:

Архитектура автоэнкодера 12,10

В классе автоэнкодера Autoencoder мы используем RELU (Rectified Linear Unit) и в энкодере, и в декодере. Эта функция активации широко используется в большинстве автоэнкодеров, с которыми вам придется столкнуться, и на это есть важная причина.

RELU эффективен с точки зрения вычислений, борется с проблемой затухания градиентов и может изучать разреженные представления, которые часто встречаются в торговых данных. Также при работе с финансовыми данными могут быть полезны и другие варианты RELU, такие как GELU и Leaky RELU.

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

Сигмоида:

  • Плюсы: часто используется для реконструкции изображений, когда выходные данные должны находиться в диапазоне от 0 до 1 (соответствует интенсивности пикселей).
  • Минусы: может не подходит для работы с финансовыми данными, поскольку может привести к затуханию градиентов во время обратного распространения, особенно в глубоких архитектурах.
    При использовании автоэнкодера с сигмоидной функцией сеть не смогла сойтись, поскольку продолжала колебаться вблизи локальных минимумов:
    Epoch 1/50
    110/110 ━━━━━━━━━━━━━━━━━━━━ 3s 5ms/step - loss: 0.4001 - val_loss: 0.3753
    Epoch 2/50
    110/110 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 0.3733 - val_loss: 0.3745
    Epoch 3/50
    110/110 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 0.3724 - val_loss: 0.3746
    Epoch 4/50
    110/110 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 0.3758 - val_loss: 0.3746
    Epoch 5/50
    110/110 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 0.3692 - val_loss: 0.3745
    Epoch 6/50
    110/110 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 0.3747 - val_loss: 0.3746
    Epoch 7/50
    110/110 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 0.3716 - val_loss: 0.3746
    Epoch 8/50
    110/110 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 0.3740 - val_loss: 0.3745
    Epoch 9/50
    110/110 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 0.3698 - val_loss: 0.3745
    Epoch 10/50
    110/110 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 0.3713 - val_loss: 0.3745
    Epoch 11/50
    110/110 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 0.3726 - val_loss: 0.3745
    Epoch 12/50
    110/110 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 0.3739 - val_loss: 0.3745
    Epoch 13/50
    110/110 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 0.3725 - val_loss: 0.3746
    Epoch 14/50
    110/110 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 0.3749 - val_loss: 0.3746

Tanh (гиперболический тангенс):

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

Эти функции активации Sigmoid и TANH, а также другие подобные функции работают лучше всего при использовании в выходном слое декодера для максимально точной реконструкции входных данных. В этом контексте выходные данные автоэнкодера должны напоминать исходные входные данные. Поскольку входные данные часто нормализуются до диапазона [0, 1] или [-1, 1] в зависимости от предварительной обработки, для масштабирования выходных значений до этого диапазона обычно используется сигмоидальная функция активации.

Python:

# Define the output layer
self.decoder.add(layers.Dense(input_dim, activation='sigmoid'))  #the output layer of the decoder with dimensions matching the original input data


Польза от Min-Max Scaler

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

Мы используем функцию активации RELU, которая возвращает значение нуля, если ей присвоено значение, меньшее или равное нулю, или же заданное значение: (x = 0 when x<=0 else x = x).

Если использовать Standard-Scaler для масштабирования, нужно помнить, что он центрирует данные, вычитая среднее значение, и масштабирует их до единичной дисперсии. Это может привести к смещению выбросов с большими положительными значениями в сторону очень отрицательных значений (потенциально -1) во время стандартизации. Если стандартизированное значение выброса становится равным -1, то при передаче этого отрицательного значения активация RELU в энкодере всегда будет выводить 0 для этого конкретного признака.

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

 

Автоэнкодер с StandardScaler

Как это исправить:

Можно использовать другие методы нормализации, такие как масштабирование Min-Max, при котором данные масштабируются до определенного диапазона от 0 до 1. То есть мы потенциально предотвращаем появление значений -1, которые и вызывают проблемы с RELU. Однако, и у Min-Max Scaler есть ограничения. Можно также попробовать Robust scaler, который менее чувствителен к выбросам, чем Standard Scaler, и может обеспечить лучшее масштабирование для функции активации RELU.

Также можно попробовать Leaky RELU (leaky_relu = 0,01x для x <= 0, relu = x для x > 0) вместо стандартного RELU. Leaky RELU допускает небольшой ненулевой градиент даже для отрицательных входных данных, смягчая проблему умирающего RELU.


Обучение автоэнкодера

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

Python:

import sklearn
from sklearn.model_selection import train_test_split
from keras import optimizers
from keras.callbacks import EarlyStopping

x_train, x_test = train_test_split(dataset, test_size=0.3, random_state=42) #train test the data

# Normalizing the input data 

scaler = sklearn.preprocessing.MinMaxScaler()
x_train = scaler.fit_transform(x_train)
x_test = scaler.transform(x_test)

print(f"x_train {x_train.shape}.dtype({x_train.dtype}) x_test {x_test.shape}.dtype({x_test.dtype})")

# compile the autoencoder

input_dim = dataset.shape[1]
latent_dim = 32  # Dimension of latent space
hidden_dims = [256, 128, 64]

autoencoder = Autoencoder(input_dim, latent_dim, hidden_dims)

optimizer = optimizers.Adam(learning_rate=1e-5)
autoencoder.compile(optimizer=optimizer, loss=losses.MeanSquaredError())

early_stopping = EarlyStopping(monitor='val_loss', patience = 5, restore_best_weights=True) //stop the training process if 5 epochs have no change in loss
history = autoencoder.fit(x_train, x_train, epochs=50, shuffle=True, callbacks=[early_stopping], validation_data=(x_test, x_test), batch_size=64, verbose=1)

Я решил использовать сложную архитектуру нейронной сети [256, 128, 64] для энкодера, а для декодера будет применена обратная схема [64,128, 256], при этом в скрытом слое будет 32 нейрона.

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

Результат

x_train (7000, 4).dtype(float64) x_test (3000, 4).dtype(float64)
Epoch 1/50
110/110 ━━━━━━━━━━━━━━━━━━━━ 3s 5ms/step - loss: 0.0669 - val_loss: 0.0636
Epoch 2/50
110/110 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 0.0648 - val_loss: 0.0608
Epoch 3/50
110/110 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 0.0624 - val_loss: 0.0550

....
....
....

Epoch 46/50
110/110 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 1.2096e-04 - val_loss: 1.0195e-04
Epoch 47/50
110/110 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 1.0758e-04 - val_loss: 9.7759e-05
Epoch 48/50
110/110 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 1.0923e-04 - val_loss: 9.4798e-05
Epoch 49/50
110/110 ━━━━━━━━━━━━━━━━━━━━ 1s 5ms/step - loss: 1.0243e-04 - val_loss: 9.0442e-05
Epoch 50/50
110/110 ━━━━━━━━━━━━━━━━━━━━ 1s 4ms/step - loss: 1.0222e-04 - val_loss: 8.7384e-05

График потерь и итераций:

График потерь и итераций

Передадим данные в автоэнкодер и посмотрим на результат:

Python:

original_norm_data = scaler.transform(dataset)

new_data = autoencoder.call(original_norm_data)

new_data = scaler.inverse_transform(new_data) #return data to the original form 

print("original data\n",dataset,"\nnew data\n",new_data)

Результат

Исходные данные
 [[1.06507 1.06633 1.06497 1.06538]
 [1.06628 1.06685 1.06463 1.06508]
 [1.06771 1.06797 1.06599 1.06627]
 ...
 [0.99941 0.99996 0.9991  0.99916]
 [0.99687 0.99999 0.99646 0.99941]
 [0.99536 0.99724 0.99444 0.99687]] 
new data
 [[1.06612682 1.06676685 1.06537819 1.06605109]
 [1.06617137 1.06679912 1.06541834 1.06609218]
 [1.06742607 1.06804771 1.06668032 1.06736937]
 ...
 [0.99906356 1.00121275 0.9980908  0.99980352]
 [0.998204   1.00034005 0.9972261  0.99893805]
 [0.99581326 0.99789913 0.99494114 0.99651365]]

Я решил визуализировать цены закрытия:

Цены закрытия и цены от автоэнкодера

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


Применение автоэнкодеров

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

Уменьшение размерности

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

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

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

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

Python:

class Autoencoder(Model):
  def __init__(self, input_dim, latent_dim, hidden_dims=[]):
    super(Autoencoder, self).__init__()
    self.hidden_dims = hidden_dims
    self.input_dim = input_dim
    
    # Encoder
    self.encoder = tf.keras.Sequential(name='encoder') #give the encoder Sequential layer name=encoder
    # Decoder ( mirrored structure )
    self.decoder = tf.keras.Sequential(name='decoder') #give the decoder Sequential layer name=decoder
    
  def build(self):

    # Add hidden layers to the encoder (if any)
    for dim in hidden_dims:
      self.encoder.add(layers.Dense(dim, activation='relu'))
      self.encoder.add(layers.Dropout(0.5))

    # Define the latent layer
    self.encoder.add(layers.Dense(latent_dim, activation='relu'))
        
    # Add hidden layers to the decoder (in reverse order)
    for dim in hidden_dims[::-1]:
      self.decoder.add(layers.Dense(dim, activation='relu'))
      self.decoder.add(layers.Dropout(0.5))

    # Define the output layer
    self.decoder.add(layers.Dense(self.input_dim, activation='sigmoid'))  #the output layer with dimensions matching the original input data

  def call(self, x):
    encoded = self.encoder(x)
    decoded = self.decoder(encoded)
    return decoded

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

Python:

# Instantiate the autoencoder and build the model
autoencoder = Autoencoder(input_dim, latent_dim, hidden_dims)
autoencoder.build()

optimizer = optimizers.Adam(learning_rate=1e-5)
autoencoder.compile(optimizer=optimizer, loss=losses.MeanSquaredError())

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

Python:

# Extract Encoder
encoder_input = autoencoder.encoder.layers[0].input
encoder_output = autoencoder.encoder.get_layer(index=-1).output # the layer at index -1 is the last layer

# Define the encoder model
encoder_model = tf.keras.Model(inputs=encoder_input, outputs=encoder_output)

# Extract Decoder
decoder_input = autoencoder.decoder.layers[0].input
decoder_output = autoencoder.decoder.get_layer(index=-1).output # the layer at index -1 is the last layer

# Define the decoder model
decoder_model = tf.keras.Model(inputs=decoder_input, outputs=decoder_output)

Итак, у нас есть энкодер. Теперь можно передать информацию и получить итоговую матрицу, прошедшую через скрытый слой (пространство).

Python:

from sklearn.decomposition import PCA

# Fit & transform the encoded data 
encoded_data = encoder_model.predict(original_norm_data)
print("decoded data.shape: ",encoded_data.shape)

# Create PCA object
pca = PCA(n_components=encoded_data.shape[1])

reduced_data = pca.fit_transform(encoded_data)
print("pca reduced data.shape: ",reduced_data.shape)

print("explained var:\n",np.cumsum(pca.explained_variance_ratio_))

# Plotting the scree plot
plt.figure(figsize=(10, 6))
plt.plot(np.cumsum(pca.explained_variance_ratio_))
plt.xlabel('Number of Components')
plt.ylabel('Cumulative Explained Variance')
plt.title('Scree Plot')
plt.grid(True)
plt.show()

Назначая количество столбцов encoded_data.shape[1] для компонентов PCA мы можем измерить дисперсию, объясняемую каждым признаком, и построить график осыпи (Scree Plot), который может помочь найти наилучшее количество компонентов, которые нужно применить к PCA для сокращения размерности данных.

313/313 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step
decoded data.shape:  (10000, 32)
pca reduced data.shape:  (10000, 32)
explained var:
 [0.99623495 0.9989214  0.99982804 0.9999363  0.99996614 0.9999872
 0.99999297 0.9999953  0.9999972  0.9999982  0.9999987  0.9999991
 0.9999994  0.9999996  0.9999997  0.9999998  0.99999994 1.
 1.         1.         1.         1.         1.         1.
 1.         1.         1.         1.         1.         1.
 1.         1.        ]

График осыпи PCA

На кумулятивной объясненной дисперсии (Cumulative Explained Variance) можно видеть коэффициенты объясненной дисперсии около 1 в большинстве случаев и 1 для некоторых компонентов. Это означает, что вы можете добиться значительного снижения размерности без потери большого количества информации.
На графике экрана показана точка перегиба почти на 2 компонентах, что объясняет около 0,9989 общей дисперсии. Это лучшее количество компонентов для сокращения наших данных. Даже один компонент должен работать нормально, поскольку я не увидел большой разницы между компонентами, отображенными на одной оси.

При следующем вызове класс PCA следует вызвать со значением 2, чтобы получить из него 2 компонента.

# Create PCA object
pca = PCA(n_components=2)

reduced_data = pca.fit_transform(encoded_data)
print("pca reduced data.shape: ",reduced_data.shape)

Результат:

pca reduced data.shape:  (10000, 2)

Я решил отобразить все 32 компонента скрытого слоя на одной оси. Только один признак существенно отличался от других, которые на графике выглядели почти одинаково.

bar = [count+1 for count in range(reduced_data.shape[0])]

plt.figure(figsize = (7,10))
for col in range(reduced_data.shape[1]):
    plt.plot(bar,  reduced_data[:, col],label=f'feature {col}')
    
plt.xlabel("index")
plt.ylabel("feature")
plt.title("PCA encoded features")
plt.legend()
plt.savefig("pca-encoded features")

График компонентов и индексов:

График компонентов PCA

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

Слон в комнате:

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


Но прежде чем можно будет использовать созданный нами автоэнкодер для снижения шума в торговых данных в MetaTrader 5, нужно сохранить его в формате ONNX.


Сохранение модели автоэнкодера в формате ONNX

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

Python:

import tf2onnx
import onnx
import os

output_path = os.path.join('/kaggle/working/',"encoder.eurusd.h1.onnx")

# saving the encoder for MetaTrader 5

input_signature = [tf.TensorSpec(encoder_input.shape, tf.float16, name='x_inputs')] #onnx input signature
# Use from_function for tf functions
onnx_model, _ = tf2onnx.convert.from_keras(encoder_model, input_signature, opset=13)
onnx.save(onnx_model, output_path)

Переменная input_signature в ONNX помогает избежать ошибок с последними версиями TensorFlow и ONNX, поскольку проясняет имена входных данных для файла .onnx при загрузке модели этого формата в MetaTrader 5.

Сохранение модели декодера:

Python:

# saving the decoder

output_path = os.path.join('/kaggle/working/',"decoder.eurusd.h1.onnx")

input_signature = [tf.TensorSpec(decoder_input.shape, tf.float16, name='decoder_inputs')] #onnx input signature

onnx_model, _ = tf2onnx.convert.from_keras(decoder_model, input_signature, opset=13) #conver keras model to onnx
onnx.save(onnx_model, output_path)

В статье Решение проблем интеграции ONNX мы обсудили проблемы интеграции одних и тех же методов снижения размерности и масштабирования, доступных как для Python, так и MQL5. Но я нашел простое решение для проблемы масштабирования.

Сохранение масштабирования:

Очень важно использовать одни и те же методы для масштабирования в Python и в MQL5. Подчеркну, это очень важно.

Python:

scaler.data_min_.tofile("minmax_min.bin")
scaler.data_max_.tofile("minmax_max.bin")

Мы сохраняем массивы информации от Min-Max Scaler в простые двоичные файлы, которые можем включить в индикатор MetaTrader 5. Их нужно сохранить в папке MQL5\Files.

MQL5 (AutoEncoder Indicator.mq5):

//Load both the encoder_model and the decoder_model
#resource "\\Files\\encoder.eurusd.h1.onnx" as uchar encoder_onnx[];
#resource "\\Files\\decoder.eurusd.h1.onnx" as uchar decoder_onnx[];

// Load the MinMax scaler also
#resource "\\Files\\minmax_min.bin" as double min_values[];
#resource "\\Files\\minmax_max.bin" as double max_values[];


Уменьшение шума в торговых данных

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

MQL5 (AutoEncoder Indicator.mq5):

#property indicator_chart_window
#property indicator_plots 1
#property indicator_buffers 5

input bool show_bars = true;
input bool show_bullish_bearish = false;

//--- plot Candle
#property indicator_label1  "autoencoded open; high; low; close"
#property indicator_type1   DRAW_COLOR_CANDLES
#property indicator_color1  clrRed, clrGray
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1

Нужно создать класс Autoencoder, чтобы проще было использовать загруженные модели ONNX в MQL5 так, как если бы мы использовали их в Python.

MQL5(Autoencoder-onnx.mqh):

class CAutoEncoderONNX
  {
protected:

   bool initialized;
   long onnx_handle;
   void PrintTypeInfo(const long num,const string layer,const OnnxTypeInfo& type_info);
   long inputs[], outputs[];
   
   void replace(long &arr[]) { for (uint i=0; i<arr.Size(); i++) if (arr[i] <= -1) arr[i] = UNDEFINED_REPLACE; }
   
public:
                     CAutoEncoderONNX(void);
                    ~CAutoEncoderONNX(void);
                     
                     bool Init(const uchar &onnx_buff[], ulong flags=ONNX_DEFAULT); //load the onnx model from a resource uchar array
                     bool Init(string onnx_filename, uint flags=ONNX_DEFAULT); //load the onnx model from a .onnx file 
                     
                     matrix predict(const matrix &x); //passing inputs for either the encoder or the decoder to the outputs in matrix form
                     vector predict(const vector &x); //passing inputs for either the encoder or the decoder to the outputs in matrix form
  };

Создание экземпляра класса CAutoEncoderONNX для каждой модели отдельно, как они есть:

MQL5 (AutoEncoder Indicator.mq5):

#include <Autoencoder-onnx.mqh>
#include <MALE5\preprocessing.mqh>

CAutoEncoderONNX encoder_model; //for the encoder model
CAutoEncoderONNX decoder_model; //for the decoder model
MinMaxScaler *scaler; //Python-like MinMax scaler

Инициализация моделей:

MQL5 (AutoEncoder Indicator.mq5):

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   if (!encoder_model.Init(encoder_onnx)) //initializing the encoder
     return INIT_FAILED;
   
   if (!decoder_model.Init(decoder_onnx)) //initializing the decoder
     return INIT_FAILED;
   
   scaler = new MinMaxScaler(min_values, max_values); //Load the Minmax scaler saved in python
     
//---
   return(INIT_SUCCEEDED);
  }

Для получения прогнозов из модели, передадим необработанные данные в энкодер, а затем передадим результат в декодер для окончательного вывода. Не забывайте, что в Python у нас были две отдельные модели, переданные одна за другой при вызове функции.

Python:

class Autoencoder(Model):
...
...

  def call(self, x):
    encoded = self.encoder(x)
    decoded = self.decoder(encoded)
    return decoded

Посмотрим, как это работает в mql5:

MQL5 (AutoEncoder Indicator.mq5):

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime& time[],
                const double& open[],
                const double& high[],
                const double& low[],
                const double& close[],
                const long& tick_volume[],
                const long& volume[],
                const int& spread[])
  {
//---
   
   int start = prev_calculated;
   if(start>=rates_total)
      start = rates_total-1;
   
   
   vector encoded_data = {}, decoded_data = {};
   for(int i = start; i<rates_total; i++)
     {
        vector x_inputs = {open[i], high[i], low[i], close[i]};
        
        x_inputs = scaler.transform(x_inputs); //Normalize the input data, important!
        encoded_data = encoder_model.predict(x_inputs); //encode the data
        decoded_data = decoder_model.predict(encoded_data); //decode the data
        
        decoded_data = scaler.inverse_transform(decoded_data); //return data to its original state  
          
        open_candle[i]= decoded_data[0];
        high_candle[i]= decoded_data[1];
        low_candle[i]=  decoded_data[2];
        close_candle[i]=decoded_data[3];
        
        // Set upper and lower body colors based on the gradient
        
        if (close_candle[i]>open_candle[i])
         {
           color_buffer[i] = 1.0; //Draw gray for bullish candle
         }
        else
         {
          color_buffer[i] = 0.0; //draw red when there was a bearish candle
         }
                         
        if (MQLInfoInteger(MQL_DEBUG))
         Comment(StringFormat("plotting [%d/%d] OPEN[%.5f] HIGH[%.5f] LOW[%.5f] CLOSE[%.5f]",i,rates_total,open_candle[i],high_candle[i],low_candle[i],close_candle[i]));
     }
     
//--- return value of prev_calculated for next call
   return(rates_total);
  }

Построение индикатора:

Свечи доджи от автоэнкодера

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

Большинство свечей являются медвежьими — это красные свечи, и очень мало являются бычьими, они серые.

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

MQL5 (AutoEncoder Indicator.mq5):

  if (close_candle[i]>open_candle[i])
   {
     color_buffer[i] = 1.0; //Draw gray for bullish candle

     close_candle[i] = high_candle[i];
     open_candle[i] = low_candle[i];
   }
  else
   {
     color_buffer[i] = 0.0; //draw red when there was a bearish candle
    
     close_candle[i] = low_candle[i];
     open_candle[i] = high_candle[i];
   }

Построение индикатора:

Новые бары от автоэнкодера

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

MQL5 (AutoEncoder Indicator.mq5):

if (show_bullish_bearish)
 {
  if (close[i]>open[i])
   color_buffer[i] = 1.0;
  else
    color_buffer[i] = 0.0;
 }

Построение индикатора:

Цветные свечи OHLC EURUSD от автоэнкодера

Также есть возможность скрыть оригинальные свечи и использовать только новые, предоставленные автоэнкодером.

Индикатор автоэнкодера show bars=false


Недостатки автоэнкодеров

Автоэнкодеры, как и все модели машинного обучения, имеют определенные проблемы:

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

  2. Трудно понять
    Сжатые форматы данных, создаваемые автоэнкодерами, сложно интерпретировать. Часто бывает непонятно, какие признаки смог зафиксировать автоэнкодер, что затрудняет объяснение принципа работы модели.

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

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

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

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

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



Заключительные мысли

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

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

Всем добра.

Таблица вложений: 

Файл Описание и назначение
Include\MatrixExtend.mqh
Дополнительные функции для работы с матрицами.
Include\ preprocessing.mqh
Библиотека для предварительной обработки "сырых" входных данных, чтобы сделать их пригодными для использования в моделях машинного обучения.
Indicators\ AutoEncoder Indicator.mq5 Основной файл индикатора. В нем используется автоэнкодер, индикатор строит свечи на основе полученных результатов.
Include\ Autoencoder-onnx.mqh  Библиотека для загрузки модели машинного обучения в формате ONNX и интерпретации результатов.
Files\...  Сохраните файлы в папку MQL5\Files
autoencoders.ipynb Python Jupyter Notebook для запуска описанного в статье кода Python 




Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/14760

Прикрепленные файлы |
Code_6_Files.zip (689.5 KB)
Возможности Мастера MQL5, которые вам нужно знать (Часть 18): Поиск нейронной архитектуры с использованием собственных векторов Возможности Мастера MQL5, которые вам нужно знать (Часть 18): Поиск нейронной архитектуры с использованием собственных векторов
Поиск нейронной архитектуры (Neural Architecture Search), автоматизированный подход к определению идеальных настроек нейронной сети, может стать преимуществом при наличии большого количества вариантов и больших наборов тестовых данных. Здесь мы рассмотрим, как этот подход можно сделать еще более эффективным с помощью парных собственных векторов (Eigen Vectors).
Статистический арбитраж с прогнозами Статистический арбитраж с прогнозами
Мы рассмотрим статистический арбитраж, выполним поиск символов корреляции и коинтеграции с помощью Python, создадим индикатор для коэффициента Пирсона, а также советник для торговли статистическим арбитражем с прогнозами, сделанными с помощью Python и моделей ONNX.
Нейросети в трейдинге: Superpoint Transformer (SPFormer) Нейросети в трейдинге: Superpoint Transformer (SPFormer)
В данной статья предлагаем познакомиться с методом сегментации 3D-люъектов на основе Superpoint Transformer (SPFormer), который устраняет необходимость в промежуточной агрегации данных. Что ускоряет процесс сегментации и повышает производительность модели.
Алгоритм выбора признаков с использованием энергетического обучения на чистом MQL5 Алгоритм выбора признаков с использованием энергетического обучения на чистом MQL5
Статья представляет реализацию алгоритма выбора признаков, описанного в научной работе "FREL: Стабильный алгоритм выбора признаков" (FREL: A stable feature selection algorithm). Сам алгоритм называется "Взвешивание признаков как регуляризованное обучение на основе энергии" (Feature weighting as regularized energy based learning).