preview
Алгоритм опыления цветов — Flower Pollination Algorithm (FPA)

Алгоритм опыления цветов — Flower Pollination Algorithm (FPA)

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

Содержание

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


Введение

FPA (Flower Pollination Algorithm) — один из самых известных метаэвристических алгоритмов последнего десятилетия. Алгоритм был предложен Xin-She Yang и опубликован в 2012 г. У него много модификаций, а описание укладывается в две формулы и одну монетку. Именно поэтому он интересен трейдеру: это тот алгоритм, который скорее всего попадётся под руку, и его скорее всего запустят с параметрами по умолчанию.

Вот здесь и начинается проблема. С авторскими параметрами FPA на нашем стенде показывает 26% — уровень, близкий к случайному поиску. Трейдер, который подключил такой оптимизатор к своему советнику, получит набор параметров, найденный почти наугад, и будет считать его результатом оптимизации.

В этой статье мы перенесём FPA в MQL5 и разберёмся, какая из двух его операций действительно работает, а какая только расходует бюджет. Посмотрим, какими параметрами алгоритм поднимается с 26% до среднего уровня большинства алгоритмов. Отдельно разберём, почему в больших размерностях не помогает уже никакая настройка. Отдельно проверим, что улучшение честное: что оно не держится на особенностях тестового стенда.


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

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

Из этой метафоры автор выводит четыре правила:

  1. Перекрёстное опыление — это глобальный поиск, а перелёты опылителей подчиняются распределению Леви.
  2. Самоопыление — это локальный поиск.
  3. Вероятность опыления между двумя цветками тем выше, чем они более похожи (постоянство цветков).
  4. Выбор между глобальным и локальным опылением задаёт вероятность переключения p с небольшим перевесом в сторону локального.

Глобальное опыление в статье и в коде автора записано по-разному, статья:

x_i(t+1) = x_i(t) + L · (g − x_i(t)),   L > 0 — длина шага из распределения Леви
код:

x_i(t+1) = x_i(t) + L · (x_i(t) − g),   L — симметричное число по алгоритму Мантеньи

Здесь g — лучшее найденное решение, а L — случайный множитель из распределения Леви, свой для каждой координаты. Распределение Леви почти всегда даёт маленькие значения, но изредка очень большие. Поэтому цветок чаще всего смещается немного, а иногда улетает далеко. В коде автора L строится алгоритмом Мантеньи с индексом β = 1.5 и умножается на 0.01:

σ = ( Γ(1+β)·sin(πβ/2) / ( Γ((1+β)/2)·β·2^((β−1)/2) ) )^(1/β) ≈ 0.6966 при β = 1.5 u ~ N(0, σ²), v ~ N(0, 1) L = 0.01 · u / |v|^(1/β)

В статье L описан как положительная длина шага, и тогда формула действительно ведёт цветок к лучшему решению. В коде, который мы переносим, алгоритм Мантеньи даёт симметричное L: положительные и отрицательные значения выпадают одинаково часто. Поэтому знак разности ничего не меняет. Значит, по каждой координате цветок с равной вероятностью приближается к лучшему или удаляется от него. В такой реализации лучшее решение задаёт не направление шага, а его размах. Чем дальше цветок от лидера по какой-то координате, тем сильнее его встряхивает по этой координате. Сам лидер глобальным шагом не двигается вовсе: для него x_i − g = 0.

Локальное опыление использует два случайных цветка j и k из популяции:

x_i(t+1) = x_i(t) + ε · (x_j(t) − x_k(t)), ε ~ U(0, 1)

Случайное число ε одно на весь вектор, поэтому цветок сдвигается строго вдоль направления от k к j, на долю этого расстояния. Это та же операция, что и в дифференциальной эволюции, только без кроссовера: меняются сразу все координаты.

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

На рисунке 1 оба шага показаны на плоскости. Слева — облако пробных точек глобального шага: оно симметрично вокруг цветка, а не вытянуто в сторону лидера, и дальние выбросы идут вдоль одной оси. Справа — локальный шаг: все пробы лежат на одном отрезке, параллельном разности двух случайных цветков.

fpa_pollination_steps

Рисунок 1. Два способа получить пробную точку в FPA: глобальное опыление (слева) — встряска вокруг цветка, локальное (справа) — сдвиг вдоль разности двух соседей

Слева на рисунке: глобальное опыление (выполняется, когда rand > pSwitch): s_i = x_i + levyScale · L ⊙ (x_i − g), где для каждой координаты берётся своё число Леви L. Пунктирный прямоугольник показывает масштаб шага: ± levyScale · |x_i − g| по каждой координате. Распределение L симметрично, поэтому это встряска вокруг x_i, а не движение к лучшему решению g. Редкие дальние прыжки идут вдоль отдельных осей, а лучший цветок (x_i = g) этим шагом не сдвигается.

Справа на рисунке: локальное опыление (выполняется, когда rand ≤ pSwitch): s_i = x_i + ε · (x_j − x_k), где одно ε ~ U(0, 1) действует на все координаты. Все пробные точки лежат на одном отрезке, параллельном x_j − x_k. Направление и размер шага задают два случайных цветка; в начале поиска шаг охватывает большую часть области сразу по всем координатам.

Наша реализация следует коду автора и выглядит следующим образом:

Вход: n (размер популяции), pSwitch (вероятность локального шага),
      levyScale (масштаб шага Леви), β = 1.5

1. Инициализация
   для каждого цветка i:
      x_i = случайная точка в области поиска
      f_i = F(x_i)
   g = лучший из x_i

2. Главный цикл, пока не исчерпан бюджет
   для каждого цветка i:
      если rand > pSwitch:                       // глобальное опыление
         для каждой координаты c:
            L = levyScale · Levy(β)               // свой L на каждую координату
            s_i[c] = x_i[c] + L · (x_i[c] − g[c])
      иначе:                                      // локальное опыление
         ε = rand(0, 1)                             // один на весь вектор
         j, k = два разных случайных цветка
         s_i = x_i + ε · (x_j − x_k)
      прижать s_i к границам области поиска
      f_new = F(s_i)
      если f_new не хуже f_i:   x_i = s_i,  f_i = f_new
      если f_new не хуже F(g): g = s_i

3. Результат: g

Levy(β), алгоритм Мантеньи:
   u = N(0,1) · σ,   v = N(0,1)
   вернуть u / |v|^(1/β)

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

Первое — условие переключения. В статье глобальный шаг выполняется при rand < p, а в коде — при rand > p. В итоге при p = 0.8 статья делает 80% глобальных шагов, а код — 20%. Код при этом согласуется с правилом 4 (перевес в сторону локального опыления), а псевдокод статьи — не согласуется.

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

Третье — масштаб шага Леви. В статье множитель γ не задан, в коде он равен 0.01. В нашей реализации это параметр levyScale.

Четвёртое — направление глобального шага. В статье длина шага L положительна и умножается на (g − x_i), так что цветок движется к лучшему решению. В коде L симметрично (алгоритм Мантеньи) и умножается на (x_i − g), поэтому знак шага случаен и глобальный шаг оказывается встряской вокруг цветка, а не движением к лидеру. Наша реализация следует коду.

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

В нашей реализации база локального шага вынесена в переключатель fromCode, а условие оставлено как в коде. Вариант статьи при этом воспроизводится без лишнего флага: (rand < 0.8 → глобальный) — то же самое, что (rand > 0.2 → глобальный), то есть pSwitch = 0.2 и fromCode = 0.

Наша реализация в коде. Алгоритм реализован классом C_AO_FPA, наследником базового C_AO. Одна итерация FPA равна одной эпохе стенда: каждый цветок делает один шаг и получает одну оценку. Отдельный массив структур не понадобился: всё состояние цветка укладывается в поля агента, которые уже есть в базовом классе.

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class C_AO_FPA : public C_AO
  {
public:
                    ~C_AO_FPA() {}
                     C_AO_FPA()
     {
      ao_name = "FPA";
      ao_desc = "Flower Pollination Algorithm";
      ao_link = "https://www.mql5.com/ru/articles/25138";

      popSize   = 10;     // размер популяции
      pSwitch   = 0.2;    // вероятность локального шага
      levyScale = 0.5;    // масштаб шага Леви
      fromCode  = false;  // база локального шага

      ArrayResize(params, 4);
      params [0].name = "popSize";
      params [0].val = popSize;
      params [1].name = "pSwitch";
      params [1].val = pSwitch;
      params [2].name = "levyScale";
      params [2].val = levyScale;
      params [3].name = "fromCode";
      params [3].val = fromCode ? 1.0 : 0.0;
     }

   void               SetParams()
     {
      popSize   = (int)params [0].val;
      pSwitch   = params [1].val;
      levyScale = params [2].val;
      fromCode  = params [3].val != 0.0;

      if(popSize < 2)
         popSize = 2;   // локальному шагу нужны два разных агента
      if(pSwitch < 0.0)
         pSwitch = 0.0;
      if(pSwitch > 1.0)
         pSwitch = 1.0;
      if(levyScale < 0.0)
         levyScale = 0.0;

      params [0].val = popSize;
      params [1].val = pSwitch;
      params [2].val = levyScale;
      params [3].val = fromCode ? 1.0 : 0.0;
     }

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

   void               Moving();
   void               Revision();

   //----------------------------------------------------------------
   double             pSwitch;     // вероятность локального опыления
   double             levyScale;   // масштаб шага Леви
   bool               fromCode;    // база локального шага: true — a[i].c, false — a[i].cB

private:
   void               GlobalPollination(int i);
   void               LocalPollination(int i);

   double             Levy();
   double             Gauss();
   double             RndOpen();
  };

