preview
Нейросети в трейдинге: Унифицированное смешивание признаков для торговых решений (Основные компоненты)

Нейросети в трейдинге: Унифицированное смешивание признаков для торговых решений (Основные компоненты)

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

Введение

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

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

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

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

По структуре UniMixer-блок напоминает Transformer, но сходство тут лишь отправная точка. Внутри него есть модуль смешивания UniMixer, отвечающий за перераспределение и преобразование информации между токенами. И есть FFN-часть, которая завершает локальное преобразование представления. Авторы фреймворка предлагают в качестве FFN компонента использовать PerToken SwiGLU — решение, которое делает упор на гибкость преобразования признаков и позволяет сохранять выразительность модели без грубого утяжеления архитектуры.

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

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

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

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


PerToken SwiGLU

Продолжим построение фреймвокра UniMixer элементом FFN-части — PerToken SwiGLU, который отвечает за тонкую настройку каждого токена по отдельности. Это важный момент. На финансовом рынке редко работает примитивная схема увидел сигнал — сразу обработал. Чаще приходится разбираться, насколько сигнал вообще уместен в текущем состоянии рынка. Не потерял ли он силу. Не стал ли шумом. И не требует ли дополнительной фильтрации. Именно такую роль берет на себя PerToken SwiGLU.

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

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

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

Следующий шаг — уже не теория, а живая инженерная сборка. В коде модуль PerToken SwiGLU оформлен как отдельный класс CNeuronPerTokenSwiGLU, унаследованный от CNeuronPerTokenFFN. Сама архитектура сразу показывает, что перед нами специализированное развитие Feed-Forward части, адаптированное под логику UniMixer. Базовый каркас остается знакомым, но внутри него появляется более тонкий механизм работы с сигналом.

class CNeuronPerTokenSwiGLU   :  public CNeuronPerTokenFFN
  {
protected:
   CNeuronFieldAwareConv   cGate;
   CNeuronBaseOCL          cAfterGate;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronPerTokenSwiGLU(void) {};
                    ~CNeuronPerTokenSwiGLU(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint units,
                          uint embed_size, uint candidates, uint topK,
                          ENUM_OPTIMIZATION optimization_type, uint batch) override;
   //---
   virtual bool      Save(const int file_handle) override;
   virtual bool      Load(const int file_handle) override;
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   //---
   virtual int       Type(void) override const  {  return defNeuronPerTokenSwiGLU; }
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   virtual void      TrainMode(bool flag) override;
  };

Ключевую роль здесь играют два внутренних объекта:

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

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

Метод инициализации задаёт архитектурную геометрию блока PerToken SwiGLU.

bool CNeuronPerTokenSwiGLU::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                 uint window, uint units, uint embed_size, uint candidates,
                                 uint topK, ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronPerTokenFFN::Init(numOutputs, myIndex, open_cl, window, units, embed_size,
                                candidates, topK, optimization_type, batch))
      ReturnFalse;
   cProj.SetActivationFunction(None);

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

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

Следом инициализируется cGate. Именно здесь появляется управляющая ветка блока.

   uint index = 1;
   if(!cGate.Init(0, index, OpenCL, window, CNeuronFieldAwareConv::GetWindow(), GetFields(),
                  embed_size, candidates, topK, optimization, iBatch))
      ReturnFalse;
   cGate.SetActivationFunction(SoftPlus);

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

После этого инициализируется cAfterGate на основе размера выхода cGate.

   index++;
   if(!cAfterGate.Init(0, index, OpenCL, cGate.Neurons(), optimization, iBatch))
      ReturnFalse;
   cAfterGate.SetActivationFunction(None);
//---
   return true;
  }

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

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

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

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

bool CNeuronPerTokenSwiGLU::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cProj.FeedForward(NeuronOCL))
      ReturnFalse;

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

Параллельно запускается cGate.

   if(!cGate.FeedForward(NeuronOCL))
      ReturnFalse;

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

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

   if(!ElementMult(cProj.getOutput(), cGate.getOutput(), cAfterGate.getOutput()))
      ReturnFalse;

Это ключевая точка. Именно здесь модель принимает решение, сколько веса дать каждому компоненту. И делает это поэлементно.

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

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

   if(!CNeuronFieldAwareConv::feedForward(cAfterGate.AsObject()))
      ReturnFalse;
//---
   return true;
  }

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

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

Обратный проход зеркально отражает эту структуру, но добавляет важный слой самоанализа. Сначала градиент проходит через Field-Aware Convolution, то есть через этап учёта контекста. Это логично: модель сначала должна понять, где она ошиблась в общей картине.

