preview
Нейросети в трейдинге: Спайковая архитектура пространственно-временного анализа рынка (Энкодер)

Нейросети в трейдинге: Спайковая архитектура пространственно-временного анализа рынка (Энкодер)

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

Введение

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

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

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

Этим принципом и руководствуется фреймворк SDformerFlow, предложенный в исследовательской работе "SDformerFlow: Spatiotemporal swin spikeformer for event-based optical flow estimation". Его архитектура построена вокруг событийного представления данных — разреженного, но точного. Она предназначена для потоков, где каждое изменение несёт смысл. Где нет регулярной сетки времени, а есть последовательность импульсов. Где плотность информации меняется от мгновения к мгновению. Именно этот тип данных всё сильнее напоминает структуру реального рыночного потока.

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

Третье преимущество — иерархическая структура обработки. В SDformerFlow информация постепенно сворачивается и уплотняется через несколько уровней Spiking Patch Merge, сохраняя временную ось нетронутой. Это даёт важный эффект. Модель умеет обрабатывать короткие участки времени без потери синхронизации. В мировой практике анализа временных рядов это редкость. Большинство архитектур жертвуют временной точностью ради абстракции; здесь же удаётся сохранить и то, и другое.

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

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

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

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

Авторская визуализация фреймворка SDformerFlow

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


Модуль внимания

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

Авторы предлагают собрать STSF-энкодер на основе трёх ключевых спайковых элементов:

  • многоголового внимания в событийной форме;
  • спайковой MLP;
  • компактного блока Patch-Merging, аккуратно уменьшающего размерность.

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

Логика построения STSF-энкодера напоминает работу мастера, который слой за слоем снимает лишнее и оставляет только наиболее выразительные детали. Многоголовое спайковое внимание играет роль тонкого фильтра. Оно удерживает локальный фокус, определяет взаимосвязи между событиями и передаёт дальше только то, что действительно влияет на динамику потока. Спайковая MLP действует как аккуратный преобразователь, подстраивая структуру признаков под будущий анализ, сохраняя при этом лёгкость модели. Завершает этот блок Patch-Merging — аналог классической линзы, которая мягко сжимает пространство, не ломая временную структуру последовательности.

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

Второй вариант — облегчённая версия внимания, построенная на принципе QK line attention. Здесь вычисления организованы так, чтобы зависимость от длины анализируемого окна была линейной, в отличие от квадратичной в классическом dot-product. Такой подход даёт ощутимый выигрыш на длинных и насыщенных потоках событий, где даже небольшое снижение вычислительной нагрузки превращается в существенный прирост скорости. И хотя линейное внимание менее ресурсоёмкое, оно сохраняет способность выделять ключевые связи в данных. Это делает его особенно привлекательным для реальных торговых потоков, где объём анализируемых событий может меняться по нескольку раз в секунду.

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

Авторская визуализация модуля SDSA c линейным QK-attention

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

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

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

Для этого мы создаём компактный, но эффективный кернел ScalarToVector, выполняющий поэлементное умножение входного вектора на скаляр.

__kernel void ScalarToVector(__global const float* scalar,
                             __global const float* vector_in,
                             __global float* vector_out
                            )
  {
   const size_t vec = get_global_id(0);
   const size_t d  = get_global_id(1);
   const size_t vectors = get_global_size(0);
   const size_t dimension = get_global_size(1);
//---
   float sc = IsNaNOrInf(scalar[vec], 0.0f);
   int shift = RCtoFlat(vec, d, vectors, dimension, 0);
   float v = IsNaNOrInf(vector_in[shift], 0.0f);
//---
   vector_out[shift] = IsNaNOrInf(sc * v, 0.0f);
  }

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

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

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

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

__kernel void ScalarToVectorGrad(__global const float* scalar,
                                 __global float* scalar_gr,
                                 __global const float* vector_in,
                                 __global float* vector_in_gr,
                                 __global float* vector_out_gr,
                                 const int dimension
                                )
  {
   const size_t vec = get_global_id(0);
   const size_t loc  = get_local_id(1);
   const size_t vectors = get_global_size(0);
   const size_t total_loc = get_local_size(1);
//---
   __local float temp[LOCAL_ARRAY_SIZE];

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

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

   if(loc == 0)
      temp[0] = IsNaNOrInf(scalar[vec], 0.0f);
   BarrierLoc
   float sc = temp[0];
   float sc_gr = 0;

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

   for(int d = loc; d < dimension; d += total_loc)
     {
      int shift = RCtoFlat(vec, d, vectors, dimension, 0);
      float v = IsNaNOrInf(vector_in[shift], 0.0f);
      float grad = IsNaNOrInf(vector_out_gr[shift], 0.0f);
      vector_in_gr[shift] = IsNaNOrInf(grad * sc, 0.0f);
      sc_gr += IsNaNOrInf(v * grad, 0.0f);
     }

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

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

   sc_gr = LocalSum(sc_gr, 1, temp);
   if(loc == 0)
      scalar_gr[vec] = sc_gr;
  }

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

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

class CNeuronSpikeQKAttention :  public CNeuronBaseOCL
  {
protected:
   CNeuronSpikeConvBlock      cQK;
   CNeuronSpikeActivation     cSpikeQK;
   CNeuronBaseOCL             cQ;
   CNeuronBaseOCL             cK;
   CNeuronMultiWindowsConvOCL cScore;
   CNeuronSpikeActivation     cSpikeScore;
   CLayer                     cProject;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronSpikeQKAttention(void) {};
                    ~CNeuronSpikeQKAttention(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint units, uint window, uint heads, uint dimension_k,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override const  {  return defNeuronSpikeQKAttention;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual void      SetOpenCL(COpenCLMy *obj)   override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) override { };
   virtual void      TrainMode(bool flag) override;
   virtual bool      Clear(void) override;
  };

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

Затем идут два базовых объекта — cQ и cK, фактически обозначающие отдельные каналы формирования Q и K. После них подключается блок cScore, который использует многоканальную свёртку для получения матрицы логитов внимания. А cSpikeScore дополняет вычисления спайковой активацией, помогая получить более выразительную, энергоэффективную форму отклика. Завершает цепочку модуль cProject, который собирает результаты воедино и формирует финальное многомерное представление.

Метод инициализации аккуратно формирует внутреннюю геометрию всего объекта и гарантирует, что каждый компонент готов к работе в рамках OpenCL-контекста.

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

Сначала вызывается базовая инициализация родительского класса. Здесь создаются основные ресурсы нейрона, учитывая общее количество результатов и указатель на OpenCL-контекст. Если базовая инициализация не удалась — метод сразу возвращает false, защищая систему от некорректного состояния.

Далее начинается поэтапная инициализация внутренних блоков. Блок cQK — это сердце механизма QK-внимания, где создаются ядра для генерации Q и K. Мы передаём размер окна, количество нейронов и размерности для ключевых измерений.

   uint index = 0;
   if(!cQK.Init(0, index, OpenCL, window, window, dimension_k * heads * 2, units, 1, optimization, iBatch))
      return false;
   index++;
   if(!cSpikeQK.Init(0, index, OpenCL, cQK.Neurons(), optimization, iBatch))
      return false;

Затем формируется слой преобразования сигналов QK-блока в форму спайковой активности.

Объекты cQ и cK получают половину нейронов от QK, что логично для разделения на Q и K. Мы также задаём им функцию активации, синхронизированную с cSpikeQK.

   index++;
   if(!cQ.Init(0, index, OpenCL, cQK.Neurons() / 2, optimization, iBatch))
      return false;
   cQ.SetActivationFunction((ENUM_ACTIVATION)cSpikeQK.Activation());
   index++;
   if(!cK.Init(0, index, OpenCL, cQ.Neurons(), optimization, iBatch))
      return false;
   cK.SetActivationFunction((ENUM_ACTIVATION)cSpikeQK.Activation());

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

   index++;
   uint windows[];
   if(ArrayResize(windows, heads) < (int)heads)
      return false;
   ArrayFill(windows, 0, heads, dimension_k);
   if(!cScore.Init(0, index, OpenCL, windows, 1, units, 1, optimization, iBatch))
      return false;
   cScore.SetActivationFunction(None);

Каждая голова получает своё окно размерности dimension_k.

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

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

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

   index++;
   if(!cSpikeScore.Init(0, index, OpenCL, cScore.Neurons(), optimization, iBatch))
      return false;

Далее происходит подготовка блока проекции, который очищается и получает доступ к OpenCL.

   cProject.Clear();
   cProject.SetOpenCL(OpenCL);

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

   index++;
   CNeuronBaseOCL* neuron = new CNeuronBaseOCL();
   if(!neuron ||
      !neuron.Init(0, index, OpenCL, cK.Neurons(), optimization, iBatch) ||
      !cProject.Add(neuron))
     {
      DeleteObj(neuron)
      return false;
     }

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

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

   index++;
   CNeuronSpikeConvBlock* conv = new CNeuronSpikeConvBlock();
   if(!conv ||
      !conv.Init(0, index, OpenCL, dimension_k * heads, dimension_k * heads,
                                  window, units, 1, optimization, iBatch) ||
      !cProject.Add(conv))
     {
      DeleteObj(conv)
      return false;
     }
//---
   return true;
  }

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

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

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

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

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

Сначала анализируемый сигнал передаётся в QK-блок, который формирует ядра для генерации векторов Q и K, задавая основу для внимания.

Далее сигнал преобразуется в спайковую активность с помощью cSpikeQK.

   uint units = cQK.GetUnits();
   uint window = cQK.GetWindow();
   uint dimension_k = cScore.GetWindow(0);
   uint hd = cQK.GetFilters() / 2;
   uint heads = hd / dimension_k;
//---
   if(!cSpikeQK.FeedForward(cQK.AsObject()))
      return false;
   if(!DeConcat(cQ.getOutput(), cK.getOutput(), cSpikeQK.getOutput(), hd, hd, units))
      return false;

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

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

   if(!cScore.FeedForward(cQ.AsObject()))
      return false;
   if(!cSpikeScore.FeedForward(cScore.AsObject()))
      return false;

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

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

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

   CNeuronBaseOCL* neuron = cProject[0];
   if(!neuron ||
      !ScalarToVector(cSpikeScore.getOutput(), cK.getOutput(), neuron.getOutput(), dimension_k))
      return false;

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

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

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

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

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

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

bool CNeuronSpikeQKAttention::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;
//---
   uint units = cQK.GetUnits();
   uint window = cQK.GetWindow();
   uint dimension_k = cScore.GetWindow(0);
   uint hd = cQK.GetFilters() / 2;
   uint heads = hd / dimension_k;

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

Затем извлекаются параметры структуры. Они нужны для точного соответствия размерностей сигналов и градиентов на всех уровнях модели.

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

   CNeuronBaseOCL* neuron = cProject[-1];
   if(!neuron ||
      !DeActivation(neuron.getOutput(), neuron.getGradient(), Gradient, neuron.Activation()))
      return false;

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

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

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

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

Затем градиенты делим между скоринговым блоком и векторным пространством K-блока.

   if(!ScalarToVectorGrad(cSpikeScore.getOutput(), cSpikeScore.getGradient(),
                          cK.getOutput(), cK.getGradient(),
                          neuron.getGradient(), dimension_k))
      return false;

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

Следующие шаги — обратное распространение через Q и Score блоки.

   if(!cScore.CalcHiddenGradients(cSpikeScore.AsObject()))
      return false;
   if(!cQ.CalcHiddenGradients(cScore.AsObject()))
      return false;

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

Затем происходит объединение градиентов Q и K обратно в спайковый QK-блок.

   if(!Concat(cQ.getGradient(), cK.getGradient(), cSpikeQK.getGradient(), hd, hd, units))
      return false;
   if(!cQK.CalcHiddenGradients(cSpikeQK.AsObject()))
      return false;

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

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

   if(!NeuronOCL.CalcHiddenGradients(cQK.AsObject()))
      return false;
   neuron = cProject[-1];
   if(!DeActivation(NeuronOCL.getOutput(), neuron.getPrevOutput(), Gradient, NeuronOCL.Activation()))
      return false;
   if(!SumAndNormilize(NeuronOCL.getGradient(), neuron.getPrevOutput(), NeuronOCL.getGradient(), window,
                                                                                     false, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

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

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


STSF-блок

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

Класс CNeuronSDSA наследует функционал MSRes-блока, расширяя его за счёт спайкового внимания.

class CNeuronSDSA :  public CNeuronMSRes
  {
protected:
   CNeuronSpikeQKAttention cAttention;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronSDSA(void) {};
                    ~CNeuronSDSA(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint units, uint window, uint heads, uint dimension_k,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override const  {  return defNeuronSDSA;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual void      SetOpenCL(COpenCLMy *obj)   override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      TrainMode(bool flag) override;
   virtual bool      Clear(void) override;
  };

Внутри него ключевым компонентом является объект cAttention (CNeuronSpikeQKAttention), который обеспечивает спайковое QK-внимание с многооконной свёрткой и взвешенным суммированием, описанным выше. Именно этот объект делает блок SDSA адаптивным к структуре анализируемого потока, позволяя каждой голове внимания обучать собственные параметры и формировать индивидуальные градиенты.

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

bool CNeuronSDSA::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                       uint units, uint window, uint heads, uint dimension_k,
                       ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronMSRes::Init(numOutputs, myIndex, open_cl, units, window, 1, optimization_type, batch))
      return false;

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

Следующий шаг — инициализация спайкового блока внимания.

   if(!cAttention.Init(0, 0, OpenCL, units, window, heads, dimension_k, optimization, iBatch))
      return false;
//---
   return true;
  }

Блок cAttention создаёт QK-внимание с многооконной свёрткой и взвешенным суммированием для каждой головы.

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

Алгоритм прямого прохода реализован в методе feedForward. Здесь всё построено предельно лаконично и линейно.

bool CNeuronSDSA::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cAttention.FeedForward(NeuronOCL))
      return false;
   return CNeuronMSRes::feedForward(cAttention.AsObject());
  }

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

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

