preview
Алгоритм оптимизации кита-белухи — Beluga Whale Optimization (BWO)

Алгоритм оптимизации кита-белухи — Beluga Whale Optimization (BWO)

MetaTrader 5Трейдинг |
53 0
Andrey Dik
Andrey Dik

Содержание

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


Введение

Продолжаем наш цикл, посвящённый популяционным алгоритмам оптимизации. Напомню, ради чего всё это затевалось. Мы последовательно перебираем алгоритмы природного и иного происхождения, реализуем каждый в едином фреймворке, прогоняем через один и тот же набор тестовых функций и заносим результат в общую рейтинговую таблицу. Таблица — не самоцель и не коллекция ради коллекции. Её смысл сугубо прикладной: трейдеру, который оптимизирует параметры торговой стратегии, нужен надёжный инструмент поиска, а не первый попавшийся. Чем больше алгоритмов прошло через одинаковую, честную и воспроизводимую проверку, тем увереннее можно сказать, какой из них стоит брать в работу под конкретную задачу. Поэтому мы и ищем лучших — методично, один за другим.

Сегодняшний кандидат — Beluga Whale Optimization (BWO), алгоритм оптимизации кита-белухи. Он был предложен в 2022 году в работе Zhong, Li и Meng (Knowledge-Based Systems) и, как часто бывает в этой области, опирается на поведенческую метафору. Авторы заметили в повадках белух три различных модели поведения и положили каждую в основу отдельной фазы алгоритма. Синхронное парное плавание, на котором строится разведка пространства поиска. Охотничье погружение с элементами полёта Леви — фаза эксплуатации. Падение кита — естественный механизм, когда погибшая особь опускается на дно, в алгоритме превращённый в способ обновления популяции. Идея, на мой взгляд, изящная: три качественно разных оператора, плавно сменяющих друг друга по ходу прогона, и при этом почти полное отсутствие свободных параметров — авторы подают это как одно из достоинств метода. Звучит многообещающе, и тем интереснее проверить, что из этого выйдет на нашем стенде.


Реализация алгоритма

В основе BWO лежит популяция из N китов-белух, и каждый кит — это одно candidate-решение, то есть точка в d-мерном пространстве поиска. На старте популяция расставляется случайно в пределах заданных границ, после чего начинается итеративный процесс. На каждой итерации кит выбирает одну из моделей поведения и сдвигается по соответствующему правилу; если новая позиция оказалась удачнее прежней, она принимается. Что именно будет делать кит, решает так называемый балансовый фактор.

Балансовый фактор. Это число, которое для каждого кита на каждой итерации вычисляется заново:

Bf = B0 · (1 − T / (2·Tmax))

Здесь B0 — случайное число из интервала (0, 1), T — номер текущей итерации, Tmax — их общее количество. Если Bf > 0.5, кит уходит в фазу разведки, иначе — в фазу эксплуатации. Простой пример того, как это работает во времени: в самом начале прогона множитель (1 − T/2Tmax) равен почти единице, и Bf может оказаться где угодно в (0, 1) — примерно половина китов разведывает. К концу прогона тот же множитель падает до 0.5, и Bf уже не может превысить 0.5 — все киты переходят к эксплуатации. То есть фактор работает как плавный регулятор: он не переключает фазы рывком, а постепенно смещает всю популяцию от исследования пространства к доводке найденного.

Фаза разведки — синхронное парное плавание. Белухи в природе плавают парами, зеркально повторяя движения друг друга, и авторы заложили это прямо в формулу. Кит i выбирает случайного напарника r, а его координаты обновляются попарно: в каждой паре одна координата считается через синус, вторая — через косинус, и обе используют один и тот же угол:

x[нечётная] = x_i + (x_r − x_i) · (1 + r1) · sin(2π·r2)
x[чётная] = x_i + (x_r − x_i) · (1 + r1) · cos(2π·r2)

где r1 и r2 — случайные числа из [0, 1]. Связка sin/cos и есть синхронное плавание: когда r2 пробегает свои значения, пара (sin, cos) обходит единичную окружность, то есть две координаты поворачиваются согласованно, как два пловца вокруг общей оси. Простой числовой пример. Пусть кит i стоит в точке (2.0, 5.0), напарник r — в (8.0, 1.0), опорным берётся значение 8.0, и выпало r1 = 0.3, r2 = 0.1 (тогда sin ≈ 0.588, cos ≈ 0.809, а множитель 1 + r1 = 1.3). Получаем:

новая x0 = 2.0 + (8.0 − 2.0) · 1.3 · 0.588 ≈ 6.59
новая x1 = 5.0 + (8.0 − 5.0) · 1.3 · 0.809 ≈ 8.16

Кит сместился в сторону напарника, но не по прямой к нему, а с «закруткой», заданной углом — отсюда и широкий охват пространства, нужный на стадии разведки.

Фаза эксплуатации — охота с полётом Леви. Здесь белухи действуют как стая на охоте, и движение к добыче моделируется полётом Леви. Позиция обновляется так:

X_i = r3·X_best − r4·X_i + C1 · LF · (X_r − X_i)

где r3, r4 — случайные числа из [0, 1], X_best — лучшее найденное решение, LF — вектор полёта Леви, а C1 = 2·r4·(1 − T/Tmax) задаёт силу рывка. В формуле сложены три тяги: к лучшему решению, масштабированный отскок от собственной позиции и леви-модулированная разность в сторону случайного кита. Полёт Леви — это случайное блуждание с тяжёлым хвостом: подавляющее большинство шагов мелкие, но изредка случается длинный прыжок. Простой образ: за сотню шагов гауссовский блуждатель сделает сотню примерно одинаковых мелких шажков, а леви-блуждатель — девяносто пять крошечных и несколько больших скачков. Эти редкие скачки и позволяют киту вырываться из локальной области. Множитель C1 убывает с ростом T, поэтому в начале прогона рывки крупные, а ближе к концу остаётся лишь тонкая доводка.

Падение кита. Третья модель поведения — редкая. С вероятностью

Wf = 0.1 − 0.05 · T / Tmax

кит погибает, и его место занимает новая особь. Координаты нового кита считаются таким образом:

X_i = r5·X_i − r6·X_r + r7·X_step, где X_step = (ub − lb) · exp(−C2·T/Tmax)

а C2 = 2·Wf·N. Смысл прост: погибший кит замещается смесью собственной позиции и позиции случайного сородича плюс шаг переселения. Величина этого шага задаётся экспонентой, которая быстро затухает: в начале прогона переселение размашистое и вбрасывает в популяцию разнообразие, к концу — почти нулевое. Сама вероятность Wf тоже невелика и падает с 0.1 до 0.05, так что падение кита остаётся фоновым механизмом обновления, а не основным движителем.

Одна итерация устроена так: для каждого кита вычисляется балансовый фактор, выбирается фаза (разведка/эксплуатация; иногда — падение кита), затем оценивается новая позиция и принимается, если она лучше прежней. Затем — следующая итерация, и так до исчерпания бюджета вычислений. Привлекательность BWO как раз в этой компактности: три качественно разных оператора, плавно сменяющих друг друга, и при этом почти нет параметров, которые пришлось бы вручную настраивать.

bwo_concept

Рисунок 1. Фазы работы алгоритма BWO

Beluga Whale Optimization — три поведенческие фазы. Баланс‑фактор Bf смещает доминирующее поведение в ходе прогона; событие whale fall остаётся редким (Wf: 0.1 → 0.05).

На иллюстрации показано: три поведенческие фазы BWO — разведка (киты плавают зеркальными парами, пунктирная орбита со стрелкой передаёт синхронное движение и охват пространства), эксплуатация (кит идёт к лучшему решению — золотой звезде — траекторией полёта Леви: мелкие шаги и один редкий длинный прыжок), и падение кита (особь тонет и заменяется новой, зелёный плюс — обновление популяции). Внизу — градиентная полоса балансового фактора Bf: переход от разведки к эксплуатации по ходу прогона, с пометкой о редком и затухающем падении кита.

Напишем псевдокод алгоритма.

АЛГОРИТМ BWO (Beluga Whale Optimization)

ВХОД:  Npop — размер популяции
       Max_it — число итераций
       lb, ub — границы поиска
       nD — размерность задачи
       fobj — целевая функция (минимизация)

ВЫХОД: xposbest, fvalbest — лучшее решение и его значение

// ---- Инициализация ----
ДЛЯ каждого агента i = 1..Npop:
    pos[i] = rand(nD) * (ub - lb) + lb
    fit[i] = fobj(pos[i])
КОНЕЦ
(fvalbest, index) = min(fit)
xposbest = pos[index]

// ---- Основной цикл ----
ДЛЯ T = 1..Max_it:

    WF = 0.1 - 0.05 * (T / Max_it)                  // вероятность падения кита

    ДЛЯ каждого агента i = 1..Npop:
        kk[i] = (1 - 0.5 * T / Max_it) * rand()     // баланс разведка/эксплуатация
    КОНЕЦ

    // === Фаза 1: плавание / охота ===
    ДЛЯ каждого агента i = 1..Npop:
        выбрать RJ — случайный индекс агента, RJ ≠ i

        ЕСЛИ kk[i] > 0.5:                           // --- РАЗВЕДКА (парное плавание) ---
            r1 = rand();  r2 = rand()

            ЕСЛИ nD ≤ Npop/5:                       // обновляем 2 случайных измерения
                (p1, p2) = два случайных номера измерений
                newpos[i][p1] = pos[i][p1] + (pos[RJ][p1] - pos[i][p2])*(r1+1)*sin(r2*360°)
                newpos[i][p2] = pos[i][p2] + (pos[RJ][p1] - pos[i][p2])*(r1+1)*cos(r2*360°)
            ИНАЧЕ:                                  // обновляем измерения парами
                params = случайная перестановка измерений 1..nD
                ДЛЯ j = 1..floor(nD/2):
                    newpos[i][2j-1] = pos[i][params[2j-1]]
                                    + (pos[RJ][params[1]] - pos[i][params[2j-1]])*(r1+1)*sin(r2*360°)
                    newpos[i][2j]   = pos[i][params[2j]]
                                    + (pos[RJ][params[1]] - pos[i][params[2j]])*(r1+1)*cos(r2*360°)
                КОНЕЦ
            КОНЕЦ

        ИНАЧЕ:                                      // --- ЭКСПЛУАТАЦИЯ (охота, полёт Леви) ---
            r3 = rand();  r4 = rand()
            C1 = 2 * r4 * (1 - T / Max_it)           // фактор интенсивности охоты
            LevyFlight = вектор полёта Леви (nD), множитель KD = 0.05
            newpos[i] = r3*xposbest - r4*pos[i] + C1 * LevyFlight ⊙ (pos[RJ] - pos[i])
        КОНЕЦ

        newpos[i] = ограничить границами [lb, ub]
        newfit = fobj(newpos[i])

        ЕСЛИ newfit < fit[i]:                        // жадный отбор
            pos[i] = newpos[i];  fit[i] = newfit
        КОНЕЦ
    КОНЕЦ

    // === Фаза 2: падение кита (whale fall) ===
    ДЛЯ каждого агента i = 1..Npop:
        ЕСЛИ kk[i] ≤ WF:
            выбрать RJ — случайный индекс агента
            r5, r6, r7 = rand()
            C2 = 2 * Npop * WF
            stepsize2 = r7 * (ub - lb) * exp(-C2 * T / Max_it)
            newpos[i] = (r5*pos[i] - r6*pos[RJ]) + stepsize2

            newpos[i] = ограничить границами [lb, ub]
            newfit = fobj(newpos[i])
            ЕСЛИ newfit < fit[i]:
                pos[i] = newpos[i];  fit[i] = newfit
            КОНЕЦ
        КОНЕЦ
    КОНЕЦ

    // === Обновление глобального лучшего ===
    (fval, index) = min(fit)
    ЕСЛИ fval < fvalbest:
        fvalbest = fval;  xposbest = pos[index]
    КОНЕЦ

КОНЕЦ
ВЕРНУТЬ xposbest, fvalbest

Приступим к реализации. Как обычно, алгоритм оформляется отдельным классом, наследующим базовый C_AO. Базовый класс даёт нам всё стандартное хозяйство — массив агентов, хранение лучшего решения, параметры; а от нас требуется реализовать четыре метода: SetParams, Init, Moving и Revision.

//+------------------------------------------------------------------+
//|                  Beluga Whale Optimization                       |
//+------------------------------------------------------------------+
class C_AO_BWO : public C_AO
 {
public:
                    ~C_AO_BWO() {}
                     C_AO_BWO()
   {
    ao_name = "BWO";
    ao_desc = "Beluga Whale Optimization";
    ao_link = "https://www.mql5.com/ru/users/joo";

    popSize = 30;
    KD      = 0.05;

    ArrayResize(params, 2);
    params [0].name = "popSize";
    params [0].val = popSize;
    params [1].name = "KD";
    params [1].val = KD;
   }

  void               SetParams();
  bool               Init(const double &rangeMinP [], const double &rangeMaxP [],
              const double &rangeStepP [], const int epochsP = 0);
  void               Moving();
  void               Revision();

  double             KD;                 // интенсивность полёта Леви (оригинал: 0.05)

private:
  double snap_c      [];          // [popSize*coords] — снимок популяции
  double snap_f      [];          // [popSize] — снимок фитнесов
  int    shuf        [];          // [coords] — буфер перестановки координат

  int                epochs, epochsDen, epochNow;   // счётчики итераций
  double             levySigma;                     // константа масштаба полёта Леви

  double             Gauss();            // стандартное нормальное число
  double             LevyStep();         // один отсчёт полёта Леви
 };