bool CNeuronPerTokenSwiGLU::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      ReturnFalse;
//---
   if(!CNeuronFieldAwareConv::calcInputGradients(cAfterGate.AsObject()))
      ReturnFalse;

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

   if(!ElementMultGrad(cProj.getOutput(), cProj.getGradient(), cGate.getOutput(), cGate.getGradient(),
                       cAfterGate.getGradient(), cProj.Activation(), cGate.Activation()))
      ReturnFalse;

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

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

Дальше градиенты проходят отдельно через cProj и cGate. И здесь появляется ещё одна тонкость. Перед передачей градиентов от cGate используется временное сохранение градиента с последующим объединение через SumAndNormalize.

   if(!NeuronOCL.CalcHiddenGradients(cProj.AsObject()))
      ReturnFalse;
   CBufferFloat* temp = NeuronOCL.getGradient();
   if(!NeuronOCL.SetGradient(PrevOutput, false) ||
      !NeuronOCL.CalcHiddenGradients(cGate.AsObject()) ||
      !SumAndNormalize(temp, NeuronOCL.getGradient(), temp, GetWindow(), false, 0, 0, 0, 1) ||
      !NeuronOCL.SetGradient(temp, false))
      ReturnFalse;
//---
   return true;
  }

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

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

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


UniMixer

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

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

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

Переходим к самой сути — к тому, как устроен UniMixer на уровне кода. Класс CNeuronUniMixer выступает как логическое продолжение уже построенной системы. Наследование от CNeuronPerTokenSwiGLU на первый взгляд может показаться неожиданным, но в действительности это аккуратный инженерный ход.

class CNeuronUniMixer  :  public CNeuronPerTokenSwiGLU
  {
protected:
   uint              iBlocks;
   //---
   CFieldAwareParams cWb;
   CNeuronBaseOCL    cLocalMixing;
   CParams           cG[2];
   CNeuronBaseOCL    cGlobalMixing[2];
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronUniMixer(void) {};
                    ~CNeuronUniMixer(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint units, uint blocks,
                          uint embed_size, uint candidates, uint topK,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual bool      Save(const int file_handle) override;
   virtual bool      Load(const int file_handle) override;
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   //---
   virtual int       Type(void) override const  {  return defNeuronUniMixer; }
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   virtual void      TrainMode(bool flag) override;
  };

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

Параметр iBlocks задаёт число блоков смешивания. Это важный рычаг управления, который определяет глубину взаимодействия токенов на локальном уровне. Чем больше блоков — тем сложнее модель может описывать зависимости. Но тем выше вычислительная нагрузка и риск переобучения. В трейдинге лучше придерживаться умеренности. Избыточная глубина редко даёт стабильное преимущество.

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

Далее идёт cLocalMixing. Это первый этап UniMixer — локальное смешивание. Здесь взаимодействие происходит в пределах ограниченного контекста. Это похоже на анализ ближайшего рыночного окружения.

Следующий уровень — cGlobalMixing[2] с параметрами cG[2] по схеме UniMixer-Lite. Сначала происходит сжатие пространства, а затем — восстановление исходной размерности. Это уже не декоративная архитектурная деталь, а вполне практичный приём. Он решает сразу две практические задачи.

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

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

Таким образом, внутри UniMixer формируется двухуровневая схема:

  • сначала локальное взаимодействие токенов;
  • затем глобальное переосмысление этих связей

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

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

bool CNeuronUniMixer::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                            uint window, uint units, uint blocks,
                            uint embed_size, uint candidates, uint topK,
                            ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(units <= blocks || units % blocks != 0)
      ReturnFalse;

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

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

   if(!CNeuronPerTokenSwiGLU::Init(numOutputs, myIndex, open_cl, window, units, embed_size,
                                   candidates, topK, optimization_type, batch))
      ReturnFalse;

После этого сохраняется число блоков iBlocks и вычисляется размер одного блока.

   iBlocks = blocks;
   uint block_size = units / iBlocks;

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

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

   uint index = 0;
   if(!cWb.Init(0, index, OpenCL, block_size * block_size, blocks, block_size, candidates, topK, optimization, iBatch))
      ReturnFalse;
   cWb.SetActivationFunction(None);

Затем идёт cLocalMixing — локальный этап смешивания. Он строится по размеру анализируемых данных и подготавливает первичную внутриблочную структуру.

   index++;
   if(!cLocalMixing.Init(0, index, OpenCL, Neurons(), optimization, iBatch))
      ReturnFalse;
   cLocalMixing.SetActivationFunction(None);

После этого настраиваются две управляющие ветки cG[0] и cG[1]. И вот здесь стоит сделать паузу и внимательно посмотреть на их роль. На первый взгляд — два одинаковых по структуре объекта. Но по факту они образуют связку, которая реализует ключевую идею UniMixer-Lite: сжатие → восстановление.

   for(uint i = 0; i < cG.Size(); i++)
     {
      index++;
      if(!cG[i].Init(0, index, OpenCL, blocks * (blocks + 3) / 4, optimization, iBatch))
         ReturnFalse;
      cG[i].SetActivationFunction(None);
     }

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

Если смотреть математически, картина предельно ясна: одна матрица имеет размерность N×M, вторая — M×N. Классическая пара Encoder–Decoder в миниатюре. Но здесь важно не увлекаться формальной записью. В реальной реализации, особенно в контексте OpenCL, никакой матрицы как таковой в памяти не существует. Все данные хранятся в виде плоского массива. Размерность — это лишь способ интерпретации этих данных на этапе вычислений. И это даёт нам практическое преимущество.

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

Дальше инициализируются cGlobalMixing[0] и cGlobalMixing[1]. Именно они завершают архитектурную подготовку блока.

   index++;
   if(!cGlobalMixing[0].Init(0, index, OpenCL, block_size * window * (blocks + 3) / 4, optimization, iBatch))
      ReturnFalse;
   cGlobalMixing[0].SetActivationFunction(None);
   index++;
   if(!cGlobalMixing[1].Init(0, index, OpenCL, window * units, optimization, iBatch))
      ReturnFalse;
   cGlobalMixing[1].SetActivationFunction(None);
//---
   return true;
  }

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

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

bool CNeuronUniMixer::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      ReturnFalse;

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

   if(bTrain)
     {
      if(!cWb.FeedForward())
         ReturnFalse;
      for(uint i = 0; i < cG.Size(); i++)
         if(!cG[i].FeedForward())
            ReturnFalse;
     }

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

После этого вычислительный путь становится особенно интересным. Первая операция матричного умножения формирует cLocalMixing.

   uint dimension = GetWindow();
   uint units = GetFields();
   uint block_size = units / iBlocks;
   if(!MatMul(cWb.getOutput(), NeuronOCL.getOutput(), cLocalMixing.getOutput(),
              block_size, block_size, dimension, iBlocks, true))
      ReturnFalse;

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

Затем в работу включается cG[0]. Через вторую матричную операцию результат локального смешивания передаётся в cGlobalMixing[0], где начинается более широкое, уже глобальное взаимодействие.

   if(!MatMul(cG[0].getOutput(), cLocalMixing.getOutput(), cGlobalMixing[0].getOutput(),
              cG[0].Neurons() / iBlocks, iBlocks, dimension * block_size, 1, true))
      ReturnFalse;

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

Следующий вызов MatMul с cG[1] и cGlobalMixing[1] завершает эту цепочку.

   if(!MatMul(cG[1].getOutput(), cGlobalMixing[0].getOutput(), cGlobalMixing[1].getOutput(),
              iBlocks, cG[1].Neurons() / iBlocks, dimension * block_size, 1, true))
      ReturnFalse;

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

   if(!Normalize(cGlobalMixing[1].getOutput(), dimension))
      ReturnFalse;

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

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

   if(!SumAndNormalize(cGlobalMixing[1].getOutput(), NeuronOCL.getOutput(),
                       cGlobalMixing[1].getOutput(), dimension, false, 0, 0, 0, 1))
      ReturnFalse;

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

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

Заключение

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

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

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


Ссылки


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

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

Проект представлен на forge.mql5.io/dng.


Прикрепленные файлы |
MQL5.zip (3865.84 KB)
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Торговые инструменты на MQL5 (Часть 16): Улучшенное сглаживание методом суперсэмплинга (SSAA) и рендеринг в высоком разрешении Торговые инструменты на MQL5 (Часть 16): Улучшенное сглаживание методом суперсэмплинга (SSAA) и рендеринг в высоком разрешении
Мы добавляем сглаживание на основе суперсэмплинга и рендеринг высокого разрешения на панель Canvas на MQL5, а затем понижаем дискретизацию до целевого размера. В статье реализованы закругленные прямоугольные заливки и границы, закругленные треугольные стрелки и пользовательская полоса прокрутки с темой оформления для статистических и текстовых панелей. Эти инструменты помогут вам создать более плавные и разборчивые компоненты пользовательского интерфейса в MetaTrader 5.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Разработка инструментария для анализа движения цен (Часть 24): Инструмент количественного анализа Price Action Разработка инструментария для анализа движения цен (Часть 24): Инструмент количественного анализа Price Action
Свечные паттерны дают ценную информацию о возможном движении рынка. Одни свечи сигнализируют о продолжении текущего тренда, а другие предвещают разворот – в зависимости от того, где именно они формируются в структуре движения цены. В этой статье представлен советник, который автоматически определяет четыре ключевые свечные формации. В разделах ниже вы узнаете, как этот инструмент может улучшить ваш анализ движения цены.