preview
Нейросети в трейдинге: Прогнозирование временных рядов при помощи адаптивного модального разложения (Окончание)

Нейросети в трейдинге: Прогнозирование временных рядов при помощи адаптивного модального разложения (Окончание)

MetaTrader 5Торговые системы | 7 мая 2025, 13:53
606 1
Dmitriy Gizlyk
Dmitriy Gizlyk

Введение

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

В этой статье мы продолжим реализацию подходов, предложенных авторами фреймворка ACEFormer. Основное внимание будет уделено построению алгоритмов на стороне основной программы. Но прежде чем перейти к технической реализации, вспомним, в чём заключается суть и сила фреймворка ACEFormer. Его основа — алгоритм ACEEMD (Alias Complete Ensemble Empirical Mode Decomposition with Adaptive Noise), направленный на устранение шумов в финансовых временных рядах. ACEEMD решает проблему эффекта границ и позволяет сохранить ключевые разворотные точки на графике, не теряя важной информации при сглаживании. Отдельное внимание уделяется первой моде (IMF), удаление которой позволяет избавиться от высокочастотного шума без излишнего подавления полезного сигнала.

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

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

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

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

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



Построение объекта вероятностного внимания

После рассмотрения реализации механизмов вероятностного внимания на стороне OpenCL-программы, переходим к следующему этапу — созданию соответствующего высокоуровневого модуля на стороне основной программы. Для этого создаем специализированный объект CNeuronMHProbAttention, в котором организован полный алгоритм вероятностного внимания.

При его создании мы наследуемся от класса CResidualConv, в структуре которого уже реализована архитектура двух последовательных свёрточных слоёв с остаточными связями. Это позволяет нам сосредоточиться исключительно на реализации логики вероятностного внимания, не затрагивая механизмы прямого распространения (Feed-Forward), которые осуществляются средствами родительского класса. Таким образом достигается высокая степень модульности, а также удобство интеграции с другими компонентами модели.

Структура нового класса представлена ниже.

class CNeuronMHProbAttention :  public CResidualConv
  {
protected:
   uint                       iWindow;
   uint                       iWindowKey;
   uint                       iHeads;
   uint                       iUnits;
   uint                       iTopKQuerys;
   uint                       iRandomKeys;
   int                        ibScore;
   //---
   CNeuronConvOCL             cQKV;
   CNeuronBaseOCL             cQ;
   CNeuronBaseOCL             cKV;
   CNeuronBaseOCL             cRandomK;
   CNeuronBaseOCL             cMHAttentionOut;
   CNeuronConvOCL             cPooling;
   CNeuronTransposeOCL        cTranspose[2];
   CNeuronConvOCL             cScaling;
   //---
   virtual bool               RandomKeys(CBufferFloat* indexes, int random, int units, int heads);
   virtual bool               QueryImportance(void);
   virtual bool               TopKIndexes(void);
   //---
   virtual bool               AttentionOut(void);
   virtual bool               AttentionInsideGradients(void);
   //---
   virtual bool               feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool               updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool               calcInputGradients(CNeuronBaseOCL *prevLayer) override;

public:
                              CNeuronMHProbAttention(void) {};
                             ~CNeuronMHProbAttention(void) {};
   //---
   virtual bool               Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                                   uint window, uint window_key, uint heads, uint units_count,
                                   ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int                Type(void)   const   {  return defNeuronMHProbAttention;   }
   //---
   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;
  };

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

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

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

bool CNeuronMHProbAttention::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                  uint window, uint window_key, uint heads, uint units_count,
                                  ENUM_OPTIMIZATION optimization_type, uint batch
                                 )
  {
   if(!CResidualConv::Init(numOutputs, myIndex, open_cl, window, window, units_count, optimization_type, batch))
      return false;

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

Далее, мы переходим к инициализации ключевых параметров, определяющих поведение механизма внимания.

iWindow = window;
iWindowKey = MathMax(5, window_key);
iHeads = MathMax(1, heads);
iUnits = units_count;
iTopKQuerys = int(MathMin(5 * MathMax(MathLog(iUnits),1), iUnits));
iRandomKeys = int(MathMin(5 * MathMax(MathLog(iUnits),1), iUnits));

Здесь, помимо уже знакомых, мы видим 2 новые переменные, относящиеся к механизму вероятностного внимания:

  • iTopKQuerys — количество наиболее значимых Запросов, отбираемых для последующего анализа;
  • iRandomKeys — число случайных Ключей, используемых в процессе отбора наиболее значимых Запросов.

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

После сохранения параметров объекта, начинается поэтапная сборка всех внутренних элементов, отвечающих за реализацию внимания. Каждый объект инициализируется отдельно, в строго заданной последовательности. Первым инициализируем cQKV — сверточный слой, формирующий сразу три сущности: Q (Query), K (Key) и V (Value). Он создаёт плотное представление анализируемых данных для всех голов внимания, кодируя их с помощью функции активации TANH.

int index = 0;
if(!cQKV.Init(0, index, OpenCL, iWindow, iWindow, 3 * iWindowKey * iHeads, iUnits, optimization, iBatch))
   return false;
cQKV.SetActivationFunction(TANH);

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

index++;
if(!cQ.Init(0, index, OpenCL, cQKV.Neurons() / 3, optimization, iBatch))
   return false;
index++;
if(!cKV.Init(0, index, OpenCL, 2 * cQ.Neurons(), optimization, iBatch))
   return false;

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

index++;
if(!cRandomK.Init(0, index, OpenCL, iHeads * MathMax(iRandomKeys, iTopKQuerys), optimization, iBatch))
   return false;

Обратите внимание, что размер слоя определяется по максимальному значению из сэмплированных Ключей и наиболее важных Запросов. Идея заключается в том, чтобы гарантировать достаточный объём памяти под все возможные индексы, которые могут потребоваться в процессе вычислений. Мы заранее не знаем, какая из двух величин окажется больше — количество случайных Ключей (iRandomKeys) или наиболее значимых Запросов (iTopKQuerys), поэтому используем максимум из двух значений. А затем, масштабируем результат на число голов внимания (iHeads), так как каждая голова работает независимо и нуждается в собственном наборе индексов.

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

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

index++;
if(!cMHAttentionOut.Init(0, index, OpenCL, iTopKQuerys * iHeads * iWindowKey, optimization, iBatch))
   return false;

А за ним, слой адаптивной агрегации работ голов внимания в единое представление.

index++;
if(!cPooling.Init(0, index, OpenCL, iHeads * iWindowKey, iHeads * iWindowKey, iWindow, iTopKQuerys,
                                                                              optimization, iBatch))
   return false;
cPooling.SetActivationFunction(TANH);

На данном этапе алгоритм уже сформировал выходы механизма вероятностного внимания. Однако, важно подчеркнуть одну критическую особенность: мы оперируем результатами, соответствующими только наиболее значимым запросам (Top-K Queries). Это означает, что итоговый тензор обладает существенно меньшей временной размерностью, чем исходные данные.

В результате, возникает архитектурная несостыковка: блок внимания даёт сжатое представление, в то время как классическая структура Self-Attention подразумевает сохранение размерности исходных данных и результатов, что необходимо, в частности, для корректного добавления остаточных (residual) связей, обеспечивающих стабильность обучения и поддержку градиентного потока.

Чтобы устранить это противоречие, мы применяем следующую стратегию:

  • Сначала мы поворачиваем матрицу результатов внимания таким образом, чтобы структура данных позволяла применить свёрточное преобразование вдоль оси признаков.

index++;
if(!cTranspose[0].Init(0, index, OpenCL, iTopKQuerys, iWindow, optimization, iBatch))
   return false;

  • Далее используется свёрточный слой cScaling, цель которого — восстановить размерность результатов до исходной длины последовательности. Таким образом, мы получаем тензор, сопоставимый по размерности с оригинальным входом. Примечательно, что масштабирование происходит в разрезе отдельных признаков, то есть, каждая временная точка восстанавливается с учётом глобальной структуры внимания.
index++;
if(!cScaling.Init(0, index, OpenCL, iTopKQuerys, iTopKQuerys, iUnits, iWindow, optimization, iBatch))
   return false;
cScaling.SetActivationFunction(None);
  • После масштабирования, мы возвращаем данные в исходное представление, снова применяя операцию транспонирования.
if(!cTranspose[1].Init(0, index, OpenCL, iWindow, iUnits, optimization, iBatch))
   return false;

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

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

   ibScore = OpenCL.AddBuffer(sizeof(float) * iTopKQuerys * iUnits * iHeads, CL_MEM_READ_WRITE);
   if(ibScore < 0)
      return false;
//---
   return true;
  }

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

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

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

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

Метод RandomKeys получает указатель на буфер indexes, который должен быть заполнен сэмплированными индексами Ключей. Параметры random, units и heads задают количество случайных значений, общее число доступных Ключей и количество голов внимания, соответственно.

bool CNeuronMHProbAttention::RandomKeys(CBufferFloat *indexes, int random, int units, int heads)
  {
   if(!indexes || random > units ||
      indexes.Total() < (random * heads)
     )
      return false;

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

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

matrix<float> ind = matrix<float>::Zeros(random, heads);

Дальше возможны два сценария. Если сэмплировать не нужно (random == units), заполняем матрицу последовательными номерами — то есть, используем все доступные Ключи.

if(random == units)
  {
   for(int r = 0; r < random; r++)
     {
      for(int c = 0; c < heads; c++)
         ind[r, c] = (float)r;
     }
  }

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

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

else
  {
   double step = double(units) / random;

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

Для каждой страты (отрезка диапазона) случайным образом выбираем значение внутри.

 for(int r = 0; r < random; r++)
   {
    for(int c = 0; c < heads; c++)
       ind[r, c] = float(int((r + MathRand() / 32767.0) * step));
   }
}

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

После генерации выборки, она записывается в буфер indexes, откуда позже используется в OpenCL-программе.

   if(!indexes.AssignArray(ind) ||
      !indexes.BufferWrite())
      return false;
//---
   return true;
  }

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

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

bool CNeuronMHProbAttention::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cQKV.FeedForward(NeuronOCL))
      return false;

Первым шагом вызывается метод FeedForward сверточного слоя cQKV, который, как подсказывает название, отвечает за формирование конкатенированного тензора Запросов, Ключей и Значений. На выходе формируется один тензор, в котором три сущности идут друг за другом.

Однако, для дальнейшей работы нам необходимо разложить этот тензор на два: один для Запросов (Q), другой — для Ключей и Значений (K и V). Для этого используется метод DeConcat, который извлекает из общего QKV-объединения соответствующие части и передаёт их в cQ и cKV.

if(!DeConcat(cQ.getOutput(), cKV.getOutput(), cQKV.getOutput(), iWindowKey * iHeads,
                                                    2 * iWindowKey * iHeads, iUnits))
   return false;

Затем, запускается вышерассмотренный алгоритм сэмплирования индексов Ключей в методе RandomKeys.

if(!RandomKeys(cRandomK.getOutput(), iRandomKeys, iUnits, iHeads))
   return false;

Следующим шагом вызываются два метода: QueryImportance и TopKIndexes. Они осуществляют постановку в очередь выполнения кернелов ранжирование значимости Запросов и выделение из них наиболее информативных.

if(!QueryImportance() || !TopKIndexes())
   return false;

После этого, выполняется основное ядро алгоритма — метод AttentionOut. В нём происходит постановка в очередь выполнения кернела внимания для отобранных Запросов.

if(!AttentionOut())
   return false;

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

if(!cPooling.FeedForward(cMHAttentionOut.AsObject()))
   return false;

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

if(!cTranspose[0].FeedForward(cPooling.AsObject()))
   return false;
if(!cScaling.FeedForward(cTranspose[0].AsObject()))
   return false;
if(!cTranspose[1].FeedForward(cScaling.AsObject()))
   return false;

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

if(!SumAndNormilize(cTranspose[1].getOutput(), NeuronOCL.getOutput(), cTranspose[1].getOutput(),
                    iWindow, true, 0, 0, 0, 1))
   return false;

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

 return CResidualConv::feedForward(cTranspose[1].AsObject());
}

Таким образом, вся логика вероятностного внимания реализована внутри объекта CNeuronMHProbAttention. Такой подход обеспечивает модульность, переиспользуемость и удобство масштабирования модели.

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

bool CNeuronMHProbAttention::calcInputGradients(CNeuronBaseOCL *prevLayer)
  {
   if(!prevLayer)
      return false;

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

Затем, выполняется важная подготовительная операция — принудительное обнуление буфера градиентов Запросов.

if(!cQ.getGradient().Fill(0))
   return false;

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

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

if(!CResidualConv::calcInputGradients(cTranspose[1].AsObject()))
   return false;

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

if(!cScaling.calcHiddenGradients(cTranspose[1].AsObject()))
   return false;
if(!cTranspose[0].calcHiddenGradients(cScaling.AsObject()))
   return false;

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

if(!cPooling.calcHiddenGradients(cTranspose[0].AsObject()))
   return false;
if(!cMHAttentionOut.calcHiddenGradients(cPooling.AsObject()))
   return false;

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

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

if(!AttentionInsideGradients())
   return false;

После этого, следует процедура объединения градиентов — выполняется слияние градиентов Запросов, Ключей и Значений в единый тензор QKV, отражающий исходную структуру, сформированную на этапе прямого прохода. Если в блоке cQKV применялась функция активации, производится коррекция градиентов через вычисление производной этой функции. Это позволяет учесть её влияние на передаваемые сигналы и гарантирует математическую точность всего процесса.

if(!Concat(cQ.getGradient(), cKV.getGradient(), cQKV.getGradient(), iWindowKey * iHeads,
                                                        2 * iWindowKey * iHeads, iUnits))
   return false;
if(cQKV.Activation() != None)
   if(!DeActivation(cQKV.getOutput(), cQKV.getGradient(), cQKV.getGradient(), cQKV.Activation()))
      return false;

Градиенты, полученные на этом этапе, передаются в предшествующий слой.

if(!prevLayer.calcHiddenGradients(cQKV.AsObject()))
   return false;

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

   if(prevLayer.Activation() != None)
      if(!DeActivation(prevLayer.getOutput(), cTranspose[1].getGradient(), cTranspose[1].getGradient(),
                                                                               prevLayer.Activation()))
         return false;
   if(!SumAndNormilize(cTranspose[1].getGradient(), prevLayer.getGradient(), prevLayer.getGradient(),
                       iWindow, false, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

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

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


Архитектура модели

Завершая обзор реализации базовых компонентов, стоит остановиться на общей архитектуре обучаемой системы. Как и в ряде предыдущих работ, мы придерживаемся иерархической схемы обучения на основе фреймворка Actor–Director–Critic. Такой подход позволяет гибко разделить функции обработки среды, принятия решений и оценки действий, что особенно важно в условиях сложной, нестабильной рыночной динамики.

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

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

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

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

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

//--- Encoder
   encoder.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = (HistoryBars * BarDescr);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

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

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

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

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormWithNoise;
   descr.count = prev_count;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = HistoryBars;
   descr.window = BarDescr;
   descr.step = BarDescr;
   int prev_out = descr.window_out = NSkills;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = prev_count;
   prev_count = descr.window = prev_out;
   prev_out = descr.count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMHProbAttention;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.step = 4;
   descr.window_out = 32;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_out = descr.count = (prev_out + 1) / 2;
   descr.window = 2;
   descr.step = 2;
   int filt=descr.window_out = 5;
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronProofOCL;
   descr.count = prev_count*prev_out;
   descr.window = filt;
   descr.step = filt;
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

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

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

//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMHProbAttention;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.step = 4;
   descr.window_out = 32;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_out = descr.count = (prev_out + 1) / 2;
   descr.window = 2;
   descr.step = 2 ;
   filt=descr.window_out = 3;
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 10
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronProofOCL;
   descr.count = prev_count*prev_out;
   descr.window = filt;
   descr.step = filt;
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 11
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count * prev_out;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 12
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMHProbAttention;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.step = 4;
   descr.window_out = 32;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 13
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_out = descr.count = (prev_out + 1) / 2;
   descr.window = 2;
   descr.step = 2 ;
   filt=descr.window_out = 3;
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 14
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronProofOCL;
   descr.count = prev_count*prev_out;
   descr.window = filt;
   descr.step = filt;
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 15
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count * prev_out;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

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

//--- layer 16
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronDMHAttention;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.step = 4;
   descr.layers = 2;
   descr.window_out = 32;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

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

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

//--- layer 17
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = 1;
   descr.window = prev_out;
   descr.step = prev_out;
   prev_out = descr.window_out = 4 * NForecast;
   descr.layers = prev_count;
   descr.activation = SoftPlus;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

//--- layer 18
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = 1;
   descr.window = prev_out;
   descr.step = prev_out;
   prev_out = descr.window_out = NForecast;
   descr.layers = prev_count;
   descr.activation = TANH;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

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

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

//--- layer 19
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = prev_count;
   prev_count=descr.window = prev_out;
   prev_out=descr.count;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

//--- layer 20
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.step = prev_out;
   prev_out = descr.window_out = BarDescr;
   descr.layers = 1;
   descr.activation = TANH;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

//--- layer 21
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronRevInDenormOCL;
   descr.count = prev_count * prev_out;
   descr.layers = 1;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

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


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

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

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

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

Для объективной оценки результатов, тестирование обученных моделей осуществлялось на исторических данных за пределами периода обучения (Январь–Март 2025 года). Это исключает переобучение и подчёркивает практическую ценность полученных результатов.

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

Результаты тестирования представлены ниже.

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

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

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


Заключение

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

В практической части было реализовано собственное видение основных компонентов ACEFormer средствами MQL5. Мы встроили их в архитектуру обучаемых моделей в рамках подхода Актёр–Режиссёр–Критик. Основное внимание было уделено построению модели Энкодера окружающей среды, в которой и применяется предложенный фреймворком механизм обработки признаков.

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


Ссылки


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

# Имя Тип Описание
1 Research.mq5 Советник Советник сбора примеров
2 ResearchRealORL.mq5
Советник
Советник сбора примеров методом Real-ORL
3 Study.mq5 Советник Советник офлайн обучения моделей
4 StudyOnline.mq5
Советник
Советник онлайн обучения моделей
4 Test.mq5 Советник Советник для тестирования модели
5 Trajectory.mqh Библиотека класса Структура описания состояния системы и архитектуры моделей
6 NeuroNet.mqh Библиотека класса Библиотека классов для создания нейронной сети
7 NeuroNet.cl Библиотека Библиотека кода OpenCL-программы

Прикрепленные файлы |
MQL5.zip (2720.51 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Aleksander
Aleksander | 8 мая 2025 в 11:26
13 сделок это по 1ой валютной паре? Если 10 пар в анализ поставить, сколько сделок будет? 
Компонент View для таблиц в парадигме MVC на MQL5: Базовый графический элемент Компонент View для таблиц в парадигме MVC на MQL5: Базовый графический элемент
В статье рассматривается процесс разработки базового графического элемента для компонента View в рамках реализации таблиц в парадигме MVC (Model-View-Controller) на языке MQL5. Это первая статья, посвященная компоненту View, и третья в серии статей о создании таблиц для клиентского терминала MetaTrader 5.
Скрытые марковские модели в торговых системах на машинном обучении Скрытые марковские модели в торговых системах на машинном обучении
Скрытые марковские модели (СММ) представляют собой мощный класс вероятностных моделей, предназначенных для анализа последовательных данных, где наблюдаемые события зависят от некоторой последовательности ненаблюдаемых (скрытых) состояний, которые формируют марковский процесс. Основные предположения СММ включают марковское свойство для скрытых состояний, означающее, что вероятность перехода в следующее состояние зависит только от текущего состояния, и независимость наблюдений при условии знания текущего скрытого состояния.
Применение Conditional LSTM и индикатора VAM в автоматической торговле Применение Conditional LSTM и индикатора VAM в автоматической торговле
В настоящей статье рассматривается разработка советника (EA) для автоматической торговли, сочетающего в себе технический анализ с прогнозами с помощью глубокого обучения.
Алгоритм на основе фракталов — Fractal-Based Algorithm (FBA) Алгоритм на основе фракталов — Fractal-Based Algorithm (FBA)
Новый метаэвристический метод, основанный на фрактальном подходе к разделению пространства поиска для решения задач оптимизации. Алгоритм последовательно идентифицирует и разделяет перспективные области, создавая самоподобную фрактальную структуру, которая концентрирует вычислительные ресурсы на наиболее перспективных участках. Уникальный механизм мутации, направленный в сторону лучших решений, обеспечивает оптимальный баланс между исследованием и использованием пространства поиска, значительно повышая эффективность алгоритма.