Стоит обратить внимание на параметры. Их всего два — и это особенность BWO: алгоритм почти беспараметрический. popSize — размер популяции, общий для всех методов серии. KD — интенсивность полёта Леви; в оригинальной статье это зашитая константа 0.05, мы вынесли её в параметры лишь для удобства экспериментов. Всё остальное — балансовый фактор, вероятность падения кита, силовые коэффициенты — алгоритм вычисляет сам по ходу прогона.

Внутренние буферы: snap_c и snap_f хранят снимок популяции на начало итерации — он нужен для жадного отбора (greedy acceptance): если новая позиция оказалась хуже, мы откатываемся к снимку. shuf — рабочий массив для перестановки индексов координат в фазе разведки. Три счётчика epochs, epochsDen, epochNow нужны, чтобы внутри алгоритма знать отношение T/Tmax. И levySigma — предвычисленная константа для полёта Леви.

Инициализация. StandardInit поднимает базовое хозяйство, после чего мы расставляем стартовую популяцию случайно — пока в массив cP, на первом же Moving она перельётся в рабочие координаты. Дальше резервируем буферы и настраиваем счётчик итераций.

//+------------------------------------------------------------------+
//|                              Init                                |
//+------------------------------------------------------------------+
bool C_AO_BWO::Init(const double &rangeMinP [], const double &rangeMaxP [],
                    const double &rangeStepP [], const int epochsP = 0)
 {
  if(!StandardInit(rangeMinP, rangeMaxP, rangeStepP))
    return false;

  for(int i = 0; i < popSize; i++)
    for(int c = 0; c < coords; c++)
      a [i].cP [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);

  ArrayResize(snap_c, popSize * coords);
  ArrayResize(snap_f, popSize);
  ArrayResize(shuf,   coords);

  epochs    = epochsP;
  epochsDen = (epochs > 1) ? epochs - 1 : 1;
  epochNow  = 0;

//--- sigma для полёта Леви (Mantegna), beta = 1.5
  double beta = 1.5;
  double g1   = 1.3293403881791;   // Г(2.5)
  double g2   = 0.9064024770554;   // Г(1.25)
  levySigma = MathPow((g1 * MathSin(M_PI * beta / 2.0)) /
                      (g2 * beta * MathPow(2.0, (beta - 1.0) / 2.0)), 1.0 / beta);
  return true;
 }

Последний блок требует пояснения. Полёт Леви по схеме Mantegna использует константу sigma, которая зависит от гамма-функции, sigma при фиксированном beta = 1.5 — это просто число, поэтому значения Г(2.5) и Г(1.25) посчитаны заранее, а сама sigma вычисляется один раз в Init (получается около 0.6966).

Полёт Леви разнесён на два вспомогательных метода:

//+------------------------------------------------------------------+
//|              Gauss — стандартное нормальное (Box–Muller)         |
//+------------------------------------------------------------------+
double C_AO_BWOm::Gauss()
 {
  double u1 = u.RNDfromCI(0.0, 1.0);
  double u2 = u.RNDfromCI(0.0, 1.0);
  if(u1 < 1e-15)
    u1 = 1e-15;
  return MathSqrt(-2.0 * MathLog(u1)) * MathCos(2.0 * M_PI * u2);
 }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//|        LevyStep — один отсчёт полёта Леви (Mantegna)             |
//+------------------------------------------------------------------+
double C_AO_BWOm::LevyStep()
 {
  double uu = Gauss() * levySigma;
  double vv = Gauss();
  double av = MathAbs(vv);
  if(av < 1e-300)
    av = 1e-300;
  return KD * (uu / MathPow(av, 1.0 / 1.5));
 }

Moving  ядро алгоритма. Прежде чем смотреть код, одна важная оговорка о переносе. В оригинале одна итерация BWO — это два хода и две оценки целевой функции: сначала все киты делают ход плавания или охоты, затем у части особей дополнительно срабатывает падение кита. Наш фреймворк отводит ровно одну оценку функции на агента за эпоху. Поэтому три модели поведения сведены во взаимоисключающие ветви: за одну эпоху кит делает либо разведку, либо эксплуатацию, либо падение кита. Диапазон балансового фактора при этом разбивается без зазоров.

Начало метода — обработка самого первого прогона и снятие снимка:

//+------------------------------------------------------------------+
//|                            Moving                                |                             |
//+------------------------------------------------------------------+
void C_AO_BWO::Moving()
 {
  if(!revision)                                 // первый прогон: cP -> 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;
   }

  epochNow++;
  double tRatio = (double)epochNow / (double)epochsDen;   // отношение T/Tmax
  if(tRatio > 1.0)
    tRatio = 1.0;

  for(int i = 0; i < popSize; i++)               // снимок популяции
   {
    for(int c = 0; c < coords; c++)
      snap_c [i * coords + c] = a [i].c [c];
    snap_f [i] = a [i].f;
   }

  double WF = 0.1 - 0.05 * tRatio;               // вероятность падения кита

На первой эпохе популяция ещё не оценена, поэтому мы просто переливаем стартовые координаты в рабочий массив и выходим — внешний цикл оценит их. На всех последующих эпохах увеличиваем счётчик, считаем отношение T/Tmax, снимаем снимок и вычисляем текущую вероятность падения кита.

