Алгоритм Цветовой Гармонии — Color Harmony Algorithm (CHA)
Содержание
Введение
Алгоритм цветовой гармонии (Color Harmony Algorithm, CHA), предложенный исследователями Заэйми и Годдосианом в 2020 году, обращается к источнику инспирации, который кажется слишком далёким от вычислительной природы задачи. На первый взгляд связь между правилами Манселла о сочетании цветов в живописи и численной оптимизацией функций выглядит надуманной. Но если разобраться, метафора оказывается не столько украшением, сколько структурным каркасом всего алгоритма — и именно эта структура делает его интересным объектом для разбора.
В стандартных метаэвристиках популяция — это «толпа», в которой все решения равноправны и взаимодействуют по одинаковым правилам. CHA вводит явную иерархию: популяция организована в круг тонов на 100 секторов в 10 цветовых группах, в каждой группе один лидер-агент, между группами действуют правила гармоничного сочетания. Кандидаты строятся не как мутации одиночных решений, а как цветовые комбинации — линейные смеси по строго определённым правилам отбора родителей.
С точки зрения трейдера или исследователя, выбирающего метаэвристику для своей задачи, у CHA есть две привлекательные особенности. Первая — заведомо структурированная популяция: алгоритм не сваливается в одну точку из-за случайной кластеризации, потому что геометрия круга тонов поддерживает разнообразие по построению. Вторая — явное переключение между фазами концентрации (эксплуатация) и рассеивания (исследование) по понятному критерию, а не через скрытые гиперпараметры.
Однако эти достоинства имеют цену. К концу статьи у читателя будет полная картина: каркас алгоритма, рабочая реализация на MQL5 в рамках стандартного фреймворка C_AO, объективные результаты на стандартизированном бенчмарке и понимание, в каких задачах CHA имеет смысл применять, а в каких лучше выбрать что-то другое.
Реализация алгоритма
Если бы вы были художником, который ищет идеальное сочетание красок для своей картины, то начинали бы с нескольких случайных цветов на палитре и постепенно смешивали их, добиваясь нужного оттенка. Иногда вы бы экспериментировали с совершенно новыми цветами, чтобы не застрять в одной цветовой гамме. Примерно так же работает алгоритм цветовой гармонии. Каждое потенциальное решение задачи представляется как «цвет» в цветовом пространстве, а гармоничные цвета в искусстве указывают на перспективные направления поиска лучших решений.
Геометрия алгоритма строится на цветовом круге Манселла, разделённом на 100 секторов. Эти секторы сгруппированы в 10 «цветовых групп» по 10 секторов каждая (красный, оранжевый, жёлтый и так далее). В каждой группе есть один особый сектор — чистейший (purest), на котором располагается лучшее решение группы, называемое агентом. Чистейшие секторы имеют номера 5, 15, 25, ..., 95 — это центры групп. Остальные 90 решений популяции распределены по оставшимся 90 секторам.
Работа алгоритма состоит из двух основных фаз, которые чередуются по мере необходимости.
Фаза концентрации — поиск лучших решений через комбинирование уже найденных. Алгоритм берёт пары решений, выбранные по правилам гармонии, и строит между ними линейные комбинации с возмущением. Это аналог того, как художник смешивает сочетающиеся краски, надеясь получить новый перспективный оттенок. Например, если у нас есть хорошее красное решение и хорошее синее решение, мы пробуем «фиолетовое» между ними, и если оно лучше любого из родителей — оно занимает место в популяции.
Фаза рассеивания — введение разнообразия. Когда популяция становится слишком кучной (все решения слишком похожи), алгоритм заменяет несколько ближайших к центру решений новыми, взятыми из «цветовой памяти» — хранилища ранее найденных перспективных, но не использованных решений. Это похоже на то, как художник может добавить неожиданный цвет на палитру, чтобы вырваться из заигранной гаммы.
Переключение между фазами регулируется измеренным разнообразием популяции Dpop и текущим порогом Dth. Если Dpop < Dth, следующая итерация идёт по фазе рассеивания, после чего порог Dth сжимается умножением на коэффициент damp (обычно 0.5 или 0.96). Со временем порог становится всё более строгим, и алгоритм фокусируется на всё более точной локализации оптимума.
Шаблоны гармонии
Гармонические цвета в теории Манселла — это не произвольные пары, а сочетания, попадающие в определённые геометрические шаблоны на круге тонов; авторы статьи отобрали пять таких шаблонов:
- X-тип: две дуги по 26% круга, расположенные на 180° друг от друга
- V-тип: одна дуга 26%
- Y-тип: малая дуга 5% и большая дуга 26% на расстоянии 180°
- T-тип: одна большая дуга 50%
- L-тип: малая дуга 5% и средняя дуга 22% на расстоянии 90°
Когда выбран шаблон под конкретного агента, серые дуги шаблона определяют, какие секторы круга считаются гармоничными для этого агента. Только из решений, попавших в эти секторы, можно выбирать второго родителя для создания нового цвета (комбинации).
Правила построения комбинаций
В каждой фазе концентрации каждый из 10 агентов порождает Ncomb комбинаций (по умолчанию 4). Тип каждой комбинации определяется параметром PA (Power of Agent), который растёт от 0 до Ncomb по мере прогресса алгоритма. Чем больше PA — тем сильнее эксплуатация, тем больше комбинаций включают агентов как родителей.
Различаются три типа комбинаций:
- Тип (i) — якорный агент комбинируется с не-агентом из гармоничных секторов. Это локальный эксплуатационный механизм: лидер группы тянет лучшие из своих рядовых членов в свою сторону.
- Тип (ii) — глобально лучший агент комбинируется с якорным агентом. Это мост между группами: отстающая группа может «зацепиться» за достижения лидера через своего агента.
- Тип (iii) — два не-агента из гармоничных секторов. Чисто исследовательский механизм, не привязанный к лидерам.
Комбинация строится по формуле:
X_new = r1·X_a + r2·X_b + perturbation,
где r1 + r2 = 1, r1 берётся равномерно из [0.1, 0.9] (исключаем вырожденные случаи, когда кандидат почти совпадает с одним из родителей), а perturbation — гауссово возмущение, амплитуда которого плавно убывает с течением алгоритма от 10% до 1% диапазона по координате.
Здесь стоит остановиться. Возмущение не описано в оригинальной статье. Это критически важное расширение, без которого алгоритм фундаментально не работает на сложном бенчмарке. Объяснение простое: чистая линейная комбинация двух родителей всегда лежит на отрезке между ними, то есть в выпуклой оболочке родителей. Если глобальный оптимум целевой функции не накрыт начальной популяцией, выпуклая оболочка не может до него добраться никакими операциями смешивания — комбинации сходятся к центру, но не покидают исходный объём.
На гладких унимодальных функциях из оригинальной статьи это не проблема: оптимум обычно внутри начального облака. На функциях с резкими пиками вне начального облака (Forest, Megacity бенчмарка) это блокирует алгоритм полностью. Возмущение — минимальное расширение, позволяющее кандидатам выходить за пределы выпуклой оболочки.
Фаза рассеивания
Когда Dpop < Dth, запускается фаза рассеивания. Алгоритм определяет центр популяции (среднее по координатам), находит Ns ближайших к центру решений (исключая агентов — они никогда не вытесняются) и заменяет их новыми цветами по формуле: X_new = rcm·X_CM + (1 - rcm)·X_old,
где X_CM — точка из цветовой памяти, rcm ∈ [rcm0, 1] (по умолчанию rcm0 = 0.7, то есть новый цвет в основном определяется памятью, но сохраняет некоторую связь со старой позицией).
Цветовая память (Color Memory, CM) — это хранилище из Ksets наборов по Ns точек каждый. В неё попадают комбинации, которые были построены, но не приняли в популяцию (потому что оказались хуже своих слотов в фазе концентрации). Идея в том, что отвергнутые в одной фазе кандидаты могут оказаться полезными в фазе рассеивания: они уже представляют какое-то разнообразие, не сводящееся к текущему центру масс.

Рисунок 1. Цветовой тональный круг алгоритма.
Hue Circle, 10 крупных закрашенных лепестков-групп с цветовыми именами (Red, Orange, ..., Magenta) вокруг. Тонкие штришки за пределами круга показывают границы 100 секторов. Чистейшие сектора видны как точки агентов: красные обводные кружки с белой заливкой. Внутри круга — мелкие белые точки (обычные решения), в самом центре — жёлтый кружок (глобальный лучший).

Рисунок 2. Пять мини-кругов
Five Harmony Templates. Пять мини-кругов, каждый с тёмными «серыми дугами», как в статье. Анкорный агент (красная точка) сверху, при переходе глазами между шаблонами легко сравнивать. Размеры дуг точно по Appendix A: X — две по 26%, V — одна 26%, Y — 5%+26%, T — одна 50%, L — 5%+22%. Снизу пояснение про размеры и углы.

Рисунок 3. Фазы работы алгоритма
Two Phases. Слева Concentration: два родителя (красный агент + серый не-агент), пунктирная линия между ними, на ней — точка линейной комбинации, вокруг неё голубое облако возмущения с кандидатами. Справа Dispersion: чёрный квадрат — центр популяции, серые круги вокруг — ближайшие к центру (заменяемые), фиолетовые ромбы — точки из памяти CM, фиолетовые стрелки показывают прыжок. Красные большие круги — агенты на отдалении, не затрагиваются. Снизу легенда на 5 типов символов.
Составим общий псевдокод алгоритма:
1. Инициализация 1.1. Установить параметры (popSize=50, Ncomb=4, Ns=10, Ksets=4, damp=0.5, Dthf=0.01, rcm0=0.7) 1.2. Установить пороги разнообразия: Dthi = 0.5 × max(rangeMax - rangeMin) Dth_cur = Dthi N_Dth = floor(log(Dthf/Dthi) / log(damp)) 1.3. Сгенерировать начальную популяцию равномерно в [rangeMin, rangeMax] 1.4. Оценить FF, разместить top-10 на чистейших секторах как агентов, остальных — равномерно по группам 2. Основной цикл 2.1. Если Dpop < Dth_cur: запустить фазу рассеивания Dth_cur ← Dth_cur × damp iter_dp ← iter_dp + 1 iter_cp ← 0 2.2. Иначе: запустить фазу концентрации iter_cp ← iter_cp + 1 2.3. Обновить лучшее решение fB, cB 2.4. Если не достигнут критерий останова — на шаг 2.1 3. Фаза концентрации 3.1. Рассчитать PA по формулам (8)-(10) Заэйми и Годдосиана 3.2. Для каждого из 10 агентов: - перебрать 5 шаблонов гармонии - для каждого собрать множество H гармоничных решений - сгенерировать Ncomb комбинаций (типы i/ii/iii по PA) - выбрать набор шаблона с максимальным разнообразием 3.3. Разместить кандидатов в слоты с подходящими секторами родителей 3.4. Оценить FF 3.5. Greedy-селекция: каждый слот сравнивает кандидата со своим snapshot, лучший побеждает 3.6. Отвергнутых — в CM_temp 3.7. Переназначить агентов групп 4. Фаза рассеивания 4.1. Найти центр популяции 4.2. Найти Ns ближайших к центру не-агентов 4.3. Источник SCM: если CM заполнена — взять самый разнообразный набор иначе — случайные точки из CM_temp или случайные в пространстве 4.4. Сгенерировать новые цвета по формуле Манселла 4.5. Записать SCM в основную CM (заменив самый бедный по разнообразию набор)
Переходим к реализации. S_CHA_Vector. Это техническая обёртка над одномерным динамическим массивом v[]. Метод Init принимает размерность задачи и резервирует под неё внутренний массив v. Вся подмена двумерности происходит на уровне обращения: вместо X[i][c] пишется X[i].v[c]. Структура используется в трёх местах класса — в snapshot популяции (snap_c[]), в буфере цветовой памяти (CMtempC[]) и в полях points структуры S_CHA_CMSet.
//+------------------------------------------------------------------+ struct S_CHA_Vector { double v []; void Init(int dim) { ArrayResize(v, dim); } }; //+------------------------------------------------------------------+
S_CHA_NewColor. Запись для одного кандидата фазы концентрации. Помимо координат и фитнеса, в структуре сохраняются индексы секторов круга тонов, в которых сидели родители комбинации (sec_a и sec_b). Эти сектора нужны на этапе размещения кандидата в слот популяции: по правилу второго типа из статьи Заэйми и Годдосиана, новый цвет должен попадать на сектор одного из своих компонентов. Метод Init инициализирует массив координат, фитнес выставляется в -DBL_MAX (минимум для задачи максимизации), индексы секторов помечаются как -1 (не назначен).
//+------------------------------------------------------------------+ //| Запись для одного кандидата фазы концентрации: | //| координаты + индексы родителей по секторам круга тонов. | //+------------------------------------------------------------------+ struct S_CHA_NewColor { double c []; // координаты кандидата double f; // фитнес (заполняется ВНЕ Moving — извне) int sec_a; // сектор первого родителя (0..99) int sec_b; // сектор второго родителя (0..99) void Init(int dim) { ArrayResize(c, dim); f = -DBL_MAX; sec_a = -1; sec_b = -1; } };
S_CHA_CMSet. Один набор плохих, но разнообразных цветов в основной памяти алгоритма. Поле points — массив из Ns обёрток S_CHA_Vector, каждая длины равной размерности задачи. Поле diversity хранит заранее вычисленное разнообразие набора относительно центра пространства поиска — это позволяет в фазе рассеивания быстро находить самый бедный по разнообразию набор для замены, не пересчитывая метрику каждый раз. Метод Init принимает число точек в наборе и размерность задачи, выделяет память и обнуляет diversity.
//+------------------------------------------------------------------+ //| Один набор «плохих, но разнообразных» цветов в основной памяти. | //+------------------------------------------------------------------+ struct S_CHA_CMSet { S_CHA_Vector points []; // [Ns] обёрток, каждая длины dim double diversity; // разнообразие набора относительно центра поиска void Init(int n_pts, int dim) { ArrayResize(points, n_pts); for(int i = 0; i < n_pts; i++) points [i].Init(dim); diversity = 0.0; } };
Класс C_AO_CHA наследуется от стандартного C_AO и реализует интерфейс Init / Moving / Revision. Сам алгоритм разбит на шесть базовых подпроцедур, каждая из которых отвечает за свой блок логики: размещение на круге тонов после первой оценки фитнеса, расчёт Power of Agent, генерацию и применение фаз концентрации и рассеивания, обновление лидеров групп. Эти подпроцедуры приватны и вызываются из методов-диспетчеров Moving и Revision.
Внутреннее состояние. Счётчики iter_cp и iter_dp отслеживают число итераций фазы концентрации (с момента последнего рассеивания) и общее число выполненных фаз рассеивания соответственно, iter_cp сбрасывается в ноль каждый раз, когда выполняется фаза рассеивания — это важно для корректного расчёта Power of Agent. Параметры Dthi (начальный порог) и Dth_cur (текущий порог) определяют момент переключения между фазами; N_Dth — оценка ожидаемого числа фаз рассеивания за весь прогон, нужная для формулы Power of Agent. Поле PA хранит текущее значение этого параметра, перерасчитываемое перед каждой фазой концентрации. Флаг nextIsDispersion решает, какая фаза будет запущена в следующем вызове Moving.
Геометрия круга тонов хранится в двух массивах, sectorOf[] для каждой особи популяции содержит её сектор от 0 до 99; группа определяется как sectorOf[i] / 10. agentIdx[10] для каждой группы хранит индекс её агента в массиве a[], или −1, если агент в группе не назначен.
Буферы фазы концентрации. Кандидаты фазы концентрации хранятся отдельно от популяции, в массиве candidates[] размера 10·Ncomb. В момент генерации кандидаты ещё не привязаны к слотам популяции — только после построения и записи родительских секторов алгоритм решает, в какой слот их разместить для оценки FF. Флаг candidateUsed[] помечает кандидатов, принятых в популяцию по результатам greedy селекции; те, кто остался непринятым, отправляются во временную цветовую память CMtempC[].
Snapshot популяции (snap_c, snap_f, snap_sectorOf) делается в начале ConcentrationPhase_Generate и используется в двух целях. Во-первых, он сохраняет старые координаты слотов на случай, если кандидат, временно помещённый в слот для оценки фитнеса, окажется хуже старого содержимого слота — тогда слот откатывается из snapshot. Во-вторых, snapshot фитнесов и секторов используется во всех правилах круга, чтобы не допустить путаницы между «состоянием до Moving» и «состоянием с временно помещённым кандидатом».
Связь между слотом популяции и индексом кандидата хранится в slotCandidate[]: если в слоте сидит кандидат, там его индекс в candidates[], иначе −1. Параллельный булев массив isCandidate[] нужен для быстрой проверки «занят ли слот кандидатом» без сравнения с −1.
Цветовая память. Основная память CM[] имеет ёмкость Ksets наборов, каждый по Ns точек. Поле CMcount показывает, сколько наборов реально записано — пока CMcount < Ksets, новые наборы добавляются; после заполнения наименее разнообразный набор перезаписывается новым, если у того разнообразие больше. Временная память CMtempC[] — это плоский кольцевой буфер ёмкости Ksets·Ns, в который складываются непринятые кандидаты фазы концентрации. Счётчик CMtempCnt инкрементируется при каждой записи; при достижении удвоенной ёмкости он зажимается обратно в CMtempCap, чтобы не разрастался без ограничений. В фазе рассеивания, если CM ещё не заполнена, источником разнообразных точек служит именно CMtempC.
//+------------------------------------------------------------------+ //| Color Harmony Algorithm | //+------------------------------------------------------------------+ class C_AO_CHA : public C_AO { public: ~C_AO_CHA() {} C_AO_CHA() { ao_name = "CHA"; ao_desc = "Color Harmony Algorithm"; ao_link = "https://www.mql5.com/ru/articles/18132"; popSize = 50; Ncomb = 4; // комбинаций на шаблон (на агента) Ns = 10; // замен в фазе рассеивания Ksets = 4; // число наборов в CM damp = 0.5; // коэффициент сжатия порога разнообразия Dthf = 0.01; // финальный порог разнообразия rcm0 = 0.7; // нижняя граница доли вклада CM в фазе рассеивания ArrayResize(params, 7); params [0].name = "popSize"; params [0].val = popSize; params [1].name = "Ncomb"; params [1].val = Ncomb; params [2].name = "Ns"; params [2].val = Ns; params [3].name = "Ksets"; params [3].val = Ksets; params [4].name = "damp"; params [4].val = damp; params [5].name = "Dthf"; params [5].val = Dthf; params [6].name = "rcm0"; params [6].val = rcm0; } void SetParams() { popSize = (int)params [0].val; Ncomb = (int)params [1].val; Ns = (int)params [2].val; Ksets = (int)params [3].val; damp = params [4].val; Dthf = params [5].val; rcm0 = params [6].val; //--- предохранители if(popSize < 20) popSize = 20; // минимум 10 агентов + 10 не-агентов if(Ncomb < 1) Ncomb = 1; if(Ncomb * 10 > popSize - 10) Ncomb = (popSize - 10) / 10; if(Ns < 1) Ns = 1; if(Ns > popSize - 10) Ns = popSize - 10; if(Ksets < 1) Ksets = 1; if(damp <= 0 || damp >= 1) damp = 0.5; if(Dthf <= 0) Dthf = 0.01; if(rcm0 < 0 || rcm0 >= 1) rcm0 = 0.7; } bool Init(const double &rangeMinP [], const double &rangeMaxP [], const double &rangeStepP [], const int epochsP = 0); void Moving(); void Revision(); //--- видимые параметры int Ncomb; // комбинаций на шаблон (на агента) int Ns; // замен в фазе рассеивания int Ksets; // число наборов в CM double damp; // коэффициент сжатия порога разнообразия double Dthf; // финальный порог разнообразия double rcm0; // нижняя граница доли вклада CM в фазе рассеивания private: //--- //--- глобальное состояние алгоритма int iter_cp; // итераций фазы концентрации подряд int iter_dp; // итераций фазы рассеивания (= число обновлений Dth) int N_Dth; // floor(log(Dthf/Dthi)/log(damp)) double Dthi; // начальный порог разнообразия double Dth_cur; // текущий порог int PA; // power of agent на текущей итерации концентрации bool nextIsDispersion; // флаг: следующее Moving — фаза рассеивания //--- круг тонов // sectorOf [i] ∈ [0..99] — сектор круга для i-й особи в a[]. // Сектора 5,15,25,...,95 — «чистейшие», на них размещаются агенты. // Группа определяется как sectorOf [i] / 10 (0..9). int sectorOf []; // [popSize] // agentIdx [g] — индекс в a[] агента группы g (0..9). −1 если не задан. int agentIdx [10]; //--- буфер кандидатов фазы концентрации // 10·Ncomb кандидатов от всех 10 агентов. Хранятся ОТДЕЛЬНО от a[], // потому что на этапе генерации мы ещё не знаем, в какой слот // каждого посадить — это решается в Revision по правилам круга. // На момент Moving() кандидаты "временно" размещаются в слотах // не-агентов для оценки FF; в Revision разбираются по правилам. S_CHA_NewColor candidates []; // [10·Ncomb] bool candidateUsed []; // [10·Ncomb], true = принят на круг //--- snapshot текущей популяции на время фазы концентрации // Нужен потому, что Moving затирает координаты не-агентов // кандидатами. Если кандидат отвергнут — слот восстанавливается // из snapshot. S_CHA_Vector snap_c []; // [popSize] обёрток, каждая длины coords double snap_f []; // [popSize] int snap_sectorOf []; // [popSize] //--- связь "слот в a[] → индекс кандидата" во время фазы // -1 если слот не занят кандидатом (агент или свободный неагент). int slotCandidate []; // [popSize] bool isCandidate []; // [popSize] //--- цветовая память S_CHA_CMSet CM []; // [<= Ksets] int CMcount; // фактический размер CM // Временная память — плоский буфер кандидатов, не попавших в круг S_CHA_Vector CMtempC []; // [<= Ksets*Ns] обёрток, каждая длины coords int CMtempCnt; int CMtempCap; //--- внутренние подпроцедуры bool InitialPopulationWithDiversity(); void PlaceOnHueCircle(); // после первичной оценки FF void ConcentrationPhase_Generate(); // в Moving() void ConcentrationPhase_UpdateCircle(); // в Revision() void DispersionPhase_Generate(); // в Moving() void DispersionPhase_UpdateCircle(); // в Revision() void ComputePA(); // PA_t по формулам (8)–(10) void UpdateAgentsByGroup(); // лучший в группе → агент //--- утилиты double DiversityPopulation(); void PopulationCenter(double &out []); // Шаблоны: для каждого агента возвращает массив секторов // (0..99), попадающих в серые дуги шаблона при повороте под этого агента. // templateType: 0=X, 1=V, 2=Y, 3=T, 4=L void GreySectorsForTemplate(int templateType, int anchorSector, int &out []); int GroupOf(int sector) { return sector / 10; } int PurestSector(int group) { return group * 10 + 5; } }; //+------------------------------------------------------------------+
Init— главная подготовительная процедура. После стандартной инициализации базового класса вычисляется максимальный диапазон по координатам и устанавливается начальный порог разнообразия Dthi = 0.5 · max(rangeMax − rangeMin). Затем по формуле N_Dth = floor(log(Dthf/Dthi) / log(damp)) оценивается ожидаемое число фаз рассеивания за весь прогон — это число используется в формулах Power of Agent. Все счётчики обнуляются, флаг nextIsDispersion сбрасывается в false. Затем выделяется память под все буферы: круг тонов, кандидаты фазы концентрации, snapshot популяции, связи слот-кандидат, основная и временная цветовая память. В завершение вызывается InitialPopulationWithDiversity для генерации начальной популяции.
//+------------------------------------------------------------------+ //| Init | //+------------------------------------------------------------------+ bool C_AO_CHA::Init(const double &rangeMinP [], const double &rangeMaxP [], const double &rangeStepP [], const int epochsP = 0) { if(!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) return false; //--- начальный порог разнообразия Dthi = 0.5 · max(rangeMax − rangeMin) double maxRange = 0.0; for(int c = 0; c < coords; c++) { double r = rangeMax [c] - rangeMin [c]; if(r > maxRange) maxRange = r; } Dthi = 0.5 * maxRange; Dth_cur = Dthi; //--- N_Dth = floor( log(Dthf/Dthi) / log(damp) ) if(Dthi > 0.0 && Dthf > 0.0 && damp > 0.0 && damp != 1.0) N_Dth = (int)MathFloor(MathLog(Dthf / Dthi) / MathLog(damp)); else N_Dth = 1; if(N_Dth < 1) N_Dth = 1; //--- счётчики и флаги iter_cp = 0; iter_dp = 0; PA = 0; nextIsDispersion = false; //--- круг тонов ArrayResize(sectorOf, popSize); ArrayInitialize(sectorOf, -1); for(int g = 0; g < 10; g++) agentIdx [g] = -1; //--- буфер кандидатов: 10 агентов · Ncomb комбинаций int Kcand = 10 * Ncomb; ArrayResize(candidates, Kcand); for(int i = 0; i < Kcand; i++) candidates [i].Init(coords); ArrayResize(candidateUsed, Kcand); ArrayInitialize(candidateUsed, false); //--- snapshot и связи слот↔кандидат ArrayResize(snap_c, popSize); for(int i = 0; i < popSize; i++) snap_c [i].Init(coords); ArrayResize(snap_f, popSize); ArrayResize(snap_sectorOf, popSize); ArrayResize(slotCandidate, popSize); ArrayResize(isCandidate, popSize); ArrayInitialize(slotCandidate, -1); ArrayInitialize(isCandidate, false); //--- цветовая память ArrayResize(CM, Ksets); for(int s = 0; s < Ksets; s++) CM [s].Init(Ns, coords); CMcount = 0; CMtempCap = Ksets * Ns; ArrayResize(CMtempC, CMtempCap); for(int i = 0; i < CMtempCap; i++) CMtempC [i].Init(coords); CMtempCnt = 0; //--- начальная популяция с проверкой Dth_i if(!InitialPopulationWithDiversity()) return false; return true; } //+------------------------------------------------------------------+
InitialPopulationWithDiversity генерирует начальную популяцию с проверкой разнообразия. На каждой попытке (до 50) равномерно сэмплируется популяция в диапазонах [rangeMin, rangeMax], затем рассчитывается её разнообразие. Если оно достигает или превышает Dthi, попытки прекращаются. На практике при равномерном распределении в больших диапазонах достаточно одной попытки. Если за все 50 попыток разнообразие не достигло порога, оставляется последняя сгенерированная популяция — это сценарий для очень узких или специфических диапазонов поиска, где геометрически невозможно достичь высокого разнообразия.
//+------------------------------------------------------------------+ //| Генерация начальной популяции с разнообразием ≥ Dthi. | //| Записывает координаты в a[i].cP[] (попадут в a[i].c через Moving)| //+------------------------------------------------------------------+ bool C_AO_CHA::InitialPopulationWithDiversity() { // Цикл регенерации; на практике сходится за 1-2 попытки при // равномерном распределении в [rangeMin, rangeMax]. const int maxAttempts = 50; for(int attempt = 0; attempt < maxAttempts; attempt++) { for(int i = 0; i < popSize; i++) for(int c = 0; c < coords; c++) a [i].cP [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]); // Расчёт разнообразия по cP[] double D = 0.0; for(int c = 0; c < coords; c++) { double mean = 0.0; for(int i = 0; i < popSize; i++) mean += a [i].cP [c]; mean /= popSize; double dj = 0.0; for(int i = 0; i < popSize; i++) dj += MathAbs(a [i].cP [c] - mean); dj /= popSize; D += dj; } D /= coords; if(D >= Dthi) return true; } // Если за maxAttempts не сошлось — оставляем последнюю популяцию. return true; } //+------------------------------------------------------------------+
Moving— главный диспетчер. На первом вызове (когда флаг revision ещё false) метод просто переносит начальную популяцию из cP[] в c[] через SeInDiSp для дискретизации — это нужно, чтобы внешний цикл тестового стенда смог посчитать FF для стартовой популяции. Размещение на круге тонов и назначение агентов делается в первом Revision, не здесь. Во всех последующих вызовах метод смотрит на флаг nextIsDispersion и вызывает либо DispersionPhase_Generate, либо комбинацию ComputePA + ConcentrationPhase_Generate. Сам метод не делает никакой содержательной работы — только маршрутизация.
//+------------------------------------------------------------------+ //| Moving | //+------------------------------------------------------------------+ // Логика диспетчера: // • Первый вызов (revision == false): копируем cP → c, чтобы внешний // цикл посчитал FF для начальной популяции. Размещение на круге // тонов и назначение агентов — в первом Revision(). // • Дальнейшие вызовы: либо фаза концентрации (генерация // 10·Ncomb_eff кандидатов в a[].c), либо фаза рассеивания // (генерация Ns кандидатов на замену ближайших к центру). //+------------------------------------------------------------------+ void C_AO_CHA::Moving() { if(!revision) { // первый прогон — просто переносим начальную популяцию в c[] for(int i = 0; i < popSize; i++) for(int c = 0; c < coords; c++) a [i].c [c] = u.SeInDiSp(a [i].cP [c], rangeMin [c], rangeMax [c], rangeStep [c]); return; } if(nextIsDispersion) { DispersionPhase_Generate(); } else { ComputePA(); ConcentrationPhase_Generate(); } } //+------------------------------------------------------------------+
Revision— второй главный диспетчер, вызываемый сразу после оценки FF в каждом слоте. Сначала идёт стандартный цикл обновления личных лучших каждой особи и глобального лучшего по всей популяции. Затем, если это первый вызов Revision, вызывается PlaceOnHueCircle для размещения отсортированной по фитнесу популяции на круге тонов и назначения 10 агентов — после этого флаг revision поднимается в true и метод завершается.
На последующих вызовах применяются правила обновления круга для только что оценённой фазы: либо ConcentrationPhase_UpdateCircle, либо DispersionPhase_UpdateCircle. После применения правил обновляются счётчики и порог разнообразия, и метод решает, какую фазу запускать дальше: если измеренное разнообразие популяции упало ниже текущего порога Dth_cur, флаг nextIsDispersion поднимается.
//+------------------------------------------------------------------+ //| Revision | //+------------------------------------------------------------------+ // Логика диспетчера: // • Если revision == false: это первая оценка популяции — обновляем // fB/cB, переносим f→fP в каждом слоте, размещаем особей на круге // тонов, назначаем агентов. revision = true. // • Иначе: применяем правила обновления круга для последней фазы // (концентрация или рассеивание), переключаем фазы по порогу // разнообразия, обновляем Dth_cur при необходимости. //+------------------------------------------------------------------+ void C_AO_CHA::Revision() { //--- обновление личных и глобального лучших for(int i = 0; i < popSize; i++) { if(a [i].f > a [i].fB) { a [i].fB = a [i].f; ArrayCopy(a [i].cB, a [i].c, 0, 0, coords); } if(a [i].f > fB) { fB = a [i].f; ArrayCopy(cB, a [i].c, 0, 0, coords); } } if(!revision) { // Первый Revision: просто разместить популяцию на круге тонов PlaceOnHueCircle(); revision = true; return; } //--- обновление круга по результатам только что оценённой фазы if(nextIsDispersion) { DispersionPhase_UpdateCircle(); iter_dp ++; Dth_cur *= damp; iter_cp = 0; // сброс — начинается новая серия концентраций nextIsDispersion = false; } else { ConcentrationPhase_UpdateCircle(); iter_cp ++; } //--- решение: какая фаза дальше if(DiversityPopulation() < Dth_cur) nextIsDispersion = true; } //+------------------------------------------------------------------+
DiversityPopulation— реализация формул (2)–(4) из оригинальной статьи. Для каждой координаты вычисляется среднее значение по популяции, затем среднее абсолютное отклонение от этого среднего. Финальное разнообразие — это среднее отклонений по всем координатам. Метрика устойчива к выбросам и хорошо работает в широком диапазоне размерностей задачи.
//+------------------------------------------------------------------+ //| Утилиты: разнообразие и центр популяции | //+------------------------------------------------------------------+ double C_AO_CHA::DiversityPopulation() { double D = 0.0; for(int c = 0; c < coords; c++) { double mean = 0.0; for(int i = 0; i < popSize; i++) mean += a [i].c [c]; mean /= popSize; double dj = 0.0; for(int i = 0; i < popSize; i++) dj += MathAbs(a [i].c [c] - mean); dj /= popSize; D += dj; } return D / coords; } //+------------------------------------------------------------------+
PopulationCenter— простой расчёт среднего арифметического по каждой координате. Используется в фазе рассеивания для определения слотов, ближайших к центру популяции — именно они подлежат замене.
//+------------------------------------------------------------------+ void C_AO_CHA::PopulationCenter(double &out []) { ArrayResize(out, coords); for(int c = 0; c < coords; c++) { double s = 0.0; for(int i = 0; i < popSize; i++) s += a [i].c [c]; out [c] = s / popSize; } } //+------------------------------------------------------------------+
ComputePA — расчёт параметра Power of Agent по формулам (8)–(10) статьи. Сначала вычисляется Ni = floor(iter_dp · Ncomb / N_Dth) с зажимом сверху на Ncomb — это «давление эксплуатации», которое со временем растёт. Затем step = (Ncomb + 1) / (Ncomb + Ni) — шаг, с которым iter_cp приближается к насыщению. Финальное значение PA = floor(iter_cp / step), тоже зажимается сверху на Ncomb. Семантика: чем больше PA, тем больше комбинаций фазы концентрации делается с агентом в качестве одного из родителей — то есть тем сильнее эксплуатация лидеров групп.
//+------------------------------------------------------------------+ //| Power of Agent: формулы (8)–(10) статьи (Zaeimi 2020) | //+------------------------------------------------------------------+ void C_AO_CHA::ComputePA() { int Ni; if(N_Dth > 0) { double denom = (double)N_Dth / (double)Ncomb; Ni = (int)MathFloor((double)iter_dp / denom); if(Ni > Ncomb) Ni = Ncomb; } else Ni = 0; if(Ni < 0) Ni = 0; double step = (double)(Ncomb + 1) / (double)(Ncomb + Ni); if(step <= 0) step = 1.0; PA = (int)MathFloor((double)iter_cp / step); if(PA > Ncomb) PA = Ncomb; if(PA < 0) PA = 0; } //+------------------------------------------------------------------+
UpdateAgentsByGroup — вызывается после применения правил круга в обеих фазах. Для каждой из 10 групп ищется лучшая по фитнесу особь среди тех, чьи секторы относятся к группе. Если найденный лидер не совпадает со старым агентом группы, происходит обмен секторов — старый агент попадает на сектор бывшего лидера, новый агент занимает чистейший сектор группы. Это реализует правило статьи «if replaced colors have better fitness value than their related agents, their positions on the hue circle will be exchanged».
//+------------------------------------------------------------------+ //| Агент группы = лучший по фитнесу. Если новый лучший не сидит | //| на чистейшем секторе — обмениваем секторы старого и нового. | //+------------------------------------------------------------------+ void C_AO_CHA::UpdateAgentsByGroup() { for(int g = 0; g < 10; g++) { int oldAg = agentIdx [g]; int bestI = oldAg; double bestF = (oldAg >= 0) ? a [oldAg].f : -DBL_MAX; for(int i = 0; i < popSize; i++) { if(i == oldAg) continue; if(sectorOf [i] / 10 != g) continue; if(a [i].f > bestF) { bestF = a [i].f; bestI = i; } } if(bestI >= 0 && bestI != oldAg) { // обмен секторов: новый агент → чистейший сектор группы if(oldAg >= 0) { int tmp = sectorOf [bestI]; sectorOf [bestI] = sectorOf [oldAg]; sectorOf [oldAg] = tmp; } else { sectorOf [bestI] = PurestSector(g); } agentIdx [g] = bestI; } } } //+------------------------------------------------------------------+
PlaceOnHueCircle— первичное размещение популяции на круге тонов после стартовой оценки FF. Сначала особи сортируются по фитнесу по убыванию (пузырьковая сортировка по индексам, без перестановки самих структур a[]). Затем top-10 распределяются по чистейшим секторам в случайном порядке групп — порядок групп тасуется через классический Фишера-Йейтса. Оставшиеся popSize − 10 особей распределяются по группам максимально равномерно: сначала вычисляется базовое число особей на группу (popSize − 10) / 10, затем остаток (popSize − 10) % 10 распределяется по первым группам. Внутри каждой группы свободные секторы (все, кроме чистейшего) тасуются и назначаются особям по очереди. Если особей в группе больше девяти, секторы переиспользуются с повтором.
//+------------------------------------------------------------------+ //| Первичное размещение популяции на круге тонов после | //| стартовой оценки FF. | //+------------------------------------------------------------------+ void C_AO_CHA::PlaceOnHueCircle() { // Сортировка индексов по фитнесу (по убыванию: лучший — первый) int idx []; ArrayResize(idx, popSize); for(int i = 0; i < popSize; i++) idx [i] = i; for(int i = 0; i < popSize - 1; i++) for(int j = i + 1; j < popSize; j++) if(a [idx [j]].f > a [idx [i]].f) { int t = idx [i]; idx [i] = idx [j]; idx [j] = t; } // Top-10 → агенты на чистейших секторах в случайном порядке групп int groupOrder [10]; for(int g = 0; g < 10; g++) groupOrder [g] = g; for(int g = 9; g > 0; g--) { int j = (int)u.RNDfromCI(0.0, (double)(g + 1)); if(j > g) j = g; int t = groupOrder [g]; groupOrder [g] = groupOrder [j]; groupOrder [j] = t; } for(int k = 0; k < 10; k++) { int slot = idx [k]; int g = groupOrder [k]; sectorOf [slot] = PurestSector(g); agentIdx [g] = slot; } // Остальные popSize-10 равномерно по группам int rem = popSize - 10; int perGroup = rem / 10; int extra = rem % 10; int sectorList []; ArrayResize(sectorList, rem); int pos = 0; for(int g = 0; g < 10; g++) { int n = perGroup + (g < extra ? 1 : 0); // Свободные секторы группы (исключая чистейший) int freeSecs [9]; int fsc = 0; for(int s = 0; s < 10; s++) { int sec = g * 10 + s; if(sec == PurestSector(g)) continue; freeSecs [fsc++] = sec; } // Перемешать for(int s = fsc - 1; s > 0; s--) { int j = (int)u.RNDfromCI(0.0, (double)(s + 1)); if(j > s) j = s; int t = freeSecs [s]; freeSecs [s] = freeSecs [j]; freeSecs [j] = t; } // Назначить n секторов (с повторами, если n > 9) for(int s = 0; s < n; s++) sectorList [pos++] = freeSecs [s % fsc]; } // Перемешать sectorList и раздать for(int i = ArraySize(sectorList) - 1; i > 0; i--) { int j = (int)u.RNDfromCI(0.0, (double)(i + 1)); if(j > i) j = i; int t = sectorList [i]; sectorList [i] = sectorList [j]; sectorList [j] = t; } for(int k = 10; k < popSize; k++) sectorOf [idx [k]] = sectorList [k - 10]; } //+------------------------------------------------------------------+
GreySectorsForTemplate. Возвращает массив секторов, попадающих в «серые дуги» одного из пяти шаблонов гармонии при повороте под анкорный сектор anchor. Размеры дуг точно соответствуют Appendix A оригинальной статьи: X-тип — две дуги по 26% круга на 180° друг от друга, V-тип — одна дуга 26%, Y-тип — малая дуга 5% и большая 26% на 180°, T-тип — одна дуга 50%, L-тип — малая 5% и средняя 22% на 90°. Все смещения секторов модулируются по 100, чтобы обходить переход через границу круга. Метод используется в фазе концентрации для отбора кандидатов на роль родителей комбинации.
//+------------------------------------------------------------------+ //| Серые секторы для шаблона при повороте под якорь anchor. | //| Размеры дуг: X/V — 26%, T — 50%, Y — 5%+26%, L — 5%+22%. | //| Углы между дугами: X/Y — 180°, L — 90°. | //+------------------------------------------------------------------+ void C_AO_CHA::GreySectorsForTemplate(int templateType, int anchor, int &out []) { ArrayResize(out, 0); // вспомогательная лямбда-имитация: добавление отрезка (offset±half) от anchor // диапазон [center - low, center + high] включительно, по модулю 100 // чтобы не плодить вспомогательную функцию — пишем in-line. if(templateType == 0) { // X type: две дуги по 26% (по 26 секторов), угол между биссектрисами 180° for(int d = -13; d <= 12; d++) { int s1 = ((anchor + d) % 100 + 100) % 100; int s2 = ((anchor + 50 + d) % 100 + 100) % 100; int n = ArraySize(out); ArrayResize(out, n + 2); out [n] = s1; out [n + 1] = s2; } } else if(templateType == 1) { // V type: одна дуга 26% for(int d = -13; d <= 12; d++) { int s = ((anchor + d) % 100 + 100) % 100; int n = ArraySize(out); ArrayResize(out, n + 1); out [n] = s; } } else if(templateType == 2) { // Y type: малая 5% на anchor + большая 26% на (anchor+50) for(int d = -2; d <= 2; d++) { int s = ((anchor + d) % 100 + 100) % 100; int n = ArraySize(out); ArrayResize(out, n + 1); out [n] = s; } for(int d = -13; d <= 12; d++) { int s = ((anchor + 50 + d) % 100 + 100) % 100; int n = ArraySize(out); ArrayResize(out, n + 1); out [n] = s; } } else if(templateType == 3) { // T type: одна дуга 50% for(int d = -25; d <= 24; d++) { int s = ((anchor + d) % 100 + 100) % 100; int n = ArraySize(out); ArrayResize(out, n + 1); out [n] = s; } } else if(templateType == 4) { // L type: малая 5% на anchor + большая 22% на (anchor+25) for(int d = -2; d <= 2; d++) { int s = ((anchor + d) % 100 + 100) % 100; int n = ArraySize(out); ArrayResize(out, n + 1); out [n] = s; } for(int d = -11; d <= 10; d++) { int s = ((anchor + 25 + d) % 100 + 100) % 100; int n = ArraySize(out); ArrayResize(out, n + 1); out [n] = s; } } } //+------------------------------------------------------------------+
ConcentrationPhase_Generate— главная процедура построения кандидатов. Первый шаг — создание snapshot всей популяции. Второй — сброс связей слот↔кандидат. Третий, основной, — цикл по 10 агентам.
Для каждого агента перебираются все 5 шаблонов гармонии. Для каждого шаблона собирается множество H — индексы слотов, чьи секторы попали в серые дуги. Это множество разделяется на Hagents (агенты в серых дугах) и Hnon (не-агенты в серых дугах). Если Hnon пусто, шаблон пропускается — нет материала для построения комбинаций.
Перед генерацией комбинаций вычисляется адаптивная амплитуда возмущения perturbScale. Она завязана на текущий порог разнообразия Dth_cur: пока порог высок (популяция разрежена), амплитуда около 10% диапазона; по мере сжатия порога до Dthf амплитуда падает до 1%. Это не статическая константа из оригинальной формулировки, а критическое расширение алгоритма — без него все комбинации лежат строго в выпуклой оболочке родителей и не могут покинуть её.
Затем строится Ncomb комбинаций. Если индекс комбинации m < PA, она строится по типу (i) или (ii): с вероятностью 1/3 берётся комбинация «глобально лучший агент × якорный агент» (Type ii — мост между лидерами), иначе «якорный агент × случайный не-агент из серых дуг» (Type i — локальный эксплоит). Если m ≥ PA, строится комбинация типа (iii) — два различных не-агента из серых дуг.
Сама комбинация — линейная смесь координат двух родителей с коэффициентами r1, r2 = 1 − r1, где r1 берётся равномерно из [0.1, 0.9] (избегаем вырожденных случаев, когда кандидат почти совпадает с одним из родителей). К результату прибавляется гауссово возмущение амплитудой perturbScale. Финальное значение зажимается в границы диапазона [rangeMin, rangeMax].
После построения всех Ncomb комбинаций под текущий шаблон вычисляется их совокупное разнообразие относительно якорного агента. Шаблон с максимальным разнообразием выигрывает — именно его комбинации сохраняются в bestSet. Если ни один шаблон не дал материала (исключительная ситуация), используется fallback с возмущением координат якорного агента в радиусе 10% диапазона.
Лучший набор записывается в общий массив candidates[]. После завершения цикла по агентам кандидаты размещаются в слоты не-агентов для оценки FF. Размещение учитывает правило статьи: новый цвет ставится на сектор одного из своих родителей. Сначала ищется неагентский слот с подходящим сектором, если такого нет — fallback в любой свободный неагентский слот. Слоты агентов в фазе концентрации не трогаются — их координаты восстанавливаются из snapshot, чтобы при следующей оценке FF их фитнес не пересчитывался зря.
//+------------------------------------------------------------------+ //| ФАЗА КОНЦЕНТРАЦИИ — генерация | //+------------------------------------------------------------------+ void C_AO_CHA::ConcentrationPhase_Generate() { //--- 1. Сохранить snapshot популяции for(int i = 0; i < popSize; i++) { for(int c = 0; c < coords; c++) snap_c [i].v [c] = a [i].c [c]; snap_f [i] = a [i].f; snap_sectorOf [i] = sectorOf [i]; } //--- 2. Сбросить связи слот↔кандидат и флаги используемости ArrayInitialize(slotCandidate, -1); ArrayInitialize(isCandidate, false); ArrayInitialize(candidateUsed, false); //--- 3. По каждому агенту: 5 шаблонов → выбрать лучший набор int Kcand = 10 * Ncomb; int candPos = 0; // временный буфер набора Ncomb комбинаций S_CHA_NewColor combo []; ArrayResize(combo, Ncomb); for(int i = 0; i < Ncomb; i++) combo [i].Init(coords); S_CHA_NewColor bestSet []; ArrayResize(bestSet, Ncomb); for(int i = 0; i < Ncomb; i++) bestSet [i].Init(coords); for(int g = 0; g < 10; g++) { int agSlot = agentIdx [g]; if(agSlot < 0) continue; int anchorSec = snap_sectorOf [agSlot]; double bestDiv = -1.0; for(int t = 0; t < 5; t++) { int greySectors []; GreySectorsForTemplate(t, anchorSec, greySectors); // Собрать H — индексы слотов, чьи секторы в серых дугах int H []; ArrayResize(H, 0); for(int i = 0; i < popSize; i++) { for(int s = 0; s < ArraySize(greySectors); s++) { if(snap_sectorOf [i] == greySectors [s]) { int n = ArraySize(H); ArrayResize(H, n + 1); H [n] = i; break; } } } // Разделить H на агентов и не-агентов int Hagents [], Hnon []; ArrayResize(Hagents, 0); ArrayResize(Hnon, 0); for(int i = 0; i < ArraySize(H); i++) { bool ag = false; for(int gg = 0; gg < 10; gg++) if(agentIdx [gg] == H [i]) { ag = true; break; } int n; if(ag) { n = ArraySize(Hagents); ArrayResize(Hagents, n + 1); Hagents [n] = H [i]; } else { n = ArraySize(Hnon); ArrayResize(Hnon, n + 1); Hnon [n] = H [i]; } } if(ArraySize(Hnon) == 0) continue; // нет «материала» для комбинаций // Найти глобально лучшего агента — нужен для Type (ii) int bestAg = agSlot; double bestAgF = snap_f [agSlot]; for(int gg = 0; gg < 10; gg++) { int s = agentIdx [gg]; if(s < 0) continue; if(snap_f [s] > bestAgF) { bestAgF = snap_f [s]; bestAg = s; } } // Адаптивная амплитуда возмущения: завязана на текущий порог // разнообразия Dth_cur, а не на счётчик iter_dp. Логика: // когда популяция рассеяна (Dth_cur высок) — возмущаем сильно, // когда собрана (Dth_cur мал) — точная фокусировка. // 10% при Dth_cur = Dthi (старт), 1% при Dth_cur = Dthf (финиш). double ratio = (Dthi > 0.0) ? MathMax(0.0, MathMin(1.0, (Dth_cur - Dthf) / (Dthi - Dthf))) : 0.0; double perturbScale = 0.01 + 0.09 * ratio; // Сгенерировать Ncomb комбинаций for(int m = 0; m < Ncomb; m++) { int ia = -1, ib = -1; if(m < PA) { // С вероятностью 1/3 — Type (ii): глобальный лучший × якорный. // Это сильный эксплоит, мост между лучшими лидерами групп. // Иначе — Type (i): якорный × не-агент (локальный эксплоит). if(bestAg != agSlot && u.RNDfromCI(0.0, 1.0) < 0.33) { ia = bestAg; ib = agSlot; } else { ia = agSlot; int kk = (int)u.RNDfromCI(0.0, (double)ArraySize(Hnon)); if(kk >= ArraySize(Hnon)) kk = ArraySize(Hnon) - 1; ib = Hnon [kk]; } } else { // Type (iii): два не-агента, эксплорация if(ArraySize(Hnon) >= 2) { int k1 = (int)u.RNDfromCI(0.0, (double)ArraySize(Hnon)); if(k1 >= ArraySize(Hnon)) k1 = ArraySize(Hnon) - 1; int k2; int safety = 0; do { k2 = (int)u.RNDfromCI(0.0, (double)ArraySize(Hnon)); if(k2 >= ArraySize(Hnon)) k2 = ArraySize(Hnon) - 1; safety++; if(safety > 20) { k2 = (k1 + 1) % ArraySize(Hnon); break; } } while(k2 == k1); ia = Hnon [k1]; ib = Hnon [k2]; } else { ia = (ArraySize(Hagents) > 0) ? Hagents [0] : agSlot; ib = Hnon [0]; } } // r1 в [0.1, 0.9] — исключаем вырожденные комбинации double r1 = u.RNDfromCI(0.1, 0.9); double r2 = 1.0 - r1; for(int c = 0; c < coords; c++) { double v = r1 * snap_c [ia].v [c] + r2 * snap_c [ib].v [c]; // Возмущение с адаптивной амплитудой по cooling-графику double range = rangeMax [c] - rangeMin [c]; v += u.RNDfromCI(-1.0, 1.0) * range * perturbScale; if(v < rangeMin [c]) v = rangeMin [c]; if(v > rangeMax [c]) v = rangeMax [c]; combo [m].c [c] = v; } combo [m].sec_a = snap_sectorOf [ia]; combo [m].sec_b = snap_sectorOf [ib]; } // Diversity комбинаций относительно агента (центр = координаты агента) double divAgg = 0.0; for(int c = 0; c < coords; c++) { double dj = 0.0; for(int m = 0; m < Ncomb; m++) dj += MathAbs(combo [m].c [c] - snap_c [agSlot].v [c]); dj /= Ncomb; divAgg += dj; } divAgg /= coords; if(divAgg > bestDiv) { bestDiv = divAgg; for(int m = 0; m < Ncomb; m++) { for(int c = 0; c < coords; c++) bestSet [m].c [c] = combo [m].c [c]; bestSet [m].sec_a = combo [m].sec_a; bestSet [m].sec_b = combo [m].sec_b; } } } // конец цикла по шаблонам // Fallback: ни один шаблон не нашёл материала → возмущения вокруг агента if(bestDiv < 0) { for(int m = 0; m < Ncomb; m++) { for(int c = 0; c < coords; c++) { double range = rangeMax [c] - rangeMin [c]; double v = snap_c [agSlot].v [c] + u.RNDfromCI(-1.0, 1.0) * range * 0.1; if(v < rangeMin [c]) v = rangeMin [c]; if(v > rangeMax [c]) v = rangeMax [c]; bestSet [m].c [c] = v; } bestSet [m].sec_a = anchorSec; bestSet [m].sec_b = anchorSec; } } // Записать bestSet в общий список candidates for(int m = 0; m < Ncomb; m++) { for(int c = 0; c < coords; c++) candidates [candPos].c [c] = bestSet [m].c [c]; candidates [candPos].sec_a = bestSet [m].sec_a; candidates [candPos].sec_b = bestSet [m].sec_b; candidates [candPos].f = -DBL_MAX; candPos++; } } // конец цикла по агентам //--- 4. Разместить кандидатов в слоты не-агентов для оценки FF // По правилу (ii) статьи: новый цвет ставится на сектор одного из своих // родителей. Сначала ищем слот с подходящим сектором; если занят или // отсутствует — fallback в любой свободный неагентский слот. bool isAgentSlot []; ArrayResize(isAgentSlot, popSize); ArrayInitialize(isAgentSlot, false); for(int g = 0; g < 10; g++) if(agentIdx [g] >= 0) isAgentSlot [agentIdx [g]] = true; bool slotTaken []; ArrayResize(slotTaken, popSize); ArrayInitialize(slotTaken, false); for(int m = 0; m < Kcand; m++) { int sec_a = candidates [m].sec_a; int sec_b = candidates [m].sec_b; int chosenSlot = -1; // 1) попытка: неагентский слот с подходящим сектором for(int i = 0; i < popSize; i++) { if(isAgentSlot [i] || slotTaken [i]) continue; if(snap_sectorOf [i] == sec_a || snap_sectorOf [i] == sec_b) { chosenSlot = i; break; } } // 2) fallback: любой свободный неагентский слот if(chosenSlot < 0) { for(int i = 0; i < popSize; i++) { if(isAgentSlot [i] || slotTaken [i]) continue; chosenSlot = i; break; } } if(chosenSlot < 0) break; // на всякий случай — все слоты заняты slotTaken [chosenSlot] = true; slotCandidate [chosenSlot] = m; isCandidate [chosenSlot] = true; for(int c = 0; c < coords; c++) a [chosenSlot].c [c] = u.SeInDiSp(candidates [m].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); } //--- 5. Слоты агентов — без изменений (a[i].c = snap_c[i]) for(int g = 0; g < 10; g++) { int s = agentIdx [g]; if(s < 0) continue; for(int c = 0; c < coords; c++) a [s].c [c] = u.SeInDiSp(snap_c [s].v [c], rangeMin [c], rangeMax [c], rangeStep [c]); } } //+------------------------------------------------------------------+
ConcentrationPhase_UpdateCircle — применение результатов оценки FF. Сначала фитнесы кандидатов копируются из a[i].f обратно в candidates[m].f через связи slotCandidate[]. Затем проводится greedy-селекция: каждый слот, в который был помещён кандидат, сравнивает фитнес кандидата с фитнесом из snapshot. Если кандидат лучше — он остаётся в слоте, флаг candidateUsed[m] поднимается. Если хуже — слот откатывается из snapshot. Это упрощение оригинального правила круга, в котором перекрёстная фильтрация шагов 4–12 пропускала только малую долю кандидатов в популяцию; greedy-селекция даёт всем равные шансы и существенно ускоряет движение алгоритма.
После селекции слоты агентов восстанавливаются из snapshot полностью — это нужно на случай, если SeInDiSp в Moving слегка сместил координаты при дискретизации. Все непринятые кандидаты складываются в кольцевой буфер CMtempC[]. В конце вызывается UpdateAgentsByGroup для возможной смены лидеров групп.
//+------------------------------------------------------------------+ //| ФАЗА КОНЦЕНТРАЦИИ — обновление круга (greedy + sector) | //+------------------------------------------------------------------+ void C_AO_CHA::ConcentrationPhase_UpdateCircle() { int Kcand = ArraySize(candidates); //--- 1. Считать фитнесы кандидатов из их временных слотов в a[] for(int i = 0; i < popSize; i++) { int m = slotCandidate [i]; if(m >= 0) candidates [m].f = a [i].f; } //--- 2. Greedy-селекция: для каждого слота с кандидатом сравнить // его фитнес с фитнесом из snapshot. Лучший — остаётся, худший — // откатывается. Это даёт всем 10·Ncomb кандидатам равный шанс на // вход в популяцию (в отличие от строгих перекрёстных правил // оригинальной статьи, которые отбрасывают большинство кандидатов // и оставляют популяцию почти без движения). for(int i = 0; i < popSize; i++) { int m = slotCandidate [i]; if(m < 0) continue; if(candidates [m].f > snap_f [i]) { // принять — координаты уже в a[i].c, фитнес уже в a[i].f candidateUsed [m] = true; // sectorOf[i] не меняется (правило (ii) статьи) } else { // отвергнуть — откатить слот for(int c = 0; c < coords; c++) a [i].c [c] = snap_c [i].v [c]; a [i].f = snap_f [i]; } } //--- 3. Слоты агентов — восстановить из snapshot на случай, если // SeInDiSp в Moving слегка сместил координаты при дискретизации. for(int g = 0; g < 10; g++) { int s = agentIdx [g]; if(s < 0) continue; for(int c = 0; c < coords; c++) a [s].c [c] = snap_c [s].v [c]; a [s].f = snap_f [s]; } //--- 4. Непринятые кандидаты → CMtemp for(int m = 0; m < Kcand; m++) { if(candidateUsed [m]) continue; int dst = CMtempCnt % CMtempCap; for(int c = 0; c < coords; c++) CMtempC [dst].v [c] = candidates [m].c [c]; CMtempCnt++; if(CMtempCnt >= 2 * CMtempCap) CMtempCnt = CMtempCap; } //--- 5. Обновить агентов групп (обмен секторов при появлении // нового лидера в группе) UpdateAgentsByGroup(); } //+------------------------------------------------------------------+
DispersionPhase_Generate— реализует формулу Манселла для рассеивания популяции. Сначала рассчитывается центр популяции, затем находятся Ns ближайших к центру не-агентов — именно они подлежат замене. Источник материала для замены выбирается так: если основная цветовая память CM уже заполнена (CMcount ≥ Ksets), берётся самый разнообразный набор из неё; иначе, если в CMtempC есть хоть какие-то накопленные кандидаты, делается случайная выборка из них; иначе — fallback на равномерную генерацию точек по всему пространству поиска.
Выбранный источник SCM оценивается по разнообразию относительно центра пространства поиска. Если основная память не заполнена, новый набор просто добавляется в неё; если заполнена, ищется существующий набор с минимальным разнообразием — и заменяется на новый, если у нового разнообразие больше.
Финальный шаг — генерация замен по формуле X_new = rcm·X_SCM + (1 − rcm)·X_old, где коэффициент rcm берётся равномерно из [rcm0, 1]. Параметр rcm0 (по умолчанию 0.7) гарантирует, что новый цвет в основном определяется точкой из памяти, но сохраняет некоторую связь со старой позицией. Замены пишутся непосредственно в слоты a[sxIdx[k]].c[] через SeInDiSp для дискретизации.
//+------------------------------------------------------------------+ //| ФАЗА РАССЕИВАНИЯ — генерация | //+------------------------------------------------------------------+ void C_AO_CHA::DispersionPhase_Generate() { //--- 1. Центр популяции double center []; PopulationCenter(center); //--- 2. SX = Ns ближайших к центру не-агентов double dist []; ArrayResize(dist, popSize); for(int i = 0; i < popSize; i++) { bool ag = false; for(int g = 0; g < 10; g++) if(agentIdx [g] == i) { ag = true; break; } if(ag) { dist [i] = DBL_MAX; continue; } double d = 0.0; for(int c = 0; c < coords; c++) d += MathAbs(a [i].c [c] - center [c]); dist [i] = d; } int sxIdx []; ArrayResize(sxIdx, Ns); for(int k = 0; k < Ns; k++) { double mn = DBL_MAX; int mi = -1; for(int i = 0; i < popSize; i++) if(dist [i] < mn) { mn = dist [i]; mi = i; } sxIdx [k] = mi; if(mi >= 0) dist [mi] = DBL_MAX; } //--- 3. Источник SCM: CM (если заполнен) или CMtemp double centerSpace []; ArrayResize(centerSpace, coords); for(int c = 0; c < coords; c++) centerSpace [c] = (rangeMin [c] + rangeMax [c]) * 0.5; S_CHA_Vector scmPoints []; ArrayResize(scmPoints, Ns); for(int i = 0; i < Ns; i++) scmPoints [i].Init(coords); bool scmReady = false; if(CMcount >= Ksets) { // выбрать самый разнообразный набор из CM int bi = 0; double bd = CM [0].diversity; for(int s = 1; s < CMcount; s++) if(CM [s].diversity > bd) { bd = CM [s].diversity; bi = s; } for(int i = 0; i < Ns; i++) for(int c = 0; c < coords; c++) scmPoints [i].v [c] = CM [bi].points [i].v [c]; scmReady = true; } else if(CMtempCnt > 0) { int avail = MathMin(CMtempCnt, CMtempCap); if(avail >= Ns) { // случайный выбор Ns точек for(int k = 0; k < Ns; k++) { int idx = (int)u.RNDfromCI(0.0, (double)avail); if(idx >= avail) idx = avail - 1; for(int c = 0; c < coords; c++) scmPoints [k].v [c] = CMtempC [idx].v [c]; } } else { for(int k = 0; k < Ns; k++) { int src = k % avail; for(int c = 0; c < coords; c++) scmPoints [k].v [c] = CMtempC [src].v [c]; } } scmReady = true; } if(!scmReady) { // Fallback: случайные точки во всём поисковом пространстве for(int i = 0; i < Ns; i++) for(int c = 0; c < coords; c++) scmPoints [i].v [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]); } //--- 4. Сохранить SCM в CM (шаг 2 dispersion) // Diversity SCM относительно центра пространства поиска double scmDiv = 0.0; for(int c = 0; c < coords; c++) { double dj = 0.0; for(int i = 0; i < Ns; i++) dj += MathAbs(scmPoints [i].v [c] - centerSpace [c]); dj /= Ns; scmDiv += dj; } scmDiv /= coords; if(CMcount < Ksets) { int idx = CMcount; for(int i = 0; i < Ns; i++) for(int c = 0; c < coords; c++) CM [idx].points [i].v [c] = scmPoints [i].v [c]; CM [idx].diversity = scmDiv; CMcount++; } else { int mi = 0; double md = CM [0].diversity; for(int s = 1; s < CMcount; s++) if(CM [s].diversity < md) { md = CM [s].diversity; mi = s; } if(scmDiv > md) { for(int i = 0; i < Ns; i++) for(int c = 0; c < coords; c++) CM [mi].points [i].v [c] = scmPoints [i].v [c]; CM [mi].diversity = scmDiv; } } //--- 5. Сгенерировать новые цвета и записать в слоты SX for(int k = 0; k < Ns; k++) { int slot = sxIdx [k]; if(slot < 0) continue; double rcm = u.RNDfromCI(rcm0, 1.0); for(int c = 0; c < coords; c++) { double v = rcm * scmPoints [k].v [c] + (1.0 - rcm) * a [slot].c [c]; a [slot].c [c] = u.SeInDiSp(v, rangeMin [c], rangeMax [c], rangeStep [c]); } } } //+------------------------------------------------------------------+
DispersionPhase_UpdateCircle— самый короткий метод алгоритма: он всего лишь вызывает UpdateAgentsByGroup. По статье, после рассеивания «if replaced colors have better fitness value than their related agents, their positions on the hue circle will be exchanged» — UpdateAgentsByGroup именно это и делает: для каждой группы проверяет, не появился ли новый лидер, и при необходимости меняет местами их секторы.
//+------------------------------------------------------------------+ //| ФАЗА РАССЕИВАНИЯ — обновление круга после оценки FF | //+------------------------------------------------------------------+ void C_AO_CHA::DispersionPhase_UpdateCircle() { // По статье: «if replaced colors have better fitness value than their // related agents, their positions on the hue circle will be exchanged». // UpdateAgentsByGroup делает именно это (обмен секторов при смене лидера). UpdateAgentsByGroup(); } //+------------------------------------------------------------------+
Результаты тестов
Тестирование проводилось на стандартном бенчмарке C_AO: функции Hilly, Forest, Megacity на размерностях 10, 50, 1000 (5, 25, 500 функций), бюджет 10000 оценок FF, 10 повторений. Параметры алгоритма по умолчанию. Тест анти-чит алгоритм CHA проходит без проблем, набирая сопоставимое количество баллов на первых размерностях основного теста.CHA|Color Harmony Algorithm|50.0|4.0|10.0|4.0|0.5|0.01|0.7|
=============================
5 Hilly's; Func runs: 10000; result: 0.5861221565962496
25 Hilly's; Func runs: 10000; result: 0.46909923573046725
500 Hilly's; Func runs: 10000; result: 0.30514641914713303
=============================
5 Forest's; Func runs: 10000; result: 0.4702465987009819
25 Forest's; Func runs: 10000; result: 0.21050099785621573
500 Forest's; Func runs: 10000; result: 0.07964154815476658
=============================
5 Megacity's; Func runs: 10000; result: 0.42000000000000004
25 Megacity's; Func runs: 10000; result: 0.2437333333333333
500 Megacity's; Func runs: 10000; result: 0.12732000000000107
=============================
All score: 2.91181 (32.35%)
Визуализация работы алгоритма на тестовом стенде и на некоторых функциях.

CHA на тестовой функции Hilly

CHA на тестовой функции Forest

CHA на тестовой функции Megacity

CHA на функции Ackley

CHA на функции Paraboloid
В рейтинговой таблице алгоритм CHA представлен ознакомительно.
| cc | AO | Description | Hilly | Hilly Final | Forest | Forest Final | Megacity (discrete) | Megacity Final | Final Result | % of MAX | ||||||
| 10 p (5 F) | 50 p (25 F) | 1000 p (500 F) | 10 p (5 F) | 50 p (25 F) | 1000 p (500 F) | 10 p (5 F) | 50 p (25 F) | 1000 p (500 F) | ||||||||
| 1 | ANS | across neighbourhood search | 1,00000 | 0,88228 | 0,40138 | 2,28366 | 1,00000 | 0,95281 | 0,28092 | 2,23373 | 0,94667 | 0,85733 | 0,22389 | 2,02789 | 6,545 | 72,72 |
| 2 | AMOm | animal migration optimization M | 0,91624 | 0,83603 | 0,46790 | 2,22017 | 0,98482 | 0,92010 | 0,36391 | 2,26883 | 0,91733 | 0,81707 | 0,25177 | 1,98617 | 6,475 | 71,94 |
| 3 | CLA | code lock algorithm (joo) | 0,95139 | 0,86199 | 0,37879 | 2,19217 | 0,99349 | 0,93500 | 0,26497 | 2,19346 | 0,93600 | 0,84267 | 0,24060 | 2,01927 | 6,405 | 71,17 |
| 4 | (P+O)ES | (P+O) evolution strategies | 0,86571 | 0,89539 | 0,39740 | 2,15850 | 0,97761 | 0,89820 | 0,26878 | 2,14459 | 0,92133 | 0,80240 | 0,23952 | 1,96325 | 6,266 | 69,62 |
| 5 | SDSm | stochastic diffusion search M | 0,95195 | 0,84944 | 0,36249 | 2,16388 | 0,98061 | 0,88457 | 0,22112 | 2,08630 | 0,92267 | 0,79013 | 0,21380 | 1,92660 | 6,177 | 68,63 |
| 6 | AAm | archery algorithm M | 0,84685 | 0,73320 | 0,42590 | 2,00595 | 0,96709 | 0,77837 | 0,27789 | 2,02335 | 0,86133 | 0,77707 | 0,28712 | 1,92552 | 5,955 | 66,17 |
| 7 | SIA | simulated isotropic annealing (joo) | 0,93543 | 0,86504 | 0,38483 | 2,18530 | 0,94069 | 0,80609 | 0,23835 | 1,98513 | 0,86400 | 0,66160 | 0,19536 | 1,72096 | 5,891 | 65,46 |
| 8 | TETA | time evolution travel algorithm (joo) | 0,91452 | 0,86369 | 0,25579 | 2,03400 | 0,99654 | 0,91291 | 0,14394 | 2,05339 | 0,85467 | 0,82213 | 0,10443 | 1,78123 | 5,869 | 65,21 |
| 9 | ESG | evolution of social groups (joo) | 0,98111 | 0,79857 | 0,31167 | 2,09135 | 0,98954 | 0,82270 | 0,15032 | 1,96256 | 0,92133 | 0,73440 | 0,15315 | 1,80888 | 5,863 | 65,14 |
| 10 | CTA | comet tail algorithm (joo) | 0,92435 | 0,86786 | 0,27838 | 2,07059 | 0,99039 | 0,84571 | 0,19448 | 2,03058 | 0,95467 | 0,69680 | 0,11008 | 1,76155 | 5,863 | 65,14 |
| 11 | ECBO | enhanced colliding bodies optimization | 0,94024 | 0,72363 | 0,32356 | 1,98743 | 0,99477 | 0,80291 | 0,13056 | 1,92824 | 0,87600 | 0,70160 | 0,17433 | 1,75193 | 5,668 | 62,98 |
| 12 | DA | dialectical algorithm | 0,93117 | 0,75400 | 0,26205 | 1,94722 | 0,98925 | 0,81375 | 0,08662 | 1,88962 | 0,92667 | 0,68107 | 0,11315 | 1,72089 | 5,558 | 61,76 |
| 13 | BBO | biogeography based optimization | 0,95876 | 0,70609 | 0,35752 | 2,02237 | 0,92981 | 0,70660 | 0,16970 | 1,80611 | 0,87467 | 0,63013 | 0,20813 | 1,71293 | 5,541 | 61,57 |
| 14 | BHAm | black hole algorithm M | 0,79558 | 0,76207 | 0,34682 | 1,90447 | 0,99836 | 0,75798 | 0,13826 | 1,89460 | 0,85067 | 0,64427 | 0,17020 | 1,66514 | 5,464 | 60,71 |
| 15 | HS | harmony search | 0,91420 | 0,69049 | 0,29924 | 1,90393 | 0,97627 | 0,73373 | 0,14193 | 1,85193 | 0,91733 | 0,62720 | 0,15364 | 1,69817 | 5,454 | 60,60 |
| 16 | RFO | royal flush optimization (joo) | 0,80989 | 0,74481 | 0,34546 | 1,90016 | 0,95251 | 0,77926 | 0,15185 | 1,88362 | 0,80400 | 0,66427 | 0,19071 | 1,65898 | 5,443 | 60,48 |
| 17 | BOAm | billiards optimization algorithm M | 0,76177 | 0,72421 | 0,25275 | 1,73873 | 0,90890 | 0,81960 | 0,28853 | 2,01703 | 0,83733 | 0,74613 | 0,09763 | 1,68109 | 5,437 | 60,41 |
| 18 | ASO | anarchy society optimization | 0,73070 | 0,73713 | 0,31195 | 1,77978 | 0,99732 | 0,87700 | 0,17619 | 2,05051 | 0,72000 | 0,68773 | 0,18988 | 1,59761 | 5,428 | 60,31 |
| 19 | EOm | extremal optimization_M | 0,76527 | 0,75205 | 0,31908 | 1,83640 | 0,99999 | 0,76426 | 0,12437 | 1,88862 | 0,84133 | 0,64133 | 0,15247 | 1,63513 | 5,360 | 59,56 |
| 20 | ACS | artificial cooperative search | 0,75545 | 0,77162 | 0,31653 | 1,84360 | 1,00000 | 0,80488 | 0,10705 | 1,91193 | 0,76933 | 0,60800 | 0,14157 | 1,51890 | 5,274 | 58,60 |
| 21 | SSG | saplings sowing and growing | 0,75436 | 0,63206 | 0,35935 | 1,74577 | 0,91907 | 0,69694 | 0,19755 | 1,81356 | 0,81867 | 0,60533 | 0,21347 | 1,63747 | 5,197 | 57,74 |
| 22 | AOSm | atomic orbital search M | 0,76184 | 0,68435 | 0,31344 | 1,75963 | 0,90015 | 0,80044 | 0,11501 | 1,81560 | 0,82800 | 0,63280 | 0,15696 | 1,61776 | 5,193 | 57,70 |
| 23 | TSEA | turtle shell evolution algorithm (joo) | 0,95809 | 0,64852 | 0,29571 | 1,90232 | 0,99522 | 0,58104 | 0,10542 | 1,68168 | 0,92133 | 0,52160 | 0,14567 | 1,58860 | 5,173 | 57,48 |
| 24 | DE | differential evolution | 0,96398 | 0,62346 | 0,26089 | 1,84833 | 0,98482 | 0,77018 | 0,11459 | 1,86959 | 0,93067 | 0,36213 | 0,11000 | 1,40280 | 5,121 | 56,90 |
| 25 | BIO | blood inheritance optimization (joo) | 0,72580 | 0,66522 | 0,31228 | 1,70330 | 0,99995 | 0,68125 | 0,11540 | 1,79660 | 0,85467 | 0,59333 | 0,15364 | 1,60164 | 5,102 | 56,69 |
| 26 | (PO)ES | (PO) evolution strategies | 0,73972 | 0,58190 | 0,38896 | 1,71058 | 0,91199 | 0,59975 | 0,21262 | 1,72436 | 0,82400 | 0,56240 | 0,23432 | 1,62072 | 5,056 | 56,18 |
| 27 | BO | bonobo optimizer | 0,75555 | 0,64366 | 0,32657 | 1,72578 | 0,94332 | 0,70442 | 0,13999 | 1,78773 | 0,73467 | 0,61440 | 0,16728 | 1,51635 | 5,030 | 55,89 |
| 28 | SRA | successful restaurateur algorithm (joo) | 0,89010 | 0,63359 | 0,29115 | 1,81484 | 0,96634 | 0,55285 | 0,08914 | 1,60833 | 0,89333 | 0,52800 | 0,13911 | 1,56044 | 4,984 | 55,38 |
| 29 | CRO | chemical reaction optimisation | 0,91281 | 0,65681 | 0,29866 | 1,86828 | 0,90513 | 0,56020 | 0,10939 | 1,57472 | 0,82800 | 0,50133 | 0,14149 | 1,47082 | 4,914 | 54,60 |
| 30 | BCOm | bacterial chemotaxis optimization M | 0,82589 | 0,61733 | 0,31584 | 1,75906 | 0,95296 | 0,63718 | 0,11984 | 1,70998 | 0,76533 | 0,51653 | 0,15800 | 1,43986 | 4,909 | 54,54 |
| 31 | DOA | dream optimization algorithm | 0,78522 | 0,78121 | 0,36036 | 1,92679 | 0,61584 | 0,42117 | 0,12254 | 1,15955 | 0,86667 | 0,72587 | 0,21127 | 1,80381 | 4,890 | 54,33 |
| 32 | ABO | african buffalo optimization | 0,92295 | 0,62528 | 0,29885 | 1,84708 | 0,92992 | 0,57468 | 0,09372 | 1,59832 | 0,73333 | 0,51333 | 0,14324 | 1,38990 | 4,835 | 53,72 |
| 33 | BSA | bird swarm algorithm | 0,94432 | 0,67941 | 0,26401 | 1,88774 | 0,91649 | 0,65619 | 0,12054 | 1,69322 | 0,80933 | 0,33547 | 0,10652 | 1,25132 | 4,832 | 53,69 |
| 34 | TSm | tabu search M | 0,87806 | 0,61040 | 0,28993 | 1,77839 | 0,98116 | 0,52165 | 0,08544 | 1,58825 | 0,82667 | 0,49547 | 0,13552 | 1,45766 | 4,824 | 53,60 |
| 35 | BSA | backtracking search algorithm | 0,87128 | 0,53190 | 0,28675 | 1,68993 | 0,92408 | 0,51602 | 0,09153 | 1,53163 | 0,96000 | 0,47253 | 0,13760 | 1,57013 | 4,792 | 53,24 |
| 36 | WOAm | whale optimization algorithm M | 0,93893 | 0,59477 | 0,26695 | 1,80065 | 0,98036 | 0,53873 | 0,07112 | 1,59021 | 0,78667 | 0,47600 | 0,11892 | 1,38159 | 4,772 | 53,02 |
| 37 | CSO | competitive swarm optimizer | 0,85151 | 0,60786 | 0,29896 | 1,75833 | 0,84085 | 0,58491 | 0,11974 | 1,54550 | 0,80000 | 0,48560 | 0,14184 | 1,42744 | 4,731 | 52,57 |
| 38 | FBA | fractal-based algorithm | 0,69419 | 0,64267 | 0,28955 | 1,62641 | 0,99812 | 0,54905 | 0,08705 | 1,63422 | 0,76133 | 0,51253 | 0,13689 | 1,41075 | 4,671 | 51,90 |
| 39 | ECOi | eco-inspired evolutionary algorithm | 0,78817 | 0,54402 | 0,29360 | 1,62579 | 0,88996 | 0,46592 | 0,09747 | 1,45335 | 0,78533 | 0,45173 | 0,14295 | 1,38001 | 4,459 | 49,54 |
| 40 | BSO | brain storm optimization | 0,92207 | 0,57625 | 0,29732 | 1,79564 | 0,80764 | 0,42508 | 0,09448 | 1,32720 | 0,77200 | 0,36533 | 0,13065 | 1,26798 | 4,391 | 48,79 |
| 41 | CAm | camel algorithm M | 0,71534 | 0,56917 | 0,35985 | 1,64436 | 0,84094 | 0,47174 | 0,10850 | 1,42118 | 0,70400 | 0,41947 | 0,19563 | 1,31910 | 4,385 | 48,72 |
| 42 | ACOm | ant colony optimization_M | 0,71885 | 0,48410 | 0,30990 | 1,51285 | 0,75792 | 0,48639 | 0,11871 | 1,36302 | 0,83600 | 0,48667 | 0,16148 | 1,48415 | 4,360 | 48,44 |
| 43 | BSO | brain storm optimization | 0,91331 | 0,55813 | 0,29705 | 1,76849 | 0,85329 | 0,44038 | 0,09447 | 1,38814 | 0,72267 | 0,32880 | 0,13325 | 1,18472 | 4,341 | 48,23 |
| 44 | AEFA | artificial electric field algorithm | 0,88798 | 0,65769 | 0,25276 | 1,79843 | 0,95550 | 0,63602 | 0,03946 | 1,63098 | 0,60800 | 0,16000 | 0,09845 | 0,86645 | 4,296 | 47,73 |
| 45 | SOA | simple optimization algorithm | 0,91664 | 0,47040 | 0,27095 | 1,65799 | 0,88264 | 0,31546 | 0,06044 | 1,25854 | 0,82933 | 0,31733 | 0,11579 | 1,26245 | 4,179 | 46,43 |
| CHA | color_harmony_algorithm | 0,58612 | 0,46909 | 0,30514 | 1,36035 | 0,47024 | 0,21050 | 0,07964 | 0,76038 | 0,42000 | 0,24373 | 0,12732 | 0,79105 | 2,912 | 32,35 | |
| RW | random walk | 0,49970 | 0,32333 | 0,25791 | 1,08094 | 0,30754 | 0,11470 | 0,04400 | 0,46624 | 0,36133 | 0,17013 | 0,10244 | 0,63390 | 2,181 | 24,23 | |
Выводы
CHA — пример того, как привлекательная метафора и как это математическая природа алгоритма могут расходиться, и проявляется это в процессе практической реализации. Структура круга тонов и правила гармоничного сочетания цветов выглядят содержательно, но базовый оператор кроссовера (линейная смесь) сводит весь алгоритм к движению внутри выпуклой оболочки начальной популяции. Без явного механизма, выводящего потомков за пределы этой оболочки, CHA не может оптимизировать функции, у которых оптимум не попал в стартовое облако. Мы добавили такой механизм — гауссово возмущение с cooling-графиком — и алгоритм начал работать. Но нужно понимать, что это не часть оригинальной формулировки.
В процессе доводки реализации до работоспособного состояния пришлось отступить от точного описания статьи в нескольких местах. В оригинале правило обновления круга включает пятнадцать шагов с перекрёстной фильтрацией кандидатов между группами. На практике эти фильтры в начальной фазе работы блокируют большинство кандидатов и сильно замедляют движение популяции. Шаг 6 («агент группы другого родителя должен быть лучше агента группы текущего слота») удалён полностью; шаг 8 («группа с минимальным средним фитнесом») заменён на простой выбор кандидата с максимальным фитнесом из подходящих.
Оригинальные шаги 4-12 правил круга строят глобальное соответствие между кандидатами и слотами. Это даёт примерно 5-10 принятых кандидатов из 40 — три четверти оценок FF тратятся впустую. Заменено на greedy: каждый слот сравнивает помещённого в него кандидата со своим snapshot до Moving, лучший побеждает. Все 40 кандидатов имеют шанс на вход в популяцию. Главное расширение, амплитуда возмущения, которая адаптивно меняется.
Сильные стороны алгоритма — структурированная популяция (геометрия круга тонов препятствует преждевременной кластеризации), явное разделение фаз эксплуатации и исследования по понятному критерию разнообразия, и память о ранее найденных решениях, используемая для перезапуска. Эти свойства делают CHA подходящим выбором для задач с известным расположением оптимума в относительно компактной области пространства поиска — типичные задачи калибровки моделей, подгонки параметров уже работающих систем, локальной оптимизации вокруг известного приближения.
Слабые стороны — низкая эффективность на функциях с сильно неравномерным ландшафтом. Для задач глобальной оптимизации в высоких размерностях с нерегулярными целевыми функциями стоит выбирать алгоритмы из верхней части рейтинговой таблицы.
Финальная оценка CHA на нашем бенчмарке — 32.35%, размещающая алгоритм вне нашего рейтинга. Это честный результат для метода, чья основная ценность лежит не в производительности, а в концептуальной свежести подхода и в напоминании о том, что за каждой эстетически привлекательной метафорой стоит проверять математическую состоятельность. Полный исходный код алгоритма и тестового скрипта доступен в приложении к статье.

Рисунок 4. Цветовая градация алгоритмов по соответствующим тестам

Рисунок 5. Гистограмма результатов тестирования алгоритмов (по шкале от 0 до 100, чем больше, тем лучше, где 100 — максимально возможный теоретический результат), в архиве скрипт для расчёта рейтинговой таблицы
Плюсы и минусы алгоритма
Плюсы:
- Решает простые задачи.
Минусы:
- Большое количество внешних параметров.
- Склонность к застреванию.
- Высокая сложность настройки и отладки алгоритма.
К статье прикреплён архив с актуальными версиями кодов алгоритмов. Автор статьи не несёт ответственности за абсолютную точность в описании канонических алгоритмов, во многие из них были внесены изменения для улучшения поисковых возможностей. Выводы и суждения, представленные в статьях, основываются на результатах проведённых экспериментов.
Программы, используемые в статье
| # | Имя | Тип | Описание |
|---|---|---|---|
| 1 | #C_AO.mqh | Включаемый файл | Родительский класс популяционных алгоритмов оптимизации |
| 2 | #C_AO_enum.mqh | Включаемый файл | Перечисление популяционных алгоритмов оптимизации |
| 3 | TestFunctions.mqh | Включаемый файл | Библиотека тестовых функций |
| 4 | TestStandFunctions.mqh | Включаемый файл | Библиотека функций тестового стенда |
| 5 | TestStand3D.mqh | Включаемый файл | 3D-панель визуализации для тестового стенда |
| 6 | Utilities.mqh | Включаемый файл | Библиотека вспомогательных функций |
| 7 | CalculationTestResults.mqh | Включаемый файл | Скрипт для расчёта результатов в сравнительную таблицу |
| 8 | Test_AO_All.mq5 | Скрипт | Единый испытательный стенд для всех популяционных алгоритмов оптимизации |
| 9 | Test_AO_AntiCheat | Скрипт | Тест на читерство алгоритмов оптимизации |
| 10 | Simple use of population optimization algorithms.mq5 | Скрипт | Простой пример использования популяционных алгоритмов оптимизации без визуализации |
| 11 | Test_AO_CHA.mq5 | Скрипт | Испытательный стенд для CHA |
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Разработка инструментария для анализа Price Action (Часть 38): VWAP на основе тикового буфера и модуль расчета дисбаланса на коротком окне
Как обучить MLP на признаках марковской цепи в MQL5
Нейросети в трейдинге: Оценка риска по несогласованности представлений (Окончание)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования