preview
Нейросети в трейдинге: Агрегация движения по времени (Основные компоненты)

Нейросети в трейдинге: Агрегация движения по времени (Основные компоненты)

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

Введение

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

В предыдущей статье мы познакомились с фреймворком Temporal Motion Aggregation (TMA), который изначально создавался для анализа событийных данных оптического потока. Как ни странно, он оказался удивительно близок по духу к задачам финансового прогнозирования. Идея в том, что поток информации можно рассматривать как последовательность событий, связанных временными зависимостями. Каждое событие несёт не просто значение, а импульс движения. Для авторов оригинального исследования это были события яркости в сенсоре. Для нас — микроколебания цены, всплески объёма, изменения ликвидности. В обоих случаях цель одна — уловить саму ткань движения, сохранить в модели ту временную непрерывность, которая обычно теряется при традиционной обработке данных.

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

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

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

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

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

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

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


Энкодер признаков движения

Сегодня мы начинаем работу с построения модуля Энкодера признаков движения (Motion Feature Encoder — MFE). В оригинальной архитектуре этот компонент построен из двух параллельных сверточных магистралей. Одна отвечает за обработку основного потока данных, а вторая — коэффициентов корреляции. После этого оба потока объединяются и проходят через дополнительный сверточный слой, где формируется итоговое представление признаков движения.

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

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

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

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

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

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

class CNeuronTMAMFE  :  public CNeuronConvOCL
  {
protected:
   CLayer            cEncoder;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronTMAMFE(void) {};
                    ~CNeuronTMAMFE(void) {};
   //---
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint units, uint &windows[], uint window_out,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   override const   {  return defNeuronTMAMFE;   }
   //--- methods for working with files
   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;
   virtual void      TrainMode(bool flag) override;
  };

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

bool CNeuronTMAMFE::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                         uint units, uint &windows[], uint window_out,
                         ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(windows.Size() != 2)
      return false;
//---
   if(!CNeuronConvOCL::Init(numOutputs, myIndex, open_cl, 2 * window_out, 2 * window_out,
                            window_out, units, 1, optimization_type, batch))
      return false;

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

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

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

   uint index = 0;
   cEncoder.Clear();
   cEncoder.SetOpenCL(OpenCL);
   CNeuronMultiWindowsConvOCL*   mw_conv  =  NULL;
   CNeuronBatchNormOCL*          normal   =  NULL;

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

   mw_conv = new CNeuronMultiWindowsConvOCL();
   if(!mw_conv ||
      !mw_conv.Init(0, index, OpenCL, windows, 2 * window_out, units, 1, optimization, iBatch) ||
      !cEncoder.Add(mw_conv))
     {
      DeleteObj(mw_conv)
      return false;
     }
   mw_conv.SetActivationFunction(SoftPlus);
   index++;

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

   normal = new CNeuronBatchNormOCL();
   if(!normal ||
      !normal.Init(0, index, OpenCL, mw_conv.Neurons(), iBatch, optimization) ||
      !cEncoder.Add(normal))
     {
      DeleteObj(normal)
      return false;
     }
   index++;

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

   uint temp[] = {2 * window_out, 2 * window_out};
   mw_conv = new CNeuronMultiWindowsConvOCL();
   if(!mw_conv ||
      !mw_conv.Init(0, index, OpenCL, temp, window_out, units, 1, optimization, iBatch) ||
      !cEncoder.Add(mw_conv))
     {
      DeleteObj(mw_conv)
      return false;
     }
   mw_conv.SetActivationFunction(SoftPlus);
   index++;
      normal = new CNeuronBatchNormOCL();
   if(!normal ||
      !normal.Init(0, index, OpenCL, mw_conv.Neurons(), iBatch, optimization) ||
      !cEncoder.Add(normal))
     {
      DeleteObj(normal)
      return false;
     }
//---
   return true;
  }

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

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

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

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

bool CNeuronTMAMFE::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   CNeuronBaseOCL* prev = NeuronOCL;
   CNeuronBaseOCL* curr = NULL;

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

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

   for(int i = 0; i < cEncoder.Total(); i++)
     {
      curr = cEncoder[i];
      if(!curr ||
         !curr.FeedForward(prev))
         return false;
      prev = curr;
     }

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

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

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

   if(!CNeuronConvOCL::feedForward(prev))
      return false;
//---
   return true;
  }

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

Финальный штрих — сигнал успешного завершения прямого прохода.

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

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

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


Модуль агрегации шаблонов движения

Следующий шаг в построении модели — модуль агрегации шаблонов движения (Motion Pattern Aggregation — MPA). Именно здесь, словно в фокусе линзы, сходятся все предварительно обработанные признаки, чтобы сформировать целостное представление о динамике процесса. На этом этапе авторы фреймворка делают нестандартный, даже смелый шаг. Они вводят механизм кросс-внимания. Но применяют его не так, как это принято в классических архитектурах.

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

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

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

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

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

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

Мы же решили пойти иным путём — более экономным и, если угодно, инженерно изящным. В нашем стеке данных последнее состояние уже хранится в первых элементах тензора. Дополнительное выделение контекста становится не только избыточным, но и ресурсоёмким. Создавать новый объект кросс-внимания ради того, чтобы поменять местами тензоры, попросту нет смысла. Ведь фактически мы имеем дело с тем же Self-Attention, но в усечённой форме: контекст ограничивается одним состоянием, а не всей последовательностью.

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

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

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

class CNeuronTMAMPA  :  public CNeuronBaseOCL
  {
protected:
   CNeuronRelativeCrossAttention cCrossAttention;
   CLayer                        cProjection;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronTMAMPA(void) {};
                    ~CNeuronTMAMPA(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint window_key, uint units,
                          uint segments, uint heads,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override   const   {  return defNeuronTMAMPA; }
   //---
   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;
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) override { };
  };

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

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

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

bool CNeuronTMAMPA::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                         uint window, uint window_key, uint units,
                         uint segments, uint heads,
                         ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units * segments,
                            optimization_type, batch))
      return false;
   activation = None;

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

Затем активируется ключевой компонент — блок относительного кросс-внимания cCrossAttention. Именно он реализует тот самый интеллектуальный механизм сопоставления состояний в контексте последнего наблюдения. На этапе инициализации ему передаются размеры окон данных и контекста, число элементов в последовательности и количество сегментов в каждом из них. Особенность здесь в том, что оба окна используют один и тот же тензор, а длина контекста искусственно ограничена одним состоянием. Таким образом, блок получает доступ ко всей последовательности, но при этом анализирует её исключительно через призму последнего состояния.

   uint index = 0;
   if(!cCrossAttention.Init(0, index, OpenCL, window, window_key, units * segments,
                            heads, window, segments, optimization, iBatch))
      return false;
   index++;

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

   cProjection.Clear();
   cProjection.SetOpenCL(OpenCL);
//---
   CNeuronBaseOCL*      neuron =  NULL;
   CNeuronConvOCL*      conv   =  NULL;
   CNeuronBatchNormOCL* norm   =  NULL;

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

//--- Concatenated
   neuron = new CNeuronBaseOCL();
   if(!neuron ||
      !neuron.Init(0, index, OpenCL, 2 * Neurons(), optimization, iBatch) ||
      !cProjection.Add(neuron))
     {
      DeleteObj(neuron)
      return false;
     }
   neuron.SetActivationFunction(None);
   index++;

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

//--- Projection
   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(0, index, OpenCL, 2 * window, 2 * window, window, units * segments, 1, optimization, iBatch) ||
      !cProjection.Add(conv))
     {
      DeleteObj(conv)
      return false;
     }
   conv.SetActivationFunction(SoftPlus);
   index++;

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

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

   norm = new CNeuronBatchNormOCL();
   if(!norm ||
      !norm.Init(0, index, OpenCL, conv.Neurons(), iBatch, optimization) ||
      !cProjection.Add(norm))
     {
      DeleteObj(norm)
      return false;
     }
   norm.SetActivationFunction(None);
   index++;

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

//--- Feed Forward
   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(0, index, OpenCL, window, window, 2 * window, units * segments, 1, optimization, iBatch) ||
      !cProjection.Add(conv))
     {
      DeleteObj(conv)
      return false;
     }
   conv.SetActivationFunction(SoftPlus);
   index++;
   conv = new CNeuronConvOCL();
   if(!conv ||
      !conv.Init(0, index, OpenCL, 2 * window, 2 * window, window, units * segments, 1, optimization, iBatch) ||
      !cProjection.Add(conv))
     {
      DeleteObj(conv)
      return false;
     }
   conv.SetActivationFunction(None);
//---
   return true;
  }

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

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

bool CNeuronTMAMPA::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;
   if(!cCrossAttention.FeedForward(NeuronOCL, NeuronOCL.getOutput()))
      return false;

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

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

Далее начинается этап агрегации. Из блока проекции извлекается первый элемент cProjection[0], который будет использоваться для объединения двух потоков данных — исходного и обработанного вниманием. С помощью функции Concat происходит конкатенация этих потоков: мы буквально сшиваем первичный сигнал с результатом внимания, формируя расширенное представление признаков.

   CNeuronBaseOCL* neuron = cProjection[0];
   uint window = cCrossAttention.GetWindow();
   uint units = cCrossAttention.GetUnits();
   uint segments = cCrossAttention.GetUnitsKV();
   if(!neuron ||
      !Concat(NeuronOCL.getOutput(), cCrossAttention.getOutput(), neuron.getOutput(),
              window, window, units))
      return false;

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

   for(int i = 1; i < cProjection.Total(); i++)
     {
      neuron = cProjection[i];
      if(!neuron ||
         !neuron.FeedForward(cProjection[i - 1]))
         return false;
     }

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

   if(!SumAndNormilize(NeuronOCL.getOutput(), neuron.getOutput(), Output, window * segments, true, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

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

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

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

bool CNeuronTMAMPA::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;
//---
   CNeuronBaseOCL* neuron = cProjection[-1];
   if(!neuron ||
      !DeActivation(neuron.getOutput(), neuron.getGradient(), Gradient, neuron.Activation()))
      return false;

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

Далее выбирается последний элемент проекционного блока cProjection[-1], на который приходится завершающая активация прямого прохода. С помощью вызова метода DeActivation выполняется обратное преобразование — градиенты, поступившие от последующих уровней, корректируются в соответствии с функцией активации, применённой на этапе прямого распространения. Это первый шаг к восстановлению причинно-следственной цепочки.

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

   for(int i = cProjection.Total() - 2; i >= 0; i--)
     {
      neuron = cProjection[0];
      if(!neuron ||
         !neuron.CalcHiddenGradients(cProjection[i + 1]))
         return false;
     }

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

   uint window = cCrossAttention.GetWindow();
   uint units = cCrossAttention.GetUnits();
   if(!DeConcat(PrevOutput, cCrossAttention.getGradient(), neuron.getGradient(),
                window, window, units))
      return false;
   Deactivation(cCrossAttention)

Следом выполняется обратная активация внутри блока внимания — символическая команда Deactivation(cCrossAttention) в коде указывает на необходимость вычисления собственных градиентов внимания, чтобы согласовать направление обратного потока информации.

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

   if(!SumAndNormilize(PrevOutput, Gradient, PrevOutput, window, false, 0, 0, 0, 1))
      return false;
   if(NeuronOCL.Activation() != None)
      if(!DeActivation(NeuronOCL.getOutput(), PrevOutput, PrevOutput, NeuronOCL.Activation()))
         return false;

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

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

   if(!NeuronOCL.CalcHiddenGradients(cCrossAttention.AsObject(), NeuronOCL.getOutput(),
                                     cProjection[-1].getPrevOutput(), (ENUM_ACTIVATION)NeuronOCL.Activation()))
      return false;
   if(!SumAndNormilize(NeuronOCL.getGradient(), cProjection[-1].getPrevOutput(), NeuronOCL.getGradient(),
                                                                                   window, false, 0, 0, 0, 1) ||
      !SumAndNormilize(NeuronOCL.getGradient(), PrevOutput, NeuronOCL.getGradient(), window, false, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

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

Если хотя бы один из этих этапов не выполняется, метод немедленно возвращает false, предотвращая накопление некорректных значений.

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

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

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

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


Заключение

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

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


Ссылки


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

# Имя Тип Описание
1 Study.mq5 Советник Советник офлайн обучения моделей
2 StudyOnline.mq5 Советник Советник онлайн обучения моделей
3 Test.mq5 Советник Советник для тестирования модели
4 Trajectory.mqh Библиотека класса Структура описания состояния системы и архитектуры моделей
5 NeuroNet.mqh Библиотека класса Библиотека классов для создания нейронной сети
6 NeuroNet.cl Библиотека Библиотека кода OpenCL-программы
Прикрепленные файлы |
MQL5.zip (3282.19 KB)
Команда ИИ-агентов с ротацией по прибыли: Эволюция живой торговой системы в MQL5 Команда ИИ-агентов с ротацией по прибыли: Эволюция живой торговой системы в MQL5
Управление финансами как экосистема: семь ИИ-трейдеров с разными характерами и стратегиями вместо одного алгоритма. Они конкурируют за капитал, учатся на ошибках и принимают решения коллективно. Статья раскрывает принципы работы системы Modern RL Trader, где код обладает сознанием и эмоциями, создавая живой, эволюционирующий торговый разум.
От новичка до эксперта: Раскрываем скрытые уровни коррекции Фибоначчи От новичка до эксперта: Раскрываем скрытые уровни коррекции Фибоначчи
В настоящей статье мы рассмотрим основанный на данных подход к обнаружению и проверке нестандартных уровней коррекции Фибоначчи, которые могут учитываться рынками. Мы представляем полный рабочий процесс, адаптированный для реализации на MQL5, начиная со сбора данных и определения баров или колебаний и заканчивая кластеризацией, проверкой статистических гипотез, бэктестингом и интеграцией в инструмент Фибоначчи на MetaTrader 5. Цель состоит в том, чтобы создать воспроизводимый конвейер, преобразующий отдельные наблюдения в статистически обоснованные торговые сигналы.
Разработка динамического советника на нескольких парах (Часть 2): Диверсификация и оптимизация портфеля Разработка динамического советника на нескольких парах (Часть 2): Диверсификация и оптимизация портфеля
Диверсификация и оптимизация портфеля позволяют стратегически распределять инвестиции по нескольким активам, чтобы минимизировать риски, и при этом выбирать идеальную комбинацию активов для максимизации доходности на основе показателей эффективности с учетом риска.
Выборочные методы MCMC: Алгоритм выборки по уровням (Slice sampling) Выборочные методы MCMC: Алгоритм выборки по уровням (Slice sampling)
В этой статье исследуется метод выборки по уровням (slice sampling) — адаптивный алгоритм MCMC, который самостоятельно регулирует параметры сэмплирования. Его эффективность продемонстрирована на моделях байесовской линейной и логистической регрессии, а результаты сравниваются с классическими частотными методами.