Нейросети в трейдинге: Двухуровневая адаптация торговой политики (D2Skill)
Введение
При обучении нейронной сети множество наблюдений сводится к изменению одного набора весов. Для торговой модели это естественно: общая политика должна учитывать рынок, открытую позицию, состояние счёта и последствия предыдущих действий. После шага оптимизации информация о конкретной ошибке растворяется в обновлённых параметрах. Если подобная ошибка возникнет снова, оптимизатор обработает её как новое наблюдение.
Так модель обобщает опыт, и сам механизм не является недостатком. Однако повторяющиеся направления коррекции могут содержать отдельную закономерность. Когда разные эпизоды снова и снова предлагают однонаправленный сдвиг внутреннего представления, его можно не только использовать для очередного изменения весов, но и сохранить как самостоятельный элемент опыта.
Для торговли такая возможность особенно интересна из-за неоднородности выборки. Одинаковое действие может возникать при разных сочетаниях рыночного состояния и риска, а внешне похожая ситуация иногда требует противоположной реакции. Общие веса неизбежно ищут компромисс. Отдельная коррекция позволяет удержать устойчивое направление опыта, не превращая его сразу в глобальное изменение всей политики.
Близкий принцип используется в градиентном бустинге. Каждый новый компонент композиции работает с остаточной ошибкой уже построенного решения. Для произвольной функции потерь эту ошибку удобно представить псевдоостатком — отрицательным градиентом функции потерь. Мы не переносим классический бустинг в нейронную сеть буквально. Из него берётся аддитивная идея: устойчивую часть остаточной ошибки можно оформить как небольшую коррекцию, которая дополняет основную модель, а не заменяет её.
Сходство заканчивается на этом принципе. Мы не строим последовательность деревьев и не замораживаем после каждого этапа новую полную модель. Псевдоостаток возникает внутри действующей нейронной сети, а дополнительный элемент хранит лишь вектор локального сдвига. Его можно формировать, применять и проверять отдельно от обычного обновления весов.
Для организации такого опыта обратимся к фреймворку D2Skill из работы Dynamic Dual-Granularity Skill Bank for Agentic RL. Авторы дополняют политику динамическим банком повторно используемых навыков. Task Skills дают высокоуровневые рекомендации для всей задачи, а Step Skills помогают в отдельных точках взаимодействия. Навыки оцениваются по фактической полезности, участвуют в обучении политики и удаляются, если перестают приносить пользу.
В нашей адаптации меняется носитель опыта. Вместо текстовой рекомендации используется коррекция в латентном пространстве торговой политики. Её источником становится псевдоостаток после обратного прохода. Если новые наблюдения подтверждают одно направление, накопленная коррекция стабилизируется. При применении она сдвигает внутреннее представление, после чего обычная вычислительная магистраль Actor формирует действие.
Навык не содержит готового приказа на покупку или продажу и не подменяет риск-менеджмент. Ограничения действий, нормализация и выходные слои продолжают работать штатно. Речь также не идёт о ремонте устаревшей модели. Базовая политика и навыки рассматриваются как взаимодополняющие части одной системы: политика решает задачу в целом, а навыки сохраняют повторяемые способы коррекции её латентного состояния.
Полный D2Skill должен определить:
- к какому состоянию относится коррекция,
- когда её применять,
- какую пользу она принесла на завершённой траектории.
Но начинать с большого банка преждевременно. Сначала нужно проверить базовую гипотезу: способна ли последовательность псевдоостатков сформировать устойчивый навык и уменьшить заданную ошибку относительно режима без коррекции.
Повторяемости недостаточно: сходные градиенты могут относиться к разным рыночным условиям, а стабильная коррекция может не улучшать результат траектории. Эти вопросы будут решать ключ состояния, жизненный цикл банка и оценка полезности. В первой статье мы сознательно изолируем один элемент и проверяем только его способность накопить согласованный сигнал.
В статье мы кратко разберём авторский фреймворк, реализуем базовый элемент CD2SkillItem и рассмотрим его GPU-конвейер. Затем проведём контролируемый эксперимент, сравним два представления навыка и выберем вариант для дальнейшего построения динамического банка.
Алгоритм фреймворка D2Skill
Исходный D2Skill разработан для языковых агентов, которые решают задачи с длинной последовательностью действий и получают редкую итоговую награду. Полное состояние среды агенту недоступно. В момент t политика видит задачу, текущее наблюдение, допустимые действия и ограниченный фрагмент истории. Причина неудачи может возникнуть задолго до того, как станет известен конечный результат.
Авторы дополняют параметрическую память политики внешним банком навыков. Каждый элемент содержит текстовую рекомендацию и ключ области применимости. Перед очередным решением агент извлекает несколько подходящих рекомендаций и добавляет их в рабочий контекст. Полученный опыт можно повторно использовать сразу, не ожидая, пока он полностью закрепится в весах модели.
Ключевая особенность фреймворка — двойная гранулярность. Task Skill относится ко всей задаче и задаёт высокоуровневое направление поведения. Step Skill связан с отдельным наблюдением и помогает исправить локальную ошибку. В торговой аналогии первое можно сопоставить с общим принципом управления риском, второе — с реакцией на изменение спреда и волатильности или на текущее состояние позиции. Общая рекомендация слишком груба для пошаговой коррекции, а локальная подсказка не образует целостную стратегию.
В примере, который приводится в авторской работе, агент может правильно выбрать общий план, но после неудачного поиска повторить то же действие. Task Skill не исправит такую ошибку: план уже разумен. Нужен короткий Step Skill, который учитывает отрицательное наблюдение и предлагает сменить направление поиска. Для торговли ситуация знакома: верная оценка режима не гарантирует правильного действия после резкого изменения текущих условий.
Различаются и ключи извлечения. Для Task Skill ключом служит описание задачи g. Для Step Skill к нему добавляется наблюдение в точке локальной ошибки. Поэтому навык уровня задачи может сопровождать всю траекторию, тогда как пошаговый элемент извлекается заново по текущей ситуации. Двойная гранулярность определяется не двумя названиями банка, а разным масштабом опыта и разной областью применимости.
Во время обучения для одной задачи строится группа параллельных траекторий. Часть проходит без внешних навыков, другая использует навыки при той же версии политики. Сравниваются два режима одного агента, а не две независимо обученные модели. Если средний результат базовой группы обозначить как Ȳgbase, а результат группы с навыками — как Ȳgskill, то разность
Δgtask = Ȳgskill − Ȳgbase
служит ретроспективным сигналом для навыков уровня задачи. Пошаговый элемент получает более локальную оценку по траектории, где он действительно использовался:
ci = Yi − Ȳgbase.
Полезность обновляется экспоненциальным сглаживанием. Один удачный эпизод не делает навык безусловно ценным, а одна неудача не стирает всю накопленную статистику. Для торговли это принципиально: прибыльная сделка с коррекцией не доказывает, что прибыль получена благодаря коррекции. Нужен результат относительно поведения той же политики без дополнительного опыта.
Для навыка уровня задачи обновление можно записать как um ← (1 − βtask)um + βtask Δgtask. Для пошагового элемента вместо групповой разницы используется сигнал ci. Масштаб оценки соответствует масштабу навыка: общий элемент получает оценку за задачу, локальный — за конкретную траекторию.
Разница между группами участвует и в обучении политики. Для траекторий с навыками авторы вводят ретроспективную внутреннюю награду, связанную с превышением базового результата:
Riint = λ(Yi − Ȳgbase).
Преимущества рассчитываются по всей группе, а политика оптимизируется на базовых и дополненных траекториях совместно. Банк направляет решения, а политика постепенно учится использовать извлечённый опыт. Полезные навыки не остаются навсегда внешними подсказками: часть поведения со временем усваивается параметрами модели.
Новые навыки формируются не после каждого эпизода. Рефлексия запускается для групп с низким результатом. Внешняя языковая модель анализирует неудачную траекторию и, когда доступно, успешный пример. За один запуск создаётся не более одного Task Skill и одного Step Skill. Пошаговый навык привязывается к ранней точке отказа, где решение ещё можно было изменить. Перед добавлением рекомендации нормализуются и проверяются на дублирование.
Извлечение выполняется в два этапа. Сначала по косинусному сходству ключей выбираются ближайшие кандидаты и применяется порог релевантности. Затем оставшиеся элементы ранжируются по сочетанию сходства, накопленной полезности и исследовательской надбавки. Последняя даёт редко использовавшимся навыкам шанс собрать статистику, не превращая выбор в жёсткую таблицу "состояние — правило".
Для навыков уровня задачи запросом служит сама задача. Для пошаговых навыков запрос дополнен текущим наблюдением, поэтому поиск повторяется по ходу траектории. Сначала отсеивается явно нерелевантная память, и лишь затем сравниваются качество и степень изученности оставшихся кандидатов. Такой порядок экономит контекст и не позволяет высокой прошлой полезности оправдать применение в чужой ситуации.
Итоговая оценка объединяет нормализованное сходство, полезность и UCB-подобную надбавку: score(m) = α·sim̂(m, q) + (1 − α)(um + η√(log(1 + Nr)/(1 + nm))). Затем формируется расширенный список кандидатов, и в контекст передаётся меньший набор лучших навыков.
Размер банка ограничен. Когда пул превышает заданную ёмкость, навыки сортируются по оценке вытеснения, построенной из полезности и исследовательской надбавки. Слабые элементы удаляются, а недавно созданные временно защищаются. Банк остаётся компактным, но новый навык получает возможность накопить достаточную статистику.
Абляции, приведённые в авторской работе, подтверждают роль обоих уровней навыков, парных базовых траекторий и динамического обслуживания банка. Авторы также отмечают зависимость формирования навыков от внешней модели-рефлектора. Для нашей реализации именно здесь проходит граница адаптации: нейронной торговой модели не требуется текстовое объяснение ошибки, поскольку направление желательной коррекции уже содержится в градиенте.
Мы сохраняем двойную гранулярность, поиск по состоянию, динамический жизненный цикл и последующую проверку полезности. Меняется носитель опыта. Навык становится сглаженной добавкой к латентному представлению, а связь с градиентным бустингом определяет способ её формирования. Это наша модификация исходного фреймворка, а не часть авторского алгоритма.

Реализация средствами MQL5
Практическую работу начнём с класса CD2SkillItem. Он представляет один вектор коррекции и пока не решает задачи поиска, выбора или оценки полезности. Его задача ограничена тремя операциями: принять подготовленный псевдоостаток, обновить состояние навыка и добавить готовую коррекцию к латентному вектору.
CD2SkillItem не является отдельной нейронной сетью. У него нет матрицы весов, momentum-буферов и собственного оптимизатора. Наследование от CNeuronBaseOCL даёт стандартные GPU-буферы, участие в вычислительном графе и сохранение состояния вместе с моделью. Коррекция, сглаженное направление и масштаб хранятся раздельно. Счётчики наблюдений и накопленная масса сигнала используются для диагностики, а два служебных буфера обеспечивают многоступенчатую редукцию на GPU.
Ключевые поля и служебные методы класса показаны ниже.
class CD2SkillItem : public CNeuronBaseOCL { protected: CBufferFloat *m_correction; ED2SkillRepresentation m_representation; float m_beta_correction; float m_beta_direction; float m_beta_magnitude; uint m_item_dimension; CBufferFloat m_item_correction; CBufferFloat m_item_direction; CBufferFloat m_item_scale; CBufferFloat m_item_observations; CBufferFloat m_item_mass; CBufferFloat m_item_partials; CBufferFloat m_item_reduced_norm; bool EnsureItemState(void); bool ItemLaunchLayout(uint &local_size, uint &groups, uint &partial_count) const; bool EnsureItemScratch(void); bool CalculateItemNorm(CBufferFloat *source, const uint local_size, const uint groups);
Указатель m_correction отделяет интерфейс применения от физического хранения. В самостоятельном объекте он связан с m_item_correction. Позже объект банка навыков сможет направить его на выбранный слот плоского GPU-хранилища без создания отдельного объекта для каждого навыка. Буферы m_item_partials и m_item_reduced_norm служат только текущему вычислению нормы.
Скаляр m_item_observations показывает число обновлений, а m_item_mass суммирует нормы поступивших псевдоостатков. Для одиночного элемента это диагностические показатели. В банке они помогут отличить навык, подтверждённый серией содержательных сигналов, от кандидата, который получил столько же обращений, но почти нулевую общую массу.
Класс поддерживает два представления. В первом коррекция обновляется экспоненциальным средним полного псевдоостатка:
dt = (1 − βc)dt−1 + βcgt.
Здесь gt — подготовленный псевдоостаток, а dt — текущая коррекция. Сильное наблюдение одновременно влияет на её направление и длину. При согласованной последовательности EMA быстро поворачивается к общему вектору, но выброс с большой нормой получает повышенный вес.
Во втором режиме направление и масштаб обновляются независимо:
ut = (1 − βd)ut−1 + βd gt /‖gt‖,
mt = (1 − βm)mt−1 + βm ‖gt‖,
dt = mt ut /‖ut‖.
Нормализация уменьшает влияние единичного большого псевдоостатка на ориентацию навыка, а его длина учитывается через отдельный EMA масштаба. При почти нулевой норме добавка принимается равной нулю. Слабый сигнал не должен создавать случайное направление из-за деления на малую величину.
enum ED2SkillRepresentation { D2SkillFullResidual = 0, D2SkillDirectionMagnitude = 1 };
void SetEMA(const float beta_correction, const float beta_direction, const float beta_magnitude) { m_beta_correction = MathMin(MathMax(beta_correction, 0.0f), 1.0f); m_beta_direction = MathMin(MathMax(beta_direction, 0.0f), 1.0f); m_beta_magnitude = MathMin(MathMax(beta_magnitude, 0.0f), 1.0f); }
По умолчанию выбран D2SkillDirectionMagnitude. Коэффициенты EMA ограничиваются диапазоном от 0 до 1. Большое значение ускоряет реакцию, но усиливает влияние шума. Малое требует больше подтверждений и лучше удерживает накопленное направление. Раздельное представление позволяет настроить скорость ориентации и масштаба независимо.
Метод инициализации сохраняет размерность, создаёт стандартные буферы базового слоя и вызывает EnsureItemState(). Постоянное состояние и служебная память подготавливаются один раз и затем используются повторно. В рабочем цикле не требуется заново выделять GPU-буферы.
Размер рабочей группы выбирает ItemLaunchLayout(). Локальный размер ограничен возможностями устройства и значением 64, после чего округляется вниз до степени двойки. Глобальный диапазон покрывает всю размерность. Метод также заранее считает, сколько частичных сумм понадобится на всех уровнях редукции.
bool CD2SkillItem::ItemLaunchLayout(uint &local_size, uint &groups, uint &partial_count) const { if(!OpenCL || m_item_dimension == 0) return(false); long limit = 64; limit = MathMin(limit, OpenCL.GetMaxWorkGroupSize()); limit = MathMin(limit, OpenCL.GetMaxLocalSize(0)); if(limit <= 0) return(false); local_size = 1; while(local_size <= (uint)limit / 2) local_size *= 2; ulong count = ((ulong)m_item_dimension + local_size - 1) / local_size; ulong total = count; groups = (uint)count; while(count > 1) { count = (count + 2 * local_size - 1) / (2 * local_size); if(count > 1) total += count; } if(total == 0 || total > 2147483647) return(false); partial_count = (uint)total; return(true); }
Норма вектора рассчитывается двумя операциями. D2SkillItemPartial распределяет координаты между рабочими группами и записывает сумму квадратов для каждой группы. Затем D2SkillItemReduce рекурсивно сводит частичные результаты. Промежуточные уровни размещаются в непересекающихся областях m_item_partials, а итоговый скаляр — в m_item_reduced_norm.
Такая редукция не связывает размерность навыка с одной рабочей группой. Последняя группа может быть заполнена частично, но глобальный диапазон всё равно покрывает весь вектор. Ни исходные координаты, ни частичные суммы не передаются на CPU.
Флаг final_output отделяет последний уровень от промежуточных. Пока частичных значений несколько, результат записывается в следующую область служебного буфера. Когда остаётся одна группа, скаляр помещается в m_item_reduced_norm. Следующий кернел читает его напрямую, без синхронизации через хост.
bool CD2SkillItem::CalculateItemNorm(CBufferFloat *source, const uint local_size, const uint groups) { if(!source || source.GetOpenCL() != OpenCL || source.GetIndex() < 0 || local_size == 0 || groups == 0) return false; const int partial_kernel = def_k_D2SkillItemPartial; setBuffer(partial_kernel, def_k_d2ip_source, source.GetIndex()) setBuffer(partial_kernel, def_k_d2ip_partials, m_item_partials.GetIndex()) setArgument(partial_kernel, def_k_d2ip_dimension, (int)m_item_dimension) setArgument(partial_kernel, def_k_d2ip_output_offset, 0) uint offset[] = {0}; uint global[] = {groups * local_size}; uint local[] = {local_size}; kernelExecuteLoc(partial_kernel, offset, global, local) uint input_count = groups; uint input_offset = 0; uint output_offset = groups; const int reduce_kernel = def_k_D2SkillItemReduce; while(input_count > 0) { const uint reduce_groups = (input_count + 2 * local_size - 1) / (2 * local_size); const int final_output = (reduce_groups == 1 ? 1 : 0); setBuffer(reduce_kernel, def_k_d2ir_partials, m_item_partials.GetIndex()) setBuffer(reduce_kernel, def_k_d2ir_reduced, m_item_reduced_norm.GetIndex()) setArgument(reduce_kernel, def_k_d2ir_input_count, (int)input_count) setArgument(reduce_kernel, def_k_d2ir_input_offset, (int)input_offset) setArgument(reduce_kernel, def_k_d2ir_output_offset, (int)output_offset) setArgument(reduce_kernel, def_k_d2ir_final_output, final_output) uint reduce_global[] = {reduce_groups * local_size}; uint reduce_local[] = {local_size}; kernelExecuteLoc(reduce_kernel, offset, reduce_global, reduce_local) if(final_output != 0) break; input_count = reduce_groups; input_offset = output_offset; output_offset += reduce_groups; } return true; }
После получения нормы UpdateFromGradient() запускает D2SkillItemApply. В фазе 0 полный псевдоостаток обновляет коррекцию непосредственно. Для представления "направление + масштаб" та же фаза обновляет direction, scale, число наблюдений и накопленную массу. Затем норма уже обновлённого направления рассчитывается тем же конвейером, а фаза 1 собирает итоговую коррекцию.
В режиме полного псевдоостатка второй проход не нужен: каждая координата вычисляется независимо по готовой норме, а общая статистика обновляется один раз. В раздельном режиме после фазового обновления все координаты direction должны образовать единый новый вектор. Только его полная норма позволяет восстановить коррекцию требуемой длины.
bool CD2SkillItem::UpdateFromGradient(CBufferFloat *gradient) { if(!gradient || !OpenCL || gradient.GetOpenCL() != OpenCL || gradient.Total() != (int)m_item_dimension || gradient.GetIndex() < 0 || !EnsureItemState()) return false; uint local_size = 0; uint groups = 0; uint partial_count = 0; if(!ItemLaunchLayout(local_size, groups, partial_count) || !CalculateItemNorm(gradient, local_size, groups)) return false; const int kernel = def_k_D2SkillItemApply; setBuffer(kernel, def_k_d2ia_gradient, gradient.GetIndex()) setBuffer(kernel, def_k_d2ia_correction, m_correction.GetIndex()) setBuffer(kernel, def_k_d2ia_direction, m_item_direction.GetIndex()) setBuffer(kernel, def_k_d2ia_scale, m_item_scale.GetIndex()) setBuffer(kernel, def_k_d2ia_observations, m_item_observations.GetIndex()) setBuffer(kernel, def_k_d2ia_mass, m_item_mass.GetIndex()) setBuffer(kernel, def_k_d2ia_reduced_norm, m_item_reduced_norm.GetIndex()) setArgument(kernel, def_k_d2ia_dimension, (int)m_item_dimension) setArgument(kernel, def_k_d2ia_representation, (int)m_representation) setArgument(kernel, def_k_d2ia_beta_correction, m_beta_correction) setArgument(kernel, def_k_d2ia_beta_direction, m_beta_direction) setArgument(kernel, def_k_d2ia_beta_magnitude, m_beta_magnitude) setArgument(kernel, def_k_d2ia_phase, 0) uint offset[] = {0}; uint global[] = {groups * local_size}; uint local[] = {local_size}; kernelExecuteLoc(kernel, offset, global, local) if(m_representation == D2SkillDirectionMagnitude) { if(!CalculateItemNorm(GetPointer(m_item_direction), local_size, groups)) return false; setArgument(kernel, def_k_d2ia_phase, 1) kernelExecuteLoc(kernel, offset, global, local) } //--- return true; }
Разделение на запуски решает задачу глобальной синхронизации. Локальный барьер действует только внутри одной рабочей группы и не гарантирует, что остальные группы уже обновили свои координаты. Здесь следующий этап начинается после завершения предыдущего кернела, поэтому норма всегда относится к целому согласованному вектору.
Перед суммированием координаты проходят проверку на NaN и бесконечность в OpenCL-коде. Некорректное значение заменяется нулём и не заражает рекурсивное состояние. Это не отменяет диагностику причины численной ошибки, но один повреждённый компонент не должен необратимо испортить EMA, который будет использоваться в следующих эпохах.
В рабочем режиме псевдоостаток появляется после обратного прохода и уже находится на GPU. Цепочка "градиент → редукция → обновление" не требует промежуточной выгрузки. Чтение на CPU допустимо в экспериментальной диагностике после блока наблюдений, но не на каждом временном шаге.
Готовый навык возвращается в вычислительный граф через метод прямого прохода. Буфер Output содержит уже скорректированное латентное представление. Если коррекция отсутствует, выполняется нейтральный проход. В противном случае она добавляется к исходному вектору, а следующий слой получает обычный Output.
bool CD2SkillItem::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!OpenCL || CheckPointer(NeuronOCL) == POINTER_INVALID || CheckPointer(Output) == POINTER_INVALID) return false; CBufferFloat *source = NeuronOCL.getOutput(); if(!source || source.Total() != Output.Total() || source.GetOpenCL() != OpenCL) return false; if(CheckPointer(m_correction) == POINTER_INVALID) return(SumAndNormalize(source, source, Output, source.Total(), false, 0, 0, 0, 0.5f)); return(SumAndNormalize(source, m_correction, Output, source.Total(), false, 0, 0, 0, 1.0f)); }
Нейтральная ветка дважды передаёт один вход с коэффициентом 0.5, поэтому в Output попадает исходный вектор без изменения масштаба. Вторая ветка реализует сумму h′ = h + d. У элемента нет обучаемых весов, поэтому стандартный оптимизатор не меняет его состояние. При обратном проходе градиент проходит через аддитивную операцию напрямую и добавляется к уже накопленному градиенту источника.
Это разделяет два контура обучения. Параметры Actor продолжают обновляться обычным оптимизатором. Состояние навыка меняется только через UpdateFromGradient(). В результате коэффициент tau, momentum или параметры Adam не могут случайно смешаться с рекурсивным EMA коррекции.
Методы Save() и Load() сохраняют только долговременное состояние: коррекцию, направление, масштаб, статистику и параметры представления. Частичные суммы и итоговый скаляр редукции существуют лишь в пределах обновления. После загрузки служебная память создаётся заново, а сформированный навык продолжает работу с прежнего состояния.
Реализация всех классов D2Skill, включая CD2SkillItem, размещена в NeuroNet_D2Skill.mqh. Общий фасад подключает этот модуль вместе с остальной библиотекой, а OpenCL-операции находятся в NeuroNet.cl. Такое размещение отделяет механизм навыков от базовых временных слоёв.
Эксперимент с формированием навыка
Корректная работа кода ещё не подтверждает исходную гипотезу. Для проверки нужен поток псевдоостатков с заранее известной устойчивой составляющей. Поэтому эксперт D2SkillItemExperiment.mq5 использует синтетическую последовательность. Тест не измеряет прибыльность; он изолирует формирование навыка от рынка, Actor и Critic.
Размерность основного опыта равна четырём, одна эпоха содержит 32 наблюдения, каждый режим проходит четыре эпохи.
Последовательность формируется детерминированно, чтобы каждый режим получил одинаковые данные. Устойчивый компонент сосредоточен в первой координате, а небольшой синусоидальный шум меняет остальные компоненты от образца к образцу. Такая конструкция позволяет заранее знать правильное направление и одновременно не подменять эксперимент повторением одного и того же вектора.
#define D2_ITEM_DIM 4 #define D2_ITEM_SAMPLES 32 #define D2_ITEM_EPOCHS 4 #define D2_ITEM_TOLERANCE 1.0e-5
Эталонное направление задано вектором s = (0.8, 0, 0, 0). К координатам добавляется небольшое синусоидальное возмущение. На шагах 9 и 22 вставлены сильные выбросы: первая координата меняет знак, а вторая становится доминирующей. Обычный первый образец имеет норму 0.80217, тогда как нормы выбросов достигают 2.63966 и 2.61196. Они более чем втрое сильнее обычного сигнала и направлены в другую область пространства.
Первый образец приблизительно равен (0.80000, 0.01808, 0.03371, 0.04478). Выброс на шаге 9 имеет вид (−1.69467, 2.02295, 0.03746, 0.04690), а на шаге 22 — (−1.69561, 1.98608, −0.03034, −0.04266). Если навык просто последует за самым сильным наблюдением, его первая координата сменит знак, а направление сместится ко второй оси.
Результат оценивается по косинусному сходству с эталоном, ошибке реконструкции и условной функции потерь. Без навыка нулевой латентный вектор даёт base_loss = 0.32. После добавления коррекции рассчитывается skill_loss. Порог стабилизации считается достигнутым, когда значение косинусного сходства впервые превышает 0.90.
Косинусное сходство отвечает только на вопрос о направлении. Вектор может смотреть точно вдоль эталона и иметь неверную длину. Поэтому дополнительно используется ошибка реконструкции ‖d − s‖. Условная функция потерь 0.5‖h + d − s‖² проверяет смысл коррекции: после её применения результат должен стать ближе к целевому латентному вектору, чем контрольный нулевой вход.
Отдельный блок проверяет произвольную размерность. Тест перебирает границы вокруг типичных размеров рабочих групп и длинный вектор из 1025 координат. Для каждого размера и обоих представлений один GPU-шаг сравнивается с полным CPU-эталоном. Последняя координата получает ненулевое значение, поэтому обрезание хвоста нельзя скрыть совпадением нулей.
const int dimensions[] = {1, 2, 4, 63, 64, 65, 257, 1025};
Проверяются все компоненты коррекции и направления, а также масштаб, число наблюдений и накопленная масса. Основные опыты запускаются только после этого покрытия:
CPU здесь служит эталоном теста, а не как часть рабочего алгоритма. После одного GPU-обновления состояние считывается и сравнивается с формулами для каждой координаты с допуском 1.0e−5. Отдельно проверяется хвост длинного вектора. В производственном цикле такого чтения нет.
if(!RunArbitraryDimensionCoverage() || !RunRepresentation(D2SkillFullResidual, 0.08f) || !RunRepresentation(D2SkillDirectionMagnitude, 0.08f) || !RunRepresentation(D2SkillDirectionMagnitude, 0.02f)) { PrintFormat("D2SkillItemExperiment: FAILED error=%d", GetLastError()); return(INIT_FAILED); } Print("D2SkillItemExperiment: PASS");
Эксперимент включает три конфигурации: EMA полного псевдоостатка с β = 0.08, раздельные направление и масштаб с тем же коэффициентом, а также более медленное раздельное сглаживание с β = 0.02.
| Представление | β | Стабилизация | Косинусное сходство | Ошибка реконструкции | skill_loss | Снижение функции потерь |
|---|---|---|---|---|---|---|
| Полный псевдоостаток | 0.08 | 1-я эпоха | 0.96671 | 0.79000 | 0.312054 | 2.48% |
| Направление + масштаб | 0.08 | 1-я эпоха | 0.96800 | 0.78127 | 0.305188 | 4.63% |
| Направление + масштаб | 0.02 | 1-я эпоха | 0.99691 | 0.78218 | 0.305903 | 4.41% |
Во всех конфигурациях направление стабилизировалось уже после первой эпохи. Даже два сильных выброса не развернули общий вектор. Простое EMA полного псевдоостатка завершило тест с косинусным сходством 0.96671 и снизило условную ошибку на 2.48%. Базовый механизм выделяет повторяемую составляющую сигнала.
Счётчики также сходятся с ожидаемым объёмом: после четырёх эпох каждый вариант содержит 128 наблюдений. Накопленная масса определяется нормами фактических GPU-градиентов. Эти показатели не выбирают победителя эксперимента, но подтверждают, что ни один образец не потерян в вычислительном конвейере.
Для полного псевдоостатка сходство после первой эпохи составило 0.98128, после второй выросло до 0.99494, затем снизилось до 0.96671. Последовательность наблюдений влияет на EMA, но навык сохраняет правильную ориентацию. Стабилизация здесь означает устойчивость общего направления при поступлении новых данных.
При β = 0.08 раздельное представление почти не изменило итоговое направление, но точнее восстановило величину коррекции и снизило функцию потерь на 4.63%. При β = 0.02 косинусное сходство достигло 0.99691. Медленный EMA устойчивее к отдельным отклонениям, быстрый немного лучше подбирает масштаб за ограниченное число обновлений.
Разница между двумя коэффициентами хорошо показывает компромисс. При β = 0.02 отдельный образец слабо поворачивает накопленное направление, поэтому косинусное сходство остаётся почти единичным. Но масштаб также меняется медленнее и за 128 наблюдений немного уступает более быстрому варианту по функции потерь. Для реального банка эти коэффициенты следует выбирать по закрытой валидации, а не по одному синтетическому тесту.
Для дальнейшего банка выбираем D2SkillDirectionMagnitude. Нормализованный псевдоостаток влияет на направление независимо от своей длины, а масштаб обучается отдельно. Это особенно важно, когда банк должен различать согласованные и противоположные коррекции. D2SkillFullResidual сохраняется как простой контрольный режим.
Границы результата определены условиями опыта. Он подтверждает образование устойчивой латентной коррекции и корректную обработку размерностей от 1 до 1025. Он не доказывает рост прибыли, устойчивость на новых рыночных данных или положительную итоговую полезность навыка. Эти свойства можно проверить только после появления банка, механизма выбора и парных траекторий с коррекцией и без неё.
Заключение
В начале статьи был поставлен вопрос: можно ли сохранить повторяемую часть градиентного сигнала отдельно от очередного обновления весов и вернуть её в модель в виде повторно используемой коррекции. Для одного навыка получен положительный ответ.
Авторский D2Skill организует опыт в динамическом банке Task Skills и Step Skills. Навыки создаются через рефлексию, извлекаются по релевантности и полезности, а их вклад оценивается сравнением базовых и дополненных траекторий одной политики. В нашей адаптации сохраняется логика организации и проверки опыта, но текстовая рекомендация заменяется вектором латентной коррекции. Принцип градиентного бустинга используется для выделения повторяемой части псевдоостатка.
Базовый элемент CD2SkillItem не содержит обучаемых весов. Он хранит состояние в GPU-буферах, обновляет его через конвейер Partial → Reduce → Apply и добавляет готовую коррекцию к стандартному Output. Иерархическая редукция позволяет обрабатывать вектор несколькими рабочими группами, не теряя хвост при большой размерности.
Контролируемый опыт показал устойчивое направление при наличии шума и сильных выбросов. Все варианты превысили порог косинусного сходства 0.90 после первой эпохи и уменьшили условную функцию потерь. Лучший баланс дало представление D2SkillDirectionMagnitude: при β = 0.08 функция потерь снизилась на 4.63%, а при β = 0.02 итоговое сходство достигло 0.99691. Этот формат выбран основным для дальнейшей работы.
Дополнительный тест сравнил полный GPU-результат с CPU-эталоном для восьми размерностей и двух представлений. Значит, дальнейший банк будет опираться не только на выбранную математическую схему, но и на проверенный механизм её выполнения.
При этом результат остаётся базовым. Одиночный элемент не различает рыночные состояния и не умеет отказаться от применения, когда подходящего опыта нет. Он также не распределяет терминальную полезность между использованными коррекциями. Эти ограничения задают точный объём следующего этапа.
Поставленная проблема решена на уровне одиночного навыка: повторяющийся псевдоостаток можно выделить, накопить и применить как устойчивую добавочную коррекцию. Следующий этап — множество навыков. Потребуется связывать их с состояниями, не смешивать противоположные направления, подтверждать кандидатов, выбирать один подходящий слот и оценивать его пользу на завершённой траектории.
Ссылки
Программы, используемые в статье
| # | Имя | Тип | Описание |
|---|---|---|---|
| 1 | D2SkillItemExperiment.mq5 | Советник | Эксперимент формирования навыка и проверка произвольной размерности |
| 2 | NeuroNet_D2Skill.mqh | Библиотека классов | Реализация CD2SkillItem, CD2SkillBank и CD2Skill |
| 3 | NeuroNet.cl | OpenCL-программа | GPU-кернелы частичных сумм, редукции и применения коррекции |
| 4 | NeuroNet.mqh | Библиотека классов | Общий фасад библиотеки нейронных сетей |
Проект представлен на forge.mql5.io/dng.
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Создание торговых систем с искусственным интеллектом на MQL5 (Часть 1): Реализация обработки JSON для API ИИ
Изучение стандартной библиотеки MQL5 (Часть 10): Канал полиномиальной регрессии
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования