preview
Алгоритм бактериальной эволюции Нумаоки для быстрой адаптации (NBE)

Алгоритм бактериальной эволюции Нумаоки для быстрой адаптации (NBE)

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

Содержание

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


Введение

В предыдущей статье, разбирая бактериальный эволюционный алгоритм, мы наткнулись на терминологическую развилку и одну её ветвь сознательно отложили. Напомню суть: под названием "бактериальный эволюционный алгоритм" в литературе живут два совершенно разных метода. Первый — оптимизатор Нава и Фурухаси 1999 года с операторами бактериальной мутации и переноса генов, который мы подробно реализовали и протестировали. Второй — историческая модель Тисато Нумаоки 1996 года под названием Bacterial Evolution Algorithm for Rapid Adaptation. Именно ей посвящена эта статья, и сразу предупрежу: это совсем другой зверь, и подходить к нему с привычной меркой оптимизатора статических функций было бы ошибкой.

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

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

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

Отсюда вытекает и честное предупреждение, которым стоит предварить всё дальнейшее. Главную силу модели Нумаоки — быструю реадаптацию к подвижному оптимуму — статический тестовый стенд попросту не создает нагрузки. Для неподвижной цели нет того перехода, который энергетический буфер призван пережить. Поэтому в этой статье мы аккуратно реинтерпретируем модель как оптимизатор в нашем фреймворке C_AO и честно прогоним её на стандартных функциях Hilly, Forest и Megacity. Это нужно для сопоставимости с остальной серией и ради того, чтобы увидеть, чего модель стоит вне своей стихии. А затем, в следующей, поставим перед ней ту задачу, для которой она создавалась, — стенд с дрейфующим оптимумом, — и посмотрим, проявит ли себя там энергетический механизм, который на статике выглядит избыточным. 


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

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

Отбор работает не через сортировку и отсечение худших, как в большинстве эволюционных алгоритмов, а через гибель и деление, и работает асинхронно. Бактероид гибнет, когда его энергия исчерпывается. Кроме того, с некоторой вероятностью он может погибнуть средово — независимо от своей приспособленности, что прямо отражает идею Нумаоки об отборе, осуществляемом средой. Освободившееся место занимает потомок энергичного бактероида. При делении родитель отдаёт потомку часть своей энергии и порождает его мутированную копию рядом с собой, в той же удачной области. Так возникает непрерывный steady-state оборот: неприспособленные и невезучие выбывают, приспособленные размножаются вокруг хороших точек, и популяция всё время обновляется, не застывая.

nbe_cycle

Рисунок 1. Оборот популяции бактероидов как механизм отбора.

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

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

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

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

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

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

nbe_plasmid

Рисунок 2. Плазмидный (горизонтальный) перенос.

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

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

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

инициализировать популяцию случайно; задать стартовую энергию E0
оценить; зафиксировать энергию
пока не исчерпан бюджет (одна эпоха = один шаг):

    # --- ход каждого бактероида ---
    для каждого бактероида i:
        с вероятностью pPlasmid:
            получить блок координат от донора (рулетка по энергии)   # перенос
        иначе:
            сместить координаты гауссом с затухающим шагом           # хемотаксис
    оценить всех бактероидов

    # --- приёмка хода ---
    для каждого i:
        если был перенос:  принять безусловно
        если было движение: принять, только если не хуже (greedy)

    # --- энергобаланс ---
    для каждого i:
        E_i += адаптированность_i - cost        # приход популяционно-относительный
        ограничить E_i сверху потолком

    # --- гибель и деление (асинхронный отбор) ---
    для каждого i:
        если E_i <= 0 или случайно с вероятностью pDeath: пометить мёртвым
    для каждого освободившегося слота:
        родитель <- живой бактероид (рулетка по энергии)
        потомок  <- мутированная копия родителя рядом
        энергию родителя разделить пополам между ним и потомком

    обновить глобально лучшее решение

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

Реализуем алгоритм. Бактероид представлен структурой с тремя полями — генотипом, приспособленностью и энергией:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
struct S_Bacteroid
  {
   double c           [];   // генотип (валидные координаты)
   double             f;    // фитнес
   double             E;    // энергия

   void               Init(int coords)
     {
      ArrayResize(c, coords);
      f = -DBL_MAX;
      E = 0.0;
     }
  };

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

Сама колония — это массив таких структур bro[] размера popSize. Рядом с ним живут три служебных целочисленных массива. Массив actType[] запоминает, что именно сделал каждый бактероид на текущей эпохе — перенос или движение, — поскольку от этого зависит правило приёмки в Revision. Массив allIdx[] содержит индексы от нуля до popSize-1 и служит пулом для выбора донора плазмидного переноса. Массив living[] заполняется в каждом Revision живыми бактероидами и используется как пул для выбора родителя при делении.

//+------------------------------------------------------------------+
//|                          Class                                   |
//+------------------------------------------------------------------+
class C_AO_NBE : public C_AO
  {
public:
                    ~C_AO_NBE() {}
                     C_AO_NBE()
     {
      ao_name = "NBE";
      ao_desc = "Numaoka Bacterial Evolution";
      ao_link = "https://www.mql5.com/ru/articles/23267";

      popSize   = 30;     // число бактероидов
      stepSize  = 0.1;    // стартовый масштаб движения (доля диапазона)
      pPlasmid  = 0.1;    // вероятность плазмидного переноса на бактероид/эпоху
      transFrac = 0.2;    // длина плазмиды как доля coords (0..1]
      cost      = 0.5;    // метаболическая стоимость (приход в [0,1])
      pDeath    = 0.0005; // «средовая» вероятность гибели на бактероид/эпоху

      ArrayResize(params, 6);
      params [0].name = "popSize";
      params [0].val = popSize;
      params [1].name = "stepSize";
      params [1].val = stepSize;
      params [2].name = "pPlasmid";
      params [2].val = pPlasmid;
      params [3].name = "transFrac";
      params [3].val = transFrac;
      params [4].name = "cost";
      params [4].val = cost;
      params [5].name = "pDeath";
      params [5].val = pDeath;
     }

   void               SetParams()
     {
      popSize   = (int)params [0].val;
      stepSize  =      params [1].val;
      pPlasmid  =      params [2].val;
      transFrac =      params [3].val;
      cost      =      params [4].val;
      pDeath    =      params [5].val;

      //--- предохранители
      if(popSize   < 1)
         popSize   = 1;
      if(stepSize  <= 0.0)
         stepSize  = 0.001;
      if(pPlasmid  < 0.0)
         pPlasmid  = 0.0;
      if(pPlasmid  > 1.0)
         pPlasmid  = 1.0;
      if(transFrac <= 0.0)
         transFrac = 0.01;
      if(transFrac >  1.0)
         transFrac = 1.0;
      if(cost      < 0.0)
         cost      = 0.0;
      if(pDeath    < 0.0)
         pDeath    = 0.0;
      if(pDeath    > 1.0)
         pDeath    = 1.0;
     }

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

   void               Moving();
   void               Revision();

   //--- видимые параметры
   double             stepSize;    // стартовый масштаб движения
   double             pPlasmid;    // вероятность переноса
   double             transFrac;   // доля coords для плазмиды
   double             cost;        // метаболическая стоимость
   double             pDeath;      // средовая гибель

private:
   //--- данные (массивы структур)
   S_Bacteroid        bro    [];   // [popSize] — бактероиды
   int                actType [];  // [popSize] 0=движение, 1=перенос (на эпоху)
   int                allIdx [];   // [popSize] — 0..popSize-1
   int                living [];   // [popSize] — живые в текущем Revision

   int                transLen;    // длина плазмиды (из transFrac)

   //--- прогресс прогона (для отжига шага)
   int                epochsDen;
   int                epochNow;

   //--- константы
   double             E0;          // стартовая энергия
   double             Emax;        // потолок энергии
   double             STEP_MIN_RATIO;

   //--- вспомогательные
   double             Gauss();
   double             StepNow();
   int                RouletteByE(int &pool [], int n, int exclude);
   void               Respawn(int i);
  };
//+------------------------------------------------------------------+

Метод Init после стандартной инициализации каркаса вычисляет абсолютную длину плазмиды из доли transFrac, готовит знаменатель для отжига шага. Далее метод задаёт энергетические константы — стартовую энергию, потолок энергии и нижнюю долю шага при отжиге — и заполняет стартовую популяцию. Создание одного бактероида вынесено в отдельный метод Respawn, который присваивает случайный генотип и стартовую энергию:

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

//--- длина плазмиды
   transLen = (int)MathRound(transFrac * coords);
   if(transLen < 1)
      transLen = 1;
   if(transLen > coords)
      transLen = coords;

//--- прогресс / константы
   epochsDen      = (epochsP > 1) ? epochsP - 1 : 1;
   epochNow       = 0;
   E0             = 1.0;
   Emax           = 5.0;
   STEP_MIN_RATIO = 0.025;

//--- буферы
   ArrayResize(bro,     popSize);
   ArrayResize(actType, popSize);
   ArrayResize(allIdx,  popSize);
   ArrayResize(living,  popSize);

   for(int i = 0; i < popSize; i++)
     {
      bro [i].Init(coords);
      allIdx [i] = i;
     }

//--- стартовая популяция
   for(int i = 0; i < popSize; i++)
      Respawn(i);

   return true;
  }

Этот же метод используется как аварийный механизм: если в какой-то момент погибнет вся колония разом, Revision просто возрождает её целиком случайной.

//+------------------------------------------------------------------+
//|   Respawn — случайный генотип, стартовая энергия                 |
//+------------------------------------------------------------------+
void C_AO_NBE::Respawn(int i)
  {
   for(int c = 0; c < coords; c++)
      bro [i].c [c] = u.SeInDiSp(u.RNDfromCI(rangeMin [c], rangeMax [c]),
                                 rangeMin [c], rangeMax [c], rangeStep [c]);
   bro [i].f = -DBL_MAX;
   bro [i].E = E0;
  }

Gauss возвращает стандартное нормальное число по преобразованию Бокса — Мюллера и используется и в движении, и в делении.

//+------------------------------------------------------------------+
//|         Gauss — стандартное нормальное (Box–Muller)              |
//+------------------------------------------------------------------+
double C_AO_NBE::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);
  }

StepNow реализует линейный отжиг масштаба шага по доле пройденного прогона.

//+------------------------------------------------------------------+
//|   StepNow — масштаб движения с линейным отжигом                  |
//+------------------------------------------------------------------+
double C_AO_NBE::StepNow()
  {
   double t = (double)epochNow / (double)epochsDen;
   if(t > 1.0)
      t = 1.0;
   return stepSize * (STEP_MIN_RATIO + (1.0 - STEP_MIN_RATIO) * (1.0 - t));
  }

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

//+------------------------------------------------------------------+
//|   RouletteByE — индекс из pool[0..n-1], взвешенный по энергии,   |
//|   с исключением exclude (или -1)                                 |
//+------------------------------------------------------------------+
int C_AO_NBE::RouletteByE(int &pool [], int n, int exclude)
  {
   double sum = 0.0;
   for(int k = 0; k < n; k++)
     {
      int idx = pool [k];
      if(idx == exclude)
         continue;
      double e = bro [idx].E;
      if(e < 1e-12)
         e = 1e-12;
      sum += e;
     }

   if(sum <= 0.0)
     {
      int idx = pool [u.RNDminusOne(n)];
      return idx;
     }

   double r   = u.RNDprobab() * sum;
   double acc = 0.0;
   for(int k = 0; k < n; k++)
     {
      int idx = pool [k];
      if(idx == exclude)
         continue;
      double e = bro [idx].E;
      if(e < 1e-12)
         e = 1e-12;
      acc += e;
      if(acc >= r)
         return idx;
     }
   return pool [n - 1];
  }

Метод Moving каждой эпохой даёт каждому бактероиду один из двух взаимоисключающих ходов. С вероятностью pPlasmid бактероид совершает плазмидный перенос: выбирает донора рулеткой по энергии и копирует от него непрерывный блок координат. В противном случае он совершает хемотаксис-движение — покоординатное гауссово смещение с затухающим шагом.

Масштаб шага step берётся из метода StepNow, который линейно снижает его по ходу прогона от стартового значения stepSize до малой доли от него. Это придаёт движению характер: сначала широкий поиск, затем точная доводка. Выбор донора делегирован методу RouletteByE, реализующему обычную рулетку, но с весами по энергии бактероидов, а не по их приспособленности.

//+------------------------------------------------------------------+
//|                            Moving                                |
//|   Каждый бактероид этой эпохой делает ЛИБО плазмидный перенос,   |
//|   ЛИБО хемотаксис-движение. Кандидат пишется в a[i].c.           |
//+------------------------------------------------------------------+
void C_AO_NBE::Moving()
  {
//--- первый прогон: генотипы -> слоты для первичной оценки
   if(!revision)
     {
      for(int i = 0; i < popSize; i++)
         for(int c = 0; c < coords; c++)
            a [i].c [c] = bro [i].c [c];
      return;
     }

   epochNow++;
   double step = StepNow();

   for(int i = 0; i < popSize; i++)
     {
      //--- стартуем кандидата с текущего генотипа
      for(int c = 0; c < coords; c++)
         a [i].c [c] = bro [i].c [c];

      if(popSize > 1 && u.RNDprobab() < pPlasmid)
        {
         //--- ПЛАЗМИДНЫЙ ПЕРЕНОС: блок от донора (рулетка по энергии)
         actType [i] = 1;

         int donor = RouletteByE(allIdx, popSize, i);
         int span  = coords - transLen + 1;
         if(span < 1)
            span = 1;
         int start = u.RNDminusOne(span);

         for(int t = 0; t < transLen; t++)
           {
            int c = start + t;
            if(c < coords)
               a [i].c [c] = bro [donor].c [c];
           }
        }
      else
        {
         //--- ХЕМОТАКСИС: покоординатное гауссово движение
         actType [i] = 0;

         for(int c = 0; c < coords; c++)
           {
            double rng = rangeMax [c] - rangeMin [c];
            double v   = bro [i].c [c] + Gauss() * step * rng;
            a [i].c [c] = u.SeInDiSp(v, rangeMin [c], rangeMax [c], rangeStep [c]);
           }
        }
     }
  }

Вся содержательная механика модели сосредоточена в Revision, и проходит он в четыре этапа. Первый — приёмка хода. Перенос принимается безусловно, как и задумано: горизонтальный обмен не тестируется немедленно, его проверит среда. Движение же принимается по greedy-правилу — только если новое положение не хуже прежнего, иначе бактероид откатывается.

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

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

Третий этап — гибель. Бактероид помечается мёртвым, если его энергия исчерпана или если сработала средовая вероятность гибели pDeath, не зависящая от приспособленности. Выжившие собираются в массив living[].

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

//+------------------------------------------------------------------+
//|                           Revision                               |
//|   Приёмка хода -> энергобаланс -> гибель -> деление.             |
//+------------------------------------------------------------------+
void C_AO_NBE::Revision()
  {
//--- глобальный лучший
   for(int i = 0; i < popSize; i++)
     {
      if(a [i].f > fB)
        {
         fB = a [i].f;
         ArrayCopy(cB, a [i].c, 0, 0, coords);
        }
     }

//--- первичная фиксация
   if(!revision)
     {
      for(int i = 0; i < popSize; i++)
        {
         bro [i].f = a [i].f;
         bro [i].E = E0;
        }
      revision = true;
      return;
     }

//--- приёмка хода: перенос безусловно, движение — greedy
   for(int i = 0; i < popSize; i++)
     {
      if(actType [i] == 1)
        {
         for(int c = 0; c < coords; c++)
            bro [i].c [c] = a [i].c [c];
         bro [i].f = a [i].f;
        }
      else
        {
         if(a [i].f >= bro [i].f)
           {
            for(int c = 0; c < coords; c++)
               bro [i].c [c] = a [i].c [c];
            bro [i].f = a [i].f;
           }
         //--- иначе откат: bro[i] остаётся прежним
        }
     }

//--- энергобаланс: приход ~ популяционно-относительной адаптированности
   double fmin =  DBL_MAX;
   double fmax = -DBL_MAX;
   for(int i = 0; i < popSize; i++)
     {
      if(bro [i].f < fmin)
         fmin = bro [i].f;
      if(bro [i].f > fmax)
         fmax = bro [i].f;
     }
   double span = fmax - fmin;
   if(span < 1e-12)
      span = 1e-12;

   for(int i = 0; i < popSize; i++)
     {
      double gain = (bro [i].f - fmin) / span;   // [0,1]
      bro [i].E += gain - cost;
      if(bro [i].E > Emax)
         bro [i].E = Emax;
     }

//--- гибель: энергетическая (E<=0) или средовая (pDeath)
   int nLiving = 0;
   bool dead [];
   ArrayResize(dead, popSize);

   for(int i = 0; i < popSize; i++)
     {
      bool die = (bro [i].E <= 0.0) || (u.RNDprobab() < pDeath);
      dead [i] = die;
      if(!die)
        {
         living [nLiving] = i;
         nLiving++;
        }
     }

//--- заполнение освободившихся слотов делением (или массовый респаун)
   if(nLiving == 0)
     {
      for(int i = 0; i < popSize; i++)
         Respawn(i);
      return;
     }

   double step = StepNow();

   for(int i = 0; i < popSize; i++)
     {
      if(!dead [i])
         continue;

      //--- родитель — энергичный живой (рулетка по энергии)
      int p = RouletteByE(living, nLiving, -1);

      //--- деление: потомок = мутированная копия родителя рядом
      for(int c = 0; c < coords; c++)
        {
         double rng = rangeMax [c] - rangeMin [c];
         double v   = bro [p].c [c] + Gauss() * step * rng;
         bro [i].c [c] = u.SeInDiSp(v, rangeMin [c], rangeMax [c], rangeStep [c]);
        }

      //--- энергия делится пополам
      double half = bro [p].E * 0.5;
      bro [i].E = half;
      bro [p].E = bro [p].E - half;

      //--- фитнес потомка оценится на следующей эпохе; оценка-заглушка
      bro [i].f = bro [p].f;
     }
  }

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

Отдельно отмечу, что глобально лучшее решение обновляется в самом начале Revision, до всей этой механики, и хранится в cB / fB каркаса. Благодаря этому непрерывный оборот популяции — гибель удачных бактероидов, перезапись неудачным переносом — не способен потерять найденный оптимум: элитизм каркаса страхует результат, пока колония продолжает молотить свой steady-state.


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

По результатам тестирования NBE набирает 44,52% и значительно уступает алгоритмам из рейтинга.

NBE|Numaoka Bacterial Evolution|30.0|0.1|0.1|0.2|0.5|0.0005| 
=============================
5 Hilly's; Func runs: 10000; result: 0.8192159815167424
25 Hilly's; Func runs: 10000; result: 0.49855422323618664
500 Hilly's; Func runs: 10000; result: 0.27964847429941897
=============================
5 Forest's; Func runs: 10000; result: 0.8390788490589818
25 Forest's; Func runs: 10000; result: 0.331571269993976
500 Forest's; Func runs: 10000; result: 0.06909155068965439
=============================
5 Megacity's; Func runs: 10000; result: 0.7293333333333335
25 Megacity's; Func runs: 10000; result: 0.3192
500 Megacity's; Func runs: 10000; result: 0.12144000000000103
=============================
All score: 4.00713 (44.52%)

Античит-тест на честность работы алгоритма.

NBE|Numaoka Bacterial Evolution (rapid adaptation)|30.0|0.1|0.1|0.2|0.5|0.0005|
=============================
Composite anti-cheat test: Hilly + Forest + Megacity + Peaks + Skin
Coordinates: 10; Epochs: 333; Repeats: 10
=============================
Run 1/10: 0.7924952224378096
Run 2/10: 0.7890303169792431
Run 3/10: 0.805857730930511
Run 4/10: 0.9025599823187104
Run 5/10: 0.7760608747746541
Run 6/10: 0.6997834424845257
Run 7/10: 0.9422331414891965
Run 8/10: 0.8147796080573018
Run 9/10: 0.8163391404105331
Run 10/10: 0.763698395783302
=============================
Average result: 0.8102837856 (81.03%)
=============================

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

Hilly

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

Forest

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

Megacity

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

Ackley

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

Peaks

NBE на тестовой функции Peaks

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

cc AO Description Hilly Hilly
Final
Forest Forest
Final
Megacity (discrete) Megacity
Final
Final
Result
% of
MAX
10 p (5 F) 50 p (25 F) 1000 p (500 F) 10 p (5 F) 50 p (25 F) 1000 p (500 F) 10 p (5 F) 50 p (25 F) 1000 p (500 F)
1 ANS across neighbourhood search 1,00000 0,88228 0,40138 2,28366 1,00000 0,95281 0,28092 2,23373 0,94667 0,85733 0,22389 2,02789 6,545 72,72
2 AMOm animal migration optimization M 0,91624 0,83603 0,46790 2,22017 0,98482 0,92010 0,36391 2,26883 0,91733 0,81707 0,25177 1,98617 6,475 71,94
3 CLA code lock algorithm (joo) 0,95139 0,86199 0,37879 2,19217 0,99349 0,93500 0,26497 2,19346 0,93600 0,84267 0,24060 2,01927 6,405 71,17
4 (P+O)ES (P+O) evolution strategies 0,86571 0,89539 0,39740 2,15850 0,97761 0,89820 0,26878 2,14459 0,92133 0,80240 0,23952 1,96325 6,266 69,62
5 SDSm stochastic diffusion search M 0,95195 0,84944 0,36249 2,16388 0,98061 0,88457 0,22112 2,08630 0,92267 0,79013 0,21380 1,92660 6,177 68,63
6 AAm archery algorithm M 0,84685 0,73320 0,42590 2,00595 0,96709 0,77837 0,27789 2,02335 0,86133 0,77707 0,28712 1,92552 5,955 66,17
7 SIA simulated isotropic annealing (joo) 0,93543 0,86504 0,38483 2,18530 0,94069 0,80609 0,23835 1,98513 0,86400 0,66160 0,19536 1,72096 5,891 65,46
8 TETA time evolution travel algorithm (joo) 0,91452 0,86369 0,25579 2,03400 0,99654 0,91291 0,14394 2,05339 0,85467 0,82213 0,10443 1,78123 5,869 65,21
9 ESG evolution of social groups (joo) 0,98111 0,79857 0,31167 2,09135 0,98954 0,82270 0,15032 1,96256 0,92133 0,73440 0,15315 1,80888 5,863 65,14
10 CTA comet tail algorithm (joo) 0,92435 0,86786 0,27838 2,07059 0,99039 0,84571 0,19448 2,03058 0,95467 0,69680 0,11008 1,76155 5,863 65,14
11 COA coyote_optimization_algorithm 0,88909 0,70681 0,32718 1,92308 0,99467 0,85358 0,15152 1,99977 0,88533 0,71040 0,18981 1,78554 5,708 63,43
12 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
13 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
14 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
15 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
16 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
17 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
18 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
19 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
20 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
21 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
22 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
23 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
24 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
25 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
26 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
27 (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
28 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
29 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
30 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
31 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
32 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
33 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
34 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
35 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
36 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
37 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
38 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
39 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
40 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
41 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
42 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
43 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
44 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
45 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
NBE numaoka_bacterial_evolution 0,81921 0,49855 0,27964 1,59740 0,83907 0,33157 0,06909 1,23973 0,72933 0,31920 0,12144 1,16997 4,007 44,52
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


Выводы

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

На статических функциях ответ однозначен и закономерен: NBE набирает 44,52% и уступает сходящимся алгоритмам. Причина не в слабости поиска, а в самом устройстве модели — она построена так, чтобы никогда не успокаиваться, постоянно поддерживая оборот популяции через гибель и деление. На неподвижном оптимуме это свойство — чистый расход: пока хороший оптимизатор замирает в найденной точке и шлифует её, NBE продолжает молотить свой steady-state, расшвыривая удачные решения. Энергетический аппарат, буфер истории приспособленности, на статике работает вхолостую, потому что нет того перехода, который этот буфер призван пережить. Отдельно это подтвердил эффект средовой гибели: чем её меньше, тем выше статический счёт, ведь всякая не-фитнесная гибель на неподвижной цели — лишь шум.

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

tab

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

chart

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


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

Плюсы:

  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_NBE.mq5
Скрипт Испытательный стенд для NBE

Прикрепленные файлы |
NBE.zip (433.27 KB)
Разработка самовосстанавливающегося советника в MQL5 (Часть 1): Архитектура постоянного хранения состояния сделки Разработка самовосстанавливающегося советника в MQL5 (Часть 1): Архитектура постоянного хранения состояния сделки
В этой статье показано, как построить базовую архитектуру постоянного хранения состояния для самовосстанавливающегося советника в MQL5 с использованием SQLite. Читатели узнают, как создать слой постоянного хранения состояния сделок, устойчивый к перезапускам терминала, выключениям и непредвиденным сбоям. В статье рассматривается интеграция SQLite в MetaTrader 5, управление жизненным циклом базы данных, структуры постоянно сохраняемого состояния сделки и восстановление рабочего состояния во время выполнения с использованием практических реализаций в MQL5.
Автоматизация торговых стратегий в MQL5 (Часть 32): Создание системы распознавания гармонического паттерна "5 Drives" с использованием Price Action Автоматизация торговых стратегий в MQL5 (Часть 32): Создание системы распознавания гармонического паттерна "5 Drives" с использованием Price Action
В этой статье мы разрабатываем систему распознавания паттернов "5 Drives" на языке MQL5, которая определяет бычьи и медвежьи гармонические паттерны "5 Drives" с использованием точек разворота и коэффициентов Фибоначчи, совершая сделки с настраиваемыми уровнями входа, стоп-лосса и тейк-профита на основе выбранных пользователем параметров. Мы улучшим работу трейдеров с помощью визуальной обратной связи, используя графические объекты, такие как треугольники, линии тренда и подписи, для наглядного отображения структуры паттерна A-B-C-D-E-F.
Валютный граф и справедливая цена 28 пар через DFS по кросс-путям Валютный граф и справедливая цена 28 пар через DFS по кросс-путям
В статье показано, как вместо треугольного арбитража использовать полный валютный граф из 8 валют и 28 пар. Для каждой пары DFS перечисляет простые кросс‑пути длиной от 2 до N рёбер, а справедливая цена получается как взвешенное геометрическое среднее в лог‑пространстве. Отклонение рынка от этой оценки служит источником mean‑reversion сигналов.
Встраивание торговой дисциплины в код (Часть 6): Построение единого фреймворка дисциплины на MQL5 Встраивание торговой дисциплины в код (Часть 6): Построение единого фреймворка дисциплины на MQL5
В статье представлен унифицированный фреймворк обеспечения дисциплины на MQL5, которая объединяет модули белого списка символов, торговых часов и новостных фильтров, а также ежедневных лимитов сделок в рамках файла CDisciplineEngine.mqh. Объясняется централизованная проверка сделок и синхронизация состояний, выполняемые совместно дашбордом графиков и советником-принудителем. Читатели узнают, как авторизовывать ордера через единый шлюз, отслеживать разрешения в реальном времени и автоматически обеспечивать соблюдение правил на уровне всего терминала.