preview
Нейросети в трейдинге: Декомпозиция вместо масштабирования — Построение модулей

Нейросети в трейдинге: Декомпозиция вместо масштабирования — Построение модулей

MetaTrader 5Торговые системы |
70 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Введение

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

Фреймворк SSCNN (Segmental Structured Convolutional Neural Network) создавался, как специализированный инструмент для анализа временных рядов в условиях рыночной нестабильности и фрагментарности сигналов. Его центральная идея — обрабатывать не отдельные значения, а целостные сегменты данных, позволяя модели учитывать и локальные особенности, и более широкие контекстуальные зависимости. Вместо линейного взгляда на временной ряд, SSCNN предлагает стратифицированный подход: каждый временной интервал рассматривается в структуре окружающего его контекста, благодаря чему становится возможным точное извлечение значимых признаков даже при высокой степени шума.

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

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

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


Модуль выделения компонент

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

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

Центральным элементом конструкции является объект CNeuronAttentNorm. Именно он отвечает за организацию взаимодействия между высокоуровневой логикой слоя нормализации с вниманием и низкоуровневым GPU-вычислением. Этот объект наследуется от базового класса CNeuronBaseOCL, что обеспечивает ему универсальный интерфейс и позволяет интегрироваться в общий стек модели без потери гибкости. Структура нового объекта представлена ниже.

class CNeuronAttentNorm :  public CNeuronBaseOCL
  {
protected:
   uint                    iPeriod;
   uint                    iVariables;
   uint                    iCount;
   //---
   CParams                 cAttention;
   CNeuronSoftMaxOCL       cSoftMax;
   CNeuronBaseOCL          cMeans;
   CNeuronBaseOCL          cSTDevs;
   //---
   virtual bool      AttentNorm(CNeuronBaseOCL *NeuronOCL);
   virtual bool      AttentNormGrad(CNeuronBaseOCL *NeuronOCL);
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronAttentNorm(void) :  iPeriod(0), iVariables(0), iCount(0) {};
                    ~CNeuronAttentNorm(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint units_count, uint period, uint variables,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual bool      Save(const int file_handle) override;
   virtual bool      Load(const int file_handle) override;
   //---
   virtual int       Type(void) override const  {  return defNeuronAttentNorm; }
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   CNeuronBaseOCL*   GetMeans(void) { return cMeans.AsObject(); }
   CNeuronBaseOCL*   GetSTDevs(void) { return cSTDevs.AsObject(); }
   virtual uint      GetPeriod(void) const { return iPeriod; }
   virtual uint      GetVariables(void) const { return iVariables; }
   virtual uint      GetUnits(void) const { return iCount; }
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) override {  activation = None; }
  };

В структуре класса мы видим, что ключевыми внутренними параметрами являются iPeriod, iVariables и iCount — они задают размер сегмента данных, число переменных и количество сегментов в одной унитарной последовательности, соответственно. Эти значения определяют структуру анализируемого массива.

Особое внимание стоит обратить на объект cAttention, который хранит параметры, связанные с механизмом внимания, и на cSoftMax — компонент, реализующий преобразование весов внимания в нормализованную форму. Эти два модуля работают в тесной связке: обучаемые параметры внимания проходят через SoftMax-преобразование, и только после этого используются для вычисления средних значений и стандартных отклонений. Для хранения этих статистик используются объекты cMeans и cSTDevs.

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

bool CNeuronAttentNorm::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                             uint units_count, uint period, uint variables,
                             ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, units_count * period * variables,
                                                                 optimization_type, batch))
      return false;
   CNeuronBaseOCL::SetActivationFunction(None);

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

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

   iCount = units_count;
   iVariables = variables;
   iPeriod = period;

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

   if(!cAttention.Init(0, 0, OpenCL, iPeriod * iVariables, optimization, iBatch))
      return false;
   cAttention.SetActivationFunction(None);
   if(!cSoftMax.Init(0, 1, OpenCL, cAttention.Neurons(), optimization, iBatch))
      return false;
   cSoftMax.SetHeads(iVariables);

Далее настраивается компонент cSoftMax, который преобразует значения внимания в вероятностное распределение. Его размерность соответствует числу нейронов в cAttention. Метод SetHeads определяет количество голов внимания — здесь оно приравнивается к количеству переменных, что позволяет обрабатывать каждый признак независимо.

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

//---
   if(!cMeans.Init(0, 2, OpenCL, iCount * iVariables, optimization, iBatch))
      return false;
   cMeans.SetActivationFunction(None);
   if(!cSTDevs.Init(0, 3, OpenCL, iCount * iVariables, optimization, iBatch))
      return false;
   cSTDevs.SetActivationFunction(None);
//---
   return true;
  }

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

Таким образом, метод Init формирует прочную архитектурную основу для слоя CNeuronAttentNorm, соединяя логические и вычислительные компоненты в единую, легко масштабируемую систему. Каждый объект отвечает за строго определённый участок обработки, а централизованная инициализация даёт полный контроль над параметрами слоя и поведением в OpenCL-контексте.

После завершения инициализации всех внутренних компонентов, ключевым этапом становится организация прямого прохода через слой. Именно на этом этапе модель начинает использовать инициализированные ранее структуры (веса внимания, SoftMax-преобразование и объекты статистических характеристик) для выполнения основной задачи: нормализации исходных данных с учетом значимости каждого элемента. Алгоритм реализован в методе feedForward.

bool CNeuronAttentNorm::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(bTrain)
     {
      if(!cAttention.FeedForward())
         return false;
      if(!cSoftMax.FeedForward(cAttention.AsObject()))
         return false;
     }
//---
   return AttentNorm(NeuronOCL);
  }

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

Если модель находится в режиме обучения (bTrain == true), первым делом выполняется прямой проход через компонент cAttention, в котором формируются сырые значения внимания. Эти значения напрямую не зависят от текущего состояния исходных данных и формируются в процессе обучения модели. Следовательно, в процессе эксплуатации результаты работы блока будут статичны, и нам нет необходимости выполнять излишние операции при каждом проходе.

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

Независимо от текущего режима (обучение это или инференс), завершающим действием метода feedForward становится вызов функции AttentNorm. Эта функция играет роль обёртки вокруг одноимённого OpenCL-кернела и отвечает за всю низкоуровневую подготовку: от расстановки параметров до постановки задачи в очередь выполнения. Структура метода строго следует стандартной схеме, принятой для всех подобных вызовов, и особенно очевидной эта стандартизация становится, благодаря ранее реализованным макросам и универсальным шаблонам. В связи с этим мы сознательно не будем подробно разбирать внутреннюю реализацию AttentNorm, ограничившись тем, что признаем его типовой и технически корректной точкой сопряжения между логикой модели и OpenCL-контекстом.

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

bool CNeuronAttentNorm::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!AttentNormGrad(NeuronOCL))
      return false;
   if(NeuronOCL.Activation() != None)
      if(!DeActivation(NeuronOCL.getOutput(), NeuronOCL.getGradient(),
                       NeuronOCL.getGradient(), NeuronOCL.Activation()))
         return false;

Метод начинается с вызова AttentNormGrad, который, по аналогии с AttentNorm, служит интерфейсом к одноимённому OpenCL-кернелу. Его задача — собрать градиенты от нормализованного выходного сигнала, учтя особенности внимания и параметры текущего сегмента данных. Это ключевой шаг, поскольку именно на его основе происходит обратный учёт влияния исходных данных на итоговые статистики, такие как среднее и дисперсия.

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

Финальный шаг метода — распространение градиентов обратно по цепочке внимания. Для этого используется метод CalcHiddenGradients объекта cAttention, которому в качестве входа передаётся выходной градиент от модуля SoftMax. Такой порядок действий обеспечивает непрерывность потока градиентной информации от выходного слоя к скрытым параметрам модели и, в конечном итоге, к её входам.

   if(!cAttention.CalcHiddenGradients(cSoftMax.AsObject()))
      return false;
//---
   return true;
  }

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

bool CNeuronAttentNorm::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
   return cAttention.UpdateInputWeights();
  }

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

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

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


Выделение пространственной компоненты

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

class CNeuronSAttentNorm :   public CNeuronTransposeOCL
  {
protected:
   CNeuronTransposeOCL     cTranspose;
   CNeuronBaseOCL          cAttention;
   CNeuronSoftMaxOCL       cSoftMax;
   CNeuronBaseOCL          cMeans;
   CNeuronBaseOCL          cSTDevs;
   CNeuronBaseOCL          cNorm;
   //---
   virtual bool      AttentNorm(CNeuronBaseOCL *NeuronOCL);
   virtual bool      AttentNormGrad(CNeuronBaseOCL *NeuronOCL);
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronSAttentNorm(void) {};
                    ~CNeuronSAttentNorm(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint units_count, uint variables,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual bool      Load(const int file_handle) override;
   //---
   virtual int       Type(void) override const  {  return defNeuronSAttentNorm; }
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   CNeuronBaseOCL*   GetMeans(void) { return cMeans.AsObject(); }
   CNeuronBaseOCL*   GetSTDevs(void) { return cSTDevs.AsObject(); }
   virtual uint      GetVariables(void) const { return iWindow; }
   virtual uint      GetUnits(void) const { return iCount; }
   //---
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) override {  activation = None; }
  };

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

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

bool CNeuronSAttentNorm::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                              uint units_count, uint variables,
                              ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronTransposeOCL::Init(numOutputs, myIndex, open_cl, units_count, variables,
                                                            optimization_type, batch))
      return false;
   activation = None;

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

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

   if(!cTranspose.Init(0, 0, OpenCL, iWindow, iCount, optimization, iBatch))
      return false;

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

   if(!cAttention.Init(0, 1, OpenCL, iWindow * iWindow, optimization, iBatch))
      return false;
   cAttention.SetActivationFunction(None);
   if(!cSoftMax.Init(0, 2, OpenCL, cAttention.Neurons(), optimization, iBatch))
      return false;
   cSoftMax.SetHeads(iWindow);

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

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

   if(!cMeans.Init(0, 3, OpenCL, iCount, optimization, iBatch))
      return false;
   cMeans.SetActivationFunction(None);
   if(!cSTDevs.Init(0, 4, OpenCL, iCount, optimization, iBatch))
      return false;
   cSTDevs.SetActivationFunction(None);

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

   if(!cNorm.Init(0, 5, OpenCL, Neurons(), optimization, iBatch))
      return false;
   cNorm.SetActivationFunction(None);
//---
   return true;
  }

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

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

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

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

   if(!MatMul(NeuronOCL.getOutput(), cTranspose.getOutput(), cAttention.getOutput(),
              iWindow, iCount, iWindow, 1, false))
      return false;
   if(!cSoftMax.FeedForward(cAttention.AsObject()))
      return false;

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

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

   if(!AttentNorm(cTranspose.AsObject()))
      return false;
//---
   return CNeuronTransposeOCL::feedForward(cNorm.AsObject());
  }

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

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

bool CNeuronSAttentNorm::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;
//---
   if(!CNeuronTransposeOCL::calcInputGradients(cNorm.AsObject()))
      return false;

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

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

   if(!AttentNormGrad(cTranspose.AsObject()))
      return false;

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

   if(!cAttention.CalcHiddenGradients(cSoftMax.AsObject()))
      return false;
   if(!MatMulGrad(NeuronOCL.getOutput(), PrevOutput,
                  cTranspose.getOutput(), cTranspose.getPrevOutput(),
                  cAttention.getGradient(),
                  iWindow, iCount, iWindow, 1, false))
      return false;

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

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

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

   if(!SumAndNormilize(cTranspose.getGradient(), cTranspose.getPrevOutput(), cTranspose.getGradient(),
                       iWindow, false, 0, 0, 0, 1))
      return false;

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

   if(!NeuronOCL.CalcHiddenGradients(cTranspose.AsObject()))
      return false;
   if(!SumAndNormilize(NeuronOCL.getGradient(), getPrevOutput(), NeuronOCL.getGradient(),
                       iCount, false, 0, 0, 0, 1))
      return false;
//---
   if(NeuronOCL.Activation() != None)
      if(!DeActivation(NeuronOCL.getOutput(), NeuronOCL.getGradient(),
                       NeuronOCL.getGradient(), NeuronOCL.Activation()))
         return false;
//---
   return true;
  }

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

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

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

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


Модуль Полиномиальная регрессия

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

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

Чтобы адекватно захватить взаимодействия второго порядка (и даже выше), используется параллельная обработка входного сигнала через два маршрута:

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

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

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

class CNeuronPolynomialRegression   :  public CNeuronBaseOCL
  {
protected:
   CNeuronSwiGLUOCL     cProjection;
   CNeuronConvOCL       cConvolution;
   CNeuronConvOCL       cResidual;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronPolynomialRegression(void) {};
                    ~CNeuronPolynomialRegression(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint units_count, uint window, uint window_out,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual bool      Save(const int file_handle) override;
   virtual bool      Load(const int file_handle) override;
   //---
   virtual int       Type(void) override const  {  return defNeuronPolynomialRegression; }
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   virtual uint      GetWindowIn(void) const { return cProjection.GetWindow(); }
   virtual uint      GetWindowOut(void) const { return cResidual.GetFilters(); }
   virtual uint      GetUnits(void) const { return cProjection.GetUnits(); }
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) override { }
  };

Внутри класса предусмотрены три ключевых компонента: проекционный блок cProjection, основанный на механизме SwiGLU, выступает в роли первого преобразователя данных. Он выполняет расширение признаков, подготавливая их к дальнейшей обработке. Кроме того, используются два сверточных слоя, которые предназначен для захвата остаточных и мультипликативных зависимостей. Такое разделение позволяет объединить как аддитивные, так и полиномиальные (второго порядка и выше) отношения между признаками, расширяя выразительные возможности модели без чрезмерного усложнения структуры.

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

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

bool CNeuronPolynomialRegression::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                       uint units_count, uint window, uint window_out,
                                       ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, units_count * window_out,
                                                         optimization_type, batch))
      return false;
   activation = None;

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

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

   if(!cProjection.Init(0, 0, OpenCL, window, window, window_out, units_count, 1,
                                                            optimization, iBatch))
      return false;
   if(!cConvolution.Init(0, 1, OpenCL, window_out, window_out, window_out, units_count,
                                                              1, optimization, iBatch))
      return false;
   cConvolution.SetActivationFunction(None);

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

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

   if(!cResidual.Init(0, 2, OpenCL, window, window, window_out, units_count, 1, optimization, iBatch))
      return false;
   cResidual.SetActivationFunction(None);

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

   if(!SetGradient(cConvolution.getGradient(), true))
      return false;
   if(!cResidual.SetGradient(getGradient(), true))
      return false;
//---
   return true;
  }

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

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

bool CNeuronPolynomialRegression::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cProjection.FeedForward(NeuronOCL))
      return false;
   if(!cConvolution.FeedForward(cProjection.AsObject()))
      return false;

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

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

Затем та же самая исходная информация передаётся в линейный блок cResidual, который отвечает за сохранение чистой линейной части сигнала.

   if(!cResidual.FeedForward(NeuronOCL))
      return false;
   if(!SumAndNormilize(cConvolution.getOutput(), cResidual.getOutput(),
                       Output, GetWindowOut(), true, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

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

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

bool CNeuronPolynomialRegression::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;

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

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

   if(!cProjection.CalcHiddenGradients(cConvolution.AsObject()))
      return false;
//---
   if(!NeuronOCL.CalcHiddenGradients(cProjection.AsObject()))
      return false;

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

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

   CBufferFloat* temp = NeuronOCL.getGradient();
   if(!NeuronOCL.SetGradient(NeuronOCL.getPrevOutput(), false) ||
      !NeuronOCL.CalcHiddenGradients(cResidual.AsObject()) ||
      !SumAndNormilize(temp, NeuronOCL.getGradient(), temp,
                       GetWindowIn(), false, 0, 0, 0, 1) ||
      !NeuronOCL.SetGradient(temp, false))
      return false;
//---
   return true;
  }

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

Теперь, когда градиенты собраны и готовы к применению, плавно переходим к этапу обновления весов в методе updateInputWeights. Здесь каждый компонент снова работает по очереди: сперва проекционный модуль cProjection корректирует свои внутренние параметры, после чего управление плавно передаётся блоку cConvolution.

bool CNeuronPolynomialRegression::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cProjection.UpdateInputWeights(NeuronOCL))
      return false;
   if(!cConvolution.UpdateInputWeights(cProjection.AsObject()))
      return false;
   if(!cResidual.UpdateInputWeights(NeuronOCL))
      return false;
//---
   return true;
  }

Наконец, линейный блок cResidual завершает цепочку, обновляя свои веса на основе исходных данных и полученных ранее градиентов ошибки. Если в любой из этих трёх стадий происходит сбой, метод немедленно возвращает false, но при безошибочном выполнении каждой корректировки, updateInputWeights мирно завершает работу, возвращая true и гарантируя, что все компоненты CNeuronPolynomialRegression синхронно готовы к следующему циклу обучения.

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

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

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


Заключение

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

Конструкция CNeuronAttentNorm стала квинтэссенцией нашего подхода: она сочетает семантически насыщенные механизмы внимания и нормализации, реализованные на GPU с учётом контекста сегмента и особенностей финансовых сигналов. Подход к инициализации, передаче и синхронизации данных между CPU и GPU был спроектирован так, чтобы избежать типичных узких мест, связанных с параллельным вычислением в OpenCL.

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

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

Ссылки


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

# Имя Тип Описание
1 Study.mq5 Советник Советник офлайн обучения моделей
2 StudyOnline.mq5 Советник Советник онлайн обучения моделей
3 Test.mq5 Советник Советник для тестирования модели
4 Trajectory.mqh Библиотека класса Структура описания состояния системы и архитектуры моделей
5 NeuroNet.mqh Библиотека класса Библиотека классов для создания нейронной сети
6 NeuroNet.cl Библиотека Библиотека кода OpenCL-программы
Прикрепленные файлы |
MQL5.zip (2963.02 KB)
От начального до среднего уровня: Шаблон и Typename (I) От начального до среднего уровня: Шаблон и Typename (I)
В этой статье мы начнем рассматривать одну из концепций, которую многие новички избегают. Это связано с тем, что шаблоны - непростая тема, поскольку многие не понимают основного принципа, лежащего в основе шаблона: перегрузка функций и процедур.
Моделирование рынка (Часть 02): Кросс-ордера (II) Моделирование рынка (Часть 02): Кросс-ордера (II)
В отличие от того, что было в предыдущей статье, здесь мы осуществим проверку опции выбора на советнике. Хотя это еще не окончательное решение, но пока этого будет достаточно. С помощью данной статьи, вы сможете понять, как реализовать одно из возможных решений.
Добавляем пользовательскую LLM в торгового робота (Часть 5): Разработка и тестирование торговой стратегии с помощью LLM (III) – Настройка адаптера Добавляем пользовательскую LLM в торгового робота (Часть 5): Разработка и тестирование торговой стратегии с помощью LLM (III) – Настройка адаптера
Языковые модели (LLM) являются важной частью быстро развивающегося искусственного интеллекта, поэтому нам следует подумать о том, как интегрировать мощные LLM в нашу алгоритмическую торговлю. Большинству людей сложно настроить эти модели в соответствии со своими потребностями, развернуть их локально, а затем применить к алгоритмической торговле. В этой серии статей будет рассмотрен пошаговый подход к достижению этой цели.
Файловые операции в MQL5: От базового ввода-вывода до собственного CSV-ридера Файловые операции в MQL5: От базового ввода-вывода до собственного CSV-ридера
В статье рассматриваются основные методы обработки файлов MQL5, ведение журналов торговли, обработка CSV-файлов и интеграция внешних данных. Статья содержит как теорию, так и практическое руководство по реализации. Читатели научатся шаг за шагом создавать собственный класс импортера CSV, получив практические навыки для реальных приложений.