Init сводится к стандартной инициализации базового класса. Своих массивов и расписаний у алгоритма нет, а σ для β = 1.5 посчитано заранее и зашито константой.

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

   return true;
  }

Генератор шагов Леви. Сам шаг Леви строится из двух независимых нормальных чисел, которые даёт преобразование Бокса — Мюллера. Здесь есть две тонкости, связанные с тем, что стандартный генератор MQL5 выдаёт целые числа от 0 до 32767. Во-первых, логарифм нуля не определён, поэтому RndOpen перевыбирает нулевое значение. Во-вторых, RNDprobab иногда возвращает ровно 1.0, и тогда нормальное число равно нулю. Для числителя это безвредно, а для знаменателя означало бы деление на ноль, поэтому v перевыбирается.

//+------------------------------------------------------------------+
//| равномерное на (0; 1]                                            |
//+------------------------------------------------------------------+
double C_AO_FPA::RndOpen()
  {
   double r;
   do
      r = u.RNDprobab();
   while(r <= 0.0);
   return r;
  }

Преобразование Бокса — Мюллера.

//+------------------------------------------------------------------+
//| N(0,1), Box-Muller                                               |
//+------------------------------------------------------------------+
double C_AO_FPA::Gauss()
  {
   return sqrt(-2.0 * log(RndOpen())) * cos(2.0 * M_PI * u.RNDprobab());
  }

Алгоритм Мантеньи. При 15-битном генераторе наименьшее ненулевое |v| около 4·10⁻⁷, поэтому множитель |v|^(−2/3) не превышает примерно 2·10⁴. Тяжёлый хвост распределения сохраняется, а бесконечностей не возникает.

//+------------------------------------------------------------------+
//| шаг Леви, алгоритм Мантенья: u / |v|^(1/beta)                    |
//| v = 0 возможно (RndOpen = 1 даёт нулевой радиус) — перевыборка   |
//+------------------------------------------------------------------+
double C_AO_FPA::Levy()
  {
   double uu = Gauss() * 0.6965745025576968;
   //double uu = Gauss() * FPA_SIGMA;
   double v;
   do
      v = Gauss();
   while(v == 0.0);

   return uu / pow(fabs(v), 1.0 / 1.5);
   //return uu / pow(fabs(v), 1.0 / FPA_BETA);
  }

Глобальное опыление. Метод строит пробную точку от принятого решения цветка. Для каждой координаты берётся своё число Леви, и размах сдвига по этой координате пропорционален расстоянию до лучшего решения. Вышедшая за границу координата прижимается к ней, как в функции simplebounds автора.

//+------------------------------------------------------------------+
//| глобальное опыление: s_i = x_i + levyScale * L (.) (x_i - g)     |
//+------------------------------------------------------------------+
void C_AO_FPA::GlobalPollination(int i)
  {
   double x;

   for(int c = 0; c < coords; c++)
     {
      x = a [i].cB [c] + levyScale * Levy() * (a [i].cB [c] - cB [c]);
      a [i].c [c] = u.SeInDiSp(x, rangeMin [c], rangeMax [c], rangeStep [c]);
     }
  }

Локальное опыление. Метод выбирает два разных случайных цветка и сдвигает базу на долю их разности. Как и в коде автора, сам цветок i не исключается из выбора: в оригинале партнёры берутся из randperm(n), и i там может оказаться. База шага задаётся переключателем fromCode.

//+------------------------------------------------------------------+
//| локальное опыление: s_i = base_i + eps * (x_j - x_k)             |
//+------------------------------------------------------------------+
void C_AO_FPA::LocalPollination(int i)
  {
   double eps = u.RNDprobab();

   int j = u.RNDminusOne(popSize);
   int k;
   do
      k = u.RNDminusOne(popSize);
   while(k == j);

   double base, x;

   for(int c = 0; c < coords; c++)
     {
      base = fromCode ? a [i].c [c] : a [i].cB [c];
      x    = base + eps * (a [j].cB [c] - a [k].cB [c]);
      a [i].c [c] = u.SeInDiSp(x, rangeMin [c], rangeMax [c], rangeStep [c]);
     }
  }

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

//+------------------------------------------------------------------+
//|                            Moving                                |
//+------------------------------------------------------------------+
void C_AO_FPA::Moving()
  {
//--- первый прогон: стартовая популяция
   if(!revision)
     {
      for(int i = 0; i < popSize; i++)
         for(int c = 0; c < coords; c++)
            a [i].c [c] = u.SeInDiSp(u.RNDfromCI(rangeMin [c], rangeMax [c]), rangeMin [c], rangeMax [c], rangeStep [c]);
      return;
     }

//--- одна итерация FPA = одна эпоха стенда
   for(int i = 0; i < popSize; i++)
     {
      if(u.RNDprobab() > pSwitch)
         GlobalPollination(i);
      else
         LocalPollination(i);
     }
  }

Revision. После оценки каждый цветок сравнивает пробную точку со своим принятым решением и переходит туда, если она не хуже. Равные значения принимаются, как и в оригинале (Fnew <= Fitness), и это позволяет цветку дрейфовать по плоским участкам, что полезно на ступенчатой Megacity. Стенд максимизирует, поэтому знак сравнения развёрнут. На первом проходе все a[i].fB равны −DBL_MAX, так что стартовая популяция принимается целиком без отдельной ветки. Обновление принятых решений в Revision — самая важная строка всей реализации. Без неё поля a[i].cB остаются нулевыми, и оба оператора начинают строить пробы от начала координат. Такой алгоритм всё ещё что-то находит, но уже за счёт положения начала координат относительно области поиска, а не за счёт популяции.

//+------------------------------------------------------------------+
//|                           Revision                               |
//+------------------------------------------------------------------+
void C_AO_FPA::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);
        }
     }

//--- первый проход: a[i].fB = -DBL_MAX, стартовая популяция принята целиком
   if(!revision)
      revision = true;
  }


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

Все прогоны выполнены на стандартном стенде серии: функции Hilly, Forest и Megacity в трёх размерностях (5, 25 и 500 функций, то есть 10, 50 и 1000 координат), 10 000 вычислений функции и 10 повторов.

Отдельной оценки разброса для FPA мы не делали. По повторным прогонам других алгоритмов серии на этом же стенде разброс итоговой оценки составляет порядка 1–2 процентных пунктов, поэтому меньшие различия мы не считаем значимыми.

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

От авторских параметров к рабочим. Начали мы с двух канонических вариантов: точной копии кода автора и варианта псевдокода статьи. Оба оказались слабыми. Причина — множитель 0.01 в шаге Леви: с ним глобальный шаг сдвигает цветок в среднем на сотые доли расстояния до лидера и практически неподвижен. Увеличение масштаба до 0.5–1.0 поднимает алгоритм сразу на 13–16 пунктов.

popSize | pSwitch | levyScale | fromCode

Вариант

5 функций

25 функций

500 функций

Итог

50          | 0.8         | 0.01         | 1

код автора

0.422

0.223

0.140

26.16%

50          | 0.2         | 0.01         | 0

статья автора

0.479

0.259

0.145

29.41%

50          | 0.1         | 1.0           | 1

крупный шаг, база кода

0.770

0.255

0.132

38.57%

50          | 0.1         | 1.0           | 0

крупный шаг, база статьи

0.779

0.264

0.133

39.19%

50          | 0.0         | 0.5           | 0

только глобальный шаг

0.871

0.278

0.133

42.72%

Весь выигрыш сосредоточен в малой размерности: среднее по 5 функциям вырастает с 0.42 до 0.87. На 25 функциях все варианты лежат в узком коридоре, а на 500 функциях крупный шаг даже немного проигрывает мелкому. Разница между базой кода и базой статьи при крупном шаге в пределах шума: локальных шагов там всего 10%, и fromCode влияет только на них.

Почему большая размерность не поддаётся. Объяснение кроется в тяжёлом хвосте распределения Леви. На одну координату дальний прыжок выпадает редко, но глобальный шаг применяется ко всем координатам сразу. В таблице — сколько координат за один ход в среднем сдвигается больше чем на 3 и больше чем на 10 расстояний до лидера при levyScale = 1:

Размерность

Координат

Сдвиг больше 3 расстояний

Сдвиг больше 10 расстояний

5 функций

10

0.8

0.13

25 функций

50

3.8

0.6

500 функций

1000

76

12.5

На 10 координатах большинство ходов аккуратные, и редкий дальний прыжок по одной координате помогает выбраться из ловушки. На 1000 координатах в каждом ходе десятки координат улетают далеко и часто прижимаются к границе, так что такой ход почти всегда оказывается хуже исходной точки и отбрасывается. Уменьшение levyScale делает хвост безопасным, но и шаг становится слишком маленьким. Смелость и разрушительность растут вместе, и ни одно из проверенных значений параметра не дало сразу и то, и другое.

Локальное опыление работает только в малой популяции. Самый неожиданный результат касается оператора, который автор считал основным: в коде при p = 0.8 на него приходится 80% бюджета. Если локальный шаг выключить совсем (pSwitch = 0), результат не падает. Чистое сравнение есть только для 10 цветков:

popSize

pSwitch 0.1

pSwitch 0.2

pSwitch 0.0

50

40.54%

—

42.72%

20

44.97%

—

45.23%

10

—

49.16%

46.00%

(levyScale 0.5, fromCode 0; в прогонах с pSwitch 0.1 была включена ориентация разности от худшего к лучшему, см. ниже.)

Вероятная причина слабости локального шага в большой популяции в том, что он на самом деле не локальный. Разность двух случайных цветков в начале поиска сравнима с размером всей области, и этот ход оказывается самым крупным в алгоритме. Он меняет все координаты разом, а его направление связано с двумя случайными цветками, а не с тем, где находится сам цветок. При fromCode = 1 к этому добавляется старт от отвергнутой пробы.

В малой популяции картина меняется. При 10 цветках 20% локальных шагов дают 49.16% против 46.00% без них, и это больше типичного разброса между прогонами. Прибавка приходится на 25 и 500 функций: среднее по 25 функциям растёт с 0.414 до 0.487, по 500 — с 0.141 до 0.160. Когда цветков мало, их разности уже соизмеримы с окрестностью лучшего решения, и шаг вдоль разности двух соседей превращается в настоящую локальную доводку. В большой популяции те же разности, по-видимому, остаются крупными и только тратят бюджет.

Меньше цветков — лучше, но до предела. Без локального шага FPA сводится к набору независимых жадных поисковиков, которые общаются только через масштаб шага. Бюджет фиксирован (10 000 оценок), поэтому таблица сравнивает не размер популяции сам по себе, а соотношение числа цветков и числа итераций на каждый. Чем меньше цветков, тем больше итераций приходится на каждый, и на 25 функциях это даёт заметный рост (pSwitch 0, levyScale 0.5, fromCode 0):

popSize

5 функций

25 функций

500 функций

Итог

50

0.871

0.278

0.133

42.72%

20

0.864

0.357

0.136

45.23%

10

0.825

0.414

0.141

46.00%

5

0.767

0.476

0.147

46.35%

3

0.615

0.450

0.159

40.78%

Провал при трёх цветках объясняется особенностью глобального шага, о которой говорилось выше: лидер им не двигается. Его оценка каждую итерацию тратится впустую: стенд вычисляет фитнес всех агентов в каждой эпохе, в том числе точки, которая не изменилась. При 50 цветках это 2% бюджета, при 10 — 10%, при трёх — уже треть. К тому же окрестность лучшего решения — самое перспективное место — никто прицельно не исследует.

Проверенные правки, не вошедшие в итоговую версию. Две правки мы проверили и не стали включать в итоговую версию. Обе полезно знать тем, кто захочет сам доработать алгоритм.

Первая — ориентация локального шага от худшего к лучшему: из двух партнёров j становится лучшим, и разность всегда указывает в его сторону. Локальный шаг это не спасло: при pSwitch 0.1 результат всё равно не выше, чем без локальных шагов вообще. Вектор от x_k к x_j говорит, куда лучше двигаться рядом с этими двумя точками. Цветок i может сидеть в другой впадине, и там это направление почти ничего не значит.

Вторая — разморозка лидера: цветок, который держит лучшее решение, берёт масштаб глобального шага от расстояния до случайного соседа, а не до себя. При малой популяции она стабильно поднимает среднюю размерность, но итоговая оценка меняется в пределах разброса (pSwitch 0, levyScale 0.5, fromCode 0):

popSize

Без правки

С правкой

25 функций: без / с

50

42.72%

41.63%

0.278/0.286

10

46.00%

47.74%

0.414/0.444

5

46.35%

46.97%

0.476/0.515

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

Итоговая конфигурация. От авторских параметров кода (26.16%) итоговая конфигурация отличается на 23 пункта. Средние по размерностям: 0.828 на 5 функциях, 0.487 на 25 и 0.160 на 500. Лучше всего алгоритм решает Forest на 5 функциях (0.939), хуже всего — Forest на 500 (0.074).

FPA|Flower Pollination Algorithm|10.0|0.2|0.5|0.0|
=============================
5 Hilly's; Func runs: 10000; result: 0.7853149391594381
25 Hilly's; Func runs: 10000; result: 0.5297058103938198
500 Hilly's; Func runs: 10000; result: 0.28144007940836896
=============================
5 Forest's; Func runs: 10000; result: 0.9385005907527866
25 Forest's; Func runs: 10000; result: 0.5048080878385968
500 Forest's; Func runs: 10000; result: 0.07367996881297378
=============================
5 Megacity's; Func runs: 10000; result: 0.7600000000000001
25 Megacity's; Func runs: 10000; result: 0.4253333333333333
500 Megacity's; Func runs: 10000; result: 0.12562666666666772
=============================
All score: 4.42441 (49.16%)

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

У FPA оба оператора разностные: x_i − g в глобальном шаге и x_j − x_k в локальном. Число Леви берётся своё для каждой координаты, а единственный общий скаляр ε лишь сохраняет направление разности двух реальных решений и не переносит значения между координатами. Поэтому мы ждём, что оба теста покажут честный результат. 

Оговоримся: эти тесты проверяют только две названные особенности стенда. Устойчивость к шуму, к разному масштабу и корреляции параметров, к ограничениям и нестационарности целевой функции они не проверяют.

Стенд со сдвигом. Область поиска сдвигается на 15 своих ширин, а перед вычислением функции сдвиг вычитается. Ландшафт остаётся прежним, меняется только положение начала координат: оно оказывается далеко за пределами области поиска. Алгоритм, чей результат здесь падает, опирался на положение нуля, а не на ландшафт. Для трейдера это прямо важно: диапазон вроде ценового уровня 1.0800–1.0900 лежит очень далеко от нуля относительно своей ширины.

FPA|Flower Pollination Algorithm|10.0|0.2|0.5|0.0|   ShiftFactor = 15.0
=============================
5 Hilly's; Func runs: 10000; result: 0.8452675640019767
25 Hilly's; Func runs: 10000; result: 0.5106539156036868
500 Hilly's; Func runs: 10000; result: 0.2800490332964667
=============================
5 Forest's; Func runs: 10000; result: 0.9117798655331132
25 Forest's; Func runs: 10000; result: 0.5270337901402008
500 Forest's; Func runs: 10000; result: 0.07346428068618865
=============================
5 Megacity's; Func runs: 10000; result: 0.7106666666666668
25 Megacity's; Func runs: 10000; result: 0.40186666666666665
500 Megacity's; Func runs: 10000; result: 0.12304000000000097
=============================
All score: 4.38382 (48.71%)

Композитный тест. Пары координат по очереди отдаются пяти разным функциям (Hilly, Forest, Megacity, Peaks, Skin) со своими диапазонами и положением оптимума, а фитнес цветка равен среднему по всем парам.

Любой выигрыш от одинаковости пар здесь исчезает. Если результат на композите заметно ниже результата на стенде в той же размерности, это признак того, что алгоритм пользовался одинаковостью пар. Обратное означает лишь отсутствие такого признака.

