Нейросети в трейдинге: Управление риском через распределение результатов (Объект верхнего уровня)
Введение
При построении торговой модели мало смотреть только на средний результат. Две стратегии могут показать одинаковую доходность, но прийти к ней совершенно разными путями. Одна зарабатывает относительно ровно. Другая компенсирует глубокие убытки редкими крупными прибылями. Для трейдера это разные профили риска. Вместе с ожидаемой прибылью нужно видеть и неблагоприятный хвост распределения.
Подобную проблему рассматривают Mehrdad Moghimi и Hyejin Ku в работе "Risk-sensitive actor-critic with static spectral risk measures for online and offline reinforcement learning". В предложенном фреймворке AC-SRM отношение к риску входит непосредственно в оптимизируемую цель. Вместо одного математического ожидания используется статическая спектральная мера риска. Она назначает разный вес отдельным квантилям полного возврата. Нижний хвост можно учитывать сильнее, чем благоприятную область распределения. При этом профиль риска задается для всего возврата траектории, а не как отдельный локальный штраф на каждом шаге.
Адаптацию этого подхода к финансовым рынкам мы начали в первой статье. В ней разобрали двухуровневую оптимизацию и роль распределительного Критика. Для статической меры риска одного скалярного значения недостаточно. Нужна оценка распределения возврата. Она позволяет отделить частые умеренные исходы от редких крупных отклонений. Практическую часть мы начали с градиента по наблюдаемой целевой величине.
Основой распределительного представления в нашей работе стал ранее реализованный фреймворк FQF. Он делит вероятностную ось на адаптивные интервалы и оценивает значение квантильной функции на выбранных уровнях. Сетка не задается заранее. Модель сама распределяет разрешение между областями, где ей требуется больше деталей. Для рынка это удобно: центральная часть распределения и редкие крайние исходы требуют разной точности.
Во второй статье мы подготовили OpenCL-примитивы для спектральной оценки. Реализовали выборку из распределения, прямой расчёт SRM и распространение градиента. Также учли влияние вероятностных масс на квантильное кодирование. Каждый примитив решает локальную задачу. Теперь их нужно объединить в один нейронный объект и согласовать общий поток данных в прямом и обратном направлении.
Но между готовыми примитивами и полноценным слоем остаётся ещё одна задача. В FQF вероятностные массы задают ширины адаптивных интервалов квантильной оси, а значения описывают оценки квантильной функции на связанных с ними уровнях. После очередного обновления эти значения не обязаны оставаться строго упорядоченными. Соседние представители могут поменяться местами. Для спектральной меры это критично, потому что риск-вес зависит от ранга результата.
В дальнейшем пары (pi, zi) используются как вычислительное представление для спектральной оценки. Это упрощение не превращает их в обычные атомы дискретного распределения. Временное упорядочивание позволяет корректно прочитать хвост в текущем SRM-расчёте, но не восстанавливает исходную адаптивную квантильную сетку FQF и не устраняет пересечение квантилей в самой модели.
Текущая статья посвящена устройству объекта нового SRM слоя. Проследим путь от готового распределения до спектральной оценки и обратно к градиенту. Основной вопрос — как временно упорядочить исходы, не разрушая структуру FQF, а затем вернуть корректирующий сигнал в исходную индексацию.

Проблемы построения SRM-слоя
FQF формирует вероятностные массы pi и связанные с ними значения zi. Массы задают интервалы на оси квантилей, а значения описывают соответствующие уровни возврата. Для математического ожидания порядок пар не важен: совместная перестановка не меняет сумму pizi. У спектральной меры другая логика. Вес исхода зависит от его положения внутри распределения.
Рассмотрим три результата: 10, −40 и 30 USD с вероятностями 0.3, 0.2 и 0.5. Их математическое ожидание равно 10 USD и не зависит от порядка пар. Для CVaR с α = 0.2 важны худшие 20% распределения. Они должны соответствовать −40 USD. Если использовать исходный порядок массива, первые 20% попадут в область результата 10 USD. Формула останется корректной, но смысл оценки будет потерян. Модель начнёт считать положительный исход частью нижнего хвоста.
В этой работе CVaR задан по возврату, который мы максимизируем. Уровень α выделяет нижние α квантилей доходности. Большее значение такой оценки предпочтительнее. В классической записи по положительной величине убытка L = −G знак и сторона хвоста меняются. Эти две конвенции смешивать нельзя.
Проблема появляется из-за возможного нарушения монотонности. В идеальном квантильном представлении значения идут по возрастанию: z1 ≤ z2 ≤ ... ≤ zN. Нейронная сеть не обязана сохранять этот порядок после каждого обновления. Соседние представители могут пересекаться. Сам набор прогнозируемых исходов от этого не меняется. Ошибка возникает тогда, когда внутренний индекс начинает определять риск-вес.
Перед расчётом SRM нужно отделить внутренний порядок FQF от ранга финансовых результатов. В нашем примере последовательность 10, −40, 30 USD временно превращается в −40, 10, 30 USD. Массы переносятся вместе со связанными значениями. Получаем пары (−40, 0.2), (10, 0.3) и (30, 0.5). В этом временном представлении нижняя часть вероятностной оси соответствует худшим результатам. Это достаточно для текущего спектрального расчёта, но не означает восстановления монотонной квантильной функции FQF.
Исходное представление FQF менять нельзя. Его индексы участвуют в квантильном кодировании и обратном проходе. Перестановка внутри базового объекта изменила бы существующий вычислительный граф. Для спектральных операций создаётся временная упорядоченная копия. После расчёта SRM исходные буферы остаются прежними.
Одной копии отсортированных значений недостаточно. Нужно сохранить происхождение каждого элемента. Введём перестановку I. Для нашего примера I = (1, 0, 2). Первый элемент нового представления пришел из позиции 1, второй — из позиции 0, третий сохранил позицию 2. По тем же индексам переносится вероятностная масса. Карта I связывает временное спектральное представление с исходным распределением.
Преобразование работает в двух направлениях. Перед спектральной оценкой пары собираются в порядке роста результата. При обратном проходе каждый сигнал возвращается по сохраненному исходному индексу:
gIjoriginal = gjordered.
При различных значениях элементов перестановка локально постоянна, поэтому обратное отображение даёт обычный градиент почти всюду. При точном равенстве функция порядка не дифференцируема. Текущая реализация снимает неоднозначность, используя исходный индекс, и проводит градиент по выбранной ветви. Это детерминированное правило, а не новая производная сортировки. Для почти равных значений ранг может меняться между соседними итерациями. Тогда хвостовой градиент способен изменяться скачком. Текущая реализация этот эффект не сглаживает, поэтому такое поведение следует учитывать как источник дополнительного шума обучения.
Перестановка решает только часть задачи. Спектральная оценка зависит и от значений zi, и от масс pi. Значения задают финансовый результат. Вероятности определяют границы интервалов, на которые накладывается спектр риска. Изменение массы поэтому сдвигает границы даже при неизменных значениях. Для хвостовых мер это может изменить долю результата, попавшую в область повышенного веса.
В FQF вероятности участвуют ещё и в квантильном кодировании. Изменение массы сдвигает границы интервалов, а вместе с ними — квантильные координаты. Через эти координаты меняются и прогнозируемые значения. У вероятностной ветви поэтому два пути влияния. Первый идёт напрямую через спектральные веса. Второй проходит через квантильное кодирование и ветвь значений. Потеря любого из них сделает обратный проход неполным.
Отдельно нужно контролировать качество самого распределения. Для SRM нужны конечные значения, неотрицательные вероятности и корректная суммарная масса. Простая подмена поврежденных данных защитным нулём здесь нежелательна. Нулевой торговый результат сам по себе содержателен: это может быть безубыточная сделка или отсутствие изменения стоимости позиции. Поэтому вычислительную ошибку лучше отделять явным статусом валидности и исключать такой результат из дальнейшего обучения. В текущем объекте для этого используются диагностические поля и флаг m_last_srm_invalid.
Рабочие преобразования распределения выполняются на GPU. Упорядоченные пары, карта перестановки и градиенты остаются в OpenCL-контексте — перенос на CPU не требуется. Однако проверка численной корректности в текущей реализации читает данные на стороне MQL5. Такое чтение создаёт точку синхронизации CPU/GPU и при частых вызовах может стать заметной частью стоимости SRMForward и SRMGradient. Здесь приоритет отдан диагностике и контролю корректности. Для производительного цикла эту проверку целесообразно переносить в GPU-редукции и возвращать на CPU только компактный статус.
Итак, слой должен решить четыре связанные задачи:
- Сохранить исходную механику FQF.
- Построить временный порядок финансовых результатов и перенести вместе с ними вероятности.
- Провести спектральную оценку.
- Вернуть градиенты в исходную индексацию и учесть оба пути вероятностной ветви.
Из этих требований складывается архитектура текущей реализации CNeuronACSRM. Она решает локальную задачу спектральной интерпретации распределительного выхода и не является самостоятельной реализацией всего AC-SRM.
Архитектура объекта
Новый объект строим как наследник CNeuronFQF. Базовый класс уже умеет формировать вероятности, квантильное кодирование и значения результата. CNeuronACSRM не создаёт вторую прогнозную голову и не добавляет обучаемых параметров. Его задача — подготовить представление распределения для SRM и правильно провести специализированный градиент обратно.
class CNeuronACSRM : public CNeuronFQF { protected: bool m_distribution_gradient_ready; bool m_rearrangement_ready; int m_last_invalid_quantile; int m_last_invalid_probability; int m_last_invalid_mass; int m_last_spectral_break; bool m_last_srm_invalid; //--- CBufferFloat m_input_gradient_scratch; CBufferFloat m_ordered_quantiles; CBufferFloat m_ordered_probabilities; CBufferFloat m_rearrange_order; CBufferFloat m_ordered_quantiles_gradient; CBufferFloat m_ordered_probabilities_gradient; CBufferFloat m_rearrange_diagnostics; //--- bool EnsureRearrangeBuffers(const uint tasks, const uint quantiles); virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; bool propagateACSRMGradients(CNeuronBaseOCL *NeuronOCL); //--- public: CNeuronACSRM(void); ~CNeuronACSRM(void); //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint actions, uint quantiles, uint numInputs, ENUM_OPTIMIZATION optimization_type, uint batch) override; //--- virtual bool calcDistributionTargetGradients(CNeuronBaseOCL *NeuronOCL, float target, float probability_weight = 1.0f, float value_weight = 1.0f); virtual bool Sample(CBufferFloat *result, CBufferFloat *random, const uint tasks = 1); virtual bool SRMForward(CBufferFloat *result, const uint tasks = 1, const int risk_type = defSRM_MEAN, const float risk_parameter = 0.1f, const float mean_weight = 0.0f); virtual bool SRMGradient(CBufferFloat *result_gradient, const uint tasks = 1, const int risk_type = defSRM_MEAN, const float risk_parameter = 0.1f, const float mean_weight = 0.0f); bool MonotoneRearrange(CBufferFloat *ordered_quantiles, CBufferFloat *ordered_probabilities, CBufferFloat *order, CBufferFloat *diagnostics, const uint tasks = 1); bool RearrangeGradient(CBufferFloat *ordered_quantiles_gradient, CBufferFloat *ordered_probabilities_gradient, CBufferFloat *order, CBufferFloat *quantiles_gradient, CBufferFloat *probabilities_gradient, const uint tasks = 1); bool ValidateDistributionForSRM(const uint tasks, const int risk_type, const float risk_parameter, const float mean_weight, const bool spectral_break_diagnostics); bool GetDistributionDiagnostics(int &invalid_quantile, int &invalid_probability, int &invalid_mass, int &spectral_break) const; bool LastSRMInvalid(void) const { return m_last_srm_invalid; } bool GetRearrangeBuffers(CBufferFloat *&ordered_quantiles, CBufferFloat *&ordered_probabilities, CBufferFloat *&order, CBufferFloat *&diagnostics); bool GetOrderedGradientBuffers(CBufferFloat *&ordered_quantiles_gr, CBufferFloat *&ordered_probabilities_gr); bool GetDistributionBuffers(CBufferFloat *&quantiles, CBufferFloat *&probabilities); bool GetDistributionGradientBuffers(CBufferFloat *&quantiles_gr, CBufferFloat *&probabilities_gr); bool GetHeadWeightNorm(double &weight_norm); //--- virtual int Type(void) override const { return defNeuronACSRM; } virtual void SetOpenCL(COpenCLMy *obj) override; virtual bool SupportsOpenCLChecked(void) const override; virtual bool SetOpenCLChecked(COpenCLMy *obj) override; virtual bool Load(int const file_handle) override; };
Выход cSoftMax содержит вероятностные массы
P = {p0, p1, ..., pN−1},
а cQuantile2 — связанные с ними значения результатов:
Z = {z0, z1, ..., zN−1}.
Один индекс связывает обе последовательности. Масса pi задает ширину соответствующего интервала квантильной оси, а zi является оценкой квантильной функции на связанном с ним уровне. Ниже эту пару будем использовать как единый вычислительный представитель, не отождествляя её с атомом обычного дискретного распределения.
Для мер, зависящих от ранга результата, создаётся второе рабочее представление. Значения располагаются по возрастанию, а связанные массы перемещаются вместе с ними. Это представление используется только для спектрального чтения текущей аппроксимации:
(Z, P) → I → (Z*, P*),
I хранит исходный индекс каждого элемента нового представления:
z*j = zIj, p*j = pIj.
Для временного представления CNeuronACSRM использует буферы m_ordered_quantiles, m_ordered_probabilities и m_rearrange_order. Карта индексов хранится в CBufferFloat (float32), что допустимо, пока используемые индексы представимы точно. Для существенно больших размерностей потребуется целочисленный буфер. Исходные выходы FQF не меняются, а временные данные считаются актуальными только для текущего распределения.
При математическом ожидании порядок пар не влияет на результат. Режим SRM_MEAN работает с исходными выходами. Для CVaR, Mean-CVaR и других неравномерных спектров используется упорядоченное представление. Лишняя перестановка для среднего значения не выполняется.
Одна задача task соответствует одному независимому распределению. При пакетной обработке и нескольких действиях каждая пара (batch, action) передаётся отдельной задачей. Упорядочивание, проверка, спектральная оценка и обратная перестановка выполняются только внутри неё. Смешивание элементов разных действий или состояний недопустимо.
Для обратного прохода предусмотрены m_ordered_quantiles_gradient и m_ordered_probabilities_gradient. Спектральный градиент рассчитывается в упорядоченном пространстве. Перестановка I возвращает его исходным элементам. Значения и вероятности проходят одну и ту же обратную карту:
gIjZ = gjZ*, gIjP = gjP*.
После обратной перестановки градиенты снова принадлежат исходным элементам FQF. Дальнейшее распространение идёт по обычной структуре базового слоя.
Буфер m_input_gradient_scratch решает другую задачу. Вероятности влияют и на спектральные интервалы, и на квантильное кодирование. Эти сигналы приходят к общему входу разными путями. Один вклад временно сохраняется, пока рассчитывается второй. Буфер создаётся размером cCosineEmbeding.Neurons(). Имя cCosineEmbeding сохранено в написании исходного поля библиотеки. Специализированный путь допустим только если эта размерность равна числу входов NeuronOCL. Ниже это равенство проверяется перед запуском FQF_FractionGradient; при нарушении контракт считается нарушенным и выполнение завершается ошибкой. В конце оба сигнала складываются.
Флаг m_rearrangement_ready показывает актуальность перестановки, а m_distribution_gradient_ready — готовность специализированных градиентов. Новый feedForward сбрасывает оба состояния. Поэтому SRMGradient должен следовать за SRMForward для того же прямого прохода и с теми же tasks, risk_type, risk_parameter и mean_weight. После нового feedForward или изменения параметров SRMForward выполняется заново.
Инициализация объекта
В конструкторе сбрасываем два флага состояния.
CNeuronACSRM::CNeuronACSRM(void) { m_distribution_gradient_ready = false; m_rearrangement_ready = false; }
Основная инициализация выполняется методом Init.
bool CNeuronACSRM::Init(uint numOutputs, uint myIndex, COpenCLMy * open_cl, uint actions, uint quantiles, uint numInputs, ENUM_OPTIMIZATION optimization_type, uint batch) { m_distribution_gradient_ready = false; m_rearrangement_ready = false; m_last_invalid_quantile = 0; m_last_invalid_probability = 0; m_last_invalid_mass = 0; m_last_spectral_break = 0; m_last_srm_invalid = false; if(!CNeuronFQF::Init(numOutputs, myIndex, open_cl, actions, quantiles, numInputs, optimization_type, batch)) ReturnFalse; if(!m_input_gradient_scratch.BufferInit(cCosineEmbeding.Neurons(), 0) || !m_input_gradient_scratch.BufferCreate(OpenCL)) ReturnFalse; //--- return true; }
В начале метода очищаем флаги и диагностические счётчики. Они используются при проверке распределения перед спектральными операциями. На параметры базового FQF эти поля не влияют.
Родительский метод CNeuronFQF::Init создаёт всю обучаемую часть слоя: вероятностную ветвь, квантильное кодирование и блок расчёта значений. Ему передаются размерности, OpenCL-контекст и параметры оптимизации. Новый объект использует эту структуру без изменений.
После инициализации родителя создаём только m_input_gradient_scratch. Его размер соответствует выходу cCosineEmbeding. Буфер временно хранит вклад вероятностной ветви в общий градиент входа. Когда будет рассчитан вклад ветви значений, оба сигнала сложатся.
Память под упорядоченное распределение создаётся по требованию. Для математического ожидания она не нужна. Это уменьшает постоянный расход GPU-памяти.
Прямой проход
Прямой проход CNeuronACSRM начинается с обычного расчёта FQF. Сначала модель строит само распределение результатов. Вероятностная и квантильная ветви работают так же, как в базовом объекте. Спектральная оценка появляется только после того, как готовы вероятности и значения.
Переопределенный метод feedForward остаётся коротким.
bool CNeuronACSRM::feedForward(CNeuronBaseOCL* NeuronOCL) { m_distribution_gradient_ready = false; m_rearrangement_ready = false; return CNeuronFQF::feedForward(NeuronOCL); }
Перед вызовом базового метода сбрасываем состояния перестановки и специализированного градиента. Новый проход изменит значения и вероятности. Старая карта индексов после этого недействительна. То же относится к градиентам, рассчитанным для предыдущего распределения.
Управление передаётся CNeuronFQF::feedForward, и на выходе получаем массы P и значения Z. Важно, что спектральная часть не сворачивает распределение навсегда в одно число. Полное представление остаётся доступным. Оценка риска рассчитывается отдельно, когда она действительно нужна следующему этапу.
Для выборки используется метод Sample. Его OpenCL-алгоритм был разобран во второй статье. Здесь важен только смысл операции. Метод берёт исходное представление FQF, накапливает вероятностную массу во внутреннем порядке и выбирает один из его представителей.
При выборке порядок значений не влияет на вероятность конкретного исхода, если пары (pi, zi) сохраняются. Перестановка меняет положение пар в массиве, но не их массы. Пересечение квантилей не требует упорядочивания перед методом Sample.
Метод Sample выбирает представителя дискретной FQF-аппроксимации. Это не точная выборка из неизвестного распределения полного возврата. В опубликованном кернеле остаётся редкий крайний случай: при u = 0 и нулевой массе первого элемента может быть выбран именно этот элемент. На расчёт SRMForward и SRMGradient это не влияет, но при самостоятельном сэмплировании ограничение нужно помнить.
Со спектральной мерой ситуация другая. Здесь порядок исходов напрямую определяет их вес. Метод SRMForward выбирает подходящее представление распределения и передаёт его в FQF_SRMForward. Именно на этом шаге отдельные OpenCL-примитивы становятся частью логики нового слоя.
В начале SRMForward определяем, зависит ли выбранная мера от ранга результатов.
const bool rank_sensitive = (risk_type != defSRM_MEAN); if(rank_sensitive && ((quantiles & (quantiles - 1)) != 0)) ReturnFalse;
Математическое ожидание не зависит от перестановки пар:
E[Z] = Σi pizi.
Режим SRM_MEAN использует исходное представление. Для CVaR, Mean-CVaR, экспоненциального и Dual Power спектров порядок результата влияет на вес интервала. Эти режимы используют временно упорядоченные пары.
Упорядочивание на GPU построено на битонической сети. Поэтому для рангово-зависимых режимов текущее число представителей должно быть степенью двойки и помещаться в локальную рабочую группу. Это не свойство самой SRM, но это реальное ограничение применимости текущего CNeuronACSRM. Произвольное число квантилей потребует другого алгоритма упорядочивания или дополнения входа до допустимого размера. Режим SRM_MEAN перестановку не использует и этим ограничением не связан.
Перед расчётом слой проверяет состояние распределения. Отдельно действует контракт параметров спектра. Для CVaR и Mean-CVaR требуется 0 < α ≤ 1, для смешанного режима 0 ≤ ω ≤ 1, для экспоненциального профиля параметр должен быть неотрицательным, а для Dual Power — не меньше единицы. Эти ограничения относятся к математическому смыслу меры. Численно выполнимый кернел сам по себе не гарантирует, что при произвольных параметрах получен допустимый спектр.
m_last_srm_invalid = !ValidateDistributionForSRM(tasks, risk_type, risk_parameter, mean_weight, true); if(m_last_srm_invalid) ReturnFalse;
Метод проверяет конечность значений, не отрицательность вероятностей и их суммарную массу. Дополнительно контролируется накопленный спектральный вес. Для CVaR и Mean-CVaR отмечаются попадания в точку излома α. Это не ошибка, а диагностический признак: на самой границе хвостовой области поведение градиента требует отдельного правила.
В точке τ = α у CVaR нет единственной производной: график имеет излом. В коде фиксируем правосторонний субградиент. Для CVaR он равен 0, для Mean-CVaR — ω. Это выбранное правило, а не единственно возможный вариант. Ограничение max(ΔΦ, 0) подавляет небольшие отрицательные значения, возникающие из-за округления при монотонном спектре. Оно не является заменой проверки параметров. Если накопленная спектральная функция стала немонотонной из-за некорректного режима или параметров, отсечение уже изменит саму меру риска и её градиент.
Если распределение корректно и мера зависит от ранга, выполняется упорядочивание.
if(rank_sensitive) { if(!EnsureRearrangeBuffers(tasks, quantiles)) ReturnFalse; if(!MonotoneRearrange(GetPointer(m_ordered_quantiles), GetPointer(m_ordered_probabilities), GetPointer(m_rearrange_order), GetPointer(m_rearrange_diagnostics), tasks)) ReturnFalse; }
Исходные выходы CNeuronFQF не меняются. Во временных буферах значения выстраиваются от меньшего к большему, а вероятности следуют за своими результатами. Одновременно сохраняется карта индексов. Если после оценки риска понадобится обратный проход, именно она вернёт градиент исходным представителям.
Для нашего примера получаем:
(10, 0.3), (−40, 0.2), (30, 0.5) → (−40, 0.2), (10, 0.3), (30, 0.5).
Теперь в рабочем ранговом представлении нижние 20% вероятностной оси соответствуют −40 USD. При CVaR с α = 0.2 весь хвостовой вес приходится на этот неблагоприятный исход. Для трейдера спектральная оценка снова читает худший результат как нижний хвост. При этом исходная немонотонность квантильной аппроксимации остаётся свойством модели и этой перестановкой не устраняется.
Для спектрального кернела выбираем входные буферы в соответствии с режимом.
const int q_index = (rank_sensitive ? m_ordered_quantiles.GetIndex() : cQuantile2.getOutputIndex()); const int p_index = (rank_sensitive ? m_ordered_probabilities.GetIndex() : cSoftMax.getOutputIndex());
Для среднего используются исходные выходы cQuantile2 и cSoftMax. Остальные спектры получают временно упорядоченные буферы. Саму свёртку выполняет уже знакомый FQF_SRMForward. Новый класс отвечает за то, чтобы кернел увидел правильное распределение и только потом вернул численную оценку риска.
Один текущий набор (P,Z) можно оценить несколькими спектрами без нового прямого прохода FQF. Это удобно для анализа одного состояния рынка. Но такой приём нельзя переносить на обучение всей системы без оговорок. В полном AC-SRM профиль риска входит в цель политики и со временем меняет саму политику. Здесь мы пока остаемся внутри одного слоя.
Обратный проход
Главные отличия CNeuronACSRM проявляются на обратном пути. Из одного градиента спектральной оценки нужно получить сигналы по значениям и вероятностям. Для рангово-зависимых мер они сначала относятся к упорядоченному распределению. Карта перестановки возвращает их исходным элементам FQF. После этого две внутренние ветви слоя проводят сигнал к общему входу.
Пусть прямой проход дал оценку ρ, а сверху пришел градиент g = ∂L/∂ρ. Алгоритм FQF_SRMGradient раскладывает его на две части: насколько нужно изменить сами значения исходов и насколько — их вероятностные массы.
giZ = ∂L/∂zi, giP = ∂L/∂pi.
Первая составляющая отвечает за величину прогнозируемого результата. Вторая — за его вероятностную массу. Для рангово-зависимых мер оба сигнала сначала рассчитаны относительно (Z*, P*). То есть они привязаны к упорядоченным позициям. Сохраненная карта перестановки возвращает их к исходным индексам:
gIjZ = gjZ*, gIjP = gjP*.
Готовность этих сигналов отмечается флагом m_distribution_gradient_ready. По нему стандартный обратный проход выбирает нужный маршрут.
bool CNeuronACSRM::calcInputGradients(CNeuronBaseOCL* NeuronOCL) { if(!m_distribution_gradient_ready) return CNeuronFQF::calcInputGradients(NeuronOCL); //--- specialized AC-SRM backward over the prepared internal gradients const bool result = propagateACSRMGradients(NeuronOCL); m_distribution_gradient_ready = false; return result; }
Если специализированный градиент не подготовлен, вызывается обычный CNeuronFQF::calcInputGradients. После SRMGradient ситуация другая: нужные производные по значениям и массам уже известны. Формировать выходной сигнал повторно нельзя. Его остаётся только провести через внутреннюю структуру слоя.
Эту работу выполняет метод propagateACSRMGradients. Сначала градиент значений проходит от cQuantile2 через cQuantile1 к cQuantile0. Мы движемся назад по той же цепочке, которая в прямом проходе формировала квантильные значения.
bool CNeuronACSRM::propagateACSRMGradients(CNeuronBaseOCL* NeuronOCL) { if(!NeuronOCL || !Gradient || !Output) ReturnFalse; //--- quantile branch: cQuantile2 -> cQuantile1 -> cQuantile0 if(!cQuantile1.CalcHiddenGradients(cQuantile2.AsObject())) ReturnFalse; if(!cQuantile0.CalcHiddenGradients(cQuantile1.AsObject())) ReturnFalse;
Дальше сигнал достигает места, где квантильное представление соединялось с входным состоянием. Отсюда расходятся два обратных пути. Один сразу даёт вклад ветви значений в общий вход. Второй возвращается к квантильному кодированию и позже станет дополнительной частью градиента вероятностей.
//--- value-path input gradient and the embedding feature gradients { uint global_work_offset[2] = {0, 0}; uint global_work_size[2] = { cCosineEmbeding.Neurons(), 1 }; setBuffer(def_k_FQF_QuantileGradient, def_k_fqfqgr_state_enbeding, NeuronOCL.getOutputIndex()); setBuffer(def_k_FQF_QuantileGradient, def_k_fqfqgr_taus_embedding, cCosineEmbeding.getOutputIndex()); setBuffer(def_k_FQF_QuantileGradient, def_k_fqfqgr_quantiles_gr, cQuantile0.getGradientIndex()); setBuffer(def_k_FQF_QuantileGradient, def_k_fqfqgr_state_gr, NeuronOCL.getGradientIndex()); setBuffer(def_k_FQF_QuantileGradient, def_k_fqfqgr_taus_gr, cCosineEmbeding.getGradientIndex()); kernelExecute(def_k_FQF_QuantileGradient, global_work_offset, global_work_size) #ifdef _DEBUG if(!NeuronOCL.getGradient().BufferRead()) ReturnFalse; #endif }
В NeuronOCL.Gradient появляется первый вклад в общий вход. Он показывает, насколько спектральная оценка меняется через величину прогнозируемых результатов. Вероятностная ветвь пока ещё не учтена.
Одновременно сигнал сохраняется в cCosineEmbeding.Gradient. Перед дальнейшим проходом учитывается производная функции активации. Это позволяет восстановить влияние квантильных признаков на их исходные координаты.
//--- apply the cCosineEmbeding LReLU derivative BEFORE the cosine //--- feature backward: e = LReLU(a), g_a = g_e * LReLU'(a). The gradient //--- of e (from the value path) sits in cCosineEmbeding.Gradient; the //--- DeActivation is exact because sign(e) == sign(a) for LReLU. if(!DeActivation(cCosineEmbeding.getOutput(), cCosineEmbeding.getGradient(), cCosineEmbeding.getGradient(), cCosineEmbeding.Activation())) ReturnFalse;
Градиент проходит через cCosine к координатам вероятностной оси.
//--- cosine features backward (the connection matrix lives in cCosine) if(!cCosine.CalcHiddenGradients(cCosineEmbeding.AsObject())) ReturnFalse;
Теперь проявляется второй путь вероятностной ветви. Координата каждого представителя зависит от накопленной массы предыдущих интервалов и половины собственной массы. Если изменить одну вероятность, сдвинется не только её интервал. Сместятся и координаты последующих представителей:
τ̂i = pi/2 + Σj=0i−1 pj.
Изменение pi сдвигает квантильную сетку. Через неё меняются и значения, рассчитанные квантильной ветвью. Этот вклад обязательно должен попасть в градиент вероятностей. Иначе модель увидит, что масса исхода изменилась, но не учтёт, что вместе с ней перестроилась сама шкала квантилей.
В нашей реализации одному интервалу соответствует один косинусный признак. Индекс i одновременно задает номер интервала и частоту cos(πiτ̂i). Это жёсткий контракт именно текущего FQF, а не общее свойство метода. Если перейти к нескольким частотам на один квантиль, недостаточно только увеличить размерность. Придётся изменить адресацию признаков и сам расчёт производной по всем координатам квантильного кодирования.
В cSoftMax.Gradient уже находится прямой сигнал от спектральной меры. Примитив FQF_CosineGradientSRM не заменяет его. Он добавляет вклад от квантильного кодирования. В рамках текущего контракта размерностей и схемы "один интервал — один косинусный признак" это два предусмотренных пути влияния вероятностей. Утверждение о полноте градиента относится именно к этой реализации, а не к произвольной архитектуре FQF.
Текущий FQF_CosineGradientSRM проходит по последующим квантильным координатам прямым циклом. Поэтому сложность растёт как O(N2). При небольшом числе интервалов это разумный компромисс. Если число квантилей заметно увеличить, этот участок первым попросится на параллельное суффиксное накопление.
//--- probability gradient at the soft-max output: ::FQF_CosineGradientSRM //--- accumulates the quantile-branch contribution on top of the direct //--- SRM/distributional gradient in cSoftMax.Gradient { uint global_work_offset[2] = {0, 0}; uint global_work_size[2] = { cSoftMax.Neurons() / Neurons(), Neurons() }; setBuffer(def_k_FQF_CosineGradientSRM, def_k_fqfcosgr_softmax, cSoftMax.getOutputIndex()); setBuffer(def_k_FQF_CosineGradientSRM, def_k_fqfcosgr_output_gr, cCosine.getGradientIndex()); setBuffer(def_k_FQF_CosineGradientSRM, def_k_fqfcosgr_softmax_gr, cSoftMax.getGradientIndex()); kernelExecute(def_k_FQF_CosineGradientSRM, global_work_offset, global_work_size) #ifdef _DEBUG if(!cSoftMax.getGradient().BufferRead()) ReturnFalse; #endif }
Получаем сумму двух составляющих:
gP = gP,SRM + gP,quantile.
Первая составляющая показывает прямое перераспределение спектрального веса. Вторая — сдвиг квантильной сетки. Для трейдера это два разных эффекта одного и того же изменения. Рост вероятности крупного убытка увеличивает его долю в нижнем хвосте. Одновременно смещаются уровни, на которых модель оценивает соседние исходы. Оба эффекта должны попасть в обучение.
Суммарный сигнал с выхода cSoftMax передаётся к логитам cFraction.
//--- fraction head: soft-max input gradient (cFraction.Gradient) plus the //--- cFraction weight gradients if(!cFraction.CalcHiddenGradients(cSoftMax.AsObject())) ReturnFalse;
В конце нужно собрать градиент общего входа. Обе ветви FQF начинаются от одного состояния рынка. Одна ветвь отвечает за значения возможных результатов, другая — за распределение вероятностной массы. Предыдущий слой должен получить влияние состояния на обе части прогноза.
Вклад ветви значений уже находится в NeuronOCL.Gradient. Вклад вероятностной ветви временно записывается в m_input_gradient_scratch. Разделение нужно только на время расчёта. При сложении оба сигнала снова относятся к одному входному представлению.
{
const uint inputs = cCosineEmbeding.Neurons();
const uint outputs = cFraction.Neurons();
if(inputs == 0 || outputs == 0 ||
(uint)m_input_gradient_scratch.Total() < inputs ||
NeuronOCL.Neurons() != inputs ||
CheckPointer(NeuronOCL.getWeights()) == POINTER_INVALID)
ReturnFalse;
uint global_work_offset[2] = {0, 0};
uint global_work_size[2];
global_work_size[0] = inputs;
//--- spec 16: FQF_FractionGradient local_size[0] == 1 (reduction
//--- strictly along dimension 1) and local_size[1] <= LOCAL_ARRAY_SIZE
global_work_size[1] = (uint)MathMin(MathMin((outputs + 3) / 4, OpenCL.GetMaxLocalSize(1)),
defFQF_LOCAL_ARRAY_SIZE);
uint local_work_size[2] = { 1, global_work_size[1] };
setBuffer(def_k_FQF_FractionGradient, def_k_fqffg_matrix_w, NeuronOCL.getWeightsIndex());
setBuffer(def_k_FQF_FractionGradient, def_k_fqffg_matrix_g, cFraction.getGradientIndex());
setBuffer(def_k_FQF_FractionGradient, def_k_fqffg_matrix_ig,
m_input_gradient_scratch.GetIndex());
setArgument(def_k_FQF_FractionGradient, def_k_fqffg_outputs, (int)outputs);
kernelExecuteLoc(def_k_FQF_FractionGradient, global_work_offset, global_work_size,
local_work_size)Вероятностная ветвь использует матрицу весов предыдущего объекта. Её вклад в общий вход равен произведению транспонированной матрицы на градиент логитов. По сути это обычный обратный проход линейного преобразования, только рассчитанный отдельно от ветви значений:
gx,P = WTglogits.
Этот результат остаётся во временном буфере. В основном буфере уже лежит градиент ветви значений. Остаётся сложить их.
//--- NeuronOCL.Gradient = value_path + fraction_branch { uint sum_work_offset[1] = {0}; uint sum_work_size[1] = { inputs }; setBuffer(def_k_MatrixSum, def_k_sum_matrix1, NeuronOCL.getGradientIndex()); setBuffer(def_k_MatrixSum, def_k_sum_matrix2, m_input_gradient_scratch.GetIndex()); setBuffer(def_k_MatrixSum, def_k_sum_matrix_out, NeuronOCL.getGradientIndex()); setArgument(def_k_MatrixSum, def_k_sum_dimension, 1); setArgument(def_k_MatrixSum, def_k_sum_multiplyer, 1.0f); setArgument(def_k_MatrixSum, def_k_sum_shift_in1, 0); setArgument(def_k_MatrixSum, def_k_sum_shift_in2, 0); setArgument(def_k_MatrixSum, def_k_sum_shift_out, 0); setBuffer(def_k_MatrixSum, def_k_sum_numeric_flags, NeuronOCL.getGradientIndex()); setArgument(def_k_MatrixSum, def_k_sum_flags_enabled, 0); kernelExecute(def_k_MatrixSum, sum_work_offset, sum_work_size) } #ifdef _DEBUG if(!NeuronOCL.getGradient().BufferRead()) ReturnFalse; #endif } //--- return true; }
Итоговый сигнал имеет вид:
gx = gx,Z + gx,P.
Первое слагаемое показывает, как состояние рынка влияет на величину прогнозируемых результатов. Второе — как то же состояние перераспределяет их вероятностную массу. Для оценки риска нужны оба пути. Глубокий возможный убыток и высокая вероятность этого убытка описывают разные стороны прогноза. Потеря любого из слагаемых даст предыдущему слою неполный сигнал.
Полный обратный проход можно записать одной цепочкой:
gρ → (gZ*, gP*) → (gZ, gP) → квантильная ветвь + вероятностная ветвь → gx.
Специализированный путь рассчитан на одну распределительную голову. В этой постановке SRM применяется к распределительному Критику для действия, выбранного Актёром, поэтому слою нужно одно распределение оцениваемого действия. Это контракт текущей реализации, а не ограничение самой SRM. Для нескольких параллельных голов потребуется расширить адресацию FQF_FractionGradient. Сам слой ещё не образует полную Actor–Critic-архитектуру AC-SRM.
В итоге CNeuronACSRM связывает спектральную оценку с текущим распределительным представлением. Корректирующий сигнал получают значения и связанные с ними массы. На уровне одного слоя SRM становится кусочно-дифференцируемой частью вычислительного графа. В точках смены ранга и изломах спектральной функции используются выбранные правила ветвления и субградиента. Это по-прежнему не полный AC-SRM. Внешний контур, функция h, расширенное состояние, политика и согласование распределительного Критика появятся только в заключительной статье.
Заключение
В этой статье мы собрали подготовленные ранее операции в единый нейронный слой CNeuronACSRM. Он наследует CNeuronFQF и сохраняет его механизм построения адаптивного распределения. Новый объект не пытается прогнозировать рынок заново. Он добавляет к готовому распределению спектральную интерпретацию риска.
Для рангово-зависимых мер слой временно выстраивает текущие значения от худших к лучшим. Связанные массы перемещаются вместе с ними, а карта индексов сохраняет связь с исходным FQF. На обратном проходе эта карта возвращает градиенты нужным представителям. Такое преобразование сохраняет смысл нижнего хвоста для спектрального расчёта, но не исправляет само пересечение квантилей и не восстанавливает монотонность квантильной функции модели.
Обратный проход учитывает два пути вероятностной ветви, предусмотренные текущей архитектурой. Массы определяют спектральные интервалы и одновременно участвуют в квантильном кодировании. Эти сигналы объединяются с градиентом ветви значений на общем входе. Такая схема полна только в рамках зафиксированных размерностей, адресации и текущего косинусного кодирования.
На этом уровне CNeuronACSRM связывает распределительный прогноз со спектральной оценкой риска и проводит выбранный градиент обратно через структуру FQF. Это локальный компонент, а не доказательство корректности всего AC-SRM. Следующий шаг — встроить слой в распределительные Критики, согласовать внешний контур и расширенное состояние. Только после этого можно переходить к полной Actor–Critic-архитектуре и обучению модели на рыночных данных.
Ссылки
- Risk-sensitive actor-critic with static spectral risk measures for online and offline reinforcement learning
- Другие статьи серии
Программы, используемые в статье
| # | Имя | Тип | Описание |
|---|---|---|---|
| 1 | Trajectory.mqh | Общий модуль | Построение моделей и вспомогательные функции обучения |
| 2 | StudyForecast.mq5 | Советник | Обучение прогнозной модели |
| 3 | Study.mq5 | Советник | Обучение базовой модели |
| 4 | StudyOnline.mq5 | Советник | Онлайн-обучение модели |
| 5 | Test.mq5 | Советник | Тестирование модели на отложенном периоде |
| 6 | NeuroNet.mqh | Библиотека классов | Библиотека классов нейронных сетей |
| 7 | NeuroNet.cl | OpenCL-программа | OpenCL-ядра библиотеки нейронных сетей |
Проект представлен на forge.mql5.io/dng.
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
От простых кнопок закрытия до панели управления рисками на основе правил в MQL5
Нейронная сеть на практике: Пример реализации элемента XOR
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования