Нейросети в трейдинге: Адаптивное масштабирование представлений (Основные компоненты)
Введение
В предыдущей статье мы познакомили читателя с фреймворком ADS, который был представлен в работе "Adaptive Domain Scaling for Personalized Sequential Modeling in Recommenders". И предложили перевести его подходы в прикладную реализацию для алгоритмической торговли. Сегодня мы концентрируемся на одном конкретном инженерном вызове. На финансовых временных рядах у нас есть одна длинная рыночная последовательность и множество интерпретаций (стратегии, уровни риска, стадии позиции). Если для каждого сценария пересчитывать ключи/значения по всей истории, вычисления и память взрываются, особенно в условиях MetaTrader 5 + OpenCL, где важны пропускная способность и предсказуемость обратного прохода. Технически это выражается в двух узких местах: стоимость пересчёта K/V по сценариям и сложность аккумулирования градиентов на GPU.
Цель этой работы — сохранить сценарную адаптивность ADS, но перенести её туда, где это дешевле и совместимо с обучением end‑to‑end: в генерацию запросов и в параметризацию проекций, не дублируя представление истории. Для масштабирования по длине окна и числу сценариев мы используем подход, аналогичный STCA (переупорядочивание матричных операций), и реализуем эффективную конкатенацию цель–сценарий на GPU без предварительного копирования. Практическая задача статьи — показать, как перестроить ADS‑идею под ограничения MT5/OpenCL, обеспечив корректный прямой и обратный проход и приемлемые по памяти/latency характеристики.

Обсуждение реализации
Прежде чем перейти к практической работе, давайте обсудим ключевые особенности нашей реализации. Сразу бросается в глаза принципиальное отличие финансовых данных от последовательностей, с которыми работают рекомендательные системы. В оригинальном фреймворке ADS анализируются действия пользователя: покупка, просмотр видео и так далее, и для каждого действия существуют явные признаки, позволяющие разделить последовательность на блоки.
В финансовых данных таких маркеров нет. Мы рассматриваем полную историю рынка и оцениваем ее через призму различных предполагаемых сценариев будущего движения цены или стратегий торговли. Следовательно, для полной исторической последовательности приходится пересчитывать набор ключей и значений каждого сценария. Это приводит к значительному росту вычислительной нагрузки и предъявляет повышенные требования к архитектуре модели.
При этом число запросов остается относительно небольшим. Наша цель одна — максимизация прибыли при минимизации рисков. Ее можно декомпозировать на стили торговли: агрессивный, умеренный, консервативный; или на этапы работы с позицией: вход, удержание, выход. Выделение этих аспектов не приводит к резкому росту количества запросов. При этом один набор сценариев используется для формирования адаптированных запросов, ключей и значений.
В таких условиях особенно эффективной становится интеграция оптимизированного подхода Stacked Target‑to‑History Cross Attention (STCA), который снижает вычислительную нагрузку при работе с длинными рыночными историями. Авторы STCA изменяют порядок вычислений — вместо классического QWQ(XWK)T используется (QWQWKT)XT. Такая перестановка позволяет масштабировать внимание линейно по длине последовательности при условии, что число запросов NQ значительно меньше длины истории NX. В результате модель может эффективно анализировать длинные временные ряды и множество торговых сценариев без критического роста затрат на вычисления, что критично для практического применения в реальном времени.
Эта оптимизация открывает новые возможности и вносит изменения в архитектуру модели. Мы дополняем работу модуля Personalized Candidate Representation Generation (PCRG) операцией умножения на матрицу WK соответствующего сценария. Благодаря этому все аспекты адаптации последовательности, которые ранее обеспечивал модуль Personalized Sequence Representation Generation (PSRG), перенесены непосредственно в PCRG. Это позволяет полностью отказаться от PSRG. Функция адаптации последовательности сохраняется, но реализуется на этапе генерации адаптированных запросов. Такой подход упрощает архитектуру, снижает вычислительные затраты и обеспечивает гибкую обработку длинных рыночных историй, сохраняя способность модели различать сценарии и формировать точные прогнозы действий.
Модуль PCRG
Теперь, когда мы определились с масштабами оптимизации и ключевыми аспектами архитектурного решения нашей реализации, можно переходить к практической части — реализации алгоритмов в коде. На этом этапе теория приобретает конкретную форму.
И здесь сразу обращает на себя внимание ключевой момент в модуле PCRG. Для генерации адаптированных запросов авторы фреймворка предлагают использовать конкатенированные пары цель‑сценарий. Это означает, что нам необходимо повторить вектор цели по числу имеющихся сценариев. А затем конкатенируем две матрицы.
Для реализации этого процесса на стороне GPU мы создаем в OpenCL-программе специализированный кернел ConcatVecMatrix, который не требует предварительного копирования вектора цели по числу сценариев. Он позволяет на лету конкатенировать повторяющиеся значения с матрицей сценариев и формирует все необходимые пары цель‑сценарий напрямую в выходной матрице. Экономит память и снижает вычислительную нагрузку, одновременно обрабатывая все строки, столбцы и запросы.
__kernel void ConcatVecMatrix(__global const float *vector_in, __global const float *matrix_in, __global float *matrix_out, const int dimension_v, const int colums_m, const int multvarsecond ) { const int r = get_global_id(0); const int c = get_global_id(1); const int v = get_global_id(2); const int rows = get_global_size(0); const int cols = get_global_size(1); const int variables = get_global_size(2);
Каждый поток операций кернела получает свои уникальные индексы строки, столбца и запроса, что позволяет одновременно обрабатывать все элементы цели и сценариев.
Сначала кернел копирует значения из вектора целей в соответствующие позиции выходной матрицы, проверяя каждый элемент на корректность и заменяя некорректные значения, такие как NaN или Inf, на ноль.
if(c < dimension_v) { int flat_v = RCtoFlat(0, c, 1, dimension_v, v); int flat_out = RCtoFlat(r, c, rows, dimension_v + colums_m, v); matrix_out[flat_out] = IsNaNOrInf(vector_in[flat_v], 0.0f); }
После этого аналогичным образом обрабатываются столбцы исходной матрицы сценариев, которые располагаются в выходной матрице сразу после столбцов целей. Для правильного отображения всех элементов используется функция преобразования индексов в плоские. Это гарантирует, что каждая строка, столбец и цель окажется на своем месте в итоговой структуре.
if(c < colums_m) { int flat_m = RCtoFlat(r, c, rows, colums_m, multvarsecond * v); int flat_out = RCtoFlat(r, c + dimension_v, rows, dimension_v + colums_m, v); matrix_out[flat_out] = IsNaNOrInf(matrix_in[flat_m], 0.0f); } }
Также следует подчеркнуть, что алгоритм кернела умеет работать с индивидуальными матрицами сценариев и одной общей для всех. На это указывает параметр multvarsecond, который фактически выполняет роль логической переменной. Такой механизм обеспечивает гибкость обработки данных и позволяет использовать один и тот же кернел в разных конфигурациях без изменения кода.
В результате кернел формирует полный набор персонализированных запросов. Каждая пара цель-сценарий представлена как отдельная комбинация в единой матрице, позволяя модели учесть различия между торговыми стратегиями.
Несмотря на то, что сам алгоритм кернела не содержит обучаемых параметров, нам все же предстоит реализовать кернел для распределения градиента ошибки. Это необходимо, поскольку мы планируем конкатенировать обучаемые запросы с матрицами сценариев, и на эти элементы нужно передавать градиент, полученный от последующих слоев модели.
Однако здесь, в отличие от прямолинейности прямого прохода, мы сталкиваемся с более сложной задачей суммирования данных. Сначала необходимо аккумулировать градиенты запросов по всем сценариям. А при использовании общей матрицы сценариев — дополнительно собрать градиенты сценариев по всем запросам. Это создает определенные трудности в организации параллельного суммирования между потоками, требуя аккуратной синхронизации и точного управления индексами.
Кернел ConcatVecMatrixGrad создает эффект аккуратного дирижирования потоком градиентов в сложной трехмерной структуре данных, где каждая комбинация цель‑сценарий оставляет свой отпечаток. Здесь потоки объединяются в рабочие группы, а для суммирования частичных значений внутри группы используется локальная память.
__kernel void ConcatVecMatrixGrad(__global float *vector_in_gr, __global float *matrix_in_gr, __global const float *matrix_out_gr, const int dimension_v, const int colums_m, const int rows, const int variables, const int multvarsecond ) { const int loc_id = get_local_id(0); const int c = get_global_id(1); const int v = get_local_id(2); const int total_loc = get_local_size(0); const int cols = get_global_size(1); const int glob_id = get_global_size(2); //--- __local float temp[LOCAL_ARRAY_SIZE];
Вначале определим градиент ошибки запросов. Каждый поток операций берет на себя свой участок строк матрицы и проходит по ним с шагом, равным размеру локальной рабочей группы, чтобы обработать все элементы.
float vec_grad = 0; for(int i = 0; (i < rows && glob_id < variables); i += total_loc) { int r = i + loc_id; if(r >= rows) continue; if(c < dimension_v) { int flat_out = RCtoFlat(r, c, rows, dimension_v + colums_m, glob_id); vec_grad += IsNaNOrInf(matrix_out_gr[flat_out], 0.0f); }
Для каждой строки вычисляется линейный индекс элемента в выходной матрице с помощью функции RCtoFlat, после чего значение градиента добавляется к локальной переменной vec_grad. Перед суммированием выполняется проверка на некорректные значения с помощью IsNaNOrInf, заменяя их на ноль, чтобы не нарушить аккумулирование.
Параллельно если используются индивидуальные матрицы сценариев (multvarsecond равен true), то градиент записывается напрямую в соответствующую позицию матрицы сценариев.
if(c < colums_m) { int flat_m = RCtoFlat(r, c, rows, colums_m, glob_id); int flat_out = RCtoFlat(r, c + dimension_v, rows, dimension_v + colums_m, glob_id); if((bool)multvarsecond) matrix_in_gr[flat_m] = IsNaNOrInf(matrix_out_gr[flat_out], 0.0f); } }
Этот механизм превращает потенциально хаотичный поток вычислений в упорядоченный и стабильный процесс, где каждый поток выполняет свою роль в общей сумме.
После завершения всех итераций цикла каждая рабочая группа выполняет локальную редукцию, суммируя градиенты запросов по всем сценариям.
if(c < dimension_v) { vec_grad = LocalSum(vec_grad, 0, temp); if(loc_id == 0) { int flat_v = RCtoFlat(0, c, 1, dimension_v, v); vector_in_gr[flat_v] = vec_grad; } }
Функция LocalSum объединяет их в единое значение для всей группы, и только один поток (loc_id == 0) записывает итоговый результат в глобальный буфер vector_in_gr по вычисленному линейному индексу. Таким образом, несмотря на распределенную обработку данных по потокам, мы получаем корректную и точную суммарную величину градиента для каждого запроса, что обеспечивает стабильное и предсказуемое обновление обучаемых параметров.
Далее при использовании общей матрицы сценариев возникает необходимость аккумулировать градиенты по всем запросам в единую матрицу. Для этого мы переопределяем смысл индексов в пространстве задач. Теперь локальный индекс потока указывает на конкретный запрос, а второе глобальное измерение определяет индекс сценария в матрице. Организуется цикл по всем запросам с шагом, равным размеру рабочей группы, чтобы каждый поток проходил через свой набор данных, не создавая конфликтов с соседними потоками.
if(!((bool)multvarsecond) && c < colums_m && glob_id < rows) { float mat_grad = 0; for(int i = 0; i < variables; i += total_loc) { int v = i + loc_id; if(v >= variables) continue; int flat_out = RCtoFlat(glob_id, c + dimension_v, rows, dimension_v + colums_m, v); mat_grad += IsNaNOrInf(matrix_out_gr[flat_out], 0.0f); }
Внутри цикла вычисляется линейный индекс элемента выходной матрицы через RCtoFlat, после чего значение градиента извлекается и проверяется на NaN или Inf, заменяя некорректные данные на ноль. Значения суммируются в переменной mat_grad.
Затем выполняется локальная редукция через LocalSum, аккумулирующая вклад всех потоков рабочей группы.
mat_grad = LocalSum(mat_grad, 0, temp); if(loc_id == 0) { int flat_m = RCtoFlat(glob_id, c, rows, colums_m, 0); matrix_in_gr[flat_m] = mat_grad; } } }
Итоговое суммарное значение записывается в глобальную память только ведущим потоком группы (loc_id == 0), что гарантирует корректное формирование градиентов для общей матрицы сценариев.
В результате кернел обеспечивает точное и аккуратное распределение градиентов на все элементы исходных данных, создавая готовую основу для обучения адаптивных запросов модуля PCRG. Это не просто механическое суммирование — алгоритм превращает сложную структуру параллельных вычислений в слаженный процесс, где каждый поток знает свое место и роль, а модель получает корректную обратную связь для адаптации к различным сценариям и целям.
Следующим этапом нашей работы становится непосредственная реализация алгоритмов модифицированного модуля PCRG на стороне основной программы. Класс CNeuronPCGR является ядром нашей реализации и аккумулирует весь необходимый функционал для адаптивной работы с финансовыми последовательностями. Его основная задача — обеспечить генерацию контекстно-зависимых запросов на основе конкатенированных пар цель–сценарий и корректную обработку градиентов при обучении модели.
class CNeuronPCGR : public CNeuronTransposeRCDOCL { protected: uint iWindowIn; uint iInsideDimension; uint iWindowOut; uint iQuerys; uint iScenarios; //--- CNeuronFieldAwareConv cShared; CFieldAwareParams cScenarios; CNeuronBaseOCL cQueryVsScenarios; CNeuronWeightGenerator cWQscenarios; CNeuronBaseOCL cGeneratedQuery; CNeuronTransposeRCDOCL cQScInToScQIn; CFieldAwareParams cWKshared; CNeuronWeightGenerator cWKscenarios; CNeuronTransposeOCL cWKscT; CNeuronBaseOCL cWKT; CNeuronTransposeOCL cWK; CNeuronBaseOCL cQueryMod; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronPCGR(void) {}; ~CNeuronPCGR(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_out, uint querys, uint scenarios, 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 int Type(void) override const { return defNeuronPCGR; } virtual void SetOpenCL(COpenCLMy *obj) override; virtual void TrainMode(bool flag) override; virtual void SetActivationFunction(ENUM_ACTIVATION value) override { }; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; };
Внутренние поля класса отражают ключевые аспекты архитектуры. Параметры iWindowIn и iWindowOut задают размеры входных и выходных окон. А iQuerys и iScenarios определяют число адаптивных запросов и сценариев. Основные вычислительные объекты аккумулируют данные и управляют преобразованиями эмбеддингов. При этом генераторы весов позволяют формировать матрицы адаптивной проекции анализируемых данных.
Метод инициализации выступает как полноценный конструктор архитектуры блока PCRG. В нем последовательно выстраивается цепочка взаимосвязанных преобразований. С самого начала происходит инициализация родительского класса, что задает общую структуру.
bool CNeuronPCGR::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_out, uint querys, uint scenarios, uint embed_size, uint candidates, uint topK, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronTransposeRCDOCL::Init(numOutputs, myIndex, open_cl, scenarios, querys, window_out, optimization_type, batch)) ReturnFalse; activation = None;
Функция активации отключается и это логично. На уровне агрегирующего блока мы работаем с линейными преобразованиями и переносим нелинейность внутрь отдельных компонентов.
Далее фиксируются ключевые параметры.
iWindowIn = window; iWindowOut = window_out; iInsideDimension = (iWindowOut + 3) / 4; iQuerys = querys; iScenarios = scenarios;
Величина iInsideDimension играет роль промежуточного пространства признаков. Своего рода рабочей сцены, на которой происходит вся дальнейшая адаптация.
После этого начинается поэтапная сборка вычислительного графа. Объект cShared формирует общее представление запросов, извлекая устойчивые закономерности рынка.
uint index = 0; if(!cShared.Init(0, index, OpenCL, iWindowIn, iInsideDimension, iQuerys, embed_size, candidates, topK, optimization, iBatch)) ReturnFalse; cShared.SetActivationFunction(TANH);
Здесь используется активация TANH, что позволяет сгладить экстремальные значения и привести данные к стабильному диапазону.
Следом инициализируется cScenarios, который отвечает за представление сценариев в том же внутреннем пространстве.
index++; if(!cScenarios.Init(0, index, OpenCL, iInsideDimension, iScenarios, embed_size, candidates, topK, optimization, iBatch)) ReturnFalse; cScenarios.SetActivationFunction(TANH);
Таким образом, уже на этом этапе формируются две согласованные проекции.
Далее эти два потока объединяются в cQueryVsScenarios.
index++; if(!cQueryVsScenarios.Init(0, index, OpenCL, 2 * iInsideDimension * iQuerys * iScenarios, optimization, iBatch)) ReturnFalse; cQueryVsScenarios.SetActivationFunction(None);
Здесь фактически происходит формирование тех самых пар цель-сценарий, но уже в развернутом виде — через полную конкатенацию по всем комбинациям запросов и сценариев. Это пространство становится отправной точкой для генерации адаптивных весов.
Объект cWQscenarios выполняет роль генератора матриц преобразования запросов. Он получает на вход объединенное представление и формирует веса, которые затем применяются к исходным данным.
index++; if(!cWQscenarios.Init(0, index, OpenCL, 2 * iInsideDimension, iWindowIn * iInsideDimension, iQuerys * iScenarios, optimization, iBatch)) ReturnFalse; cWQscenarios.SetActivationFunction(None);
Важно, что здесь размерность выходного пространства напрямую связана с произведением числа запросов и сценариев, что отражает идею полной персонализации.
Результат этого этапа проходит через cGeneratedQuery, где применяется нелинейность TANH, стабилизируя сформированные запросы и подготавливая их к дальнейшему использованию в механизме внимания.
index++; if(!cGeneratedQuery.Init(0, index, OpenCL, iInsideDimension * iQuerys * iScenarios, optimization, iBatch)) ReturnFalse; cGeneratedQuery.SetActivationFunction(TANH);
Параллельно формируется вторая ветка — генерация матриц ключей. Это аккуратная интеграция PSRG, о которой говорилось выше.
Сначала cWKshared задает базовую, общую структуру преобразования, отражающую универсальные закономерности.
index++; if(!cWKshared.Init(0, index, OpenCL, iInsideDimension * iWindowOut, iScenarios, embed_size, candidates, topK, optimization, iBatch)) ReturnFalse; cWKshared.SetActivationFunction(None);
Затем cWKscenarios генерирует сценарно-зависимую корректировку с использованием SIGMOID. Фактически выступая в роли маски или фильтра, который усиливает или подавляет отдельные компоненты.
index++; if(!cWKscenarios.Init(0, index, OpenCL, iInsideDimension, iInsideDimension * iWindowOut, iScenarios, optimization, iBatch)) ReturnFalse; cWKscenarios.SetActivationFunction(SIGMOID);
Далее нам необходимо поэлементно умножить маску сценариев на общие параметры проекции cWKshared. Но нас встречает ограничение поэлементного умножения — матрицы должны быть равны. У нас же для каждого сценария генерируется своя маска, что в разы увеличивает тензор результатов.
Выход был найден несколько неординарный. Сначала мы транспонируем тензор маски таким образом, чтобы в последнем измерении были сценарии.
index++; if(!cWKscT.Init(0, index, OpenCL, iScenarios, cWKshared.Neurons(), optimization, iBatch)) ReturnFalse; cWKscT.SetActivationFunction((ENUM_ACTIVATION)cWKscenarios.Activation());
Далее мы переформатируем тензор в матрицу, число строк которой равно линейному размеру общей матрицы. А общую матрицу представляем в виде вертикального вектора. И осуществляем операцию построчного умножения скаляра на вектор. Результат записываем в cWKT.
index++; if(!cWKT.Init(0, index, OpenCL, cWKscT.Neurons(), optimization, iBatch)) ReturnFalse; cWKT.SetActivationFunction(None);
После этого нам остается провести обратное транспонирование полученного тензора.
index++; if(!cWK.Init(0, index, OpenCL, cWKshared.Neurons(), iScenarios, optimization, iBatch)) ReturnFalse; cWK.SetActivationFunction((ENUM_ACTIVATION)cWKT.Activation());
Финальный элемент cQueryMod завершает формирование запросов, приводя их к размерности выходного слоя.
index++; if(!cQueryMod.Init(0, index, OpenCL, Neurons(), optimization, iBatch)) ReturnFalse; cQueryMod.SetActivationFunction(None); //--- return true; }
В результате метод инициализации буквально собирает архитектуру как механизм. Каждый элемент занимает свое строго определенное место. А вся конструкция в итоге работает как единая система, ориентированная на эффективный анализ длинных рыночных последовательностей.
Метод feedForward реализует последовательный и строго выверенный процесс формирования адаптивных запросов. Каждая операция логически продолжает предыдущую и постепенно переводит анализируемую цель в пространство сценарно-зависимых представлений.
bool CNeuronPCGR::feedForward(CNeuronBaseOCL *NeuronOCL) { //--- Shared Query if(!cShared.FeedForward(NeuronOCL)) ReturnFalse;
Сначала формируется базовое, общее представление цели через cShared. Это фундаментальный слой, который извлекает устойчивые представления, не привязанные к конкретному сценарию. По сути, здесь модель смотрит в нейтральной постановке, без интерпретации.
Далее, в режиме обучения, активируются генераторы сценариев и базовой матрицы проекции ключей. Эти элементы можно рассматривать как источник контекста, который будет управлять дальнейшей адаптацией.
//--- Generator if(bTrain) { if(!cScenarios.FeedForward()) ReturnFalse; if(!cWKshared.FeedForward()) ReturnFalse; }
Следующий шаг — ключевой для всей архитектуры. С помощью ConcatVecMatrix происходит объединение общего представления цели и сценариев. Причем это делается без предварительного копирования, формируя полное пространство комбинаций запрос-сценарий.
//--- Private Query if(!ConcatVecMatrix(cShared.getOutput(), cScenarios.getOutput(), cQueryVsScenarios.getOutput(), iInsideDimension, iScenarios, iInsideDimension, iQuerys, false)) ReturnFalse;
Здесь модель буквально разворачивает все возможные точки зрения на одну и ту же рыночную историю.
Полученное пространство передается в cWQscenarios, который генерирует адаптивные матрицы преобразования запросов.
if(!cWQscenarios.FeedForward(cQueryVsScenarios.AsObject()))
ReturnFalse;
Это уже не фиксированные веса, а динамически сформированные параметры, зависящие от сценария. Далее через матричное умножение эти веса применяются к исходному входу, формируя персонализированные запросы.
if(!MatMul(cWQscenarios.getOutput(), NeuronOCL.getOutput(), cGeneratedQuery.getOutput(), iInsideDimension * iScenarios, iWindowIn, 1, iQuerys, true)) ReturnFalse; dActivation(cGeneratedQuery);
После этого применяется нелинейность, стабилизирующая распределение значений.
Затем происходит важный шаг объединения. С помощью SumVecMatrix к сгенерированным запросам добавляется базовое представление cShared. Это аккуратное наложение общей структуры рынка и сценарной интерпретации.
//--- Shared + Private if(!SumVecMatrix(cShared.getOutput(), cGeneratedQuery.getOutput(), cGeneratedQuery.getOutput(), iInsideDimension, iQuerys, 0, 0, 0, 1)) ReturnFalse;
Нормализация завершает этот этап, выравнивая масштаб признаков и подготавливая данные к следующему преобразованию.
if(!Normilize(cGeneratedQuery.getOutput(), iInsideDimension))
ReturnFalse;
Параллельно формируется матрица ключей. Сначала cWKscenarios генерирует сценарно-зависимую компоненту.
//--- W Key if(!cWKscenarios.FeedForward(cScenarios.AsObject())) ReturnFalse;
Затем она транспонируется и объединяется с глобальной матрицей cWKshared через ScalarToVector.
if(!cWKscT.FeedForward(cWKscenarios.AsObject())) ReturnFalse; if(!ScalarToVector(cWKshared.getOutput(), cWKscT.getOutput(), cWKT.getOutput(), iScenarios)) ReturnFalse;
Это создает итоговое представление ключей, в котором общие закономерности корректируются маской сценарной специфики.
Финальное преобразование через cWK приводит данные к нужному формату.
if(!cWK.FeedForward(cWKT.AsObject()))
ReturnFalse;
После этого нам предстоит скорректировать адаптивные запросы на проекции WK соответствующих сценариев. Для этого мы сначала перестроим структуру запросов через cQScInToScQIn.
//--- Modify Query if(!cQScInToScQIn.FeedForward(cGeneratedQuery.AsObject())) ReturnFalse;
На этом этапе запросы и ключи уже полностью согласованы по размерностям и семантике. Выполняется матричное умножение, в котором запросы проецируются через адаптивные ключи, формируя итоговое представление cQueryMod.
if(!MatMul(cQScInToScQIn.getOutput(), cWK.getOutput(), cQueryMod.getOutput(), iQuerys, iInsideDimension, iWindowOut, iScenarios, true)) ReturnFalse;
Это уже концентрированная информация о том, какие аспекты рыночной истории релевантны для каждого сценария. Финальная нормализация стабилизирует результат.
if(!Normilize(cQueryMod.getOutput(), iWindowOut)) ReturnFalse; if(!CNeuronTransposeRCDOCL::feedForward(cQueryMod.AsObject())) ReturnFalse; //--- return true; }
После чего управление передается родительскому классу, где происходит обратная перестройка структуры данных модифицированных запросов.
Алгоритм обратного распространения в данном случае уже не выглядит линейной зеркальной копией прямого прохода, а больше напоминает аккуратную разборку сложного механизма. Один сигнал ошибки должен быть корректно распределен по нескольким ветвям, каждая из которых в прямом проходе участвовала в формировании итогового представления.
bool CNeuronPCGR::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) ReturnFalse;
Процесс начинается с финального блока cQueryMod. Через вызов одноименного метода родительского класса мы получаем исходный градиент, который далее начинаем распаковывать по всем предыдущим преобразованиям.
//--- Modify Query if(!CNeuronTransposeRCDOCL::calcInputGradients(cQueryMod.AsObject())) ReturnFalse;
Сразу после этого градиент разделяется между матрицей ключей cWK и преобразованными запросами cQScInToScQIn.
if(!MatMulGrad(cQScInToScQIn.getOutput(), cQScInToScQIn.getGradient(), cWK.getOutput(), cWK.getGradient(), cQueryMod.getGradient(), iQuerys, iInsideDimension, iWindowOut, iScenarios, true)) ReturnFalse;
Это критическая точка. Именно здесь ошибка начинает расходиться по двум основным потокам.
Далее градиент проходит через cGeneratedQuery, возвращаясь к пространству персонализированных запросов.
if(!cGeneratedQuery.CalcHiddenGradients(cQScInToScQIn.AsObject()))
ReturnFalse;
Это обратный путь той самой адаптации, которая формировалась через сценарии. Параллельно начинается разбор ветки ключей. Через cWKT и ScalarToVectorGrad градиент декомпозируется на глобальную компоненту cWKshared и сценарно-зависимую cWKscT.
//--- W Key if(!cWKT.CalcHiddenGradients(cWK.AsObject())) ReturnFalse; if(!ScalarToVectorGrad(cWKshared.getOutput(), cWKshared.getGradient(), cWKscT.getOutput(), cWKscT.getGradient(), cWKT.getGradient(), iScenarios)) ReturnFalse;
Фактически мы разделяем вклад ошибки на общую геометрию и сценарную поправку. Это тонкий момент. Модель уточняет, где ошибка связана с универсальными закономерностями рынка, а где — с конкретной интерпретацией.
После этого градиент проходит через cWKscenarios обратно к представлению сценариев. Выполняется деактивация, что возвращает значения из пространства активации в линейное, обеспечивая корректность градиентных вычислений.
if(!cWKscenarios.CalcHiddenGradients(cWKscT.AsObject())) ReturnFalse; Deactivation(cWKshared); Deactivation(cWKscenarios); if(!cScenarios.CalcHiddenGradients(cWKscenarios.AsObject())) ReturnFalse;
Затем cScenarios получает свою долю ошибки — уже с учетом всех трансформаций, которые она инициировала.
Возвращаясь к ветке запросов, где происходит аккумулирование градиентов, пришедших из объединенного представления Shared + Private.
//--- Shared + Private if(!SumVecMatrixGrad(cShared.getGradient(), cGeneratedQuery.getPrevOutput(), cGeneratedQuery.getGradient(), iInsideDimension, iQuerys, 0, 0, 0, 1)) ReturnFalse;
Это тот самый момент, где общий и сценарный вклад снова разворачиваются. Далее, после деактивации cGeneratedQuery, выполняется обратное матричное умножение, распределяющее ошибку между весами генератора cWQscenarios и исходными данными.
//--- Private Query Deactivation(cGeneratedQuery); if(!MatMulGrad(cWQscenarios.getOutput(), cWQscenarios.getGradient(), NeuronOCL.getOutput(), NeuronOCL.getGradient(), cGeneratedQuery.getGradient(), iInsideDimension * iScenarios, iWindowIn, 1, iQuerys, true)) ReturnFalse;
Следующий шаг — один из самых чувствительных с точки зрения архитектуры. Через cQueryVsScenarios и ConcatVecMatrixGrad градиент разделяется обратно на две сущности: общее представление и сценарии.
if(!cQueryVsScenarios.CalcHiddenGradients(cWQscenarios.AsObject())) ReturnFalse; if(!ConcatVecMatrixGrad(cShared.getPrevOutput(), cScenarios.getPrevOutput(), cQueryVsScenarios.getGradient(), iInsideDimension, iScenarios, iInsideDimension, iQuerys, false)) ReturnFalse;
Здесь проявляется специфика нашей реализации. Ошибка должна быть корректно суммирована по всем сценариям для вектора цели и, при необходимости, агрегирована по всем запросам для общей матрицы сценариев. Это не просто обратное копирование — это контролируемая редукция, в которой важно не потерять вклад ни одного потока.
Далее выполняется суммирование градиентов для cShared, после чего применяется деактивация.
if(!SumAndNormilize(cShared.getGradient(), cShared.getPrevOutput(), cShared.getGradient(), iInsideDimension, false, 0, 0, 0, 1)) ReturnFalse; Deactivation(cShared);
Аналогичный процесс проходит и для cScenarios, с учетом функции активации.
if(cScenarios.Activation() != None) if(!DeActivation(cScenarios.getOutput(), cScenarios.getPrevOutput(), cScenarios.getPrevOutput(), cScenarios.Activation())) ReturnFalse; if(!SumAndNormilize(cScenarios.getGradient(), cScenarios.getPrevOutput(), cScenarios.getGradient(), iInsideDimension, false, 0, 0, 0, 1)) ReturnFalse;
Это завершает восстановление градиентов на уровне внутренних представлений.
Финальный этап — возврат градиента на уровень исходных данных NeuronOCL. Здесь снова учитывается активация и выполняется ее обратное преобразование.
//--- Shared Query if(!NeuronOCL.CalcHiddenGradients(cShared.AsObject())) ReturnFalse; if(NeuronOCL.Activation() != None) if(!DeActivation(NeuronOCL.getOutput(), NeuronOCL.getPrevOutput(), NeuronOCL.getPrevOutput(), NeuronOCL.Activation())) ReturnFalse; if(!SumAndNormilize(NeuronOCL.getGradient(), NeuronOCL.getPrevOutput(), NeuronOCL.getGradient(), iWindowIn, false, 0, 0, 0, 1)) ReturnFalse; //--- return true; }
В итоге модель получает корректный градиент на самом входе, замыкая полный цикл обучения.
Вся эта схема работает как система сообщающихся сосудов. Ошибка, возникшая на выходе, аккуратно распределяется между всеми компонентами. При этом каждая операция не просто передает градиент дальше, а переосмысливает его в контексте своей роли. Именно это и делает обучение устойчивым. Модель не размазывает ошибку, а точно понимает, где и почему возникло отклонение.
Заключение
В статье мы показали, как адаптивность ADS можно практически реализовать в задачах анализа финансовых рядов, оформив сценарность не через дублирование всей истории, а через генерацию контекстно‑зависимых запросов и сценарно‑зависимых матриц проекций. Основные достижения и практические артефакты:
- архитектурное решение: перенос части функционала PSRG в модифицированный PCRG — сценарная адаптация теперь формируется в пространстве запросов/весов, а не путём полных копий истории;
- вычислительная оптимизация: использование STCA‑подобного порядка операций для линейного масштабирования по длине истории при малом числе запросов;
- реализация на GPU/MT5: два OpenCL‑кернела ConcatVecMatrix и ConcatVecMatrixGrad (формирование пар цель–сценарий на лету и аккумулирование градиентов) и класс CNeuronPCGR, инкапсулирующий forward/backward‑граф PCRG с обучаемыми параметрами сценариев.
Практически это значит: вы получаете готовый модуль PCRG для встраивания в модель внимания. Задаёте число сценариев/запросов/окон и получаете нормализованные сценарно‑специфичные запросы, совместимые с последующими шагами внимания и принятия решения. При этом архитектура экономит память и снижает накладные расходы на пересчёт представлений.
В следующей статье мы подробно опишем интеграцию CNeuronPCGR в торговые модели и представим эмпирическую оценку на исторических данных.
Ссылки
Программы, используемые в статье
| # | Имя | Тип | Описание |
|---|---|---|---|
| 1 | Study.mq5 | Советник | Советник офлайн-обучения моделей |
| 2 | StudyOnline.mq5 | Советник | Советник онлайн-обучения моделей |
| 3 | Test.mq5 | Советник | Советник для тестирования модели |
| 4 | Trajectory.mqh | Библиотека класса | Структура описания состояния системы и архитектуры моделей |
| 5 | NeuroNet.mqh | Библиотека класса | Библиотека классов для создания нейронной сети |
| 6 | NeuroNet.cl | Библиотека | Библиотека кода OpenCL-программы |
Проект представлен по ссылке.
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Автоматизация торговых стратегий на MQL5 (Часть 23): Зональное восстановление с трейлинг-стопом и логикой корзин
Архитектура коллективных торговых решений ИИ-агентов
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования