English 中文 Español Deutsch 日本語 Português
preview
Нейросети — это просто (Часть 20): Автоэнкодеры

Нейросети — это просто (Часть 20): Автоэнкодеры

MetaTrader 5Торговые системы | 11 июля 2022, 15:56
1 795 3
Dmitriy Gizlyk
Dmitriy Gizlyk

Содержание


      Введение

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


      1. Архитектура Автоэнкодера

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

      Давайте подумаем, что в таком случае может выучить нейронная сеть? Она выучит именно то, что мы от неё просим. А именно, найти признаки, от которых зависит целевой результат. При этом она обнулит влияние признаков, которые не влияют на изучаемый целевой результат или их влияние не значительно. Т.е. обучение модели происходит узко направленно. И в этом нет ничего плохого. Модель отлично делает то, что мы от нее хотим.

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

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

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

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

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

      Автоэнкодер

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

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

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

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

      Кроме того, для работы с последовательностями могут быть использованы рекуррентные модели и алгоритмы внимания. Но подобные изыскания я планирую рассмотреть в последующих статьях.


      2. Классические задачи, решаемые Автоэнкодерами

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

      Алгоритмы сжатия данных можно разделить на 2 вида:

      • сжатие с потерями;
      • сжатие без потерь.

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

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

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

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

      Следующая задача, решаемая с помощью автоэнкодеров — это очищение исходных данных от шума. И здесь 2 подхода к решению данной задачи. Первый вариант, как и в случае PCA, сжатие данных с потерями. При этом мы надеемся, что при сжатии потеряем именно "шум".

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

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

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

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


      3. Сравнение Автоэнкодера с PCA

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

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

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

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

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

      Автоэнкодер -  PCA

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


      4. Потенциальные варианты использования Автоэнкодеров в трейдинге

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

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

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

      И конечно, не забываем про Transfer Learning. Именно с этого и начиналась наша статья. Благодаря технологии обучения автоэнкодера мы можем обучить модель выделять признаки из исходных данных. А затем, возьмем только энкодер. Добавим к нему несколько слоёв принятия решения и до обучим модель с учителем для решения наших задач. При этом надо помнить, что при обучении автоэнкодера латентное состояние содержит все признаки из исходных данных. Поэтому, однажды обученный энкодер мы можем использовать для решения различных задач. Конечно, если они используют одни и те же исходные данные.

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


      5. Практические эксперименты

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

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

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

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

      Теоретическое решение найдено. Но для его реализации на практике нам нужно будет на каждой итерации обучения модели иметь доступ к результатам первого скрытого слоя нашей модели. А ранее мы еще не заглядывали внутрь наших моделей. Скрытые состояния наших нейронных сетей всегда были "черным ящиком". И для организации процесса обучения нам нужно его открыть. Для этого в наш класс организации работы нейронных сетей CNet добавим метод получения значений буфера результатов любого скрытого слоя GetLayerOutput.

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

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

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

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

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

      bool CNet::GetLayerOutput(uint layer, CBufferDouble *&result)
        {
         if(!layers || layers.Total() <= (int)layer)
            return false;
         CLayer *Layer = layers.At(layer);
         if(!Layer)
            return false;
      //---
         if(!result)
           {
            result = new CBufferDouble();
            if(!result)
               return false;
           }
      //---
         CNeuronBaseOCL *temp = Layer.At(0);
         if(!temp || temp.getOutputVal(result) <= 0)
            return false;
      //---
         return true;
        }
      

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

      В качестве исходных данных мы будем использовать ценовые котировки и данные 4-х индикаторов: RSI, CCI, ATR и MACD. Эти же данные мы использовали для тестирования всех предыдущих моделей. Все параметры индикаторов указываются во внешних параметрах советника. В функции OnInit инициализируем экземпляры объектов для работы с индикаторами.

      int OnInit()
        {
      //---
         Symb = new CSymbolInfo();
         if(CheckPointer(Symb) == POINTER_INVALID || !Symb.Name(_Symbol))
            return INIT_FAILED;
         Symb.Refresh();
      //---
         RSI = new CiRSI();
         if(CheckPointer(RSI) == POINTER_INVALID || !RSI.Create(Symb.Name(), TimeFrame, RSIPeriod, RSIPrice))
            return INIT_FAILED;
      //---
         CCI = new CiCCI();
         if(CheckPointer(CCI) == POINTER_INVALID || !CCI.Create(Symb.Name(), TimeFrame, CCIPeriod, CCIPrice))
            return INIT_FAILED;
      //---
         ATR = new CiATR();
         if(CheckPointer(ATR) == POINTER_INVALID || !ATR.Create(Symb.Name(), TimeFrame, ATRPeriod))
            return INIT_FAILED;
      //---
         MACD = new CiMACD();
         if(CheckPointer(MACD) == POINTER_INVALID || !MACD.Create(Symb.Name(), TimeFrame, FastPeriod, SlowPeriod, SignalPeriod, MACDPrice))
            return INIT_FAILED;
      

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

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

      class CLayerDescription    :  public CObject
        {
      public:
         /** Constructor */
                           CLayerDescription(void);
         /** Destructor */~CLayerDescription(void) {};
         //---
         int               type;          ///< Type of neurons in layer (\ref ObjectTypes)
         int               count;         ///< Number of neurons
         int               window;        ///< Size of input window
         int               window_out;    ///< Size of output window
         int               step;          ///< Step size
         int               layers;        ///< Layers count
         int               batch;         ///< Batch Size
         ENUM_ACTIVATION   activation;    ///< Type of activation function (#ENUM_ACTIVATION)
         ENUM_OPTIMIZATION optimization;  ///< Type of optimization method (#ENUM_OPTIMIZATION)
         double            probability;   ///< Probability of neurons shutdown, only Dropout used
        };
      

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

         Net = new CNet(NULL);
         ResetLastError();
         double temp1, temp2;
         if(CheckPointer(Net) == POINTER_INVALID || !Net.Load(FileName + ".nnw", dError, temp1, temp2, dtStudied, false))
           {
            printf("%s - %d -> Error of read %s prev Net %d", __FUNCTION__, __LINE__, FileName + ".nnw", GetLastError());
            CArrayObj *Topology = new CArrayObj();
            if(CheckPointer(Topology) == POINTER_INVALID)
               return INIT_FAILED;
            //--- 0
            CLayerDescription *desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            int prev = desc.count = (int)HistoryBars * 12;
            desc.type = defNeuronBaseOCL;
            desc.activation = None;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      

      После описания архитектуры нейронного слоя добавляем его в динамический массив описания архитектуры модели.

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

            //--- 1
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            desc.count = prev;
            desc.batch = 1000;
            desc.type = defNeuronBatchNormOCL;
            desc.activation = None;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      

      Обязательно запоминаем индекс слоя нормализации в архитектуре нашего автоэнкодера.

      Далее мы начинаем строить энкодер нашего автоэнкодера. В нем мы будем постепенно уменьшать размер нейронных слоёв до 2 элементов латентного состояния. Его архитектура может вам напомнить воронку.

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

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

            //--- 2
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            prev = desc.count = (int)HistoryBars;
            desc.type = defNeuronBaseOCL;
            desc.activation = TANH;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      
            //--- 3
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            prev = desc.count = prev / 2;
            desc.type = defNeuronBaseOCL;
            desc.activation = TANH;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      
            //--- 4
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            prev = desc.count = prev / 2;
            desc.type = defNeuronBaseOCL;
            desc.activation = TANH;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      
            //--- 5
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            desc.count = 2;
            desc.type = defNeuronBaseOCL;
            desc.activation = SIGMOID;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      

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

            //--- 6
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            desc.count = (int) HistoryBars;
            desc.type = defNeuronBaseOCL;
            desc.activation = TANH;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      
            //--- 7
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            desc.count = (int) HistoryBars * 4;
            desc.type = defNeuronBaseOCL;
            desc.activation = TANH;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      
            //--- 8
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            desc.count = (int) HistoryBars * 12;
            desc.type = defNeuronBaseOCL;
            desc.activation = TANH;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      

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

            delete Net;
            Net = new CNet(Topology);
            delete Topology;
            if(CheckPointer(Net) == POINTER_INVALID)
               return INIT_FAILED;
            dError = DBL_MAX;
           }
      

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

         TempData = new CBufferDouble();
         if(CheckPointer(TempData) == POINTER_INVALID)
            return INIT_FAILED;
      //---
         bEventStudy = EventChartCustom(ChartID(), 1, (long)MathMax(0, MathMin(iTime(Symb.Name(), 
                                        PERIOD_CURRENT, (int)(100 * Net.recentAverageSmoothingFactor * 10)),
                                        dtStudied)), 0, "Init");
      //---
         return(INIT_SUCCEEDED);
        }
      

      С полным кодом всех методов и функций можно ознакомиться во вложении.

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

      void Train(datetime StartTrainBar = 0)
        {
         int count = 0;
      //---
         MqlDateTime start_time;
         TimeCurrent(start_time);
         start_time.year -= StudyPeriod;
         if(start_time.year <= 0)
            start_time.year = 1900;
         datetime st_time = StructToTime(start_time);
         dtStudied = MathMax(StartTrainBar, st_time);
         ulong last_tick = 0;
      
         double prev_er = DBL_MAX;
         datetime bar_time = 0;
         bool stop = IsStopped();
         CArrayDouble *loss = new CArrayDouble();
         MqlDateTime sTime;
      

      После чего мы загрузим исторические данные для обучения модели.

         int bars = CopyRates(Symb.Name(), TimeFrame, st_time, TimeCurrent(), Rates);
         prev_er = dError;
      //---
         if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
           {
            ExpertRemove();
            return;
           }
         if(!ArraySetAsSeries(Rates, true))
           {
            ExpertRemove();
            return;
           }
         RSI.Refresh(OBJ_ALL_PERIODS);
         CCI.Refresh(OBJ_ALL_PERIODS);
         ATR.Refresh(OBJ_ALL_PERIODS);
         MACD.Refresh(OBJ_ALL_PERIODS);
      

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

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

         int total = (int)(bars - MathMax(HistoryBars, 0));
         do
           {
            //---
            stop = IsStopped();
            prev_er = dError;
            for(int it = total - 1; it >= 0 && !stop; it--)
              {
               int i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (total));
               if((GetTickCount64() - last_tick) >= 250)
                 {
                  com = StringFormat("Study -> Era %d -> %.6f\n %d of %d -> %.2f%% \nError %.5f",
                                     count, prev_er, bars - it + 1, bars,
                                     (double)(bars - it + 1.0) / bars * 100, Net.getRecentAverageError());
                  Comment(com);
                  last_tick = GetTickCount64();
                 }
      

      В теле вложенного цикла мы сначала выведем информацию о процессе обучения на график в виде комментариев. И случайным образом определим следующий паттерн для обучения модели. После чего заполним временный буфер историческими данными.

               TempData.Clear();
               int r = i + (int)HistoryBars;
               if(r > bars)
                  continue;
               //---
               for(int b = 0; b < (int)HistoryBars; b++)
                 {
                  int bar_t = r - b;
                  double open = Rates[bar_t].open;
                  TimeToStruct(Rates[bar_t].time, sTime);
                  double rsi = RSI.Main(bar_t);
                  double cci = CCI.Main(bar_t);
                  double atr = ATR.Main(bar_t);
                  double macd = MACD.Main(bar_t);
                  double sign = MACD.Signal(bar_t);
                  if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE ||
                     macd == EMPTY_VALUE || sign == EMPTY_VALUE)
                     continue;
                  //---
                  if(!TempData.Add(Rates[bar_t].close - open) || !TempData.Add(Rates[bar_t].high - open) ||
                     !TempData.Add(Rates[bar_t].low - open) || !TempData.Add((double)Rates[bar_t].tick_volume / 1000.0) ||
                     !TempData.Add(sTime.hour) || !TempData.Add(sTime.day_of_week) || !TempData.Add(sTime.mon) ||
                     !TempData.Add(rsi) || !TempData.Add(cci) || !TempData.Add(atr) || !TempData.Add(macd) || !TempData.Add(sign))
                     break;
                 }
               if(TempData.Total() < (int)HistoryBars * 12)
                  continue;
      

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

               Net.feedForward(TempData, 12, true);
               TempData.Clear();
      

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

               if(!Net.GetLayerOutput(1, TempData))
                  break;
               Net.backProp(TempData);
               stop = IsStopped();
              }
      

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

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

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

            if(!stop)
              {
               dError = Net.getRecentAverageError();
               Net.Save(FileName + ".nnw", dError, 0, 0, dtStudied, false);
               printf("Era %d -> error %.5f %%", count, dError);
               loss.Add(dError);
               count++;
              }
           }
         while(!(dError < 0.01 && (prev_er - dError) < 0.01) && !stop);
      

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

         Comment("Write dinamic of error");
         int handle = FileOpen("ae_loss.csv", FILE_WRITE | FILE_CSV | FILE_ANSI, ",", CP_UTF8);
         if(handle == INVALID_HANDLE)
           {
            PrintFormat("Error of open loss file: %d", GetLastError());
            delete loss;
            return;
           }
         for(int i = 0; i < loss.Total(); i++)
            if(FileWrite(handle, loss.At(i)) <= 0)
               break;
         FileClose(handle);
         PrintFormat("The dynamics of the error change is saved to a file %s\\%s",
                     TerminalInfoString(TERMINAL_DATA_PATH), "ae_loss.csv");
         delete loss;
         Comment("");
         ExpertRemove();
        }
      

      Надо сказать, что в приведенном варианте я использую функцию ExpertRemove для завершения работы советника, так как его задача была обучить модель. Если же перед вашим советникам будут стоять и другие задачи, то необходимо удалить данную функцию из кода. Или перенести её в то место, где Ваш советник выполнит все поставленные перед ним задачи.

      С полным кодом советника и всех используемых классов можно познакомиться во вложении.

      После создания советника мы можем провести его тестирование на реальных данных. Обучение автоэнкодера мы осуществили на инструменте EURUSD таймфрейма H1 на историческом отрезке в последние 15 лет. Таким образом, обучение автоэнкодера произошло на обучающей выборке из более 92 тыс. паттернов из 40 свечей. Динамика ошибки в процессе обучения представлена на графике ниже.

      Динамика ошибки обучения Автоэнкодера

      Как можно заметить на графике, буквально за 10 эпох значение среднеквадратичной ошибки снизилась до показателя 0.28 и далее продолжила медленное снижение. То есть, автоэнкодер способен сжать до 2-х элементов латентного состояния информацию из 480 признаков (40 свечей * 12 признаков на свечу) с сохранением 78% информации. Надо сказать, что при использовании PCA на первых 2-х компонентах сохраняется чуть менее 25% аналогичных данных.

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

      Сразу после создания обучающей выборки мы добавим обучение объекта метода главных компонент.

      void Train(datetime StartTrainBar = 0)
        {
      //---
          Процесс создания обучающей выборки без изменений
      //---
         if(!PCA.Study(data))
           {
            printf("Ошибка выполнения %d", GetLastError());
            return;
           }
      

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

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

           {
            //---
            stop = IsStopped();
            bool add_loop = false;
            for(int it = 0; i < 1000 && !stop; i++)
              {
               if((GetTickCount64() - last_tick) >= 250)
                 {
                  com = StringFormat("Calculation -> %d of %d -> %.2f%%", it + 1, 1000, (double)(it + 1.0) / 1000 * 100);
                  Comment(com);
                  last_tick = GetTickCount64();
                 }
               int i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (total));
               TempData.Clear();
               int r = i + (int)HistoryBars;
               if(r > bars)
                  continue;
               //---
               for(int b = 0; b < (int)HistoryBars; b++)
                 {
                  int bar_t = r - b;
                  double open = Rates[bar_t].open;
                  TimeToStruct(Rates[bar_t].time, sTime);
                  double rsi = RSI.Main(bar_t);
                  double cci = CCI.Main(bar_t);
                  double atr = ATR.Main(bar_t);
                  double macd = MACD.Main(bar_t);
                  double sign = MACD.Signal(bar_t);
                  if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
                     continue;
                  //---
                  if(!TempData.Add(Rates[bar_t].close - open) || !TempData.Add(Rates[bar_t].high - open) ||
                     !TempData.Add(Rates[bar_t].low - open) || !TempData.Add((double)Rates[bar_t].tick_volume / 1000.0) ||
                     !TempData.Add(sTime.hour) || !TempData.Add(sTime.day_of_week) || !TempData.Add(sTime.mon) ||
                     !TempData.Add(rsi) || !TempData.Add(cci) || !TempData.Add(atr) || !TempData.Add(macd) || !TempData.Add(sign))
                     break;
                 }
               if(TempData.Total() < (int)HistoryBars * 12)
                  continue;
      

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

               Net.feedForward(TempData, 12, true);
               data = PCA.ReduceM(TempData);
               TempData.Clear();
               if(!Net.GetLayerOutput(5, TempData))
                  break;
      

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

               bool sell = (Rates[i - 1].high <= Rates[i].high && Rates[i + 1].high < Rates[i].high);
               bool buy = (Rates[i - 1].low >= Rates[i].low && Rates[i + 1].low > Rates[i].low);
               if(buy && sell)
                  buy = sell = false;
      

      И сохраняем полученные данные в файл для последующей визуализации. После чего переходим к следующему паттерну.

               FileWrite(handle, (buy ? DoubleToString(TempData.At(0)) : " "), (buy ? DoubleToString(TempData.At(1)) : " "),
                         (sell ? DoubleToString(TempData.At(0)) : " "), (sell ? DoubleToString(TempData.At(1)) : " "),
                         (!(buy || sell) ? DoubleToString(TempData.At(0)) : " "),
                         (!(buy || sell) ? DoubleToString(TempData.At(1)) : " "),
                         (buy ? DoubleToString(data[0, 0]) : " "), (buy ? DoubleToString(data[0, 1]) : " "),
                         (sell ? DoubleToString(data[0, 0]) : " "), (sell ? DoubleToString(data[0, 1]) : " "),
                         (!(buy || sell) ? DoubleToString(data[0, 0]) : " "),
                         (!(buy || sell) ? DoubleToString(data[0, 1]) : " "));
               stop = IsStopped();
              }
           }
      

      По завершении всех итераций цикла мы очищаем поле комментариев на графике и завершаем работу советника.

         Comment("");
         ExpertRemove();
        }
      

      С полным кодом советника можно ознакомиться во вложении.

      В результате работы советника был сформирован файл "AE_latent.csv", в котором были собраны данные латентного состояния автоэнкодера и первых двух компонент для соответствующих паттернов. По данным указанного файла были построены 2 ниже представленных графика.

      Визуализация латентного состояния автоэнкодера Визуализация 2-х первых главных компонент

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

      В то же время, сжатие данных методом главных компонент даёт довольно большие значения. При этом по значения по осям отличаются в 6-7 раз. А центр распределения находится примерно в точке [18000, 130000]. Кроме того, обращает на себя ярко выраженные линейные верхняя и нижняя границы диапазона.

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


      Заключение

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

      На все Ваши вопросы я с удовольствием отвечу в ветке форума, посвященной данной статье.


      Ссылки

      1. Нейросети — это просто (Часть 14): Кластеризация данных
      2. Нейросети — это просто (Часть 15): Кластеризации данных средствами MQL5
      3. Нейросети — это просто (Часть 16): Практическое использование кластеризации
      4. Нейросети — это просто (Часть 17): Понижение размерности
      5. Нейросети — это просто (Часть 18): Ассоциативные правила
      6. Нейросети — это просто (Часть 19): Ассоциативные правила средствами MQL5


      Программы, используемые в статье

      # Имя Тип Описание
      1 ae.mq5 Советник   Советник обучения автоэнкодера 
      2 ae2.mq5 Советник Советник подготовки данных для визуализации 
      2 NeuroNet.mqh Библиотека класса Библиотека классов для создания нейронной сети
      3 NeuroNet.cl Библиотека Библиотека кода программы OpenCL


      Прикрепленные файлы |
      MQL5.zip (67.49 KB)
      Последние комментарии | Перейти к обсуждению на форуме трейдеров (3)
      Vasiliy Smirnov
      Vasiliy Smirnov | 11 июл. 2022 в 16:01
      Это просто, только разделов уже 20 частей).
      Dmitriy Gizlyk
      Dmitriy Gizlyk | 11 июл. 2022 в 16:06
      Vasiliy Smirnov #:
      Это просто, только разделов уже 20 частей).

      Я стараюсь показать различные подходы и продемонстрировать различные варианты использования.
      А названием "это просто" хотел подчеркнуть доступность технологии каждому желающемую

      byronjames88
      byronjames88 | 30 сент. 2022 в 01:45
      Hi Dmitry, are you available to help me setup and run this EA?
      Разработка торгового советника с нуля (Часть 18): Новая система ордеров (I) Разработка торгового советника с нуля (Часть 18): Новая система ордеров (I)
      Это первая часть новой системы ордеров. С тех пор, как мы начали создавать документацию данного советника в наших статьях, он претерпел различные изменения и улучшения, сохраняя при этом ту же модель системы ордеров на графике.
      Разработка торговой системы на основе индикатора ADX Разработка торговой системы на основе индикатора ADX
      Эта статья продолжает серию о построении торговых систем с использованием самых популярных индикаторов. На этот раз мы поговорим об индикаторе ADX (Average Directional Index, Индекс среднего направленного движения). Мы подробно изучим этот индикатор, чтобы понять, чем он может быть полезен в торговле. Также с помощью простых стратегий мы узнаем, как его использовать. Изучая самую суть вещей, мы можем получить больше информации и использовать это с максимальной выгодой.
      Видео: Настройка MetaTrader 5 и MQL5 для простой автоматизированной торговли Видео: Настройка MetaTrader 5 и MQL5 для простой автоматизированной торговли
      В этом небольшом видеокурсе вы узнаете, как скачать, установить и настроить MetaTrader 5 для автоматизированной торговли. Вы также узнаете, как настроить график и параметры автоматизированной торговли. Вы проведете свое первое тестирование на истории и узнаете, как импортировать советника, который может самостоятельно торговать 24 часа в сутки 7 дней в неделю, избавляя вас от необходимости сидеть перед экраном.
      Разработка торговой системы на основе Стохастика Разработка торговой системы на основе Стохастика
      Это очередная статья из обучающей серии, в которой мы знакомимся с различными индикаторами. В этот раз мы обратимся к другому популярному индикатору — Stochastic Oscillator. Изучим его, рассмотрим стратегии на его основе и создадим торговую систему.