preview
Алгоритм оптимизации бабочек — Butterfly Optimization Algorithm (BOA)

Алгоритм оптимизации бабочек — Butterfly Optimization Algorithm (BOA)

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

Содержание

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


Введение

Рассмотрим в статье один из новых алгоритмов оптимизации, подсмотренный у природы. Бабочки — одни из самых удивительных созданий, чья жизнь неразрывно связана с поиском: поиском пищи, партнёра, места для откладывания яиц. В отличие от многих других насекомых, бабочки обладают уникальной системой химической коммуникации, основанной на восприятии и испускании ароматических веществ — феромонов. Именно эта особенность легла в основу алгоритма оптимизации бабочек Butterfly Optimization Algorithm, BOA, предложенного индийскими исследователями С. Аророй и С. Сингхом в 2019 году.

В природе бабочки используют специализированные хеморецепторы, расположенные на антеннах, для обнаружения химических сигналов на поразительно больших расстояниях. Самцы некоторых видов способны улавливать феромоны самки за несколько километров. Интенсивность воспринимаемого запаха зависит от двух факторов: силы источника аромата и расстояния до него. Согласно закону Стивенса, описывающему психофизическое восприятие стимулов у живых организмов, воспринимаемая величина ощущения связана с физической интенсивностью стимула степенной зависимостью. Этот принцип авторы алгоритма формализовали в виде уравнения аромата: f = c·I^a, где f — воспринимаемая величина аромата, "I" — интенсивность стимула (связанная с качеством источника пищи), "c" — сенсорная модальность (способность бабочки воспринимать запахи), а показатель степени "a" определяет характер зависимости восприятия от интенсивности.

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

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

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

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



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

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

В оригинальной статье формула глобального поиска записана как x_new = x + (r²·g - x)·f*, где x — текущая позиция бабочки, g* — лучшее найденное решение, r — случайное число из [0,1], а f — аромат бабочки. На первый взгляд формула выглядит логично: бабочка должна двигаться к лучшему решению. Однако при внимательном рассмотрении выражения в скобках, (r²·g - x)* становится очевидной проблемой.

Рассмотрим конкретный пример: пусть лучшее решение g* = 10, текущая позиция x = 5, случайное число r = 0.7 (тогда r² = 0.49), аромат f = 0.5. Подставляя значения, получаем: x_new = 5 + (0.49·10 - 5)·0.5 = 5 + (4.9 - 5)·0.5 = 5 + (-0.1)·0.5 = 4.95. Бабочка сместилась не к оптимуму g* = 10, а в противоположную сторону — к нулю! Это происходит потому, что выражение r²·g* при r < 1 всегда даёт значение меньше g*, и разность (r²·g* - x) направлена не к g*, а к точке r²·g*, которая ближе к нулю.

Аналогичная проблема присутствует в формуле локального поиска x_new = x + (r²·x_j - x_k)·f. Выражение (r²·x_j - x_k) не создаёт осмысленного направления между двумя бабочками j и k. Вместо этого оно генерирует вектор, систематически смещённый к началу координат из-за множителя r² < 1 перед x_j.

Интересно отметить, что оригинальные формулы могут давать приемлемые результаты на стандартных тестовых функциях (Sphere, Rastrigin, Ackley и других), глобальный оптимум которых находится именно в точке (0, 0, ..., 0). В этом случае смещение к началу координат случайно совпадает с движением к оптимуму, что маскирует ошибку в логике алгоритма. Однако на реальных задачах оптимизации, где оптимум может находиться в произвольной точке пространства поиска, алгоритм демонстрирует полную неработоспособность.

Для исправления ситуации пришлось изменить расстановку скобок в формулах, сохранив при этом все оригинальные компоненты алгоритма. Скорректированная формула глобального поиска будет иметь вид x_new = x + r²·(g - x)·f*. Теперь выражение (g* - x) представляет собой вектор направления от текущей позиции к лучшему решению, что соответствует биологической метафоре: бабочка летит на аромат лучшего цветка. Множитель r² масштабирует длину шага, а аромат f определяет интенсивность движения. Проверим на том же примере: x_new = 5 + 0.49·(10 - 5)·0.5 = 5 + 0.49·5·0.5 = 5 + 1.225 = 6.225. Теперь бабочка корректно движется от x = 5 в сторону g* = 10.

Формула локального поиска была скорректирована аналогично: x_new = x + r²·(x_j - x_k)·f. Выражение (x_j - x_k) теперь представляет разностный вектор между двумя случайными бабочками, задающий направление локального исследования. Это создаёт случайное блуждание в пределах текущего распределения популяции, что соответствует локальному поиску пищи бабочками в ограниченной области.

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

BOA_Illustration

Иллюстрация работы алгоритма BOA

Иллюстрация показывает ночную поляну с цветами и бабочками. Лучший цветок (g*) — крупный жёлтый с золотым ореолом аромата и концентрическими волнами распространения. Три бабочки (оранжевая, синяя, фиолетовая) летят к нему по пунктирным оранжевым траекториям — глобальный поиск. Две другие (жёлтая, бирюзовая) зигзагообразно исследуют область вокруг второго цветка по зелёным траекториям — локальный поиск.

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

ВХОД:

  N — размер популяции
  c — сенсорная модальность, [0, 1]
  aStart — начальный показатель поглощения
  p — вероятность переключения глобальный/локальный поиск
  T — общее число эпох
  D — размерность пространства поиска
  min[d], max[d] — границы поиска по каждой координате d

ВЫХОД:
  g* — лучшая найденная позиция
  fB — лучшее значение целевой функции

//-----------------------------------------------------------------------------
// Инициализация
//-----------------------------------------------------------------------------
1.  aCurrent ← aStart
2.  fB ← −∞
3.  ДЛЯ КАЖДОЙ бабочки i = 1..N:
4.    fragrance[i] ← c
5.    intensity[i] ← 0.5
6.    ДЛЯ КАЖДОЙ координаты d = 1..D:
7.    x[i][d] ← min[d] + rand(0,1) × (max[d] − min[d])
8.    f[i] ← FitnessFunction(x[i])


//-----------------------------------------------------------------------------
// Основной цикл
//-----------------------------------------------------------------------------
9.  ДЛЯ КАЖДОЙ эпохи t = 1..T:

    //--- Обновление лучшего глобального решения ---
10.   ДЛЯ КАЖДОЙ бабочки i = 1..N:
11.     ЕСЛИ f[i] > fB:
12.       fB ← f[i]
13.       g* ← x[i]

    //--- Нормализация интенсивности стимула ---
14.   fMin ← min(f[1..N])
15.   fMax ← max(f[1..N])
16.   range ← fMax − fMin
17.   ДЛЯ КАЖДОЙ бабочки i = 1..N:
18.     ЕСЛИ range < ε:
19.       intensity[i] ← 0.5
20.     ИНАЧЕ:
21.       intensity[i] ← 0.1 + 0.9 × (f[i] − fMin) / range

    //--- Обновление показателя поглощения ---
22.   aCurrent ← aStart + (t / T) × (1.0 − aStart)
23.   ЕСЛИ aCurrent > 1.0: aCurrent ← 1.0

    //--- Вычисление аромата (Формула 1) ---
24.   ДЛЯ КАЖДОЙ бабочки i = 1..N:
25.     fragrance[i] ← c × intensity[i] ^ aCurrent

    //--- Перемещение бабочек ---
26.   ДЛЯ КАЖДОЙ бабочки i = 1..N:
27.     rnd ← rand(0,1)

28.     ЕСЛИ rnd < p:
        //--- Глобальный поиск (Формула 2) ---
29.       ДЛЯ КАЖДОЙ координаты d = 1..D:
30.         r ← rand(0,1)
31.         r² ← r × r
32.         direction ← g*[d] − x[i][d]
33.         x[i][d] ← x[i][d] + r² × direction × fragrance[i]

34.     ИНАЧЕ:
        //--- Локальный поиск (Формула 3) ---
35.       j ← случайная бабочка из [1..N]
36.       k ← случайная бабочка из [1..N], k ≠ j
37.       ДЛЯ КАЖДОЙ координаты d = 1..D:
38.         r ← rand(0,1)
39.         r² ← r × r
40.         direction ← x[j][d] − x[k][d]
41.         x[i][d] ← x[i][d] + r² × direction × fragrance[i]

      //--- Проверка границ ---
42.     ДЛЯ КАЖДОЙ координаты d = 1..D:
43.       ЕСЛИ x[i][d] < min[d]: x[i][d] ← min[d]
44.       ЕСЛИ x[i][d] > max[d]: x[i][d] ← max[d]

      //--- Вычисление фитнеса ---
45.     f[i] ← FitnessFunction(x[i])

46. ВЕРНУТЬ g*, fB

Переходим к реализации алгоритма BOA.

Класс "C_AO_BOA" наследуется от базового класса "C_AO" и реализует алгоритм оптимизации бабочек. В конструкторе задаются имя алгоритма, его описание и ссылка на статью, а также значения параметров по умолчанию: размер популяции, сенсорная модальность, начальный показатель поглощения, вероятность переключения между глобальным и локальным поиском.

Массив "params" содержит четыре элемента, каждый из которых хранит имя и значение соответствующего параметра. Публичные поля "sensorModC", "aStart" и "switchP" доступны извне для прямого задания значений перед инициализацией. Приватная часть класса содержит текущий показатель поглощения "aCurrent", общее число эпох "epochs", счётчик текущей эпохи "epochNow", а также два массива, "fragrance" для хранения вычисленного аромата каждой бабочки и "intensity" для хранения нормализованной интенсивности стимула. Метод "SetParams" считывает значения из массива "params" и присваивает их соответствующим полям класса.

//————————————————————————————————————————————————————————————————————
class C_AO_BOA : public C_AO
{
  public:
  ~C_AO_BOA () { }

  C_AO_BOA ()
  {
    ao_name = "BOA";
    ao_desc = "Butterfly Optimization Algorithm";
    ao_link = "https://www.mql5.com/ru/articles/21209";

    popSize    = 50;
    sensorModC = 0.9;
    aStart     = 0.5;
    switchP    = 0.8;

    ArrayResize (params, 4);
    params [0].name = "popSize";    params [0].val = popSize;
    params [1].name = "sensorModC"; params [1].val = sensorModC; // сенсорная модальность [0, 1]
    params [2].name = "aStart";     params [2].val = aStart;     // начальный показатель поглощения
    params [3].name = "switchP";    params [3].val = switchP;    // вероятность переключения (switch probability)
  }

  void SetParams ()
  {
    popSize    = (int)params [0].val;
    sensorModC = params      [1].val;
    aStart     = params      [2].val;
    switchP    = params      [3].val;
  }

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

  void Moving   ();
  void Revision ();

  //------------------------------------------------------------------
  double sensorModC;  // сенсорная модальность c (sensory modality)
  double aStart;      // начальный показатель поглощения
  double switchP;     // вероятность переключения p (switch probability)

  private: //—————————————————————————————————————————————————————————
  double aCurrent;    // текущий показатель поглощения
  int    epochs;      // общее число эпох
  int    epochNow;    // текущая эпоха

  double fragrance []; // аромат каждой бабочки
  double intensity []; // интенсивность стимула (нормализованный фитнес)

  void CalculateFragrance ();
  void NormalizeIntensity ();
};
//————————————————————————————————————————————————————————————————————

Метод "Init" принимает массивы минимальных и максимальных границ пространства поиска, массив шагов дискретизации и общее число эпох. Сначала вызывается метод "StandardInit" базового класса, который выполняет стандартную инициализацию: выделение памяти под популяцию, копирование границ и шагов поиска, определение числа координат. Если стандартная инициализация завершилась неудачно, метод возвращает "false".

Затем запоминается общее число эпох, счётчик текущей эпохи обнуляется, текущий показатель поглощения устанавливается равным начальному значению "aStart". После этого выделяется память под массивы аромата и интенсивности размером "popSize", и каждому элементу аромата присваивается начальное значение "sensorModC", а каждому элементу интенсивности — значение 0.5. Это означает, что до первой оценки фитнеса все бабочки считаются одинаковыми по привлекательности, а их начальный аромат определяется только сенсорной модальностью.

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

  //------------------------------------------------------------------
  epochs   = epochsP;
  epochNow = 0;
  aCurrent = aStart;

  //------------------------------------------------------------------
  // Инициализация массивов аромата и интенсивности
  //------------------------------------------------------------------
  ArrayResize (fragrance, popSize);
  ArrayResize (intensity, popSize);

  for (int i = 0; i < popSize; i++)
  {
    fragrance [i] = sensorModC;
    intensity [i] = 0.5;
  }

  return true;
}
//————————————————————————————————————————————————————————————————————

Метод "Moving" реализует перемещение всех бабочек на текущей итерации. В начале увеличивается счётчик эпох "epochNow". Если флаг "revision" установлен в "false", что означает первую итерацию, каждой бабочке назначается случайная позиция: для каждой координаты генерируется случайное число из равномерного распределения на отрезке от нуля до единицы, и позиция вычисляется как линейная интерполяция между минимальной и максимальной границей с последующей дискретизацией функцией "SeInDiSp". После инициализации флаг "revision" устанавливается в "true", и метод завершается.

На всех последующих итерациях сначала вызывается метод "CalculateFragrance", вычисляющий аромат каждой бабочки на основе текущей интенсивности стимула и показателя поглощения. Затем для каждой бабочки "i" генерируется случайное число и сравнивается с вероятностью переключения "switchP". Если случайное число меньше "switchP", выполняется глобальный поиск: для каждой координаты "d" генерируется случайное число "r", вычисляется его квадрат "r²", определяется направление как разность между координатой лучшего глобального решения "cB" и текущей координатой бабочки, после чего позиция обновляется прибавлением произведения "r²", направления и аромата данной бабочки. Таким образом бабочка смещается в сторону лучшего найденного решения, причём величина шага пропорциональна расстоянию до цели, случайному множителю и силе аромата.

Если случайное число больше или равно "switchP", выполняется локальный поиск. Случайным образом выбираются две различные бабочки "j" и "k" из популяции. Для каждой координаты "d" аналогично генерируется "r²", вычисляется направление как разность координат бабочек "j" и "k", и позиция обновляется прибавлением произведения "r²", направления и аромата. Этот механизм создаёт случайное блуждание, масштаб которого определяется текущим разбросом популяции.

Дополнительно после локального поиска с вероятностью 0.2 одна случайно выбранная координата бабочки заменяется значением, сгенерированным по нормальному распределению с центром в соответствующей координате лучшего решения "cB" и границами, совпадающими с границами пространства поиска. Этот приём отсутствует в оригинальном алгоритме и служит для предотвращения застревания популяции в локальных экстремумах путём внесения контролируемого шума вблизи лучшего известного решения.

В завершение для каждой координаты каждой бабочки вызывается функция "SeInDiSp", которая одновременно ограничивает значение допустимым диапазоном и выполняет дискретизацию с заданным шагом.

//————————————————————————————————————————————————————————————————————
void C_AO_BOA::Moving ()
{
  epochNow++;

  //------------------------------------------------------------------
  // Первая итерация - случайная инициализация популяции
  // Позиции бабочек генерируются случайно в пространстве поиска
  //------------------------------------------------------------------
  if (!revision)
  {
    for (int i = 0; i < popSize; i++)
    {
      for (int d = 0; d < coords; d++)
      {
        double rnd = u.RNDfromCI (0.0, 1.0);
        a [i].c [d] = rangeMin [d] + rnd * (rangeMax [d] - rangeMin [d]);
        a [i].c [d] = u.SeInDiSp (a [i].c [d], rangeMin [d], rangeMax [d], rangeStep [d]);
      }
    }
    revision = true;
    return;
  }

  //------------------------------------------------------------------
  // Вычисление аромата для каждой бабочки (формула 1)
  // f = c * I^a
  //------------------------------------------------------------------
  CalculateFragrance ();

  //------------------------------------------------------------------
  // Основной цикл - движение бабочек
  //------------------------------------------------------------------
  for (int i = 0; i < popSize; i++)
  {
    if (u.RNDprobab () < switchP)
    {
      //==============================================================
      // ГЛОБАЛЬНЫЙ ПОИСК (формула 2, скорректированная)
      // x_i(t+1) = x_i(t) + r² × (g - x_i(t)) × f_i
      // Бабочка движется к лучшему глобальному решению g
      //
      // Логика: направление (g - x) указывает от текущей позиции к лучшей,
      // шаг масштабируется r² (случайность) и f (аромат/фитнес)
      //==============================================================
      for (int d = 0; d < coords; d++)
      {
        double r = u.RNDprobab ();
        double r2 = r * r;

        // Направление от текущей позиции к лучшей
        double direction = cB [d] - a [i].c [d];

        // Шаг: r² × направление × аромат
        double step = r2 * direction * fragrance [i];

        a [i].c [d] = a [i].c [d] + step;
      }
    }
    else
    {
      //==============================================================
      // ЛОКАЛЬНЫЙ ПОИСК (формула 3, скорректированная)
      // x_i(t+1) = x_i(t) + r² × (x_j(t) - x_k(t)) × f_i
      // Случайное блуждание: направление определяется разницей между
      // двумя случайными бабочками j и k
      //
      // Логика: (x_j - x_k) даёт случайное направление в пространстве,
      // определяемое текущим распределением популяции
      //==============================================================

      // Выбираем две случайные бабочки j и k
      int j = u.RNDminusOne (popSize);
      int k = u.RNDminusOne (popSize);

      // Убеждаемся, что j и k различны
      while (k == j) k = u.RNDminusOne (popSize);

      for (int d = 0; d < coords; d++)
      {
        double r = u.RNDprobab ();
        double r2 = r * r;

        // Случайное направление между двумя бабочками
        double direction = a [j].c [d] - a [k].c [d];

        // Шаг: r² × направление × аромат
        double step = r2 * direction * fragrance [i];

        a [i].c [d] = a [i].c [d] + step;
      }

      //==============================================================
      // Добавление шума к координатам лучшего решения
      // для предотвращения застревания в локальных экстремумах
      // этого нет в оригинальном BOA
      //==============================================================
      if (u.RNDprobab () < 0.2)
      {
        int ind = u.RNDminusOne (coords);
        a [i].c [ind] = u.GaussDistribution (cB [ind], rangeMin [ind], rangeMax [ind], 1);
      }
    }

    //----------------------------------------------------------------
    // Проверка границ и дискретизация
    //----------------------------------------------------------------
    for (int d = 0; d < coords; d++)
    {
      a [i].c [d] = u.SeInDiSp (a [i].c [d], rangeMin [d], rangeMax [d], rangeStep [d]);
    }
  }
}
//————————————————————————————————————————————————————————————————————

Метод "Revision" выполняет три операции после того, как все бабочки были перемещены и их фитнес вычислен. Первая операция — обновление глобального лучшего решения: для каждой бабочки "i" проверяется, превышает ли её значение фитнеса текущее лучшее значение "fB", и если да, "fB" обновляется и позиция бабочки копируется в массив лучших координат "cB". Вторая операция — вызов метода "NormalizeIntensity", который пересчитывает интенсивность стимула для каждой бабочки на основе текущих значений фитнеса.

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

//————————————————————————————————————————————————————————————————————
void C_AO_BOA::Revision ()
{
  //------------------------------------------------------------------
  // 1. Обновляем глобальное лучшее решение
  //------------------------------------------------------------------
  for (int i = 0; i < popSize; i++)
  {
    if (a [i].f > fB)
    {
      fB = a [i].f;
      ArrayCopy (cB, a [i].c, 0, 0, coords);
    }
  }

  //------------------------------------------------------------------
  // 2. Нормализуем интенсивность стимула (I)
  //    Интенсивность I пропорциональна фитнесу f(x)
  //------------------------------------------------------------------
  NormalizeIntensity ();

  //------------------------------------------------------------------
  // 3. Обновляем показатель поглощения a
  //    Согласно псевдокоду: "Update the value of a"
  //    a линейно возрастает от aStart до 1.0
  //    Это постепенно уменьшает влияние интенсивности на аромат
  //------------------------------------------------------------------
  if (epochs > 0)
  {
    aCurrent = aStart + (double)epochNow / (double)epochs * (1.0 - aStart);
    if (aCurrent > 1.0) aCurrent = 1.0;
  }
}
//————————————————————————————————————————————————————————————————————

Метод "NormalizeIntensity" отвечает за приведение значений фитнеса к единой шкале интенсивности стимула. Сначала определяются минимальное и максимальное значения фитнеса среди всех бабочек текущей популяции. Если разница между максимальным и минимальным фитнесом пренебрежимо мала — меньше 10⁻¹⁰, что означает практическое равенство всех значений фитнеса, — всем бабочкам присваивается одинаковая интенсивность 0.5.

В противном случае для каждой бабочки интенсивность вычисляется по формуле линейного отображения значения фитнеса в диапазон от 0.1 до 1.0: I = 0.1 + 0.9 × (f − fMin) / (fMax − fMin). Бабочка с наихудшим фитнесом получает интенсивность 0.1, с наилучшим — 1.0. Нижняя граница 0.1 выбрана ненулевой намеренно: даже худшие бабочки должны иметь ненулевой аромат, чтобы сохранять способность участвовать в поисковом процессе и не превращаться в полностью пассивных агентов.

//————————————————————————————————————————————————————————————————————
// Нормализация интенсивности стимула
// Согласно статье: "Stimulus Intensity I at x is determined by f(x)"
// I пропорционален фитнесу
//————————————————————————————————————————————————————————————————————
void C_AO_BOA::NormalizeIntensity ()
{
  //------------------------------------------------------------------
  // Находим минимальный и максимальный фитнес
  //------------------------------------------------------------------
  double minF = a [0].f;
  double maxF = a [0].f;

  for (int i = 1; i < popSize; i++)
  {
    if (a [i].f < minF) minF = a [i].f;
    if (a [i].f > maxF) maxF = a [i].f;
  }

  //------------------------------------------------------------------
  // Нормализуем интенсивность в диапазон [0.1, 1.0]
  // Нижняя граница 0.1 предотвращает слишком малый аромат у худших бабочек
  //------------------------------------------------------------------
  double range = maxF - minF;

  if (range < 1e-10)
  {
    // Все фитнесы примерно равны
    for (int i = 0; i < popSize; i++)
    {
      intensity [i] = 0.5;
    }
  }
  else
  {
    for (int i = 0; i < popSize; i++)
    {
      intensity [i] = 0.1 + 0.9 * (a [i].f - minF) / range;
    }
  }
}
//————————————————————————————————————————————————————————————————————

Метод "CalculateFragrance" вычисляет аромат каждой бабочки по основной формуле алгоритма: fragrance = c × I^a, где "c" — сенсорная модальность "sensorModC", "I" — нормализованная интенсивность стимула "intensity", "a" — текущий показатель поглощения "aCurrent". Эта формула основана на законе Стивенса из психофизики, описывающем связь между физической интенсивностью стимула и субъективным восприятием.

При малых значениях показателя "a", характерных для начала оптимизации, возведение интенсивности в малую степень даёт значения близкие к единице для всех бабочек независимо от их фитнеса, и аромат всех бабочек оказывается примерно равным сенсорной модальности "c", что обеспечивает равномерное исследование пространства. По мере роста "a" к единице различия в интенсивности всё сильнее проявляются в аромате: бабочки с высоким фитнесом получают аромат близкий к "c", а бабочки с низким фитнесом — существенно меньший, что направляет поиск в сторону наиболее перспективных областей.

//————————————————————————————————————————————————————————————————————
// Вычисление аромата по формуле 1: f = c * I^a
// где:
// c - сенсорная модальность (определяет базовую величину шага)
// I - интенсивность стимула (нормализованный фитнес)
// a - показатель степени поглощения
//
// При малом a (начало): I^a → 1 для всех I, аромат ≈ c (равномерный поиск)
// При большом a (конец): I^a → I, лучшие бабочки имеют больший аромат
//————————————————————————————————————————————————————————————————————
void C_AO_BOA::CalculateFragrance ()
{
  for (int i = 0; i < popSize; i++)
  {
    // Формула 1: fragrance = c * I^a
    fragrance [i] = sensorModC * MathPow (intensity [i], aCurrent);
  }
}
//————————————————————————————————————————————————————————————————————


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