Аналогично выстроены и методы обратного прохода: они следуют той же логике, аккуратно распространяя градиенты через внимание и MLP. Чтобы не перегружать текст, мы оставляем их для самостоятельного изучения; полный код класса CNeuronSDSA с реализацией всех методов представлен во вложении.

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

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


Заключение

Мы подробно рассмотрели построение и интеграцию спайкового блока внимания с остаточной MLP-структурой в рамках STSF-блока. Показали, как использование взвешенного суммирования Query и многооконной свёртки для каждой головы внимания повышает адаптивность алгоритма и точность обработки потоковых данных. Реализация CNeuronSDSA объединяет стабильность MSRes-блока и гибкость спайкового внимания, создавая единый механизм, способный выявлять локальные и глобальные зависимости.

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


Ссылки


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

# Имя Тип Описание
1 Study.mq5 Советник Советник офлайн обучения моделей
2 StudyOnline.mq5 Советник Советник онлайн обучения моделей
3 Test.mq5 Советник Советник для тестирования модели
4 Trajectory.mqh Библиотека класса Структура описания состояния системы и архитектуры моделей
5 NeuroNet.mqh Библиотека класса Библиотека классов для создания нейронной сети
6 NeuroNet.cl Библиотека Библиотека кода OpenCL-программы
Прикрепленные файлы |
MQL5.zip (3340.77 KB)
Разрабатываем менеджер терминалов (Часть 3): Получаем информацию о счёте и добавляем конфигурацию Разрабатываем менеджер терминалов (Часть 3): Получаем информацию о счёте и добавляем конфигурацию
Добавляем в наше веб-приложение возможность получения и отображения информации о торговых счетах терминалов: о балансе, прибыли, статусе подключения и другой важной информации. Также реализуем гибкую систему конфигурации, позволяющую управлять параметрами приложения через внешний JSON-файл, и улучшаем пользовательский интерфейс главной страницы.
Моделирование рынка (Часть 09): Сокеты (III) Моделирование рынка (Часть 09): Сокеты (III)
Сегодняшняя статья является продолжением предыдущей. В ней мы рассмотрим, как будет реализован советник, сосредоточившись в основном на том, как выполняется серверный код. Кода, приведенного в предыдущей статье, недостаточно для того, чтобы всё работало как надо, поэтому необходимо немного углубиться в него. Поэтому нужно прочитать обе статьи, чтобы лучше понять то, что произойдет.
Моделирование рынка (Часть 14): Сокеты (VIII) Моделирование рынка (Часть 14): Сокеты (VIII)
Многие программисты могут предположить, что нам следует отказаться от использования Excel и перейти непосредственно на Python, используя некоторые пакеты, позволяющие Python создавать Excel-файл, чтобы потом проанализировать результаты. Но, как уже говорилось в предыдущей статье, хотя это решение и является наиболее простым для многих программистов, оно не будет воспринято некоторыми пользователями. И в данном вопросе пользователь всегда прав. Мы, как программисты, должны найти способ заставить всё работать.
Оптимизатор Бонобо — Bonobo Optimizer (BO) Оптимизатор Бонобо — Bonobo Optimizer (BO)
В статье представлена реализация и анализ алгоритма Bonobo Optimizer, основанного на уникальных особенностях поведения приматов бонобо — динамической социальной структуре fission-fusion и трех стратегиях спаривания. Каковы интересные возможности этого метода?