preview
Алгоритм оптимизации на основе кровеносной системы — Circulatory System Based Optimization (CSBO)

Алгоритм оптимизации на основе кровеносной системы — Circulatory System Based Optimization (CSBO)

MetaTrader 5Трейдинг |
60 2
Andrey Dik
Andrey Dik

Содержание

  1. Введение
  2. Реализация алгоритма
  3. Результаты тестов
  4. Выводы


Введение

Метафоры для популяционных алгоритмов берутся отовсюду: стаи, косяки, рои, эволюция, физика, экономика. Инспирации, основанные на биологических процессах, становятся более популярными. 

Кровеносная система человека — одна из самых продвинутых метафор, которые встречались в нашей серии. Сердце гонит кровь по двум кругам: малый круг ведёт венозную, бедную кислородом кровь через лёгкие, большой круг разносит артериальную по телу. Авторы алгоритма оптимизации на основе кровеносной системы 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 вызовов целевой функции.

CSBO_one_iteration

Рисунок 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 на тестовых функциях и на двух дополнительных, случайно выбранных из списка программы.

Hilly

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

Forest

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

Megacity

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

Ackley

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

Shaffer

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)
1ANSacross neighbourhood search1,000000,882280,401382,283661,000000,952810,280922,233730,946670,857330,223892,027896,54572,72
2AMOmanimal migration optimization M0,916240,836030,467902,220170,984820,920100,363912,268830,917330,817070,251771,986176,47571,94
3CLAcode lock algorithm (joo)0,951390,861990,378792,192170,993490,935000,264972,193460,936000,842670,240602,019276,40571,17
4(P+O)ES(P+O) evolution strategies0,865710,895390,397402,158500,977610,898200,268782,144590,921330,802400,239521,963256,26669,62
5SDSmstochastic diffusion search M0,951950,849440,362492,163880,980610,884570,221122,086300,922670,790130,213801,926606,17768,63
6AAmarchery algorithm M0,846850,733200,425902,005950,967090,778370,277892,023350,861330,777070,287121,925525,95566,17
7SIAsimulated isotropic annealing (joo)0,935430,865040,384832,185300,940690,806090,238351,985130,864000,661600,195361,720965,89165,46
8TETAtime evolution travel algorithm (joo)0,914520,863690,255792,034000,996540,912910,143942,053390,854670,822130,104431,781235,86965,21
9ESGevolution of social groups (joo)0,981110,798570,311672,091350,989540,822700,150321,962560,921330,734400,153151,808885,86365,14
10CTAcomet tail algorithm (joo)0,924350,867860,278382,070590,990390,845710,194482,030580,954670,696800,110081,761555,86365,14
11COAcoyote_optimization_algorithm0,889090,706810,327181,923080,994670,853580,151521,999770,885330,710400,189811,785545,70863,43
12CSBOcirculatory_system_based_optimization0,877930,791050,300251,969230,950260,880270,153641,984170,858130,715250,147771,721155,77663,05
13ECBOenhanced colliding bodies optimization0,940240,723630,323561,987430,994770,802910,130561,928240,876000,701600,174331,751935,66862,98
14DAdialectical algorithm0,931170,754000,262051,947220,989250,813750,086621,889620,926670,681070,113151,720895,55861,76
15BBObiogeography based optimization0,958760,706090,357522,022370,929810,706600,169701,806110,874670,630130,208131,712935,54161,57
16BHAmblack hole algorithm M0,795580,762070,346821,904470,998360,757980,138261,894600,850670,644270,170201,665145,46460,71
17HSharmony search0,914200,690490,299241,903930,976270,733730,141931,851930,917330,627200,153641,698175,45460,60
18RFOroyal flush optimization (joo)0,809890,744810,345461,900160,952510,779260,151851,883620,804000,664270,190711,658985,44360,48
19BOAmbilliards optimization algorithm M0,761770,724210,252751,738730,908900,819600,288532,017030,837330,746130,097631,681095,43760,41
20ASOanarchy society optimization0,730700,737130,311951,779780,997320,877000,176192,050510,720000,687730,189881,597615,42860,31
21EOmextremal optimization_M0,765270,752050,319081,836400,999990,764260,124371,888620,841330,641330,152471,635135,36059,56
22ACSartificial cooperative search0,755450,771620,316531,843601,000000,804880,107051,911930,769330,608000,141571,518905,27458,60
23SSGsaplings sowing and growing0,754360,632060,359351,745770,919070,696940,197551,813560,818670,605330,213471,637475,19757,74
24AOSmatomic orbital search M0,761840,684350,313441,759630,900150,800440,115011,815600,828000,632800,156961,617765,19357,70
25TSEAturtle shell evolution algorithm (joo)0,958090,648520,295711,902320,995220,581040,105421,681680,921330,521600,145671,588605,17357,48
26DEflow_direction_algorithm 0,963980,623460,260891,848330,984820,770180,114591,869590,930670,362130,110001,402805,12156,90
27BIOblood inheritance optimization (joo)0,725800,665220,312281,703300,999950,681250,115401,796600,854670,593330,153641,601645,10256,69
28(PO)ES(PO) evolution strategies0,739720,581900,388961,710580,911990,599750,212621,724360,824000,562400,234321,620725,05656,18
29BObonobo optimizer0,755550,643660,326571,725780,943320,704420,139991,787730,734670,614400,167281,516355,03055,89
30SRAsuccessful restaurateur algorithm (joo)0,890100,633590,291151,814840,966340,552850,089141,608330,893330,528000,139111,560444,98455,38
31FDAmflow_direction_algorithm_M0,875730,588060,271351,735140,999970,643990,096331,740290,842670,484530,118881,446084,92254,70
32CROchemical reaction optimisation0,912810,656810,298661,868280,905130,560200,109391,574720,828000,501330,141491,470824,91454,60
33BCOmbacterial chemotaxis optimization M0,825890,617330,315841,759060,952960,637180,119841,709980,765330,516530,158001,439864,90954,54
34DOAdream optimization algorithm0,785220,781210,360361,926790,615840,421170,122541,159550,866670,725870,211271,803814,89054,33
35ABOafrican buffalo optimization0,922950,625280,298851,847080,929920,574680,093721,598320,733330,513330,143241,389904,83553,72
36BSAbird swarm algorithm0,944320,679410,264011,887740,916490,656190,120541,693220,809330,335470,106521,251324,83253,69
37TSmtabu search M0,878060,610400,289931,778390,981160,521650,085441,588250,826670,495470,135521,457664,82453,60
38BSAbacktracking search algorithm0,871280,531900,286751,689930,924080,516020,091531,531630,960000,472530,137601,570134,79253,24
39BEAbacterial_evolutionary_algorithm0,921700,596150,293401,811250,969060,445000,082331,496390,905330,431730,136761,473824,78153,13
40BWOmbeluga_whale_optimization_M0,784880,568720,295571,649170,913700,617600,129881,661180,813330,499460,150041,462834,77353,04
41WOAmwhale optimization algorithm M0,938930,594770,266951,800650,980360,538730,071121,590210,786670,476000,118921,381594,77253,02
42ACAandean_condor_algorithm0,784440,532600,331081,648120,790710,449600,106851,347160,922660,677330,176131,776124,77153,02
43CSOcompetitive swarm optimizer0,851510,607860,298961,758330,840850,584910,119741,545500,800000,485600,141841,427444,73152,57
44FBAfractal-based algorithm0,694190,642670,289551,626410,998120,549050,087051,634220,761330,512530,136891,410754,67151,90
45CCEmcity_councils_evolution_M_0,780190,567480,255741,603410,934390,649270,117141,700800,817330,437860,108761,363954,66851,87
RWrandom walk0,499700,323330,257911,080940,307540,114700,044000,466240,361330,170130,102440,633902,18124,23


Выводы

Метафора кровеносной системы в CSBO делится на две части. Работающая — вены и большой круг: DE-подобное движение с направленными разностями и фитнес-зависимый DE/rand/1 с редким кроссовером. Декоративная — малый круг, лёгочное кровообращение, на которое авторы возлагают разведку: он измеримо даёт нулёвый вклад, потому что множитель 1/it в формуле (10) гасит шаг за первые сто вызовов функции. Отключение этой фазы и уменьшение популяции с 45 до 10 дают 28 pp, и оба изменения — против рекомендаций авторов.

Для практики: CSBO — крепкий алгоритм с сильным профилем в низких и средних размерностях.

Композитный античит подтверждает, что ни одна из этих цифр не опирается на геометрию тестовых функций. Ключевая рекомендация для параметров торговых систем — не брать авторские popSize 45 и NR = N/3; при бюджете в тысячи прогонов тестера они отдают половину оценок в никуда.

Алгоритм честный и заслуживает высокого рейтинга в нашей сравнительной таблице лучших популяционных методов оптимизации. Все необходимые инструменты для трейдера прикреплены к статье (в архиве), пожалуйста, экспериментируйте.

tab

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

chart

Рисунок 3. Гистограмма результатов тестирования алгоритмов (по шкале от 0 до 100: чем больше, тем лучше, где 100 — максимально возможный теоретический результат). В архиве — скрипт для расчёта рейтинговой таблицы


Плюсы и минусы алгоритма CSBO

Плюсы:

  • Два простых оператора на разностных векторах, полностью инвариантных к сдвигу и повороту — античит это подтверждает.
  • Сильный профиль в низких и средних размерностях.
  • Минимум параметров, которые реально что-то решают: popSize и NR; crossProb и cauchyScale можно не трогать.
  • Малая популяция и нулевые накладные расходы на агента — дёшево по памяти и по коду.

Минусы:

  • Авторская конфигурация неработоспособна на нашем стенде: 35% и ноль в высокой размерности; без подбора параметров алгоритм выглядит вдвое хуже, чем он есть.
  • Малый круг — мёртвая фаза, треть популяции в каноне сжигает бюджет впустую.
  • В высоких размерностях результат честный, но слабее, чем в алгоритмах рядом по рейтинговой таблице.
  • Скалярный p_i на весь вектор — полновекторный ход, который в высокой размерности принимается редко; резерва для модификации в этом месте не нашлось.


    К статье прикреплён архив с актуальными версиями кодов алгоритмов. Автор статьи не несёт ответственности за абсолютную точность в описании канонических алгоритмов, во многие из них внесены изменения для улучшения поисковых возможностей. Выводы и суждения, представленные в статьях, основываются на результатах проведённых экспериментов.


    Программы, используемые в статье

    #ИмяТипОписание
    1#C_AO.mqh
    Включаемый файл
    Родительский класс популяционных алгоритмов оптимизации
    2#C_AO_enum.mqh
    Включаемый файл
    Перечисление популяционных алгоритмов оптимизации
    3TestFunctions.mqh
    Включаемый файл
    Библиотека тестовых функций
    4
    TestStandFunctions.mqh
    Включаемый файл
    Библиотека функций тестового стенда
    5TestStand3D.mqhВключаемый файл3D-панель визуализации для тестового стенда 
    6Utilities.mqh
    Включаемый файл
    Библиотека вспомогательных функций
    7CalculationTestResults.mqh
    Включаемый файл
    Скрипт для расчёта результатов в сравнительную таблицу
    8Test_AO_All.mq5
    СкриптЕдиный испытательный стенд для всех популяционных алгоритмов оптимизации
    9Test_AO_AntiCheatСкриптТест на читерство алгоритмов оптимизации
    10Simple use of population optimization algorithms.mq5
    Скрипт
    Простой пример использования популяционных алгоритмов оптимизации без визуализации
    11Test_AO_CSBO.mq5
    СкриптИспытательный стенд для CSBO
    Прикрепленные файлы |
    CSBO.zip (456.61 KB)
    Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
    Maxim Kuznetsov
    Maxim Kuznetsov | 8 сент. 2026 в 15:04

    Андрей, а есть-ли словосочетание на основе которого нельзя сделать оптимизацию ? :-)

    например оптимизация на основе рубки леса запрещена по экологическим соображениям. А оптимизация путём кота по златой цепи невозможна ибо Пушкин наше всё

    Alexey Volchanskiy
    Alexey Volchanskiy | 8 сент. 2026 в 19:07
    Maxim Kuznetsov #:

    Андрей, а есть-ли словосочетание на основе которого нельзя сделать оптимизацию ? :-)

    например оптимизация на основе рубки леса запрещена по экологическим соображениям. А оптимизация путём кота по златой цепи невозможна ибо Пушкин наше всё

    Мельчает Кривбасс. За $200 даже на основе разрешения на сбор пенсами валежника готов написать! А вот про реальные DSP скальперы 200 маловато будет ))
    1
    Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
    Написание пользовательских индикаторов в торговой системе MetaTrader 4
    Репликация и моделирование рынка: Большой финал Репликация и моделирование рынка: Большой финал
    Я знаю, что многие, возможно, думали, что я опубликую ещё несколько статей, чтобы объяснить другие аспекты этой системы. Недостающие элементы легко реализовать. Тем не менее, её разработка позволит вам проверить, насколько вы действительно готовы.
    Особенности написания экспертов Особенности написания экспертов
    Написание и тестирование экспертов в торговой системе MetaTrader 4.
    Нейросети в трейдинге: Адаптация прогноза при смене рыночного режима (OpenCL примитивы) Нейросети в трейдинге: Адаптация прогноза при смене рыночного режима (OpenCL примитивы)
    Статья продолжает практическую реализацию фреймворка OMPB для адаптации прогнозных моделей к смене рыночного режима. Рассматриваем вычислительную декомпозицию фреймворка и реализуем недостающие OpenCL-примитивы: ограниченный Disagreement, распространение градиента Mismatch и поэлементный расчёт KL-дивергенции для байесовского слоя. Полученные компоненты образуют параллельную GPU-основу без переноса больших промежуточных тензоров на CPU и подготавливают сборку полноценного OMPB-слоя в библиотеке нейронных сетей MQL5.