
Машинное обучение и Data Science (Часть 22): Автоэнкодеры для устранения шума и выявления сигналов в трейдинге
Что такое Автоэнкодер?
Автоэнкодеры — это искусственные нейронные сети прямого распространения. В простейшем виде автоэнкодер представляет собой нейронную сеть, которая выполняет две задачи. Сначала сжимает входные данные для снижения размерности, а затем пытается использовать это представление данных для воссоздания исходных входных данных.
Допустим, вы передали в автоэнкодер размытое изображение кошки. Это изображение будет сжато и распаковано обратно в исходное состояние, при этом будут потеряны некоторые из его шумных/размытых пикселей, и в итоге получится четкое изображение кошки.
В этой статье мы рассмотрим, как автоэнкодер можно использовать в финансовой сфере для фильтрации рыночного шума, чтобы получать четкие торговые сигналы.
Эту статью будет понять проще, если у вас уже есть базовые знания об ONNX, PCA и нейронных сетях.
Автоэнкодер состоит из двух частей:
- Энкодер принимает входные данные и сжимает их в скрытое представление меньшей размерности, фиксируя основные характеристики.
- Декодер получает скрытое представление и пытается восстановить исходные входные данные как можно точнее.
Преимущества автоэнкодеров:
- Они эффективны в задачах снижения размерности и могут успешно применяться на финансовых рынках для извлечения признаков, сжатия данных и визуализации в многомерных наборах данных.
- Пытаясь реконструировать входные данные, автоэнкодер изучает основные характеристики и удаляет шум или нерелевантную информацию. Изученные признаки могут быть полезны для других задач машинного обучения, таких как классификация или обнаружение аномалий.
- Поскольку это сети обучения без учителя, они могут обнаруживать скрытые закономерности в данных без вмешательства человека.
- Полученное скрытое представление от автоэнкодера можно использовать в качестве предварительно обученных признаков для других моделей, что может потенциально улучшить их работу.
Из чего они сделаны?
Давайте разберем автоэнкодеры и посмотрим, из чего они состоят и что делает их особенными.
В основе автоэнкодера лежит искусственная нейронная сеть, состоящая из трех частей.
- Энкодер
- Вектор эмбеддинга/скрытый слой
- Декодер
Левая часть нейронной сети называется энкодером. Его задача — преобразовать исходные входные данные в представление меньшей размерности.
Средняя часть — скрытый слоем или вектор эмбеддинга, его роль заключается в сжатии входных данных в данные меньшей размерности. Предполагается, что этот слой имеет меньше нейронов, чем энкодер и декодер.
Правая часть этой нейронной сети называется декодером. Его задача — воссоздать исходные входные данные, используя выходные данные энкодера. Другими словами, он пытается обратить процесс энкодинга.
Это интересно, поскольку декодер пытается воссоздать данные более высокой размерности из данных более низкой размерности, возвращаемых энкодером. Это как пытаться построить дом по фотографии такого дома.
Приходится иметь дело с потерей информации, которая является ключом к работе всего этого процесса. Декодер затем имеет несовершенную информацию, а наша цель - обучить сеть минимизировать ошибки. Во время обучения энкодер и декодер вынуждены работать вместе, чтобы минимизировать ошибку построения.
Ошибка построения — разница между попыткой воссоздания и исходными входными данными.
Если бы не было потери информации между энкодером и декодером, то сеть просто научилась бы умножать входные данные на единицу и получать идеальную реконструкцию. При этом автоэнкодер был бы попросту бесполезным. В этой технологии машинного обучение заложено наличие энкодера с некоторой степенью ошибок. Важно не переобучить модель.
Как энкодеры, так и декодеры не ограничиваются одним слоем — это видно на схеме архитектуры автокодера выше. Они могут содержать несколько слоев. Пример такой архитектуры показан ниже в коде на 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)
Ниже представлена архитектура автоэнкодера:
В классе автоэнкодера 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.
Как это исправить:
Можно использовать другие методы нормализации, такие как масштабирование 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. ]
На кумулятивной объясненной дисперсии (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 только к независимым переменным.
Но прежде чем можно будет использовать созданный нами автоэнкодер для снижения шума в торговых данных в 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; }
Построение индикатора:
Также есть возможность скрыть оригинальные свечи и использовать только новые, предоставленные автоэнкодером.
Недостатки автоэнкодеров
Автоэнкодеры, как и все модели машинного обучения, имеют определенные проблемы:
-
Несовершенная реконструкция данных
Автоэнкодеры пытаются воссоздать данные после сжатия. Иногда они не справляются с этой задачей, что приводит к ошибкам при восстановлении исходных данных. Это серьезная проблема, если необходимо очень точно воссоздать исходные данные. -
Трудно понять
Сжатые форматы данных, создаваемые автоэнкодерами, сложно интерпретировать. Часто бывает непонятно, какие признаки смог зафиксировать автоэнкодер, что затрудняет объяснение принципа работы модели. -
Чувствительность к шуму
Автоэнкодеры пытаются выделить основные закономерности в данных, но могут быть сложности с шумом и выбросами. Это может привести к некачественной реконструкции и искажению признаков. -
Проблема размерности
Средний уровень автоэнкодера, в котором сжимаются данные, иногда может быть слишком мал. Если в нем недостаточно измерений, он может не вместить всю важную информацию для определенных задач. Очень важно подобрать правильный размера этого слоя для конкретных задач. -
Дороговизна обучения
Обучение глубоких автоэнкодеров, особенно на больших наборах данных, может потребовать больших вычислительных мощностей. Это важно помнить, если у вас ограниченные ресурсы или время. -
Подходит не для всех задач
Автоэнкодеры могут оказаться не лучшим выбором для задач классификации или регрессии — в них работа напрямую с исходными данными может быть более эффективной. -
Риск переобучения
Использование сложных моделей для решения простых задач может привести к переобучению, при котором модель слишком хорошо усваивает обучающие данные, но плохо работает на новых, неизвестных данных.
Заключительные мысли
Автоэнкодеры вполне можно использовать для снижения шума на рынке, что показал наш индикатор: в результате мы получили менее шумные свечи, которые по-прежнему отражают рыночную информацию. Однако полученные свечи могли быть как лучше, так и хуже оригинальных. Эти новые свечи дают иной взгляд на рынок.
Чтобы это проверить, можете воспользоваться приложенными программами, построить торговые стратегии на основе таких свечей от автоэнкодера и протестировать их.
Всем добра.
Таблица вложений:
Файл | Описание и назначение |
---|---|
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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования