preview
Нейросети в трейдинге: Безмасочный подход к прогнозированию ценового движения

Нейросети в трейдинге: Безмасочный подход к прогнозированию ценового движения

MetaTrader 5Торговые системы | 25 сентября 2024, 12:29
153 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Введение

Мы продолжаем изучение методом обработки облака точек. В предыдущей статье мы познакомились с методом SPFormer. Его авторы разработали комплексный алгоритм, основанный на архитектуре Transformer. Несколько слоев декодера Transformer используют фиксированное количество запросов к объектам, что позволяет итеративно обрабатывать глобальные функции и напрямую выводит прогнозы объектов. SPFormer не требует постобработки для удаления дубликатов так как во время обучения используется двустороннее сопоставление «один к одному». При этом маски объектов, определенные в последнем слое, используются для направления перекрестного внимания.

Однако авторы работы "Mask-Attention-Free Transformer for 3D Instance Segmentation" отмечают, что современные методы, основанные на Transformer, страдают от проблемы медленной сходимости. В процессе анализа базового метода они обнаружили, что проблема может быть вызвана низким уровнем исходных масок. В частности, исходные маски объектов создаются путем отображения сходства между исходными запросами объектов и функциями маски по точкам. Некачественные маски начальных экземпляров увеличивают сложность обучения, тем самым замедляя сходимость.

Учитывая низкую полноту исходных масок, авторы указанной работы предлагают новый алгоритм Mask-Attention-Free Transformer (MATF), в котором отказываются от дизайна маски внимания и вместо этого конструируют вспомогательную задачу регрессии центра для управления перекрестным вниманием. Чтобы обеспечить регрессию центра, была разработана целая серия конструкций с учетом позиций точек. Во-первых, авторы метода добавляют набор обучаемых позиционных запросов, каждый из которых обозначает позицию соответствующего запроса контента. Позиции запросов плотно распределены по изучаемому пространству. И авторы метода вводят ограничение, чтобы каждый запрос учитывал свой локальный регион. В результате запросы могут легко захватывать объекты в сцене с более высоким уровнем запоминаемости, что имеет решающее значение для снижения сложности обучения и ускорения сходимости.

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

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


1. Алгоритм MATF

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

Поэтому авторы метода MAFT внедряют вспомогательную задачу регрессии центра для управления сегментацией экземпляров. Сначала выбираются глобальные позиции 𝒫 из исходного облака точек и извлекаются глобальные признаки объектов ℱ с помощью базовой магистрали. Это могут быть как воксели, так и Superpoints. Помимо контентных запросов 𝒬0c, авторы MAFT вводят фиксированное количество позиционных запросов𝒬0p, которые представляют собой нормализованные центры объектов. 𝒬0p инициализируется случайным образом, а 𝒬0c — нулевыми значениями. Основная цель заключается в том, чтобы позволить позиционным запросам направлять соответствующие контекстуальные запросы в перекрестном внимании, а затем итеративно уточнить оба набора запросов и спрогнозировать центры объектов, их классы и маски.

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

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

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

Помимо кодирования абсолютного положения, в MAFT применяется контекстуальное кодирование относительного положения при перекрестном внимании. Для этого сначала вычисляются взаимные позиции 𝐫 между позиционными запросами 𝒬tp и глобальными позициями 𝒫 с последующим квантованием до дискретных целых чисел 𝐫'. Затем дискретные взаимные позиции используются в качестве индексов для поиска соответствующих значений в таблице кодирования позиций.

Далее, кодирование относительного положения 𝐟pos умножается на функцию Query 𝐟q или Key признаков 𝐟k в перекрестном внимании. Затем она добавляется к весам перекрестного внимания, за которыми следует функция SoftMax.

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

Поскольку контекстные запросы в слоях декодера регулярно обновляются, не оптимально поддерживать замороженные позиционные запросы на протяжении всего процесса декодирования. Кроме того, начальные запросы к позициям являются статическими, поэтому в последующих слоях их полезно адаптировать к конкретной исходной сцене. Для этого авторы метода итеративно уточняют позиционные запросы на основе контентных запросов. В частности, они используют MLP для прогнозирования смещения центра Δpt из обновленного контекстного запроса 𝒬t+1c. Затем полученное смещение добавляется к предыдущему позиционному запросу 𝒬tp.

Авторская визуализация метода MAFT представлена ниже.


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

После рассмотрения теоретических аспектов метода Mask-Attention-Free Transformer мы переходим к практической части нашей статьи, в которой реализуем свое видение предложенных подходов средствами MQL5. И начнем мы работу с внесения дополнений в программу OpenCL.

2.1 Дополнение OpenCL-программы


Начнем мы свою работу с построения алгоритма относительного позиционного кодирования. С одной стороны, алгоритм довольно прост. Нам лишь необходимо найти расстояние между 2 точками. Тем более, авторы метода вычисляют расстояние по каждой отдельной координате. С другой стороны, авторы MAFT осуществляют квантование полученных отклонений, которые затем используют для поиска значений в обучаемой таблице параметров. Мы решили немного оптимизировать предложенное решение. И наш вариант реализации основывается на предположении, что наибольшее  влияние оказывают точки, находящиеся в непосредственной близости от анализируемого запроса. Руководствуясь этой логикой, мы сначала определяем расстояние S между двумя точками в N-мерном пространстве. А затем вычисляем коэффициент позиционного смещение kpb по формуле:

Очевидно, что расстояние между двумя точками всегда будет не меньше 0. При совпадении точек, коэффициент будет равен 1. С ростом расстояния между точками, коэффициент относительного позиционного кодирования стремится к 0.

Реализация предложенного алгоритма приведена в кернеле CalcPositionBias. В параметрах кернела мы передаем указатели на 3 глобальных буфера данных, 2 из которых содержат исходные данные. А третий предназначен для записи результатов. Здесь мы так же добавим размерность вектора описания одного элемента.

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

__kernel void CalcPositionBias(__global const float *data1,
                               __global const float *data2,
                               __global float *result,
                               const int dimension
                              )
  {
   const size_t idx1 = get_global_id(0);
   const size_t idx2 = get_global_id(1);
   const size_t total1 = get_global_size(0);
   const size_t total2 = get_global_size(1);

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

На следующем шаге мы определяем смещение в буферах данных.

   const int shift1 = idx1 * dimension;
   const int shift2 = idx2 * dimension;
   const int shift_out = idx1 * total2 + idx2;

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

   float res = 0;
   for(int i = 0; i < dimension; i++)
      res = pow(data1[shift1 + i] - data2[shift2 + i], 2.0f);
   res = sqrt(res);

А затем вычисляем коэффициент относительного смещения.

   res = 1.0f / exp(res);
   if(isnan(res) || isinf(res))
      res = 0;
//---
   result[shift_out] = res;
  }

И записываем полученное значение в соответствующий элемент буфера результатов.

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

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

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

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

__kernel void MHPosBiasAttentionOut(__global const float *q,         ///<[in] Matrix of Querys
                                    __global const float *k,         ///<[in] Matrix of Keys
                                    __global const float *v,         ///<[in] Matrix of Values
                                    __global float *score,           ///<[out] Matrix of Scores
                                    __global const float *pos_bias,  ///<[in] Position Bias
                                    __global float *out,             ///<[out] Matrix of attention
                                    const int dimension,             ///< Dimension of Key
                                    const int heads_kv,
                                    const int use_pos_bias
                                   )
  {
//---
   const int q_id = get_global_id(0);
   const int k_id = get_global_id(1);
   const int h = get_global_id(2);
   const int qunits = get_global_size(0);
   const int kunits = get_global_size(1);
   const int heads = get_global_size(2);

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

Далее мы определяем все необходимые константы.

   const int h_kv = h % heads_kv;
   const int shift_q = dimension * (q_id * heads + h);
   const int shift_kv = dimension * (heads_kv * k_id + h_kv);
   const int shift_s = kunits * (q_id *  heads + h) + k_id;
   const int shift_pb = q_id * kunits + k_id;
   const uint ls = min((uint)get_local_size(1), (uint)LOCAL_ARRAY_SIZE);
   float koef = sqrt((float)dimension);
   if(koef < 1)
      koef = 1;

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

   __local float temp[LOCAL_ARRAY_SIZE];

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

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

//--- sum of exp
   uint count = 0;
   if(k_id < ls)
     {
      temp[k_id] = 0;
      do
        {
         if(q_id >= (count * ls + k_id))
            if((count * ls) < (kunits - k_id))
              {
               float sum = 0;
               int sh_k = dimension * heads_kv * count * ls;
               for(int d = 0; d < dimension; d++)
                  sum = q[shift_q + d] * k[shift_kv + d + sh_k];
               sum = exp(sum / koef);
               if(isnan(sum))
                  sum = 0;
               temp[k_id] = temp[k_id] + sum + (use_pos_bias > 0 ? pos_bias[shift_pb + count * ls] : 0);
              }
         count++;
        }
      while((count * ls + k_id) < kunits);
     }
   barrier(CLK_LOCAL_MEM_FENCE);

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

Далее мы суммируем значение элементов локального массива.

   count = min(ls, (uint)kunits);
//---
   do
     {
      count = (count + 1) / 2;
      if(k_id < ls)
         temp[k_id] += (k_id < count && (k_id + count) < kunits ? temp[k_id + count] : 0);
      if(k_id + count < ls)
         temp[k_id + count] = 0;
      barrier(CLK_LOCAL_MEM_FENCE);
     }
   while(count > 1);

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

//--- score
   float sum = temp[0];
   float sc = 0;
   if(q_id >= (count * ls + k_id))
      if(sum != 0)
        {
         for(int d = 0; d < dimension; d++)
            sc = q[shift_q + d] * k[shift_kv + d];
         sc = (exp(sc / koef) + (use_pos_bias > 0 ? pos_bias[shift_pb] : 0)) / sum;
         if(isnan(sc))
            sc = 0;
        }
   score[shift_s] = sc;
   barrier(CLK_LOCAL_MEM_FENCE);

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

//--- out
   for(int d = 0; d < dimension; d++)
     {
      uint count = 0;
      if(k_id < ls)
         do
           {
            if((count * ls) < (kunits - k_id))
              {
               int sh_v = 2 * dimension * heads_kv * count * ls;
               float sum =
                  v[shift_kv + d + sh_v] * (count == 0 ? sc : score[shift_s + count * ls]);
               if(isnan(sum))
                  sum = 0;
               temp[k_id] = (count > 0 ? temp[k_id] : 0) + sum;
              }
            count++;
           }
         while((count * ls + k_id) < kunits);
      barrier(CLK_LOCAL_MEM_FENCE);
      //---
      count = min(ls, (uint)kunits);
      do
        {
         count = (count + 1) / 2;
         if(k_id < ls)
            temp[k_id] += (k_id < count && (k_id + count) < kunits ? temp[k_id + count] : 0);
         if(k_id + count < ls)
            temp[k_id + count] = 0;
         barrier(CLK_LOCAL_MEM_FENCE);
        }
      while(count > 1);
      //---
      out[shift_q + d] = temp[0];
     }
  }

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

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

2.2 Создание класса MAFT


Следующим этапом нашей работы является создание нового объекта, в коде которого мы имплементируем свое видение подходов, предложенных авторами метода Mask-Attention-Free Transformer. С этой целью мы создаем новый класс CNeuronMAFT.

Надо сказать, что авторы метода MAFT построили свой алгоритм на базе рассмотренного нами ранее SPFormer. И в своей работе мы так же будем использовать наработки класса CNeuronSPFormer. Однако масштаб изменений нивелирует преимущества наследования от указанного класса. Поэтому мы создаем наш новый объект наследником от базового класса полносвязного слоя CNeuronBaseOCL. Структура нового класса представлена ниже.

class CNeuronMAFT   : public CNeuronBaseOCL
  {
protected:
   uint              iWindow;
   uint              iUnits;
   uint              iHeads;
   uint              iSPWindow;
   uint              iSPUnits;
   uint              iSPHeads;
   uint              iWindowKey;
   uint              iLayers;
   uint              iLayersSP;
   //---
   CLayer            cSuperPoints;
   CLayer            cQuery;
   CLayer            cQPosition;
   CLayer            cQKey;
   CLayer            cQValue;
   CLayer            cMHSelfAttentionOut;
   CLayer            cSelfAttentionOut;
   CLayer            cSPKey;
   CLayer            cSPValue;
   CArrayInt         cScores;
   CArrayInt         cPositionBias;
   CLayer            cMHCrossAttentionOut;
   CLayer            cCrossAttentionOut;
   CLayer            cResidual;
   CLayer            cFeedForward;
   CBufferFloat      cTempSP;
   CBufferFloat      cTempQ;
   CBufferFloat      cTempCrossK;
   CBufferFloat      cTempCrossV;
   //---
   virtual bool      CreateBuffers(void);
   virtual bool      CalcPositionBias(CBufferFloat *pos_q, CBufferFloat *pos_k, const int pos_bias,
                                      const int units,
                                      const int units_kv,
                                      const int dimension);
   virtual bool      AttentionOut(CNeuronBaseOCL *q, CNeuronBaseOCL *k, CNeuronBaseOCL *v,
                                  const int scores, CNeuronBaseOCL *out, const int pos_bias,
                                  const int units,
                                  const int heads,
                                  const int units_kv,
                                  const int heads_kv,
                                  const int dimension,
                                  const bool use_pos_bias);
   virtual bool      AttentionInsideGradients(CNeuronBaseOCL *q, CNeuronBaseOCL *k, CNeuronBaseOCL *v,
         const int scores, CNeuronBaseOCL *out,
         const int units, const int heads,
         const int units_kv, const int heads_kv,
         const int dimension);
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   //---
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronMAFT(void) {};
                    ~CNeuronMAFT(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint window_key, uint units_count, uint heads,
                          uint window_sp, uint units_sp, uint heads_sp,
                          uint layers, uint layers_to_sp,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override   const   {  return defNeuronMAFT; }
   //---
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      SetOpenCL(COpenCLMy *obj) override;
  };

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

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

bool CNeuronMAFT::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                       uint window, uint window_key, uint units_count,
                       uint heads, uint window_sp, uint units_sp, uint heads_sp,
                       uint layers, uint layers_to_sp,
                       ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch))
      return false;

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

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

   iWindow = window;
   iUnits = units_count;
   iHeads = heads;
   iSPUnits = units_sp;
   iSPWindow = window_sp;
   iSPHeads = heads_sp;
   iWindowKey = window_key;
   iLayers = MathMax(layers, 1);
   iLayersSP = MathMax(layers_to_sp, 1);

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

   CNeuronBaseOCL *base = new CNeuronBaseOCL();
   if(!base)
      return false;
   if(!base.Init(iWindow * iUnits, 0, OpenCL, 1, optimization, iBatch))
      return false;
   CBufferFloat *buf = base.getOutput();
   if(!buf || !buf.BufferInit(1, 1) || !buf.BufferWrite())
      return false;
   buf = base.getWeights();
   if(!buf || !buf.BufferInit(buf.Total(), 0) ||
      !buf.BufferWrite())
      return false;
   if(!cQuery.Add(base))
      return false;
   base = new CNeuronBaseOCL();
   if(!base || !base.Init(0, 1, OpenCL, iWindow * iUnits, optimization, iBatch))
      return false;
   if(!cQuery.Add(base))
      return false;

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

   CNeuronLearnabledPE *pe = new CNeuronLearnabledPE();
   if(!pe || !pe.Init(0, 2, OpenCL, base.Neurons(), optimization, iBatch))
      return false;
   if(!cQuery.Add(pe))
      return false;

Надо сказать, что позиционное кодирование идет отдельным потоком информации через весь алгоритм MAFT. Поэтому мы выделим его отдельными объектами.

   if(!base || !base.Init(0, 3, OpenCL, pe.Neurons(), optimization, iBatch))
      return false;
   if(!base.SetOutput(pe.getOutput()))
      return false;
   if(!cQPosition.Add(base))
      return false;

Следующий этап — первичная обработка данных. И здесь мы заимствуем подход Superpoints, который был представлен в методе SPFormer.

//--- Init SuperPoints
   int layer_id = 4;
   for(int r = 0; r < 4; r++)
     {
      if(iSPUnits % 2 == 0)
        {
         iSPUnits /= 2;
         CResidualConv *residual = new CResidualConv();
         if(!residual)
            return false;
         if(!residual.Init(0, layer_id, OpenCL, 2 * iSPWindow, iSPWindow, iSPUnits, optimization, iBatch))
            return false;
         if(!cSuperPoints.Add(residual))
            return false;
        }
      else
        {
         iSPUnits--;
         CNeuronConvOCL *conv = new CNeuronConvOCL();
         if(!conv.Init(0, layer_id, OpenCL, 2 * iSPWindow, iSPWindow, iSPWindow, iSPUnits, 1,
                                                                        optimization, iBatch))
            return false;
         if(!cSuperPoints.Add(conv))
            return false;
        }
      layer_id++;
     }

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

   CNeuronConvOCL *conv = new CNeuronConvOCL();
   if(!conv.Init(0, layer_id, OpenCL, iSPWindow, iSPWindow, iWindow, iSPUnits, 1, optimization, iBatch))
      return false;
   if(!cSuperPoints.Add(conv))
      return false;
   layer_id++;

И добавляем слой позиционного кодирования.

   pe = new CNeuronLearnabledPE();
   if(!pe || !pe.Init(0, layer_id, OpenCL, conv.Neurons(), optimization, iBatch))
      return false;
   if(!cSuperPoints.Add(pe))
      return false;
   layer_id++;

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

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

//--- Inside layers
   for(uint l = 0; l < iLayers; l++)
     {
      //--- Self-Attention
      //--- Query
      conv = new CNeuronConvOCL();
      if(!conv || !conv.Init(0, layer_id, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, 1, 
                                                                              optimization, iBatch))
         return false;
      if(!cQuery.Add(conv))
         return false;
      layer_id++;

И здесь стоит заметить, что авторы MAFT используют классическую компоновку: Sefl-Attention -> Cross-Attention -> Feed Forward. В то время как авторы метода SPFormer переставили местами Self-Attention и Cross-Attention.

Вначале мы генерируем сущности Query. А затем добавляем Key и Value.

      //--- Key
      conv = new CNeuronConvOCL();
      if(!conv || !conv.Init(0, layer_id, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, 1, 
                                                                              optimization, iBatch))
         return false;
      if(!cQKey.Add(conv))
         return false;
      layer_id++;
      //--- Value
      conv = new CNeuronConvOCL();
      if(!conv || !conv.Init(0, layer_id, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, 1, 
                                                                               optimization, iBatch))
         return false;
      if(!cQValue.Add(conv))
         return false;
      layer_id++;

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

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

      //--- Multy-Heads Attention Out
      base = new CNeuronBaseOCL();
      if(!base || !base.Init(0, layer_id, OpenCL, iWindowKey * iHeads * iUnits, optimization, iBatch))
         return false;
      if(!cMHSelfAttentionOut.Add(base))
         return false;
      layer_id++;

И добавляем слой масштабирования результатов многоголового внимания.

      //--- Self-Attention Out
      conv = new CNeuronConvOCL();
      if(!conv || !conv.Init(0, layer_id, OpenCL, iWindowKey * iHeads, iWindowKey * iHeads, iWindow,
                                                                     iUnits, 1, optimization, iBatch))
         return false;
      if(!cSelfAttentionOut.Add(conv))
         return false;
      layer_id++;

И в завершении блока Self-Attention, следуя алгоритму классического Transformer, мы добавляем слой остаточных связей.

      //--- Residual
      base = new CNeuronBaseOCL();
      if(!base || !base.Init(0, layer_id, OpenCL, iWindow * iUnits, optimization, iBatch))
         return false;
      if(!cResidual.Add(base))
         return false;
      layer_id++;

Далее мы выстраиваем объекты блока кросс-внимания. И первым, как обычно, создаем тензор сущности Query.

      //--- Cross-Attention
      //--- Query
      conv = new CNeuronConvOCL();
      if(!conv || !conv.Init(0, layer_id, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, 1,
                                                                              optimization, iBatch))
         return false;
      if(!cQuery.Add(conv))
         return false;
      layer_id++;

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

      if(l % iLayersSP == 0)
        {
         //--- Key
         conv = new CNeuronConvOCL();
         if(!conv || !conv.Init(0, layer_id, OpenCL, iWindow, iWindow, iWindowKey * iSPHeads, iSPUnits, 1,
                                                                                     optimization, iBatch))
            return false;
         if(!cSPKey.Add(conv))
            return false;
         layer_id++;
         //--- Value
         conv = new CNeuronConvOCL();
         if(!conv || !conv.Init(0, layer_id, OpenCL, iWindow, iWindow, iWindowKey * iSPHeads, iSPUnits, 1, 
                                                                                     optimization, iBatch))
            return false;
         if(!cSPValue.Add(conv))
            return false;
         layer_id++;
        }

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

      //--- Multy-Heads Attention Out
      base = new CNeuronBaseOCL();
      if(!base || !base.Init(0, layer_id, OpenCL, iWindowKey * iHeads * iUnits, optimization, iBatch))
         return false;
      if(!cMHCrossAttentionOut.Add(base))
         return false;
      layer_id++;

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

      //--- Cross-Attention Out
      conv = new CNeuronConvOCL();
      if(!conv || !conv.Init(0, layer_id, OpenCL, iWindowKey * iHeads, iWindowKey * iHeads, iWindow,
                                                                    iUnits, 1, optimization, iBatch))
         return false;
      if(!cCrossAttentionOut.Add(conv))
         return false;
      layer_id++;
      //--- Residual
      base = new CNeuronBaseOCL();
      if(!base || !base.Init(0, layer_id, OpenCL, iWindow * iUnits, optimization, iBatch))
         return false;
      if(!cResidual.Add(base))
         return false;
      layer_id++;

А завершает Декодер блок FeedForward, к которому мы так же добавляем остаточные связи.

      //--- Feed Forward
      conv = new CNeuronConvOCL();
      if(!conv || !conv.Init(0, layer_id, OpenCL, iWindow, iWindow, 4 * iWindow, iUnits, 1, 
                                                                         optimization, iBatch))
         return false;
      conv.SetActivationFunction(LReLU);
      if(!cFeedForward.Add(conv))
         return false;
      layer_id++;
      conv = new CNeuronConvOCL();
      if(!conv || !conv.Init(0, layer_id, OpenCL, 4 * iWindow, 4 * iWindow, iWindow, iUnits, 1, 
                                                                         optimization, iBatch))
         return false;
      if(!cFeedForward.Add(conv))
         return false;
      layer_id++;
      //--- Residual
      base = new CNeuronBaseOCL();
      if(!base || !base.Init(0, layer_id, OpenCL, iWindow * iUnits, optimization, iBatch))
         return false;
      if(!base.SetGradient(conv.getGradient()))
         return false;
      if(!cResidual.Add(base))
         return false;
      layer_id++;

Остается добавить MLP корректировки позиционного кодирования обучаемых запросов.

      //--- Delta position
      conv = new CNeuronConvOCL();
      if(!conv || !conv.Init(0, layer_id, OpenCL, iWindow, iWindow, iWindow, iUnits, 1, optimization, iBatch))
         return false;
      conv.SetActivationFunction(SIGMOID);
      if(!cQPosition.Add(conv))
         return false;
      layer_id++;
      base = new CNeuronBaseOCL();
      if(!base || !base.Init(0, layer_id, OpenCL, conv.Neurons(), optimization, iBatch))
         return false;
      if(!base.SetGradient(conv.getGradient()))
         return false;
      if(!cQPosition.Add(base))
         return false;
      layer_id++;
     }

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

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

   base = cResidual[iLayers * 3 - 1];
   if(!SetGradient(base.getGradient()))
      return false;
//---
   SetOpenCL(OpenCL);
//---
   return true;
  }

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

Напомню, что полный код данного класса и всех его методов вы можете найти во вложении.

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

bool CNeuronMAFT::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
//--- Superpoints
   CNeuronBaseOCL *superpoints = NeuronOCL;
   int total_sp = cSuperPoints.Total();
   for(int i = 0; i < total_sp; i++)
     {
      if(!cSuperPoints[i] ||
         !((CNeuronBaseOCL*)cSuperPoints[i]).FeedForward(superpoints))
         return false;
      superpoints = cSuperPoints[i];
     }

И сразу используем полученный объект в процессе генерации признаков суперточек. Для этого мы воспользуемся вложенной моделью cSuperPoints.

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

Далее мы генерируем обучаемые запросы с позиционным кодированием.

//--- Query
   CNeuronBaseOCL *inputs = NULL;
   for(int i = 0; i < 2; i++)
     {
      inputs = cQuery[i + 1];
      if(!inputs ||
         !inputs.FeedForward(cQuery[i]))
         return false;
     }

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

   CNeuronBaseOCL *query = NULL, *key = NULL, *value = NULL, *base = NULL;

И организуем цикл перебора внутренних слоев Декодера.

//--- Inside layers
   for(uint l = 0; l < iLayers; l++)
     {
      //--- Self-Atention
      query = cQuery[l * 2 + 3];
      if(!query || !query.FeedForward(inputs))
         return false;
      key = cQKey[l];
      if(!key || !key.FeedForward(inputs))
         return false;
      value = cQValue[l];
      if(!value || !value.FeedForward(inputs))
         return false;

Здесь мы сначала организовываем работу блока Self-Attention набор обучаемых запросов с позиционным кодированием. Для этого мы сначала сгенерируем необходимые сущности, которые передадим в блок многоголового внимания.

      if(!AttentionOut(query, key, value, cScores[l * 2], cMHSelfAttentionOut[l], -1, 
                                   iUnits, iHeads, iUnits, iHeads, iWindowKey, false))
         return false;

А затем масштабируем полученные результаты и добавим остаточные связи.

      base = cSelfAttentionOut[l];
      if(!base || !base.FeedForward(cMHSelfAttentionOut[l]))
         return false;
      value = cResidual[l * 3];
      if(!value ||
         !SumAndNormilize(inputs.getOutput(), base.getOutput(), value.getOutput(), iWindow, true, 0, 0, 0, 1))
         return false;
      inputs = value;

Отдельным потоком добавим позиционное кодирование.

      value = cQPosition[l * 2];
      if(!value ||
         !SumAndNormilize(inputs.getOutput(), value.getOutput(),inputs.getOutput(), iWindow, false, 0, 0, 0, 1))
         return false;

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

      //--- Calc Position bias
      if(!CalcPositionBias(value.getOutput(),
                           ((CNeuronLearnabledPE*)superpoints).GetPE(), cPositionBias[l],
                           iUnits, iSPUnits, iWindow))
         return false;

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

      //--- Cross-Attention
      query = cQuery[l * 2 + 4];
      if(!query || !query.FeedForward(inputs))
         return false;

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

      key = cSPKey[l / iLayersSP];
      value = cSPValue[l / iLayersSP];
      if(l % iLayersSP == 0)
        {
         if(!key || !key.FeedForward(superpoints))
            return false;
         if(!value || !value.FeedForward(cSuperPoints[total_sp - 2]))
            return false;
        }

А во-вторых, Key генерируется на данных последнего слоя cSuperPoints, который содержит позиционное кодирование. А вот для генерации Value мы используем предпоследний слой, в котором отсутствует позиционное кодирование.

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

      if(!AttentionOut(query, key, value, cScores[l * 2 + 1], cMHCrossAttentionOut[l], cPositionBias[l], 
                                                  iUnits, iHeads, iSPUnits, iSPHeads, iWindowKey, true))
         return false;

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

      base = cCrossAttentionOut[l];
      if(!base || !base.FeedForward(cMHCrossAttentionOut[l]))
         return false;
      value = cResidual[l * 3 + 1];
      if(!value ||
         !SumAndNormilize(inputs.getOutput(), base.getOutput(), value.getOutput(), iWindow, true, 0, 0, 0, 1))
         return false;
      inputs = value;

В завершении Декодера мы проводим данные через блок FeedForward с последующими остаточными связями.

      //--- Feed Forward
      base = cFeedForward[l * 2];
      if(!base || !base.FeedForward(inputs))
         return false;
      base = cFeedForward[l * 2 + 1];
      if(!base || !base.FeedForward(cFeedForward[l * 2]))
         return false;
      value = cResidual[l * 3 + 2];
      if(!value ||
         !SumAndNormilize(inputs.getOutput(), base.getOutput(), value.getOutput(), iWindow, true, 0, 0, 0, 1))
         return false;
      inputs = value;

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

      //--- Delta Query position
      base = cQPosition[l * 2 + 1];
      if(!base ||
         !base.FeedForward(inputs))
         return false;
      value = cQPosition[(l + 1) * 2];
      query = cQPosition[l * 2];
      if(!value ||
         !SumAndNormilize(query.getOutput(), base.getOutput(), value.getOutput(), iWindow, false, 0,0,0,0.5f))
         return false;
     }

И теперь мы можем перейти к выполнению операций следующего внутреннего слоя Декодера.

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

   value = cQPosition[iLayers * 2];
   if(!value ||
      !SumAndNormilize(inputs.getOutput(), value.getOutput(), Output, iWindow, true, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

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

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

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

Напомню, что полный код данного класса и всех его методов вы можете найти во вложении.

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


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

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

Для обучения модели мы используем реальные исторические данные инструмента EURUSD за весь 2023 год на таймфрейме H1. Все параметры анализируемых индикаторов используются по умолчанию.

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

Тестирование обученной политики Актера проводится в тестере стратегий MetaTrader 5 на исторических данных Января 2024 года, при этом остальные параметры сохраняются неизменными. Результаты тестирования приведены ниже.

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


Заключение

В данной статье мы познакомились с методом Mask-Attention-Free Transformer (MAFT) и его применением в алгоритмическом трейдинге. В отличие от классических Transformer, MAFT демонстрирует высокую вычислительную эффективность, устраняя необходимость в маскировании данных и ускоряя обработку последовательностей.

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

Ссылки

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

#ИмяТипОписание
1Research.mq5СоветникСоветник сбора примеров
2ResearchRealORL.mq5
Советник
Советник сбора примеров методом Real-ORL
3Study.mq5СоветникСоветник обучения Моделей
4Test.mq5СоветникСоветник для тестирования модели
5Trajectory.mqhБиблиотека классаСтруктура описания состояния системы
6NeuroNet.mqhБиблиотека классаБиблиотека классов для создания нейронной сети
7NeuroNet.clБиблиотекаБиблиотека кода программы OpenCL
Прикрепленные файлы |
MQL5.zip (1878.59 KB)
Оптимизация атмосферными облаками — Atmosphere Clouds Model Optimization (ACMO): Практика Оптимизация атмосферными облаками — Atmosphere Clouds Model Optimization (ACMO): Практика
В данной статье мы продолжим погружение в реализацию алгоритма ACMO (Atmospheric Cloud Model Optimization). В частности, обсудим два ключевых аспекта: перемещение облаков в регионы с низким давлением и моделирование процесса дождя, включая инициализацию капель и распределение их между облаками. Мы также разберем другие методы, которые играют важную роль в управлении состоянием облаков и обеспечении их взаимодействия с окружающей средой.
Оптимизация атмосферными облаками — Atmosphere Clouds Model Optimization (ACMO): Теория Оптимизация атмосферными облаками — Atmosphere Clouds Model Optimization (ACMO): Теория
Статья посвящена метаэвристическому алгоритму Atmosphere Clouds Model Optimization (ACMO), который моделирует поведение облаков для решения задач оптимизации. Алгоритм использует принципы генерации, движения и распространения облаков, адаптируясь к "погодным условиям" в пространстве решений. Статья раскрывает, как метеорологическая симуляция алгоритма находит оптимальные решения в сложном пространстве возможностей и подробно описывает этапы работы ACMO, включая подготовку "неба", рождение облаков, их перемещение и концентрацию дождя.
Возможности Мастера MQL5, которые вам нужно знать (Часть 19): Байесовский вывод Возможности Мастера MQL5, которые вам нужно знать (Часть 19): Байесовский вывод
Байесовский вывод — это применение теоремы Байеса для обновления вероятностной гипотезы по мере поступления новой информации. Это намекает на необходимость адаптации в анализе временных рядов, и поэтому мы рассмотрим, как мы могли бы использовать его при создании пользовательских классов не только применительно к сигналам, но и для управления капиталом и трейлинг-стопами.
Ложные регрессии в Python Ложные регрессии в Python
Ложные регрессии возникают, когда два временных ряда демонстрируют высокую степень корреляции чисто случайно, что приводит к вводящим в заблуждение результатам регрессионного анализа. В таких случаях, даже если переменные кажутся связанными, корреляция является случайной и модель может быть ненадежной.