FPA|Flower Pollination Algorithm|10.0|0.2|0.5|0.0|
=============================
Composite anti-cheat test: Hilly + Forest + Megacity + Peaks + Skin
Pairs: 5; Coordinates: 10; Epochs: 1000; Repeats: 10
=============================
Run 1/10: 0.8546058922251879
Run 2/10: 0.8510964794303302
Run 3/10: 0.9809875328848362
Run 4/10: 0.8212067160441678
Run 5/10: 0.9839281875997858
Run 6/10: 0.8510964795233779
Run 7/10: 0.9866663121593137
Run 8/10: 0.9586587260729121
Run 9/10: 0.852831708770734
Run 10/10: 0.8528317077249306
=============================
Average result: 0.8993909742 (89.94%)
=============================

Визуализация работы алгоритма FPA.

Hilly

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

Forest

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

Megacity

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

Ackley

FPA на функции Ackley

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

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


Выводы

С параметрами автора FPA находится на уровне случайного поиска: 26% по коду и 29% по статье. Трейдер, который возьмёт его из библиотеки как есть, получит параметры советника, найденные почти наугад. Причина не в идее алгоритма, а в двух числах: множитель 0.01 делает глобальный шаг неподвижным, а p = 0.8 отдаёт большую часть бюджета локальному шагу, который в большой популяции ничего не показал.

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

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

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

tab

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

chart

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


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

Плюсы

  • Очень простой алгоритм: два оператора, никаких сортировок и расписаний.
  • Хорошие результаты в малой размерности, в том числе на ступенчатой Megacity.
  • Оба оператора разностные, без формул, зависящих от начала координат или диагонали области.

Минусы

  • Авторские параметры дают уровень случайного поиска, алгоритм нужно перенастраивать.
  • Локальное опыление полезно только в малой популяции: при 20–50 цветках пользы от него не обнаружено.
  • На 1000 координатах результат не выше случайного поиска при любых настройках.


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


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

#ИмяТипОписание
1#C_AO.mqh
Включаемый файл
Родительский класс популяционных алгоритмов оптимизации
2#C_AO_enum.mqh
Включаемый файл
Перечисление популяционных алгоритмов оптимизации
3TestFunctions.mqh
Включаемый файл
Библиотека тестовых функций
4
TestStandFunctions.mqh
Включаемый файл
Библиотека функций тестового стенда
5TestStand3D.mqhВключаемый файл3D-панель визуализации для тестового стенда 
6Utilities.mqh
Включаемый файл
Библиотека вспомогательных функций
7CalculationTestResults.mqh
Включаемый файл
Скрипт для расчёта результатов в сравнительную таблицу
8Test_AO_All.mq5
СкриптЕдиный испытательный стенд для всех популяционных алгоритмов оптимизации
9Test_AO_AntiCheatСкриптТест на читерство алгоритмов оптимизации
10Simple use of population optimization algorithms.mq5
Скрипт
Простой пример использования популяционных алгоритмов оптимизации без визуализации
11Test_AO_FPA.mq5
СкриптИспытательный стенд для FPA
Прикрепленные файлы |
FPA.zip (464.08 KB)
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Создание графической библиотеки для MetaTrader 5 по образцу Cairo (Часть 2): Точки, контуры и путь Создание графической библиотеки для MetaTrader 5 по образцу Cairo (Часть 2): Точки, контуры и путь
Во второй части мы добавим слой геометрии в графическую библиотеку для MetaTrader 5, создаваемую по образцу Cairo. В ней определяется путь из точек с координатами двойной точности, сгруппированных в контуры, сохраняется признак того, является ли контур открытым или замкнутым, а вершины хранятся в плоском массиве с индексами начала контуров. Мы реализуем методы MoveTo, LineTo и Close, добавим базовые вспомогательные методы для построения фигур и демонстрационный пример, который отображает построенную геометрию для визуальной проверки и повторного использования.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Автоматизация графических паттернов в MQL5 (Часть 2): Паттерны "двойная вершина" и "двойное дно" Автоматизация графических паттернов в MQL5 (Часть 2): Паттерны "двойная вершина" и "двойное дно"
Мы создадим надежный алгоритм обнаружения паттернов "двойная вершина" и "двойное дно" на языке MQL5. Сначала он подтвердит тренд на H4, а затем проверит шесть условий (равенство точек, положение линии шеи, порядок следования, ширину, высоту и допуски на основе среднего истинного диапазона, ATR). Пробой линии шеи определяется на таймфрейме графика, а машина состояний с тремя состояниями гарантирует, что по каждому паттерну сделка совершается только один раз. Проекция высоты паттерна позволяет определить четкий целевой уровень выхода.