Алгоритм оптимизации на основе кровеносной системы — Circulatory System Based Optimization (CSBO)
Содержание
Введение
Метафоры для популяционных алгоритмов берутся отовсюду: стаи, косяки, рои, эволюция, физика, экономика. Инспирации, основанные на биологических процессах, становятся более популярными. Кровеносная система человека — одна из самых продвинутых метафор, которые встречались в нашей серии. Сердце гонит кровь по двум кругам: малый круг ведёт венозную, бедную кислородом кровь через лёгкие, большой круг разносит артериальную по телу. Авторы алгоритма оптимизации на основе кровеносной системы CSBO увидели в этом готовую схему. Популяция — это масса крови. Слабые особи — венозная кровь, которую нужно насытить кислородом в малом круге, а сильные особи — артериальная кровь, которая работает в большом круге.
Авторы утверждают, что на тестовых наборах различных функций CSBO обходит широкий круг конкурентов. Нас интересует другое: как алгоритм поведёт себя на нашем стенде. Здесь дискретные ступени Megacity, острый гребень Forest и высокая размерность отсеивают методы, которые хорошо выглядят на гладких смещённых сферах. Войдет ли данный метод после серии испытаний в нашу рейтинговую таблицу?
Реализация алгоритма
У меня сформировалось два вопроса, на которые в ходе реализации мы ответим вместе. Первый — работает ли биологическая схема как схема оптимизации. CSBO разделяет популяцию по рангу и применяет к слабой и сильной частям разные операторы. Идея разумная: слабым нужна разведка, сильным — уточнение. Но реализация этой идеи в CSBO очень конкретна, и заранее видно, где могут быть проблемы: шаг венозной мутации убывает как обратный счётчик вызовов целевой функции, то есть после первой тысячи оценок эта фаза практически замирает.
Второй — что делает алгоритм в высокой размерности. Основной оператор CSBO (движение в венах) масштабирует шаг одним скалярным коэффициентом сразу по всем координатам. Опыт в нашей серии статей говорит, что полновекторный ход с жадной приёмкой в высокой размерности почти всегда отвергается: вероятность улучшить все координаты разом падает с размерностью. Гипотеза была, что это структурный потолок алгоритма. Забегая вперёд — она не подтвердилась, и то, как именно она не подтвердилась, оказалось главным результатом статьи.
И как всегда, нужен честный контроль: сравнение с чистым случайным поиском на том же бюджете. Для алгоритма, у которого слабые операторы, эта планка оказывается неожиданно высокой.
Авторы приводят таблицу соответствий между кровеносной системой и алгоритмом. В сжатом виде она такая: масса крови — популяция; движение крови по телу — движение в пространстве поиска; кровь с большим содержанием кислорода — целевая функция; цикл кровообращения — итерация; венозная кровь — слабые особи; артериальная кровь — сильные особи; очистка крови — изменение состава популяции; отделение CO₂ — кроссовер; малый и большой круги — разделение популяции.
Одна итерация CSBO состоит из трёх последовательных операторов, и каждый из них — жадный: кандидат заменяет агента только если строго лучше.
Инициализация
Популяция из N агентов размещается равномерно случайно в допустимом диапазоне:
BM_i = BM_min + rand(1, D) · (BM_max − BM_min), i = 1..N
Каждому агенту приписывается коэффициент p_i — случайное число из [0, 1]. Число агентов, попадающих в малый круг, NR = N / 3, в авторском варианте.
Оператор 1: движение крови в венах. Применяется ко всем N агентам. Для агента i выбираются три случайных попарно различных партнёра a1, a2, a3, не совпадающих с i, и строится новая позиция:
BM_i^new = BM_i + K2 · p_i · (BM_i − BM_a1) + K1 · p_i · (BM_a3 − BM_a2)где K1 и K2 — знаковые коэффициенты:
K2 = sign(F(BM_a1) − F(BM_i)) → +1 если a1 хуже i, −1 если лучше K1 = sign(F(BM_a2) − F(BM_a3)) → +1 если a2 хуже a3, −1 если лучше
(здесь F — минимизируемая функция; при равенстве значений авторы ставят +1).
Разберём, что это означает геометрически. Первое слагаемое: если случайный партнёр a1 хуже агента, агент отступает от него; если лучше — идёт к нему. Второе слагаемое: разность двух других случайных партнёров, ориентированная от худшего к лучшему. По сути это гибрид с направленными разностями. Важно, что p_i — один скаляр на агента, и он умножает обе разности целиком: направление шага задаётся разностными векторами, а p_i только масштабирует его.
После прохода по всем агентам популяция сортируется по возрастанию F. Лучшие N − NR агентов идут в большой круг, худшие NR — в малый.
Оператор 2: большой круг (систематическое кровообращение). Применяется к лучшим N − NR агентам. Для агента i вычисляется его относительное качество:
ratio_i = (F(BM_i) − F_worst) / (F_best − F_worst)
ratio_i = 1 для лучшего агента популяции, 0 для худшего. Это значение записывается в p_i и будет использовано в венозном операторе следующей итерации — то есть лучший агент в следующий раз сделает полный шаг, а агент у нижней границы элиты — почти нулевой.
Затем строится мутантный вектор по схеме с этим же коэффициентом:
V = BM_a1 + ratio_i · (BM_a3 − BM_a2)
и применяется равномерный кроссовер: каждая координата берётся из V с вероятностью 0.1, иначе остаётся от BM_i .
Оператор 3: малый круг (лёгочное кровообращение). Применяется к худшим NR агентам. Каждый получает возмущение с распределением Коши:
BM_i^new = BM_i + (randn / it) · randc(1, D)
где randn — одно нормальное число на агента, randc — вектор из D независимых значений с распределением Коши, it — счётчик вызовов целевой функции. После этого p_i агента сбрасывается в случайное значение из [0, 1]. Тяжёлый хвост Коши изредка выбрасывает агента далеко, но основная масса ходов — микроскопическая. Шаг записан в абсолютных единицах и никак не соотнесён с диапазоном задачи. Для реализации на произвольных диапазонах его придётся нормировать.
Псевдокод алгоритма:
ПАРАМЕТРЫ: N, NR = N/3, CR = 0.1 ИНИЦИАЛИЗАЦИЯ x_i ~ U(lo, hi), f_i = F(x_i), p_i ~ U(0,1) для i = 1..N it = N ПОКА it < MaxFE: // 1. Вены — все агенты для i = 1..N: a1, a2, a3 — случайные, различные, ≠ i K1 = sign(f_a2 − f_a3), K2 = sign(f_a1 − f_i) (0 → +1) y = clamp(x_i + K2·p_i·(x_i − x_a1) + K1·p_i·(x_a3 − x_a2)) если F(y) < f_i: принять; it++ сортировать по f (лучшие первыми) // 2. Большой круг — лучшие N−NR для i = 1..N−NR: ratio = (f_i − f_worst)/(f_best − f_worst); p_i = ratio a1, a2, a3 — случайные, различные, ≠ i v = x_a1 + ratio·(x_a3 − x_a2) y_j = (rand < CR) ? v_j : x_ij для каждой координаты j если F(clamp(y)) < f_i: принять; it++ // 3. Малый круг — худшие NR для i = N−NR+1..N: s = randn / it y_j = x_ij + s · cauchy() для каждой координаты j если F(clamp(y)) < f_i: принять; it++ p_i ~ U(0,1)
Бюджет одной итерации — ровно 2N вызовов целевой функции.

Рисунок 1. Одна итерация CSBO: три оператора, 2·N оценок целевой функции.
Четыре главных итога:
- Работающее ядро — вены и большой круг.
- Малый круг — измеренный ноль: множитель 1/it гасит шаг примерно за первые 100 оценок. Отключение фазы (NR = 0) даёт +7.6 pp при N = 7.
- Главный рычаг — размер популяции, и он тянет вниз: N = 45 (авторский) → 35%, N = 7 → 53%, NR = 0 → 61–63% при N = 7–10.
- Разностное слагаемое в большом круге несущее: чистое покоординатное копирование (diffScale = 0) теряет 13 pp во всех девяти клетках.
Реализация в C_AO. Решения, принятые до первого прогона
Стенд C_AO максимизирует, авторы минимизируют — знаки K1, K2 и формула ratio переведены; при равенстве фитнесов знак остаётся +1.
Одна итерация CSBO не укладывается в одну эпоху стенда: большой и малый круги зависят от сортировки, которая делается после венозной фазы. Поэтому одна итерация разделена на две эпохи. Фаза 0: вены (popSize кандидатов). Фаза 1: большой круг для лучших и малый для худших (ещё popSize кандидатов). Сортировка выполняется в Revision фазы 0. Бюджет итерации — 2·popSize оценок, как в каноне; при бюджете 10 000 и popSize 45 это 111 полных итераций.
Шаг малого круга у авторов записан в абсолютных единицах. На произвольных диапазонах стенда это бессмысленно, поэтому он нормирован на ширину диапазона по каждой координате:
step = cauchyScale · N(0,1) / feCount · Cauchy · rSize
где feCount — счётчик выполненных оценок. Авторскому диапазону соответствует cauchyScale = 0.005.
Число агентов малого круга задаётся долей nrPart :
nr = round(popSize · nrPart)допускается nr = 0 — в этом случае большой круг обрабатывает всех агентов, а малый не вызывается.
Класс C_AO_CSBO. Отдельной структуры агента у CSBO нет — хватает базовой S_AO_Agent: в c лежит кандидат текущей эпохи, в cB/fB — принятая позиция и её фитнес. Единственное, чего в базовом классе нет, — скалярный коэффициент p_i венозной фазы; он хранится в отдельном массиве и переставляется вместе с агентами при сортировке.
Четыре параметра. popSize — размер популяции; нижняя граница 4, потому что каждому агенту нужны три различных партнёра, не совпадающих с ним самим. nrPart — доля популяции в малом круге, у авторов 1/3; по итогам исследования дефолт 0. crossProb — вероятность взять координату из мутантного вектора в большом круге, авторские 0.1. cauchyScale — масштаб шага малого круга в долях диапазона; при nrPart = 0 он не используется. SetParams после предохранителей записывает значения обратно в params, чтобы строка в логе стенда показывала то, что реально работает, а не то, что ввели.
Приватное состояние: p[] — коэффициенты венозной фазы; nr — вычисленное число агентов малого круга; phase — переключатель эпох; feCount — счётчик выполненных оценок, знаменатель в формуле малого круга; пара nCached / nSpare — кэш второго значения преобразования Бокса–Мюллера.
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class C_AO_CSBO : public C_AO { public: ~C_AO_CSBO() {} C_AO_CSBO() { ao_name = "CSBO"; ao_desc = "Circulatory System Based Optimization"; ao_link = "https://www.mql5.com/ru/articles/24504"; popSize = 10; // размер популяции nrPart = 0.0; // доля худших в малом круге (канон: 1/3) crossProb = 0.1; // вероятность взять координату мутанта в большом круге cauchyScale = 0.2; // масштаб шага Коши в долях диапазона ArrayResize(params, 4); params [0].name = "popSize"; params [0].val = popSize; params [1].name = "nrPart"; params [1].val = nrPart; params [2].name = "crossProb"; params [2].val = crossProb; params [3].name = "cauchyScale"; params [3].val = cauchyScale; } void SetParams() { popSize = (int)params [0].val; nrPart = params [1].val; crossProb = params [2].val; cauchyScale = params [3].val; //--- предохранители if(popSize < 4) // агент + три различных партнёра popSize = 4; if(nrPart < 0.0) nrPart = 0.0; if(nrPart > 1.0) nrPart = 1.0; if(crossProb < 0.0) crossProb = 0.0; if(crossProb > 1.0) crossProb = 1.0; if(cauchyScale < 0.0) cauchyScale = 0.0; params [0].val = popSize; params [1].val = nrPart; params [2].val = crossProb; params [3].val = cauchyScale; } bool Init(const double &rangeMinP [], const double &rangeMaxP [], const double &rangeStepP [], const int epochsP = 0); void Moving(); void Revision(); //--- видимые параметры double nrPart; // доля популяции в малом круге double crossProb; // CR большого круга double cauchyScale; // масштаб шага малого круга private: double p []; // [popSize] — скалярный коэффициент p_i (фаза вен) int nr; // число агентов в малом круге int phase; // 0 — вены; 1 — большой + малый круг int feCount; // выполненных вызовов ФФ (аналог `it` канона) bool nCached; // кэш Бокса-Мюллера double nSpare; //--- вспомогательные double RandN(); double RandC(); void Pick3(int i, int &a1, int &a2, int &a3); void SwapAgents(int i, int j); void SortByFitness(); void MakeVein(int i); void MakeSystemic(int i, double fBestPop, double fWorstPop); void MakePulmonary(int i); };
Инициализация Init. StandardInit базового класса выделяет агентов и копирует диапазоны. Здесь вычисляется nr — с округлением, поэтому при popSize 7 и nrPart 0.3333 получается 2, а не 2.33. Верхний предохранитель nr ≤ popSize − 1 оставляет в большом круге хотя бы одного агента. Нижний допускает ноль.
//+------------------------------------------------------------------+ //| Init | //+------------------------------------------------------------------+ bool C_AO_CSBO::Init(const double &rangeMinP [], const double &rangeMaxP [], const double &rangeStepP [], const int epochsP = 0) { if(!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) return false; nr = (int)MathRound(popSize * nrPart); if(nr < 0) nr = 0; if(nr > popSize - 1) nr = popSize - 1; phase = 0; feCount = 0; nCached = false; nSpare = 0.0; ArrayResize(p, popSize); ArrayInitialize(p, 0.0); return true; }
Нормальное распределение по Боксу–Мюллеру. Пара равномерных чисел даёт пару независимых нормальных, второе кэшируется до следующего вызова. Срез на ±4σ защищает от редких выбросов, u1 ≥ 10⁻¹² — от логарифма нуля. Используется только в малом круге.
//+------------------------------------------------------------------+ //| RandN — N(0,1) по Боксу-Мюллеру, срез на +-4 сигмы, кэш пары | //+------------------------------------------------------------------+ double C_AO_CSBO::RandN() { if(nCached) { nCached = false; return nSpare; } double u1 = u.RNDprobab(); double u2 = u.RNDprobab(); if(u1 < 1.0e-12) u1 = 1.0e-12; double rad = MathSqrt(-2.0 * MathLog(u1)); double ang = 2.0 * M_PI * u2; double z0 = rad * MathCos(ang); double z1 = rad * MathSin(ang); if(z1 > 4.0) z1 = 4.0; if(z1 < -4.0) z1 = -4.0; nSpare = z1; nCached = true; if(z0 > 4.0) z0 = 4.0; if(z0 < -4.0) z0 = -4.0; return z0; }
Стандартное распределение Коши через обратную функцию. Хвост не срезается — SeInDiSp при построении кандидата загонит любой выброс в диапазон, а тяжёлый хвост как раз и есть смысл этого распределения.
//+------------------------------------------------------------------+ //| RandC — стандартное Коши: tan(pi*(u-0.5)), хвост не режем | //| (SeInDiSp сам загонит выброс в диапазон) | //+------------------------------------------------------------------+ double C_AO_CSBO::RandC() { double v = u.RNDprobab(); if(v < 1.0e-12) v = 1.0e-12; if(v > 1.0 - 1.0e-12) v = 1.0 - 1.0e-12; return MathTan(M_PI * (v - 0.5)); }Pick3. Три попарно различных индекса, ни один не равен i.
//+------------------------------------------------------------------+ //| Pick3 — три попарно различных индекса, все != i (randperm) | //+------------------------------------------------------------------+ void C_AO_CSBO::Pick3(int i, int &a1, int &a2, int &a3) { do a1 = u.RNDminusOne(popSize); while(a1 == i); do a2 = u.RNDminusOne(popSize); while(a2 == i || a2 == a1); do a3 = u.RNDminusOne(popSize); while(a3 == i || a3 == a1 || a3 == a2); }
Обмен принятых состояний.
//+------------------------------------------------------------------+ //| SwapAgents — обмен принятых состояний (cB, fB, p) | //+------------------------------------------------------------------+ void C_AO_CSBO::SwapAgents(int i, int j) { double tmpC []; ArrayResize(tmpC, coords); ArrayCopy(tmpC, a [i].cB, 0, 0, coords); ArrayCopy(a [i].cB, a [j].cB, 0, 0, coords); ArrayCopy(a [j].cB, tmpC, 0, 0, coords); double tf = a [i].fB; a [i].fB = a [j].fB; a [j].fB = tf; double tp = p [i]; p [i] = p [j]; p [j] = tp; }
Сортировка по убыванию fB. Лучшие первыми, как в каноне после перевода в максимизацию. Переставляются только принятые состояния (cB, fB) и коэффициент p. Кандидат c и его фитнес f к этому моменту уже обработаны приёмкой и будут перезаписаны в следующей эпохе. Сортировка вставками: популяция в десяток агентов, вызов раз в две эпохи — сложность значения не имеет. Сохранение порядка при равных фитнесах на ступенях Megacity будет полезно.
//+------------------------------------------------------------------+ //| SortByFitness — по убыванию fB (лучшие первыми), вставками | //+------------------------------------------------------------------+ void C_AO_CSBO::SortByFitness() { for(int i = 1; i < popSize; i++) { int j = i; while(j > 0 && a [j].fB > a [j - 1].fB) { SwapAgents(j, j - 1); j--; } } }
MakeVein — движение крови в венах. Формулы (8)–(9) в переводе на максимизацию. K2 = +1, если партнёр a1 хуже агента (fB меньше) — агент отходит от него; −1, если лучше — идёт к нему. K1 = +1, если a2 хуже a3 — разность x_a3 − x_a2 направлена от худшего к лучшему. p[i] — один скаляр на агента, умножает обе разности целиком. Направление шага задают разностные векторы, скаляр только масштабирует. Кандидат идёт через SeInDiSp, который загоняет координату в диапазон и на сетку шага.
//+------------------------------------------------------------------+ //| MakeVein — движение крови в венах, (8)(9) канона. | //| K2 = +1 если a1 хуже i (уход от худшего), -1 если лучше | //| (движение к лучшему), при равенстве +1 как у авторов. | //| K1 — то же для пары (a2, a3). p_i — скаляр на агента. | //+------------------------------------------------------------------+ void C_AO_CSBO::MakeVein(int i) { int a1, a2, a3; Pick3(i, a1, a2, a3); double K1 = (a [a2].fB < a [a3].fB) ? 1.0 : (a [a2].fB > a [a3].fB) ? -1.0 : 1.0; double K2 = (a [a1].fB < a [i].fB) ? 1.0 : (a [a1].fB > a [i].fB) ? -1.0 : 1.0; double x; for(int c = 0; c < coords; c++) { x = a [i].cB [c] + K2 * p [i] * (a [i].cB [c] - a [a1].cB [c]) + K1 * p [i] * (a [a3].cB [c] - a [a2].cB [c]); a [i].c [c] = u.SeInDiSp(x, rangeMin [c], rangeMax [c], rangeStep [c]); } }MakeSystemic — большой круг. Формулы (12)–(13) плюс кроссовер. ratio — относительное качество агента в популяции: 1 для лучшего, 0 для худшего; при spread около нуля — 1, вместо NaN. Оно сразу записывается в p[i] и станет коэффициентом венозной фазы следующей итерации: лучший агент в следующий раз сделает полный шаг, худший — нулевой. Мутантный вектор — DE/rand/1 с этим же коэффициентом, от случайного a1, а не от самого агента. Кроссовер равномерный: координата берётся из мутанта с вероятностью crossProb, иначе остаётся от агента. Нетронутые координаты копируются из cB явно — кандидат c должен быть полным вектором для оценки.
//+------------------------------------------------------------------+ //| MakeSystemic — большой круг, (12)(13) канона + кроссовер. | //| ratio = 1 для лучшего, 0 для худшего; запоминается в p_i для | //| следующей фазы вен. Мутант x_a1 + ratio*(x_a3 - x_a2), из | //| него берётся координата с вероятностью crossProb. | //+------------------------------------------------------------------+ void C_AO_CSBO::MakeSystemic(int i, double fBestPop, double fWorstPop) { double spread = fBestPop - fWorstPop; double ratio = (spread > 1.0e-12) ? (a [i].fB - fWorstPop) / spread : 1.0; p [i] = ratio; int a1, a2, a3; Pick3(i, a1, a2, a3); double x; for(int c = 0; c < coords; c++) { if(u.RNDprobab() < crossProb) { x = a [a1].cB [c] + ratio * (a [a3].cB [c] - a [a2].cB [c]); a [i].c [c] = u.SeInDiSp(x, rangeMin [c], rangeMax [c], rangeStep [c]); } else a [i].c [c] = a [i].cB [c]; } }
MakePulmonary — малый круг. Формула (10) в нормированном виде. Один нормальный скаляр s на агента, затухающий как 1/feCount , умножает вектор независимых значений Коши по координатам; масштаб — доля ширины диапазона по каждой координате. При бюджете 10 000 и popSize 10 feCount к десятой итерации равен 200, к сотой — 2000; типичный шаг — доли процента диапазона и меньше. В итоговой конфигурации метод не вызывается.
//+------------------------------------------------------------------+ //| MakePulmonary — малый круг, (10) канона. | //| Один скаляр N(0,1)/feCount на агента, вектор Коши по | //| координатам, масштаб — доля ширины диапазона. | //+------------------------------------------------------------------+ void C_AO_CSBO::MakePulmonary(int i) { double s = cauchyScale * RandN() / (double)MathMax(feCount, 1); double rSize, x; for(int c = 0; c < coords; c++) { rSize = rangeMax [c] - rangeMin [c]; x = a [i].cB [c] + s * RandC() * rSize; a [i].c [c] = u.SeInDiSp(x, rangeMin [c], rangeMax [c], rangeStep [c]); } }Сердце алгоритма. Три ветки. Первая эпоха — равномерная стартовая популяция, формула (7). Фаза 0 — вены для всех. Фаза 1 — лучший и худший фитнес считаются один раз на эпоху по принятым fB. Затем первые (popSize − nr) отсортированных агентов идут в большой круг, остальные — в малый. При nr = 0 второй цикл пуст.
//+------------------------------------------------------------------+ //| Moving | //+------------------------------------------------------------------+ void C_AO_CSBO::Moving() { //--- первый прогон: стартовая популяция if(!revision) { for(int i = 0; i < popSize; i++) for(int c = 0; c < coords; c++) a [i].c [c] = u.SeInDiSp(u.RNDfromCI(rangeMin [c], rangeMax [c]), rangeMin [c], rangeMax [c], rangeStep [c]); return; } //--- фаза 0: вены, все агенты if(phase == 0) { for(int i = 0; i < popSize; i++) MakeVein(i); return; } //--- фаза 1: популяция отсортирована в Revision фазы 0 double fBestPop = -DBL_MAX; double fWorstPop = DBL_MAX; for(int i = 0; i < popSize; i++) { if(a [i].fB > fBestPop) fBestPop = a [i].fB; if(a [i].fB < fWorstPop) fWorstPop = a [i].fB; } int nl = popSize - nr; for(int i = 0; i < nl; i++) MakeSystemic(i, fBestPop, fWorstPop); for(int i = nl; i < popSize; i++) MakePulmonary(i); }
Ревизия. Сначала глобальный лучший по всем кандидатам эпохи — до приёмки, чтобы не потерять кандидата, который оказался лучше глобального, но по какой-то причине не заменит своего агента (здесь такого не бывает, но порядок общий для серии). Счётчик оценок увеличивается на popSize за эпоху. На первой эпохе кандидаты фиксируются как принятые позиции; каждому агенту присваивается случайный p_i. Дальше — жадная приёмка со строгим неравенством: равный по фитнесу кандидат отвергается. После фазы 0 — сортировка и переключение на фазу 1. После фазы 1 — сброс p_i у агентов малого круга в случайное значение, формула (11), и возврат в фазу 0. Заметьте, что при nr = 0 сброса нет: все p_i берутся из ratio, и в венах каждый агент ходит с шагом, пропорциональным своему качеству.
//+------------------------------------------------------------------+ //| Revision | //+------------------------------------------------------------------+ void C_AO_CSBO::Revision() { //--- глобальный лучший for(int i = 0; i < popSize; i++) { if(a [i].f > fB) { fB = a [i].f; ArrayCopy(cB, a [i].c, 0, 0, coords); } } feCount += popSize; //--- первый проход: стартовая популяция становится принятой if(!revision) { for(int i = 0; i < popSize; i++) { ArrayCopy(a [i].cB, a [i].c, 0, 0, coords); a [i].fB = a [i].f; p [i] = u.RNDprobab(); } phase = 0; revision = true; return; } //--- жадная приёмка (строго лучше, как в коде авторов) 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(phase == 0) { //--- после вен: сортировка, дальше большой + малый круг SortByFitness(); phase = 1; return; } //--- после малого круга: сброс p_i у худших (11), обратно в вены for(int i = popSize - nr; i < popSize; i++) p [i] = u.RNDprobab(); phase = 0; }
Результаты тестов
Все прогоны — стандартный протокол серии: бюджет 10 000 оценок, 10 повторов, кроме итогового — там 50. Параметры в строке — popSize|nrPart|crossProb|cauchyScale.
Авторская конфигурация:
CSBO|Circulatory System Based Optimization|45.0|0.3333|0.1|1.0|
=============================
5 Hilly's; Func runs: 10000; result: 0.6607251542463473
25 Hilly's; Func runs: 10000; result: 0.3861597953635898
500 Hilly's; Func runs: 10000; result: 0.25998936851111754
=============================
5 Forest's; Func runs: 10000; result: 0.7175591891670521
25 Forest's; Func runs: 10000; result: 0.22806142896649267
500 Forest's; Func runs: 10000; result: 0.05441727647084734
=============================
5 Megacity's; Func runs: 10000; result: 0.5133333333333334
25 Megacity's; Func runs: 10000; result: 0.2392
500 Megacity's; Func runs: 10000; result: 0.10786666666666762
=============================
All score: 3.16731 (35.19%)
35%, и главное не в композите. В высокой размерности авторская конфигурация CSBO не делает ничего. На этом месте было бы легко остановиться и объявить потолок.
Мы ушли от авторских рекомендаций в сторону упрощения: популяция в четыре с половиной раза меньше, малый круг отключён. Это по-прежнему каноническая реализация: NR — параметр авторов, мы только выставили его в ноль. cauchyScale при nr = 0 не используется, значение в строке — ради воспроизводимости.
Итоговая конфигурация, всё же большой разброс в результатах, поэтому 50 повторов для усреднения:
CSBO|Circulatory System Based Optimization|10.0|0.0|0.1|0.2|
=============================
5 Hilly's; Func runs: 10000; result: 0.8779324877853923
25 Hilly's; Func runs: 10000; result: 0.7910554460390163
500 Hilly's; Func runs: 10000; result: 0.3002582938747114
=============================
5 Forest's; Func runs: 10000; result: 0.9502634902207033
25 Forest's; Func runs: 10000; result: 0.8802785984131085
500 Forest's; Func runs: 10000; result: 0.15364161525030118
=============================
5 Megacity's; Func runs: 10000; result: 0.8581333333333333
25 Megacity's; Func runs: 10000; result: 0.7152533333333336
500 Megacity's; Func runs: 10000; result: 0.14777066666666738
=============================
All score: 5.67459 (63.05%)
Композитный античит-тест. Стандартная для серии проверка на геометрическую зависимость: 10 координат разбиты на пять пар, каждая пара — своя функция (Hilly, Forest, Megacity, Peaks, Skin) со своим диапазоном и своей геометрией оптимума, фитнес — среднее по пяти. Любой осевой или диагональный чит выигрывает не больше чем на одной паре из пяти и в общем счёте не виден. Для CSBO проверка нужна: в обеих рабочих фазах один скаляр (p_i, ratio_i) умножает разностный вектор целиком.
Итоговая конфигурация, 10 повторов:
CSBO|Circulatory System Based Optimization|10.0|0.0|0.1|0.2|
=============================
Composite anti-cheat test: Hilly + Forest + Megacity + Peaks + Skin
Coordinates: 10; Epochs: 1000; Repeats: 10
=============================
Run 1/10: 0.9866639881113987
Run 2/10: 0.9866666666666667
Run 3/10: 0.9999999934108498
Run 4/10: 0.9866666666666595
Run 5/10: 0.9994982235385171
Run 6/10: 0.9999999744583151
Run 7/10: 0.9866666666666667
Run 8/10: 0.99999999962638
Run 9/10: 0.9994983028905517
Run 10/10: 0.8684408518425497
=============================
Average result: 0.9814101334 (98.14%)
=============================
92% на композите против 0.86–0.95 малой размерности стенда для той же конфигурации — результат на разнородных функциях не ниже, чем на однотипных. Скалярная связка в венах и большом круге геометрического выигрыша не даёт: разностные ходы инвариантны к сдвигу и повороту, скаляр их только масштабирует. Прирост 35 → 63 — от концентрации бюджета и отключения мёртвой фазы, а не от совпадения геометрий тестовых функций.
Визуализация работы алгоритма CSBO на тестовых функциях и на двух дополнительных, случайно выбранных из списка программы.

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

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

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

CSBO на тестовой функции Ackley

CSBO на тестовой функции Shaffer
По результатам тестирования алгоритм CSBO занимает 12-е место в рейтинге лучших популяционных методов оптимизации.
| 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 | COA | coyote_optimization_algorithm | 0,88909 | 0,70681 | 0,32718 | 1,92308 | 0,99467 | 0,85358 | 0,15152 | 1,99977 | 0,88533 | 0,71040 | 0,18981 | 1,78554 | 5,708 | 63,43 |
| 12 | CSBO | circulatory_system_based_optimization | 0,87793 | 0,79105 | 0,30025 | 1,96923 | 0,95026 | 0,88027 | 0,15364 | 1,98417 | 0,85813 | 0,71525 | 0,14777 | 1,72115 | 5,776 | 63,05 |
| 13 | 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 |
| 14 | 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 |
| 15 | 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 |
| 16 | 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 |
| 17 | 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 |
| 18 | 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 |
| 19 | 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 |
| 20 | 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 |
| 21 | 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 |
| 22 | 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 |
| 23 | 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 |
| 24 | 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 |
| 25 | 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 |
| 26 | DE | flow_direction_algorithm | 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 |
| 27 | 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 |
| 28 | (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 |
| 29 | 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 |
| 30 | 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 |
| 31 | FDAm | flow_direction_algorithm_M | 0,87573 | 0,58806 | 0,27135 | 1,73514 | 0,99997 | 0,64399 | 0,09633 | 1,74029 | 0,84267 | 0,48453 | 0,11888 | 1,44608 | 4,922 | 54,70 |
| 32 | 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 |
| 33 | 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 |
| 34 | 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 |
| 35 | 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 |
| 36 | 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 |
| 37 | 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 |
| 38 | 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 |
| 39 | BEA | bacterial_evolutionary_algorithm | 0,92170 | 0,59615 | 0,29340 | 1,81125 | 0,96906 | 0,44500 | 0,08233 | 1,49639 | 0,90533 | 0,43173 | 0,13676 | 1,47382 | 4,781 | 53,13 |
| 40 | BWOm | beluga_whale_optimization_M | 0,78488 | 0,56872 | 0,29557 | 1,64917 | 0,91370 | 0,61760 | 0,12988 | 1,66118 | 0,81333 | 0,49946 | 0,15004 | 1,46283 | 4,773 | 53,04 |
| 41 | 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 |
| 42 | ACA | andean_condor_algorithm | 0,78444 | 0,53260 | 0,33108 | 1,64812 | 0,79071 | 0,44960 | 0,10685 | 1,34716 | 0,92266 | 0,67733 | 0,17613 | 1,77612 | 4,771 | 53,02 |
| 43 | 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 |
| 44 | 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 |
| 45 | CCEm | city_councils_evolution_M_ | 0,78019 | 0,56748 | 0,25574 | 1,60341 | 0,93439 | 0,64927 | 0,11714 | 1,70080 | 0,81733 | 0,43786 | 0,10876 | 1,36395 | 4,668 | 51,87 |
| 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 | |
Выводы
Метафора кровеносной системы в CSBO делится на две части. Работающая — вены и большой круг: DE-подобное движение с направленными разностями и фитнес-зависимый DE/rand/1 с редким кроссовером. Декоративная — малый круг, лёгочное кровообращение, на которое авторы возлагают разведку: он измеримо даёт нулёвый вклад, потому что множитель 1/it в формуле (10) гасит шаг за первые сто вызовов функции. Отключение этой фазы и уменьшение популяции с 45 до 10 дают 28 pp, и оба изменения — против рекомендаций авторов.
Для практики: CSBO — крепкий алгоритм с сильным профилем в низких и средних размерностях.
Композитный античит подтверждает, что ни одна из этих цифр не опирается на геометрию тестовых функций. Ключевая рекомендация для параметров торговых систем — не брать авторские popSize 45 и NR = N/3; при бюджете в тысячи прогонов тестера они отдают половину оценок в никуда.
Алгоритм честный и заслуживает высокого рейтинга в нашей сравнительной таблице лучших популяционных методов оптимизации. Все необходимые инструменты для трейдера прикреплены к статье (в архиве), пожалуйста, экспериментируйте.

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

Рисунок 3. Гистограмма результатов тестирования алгоритмов (по шкале от 0 до 100: чем больше, тем лучше, где 100 — максимально возможный теоретический результат). В архиве — скрипт для расчёта рейтинговой таблицы
Плюсы и минусы алгоритма CSBO
Плюсы:
- Два простых оператора на разностных векторах, полностью инвариантных к сдвигу и повороту — античит это подтверждает.
- Сильный профиль в низких и средних размерностях.
- Минимум параметров, которые реально что-то решают: popSize и NR; crossProb и cauchyScale можно не трогать.
- Малая популяция и нулевые накладные расходы на агента — дёшево по памяти и по коду.
Минусы:
- Авторская конфигурация неработоспособна на нашем стенде: 35% и ноль в высокой размерности; без подбора параметров алгоритм выглядит вдвое хуже, чем он есть.
- Малый круг — мёртвая фаза, треть популяции в каноне сжигает бюджет впустую.
- В высоких размерностях результат честный, но слабее, чем в алгоритмах рядом по рейтинговой таблице.
- Скалярный p_i на весь вектор — полновекторный ход, который в высокой размерности принимается редко; резерва для модификации в этом месте не нашлось.
К статье прикреплён архив с актуальными версиями кодов алгоритмов. Автор статьи не несёт ответственности за абсолютную точность в описании канонических алгоритмов, во многие из них внесены изменения для улучшения поисковых возможностей. Выводы и суждения, представленные в статьях, основываются на результатах проведённых экспериментов.
Программы, используемые в статье
| # | Имя | Тип | Описание |
|---|---|---|---|
| 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_CSBO.mq5 | Скрипт | Испытательный стенд для CSBO |
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Репликация и моделирование рынка: Большой финал
Нейросети в трейдинге: Адаптация прогноза при смене рыночного режима (OpenCL примитивы)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Андрей, а есть-ли словосочетание на основе которого нельзя сделать оптимизацию ? :-)
например оптимизация на основе рубки леса запрещена по экологическим соображениям. А оптимизация путём кота по златой цепи невозможна ибо Пушкин наше всё
Андрей, а есть-ли словосочетание на основе которого нельзя сделать оптимизацию ? :-)
например оптимизация на основе рубки леса запрещена по экологическим соображениям. А оптимизация путём кота по златой цепи невозможна ибо Пушкин наше всё