English Español Deutsch 日本語 Português
preview
Нейросети — это просто (Часть 41): Иерархические модели

Нейросети — это просто (Часть 41): Иерархические модели

MetaTrader 5Эксперты | 12 мая 2023, 13:03
1 106 4
Dmitriy Gizlyk
Dmitriy Gizlyk

Введение

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

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

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


1. Преимущества иерархических моделей

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

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

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

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

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

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

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

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

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

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

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

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

Один из ярких примеров алгоритмов обучения иерархических моделей в трейдинге - это Scheduled Auxiliary Control (SAC-X).

Алгоритм Scheduled Auxiliary Control (SAC-X) является методом обучения с подкреплением, который использует иерархическую структуру для принятия решений. И представляет собой новый подход в направлении решения задач с разряженным вознаграждением. Он основан на четырех основных принципах:

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

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

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

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

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

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

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

В целом, алгоритм Scheduled Auxiliary Control (SAC-X) представляет собой инновационный подход к обучению агентов в средах с разреженной наградой. Он объединяет использование внешних и внутренних вспомогательных наград, планировщика и асинхронного обучения для достижения высокой производительности и адаптивности агента. SAC-X предоставляет новые возможности для решения сложных задач и может быть применен во множестве приложений, где разреженная награда является вызовом.

Алгоритм действий SAC-X можно описать следующим образом:

  1. Инициализация: начинается с инициализации политик для каждого намерения и их соответствующих наградных векторов. Также инициализируется планировщик, который будет выбирать и выполнять намерения.
  2. Цикл обучения:
    1. Сбор опыта: Агент взаимодействует со средой, выполняя действия на основе выбранного намерения. Он собирает опыт в виде состояний, действий, полученных внешних наград и внутренних вспомогательных наград.
    2. Обновление намерений: для каждого намерения происходит обновление соответствующей политики с использованием собранного опыта. Политика настраивается для максимизации кумулятивной вспомогательной награды, присвоенной этому намерению.
    3. Планирование: Планировщик выбирает, какое намерение будет выполнено в следующем шаге, основываясь на текущем состоянии и предыдущих выполненных намерениях. Цель планировщика — улучшить общую производительность агента на внешних задачах.
    4. Асинхронное обучение: Обновление политик и планировщика происходит асинхронно, позволяя агенту эффективно использовать полученную информацию и опыт от других намерений.
  3. Завершение: Алгоритм продолжает цикл обучения до достижения заданного критерия остановки, такого как достижение определенной производительности или количества итераций.

Алгоритм SAC-X позволяет агенту эффективно использовать внешние и внутренние вспомогательные награды для обучения и выбирать наилучшие намерения для достижения оптимальных результатов на внешних задачах. Это позволяет преодолеть проблему разреженности награды и улучшить производительность агента в средах с низкими наградами.


2. Реализация средствами MQL5

Алгоритм Scheduled Auxiliary Control (SAC-X) предусматривает асинхронное обучение агентов с возможностью свободного обмена опытом между различными агентами. Для организации этого процесса мы, как и в предыдущей статье, разделим весь процесс обучения на 2 этапа:

  • Сбор опыта
  • Обучение политик (стратегий поведения агентов)

Для сбора опыта мы вначале создадим 2 структуры. В первую структуру SState мы будем записывать описание отдельного состояния системы. Она будет содержать лишь один статический массив для записи значений с плавающей запятой.

struct SState
  {
   float             state[HistoryBars * 12 + 9];
   //---
                     SState(void);
   //---
   bool              Save(int file_handle);
   bool              Load(int file_handle);
   //--- overloading
   void              operator=(const SState &obj)   { ArrayCopy(state, obj.state); }
  };

Для удобства использования создадим в структуре методы работы с файлами Save и Load. Код методов довольно прост. И Вы можете самостоятельно ознакомиться с ним во вложении.

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

  • States — массив состояний. Это массив выше созданных структур, в который будут записаны все состояния, посещенные агентом
  • Actions — массив действий, совершенных агентом
  • Revards — массив вознаграждений, полученных от внешней среды.

Кроме того, мы добавим 3 переменные:

  • Total — количество посещённых состояний
  • DiscountFactor — фактор дисконтирования
  • CumCounted — флаг, указывающий на выполнение пересчета кумулятивного вознаграждения с учетом фактора дисконтирования.

struct STrajectory
  {
   SState            States[Buffer_Size];
   int               Actions[Buffer_Size];
   float             Revards[Buffer_Size];
   int               Total;
   float             DiscountFactor;
   bool              CumCounted;
   //---
                     STrajectory(void);
   //---
   bool              Add(SState &state, int action, float reward);
   void              CumRevards(void);
   //---
   bool              Save(int file_handle);
   bool              Load(int file_handle);
  };

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

STrajectory::STrajectory(void)  :   Total(0),
                                    DiscountFactor(0.99f),
                                    CumCounted(false)
  {
   ArrayInitialize(Actions, -1);
   ArrayInitialize(Revards, 0);
  }

Обратите внимание, что в конструкторе мы определяем общее количество посещенных состояний равным "0". И флаг выполнения подсчета накопительного вознаграждения CumCounted в false. Непосредственно расчет накопительного вознаграждения мы будем осуществлять перед сохранением данных в файл. Эти значения нам понадобятся при обучении модели.

Методом Add мы будем добавлять в базу наборы состояние-действие-вознаграждение.

bool STrajectory::Add(SState &state, int action, float reward)
  {
   if(Total + 1 >= ArraySize(Actions))
      return false;
   States[Total] = state;
   Actions[Total] = action;
   if(Total > 0)
      Revards[Total - 1] = reward;
   Total++;
//---
   return true;
  }

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

Метод подсчета накопительного вознаграждения CumRevards довольно прост. Но следует обратить внимание на контроль флага выполненного расчета CumCounted. Это очень важный момент. Так как данный контроль предотвращает повторный подсчет накопительного вознаграждения, что может коренным образом исказить данные обучающей выборки. И, как следствие, обучение модели в целом.

void STrajectory::CumRevards(void)
  {
   if(CumCounted)
      return;
//---
   for(int i = Buffer_Size - 2; i >= 0; i--)
      Revards[i] += Revards[i + 1] * DiscountFactor;
   CumCounted = true;
  }

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

Первый советник для сбора опыта мы создадим в файле Research.mq5. Мы планируем запускать данный советник в режиме оптимизации тестера стратегий для параллельного сбора опыта нескольких проходом агента по обучающему эпизоду исторических данных. Точно такой подход мы использовали на Фазе 1 в предыдущей статье. Как и в советнике "Fasa1.mql5", мы будем использовать методы OnTester, OnTesterInit, OnTesterPass и OnTesterDeinit для сбора и сохранения информации с различных проходов в единый буфер накопления опыта. Только сейчас для выбора действий мы будем использовать нашу модель, а не генератор случайных значений, как в указанном советнике.

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

//+------------------------------------------------------------------+
//| Input parameters                                                 |
//+------------------------------------------------------------------+
input ENUM_TIMEFRAMES      TimeFrame   =  PERIOD_H1;
//---
input group                "---- RSI ----"
input int                  RSIPeriod   =  14;            //Period
input ENUM_APPLIED_PRICE   RSIPrice    =  PRICE_CLOSE;   //Applied price
//---
input group                "---- CCI ----"
input int                  CCIPeriod   =  14;            //Period
input ENUM_APPLIED_PRICE   CCIPrice    =  PRICE_TYPICAL; //Applied price
//---
input group                "---- ATR ----"
input int                  ATRPeriod   =  14;            //Period
//---
input group                "---- MACD ----"
input int                  FastPeriod  =  12;            //Fast
input int                  SlowPeriod  =  26;            //Slow
input int                  SignalPeriod =  9;            //Signal
input ENUM_APPLIED_PRICE   MACDPrice   =  PRICE_CLOSE;   //Applied price
input int                  Agent=1;

Для целей запуска оптимизатора стратегий добавим параметр Agent. В коде советника он не используется и нужен только для регулирования количества агентов в оптимизаторе тестера стратегий.

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

SState               sState;
STrajectory          Base;
STrajectory          Buffer[];
STrajectory          Frame[1];
CNet                 Actor;
CFQF                 Schedule;
int                  Models = 1;

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

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

Я же хочу остановиться на методе описания архитектуры моделей CreateDescriptions. Наших агентов намерений мы будем обучать методом Актер-Критик. Поэтому создавать описание мы будем для трем моделей:

  • Агент (Актер)
  • Критик
  • Планировщик (модель верхнего уровня иерархии).

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

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

В параметрах метода мы получаем указатели на 3 объекта для передачи архитектур создаваемых моделей. В теле метода мы проверяем актуальность полученных указателей. И, при необходимости создаем новые объекты.

bool CreateDescriptions(CArrayObj *actor, CArrayObj *critic, CArrayObj *scheduler)
  {
//---
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
         return false;
     }
//---
   if(!critic)
     {
      critic = new CArrayObj();
      if(!critic)
         return false;
     }
//---
   if(!scheduler)
     {
      scheduler = new CArrayObj();
      if(!scheduler)
         return false;
     }

Первым мы создаем описание архитектуры Актера (агента). Как всегда, первым мы используем полносвязный слой. А за ним идет слой нормализации данных.

//--- Actor
   actor.Clear();
   CLayerDescription *descr;
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = (int)(HistoryBars * 12 + 9);
   descr.window = 0;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = 1000;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Следующим я поставил еще один полносвязный слой.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 300;
   descr.optimization = ADAM;
   descr.activation = SIGMOID;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Далее сверточный слой попробует выделить некие паттерны данных.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = 100;
   descr.window = 3;
   descr.step = 3;
   descr.window_out = 2;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

И его результаты мы обработаем полносвязным слоем.

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 100;
   descr.optimization = ADAM;
   descr.activation = SIGMOID;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

За ним поставим ещё один сверточный слой.

//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = 50;
   descr.window = 2;
   descr.step = 2;
   descr.window_out = 4;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

В результате, такой "слоёный пирог" понизит нам размерность данных до 100 элементов. Такая архитектура будет осуществлять предварительную обработку данных.

Далее нам предстоит создать несколько агентов намерения. Чтобы не создавать несколько моделей, мы воспользуемся нашими наработками и будем использовать класс мульти-модельного полносвязного нейронного слоя CNeuronMultiModel. Сначала мы создаем полносвязный слой достаточного размера.

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 1000;
   descr.optimization = ADAM;
   descr.activation = TANH;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

А затем создадим 2 скрытых мульти-модельных полносвязных нейронных слоя по 10 моделей в каждом.

//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMultiModels;
   descr.count = 200;
   descr.window = 100;
   descr.step = 10;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMultiModels;
   descr.count = 50;
   descr.window = 200;
   descr.step = 10;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

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

Благодаря использованию нашего полносвязного мульти-модельного слоя результаты всех 10 моделей хранятся в одной матрице. Мы можем использовать наш слой CNeuronSoftMaxOCL для нормализации данных. При инициализации слоя мы указываем, что нужно нормализовать матрицу, состоящую из 10 строк.

//--- layer 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMultiModels;
   descr.count = 4;
   descr.window = 50;
   descr.step = 10;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 10
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSoftMaxOCL;
   descr.count = 4;
   descr.step = 10;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Мы разработали модель с единым блоком предварительной обработки данных, за которым следует 10 параллельных актеров (агентов намерений). У каждого актера на выходе имеется вероятностное распределение действий.

Аналогично, модель критика создается с 10 критиками на выходе. Однако, на выходе критика мы ожидаем получить значение функции ценности (value) для каждого действия. Поэтому в модели критика мы не используем слой SoftMax.

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

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

В описании модели планировщика укажем слой исходных данных соответствующего размера.

//--- Scheduler
   scheduler.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = (int)(HistoryBars * 12 + 9+40);
   descr.window = 0;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!scheduler.Add(descr))
     {
      delete descr;
      return false;
     }

За ним следует слой нормализации исходных данных.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = 1000;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!scheduler.Add(descr))
     {
      delete descr;
      return false;
     }

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

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 300;
   descr.optimization = ADAM;
   descr.activation = SIGMOID;
   if(!scheduler.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = 100;
   descr.window = 3;
   descr.step = 3;
   descr.window_out = 2;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!scheduler.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 100;
   descr.optimization = ADAM;
   descr.activation = SIGMOID;
   if(!scheduler.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = 50;
   descr.window = 2;
   descr.step = 2;
   descr.window_out = 4;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!scheduler.Add(descr))
     {
      delete descr;
      return false;
     }

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

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

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

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 100;
   descr.optimization = ADAM;
   descr.activation = TANH;
   if(!scheduler.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 100;
   descr.optimization = ADAM;
   descr.activation = TANH;
   if(!scheduler.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronFQF;
   descr.count = 10;
   descr.window_out = 32;
   descr.optimization = ADAM;
   if(!scheduler.Add(descr))
     {
      delete descr;
      return false;
     }

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

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

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

Сначала мы выполняем прямой проход через пул Агентов намерения.

   State1.AssignArray(sState.state);
   if(!Actor.feedForward(GetPointer(State1), 12, true))
      return;

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

   Actor.getResults(Result);
   State1.AddArray(Result);
   if(!Schedule.feedForward(GetPointer(State1),12,true))
      return;

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

   int act = GetAction(Result, Schedule.getSample(), Models);

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

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

   switch(act)
     {
      case 0:
         if(!Trade.Buy(Symb.LotsMin(), Symb.Name()))
            act = 3;
         break;
      case 1:
         if(!Trade.Sell(Symb.LotsMin(), Symb.Name()))
            act = 3;
         break;
      case 2:
         for(int i = PositionsTotal() - 1; i >= 0; i--)
            if(PositionGetSymbol(i) == Symb.Name())
               if(!Trade.PositionClose(PositionGetInteger(POSITION_IDENTIFIER)))
                 {
                  act = 3;
                  break;
                 }
         break;
     }
//---
   float reward = 0;
   if(Base.Total > 0)
      reward = ((sState.state[240] + sState.state[241]) - 
               (Base.States[Base.Total - 1].state[240] + Base.States[Base.Total - 1].state[241])) / 10;
   if(!Base.Add(sState, act, reward))
      ExpertRemove();
//---
  }

После каждого прохода информация о совершенных действиях, пройденных состояниях системы и полученном вознаграждении сохраняется в единый буфер для последующего обучения моделей. Данные операции осуществляются в методах OnTester, OnTesterInit, OnTesterPass и OnTesterDeinit, принцип построения которых был подробно описан в статье об алгоритме Go-Explore.

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

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

//+------------------------------------------------------------------+
//| Input parameters                                                 |
//+------------------------------------------------------------------+
input int                  Iterations     = 100000;

В блоке глобальных переменных мы уже указываем 3 модели: Актер, Критик и Планировщик. Архитектура моделей была описана выше.

STrajectory          Buffer[];
CNet                 Actor;
CNet                 Critic;
CFQF                 Scheduler;

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

int OnInit()
  {
//---
   ResetLastError();
   if(!LoadTotalBase())
     {
      PrintFormat("Error of load study data: %d", GetLastError());
      return INIT_FAILED;
     }

Загружаем предварительно обученные или создаем новые модели.

//--- load models
   float temp;
   if(!Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) ||
      !Critic.Load(FileName + "Crt.nnw", temp, temp, temp, dtStudied, true) ||
      !Scheduler.Load(FileName + "Sch.nnw", dtStudied, true))
     {
      CArrayObj *actor = new CArrayObj();
      CArrayObj *critic = new CArrayObj();
      CArrayObj *schedule = new CArrayObj();
      if(!CreateDescriptions(actor, critic, schedule))
        {
         delete actor;
         delete critic;
         delete schedule;
         return INIT_FAILED;
        }
      if(!Actor.Create(actor) || !Critic.Create(critic) || !Scheduler.Create(schedule))
        {
         delete actor;
         delete critic;
         delete schedule;
         return INIT_FAILED;
        }
      delete actor;
      delete critic;
      delete schedule;
     }
   Scheduler.getResults(SchedulerResult);
   Models = (int)SchedulerResult.Size();
   Actor.getResults(ActorResult);
   Scheduler.SetUpdateTarget(Iterations);
   if(ActorResult.Size() % Models != 0)
     {
      PrintFormat("The scope of the scheduler does not match the scope of the Agent (%d <> %d)", 
                                                                     Models, ActorResult.Size());
      return INIT_FAILED;
     }

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

//---
   if(!EventChartCustom(ChartID(), 1, 0, 0, "Init"))
     {
      PrintFormat("Error of create study event: %d", GetLastError());
      return INIT_FAILED;
     }
//---
   return(INIT_SUCCEEDED);
  }

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

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

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

void Train(void)
  {
   int total_tr = ArraySize(Buffer);
   uint ticks = GetTickCount();

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

   for(int iter = 0; (iter < Iterations && !IsStopped()); iter ++)
     {
      int tr = (int)(((double)MathRand() / 32767.0) * (total_tr - 1));
      int i = 0;
      i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2));

Внутри цикла обучения мы сначала выбираем проход из обучающей базы примеров, как было упомянуто ранее. Затем мы случайным образом выбираем состояние из выбранного прохода. Это состояние передается в качестве исходных данных для прямого прохода моделей Актера и Критика.

      State1.AssignArray(Buffer[tr].States[i].state);
      if(IsStopped())
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         ExpertRemove();
         return;
        }
      if(!Actor.feedForward(GetPointer(State1), 12, true) ||
         !Critic.feedForward(GetPointer(State1), 12, true))
         return;

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

      Actor.getResults(ActorResult);
      Critic.getResults(CriticResult);

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

      State1.AddArray(ActorResult);
      if(!Scheduler.feedForward(GetPointer(State1), 12, true))
         return;

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

      Scheduler.getResults(SchedulerResult);
      int agent = Scheduler.getAction();
      if(agent < 0)
        {
         iter--;
         continue;
        }

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

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

      int actions = (int)(ActorResult.Size() / SchedulerResult.Size());
      float max_value = CriticResult[agent * actions];
      for(int j = 1; j < actions; j++)
         max_value = MathMax(max_value, CriticResult[agent * actions + j]);
      SchedulerResult[agent] = Buffer[tr].Revards[i];
      Result.AssignArray(SchedulerResult);
      //---
      if(!Scheduler.backProp(GetPointer(Result),0.0f,NULL))
         return;

Затем вызываем метод обратного прохода критика.

      int agent_action = agent * actions + Buffer[tr].Actions[i];
      CriticResult[agent_action] = Buffer[tr].Revards[i];
      Result.AssignArray(CriticResult);
      //---
      if(!Critic.backProp(GetPointer(Result)))
         return;

А за ним следует модель агентов намерения.

      ActorResult.Fill(0);
      ActorResult[agent_action] = Buffer[tr].Revards[i] - max_value;
      Result.AssignArray(ActorResult);
      //---
      if(!Actor.backProp(GetPointer(Result)))
         return;

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

      if(GetTickCount() - ticks > 500)
        {
         string str = StringFormat("Actor %.2f%% -> Error %.8f\n", 
                                iter * 100.0 / (double)(Iterations), Actor.getRecentAverageError());
         str += StringFormat("Critic %.2f%% -> Error %.8f\n", 
                                iter * 100.0 / (double)(Iterations), Critic.getRecentAverageError());
         str += StringFormat("Scheduler %.2f%% -> Error %.8f\n", 
                                iter * 100.0 / (double)(Iterations), Scheduler.getRecentAverageError());
         Comment(str);
         ticks = GetTickCount();
        }
     }

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

   Comment("");
//---
   PrintFormat("%s -> %d -> %10.7f", __FUNCTION__, __LINE__, Actor.getRecentAverageError());
   PrintFormat("%s -> %d -> %10.7f", __FUNCTION__, __LINE__, Critic.getRecentAverageError());
   PrintFormat("%s -> %d -> %10.7f", __FUNCTION__, __LINE__, Scheduler.getRecentAverageError());
   ExpertRemove();
//---
  }

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

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

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

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

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

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


3. Тестирование

Результаты обучения модели на исторических данных EURUSD таймфрейма H1 за первые 4 месяца 2023 года показали, что модель способна генерировать прибыль как на обучающей выборке, так и вне её. Было проведено более 10 итераций сбора примеров и обучения модели, включая от 8 до 24 проходов оптимизации в каждой итерации. Всего было собрано более 200 проходов, а процесс обучения включал от 100 000 до 10 000 000 итераций.

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

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

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

Результаты обучения Результаты обучения


Заключение

Можно подчеркнуть эффективность метода Scheduled Auxiliary Control (SAC-X) в области обучения моделей агентов намерения для финансовых рынков. SAC-X представляет собой развитие классического подхода к обучению с подкреплением, учитывающего специфику финансовых данных и требования торговых стратегий.

Одной из главных особенностей SAC-X является использование нескольких моделей (Актера, Критика, Планировщика) для оценки состояния системы и принятия решений. Это позволяет учесть различные аспекты торговли и создать более гибкую и адаптивную политику агента.

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

Тестирование SAC-X на исторических данных EURUSD показало его способность генерировать прибыль как на обучающей выборке, так и вне её. Однако, необходимо отметить, что в некоторых случаях были обнаружены убыточные зоны на графике баланса, что может указывать на потребность в дополнительном обучении модели.

В целом, метод Scheduled Auxiliary Control (SAC-X) представляет собой мощный инструмент для обучения моделей агентов намерения в финансовой сфере. Он учитывает специфику рыночных данных, позволяет создавать адаптивные и гибкие торговые стратегии, и демонстрирует потенциал для достижения стабильной и прибыльной торговли. Дальнейшие исследования и улучшения SAC-X могут привести к еще более высоким результатам и расширению его применения в финансовых рынках.…


Ссылки

  • Learning by Playing – Solving Sparse Reward Tasks from Scratch
  • Нейросети — это просто (Часть 29): Алгоритм актер-критик с преимуществом (Advantage actor-critic)
  • Нейросети — это просто (Часть 35): Модуль внутреннего любопытства (Intrinsic Curiosity Module)
  • Нейросети — это просто (Часть 36): Реляционные модели обучения с подкреплением (Relational Reinforcement Learning)
  • Нейросети — это просто (Часть 37): Разреженное внимание (Sparse Attention)
  • Нейросети — это просто (Часть 38): Исследование с самоконтролем через несогласие (Self-Supervised Exploration via Disagreement)
  • Нейросети — это просто (Часть 39): Go-Explore — иной подход к исследованию
  • Нейросети — это просто (Часть 40): Подходы к использованию Go-Explore на большом объеме данных


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

    # Имя Тип Описание
    1 Research.mq5 Советник Советник сбора примеров
    2 Study.mql5 Советник Советник обучения моделей
    3 Test.mq5 Советник Советник для тестирования модели
    4 Trajectory.mqh Библиотека класса Структура описания состояния системы
    5 FQF.mqh Библиотека класса Библиотека класса организации работы полностью параметризированной модели
    6 NeuroNet.mqh Библиотека класса Библиотека классов для создания нейронной сети
    7 NeuroNet.cl Библиотека Библиотека кода программы OpenCL

    Прикрепленные файлы |
    MQL5.zip (220.03 KB)
    Последние комментарии | Перейти к обсуждению на форуме трейдеров (4)
    star-ik
    star-ik | 14 мая 2023 в 09:18

    Советник открыл бай и доливал на каждой свече. Где то я уже такое видел.

    У Actor ошибка отрицательная или это просто дефис?

    star-ik
    star-ik | 15 мая 2023 в 13:00
    Уважаемый   Dmitriy Gizlyk. Вы когда то обещали нам перевести на многопоточность Fractal_LSTM. Будьте так добры, найдите время. Я на том уровне еще что то понимаю, дальше уже полный аут. А чисто механически в этом деле вряд ли что получится. Думаю, многие из здесь присутствующих будут вам благодарны. Как никак это форум совсем не программистов.
    Dmitriy Gizlyk
    Dmitriy Gizlyk | 15 мая 2023 в 18:04
    star-ik #:
    Уважаемый   Dmitriy Gizlyk. Вы когда то обещали нам перевести на многопоточность Fractal_LSTM. Будьте так добры, найдите время. Я на том уровне еще что то понимаю, дальше уже полный аут. А чисто механически в этом деле вряд ли что получится. Думаю, многие из здесь присутствующих будут вам благодарны. Как никак это форум совсем не программистов.

    LSTM слой в реализации OpenCL описан в статье "Нейросети — это просто (Часть 22): Обучение без учителя рекуррентных моделей"

    star-ik
    star-ik | 15 мая 2023 в 20:43
    Dmitriy Gizlyk #:

    LSTM слой в реализации OpenCL описан в статье "Нейросети — это просто (Часть 22): Обучение без учителя рекуррентных моделей"

    Это один из тех советников, которых я не сумел заставить торговать. Потому и хотелось бы увидеть многопоточность именно в советнике из 4 части (обучение с учителем). Или этот (22), но снабженный какой-никакой торговой функцией.

    Теория категорий в MQL5 (Часть 5): Эквалайзеры Теория категорий в MQL5 (Часть 5): Эквалайзеры
    Теория категорий представляет собой разнообразный и расширяющийся раздел математики, который лишь недавно начал освещаться в MQL5-сообществе. Эта серия статей призвана рассмотреть некоторые из ее концепций для создания открытой библиотеки и дальнейшему использованию этого замечательного раздела в создании торговых стратегий.
    Оборачиваем ONNX-модели в классы Оборачиваем ONNX-модели в классы
    Объектно-ориентированное программирование позволяет создавать более компактный код, который легко читать и модифицировать. Представляем пример для трёх ONNX-моделей.
    Поиск свечных паттернов с помощью MQL5 Поиск свечных паттернов с помощью MQL5
    В этой статье мы поговорим о том, как автоматически определять свечные паттерны с помощью MQL5.
    Понимание и эффективное использование OpenCL API путем воссоздания встроенной поддержки в виде DLL в Linux (Часть 2): Реализация OpenCL Simple DLL Понимание и эффективное использование OpenCL API путем воссоздания встроенной поддержки в виде DLL в Linux (Часть 2): Реализация OpenCL Simple DLL
    В продолжение первой части создадим простую DLL и протестируем ее с помощью MetaTrader 5. Это хорошо подготовит нас к разработке полноценной поддержки OpenCL в виде DLL в следующей части.