Будет важным отметить, что без вот этого небольшого участка кода ниже, недостаточным является разнообразие в популяции для мотыльков. Этот фрагмент реализует механизм стохастической мутации, отсутствующий в оригинальном описании алгоритма BOA и добавленный для компенсации его структурной слабости. Проблема заключается в том, что локальный поиск в оригинальной идее авторов целиком зависит от разности позиций двух случайных бабочек (x_j − x_k). По мере схождения популяции расстояния между бабочками уменьшаются, разности стремятся к нулю, и шаги локального поиска становятся микроскопическими. Популяция теряет способность покинуть окрестность текущего лучшего решения, даже если оно является локальным оптимумом.

Добавленный механизм работает следующим образом: с вероятностью 0.2, то есть примерно для каждой пятой бабочки при локальном поиске, случайным образом выбирается одна координата из всех доступных, и её значение заменяется числом, сгенерированным по нормальному распределению с центром в соответствующей координате лучшего глобального решения "cB". Параметр сигма равен 1, а значения ограничены допустимым диапазоном координаты. Это создаёт контролируемое возмущение: новое значение с высокой вероятностью окажется вблизи лучшего решения, но с некоторой вероятностью может оказаться достаточно далеко, чтобы вытолкнуть бабочку в новую область пространства поиска.

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

//==============================================================
      // Добавление шума к координатам лучшего решения
      // для предотвращения застревания в локальных экстремумах
      // этого нет в оригинальном BOA
      //==============================================================
      if (u.RNDprobab () < 0.2)
      {
        int ind = u.RNDminusOne (coords);
        a [i].c [ind] = u.GaussDistribution (cB [ind], rangeMin [ind], rangeMax [ind], 1);
      }
    }

    //----------------------------------------------------------------

Вот такие результаты без этого возмущения.

BOA|Butterfly Optimization Algorithm|50.0|0.9|0.5|0.8|
=============================
5 Hilly's; Func runs: 10000; result: 0.6635729348082547
25 Hilly's; Func runs: 10000; result: 0.3933240668190092
500 Hilly's; Func runs: 10000; result: 0.2670951838030907
=============================
5 Forest's; Func runs: 10000; result: 0.5030842340559138
25 Forest's; Func runs: 10000; result: 0.31195194496509265
500 Forest's; Func runs: 10000; result: 0.1922137682213402
=============================
5 Megacity's; Func runs: 10000; result: 0.32461538461538464
25 Megacity's; Func runs: 10000; result: 0.16953846153846158
500 Megacity's; Func runs: 10000; result: 0.10381538461538553
=============================
All score: 2.92921 (32.55%)

А такие получаем, когда добавляем шум к координатам лучшего решения.

BOA|Butterfly Optimization Algorithm|50.0|0.9|0.5|0.8|
=============================
5 Hilly's; Func runs: 10000; result: 0.7293075035168879
25 Hilly's; Func runs: 10000; result: 0.5005498140056686
500 Hilly's; Func runs: 10000; result: 0.2754406215727097
=============================
5 Forest's; Func runs: 10000; result: 0.94839177748601
25 Forest's; Func runs: 10000; result: 0.45575888873544235
500 Forest's; Func runs: 10000; result: 0.19904714403024765
=============================
5 Megacity's; Func runs: 10000; result: 0.5
25 Megacity's; Func runs: 10000; result: 0.24523076923076922
500 Megacity's; Func runs: 10000; result: 0.10603076923077022
=============================
All score: 3.95976 (44.00%)

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

Hilly

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

Forest

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

Megacity

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

Ackley

BOA на стандартной функции Ackey

Shaffer

BOA на стандартной функции Shaffer

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

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 search0,949480,847760,438572,235811,000000,923340,399882,323230,709230,634770,230911,574916,13468,15
2CLAcode lock algorithm (joo)0,953450,871070,375902,200420,989420,917090,316422,222940,796920,693850,193031,683806,10767,86
3AMOmanimal migration ptimization M0,903580,843170,462842,209590,990010,924360,465982,380340,567690,591320,237731,396755,98766,52
4(P+O)ES(P+O) evolution strategies0,922560,881010,400212,203790,977500,874900,319452,171850,673850,629850,186341,490035,86665,17
5CTAcomet tail algorithm (joo)0,953460,863190,277702,094350,997940,857400,339492,194840,887690,564310,105121,557125,84664,96
6TETAtime evolution travel algorithm (joo)0,913620,823490,319902,057010,970960,895320,293242,159520,734620,685690,160211,580525,79764,41
7SDSmstochastic diffusion search M0,930660,854450,394762,179880,999830,892440,196192,088460,723330,611000,106701,441035,70963,44
8ECBOenhanced_colliding_bodies_optimization0,934790,757470,324712,016970,974360,774460,230371,979190,889230,580610,152241,622085,61862,43
9BOAmbilliards optimization algorithm M0,957570,825990,252352,035901,000000,900360,305022,205380,735380,525230,095631,356255,59862,19
10AAmarchery algorithm M0,917440,708760,421602,047800,925270,758020,353282,036570,673850,552000,237381,463235,54861,64
11ESGevolution of social groups (joo)0,999060,796540,350562,146161,000000,828630,131021,959650,823330,553000,047251,423585,52961,44
12SIAsimulated isotropic annealing (joo)0,957840,842640,414652,215130,982390,795860,205071,983320,686670,493000,090531,270205,46960,76
13EOmextremal_optimization_M0,761660,772420,317471,851550,999990,767510,235272,002770,747690,539690,142491,429875,28458,71
14BBObiogeography based optimization0,949120,694560,350311,993990,938200,673650,256821,868670,746150,482770,173691,402615,26558,50
15ACSartificial cooperative search0,755470,747440,304071,806981,000000,888610,224132,112740,690770,481850,133221,305835,22658,06
16DAdialectical algorithm0,861830,700330,337241,899400,981630,727720,287181,996530,703080,452920,163671,319675,21657,95
17BHAmblack hole algorithm M0,752360,766750,345831,864930,935930,801520,271772,009230,650770,516460,154721,321955,19657,73
18ASOanarchy society optimization0,848720,746460,314651,909830,961480,791500,238031,991010,570770,540620,166141,277525,17857,54
19RFOroyal flush optimization (joo)0,833610,737420,346291,917330,894240,738240,240981,873460,631540,502920,164211,298675,08956,55
20AOSmatomic orbital search M0,802320,704490,310211,817020,856600,694510,219961,771070,746150,528620,143581,418355,00655,63
21TSEAturtle shell evolution algorithm (joo)0,967980,644800,296721,909490,994490,619810,227081,841390,690770,426460,135981,253225,00455,60
22BSAbacktracking_search_algorithm0,973090,545340,290981,809410,999990,585430,217471,802890,847690,369530,129781,347004,95955,10
23DEdifferential evolution0,950440,616740,303081,870260,953170,788960,166521,908650,786670,360330,029531,176534,95555,06
24SRAsuccessful restaurateur algorithm (joo)0,968830,634550,292171,895550,946370,555060,191241,692670,749230,440310,125261,314804,90354,48
25BObonobo_optimizer0,775650,638050,329081,742780,880880,763440,255731,900050,610770,498460,142461,251694,89554,38
26CROchemical reaction optimisation0,946290,661120,298531,905930,879060,584220,211461,674730,758460,426460,126861,311784,89254,36
27BIOblood inheritance optimization (joo)0,815680,653360,308771,777810,899370,653190,217601,770160,678460,476310,139021,293784,84253,80
28DOAdream_optimization_algorithm0,855560,700850,372801,929210,734210,489050,241471,464730,772310,473540,185611,431464,82553,62
29BSAbird swarm algorithm0,893060,649000,262501,804550,924200,711210,249391,884790,693850,326150,100121,120124,80953,44
30DEAdolphin_echolocation_algorithm0,759950,675720,341711,777380,895820,642230,239411,777460,615380,440310,151151,206844,76252,91
31HSharmony search0,865090,687820,325271,878180,999990,680020,095901,775920,620000,422670,054581,097254,75152,79
32SSGsaplings sowing and growing0,778390,649250,395431,823080,859730,624670,174291,658690,646670,441330,105981,193984,67651,95
33BCOmbacterial chemotaxis optimization M0,759530,622680,314831,697040,893780,613390,225421,732590,653850,420920,144351,219124,64951,65
34ABOafrican buffalo optimization0,833370,622470,299641,755480,921700,586180,197231,705110,610000,431540,132251,173784,63451,49
35(PO)ES(PO) evolution strategies0,790250,626470,429351,846060,876160,609430,195911,681510,590000,379330,113221,082554,61051,22
36FBAfractal-based Algorithm0,790000,651340,289651,730990,871580,568230,188771,628580,610770,460620,123981,195374,55550,61
37TSmtabu search M0,877950,614310,291041,783300,928850,518440,190541,637830,610770,382150,121571,114494,53650,40
38BSObrain storm optimization0,937360,576160,296881,810410,931310,558660,235371,725340,552310,290770,119140,962224,49849,98
39WOAmwale optimization algorithm M0,845210,562980,262631,670810,931000,522780,163651,617430,663080,411380,113571,188034,47649,74
40AEFAartificial electric field algorithm0,877000,617530,252351,746880,927290,726980,180641,834900,666150,116310,095080,877544,45949,55
41AEOartificial ecosystem-based optimization algorithm0,913800,467130,264701,645630,902230,437050,214001,553270,661540,308000,285631,255174,45449,49
42CAmcamel algorithm M0,786840,560420,351331,698590,827720,560410,243361,631490,648460,330920,134181,113564,44449,37
43ACOmant colony optimization M0,881900,661270,303771,846930,858730,586800,150511,596040,596670,373330,024720,994724,43849,31
44CMAEScovariance_matrix_adaptation_evolution_strategy0,762580,720890,000001,483470,820560,796160,000001,616720,758460,490770,000001,249234,34948,33
45BOAbutterfly_optimization_algorithm0,729300,500540,275441,505280,948390,455750,199041,603180,500000,245230,106030,851263,96044,00
RWrandom walk0,487540,321590,257811,066940,375540,219440,158770,753750,279690,149170,098470,527342,34826,09


Выводы

В основе алгоритма BOA лежит идея: каждая бабочка одновременно является и искателем, и источником аромата, сила которого определяется качеством найденного решения. Формула аромата, основанная на законе Стивенса из психофизики, обеспечивает плавный переход от равномерного исследования пространства на ранних итерациях к целенаправленной эксплуатации перспективных областей на поздних.

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

Тем не менее даже с исправленными формулами алгоритм продемонстрировал склонность к преждевременной сходимости. По мере схождения популяции разности между позициями бабочек уменьшаются, шаги локального поиска становятся пренебрежимо малыми, и популяция утрачивает способность покинуть окрестность текущего лучшего решения. Для компенсации этого недостатка был добавлен механизм стохастической мутации, отсутствующий в оригинальном описании: с небольшой вероятностью одна координата бабочки заменяется значением, сгенерированным по нормальному распределению с центром в лучшем найденном решении. Это позволило поднять результативность алгоритма с 32.5% до 44% на тестовом наборе функций.

Несмотря на улучшение, достигнутый результат не позволяет алгоритму войти в рейтинговую таблицу. Основная причина заключается в архитектурных ограничениях самого подхода. Механизм перемещения бабочек опирается всего на два режима — глобальный, направленный к единственному лучшему решению, и локальный, определяемый разностью позиций двух случайных агентов. Оба режима порождают шаги, масштабируемые квадратом случайного числа r² со средним значением около 0.33, что в сочетании с аромат-множителем порядка 0.5 даёт типичный шаг около 12% от расстояния до цели. Такая динамика слишком консервативна для эффективного исследования многомерных пространств с множеством локальных оптимумов.

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

tab

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

chart

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


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

Плюсы:

  1. Неплохие показатели на задачах типа Forest (гладкие с "острыми" экстремумами).

Минусы:

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

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

Обновления в едином испытательном стенде - поправил енумератор выбора алгоритмов (исправил выбор этих алгоритмов):

  • AO_CryStAlm (Crystal Structure Algorithm M)
  • AO_CoSO (Community of Scientist Optimization)



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

#ИмяТипОписание
1#C_AO.mqh
Включаемый файл
Родительский класс популяционных алгоритмов оптимизации
2#C_AO_enum.mqh
Включаемый файл
Перечисление популяционных алгоритмов оптимизации
3TestFunctions.mqh
Включаемый файл
Библиотека тестовых функций
4
TestStandFunctions.mqh
Включаемый файл
Библиотека функций тестового стенда
5
Utilities.mqh
Включаемый файл
Библиотека вспомогательных функций
6
CalculationTestResults.mqh
Включаемый файл
Скрипт для расчета результатов в сравнительную таблицу
7
Testing AOs.mq5
СкриптЕдиный испытательный стенд для всех популяционных алгоритмов оптимизации
8
Simple use of population optimization algorithms.mq5
Скрипт
Простой пример использования популяционных алгоритмов оптимизации без визуализации
9
Test_BOA.mq5
СкриптИспытательный стенд для BOA
Прикрепленные файлы |
BOA.zip (411.27 KB)
Машинное обучение и Data Science (Часть 36): Работа с несбалансированными финансовыми рынками Машинное обучение и Data Science (Часть 36): Работа с несбалансированными финансовыми рынками
Финансовые рынки не находятся в идеальном равновесии. Некоторые рынки демонстрируют бычий тренд, другие — медвежий, а третьи — флэт. Эта несбалансированная информация, используемая для обучения моделей машинного обучения, может вводить в заблуждение, поскольку рынки часто меняют направление. В этой статье мы обсудим несколько способов решения этой проблемы.
Знакомство с языком MQL5 (Часть 29): Освоение API и функции WebRequest в языке MQL5 (III) Знакомство с языком MQL5 (Часть 29): Освоение API и функции WebRequest в языке MQL5 (III)
В этой статье мы продолжаем осваивать API и WebRequest в языке MQL5, получая свечные данные из внешнего источника. Мы разберем ответ сервера, очистим данные и извлечем ключевые элементы – время открытия и значения OHLC для нескольких дневных свечей, подготовив все для дальнейшего анализа.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Разрабатываем мультивалютный советник (Часть 30): От торговой стратегии — к запуску мультивалютного советника Разрабатываем мультивалютный советник (Часть 30): От торговой стратегии — к запуску мультивалютного советника
Статья показывает полный цикл работы по созданию мультивалютного советника с использованием библиотеки Adwizard для MetaTrader 5: от подготовки окружения для создания проектов оптимизации до получения итоговых мультивалютных советников, объединяющих много экземпляров простой торговой стратегии. Разбираем настройку нужных входных параметров, соглашения об удобных именах файлов и запуск трёх экземпляров итоговых советников на разных торговых счетах с разными параметрами.