Дальше — основной цикл по китам. Для каждого считается балансовый фактор и выбирается случайный сосед:

  for(int i = 0; i < popSize; i++)
   {
    double Bf = (1.0 - 0.5 * tRatio) * u.RNDprobab();    // балансовый фактор

    int RJ = i;                                          // случайный сосед RJ != i
    if(popSize > 1)
      while(RJ == i)
        RJ = u.RNDminusOne(popSize);

Первая ветвь — падение кита, срабатывает при Bf ≤ WF:

    if(Bf <= WF)
     {
      double r5 = u.RNDprobab(), r6 = u.RNDprobab(), r7 = u.RNDprobab();
      double C2    = 2.0 * (double)popSize * WF;
      double decay = MathExp(-C2 * tRatio);

      for(int c = 0; c < coords; c++)
       {
        double step = r7 * (rangeMax [c] - rangeMin [c]) * decay;
        double v    = r5 * snap_c [i * coords + c] -
                      r6 * snap_c [RJ * coords + c] + step;
        a [i].c [c] = u.SeInDiSp(v, rangeMin [c], rangeMax [c], rangeStep [c]);
       }
     }

Вторая ветвь — разведка, парное плавание, при Bf > 0.5. Координаты обрабатываются попарно: нечётная через синус, чётная через косинус. Опорой служит одна координата соседа, индексы координат предварительно перемешиваются:

else
  if(Bf > 0.5)
   {
    double r1 = u.RNDprobab(), r2 = u.RNDprobab();
    double sinA = MathSin(2.0 * M_PI * r2);
    double cosA = MathCos(2.0 * M_PI * r2);

    for(int c = 0; c < coords; c++)            // незатронутые координаты — прежние
      a [i].c [c] = snap_c [i * coords + c];

    for(int c = 0; c < coords; c++)
      shuf [c] = c;          // перестановка Фишера-Йетса
    for(int c = 0; c < coords - 1; c++)
     {
      int r = c + u.RNDminusOne(coords - c);
      int t = shuf [c];
      shuf [c] = shuf [r];
      shuf [r] = t;
     }

    int    refDim = shuf [0];
    double ref    = snap_c [RJ * coords + refDim];         // ОДИН скаляр-опора
    int    pairs  = coords / 2;

    for(int j = 0; j < pairs; j++)
     {
      int dA = 2 * j, dB = 2 * j + 1;
      int pA = shuf [2 * j], pB = shuf [2 * j + 1];
      double xA = snap_c [i * coords + pA];
      double xB = snap_c [i * coords + pB];
      double vA = xA + (ref - xA) * (r1 + 1.0) * sinA;
      double vB = xB + (ref - xB) * (r1 + 1.0) * cosA;
      a [i].c [dA] = u.SeInDiSp(vA, rangeMin [dA], rangeMax [dA], rangeStep [dA]);
      a [i].c [dB] = u.SeInDiSp(vB, rangeMin [dB], rangeMax [dB], rangeStep [dB]);
     }
   }

Третья ветвь — эксплуатация, охота с полётом Леви, для всех остальных значений Bf:

else
     {
      double r3 = u.RNDprobab(), r4 = u.RNDprobab();
      double C1 = 2.0 * r4 * (1.0 - tRatio);

      for(int c = 0; c < coords; c++)
       {
        double xi = snap_c [i * coords + c];
        double xr = snap_c [RJ * coords + c];
        double Lv = LevyStep();
        double v  = r3 * cB [c] - r4 * xi + C1 * Lv * (xr - xi);
        a [i].c [c] = u.SeInDiSp(v, rangeMin [c], rangeMax [c], rangeStep [c]);
       }
     }
   }
 }

Все три ветви — прямой перенос формул, разобранных в предыдущем разделе. Граничные условия мы не выписываем отдельно: функция SeInDiSp, через которую проходит каждая новая координата, заодно и зажимает её в допустимый диапазон.

Revision делает две вещи: обновляет личные и глобальный рекорды и выполняет жадный отбор — если новая позиция кита оказалась хуже снимка, координаты и фитнес откатываются назад. Именно поэтому популяция в BWO не деградирует: ход может быть неудачным, но он не закрепляется.

//+------------------------------------------------------------------+
//|                           Revision                               |
//+------------------------------------------------------------------+
void C_AO_BWO::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 = true;
    return;
   }

  for(int i = 0; i < popSize; i++)                  // greedy acceptance: откат если хуже
    if(a [i].f < snap_f [i])
     {
      for(int c = 0; c < coords; c++)
        a [i].c [c] = snap_c [i * coords + c];
      a [i].f = snap_f [i];
     }
 }


Результаты тестов

Прогоняем стандартный стенд: размер популяции 30, бюджет 10000 вычислений функции, 10 повторов на каждую конфигурацию.

Итог — 47.51%. Казалось бы, можно записывать результат и переходить к следующему алгоритму. Но если вглядеться не в общий балл, а в то, как он распределён по размерности, обнаруживается странность. Посмотрите на строку Hilly: 0.6354 на 10 координатах, 0.6083 на 50 и вдруг 0.6898 на 1000. С ростом размерности счёт не падает, а растёт — и на тысяче координат оказывается выше, чем на десяти. Для популяционного алгоритма на фиксированном бюджете вычислений это противоестественно. 

Монотонный спад счёта по размерности — это норма, физика процесса. Подъём — это сигнал, что часть результата получена не поиском. И сигнал тем громче, чем сильнее подъём: у BWO счёт на 1000 координат не просто не упал, он превысил результат на 10 координатах.

BWO|Beluga Whale Optimization|30.0|0.05|
=============================
5 Hilly's; Func runs: 10000; result: 0.6353865355283055
25 Hilly's; Func runs: 10000; result: 0.6082998984102445
500 Hilly's; Func runs: 10000; result: 0.6898049949555738
=============================
5 Forest's; Func runs: 10000; result: 0.42042059098839246
25 Forest's; Func runs: 10000; result: 0.34230526219017604
500 Forest's; Func runs: 10000; result: 0.34595127333449394
=============================
5 Megacity's; Func runs: 10000; result: 0.488
25 Megacity's; Func runs: 10000; result: 0.34586666666666666
500 Megacity's; Func runs: 10000; result: 0.39973333333333316
=============================
All score: 4.27577 (47.51%)

В композитном тесте десять координат раздаются пяти разным функциям: пара Hilly, пара Forest, пара Megacity, пара Peaks, пара Skin, а фитнес агента — среднее по всем пяти. Замысел простой: любой геометрический трюк, который алгоритм мог бы провернуть, опирается на повторяемость — на то, что все пары устроены одинаково. В композите пять функций имеют пять разных оптимумов с разной геометрией, поэтому такой трюк способен дать выигрыш максимум на одной паре из пяти и на общем счёте теряется. Композитный тест — это, по сути, проверка: держится ли результат, когда геометрическую опору выбивают из-под ног.

BWO на этом тесте набирает 38.57%. Теперь сравним. На обычном стенде те же первые функции на 10 координатах дали Hilly 0.6354, Forest 0.4204, Megacity 0.4880 — в среднем около 0.515. В композите общий результат — 0.386. То есть на первых функциях, которые поодиночке выглядели вполне достойно, в смешанной раскладке динамика заметно проседает. И это уже знак. Композитный тест не делает задачу принципиально другой. Он лишь лишает алгоритм возможности эксплуатировать одинаковость пар.

BWO|Beluga Whale Optimization |30.0|0.05|
=============================
Composite anti-cheat test: Hilly + Forest + Megacity + Peaks + Skin
Coordinates: 10; Epochs: 333; Repeats: 10
=============================
Run 1/10: 0.37161205365313077
Run 2/10: 0.40112339728582336
Run 3/10: 0.3706551427065614
Run 4/10: 0.4216333585559708
Run 5/10: 0.4101199993944876
Run 6/10: 0.3934049133990568
Run 7/10: 0.3593573524858875
Run 8/10: 0.3965450586725149
Run 9/10: 0.3584922015596293
Run 10/10: 0.3737824222225442
=============================
Average result: 0.3856725900 (38.57%)

Давайте разберём подробнее, что же всё-таки происходит с алгоритмом во время работы.

exploration_collapse

Рисунок 2. Коллапс координат в разведке

На этапе exploration в BWO один опорный скаляр приводит к коллапсу всего агента. Множитель M = (1 + r₁)·sin(2π·r₂) остаётся постоянным для всего агента. Когда M ≈ 1, каждая обновлённая координата становится равной опорной X[RJ][P1] — вектор схлопывается.

Вскрытие, часть первая: фаза разведки. 

Симптомы у нас есть, теперь нужна причина. Открываем Moving и смотрим на операторы по очереди. Начнём с разведки — той ветви, где Bf > 0.5. Вот её сердцевина, уже знакомая по разделу о реализации:

double r1 = u.RNDprobab(), r2 = u.RNDprobab();   // разыграны ОДИН раз на агента
double sinA = MathSin(2.0 * M_PI * r2);
double cosA = MathCos(2.0 * M_PI * r2);

int    refDim = shuf [0];
double ref    = snap_c [RJ * coords + refDim];   // ОДИН скаляр на все пары

for(int j = 0; j < pairs; j++)
 {
  double xA = snap_c [i * coords + pA];
  double vA = xA + (ref - xA) * (r1 + 1.0) * sinA;
  ...
 }

В этом фрагменте два факта, которые по отдельности безобидны, а вместе дают проблему. Первый: опорное значение ref — это один скаляр, одна координата случайного соседа, и она одна и та же для всех пар цикла. Второй: r1 и r2 разыгрываются до цикла, то есть один раз на агента, поэтому sinA, cosA и множитель (1 + r1) — постоянные величины для всех координат этого кита.

Распишем, что тогда получает каждая координата. Формула обновления v = x + (ref − x)·(1+r1)·sin раскрывается в смесь:

newpos[d] = M · ref + (1 − M) · x[d],   где M = (1 + r1) · sin(2π·r2)

и M — константа по агенту (для нечётных координат; для чётных то же самое с косинусом). Это ключевой момент. Все нечётные координаты кита проходят через одно и то же аффинное преобразование, с одним и тем же M , в сторону одного и того же ref. И если M оказался близок к единице, то newpos[d] = ref — для всех нечётных координат сразу. Вектор кита, каким бы длинным он ни был, схлопывается в два значения: одно для нечётных координат, другое для чётных. Наглядно это показано на иллюстрации (рисунок 1) — слева разнообразный набор координат, справа от него после одного хода разведки не остаётся почти ничего.

Почему это чит именно на нашем стенде? Стандартный тест — это N копий одной и той же функции. Глобальный оптимум такой задачи осесимметричен по построению: все пары координат должны прийти в одну и ту же точку. А оператор разведки BWO как раз и тянет кита к осесимметричному виду — вектору из двух повторяющихся значений. То есть склонность алгоритма схлопывать агента бьёт прямо в структуру правильного ответа, и не потому, что алгоритм хорошо ищет, а потому, что форма его хода случайно совпала с формой оптимума.

Почему эффект усиливается с размерностью — то, что мы видели в строке Hilly? На 1000 координат каждый кит несёт тысячу координатных значений. По статистике порядка хотя бы одно из них почти наверняка окажется очень близким к оптимальной координате функции. Оператор разведки берёт удачное значение у соседа и за один ход размножает его на половину пространства, а жадный отбор закрепляет результат — ведь на стенде из одинаковых функций залить хорошее значение сразу в сотни пар означает резко поднять средний фитнес. Чем больше размерность, тем богаче запас координат у каждого кита и тем лучше посевной материал для широковещания. Отсюда и противоестественный рост счёта.

Напрашивается первая правка: убрать оба источника широковещания. Пусть каждая пара координат тянется к своей одноимённой паре соседа, а r1 и r2 разыгрываются заново на каждую пару:

for(int j = 0; j < pairs; j++)
 {
  int dA = 2 * j, dB = 2 * j + 1;

  double r1 = u.RNDprobab(), r2 = u.RNDprobab();   // на КАЖДУЮ пару
  double sinA = MathSin(2.0 * M_PI * r2);
  double cosA = MathCos(2.0 * M_PI * r2);

  double xA = snap_c [i  * coords + dA];
  double refA = snap_c [RJ * coords + dA];         // своя одноимённая координата соседа
  double vA = xA + (refA - xA) * (r1 + 1.0) * sinA;
  ...
 }

Геометрия парного плавания при этом сохраняется — внутри пары синус и косинус по-прежнему делят один угол и стоят на 90° друг от друга, — но общий скаляр-опора исчез, и единого по агенту множителя больше нет. Схлопывать вектор целиком теперь нечем.

Прогоняем стенд заново. И вот тут начинается самое интересное: общий балл изменился, но профиль по размерности всё ещё нездоровый — на больших размерностях алгоритм по-прежнему набирает подозрительно много. Фаза разведки была не единственным каналом. Где спрятался второй — в следующем разделе.

Вскрытие, часть вторая: фаза эксплуатации.

Эксплуатационная фаза BWO: член r₃·cB сканирует главную диагональ, далее на картинке.

exploitation_diagonal

Рисунок 3. Диагональное сканирование в эксплуатации

Красный луч — это r₃·cB: когда r₃ пробегает диапазон [0,1], кандидат скользит по отрезку от начала координат к cB. На наборе идентичных функций вектор cB осесимметричен, поэтому этот луч совпадает с главной диагональю — именно там расположены пики целевой функции. В высоких размерностях шум Леви усредняется.

После правки разведки прогон показал, что аномалия никуда не делась — на больших размерностях счёт по-прежнему завышен, и алгоритм всё так же «считывает диагональ». Значит, канал не один. Открываем вторую ветвь Moving — эксплуатацию, ту, где идёт охота с полётом Леви:

double r3 = u.RNDprobab(), r4 = u.RNDprobab();   // r3 — ОДИН скаляр на агента
double C1 = 2.0 * r4 * (1.0 - tRatio);

for(int c = 0; c < coords; c++)
 {
  double xi = snap_c [i * coords + c];
  double xr = snap_c [RJ * coords + c];
  double Lv = LevyStep();
  double v  = r3 * cB [c] - r4 * xi + C1 * Lv * (xr - xi);
  ...
 }

Болевая точка здесь — слагаемое r3·cB. Это скаляр, умноженный на вектор глобального лучшего решения. Вспомним, что на стенде из одинаковых функций cB осесимметричен — все его пары координат сидят в одной точке. И когда r3 пробегает диапазон [0, 1], точка r3·cB скользит по лучу из начала координат в cB. А луч из начала координат в осесимметричную точку — это и есть главная диагональ пространства поиска.

Дальше — хуже. Как только популяция стянулась на эту диагональ (а она стягивается, потому что жадный отбор закрепляет удачные ходы вдоль луча), позиция самого кита xi тоже оказывается на диагонали. Тогда весь скелет хода r3·cB − r4·xi — это линейная комбинация двух коллинеарных векторов, и она не выходит с диагональной прямой. Единственное слагаемое, которое уводит кандидата в сторону от диагонали, — это полёт Леви.

И вот тут вступает размерность. Жадный отбор у нас принимает кандидата по среднему фитнесу — среднему значению Core по всем парам координат. Скелет r3·cB − r4·xi сдвигает все пары когерентно, в одну сторону, поэтому он надёжно двигает это среднее. А слагаемое Леви сдвигает каждую пару в свою независимую случайную сторону — и на больших размерностях его вклад в среднее усредняется в ноль по закону больших чисел. Отбор попросту перестаёт видеть Леви. В итоге на тысячемерной задаче алгоритм ведёт себя как на одномерной: подбирает r3 для сканирования диагонали. Чем выше размерность — тем чище усредняется шум Леви и тем ближе поведение алгоритма к чистому сканированию диагонали. Этот механизм наглядно показан на иллюстрации (рисунок 2).

Теперь видно, что оба канала — это одна и та же болезнь. И в разведке, и в эксплуатации виноват один и тот же шаблон: случайный множитель, разыгранный раз на агента и домноженный на структурированный вектор. В разведке это был общий скаляр-опора X[RJ][P1], здесь — r3, домноженный на cB. Любой такой скаляр запирает кандидата в низкоразмерное подпространство, и на стенде из одинаковых функций это подпространство совпадает с диагональю, где лежит оптимум. Отсюда и формулируется правило честной правки: ни один случайный скаляр-на-агента не должен домножать осмысленный вектор.

Применяем это правило. В эксплуатации убираем масштаб-скаляр r3·cB и делаем тягу к лучшему решению покоординатной интерполяцией — каждая координата подтягивается к своему cB[c] на свою собственную случайную долю:

double r4 = u.RNDprobab();
double C1 = 2.0 * r4 * (1.0 - tRatio);

for(int c = 0; c < coords; c++)
 {
  double r3c = u.RNDprobab();                    // на КАЖДУЮ координату
  double xi  = snap_c [i * coords + c];
  double xr  = snap_c [RJ * coords + c];
  double Lv  = LevyStep();
  double v   = xi + r3c * (cB [c] - xi) + C1 * Lv * (xr - xi);
  ...

Тем же приёмом лечится и фаза падения кита: множители r5 и r6 становятся покоординатными, чтобы скелет r5·xi − r6·xr не запирал кандидата в плоскость двух векторов:

for(int c = 0; c < coords; c++)
 {
  double r5c  = u.RNDprobab();
  double r6c  = u.RNDprobab();
  double step = r7 * (rangeMax [c] - rangeMin [c]) * decay;
  double v    = r5c * snap_c [i * coords + c] -
                r6c * snap_c [RJ * coords + c] + step;
  ...
 }

Стоит оговорить, что мы оставили на агента и почему это не нарушает правило. Коэффициенты C1 (затухание Леви) и r7 (магнитуда шага падения кита) по-прежнему разыгрываются раз на агента. Но они домножают не структурированные векторы, а покоординатно-случайные — вектор Леви и шаг переселения. Равномерный масштаб случайного вектора не схлопывает его в подпространство, диагонали не создаёт. Запирает только скаляр, домножающий осмысленный вектор вроде cB или xi , — а такие убраны все три.

Получившуюся версию назовём BWOm. Трёхфазная структура, балансовый фактор, вероятность падения кита, полёт Леви и временное затухание — всё на месте; изменился лишь способ розыгрыша случайности, теперь он покоординатный во всех трёх фазах. Осталось прогнать стенд и посмотреть, что покажет честный профиль, — этим займёмся в следующем разделе.

BWO_dimensional_profile

Рисунок 4. График профиля по размерности

Результаты тестирования:

BWOm|Beluga Whale Optimization (modified)|30.0|0.05|
=============================
5 Hilly's; Func runs: 10000; result: 0.7130978249513108
25 Hilly's; Func runs: 10000; result: 0.5129375854531272
500 Hilly's; Func runs: 10000; result: 0.29394203648024203
=============================
5 Forest's; Func runs: 10000; result: 0.7714504280808662
25 Forest's; Func runs: 10000; result: 0.42740547099458537
500 Forest's; Func runs: 10000; result: 0.09803668589845292
=============================
5 Megacity's; Func runs: 10000; result: 0.656
25 Megacity's; Func runs: 10000; result: 0.35386666666666666
500 Megacity's; Func runs: 10000; result: 0.13465333333333432
=============================
All score: 3.96139 (44.02%)

Сведём оба варианта результатов работы алгоритма в одну таблицу:

Функция, размерность BWO (оригинал) BWOm (исправленный)
Hilly, 10 0.635 0.713
Hilly, 50 0.608 0.513
Hilly, 1000 0.690 0.294
Forest, 10 0.420 0.771
Forest, 50 0.342 0.427
Forest, 1000 0.346 0.098
Megacity, 10 0.488 0.656
Megacity, 50 0.346 0.354
Megacity, 1000 0.400 0.135

Профиль по размерности — на иллюстрации выше — теперь читается однозначно. У BWOm счёт по всем трём функциям монотонно убывает с ростом размерности: никакого подъёма на 1000 координат, кривая идёт так, как и положено идти кривой честного популяционного алгоритма на фиксированном бюджете. Аномалия, с которой начиналось расследование, закрыта.

Дальше — два наблюдения, которые мне кажутся самыми важными во всей этой истории.

Первое. На 10 координатах BWOm обыгрывает оригинал по всем трём функциям, и с заметным отрывом: Hilly 0.713 против 0.635, Forest 0.771 против 0.420, Megacity 0.656 против 0.488. То есть геометрический трюк не просто надувал счёт на больших размерностях — он одновременно душил настоящий поиск на малых, запирая китов в осесимметричное подпространство вместо полноценного исследования пространства. Убрав чит, мы не отняли у алгоритма силу, а вернули её.

Второе. На 1000 координат картина обратная — честная BWOm проседает ниже оригинала, особенно резко на Forest: 0.098 против 0.346. Но эти 0.346 у BWO и были диагональным сканированием. Уберите механизм — и обнажится истинная сложность тысячемерного Forest на таком бюджете. 0.098 — это не провал реализации, это честная цена задачи.

Поэтому и общий балл, упавший с 47.51% до 44.02%, надо читать правильно. Это не регресс — это дефляция фиктивной части счёта. 44.02% BWOm заработаны поиском, а не формой стенда; 47.51% BWO — нет. Косвенное подтверждение мы получили ещё в начале: на композитном анти-чит тесте, где геометрический трюк не работает, оригинал давал 38.57% — и это куда ближе к честным низкоразмерным числам BWOm, чем к репозиторным 47.51%.

Что из этого следует для нашей рейтинговой таблицы. Таблица существует, чтобы трейдер мог выбрать инструмент для оптимизации торговой стратегии. А реальная задача оптимизации стратегии — это всегда разнородный ландшафт: параметры стопа, тейка, фильтров, размера позиции имеют разную природу и разные оптимумы. Алгоритм, набравший высокий балл диагональным трюком, на такой задаче просто не сработает — трюку не за что зацепиться. Поэтому в таблицу должна идти честная цифра. BWO в его исходном виде претендовать на честное место не может; BWOm с его 44.02% занимает законную позицию — скромную, но настоящую.

И два вывода более общего характера, которые стоит вынести из этой статьи читателю.

Диагностический. Немонотонность счёта по размерности — это красный флаг. Если алгоритм на фиксированном бюджете вычислений работает на больших размерностях не хуже, а лучше, он почти наверняка не оптимизирует, а считывает геометрию стенда. Проверять профиль по размерности стоит у каждого нового кандидата в таблицу, и композитный анти-чит тест здесь — надёжный второй индикатор.

Механический. Источник чита в BWO — повторяющийся шаблон: случайный скаляр, разыгранный раз на агента и домноженный на структурированный вектор. Такой множитель запирает кандидата в низкоразмерное подпространство. Это типовая ловушка проектирования метаэвристик, и теперь мы знаем, как её распознавать и лечить — заменой скалярной случайности на покоординатную. 

Теперь можем посмотреть результаты прогона античит-теста, они соответствуют результатам по первым размерностям основного теста, однако заметен значительный разброс занчений.

BWOm|Beluga Whale Optimization (modified)|30.0|0.05|
=============================
Composite anti-cheat test: Hilly + Forest + Megacity + Peaks + Skin
Coordinates: 10; Epochs: 333; Repeats: 10
=============================
Run 1/10: 0.6768112016070393
Run 2/10: 0.8580079686168114
Run 3/10: 0.7773490639079661
Run 4/10: 0.6239346517249487
Run 5/10: 0.8031346429925448
Run 6/10: 0.7329426521976851
Run 7/10: 0.5579271937001347
Run 8/10: 0.7388632225403626
Run 9/10: 0.6460757525932408
Run 10/10: 0.7374171635629155
=============================
Average result: 0.7152463513 (71.52%)
=============================

Визуализация работы алгоритма BWOm на тестовых функциях и демонстрация работы на некоторых других. 

Hilly

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

Forest

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

Megacity

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

Rosenbrock

BWOm на тестовой функции Rosenbrock

Ackley

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

Прежде чем выставлять алгоритму окончательную оценку, разумно потратить немного времени на параметры — мы их специально оставляли как в статье. Defaults были такими: popSize = 30, KD = 0.05. С ними BWOm давал честные, но скромные 44.02%. KD = 0.05 — это, по сути, не настраиваемый параметр, а вшитая константа полёта Леви из эталонной реализации, и она консервативна. После того как мы убрали геометрический чит, полёт Леви стал единственным дальнобойным механизмом в фазе эксплуатации, и его масштаб напрямую задаёт энергию поиска. На наших трёх многомодальных функциях значение 0.05 даёт слишком короткие прыжки.

Поднимем KD и одновременно уменьшим популяцию — при фиксированном бюджете в 10000 вычислений меньшая популяция означает больше итераций на агента, что даёт покоординатной интерполяции к лучшему решению отработать на ход глубже. С popSize = 20 и KD = 1.5 получаем:

BWOm|Beluga Whale Optimization (modified)|20.0|1.5|
=============================
5 Hilly's; Func runs: 10000; result: 0.784881775284155
25 Hilly's; Func runs: 10000; result: 0.5687271425947741
500 Hilly's; Func runs: 10000; result: 0.2955752923941583
=============================
5 Forest's; Func runs: 10000; result: 0.9137000614171915
25 Forest's; Func runs: 10000; result: 0.6176092379746694
500 Forest's; Func runs: 10000; result: 0.129880770631638
=============================
5 Megacity's; Func runs: 10000; result: 0.8133333333333332
25 Megacity's; Func runs: 10000; result: 0.4994666666666667
500 Megacity's; Func runs: 10000; result: 0.1500400000000007
=============================
All score: 4.77321 (53.04%)

Композит 83.96% — это не «хорошо», это очень хорошо: BWOm, лишившись чита, не просто остался конкурентоспособным, а на низкой размерности вышел в верхнюю часть таблицы. А теперь посмотрим на дисперсию результатов, она высока. Это типичная подпись алгоритма с агрессивным механизмом дальнего прыжка на многоэкстремальном ландшафте: либо случайный длинный шаг Леви попадает в правильный бассейн притяжения, либо нет. У оригинальной BWO такого выбора не было — её диагональный трюк работал стабильно и всегда давал одну и ту же скромную цифру около 0.39. Стабильность была обманчивой.

BWOm|Beluga Whale Optimization (modified)|20.0|1.5|
=============================
Composite anti-cheat test: Hilly + Forest + Megacity + Peaks + Skin
Coordinates: 10; Epochs: 500; Repeats: 10
=============================
Run 1/10: 0.8510959078175775
Run 2/10: 0.6776209662686439
Run 3/10: 0.6864531057459684
Run 4/10: 0.9823673728411345
Run 5/10: 0.9999793806023446
Run 6/10: 0.6755386763260092
Run 7/10: 0.9720418753408225
Run 8/10: 0.8871892660061993
Run 9/10: 0.7957332506268917
Run 10/10: 0.8675285275873976
=============================
Average result: 0.8395548329 (83.96%)

Рассмотрим ниже иллюстрацию, которая показывает прямой контраст двух распределений на одной шкале. Композитный античит‑тест: разброс 10 прогонов. BWO плотно кластеризуется вокруг значения 0.39 — его диагональное сканирование стабильно извлекает тот же скромный результат. BWOm лежит в диапазоне от 0.68 до 1.00 и показывает двумодальный характер: агрессивный шаг Леви либо попадает в глобальный бассейн (в одном прогоне значение достигло 0.9999).

variance_distribution

Рисунок 5. Девиация распределений двух алгоритмов.

Верхняя строка — десять прогонов оригинальной BWO: точки плотно скучены в коридоре 0.36–0.42, светлая полоса — это ±1σ (ширина ~5% от шкалы), вертикальная риска — среднее 0.386. Подпись под кластером: диагональное сканирование — стабильный механизм, то есть алгоритм надёжно извлекал из стенда одну и ту же скромную цифру, прогон за прогоном на анти-чит тесте.

Нижняя строка — десять прогонов BWOm: те же точки и та же ±1σ полоса, но теперь полоса в шесть раз шире — 0.71–0.97, — и три точки слева и три точки справа выпадают даже за её пределы. Бимодальная структура наглядна: слева кластер неудачных (0.676–0.687), справа кластер удачных (0.972–1.000), причём самый правый прогон практически на границе шкалы — это та самая попытка, где алгоритм добрался до глобального оптимума с результатом 0.9999.

По результатам тестирования алгоритм BWOm занимает 38-ю строчку нашего рейтинга лучших популяционных методов оптимизации.

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
38 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
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 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
39 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
40 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
41 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
42 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
43 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
44 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
45 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
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


Выводы

Итак, общий балл вырос с 44.02% до 53.04% — это уверенная верхне-средняя часть рейтинговой таблицы. Профиль по размерности при этом остался монотонно убывающим по всем трём функциям — то есть прироста за счёт каких-то новых  артефактов не возникло, мы получили просто лучше настроенный честный оптимизатор. И на низкой размерности результаты сильные: Hilly 0.78, Forest 0.91, Megacity 0.81 — это серьёзные цифры.

Композитный анти-чит тест в той же настройке дал 83.96%. На минуту вглядимся в эту цифру. Стандартный стенд на 10 координатах усреднил BWOm по трём функциям как (0.7849 + 0.9137 + 0.8133)/3 ≈ 0.8373. Композит на тех же 10 координатах, но из пяти разных функций с разными оптимумами, даёт 0.8396. Совпадение почти посимвольное. Это и есть визитная карточка честной оптимизации: на одном и том же бюджете и размерности алгоритм работает одинаково хорошо что на однородном, что на разнородном ландшафте — потому что не цепляется за форму стенда, а действительно ищет. У оригинального BWO здесь был провал с 47.51% до 38.57%, у BWOm никакого провала нет.

Сильные стороны. Главное — алгоритм после правки выдаёт честный результат и силён там, где это действительно важно для прикладной оптимизации торговых стратегий: на низкой и средней размерности. Лишь немногие методы из таблицы способны показать одновременно 0.83 на гомогенном бенче и 0.84 на композите. Профиль по размерности корректный, без подъёмов и аномалий. Параметров для настройки всего два — popSize и KD , — то есть пользоваться алгоритмом легко, а KD к тому же оказался полезным управляемым параметром, а не догмой 0.05 из статьи. Концептуально устройство тоже привлекательное: три качественно разных оператора, плавно переключающихся балансовым фактором, плюс полёт Леви, дающий редкие, но критичные дальние прыжки для выхода из локальных оптимумов.

Слабые стороны. Главная — резкое падение качества с ростом размерности. На 1000 координат Forest даёт лишь 0.13, Megacity 0.15 — это уже не уровень верхних строк рейтинга, а скромная середина. Покоординатная случайность во всех трёх фазах, которую мы внесли при правке, по построению не накачивает среднее усреднённой по парам приёмкой; на тысячемерных задачах с фиксированным бюджетом этого не хватает. Так что BWOm — не выбор для оптимизации задач с сотнями параметров. Второй минус — фаза падения кита. Она редкая (Wf не превышает 0.1 и затухает к 0.05), и её вклад в общую динамику невелик; на части тестов её можно было бы выключить почти без потерь, что слегка обесценивает её роль в концепции. Третий — наследие оригинала. Опубликованный алгоритм в чистом виде мухлюет, и без понимания, где и почему, можно занести в таблицу несправедливую цифру 47.51%. Этот разбор, собственно, для того и нужен.

Где использовать. Для типичной задачи трейдера — оптимизация набора параметров стратегии в диапазоне от единиц до нескольких десятков значений — BWOm в нашей таблице один из вариантов: низкая размерность, разнородный ландшафт параметров (стопы, тейки, фильтры, ATR-множители — всё с разной природой), необходимость уходить из локальных оптимумов. Полёт Леви и честный покоординатный поиск здесь работают на полную. Если же стратегия параметризована очень богато (сотни весов нейросети, тысячи переменных в портфельной задаче), BWOm уступит специализированным высокоразмерным методам — и здесь резонно смотреть на верхние строки таблицы для соответствующей категории.

Практический смысл для трейдера. Алгоритм, у которого 30% прогонов отстают от среднего на 16-18 процентных пунктов, нельзя запускать один раз и доверять результату — нужна стратегия мульти-рестарта. Прогон BWOm с тремя-пятью независимыми инициализациями и выбором лучшего результата практически гарантирует попадание в верхнюю часть распределения (вероятность не получить ни одного удачного прогона из пяти при ~70% доли удачных — около 0.3⁵ ≈ 0.2%). Для серьёзной задачи оптимизации стратегии 3-5 рестартов — копейки, в общем бюджете вычислений ничего не меняет. А вот без рестартов одинокая 0.68 в реальной задаче ставит трейдера в положение то ли стратегия слабая, то ли оптимизатор не дотянул, и это плохая позиция.

В следующей статье возьмём очередного кандидата в таблицу — и инструменты, которыми мы здесь обзавелись (проверка профиля по размерности, композитный анти-чит тест, правило никаких скаляров-на-агента, домножающих структурированный вектор), пойдут в работу с первой же строки.

tab

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

chart

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


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

Плюсы:

  1. Небольшое количество внешних параметров.
  2. Быстрый.

Минусы:

  1. Склонность к застреванию.

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



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

# Имя Тип Описание
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_BWOm.mq5
Скрипт Испытательный стенд для BWOm

Прикрепленные файлы |
BWOm.zip (416.35 KB)
Разработка инструментария для анализа Price Action (Часть 58): Модуль анализа сжатия диапазона и классификации зрелости Разработка инструментария для анализа Price Action (Часть 58): Модуль анализа сжатия диапазона и классификации зрелости
В продолжение предыдущей статьи, где был представлен модуль классификации состояния рынка, в этой части мы сосредоточимся на реализации основной логики выявления и оценки зон сжатия. В статье представлена система обнаружения сжатия диапазона и оценки зрелости на языке MQL5, которая анализирует зоны рыночной консолидации, опираясь только на динамику цены.
Нейросети в трейдинге: Принятие торговых решений с учётом неопределённости (Модули прогнозирования и планирования) Нейросети в трейдинге: Принятие торговых решений с учётом неопределённости (Модули прогнозирования и планирования)
Статья продолжает адаптацию фреймворка UncAD к алгоритмическому трейдингу и фокусируется на модулях прогнозирования и планирования. Унитарные рыночные ряды заменяют участников сцены, а состояние счёта играет роль ego-агента. Реализованы CNeuronUncADUGP и CNeuronUncADUGPL, которые связывают прогноз, карту рыночных состояний и неопределённость с торговым контекстом, чтобы формировать согласованные сценарии и подготавливать решения по входу, удержанию и снижению риска.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Разработка инструментария для анализа Price Action (Часть 57): Создание модуля классификации состояния рынка на MQL5 Разработка инструментария для анализа Price Action (Часть 57): Создание модуля классификации состояния рынка на MQL5
В этой статье представлен модуль классификации состояния рынка на MQL5, который интерпретирует поведение цены по данным закрытых свечей. Анализируя сжатие и расширение волатильности, а также согласованность структуры, инструмент классифицирует состояние рынка как сжатие, переходное состояние, расширение или тренд и тем самым формирует четкий контекст для анализа Price Action.