preview
Алгоритм оптимизации на основе коронавируса — Corona Virus Optimization (CVO)

Алгоритм оптимизации на основе коронавируса — Corona Virus Optimization (CVO)

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

Содержание

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


Введение

Алгоритмов оптимизации, вдохновлённых природой, с каждым годом всё больше и почти каждая новая работа обещает быстрые и красивые графики сходимости. Трейдеру, который ищет, чем оптимизировать параметры своей торговой системы, всё труднее отделить действительно сильный инструмент от середняка, удачно поданного на лёгких низкоразмерных задачах. Corona Virus Optimization (CVO) построен на модели распространения коронавируса: заражённые особи передают симптомы восприимчивым, популяция инфицированных то разрастается, то обновляется при очередной удаче, а сила инфекции управляет масштабом шага поиска. Идея свежая, авторская статья показывает уверенные результаты — но как алгоритм поведёт себя на честном, едином для всех бенчмарке, без подбора удобных функций?

В этой статье я провожу CVO: от формул оригинала до рабочего кода в стандартном стенде C_AO. Поясняю устройство динамической популяции, смысл R0 и силы инфекции I/N, а также необходимые отступления от текста статьи. Главное из них — покоординатное возмущение, чтобы алгоритм не эксплуатировал диагональную симметрию тестовых функций. Затем — прогон по стандартному набору (Hilly, Forest, Megacity на 10, 50 и 1000 измерениях) и, что важнее результата, разбор: почему буквально точная реализация села на уровень случайного блуждания и что именно её оттуда вытаскивает.

К концу у вас будет не только готовая, совместимая с фреймворком реализация CVO и его честное место в общем рейтинге. Также более переносимая вещь — понимание, почему дословное следование опубликованной формуле порой даёт слабый результат, и как одно изменение возвращает алгоритму работоспособность. 


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

CVO построен на эпидемиологической модели SIR (Susceptible–Infectious–Recovered). Популяция делится на восприимчивых, инфицированных и выздоровевших/умерших. Заражённый контактирует с восприимчивыми и передаёт им симптомы; если иммунитет восприимчивого слабее, чем у носителя, он заболевает и пополняет инфекционную популяцию. Со временем число заражённых растёт по базовому репродуктивному числу R0, а сила инфекции масштабирует интенсивность передачи. Носители с сильным иммунитетом выздоравливают или умирают и выбывают.

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

Остальные обозначения: basePop — стартовое число заражённых, nPop — потолок популяции, Iters — число итераций (повторений пандемии), R0 — сколько восприимчивых заражает один носитель, pop — вся инфекционная популяция, newPop — новые заражённые текущей итерации, ρ — контактность (принята равной 1).

Три формулы. Скорость передачи симптомов (Eq. 1):

τ = ρ · φ;

где ρ — контактность, φ — вероятность передачи симптома.

Сила инфекции (Eq. 2):

λ(I) = τ · (I / N);

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

Для непрерывных задач φ берётся из стандартного нормального распределения через преобразование Бокса–Мюллера (Eq. 3):

Z = √(−2·ln U₁) · cos(2π·U₂),   U₁, U₂ ~ U(0,1)

Подставив всё вместе, носитель x порождает восприимчивого, меняя каждый симптом независимо:

xₙₒв[j] = x[j] + ρ · Zⱼ · (I / N); с зажимом в [LB, UB]

Важно: Zⱼ своё на каждую координату — у автора прямо сказано, что каждый симптом изменяется случайно по отдельности, хотя передаются все одновременно. 

Правило заражения (отбор). Для каждого восприимчивого считается иммунитет (фитнес). Если иммунитет восприимчивого слабее иммунитета носителя (f(xₙₒв) < f(x) — то есть лучше по задаче), он заражается и попадает в newPop. Восприимчивые с сильным иммунитетом "соблюдали протоколы" и в новую популяцию не идут. Заодно отслеживается LocalBest итерации.

Динамика популяции (конец итерации):

  • если LocalBest < GlobalBest (нашли новое глобально лучшее) — GlobalBest обновляется, а вся pop заменяется на newPop;
  • иначе newPop дописывается к pop (популяция растёт);
  • если |pop| > nPop — pop сортируется по возрастанию иммунитета и оставляются лучшие nPop (остальные выздоровели/умерли).

Псевдокод алгоритма:

ρ = 1
pop ← basePop случайных решений в [LB, UB]; оценить
GlobalBest ← лучший из pop
для iter = 1..Iters:
    newPop ← ∅;  LocalBest ← худшее
    для каждого носителя x из pop:
        повторить R0 раз:
            для каждой переменной j:
                Z ← BoxMuller()
                xₙₒв[j] ← зажим( x[j] + ρ·Z·(|newPop|/nPop), LB, UB )
            f ← фитнес(xₙₒв)
            если f < фитнес(x):            # слабее носителя → заражается
                newPop ← newPop ∪ {xₙₒв}
            если f < LocalBest: LocalBest ← xₙₒв
    если LocalBest < GlobalBest:
        GlobalBest ← LocalBest;  pop ← newPop      # замена
    иначе:
        pop ← pop ∪ newPop                         # рост
    если |pop| > nPop:
        сортировать pop по возрастанию фитнеса; оставить лучшие nPop
вернуть GlobalBest

Простой пример (1 переменная). Носитель в x = 2.0, ρ = 1, на ранней итерации заражено мало, пусть I/N = 0.1. При R0 = 3 он порождает трёх восприимчивых с Z = {0.8, −1.5, 0.2}: шаги ρ·Z·(I/N) = {0.08, −0.15, 0.02}, новые точки {2.08, 1.85, 2.02}. Шаги крошечные — поиск локальный. По мере того как пандемия разрастается и I/N идёт к 1, тот же механизм даёт шаги порядка Z — поиск становится размашистым.

Отсюда два свойства канона, которые стоит держать в голове (разбирать будем позже): шаг растёт по мере заполнения популяции — противоположно классическому отжигу, где он затухает; а замена pop на newPop при улучшении сбрасывает популяцию и снова сжимает шаг. То есть застой расширяет поиск, удача — фокусирует.

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

Фреймворк C_AO, в который мы переносим алгоритм, устроен наоборот — он максимизирует. Все тестовые функции серии нормированы так, что больший балл — лучше; глобально лучший fB — это максимум, а базовый класс и весь рейтинг исходят из "больше значит лучше". Чтобы CVO встроился в серию и честно сравнивался с остальными, он обязан работать в той же, конвенции максимизации.

Поэтому при переносе мы не меняем сам алгоритм — мы зеркалим направление всех сравнений, чтобы "лучше" везде значило "больше f". Соответствие такое:

  • слабее иммунитет = лучше (min) → больше f = лучше (max);
  • заражение: было f(восприимчивый) < f(носитель) → стало f(восприимчивый) >= f(носителя);
  • обновление рекорда: было LocalBest < GlobalBest → стало LocalBest > GlobalBest;
  • отсечение пула: была сортировка по возрастанию с отбором наименьших → стала по убыванию с отбором наибольших.

Механика при этом неизменна — динамический пул, R0-распространение, "заражается, если не хуже носителя", замена/рост/отсечение, рост шага по доле заражённых. Меняется только знак сравнения. Поэтому дальше в коде сравнения будут выглядеть зеркально формулировкам статьи, и это ожидаемо: мы считаем максимум там, где оригинал искал минимум.

Реализация в C_AO. Канонический CVO работает с динамической популяцией: pop то разрастается, то заменяется, и за одну итерацию порождается |pop| · R0 новых пациентов — число плавающее. А контракт тестового стенда C_AO жёсткий: за каждую эпоху стенд оценивает ровно ArraySize(a) = popSize агентов — вызывает Moving(), считает a[i].f через CalcFunc, вызывает Revision(), и так epochCount = NumbTestFuncRuns / popSize раз. Бюджет вычислений функции на эпоху фиксирован. Эти две модели — динамическая популяция и фиксированный бюджет — надо примирить.

Решение: popSize — это не популяция CVO, а бюджет восприимчивых на эпоху. За проход стенд оценивает popSize кандидатов — это восприимчивые, рождённые носителями. А сам инфекционный пул живёт отдельным внутренним состоянием inf[], переживает перезапись слотов a[] и растёт/сжимается по правилам CVO. Так суммарно FE = popSize · epochCount ≈ NumbTestFuncRuns, как у всех алгоритмов серии, и сравнение честное.

Особь пула — структура: симптомы (координаты) и иммунитет (фитнес) одной особи лежат вместе, в стиле S_AO_Agent.

//+------------------------------------------------------------------+
//|                            Structure                             |
//+------------------------------------------------------------------+
struct S_CVO_Patient
 {
  double             c [];
  double             f;
 };

Сам класс объявляем целиком так — параметры с предохранителями и поля пула:

//+------------------------------------------------------------------+
//|                             Сlass                                |
//+------------------------------------------------------------------+
class C_AO_CVO : public C_AO
 {
public:
                    ~C_AO_CVO() {}
                     C_AO_CVO()
   {
    ao_name = "CVO";
    ao_desc = "Corona Virus Optimization";
    ao_link = "https://www.mql5.com/ru/articles/22887";

    popSize = 50;     // бюджет FE на эпоху (= кандидатов за проход)
    nPop    = 200;    // потолок инфекционной популяции
    basePop = 5;      // стартовая инфекционная популяция
    R0      = 5;      // потомков-восприимчивых на одного носителя
    rho     = 0.1;    // контактность: общий множитель масштаба λ

    ArrayResize(params, 5);
    params [0].name = "popSize";
    params [0].val = popSize;
    params [1].name = "nPop";
    params [1].val = nPop;
    params [2].name = "basePop";
    params [2].val = basePop;
    params [3].name = "R0";
    params [3].val = R0;
    params [4].name = "rho";
    params [4].val = rho;
   }

  void               SetParams()
   {
    popSize = (int)params [0].val;
    nPop    = (int)params [1].val;
    basePop = (int)params [2].val;
    R0      = (int)params [3].val;
    rho     =      params [4].val;

    //--- предохранители
    if(popSize < 1)
      popSize = 1;
    if(nPop    < 1)
      nPop    = 1;
    if(R0      < 1)
      R0      = 1;
    if(rho   < 0.0)
      rho     = 0.0;
    if(basePop < 1)
      basePop = 1;
    if(basePop > popSize)
      basePop = popSize; // пул засевается из 1-го батча
    if(basePop > nPop)
      basePop = nPop;
   }

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

  void               Moving();
  void               Revision();

  //--- видимые параметры
  int                nPop;       // потолок инфекционной популяции
  int                basePop;    // стартовая инфекционная популяция
  int                R0;         // потомков на носителя
  double             rho;        // множитель масштаба λ

private:
  //--- инфекционный пул (массив структур: координаты + фитнес вместе)
  S_CVO_Patient      inf [];        // [nPop]
  int                infCount;      // текущий размер пула (<= nPop)

  //--- привязка слота-кандидата к носителю
  int                parentOf [];   // [popSize]

  //--- временный пул пересборки
  S_CVO_Patient      tmp [];        // [nPop + popSize]

  //--- стандартное нормальное (Box-Muller), Eq.3
  double             Gauss();
  //--- индексная сортировка по убыванию f
  void               ArgSortDesc(const double &f [], int &idx [], const int count);
 };

Привязка слотов к носителям. В каноне каждый носитель заражает R0 восприимчивых. Под фиксированный бюджет это проецируется так: слот k рождается носителем p = (k / R0) % infCount, то есть R0 подряд идущих слотов делят одного носителя. Если пул больше, чем popSize/R0, размножаются только первые ceil(popSize/R0) носителей — а так как пул отсортирован по убыванию иммунитета, это лучшие. Если пул меньше — модуль заворачивает, носители переиспользуются. Получается элитарная проекция правила "каждый заражает R0" на фиксированный бюджет эпохи.

Init заполняет стартовые случайные координаты для каждого агента и один раз выделяет пул и буфер:

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

//--- стартовый случайный батч в cP[] (оценится на первом проходе)
  for(int i = 0; i < popSize; i++)
    for(int c = 0; c < coords; c++)
      a [i].cP [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);

//--- инфекционный пул и временный пул (массивы структур)
  ArrayResize(inf, nPop);
  for(int i = 0; i < nPop; i++)
    ArrayResize(inf [i].c, coords);

  ArrayResize(tmp, nPop + popSize);
  for(int i = 0; i < nPop + popSize; i++)
    ArrayResize(tmp [i].c, coords);

  ArrayResize(parentOf, popSize);
  infCount = 0;

  return true;
 }

Moving в рабочем проходе порождает popSize восприимчивых — каждый симптом по формуле статьи ( Eq.1·3 ), покоординатно. Шаг здесь абсолютный, буква в букву по статье — это версия для первого честного прогона; в разделе диагностики мы заменим ровно строку v:

//+------------------------------------------------------------------+
//|                            Moving                                |
//+------------------------------------------------------------------+
void C_AO_CVO::Moving()
 {
//--- первый прогон: cP -> c
  if(!revision)
   {
    for(int i = 0; i < popSize; i++)
      for(int c = 0; c < coords; c++)
        a [i].c [c] = u.SeInDiSp(a [i].cP [c], rangeMin [c], rangeMax [c], rangeStep [c]);
    return;
   }

//--- генерация кандидатов-восприимчивых
  for(int k = 0; k < popSize; k++)
   {
    int denom = infCount > 0 ? infCount : 1; // защита от %0
    int p = (k / R0) % denom;          // носитель данного слота
    parentOf [k] = p;

    for(int c = 0; c < coords; c++)
     {
      double phi  = Gauss();                                   // Eq.3, [D1] свой на координату
      double frac = (double)(infCount + k) / (double)nPop;     // I/N, Eq.1*Eq.2, [D3]
      if(frac > 1.0)
        frac = 1.0;
      double lam  = rho * phi * frac;        // абсолютный шаг, ТОЧНО по статье (Eq.1*2*3)
      double v    = inf [p].c [c] + lam;
      a [k].c [c] = u.SeInDiSp(v, rangeMin [c], rangeMax [c], rangeStep [c]);
     }
   }
 }

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

//--- принятые восприимчивые: иммунитет не лучше носителя -> заразился
//    (читаем inf[p].f ДО перезаписи пула — наложения нет)
  for(int k = 0; k < popSize; k++)
   {
    int p = parentOf [k];
    if(MathIsValidNumber(a [k].f) && a [k].f >= inf [p].f)
     {
      ArrayCopy(tmp [cnt].c, a [k].c, 0, 0, coords);
      tmp [cnt].f = a [k].f;
      cnt++;
     }
   }

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

Gauss — нормальное по Бокс–Мюллеру (Eq. 3). Превращает два равномерных числа из (0,1) в стандартное нормальное; вызывается отдельно на каждую координату при генерации восприимчивого:

//+------------------------------------------------------------------+
//|              Gauss — стандартное нормальное (Box-Muller)         |
//+------------------------------------------------------------------+
double C_AO_CVO::Gauss()
 {
  double u1 = u.RNDfromCI(0.0, 1.0);
  double u2 = u.RNDfromCI(0.0, 1.0);
  if(u1 < 1e-15)
    u1 = 1e-15;                       // защита от ln(0)
  return MathSqrt(-2.0 * MathLog(u1)) * MathCos(2.0 * M_PI * u2);
 }

ArgSortDesc — порядок индексов по убыванию f. Нужен в двух местах (посев и отсечение). Чтобы не таскать структуры, сортируем не сам пул, а плоский массив фитнесов, получая порядок индексов, по которому потом собираем:

//+------------------------------------------------------------------+
//|     ArgSortDesc — порядок индексов по убыванию f                 |
//+------------------------------------------------------------------+
void C_AO_CVO::ArgSortDesc(const double &f [], int &idx [], const int count)
 {
  for(int i = 0; i < count; i++)
    idx [i] = i;

  for(int i = 0; i < count - 1; i++)
   {
    int b = i;
    for(int j = i + 1; j < count; j++)
      if(f [idx [j]] > f [idx [b]])
        b = j;
    if(b != i)
     {
      int t = idx [i];
      idx [i] = idx [b];
      idx [b] = t;
     }
   }
 }

Это сортировка выбором, O(count²), но count не превышает nPop + popSize и вызывается раз за эпоху — на фоне оценок функции накладные незаметны, зато код предельно прозрачен.


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

Прогоняем по стандартному набору серии: функции Hilly, Forest и Megacity на 10, 50 и 1000 переменных, бюджет 10000 обращений к функции, по 10 повторов, итоговый балл — нормированная доля от максимума. Шаг абсолютный, ровно по статье, контактность ρ = 1, как у авторов. Лучшая чистая конфигурация (popSize=50, nPop=100, basePop=20, R0=10):

CVO|Corona Virus Optimization|50.0|100.0|20.0|10.0|1.0|
=============================
5 Hilly's; Func runs: 10000; result: 0.5811428685355893
25 Hilly's; Func runs: 10000; result: 0.3689998607642066
500 Hilly's; Func runs: 10000; result: 0.26725319445074575
=============================
5 Forest's; Func runs: 10000; result: 0.4021701300516501
25 Forest's; Func runs: 10000; result: 0.1679713459276718
500 Forest's; Func runs: 10000; result: 0.05216720027056517
=============================
5 Megacity's; Func runs: 10000; result: 0.4613333333333333
25 Megacity's; Func runs: 10000; result: 0.21520000000000006
500 Megacity's; Func runs: 10000; result: 0.11194666666666767
=============================
All score: 2.62818 (29.20%)

29.20%. Это уровень рядом со случайным блужданием — там, где сильные алгоритмы серии берут пятьдесят с лишним процентов. Видно и характерное: на 10 переменных результаты ещё приличные (0.58 на Hilly), но с ростом размерности всё быстро проседает — на 1000 переменных Forest падает до 0.05, фактически шум. Алгоритм почти не держит высокую размерность.

Прежде чем винить алгоритм, надо убедиться, что считает он корректно, а не сломан. Проверка — чувствительность к параметрам. При фиксированных basePop=10, R0=5 крутим контактность: ρ = 0.001 -> 18.40%; ρ = 0.1 -> 28.91%; ρ = 10 -> 24.93%.

Отклик гладкий и осмысленный: при крошечном ρ шаг почти нулевой, поиск замерзает (18%); при большом ρ шаг разболтан, скатывается к случайному (25%); оптимум — где-то в районе ρ ≈ 0.1…1. Аналогично реагирует на basePop и R0 (лучшее — basePop=20, R0=10 ). Будь логика порвана, был бы плоский мусор при любых параметрах. Раз есть внятный экстремум и отклик на настройку — механика жива и считает правильно. Потолок ~29% не баг реализации, а свойство самого метода в этой постановке.

Диагностика и правка. Вернёмся к формуле шага в её точном виде:

λ = ρ · φ · (I / N),   xₙₒв[j] = x[j] + λ

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

Вот тут и зарыта собака. Шаг не привязан к ширине области поиска, а функции бенчмарка живут на разных диапазонах. Грубо: если ось тянется, скажем, на двести единиц, то шаг порядка единицы — это полпроцента диапазона, алгоритм почти не двигается, ползёт на месте. Если ось всего пара единиц шириной — тот же шаг это половина всего диапазона, дикие прыжки, чистый случайный поиск. Один и тот же λ на одной функции — микрошаги, на другой — хаос. Отсюда и картина из прошлого раздела: на 10 переменных кое-как, а на 1000 — коллапс до уровня шума, потому что покрыть огромный объём фиксированным мелким абсолютным шагом невозможно.

Гипотезу легко проверить: если дело в масштабе, то привязка шага к ширине диапазона должна выправить ситуацию. Так и есть.

Правка от здравого смысла. Делаем шаг долей диапазона каждой координаты — одна строка в Moving :

// было (абсолют, буква статьи):
double v = inf [p].c [c] + lam;

// стало (доля диапазона координаты):
double v = inf [p].c [c] + lam * (rangeMax [c] - rangeMin [c]);

Теперь λ — не единица по оси, а доля ширины оси, одинаковая по смыслу на всех функциях и на каждой координате. Масштабная инвариантность, которой не хватало.

Но за это надо переставить величину. С range-scaling прежний ρ = 1 даёт шаг во весь диапазон — ровно тот хаос, что мы только что описали (кстати, это объясняет, почему ранняя попытка масштабировать с ρ = 1 скатывалась в случайный поиск). Поэтому ρ = 0.1 : при полном пуле шаг ~0.1·φ·диапазон, то есть порядка 5–10% ширины оси — разумная величина для уточнения. Заодно подстроены пул и посев: nPop = 200 (больше потолок, и заодно меньше frac = (infCount+k)/nPop → мельче шаг → больше эксплуатации) и basePop = 5 (сфокусированный старт). Все три параметра тянут в одну сторону — к управляемому, привязанному к масштабу шагу.

CVO|Corona Virus Optimization|50.0|200.0|5.0|5.0|0.1|
=============================
5 Hilly's; Func runs: 10000; result: 0.5946817995500855
25 Hilly's; Func runs: 10000; result: 0.36906584070682485
500 Hilly's; Func runs: 10000; result: 0.26998944552451903
=============================
5 Forest's; Func runs: 10000; result: 0.5225857394828209
25 Forest's; Func runs: 10000; result: 0.19382751005851218
500 Forest's; Func runs: 10000; result: 0.053702157009133125
=============================
5 Megacity's; Func runs: 10000; result: 0.4413333333333334
25 Megacity's; Func runs: 10000; result: 0.2424
500 Megacity's; Func runs: 10000; result: 0.11301333333333437
=============================
All score: 2.80060 (31.12%)

Сильно ли мы отклонились. Формально — да, это отступление от буквы: статья прибавляет λ как есть, мы умножаем на ширину диапазона. Но отступление принципиальное, а не косметический чит, и вот почему. Во-первых, вся структура CVO нетронута — динамический пул, R0-распространение, заражение «если иммунитет не хуже носителя», замена/рост, отсечение, рост шага по доле заражённых. Поменялись только единицы возмущения: абсолют → доля диапазона. Во-вторых, если функции в статье были одного, умеренного масштаба, то абсолютный λ у авторов по факту и был разумной долей их диапазона — а в нашем ненормированном бенче эту долю приходится восстанавливать явно. То есть range-scaling ближе к замыслу (относительный шаг), чем дословная формула. И в-третьих, это стандартная конвенция всей твоей серии: все алгоритмы масштабируют шаг к диапазону, иначе сравнение между ними нечестное.

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

Композитный тест на 10 переменных даёт 56.69%, но это другой, более лёгкий режим (только низкая размерность), и смешивать эти два числа нельзя — в рейтинг идут стандартные 31%.

CVO|Corona Virus Optimization|50.0|200.0|5.0|5.0|0.1|
=============================
Composite test: Hilly + Forest + Megacity + Peaks + Skin
Coordinates: 10; Epochs: 200; Repeats: 10
=============================
Run 1/10: 0.5598502390640241
Run 2/10: 0.46218834024270095
Run 3/10: 0.5472841349143641
Run 4/10: 0.6438002840259478
Run 5/10: 0.5363633183876485
Run 6/10: 0.6189199641420557
Run 7/10: 0.5775251324533818
Run 8/10: 0.5168167745601945
Run 9/10: 0.5741164838588073
Run 10/10: 0.6317059909142314
=============================
Average result: 0.5668570663 (56.69%)

=============================

CVO

Рисунок 1. Схема работы алгоритма CVO

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

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

λ = ρ · φ · (I / N);

здесь ρ — контактность, φ = Z_j — случайное число из стандартного нормального распределения (преобразование Бокса–Мюллера), своё на каждую координату j, а I / N — доля заражённых в популяции. Чем шире разрослась пандемия и больше I / N, тем крупнее шаг — возмущение нарастает по ходу заполнения пула.

Ключевое отличие от оригинала — последний множитель (UB_j − LB_j), выделенный на схеме оранжевым. В статье сила инфекции прибавляется к позиции напрямую, в абсолютных единицах координаты. Мы же домножаем её на ширину диапазона каждой координаты, и шаг из "стольких-то единиц по оси" превращается в долю поискового пространства. Это и есть правка от здравого смысла: один и тот же относительный шаг ведёт себя одинаково на функциях любого масштаба — не теряется в микронудж на широких диапазонах и не разносит поиск в случайный на узких. Структуру самого CVO это не трогает: меняются только единицы возмущения.

Hilly

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

Forest

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

Megacity

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

Ackley

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

Rosenbrock

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

В рейтинговую таблицу CVO не попадает и представлен только ознакомительно.

cc AO Description Hilly Hilly
Final
Forest Forest
Final
Megacity (discrete) Megacity
Final
Final
Result
% of
MAX
10 p (5 F)50 p (25 F)1000 p (500 F)10 p (5 F)50 p (25 F)1000 p (500 F)10 p (5 F)50 p (25 F)1000 p (500 F)
1ANSacross neighbourhood search1,000000,882280,401382,283661,000000,952810,280922,233730,946670,857330,223892,027896,54572,72
2AMOmanimal migration optimization M0,916240,836030,467902,220170,984820,920100,363912,268830,917330,817070,251771,986176,47571,94
3CLAcode lock algorithm (joo)0,951390,861990,378792,192170,993490,935000,264972,193460,936000,842670,240602,019276,40571,17
4(P+O)ES(P+O) evolution strategies0,865710,895390,397402,158500,977610,898200,268782,144590,921330,802400,239521,963256,26669,62
5SDSmstochastic diffusion search M0,951950,849440,362492,163880,980610,884570,221122,086300,922670,790130,213801,926606,17768,63
6AAmarchery algorithm M0,846850,733200,425902,005950,967090,778370,277892,023350,861330,777070,287121,925525,95566,17
7SIAsimulated isotropic annealing (joo)0,935430,865040,384832,185300,940690,806090,238351,985130,864000,661600,195361,720965,89165,46
8TETAtime evolution travel algorithm (joo)0,914520,863690,255792,034000,996540,912910,143942,053390,854670,822130,104431,781235,86965,21
9ESGevolution of social groups (joo)0,981110,798570,311672,091350,989540,822700,150321,962560,921330,734400,153151,808885,86365,14
10CTAcomet tail algorithm (joo)0,924350,867860,278382,070590,990390,845710,194482,030580,954670,696800,110081,761555,86365,14
11ECBOenhanced colliding bodies optimization0,940240,723630,323561,987430,994770,802910,130561,928240,876000,701600,174331,751935,66862,98
12DAdialectical algorithm0,931170,754000,262051,947220,989250,813750,086621,889620,926670,681070,113151,720895,55861,76
13BBObiogeography based optimization0,958760,706090,357522,022370,929810,706600,169701,806110,874670,630130,208131,712935,54161,57
14BHAmblack hole algorithm M0,795580,762070,346821,904470,998360,757980,138261,894600,850670,644270,170201,665145,46460,71
15HSharmony search0,914200,690490,299241,903930,976270,733730,141931,851930,917330,627200,153641,698175,45460,60
16RFOroyal flush optimization (joo)0,809890,744810,345461,900160,952510,779260,151851,883620,804000,664270,190711,658985,44360,48
17BOAmbilliards optimization algorithm M0,761770,724210,252751,738730,908900,819600,288532,017030,837330,746130,097631,681095,43760,41
18ASOanarchy society optimization0,730700,737130,311951,779780,997320,877000,176192,050510,720000,687730,189881,597615,42860,31
19EOmextremal optimization_M0,765270,752050,319081,836400,999990,764260,124371,888620,841330,641330,152471,635135,36059,56
20ACSartificial cooperative search0,755450,771620,316531,843601,000000,804880,107051,911930,769330,608000,141571,518905,27458,60
21SSGsaplings sowing and growing0,754360,632060,359351,745770,919070,696940,197551,813560,818670,605330,213471,637475,19757,74
22AOSmatomic orbital search M0,761840,684350,313441,759630,900150,800440,115011,815600,828000,632800,156961,617765,19357,70
23TSEAturtle shell evolution algorithm (joo)0,958090,648520,295711,902320,995220,581040,105421,681680,921330,521600,145671,588605,17357,48
24DEdifferential evolution0,963980,623460,260891,848330,984820,770180,114591,869590,930670,362130,110001,402805,12156,90
25BIOblood inheritance optimization (joo)0,725800,665220,312281,703300,999950,681250,115401,796600,854670,593330,153641,601645,10256,69
26(PO)ES(PO) evolution strategies0,739720,581900,388961,710580,911990,599750,212621,724360,824000,562400,234321,620725,05656,18
27BObonobo optimizer0,755550,643660,326571,725780,943320,704420,139991,787730,734670,614400,167281,516355,03055,89
28SRAsuccessful restaurateur algorithm (joo)0,890100,633590,291151,814840,966340,552850,089141,608330,893330,528000,139111,560444,98455,38
29CROchemical reaction optimisation0,912810,656810,298661,868280,905130,560200,109391,574720,828000,501330,141491,470824,91454,60
30BCOmbacterial chemotaxis optimization M0,825890,617330,315841,759060,952960,637180,119841,709980,765330,516530,158001,439864,90954,54
31DOAdream optimization algorithm0,785220,781210,360361,926790,615840,421170,122541,159550,866670,725870,211271,803814,89054,33
32ABOafrican buffalo optimization0,922950,625280,298851,847080,929920,574680,093721,598320,733330,513330,143241,389904,83553,72
33BSAbird swarm algorithm0,944320,679410,264011,887740,916490,656190,120541,693220,809330,335470,106521,251324,83253,69
34TSmtabu search M0,878060,610400,289931,778390,981160,521650,085441,588250,826670,495470,135521,457664,82453,60
35BSAbacktracking search algorithm0,871280,531900,286751,689930,924080,516020,091531,531630,960000,472530,137601,570134,79253,24
38BWOmbeluga_whale_optimization_M0,784880,568720,295571,649170,913700,617600,129881,661180,813330,499460,150041,462834,77353,04
36WOAmwhale optimization algorithm M0,938930,594770,266951,800650,980360,538730,071121,590210,786670,476000,118921,381594,77253,02
37ACAandean_condor_algorithm0,784440,532600,331081,648120,790710,449600,106851,347160,922660,677330,176131,776124,77153,02
39CSOcompetitive swarm optimizer0,851510,607860,298961,758330,840850,584910,119741,545500,800000,485600,141841,427444,73152,57
40FBAfractal-based algorithm0,694190,642670,289551,626410,998120,549050,087051,634220,761330,512530,136891,410754,67151,90
41ECOieco-inspired evolutionary algorithm0,788170,544020,293601,625790,889960,465920,097471,453350,785330,451730,142951,380014,45949,54
42BSObrain storm optimization0,922070,576250,297321,795640,807640,425080,094481,327200,772000,365330,130651,267984,39148,79
43CAmcamel algorithm M0,715340,569170,359851,644360,840940,471740,108501,421180,704000,419470,195631,319104,38548,72
44ACOmant colony optimization_M0,718850,484100,309901,512850,757920,486390,118711,363020,836000,486670,161481,484154,36048,44
45BSObrain storm optimization 0,913310,558130,297051,768490,853290,440380,094471,388140,722670,328800,133251,184724,34148,23
CVOcorona_virus_optimization0,594680,369060,269981,233720,522580,193820,053700,770100,441330,242400,113010,796742,80131,12
RWrandom walk0,499700,323330,257911,080940,307540,114700,044000,466240,361330,170130,102440,633902,18124,23


Выводы

В общий рейтинг CVO входит со своим честным результатом — 31.12% на стандартном наборе после привязки шага к диапазону. Алгоритм заметно слабее лидеров серии. 

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

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

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

tab

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

chart

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


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

Плюсы:

  1. Не выявлено.

Минусы:

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

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



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

#ИмяТипОписание
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_CVO.mq5
СкриптИспытательный стенд для CVO
Прикрепленные файлы |
CVO.zip (419.83 KB)
Моделирование рынка: Первые шаги на SQL в MQL5 (II) Моделирование рынка: Первые шаги на SQL в MQL5 (II)
Хотя многие считают, что мы можем без проблем встраивать SQL-код в другой код, обычно это не так. Причина заключается в том, что SQL-код включается в исполняемый файл в виде строки. И тот факт, что SQL-код внедряется в виде строки, хотя и не вызывает проблем в небольших фрагментах, в итоге это может создать нам немало головной боли.
Архитектура машинного обучения для MetaTrader 5 (Часть 16): Вложенная кросс-валидация для несмещённой оценки Архитектура машинного обучения для MetaTrader 5 (Часть 16): Вложенная кросс-валидация для несмещённой оценки
В статье представлен конвейер вложенной кросс-валидации V-in-V для финансовых данных, который устраняет утечку информации в трех точках принятия решений: подбор гиперпараметров, калибровка и итоговая оценка. Временное разделение на три зоны изолирует внутренний walk-forward поиск с правилом 1-SE от внешней walk-forward или CPCV-оценки, а изотоническая OOF (out-of-fold) калибровка обучается независимо. Итоговый UnifiedValidationCalibrator дает несмещенные оценки на вневыборочных данных и хорошо откалиброванные вероятности для продакшена.
Торговые инструменты MQL5 (Часть 26): Интеграция частотного разбиения, энтропии и критерия хи-квадрат в визуальный анализатор Торговые инструменты MQL5 (Часть 26): Интеграция частотного разбиения, энтропии и критерия хи-квадрат в визуальный анализатор
В этой статье мы разработаем инструмент частотного анализа на языке MQL5, который группирует данные о ценах в гистограммы, вычисляет энтропию для оценки информационного содержания и применяет тесты хи-квадрат для проверки соответствия распределения, а также интерактивные логи и статистические панели для более глубокого понимания рыночной структуры. Мы интегрируем режимы вычислений для каждого бара или тика, рендеринг с суперсэмплированием для плавной визуализации и перетаскиваемые/изменяемые по размеру объекты Canvas с автоматически прокручивающимися логами для повышения удобства использования при выполнении торгового анализа.
Моделирование рынка: Первые шаги на SQL в MQL5 (III) Моделирование рынка: Первые шаги на SQL в MQL5 (III)
В предыдущей статье мы рассмотрели пример реализации класса на MQL5 для обеспечения базовой поддержки. Его цель заключается именно в том, чтобы позволить хранить SQL-код в отдельном файле скрипта. Таким образом, нам не потребуется писать тот же SQL-код в виде строки внутри кода MQL5. Хотя данное решение функционально, в нём есть некоторые детали, которые мы можем и должны улучшить.