preview
Алгоритм адаптивного социального поведения — Adaptive Social Behavior Optimization (ASBO): Метод Швефеля, Бокса-Мюллера

Алгоритм адаптивного социального поведения — Adaptive Social Behavior Optimization (ASBO): Метод Швефеля, Бокса-Мюллера

MetaTrader 5Примеры | 18 июля 2024, 12:45
108 0
Andrey Dik
Andrey Dik
Содержание
  1. Введение
  2. Реализация методов алгоритма


1. Введение

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

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

В данной статье рассматривается концепция социальной структуры и ее влияние на процессы принятия решений в группах. Мы также представляем математическую модель, основанную на принципах социального поведения и взаимодействия в обществах, которая может быть применена для достижения глобальной оптимизации. Эта модель, названная ASBO (Adaptive Social Behavior Optimization), учитывает влияние окружающей среды на принятие решений в группе, включая лидерство, соседство и самоорганизацию. Алгоритм "Adaptive Social Behavior Optimization" (ASBO) был предложен Manojo Kumar Singh и опубликован в 2013 году в сборнике трудов конференции "Proceedings of ICAdC, AISC 174" под редакцией Aswatha Kumar M. et al.

Структура социума и модель влияния:

  • Общество - это группа взаимосвязанных живых существ, объединенных общим поведением или характеристиками.
  • Преимущества жизни в обществе: увеличение возможностей добычи, размножения, защиты от хищников, инноваций.
  • Ключевые факторы, влияющие на развитие индивида в обществе: лидерство, соседство, самооценка.
  • Предлагаемая модель ASBO основана на динамическом лидерстве, динамическом логическом соседстве и самооценке.

Основные принципы, заложенные в алгоритм ASBO:

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


2. Реализация методов алгоритма

И перед тем, как мы погрузимся в изучение самого алгоритма, важно понять основную концепцию, лежащую в его основе. Эта концепция связана с использованием метода Швефеля (Schwefel), который представляет собой один из подходов к самоадаптивной мутации и используется в некоторых алгоритмах оптимизации, таких как эволюционные алгоритмы. Основные его особенности:

1. Самоадаптация параметров мутации:

  • У каждого индивида (решения) есть свои собственные параметры мутации (например, размер шага мутации).
  • Эти параметры мутации также эволюционируют вместе с самими решениями.
  • Таким образом, размер шага мутации адаптируется к ландшафту функции для каждого индивида.

2. Гауссова мутация:

  • Для генерации новых решений используется гауссово (нормальное) распределение.
  • Среднее значение мутации равно предыдущему решению, а стандартное отклонение определяется параметрами мутации.

3. Связь между решением и параметрами мутации:

  • Параметры мутации (размер шага) зависят от значения целевой функции (приспособленности) решения.
  • Чем лучше решение, тем меньше размер шага мутации, и наоборот.

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

Пример ниже демонстрирует реализацию концепции Швефеля для оптимизации параметров торговой стратегии. Как работает сам метод, подробно рассмотрим ниже.

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

a. Оценивается приспособленность каждого индивида в популяции.
b. Популяция сортируется по приспособленности.
c. Применяется мутация ко всем индивидам, кроме лучшего.
d. Счетчик поколений увеличивается.

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

// Входные параметры
input int    PopulationSize = 50;   // Размер популяции
input int    Generations = 100;     // Число поколений
input double InitialStepSize = 1.0; // Начальный размер шага

// Структура для хранения индивида
struct Individual
{
    double genes [3];  // Параметры стратегии
    double stepSizes [3];  // Размеры шага для каждого параметра
    double fitness;   // Приспособленность
};

// Глобальные переменные
Individual population [];
int generation = 0;

// Функция инициализации
int OnInit ()
{
  ArrayResize (population, PopulationSize);
  InitializePopulation ();
  return (INIT_SUCCEEDED);
}

// Функция основного цикла
datetime lastOptimizationTime = 0;

void OnTick ()
{
  datetime currentTime = TimeCurrent ();

  // Проверяем, прошли ли сутки с последней оптимизации
  if (currentTime - lastOptimizationTime >= 86400) // 86400 секунд в сутках
  {
    Optimize ();
    lastOptimizationTime = currentTime;
  }

  // Здесь код для торговли с текущими оптимальными параметрами
  TradingLogic ();
}

void Optimize ()
{
  // Код оптимизации (текущее содержимое OnTick)
}

void TradingLogic ()
{
  // Реализация торговой логики с использованием оптимизированных параметров
}

// Инициализация популяции
void InitializePopulation ()
{
  for (int i = 0; i < PopulationSize; i++)
  {
    for (int j = 0; j < 3; j++)
    {
      population [i].genes [j] = MathRand () / 32767.0 * 100;  // Случайные значения от 0 до 100
      population [i].stepSizes [j] = InitialStepSize;
    }
  }
}

// Оценка приспособленности популяции
void EvaluatePopulation ()
{
  for (int i = 0; i < PopulationSize; i++)
  {
    population [i].fitness = CalculateFitness (population [i]);
  }
}

// Расчет приспособленности индивида (здесь нужно реализовать вашу целевую функцию)
double CalculateFitness (Individual &ind)
{
  // Пример простой целевой функции
  return -(MathPow (ind.genes [0] - 50, 2) + MathPow (ind.genes [1] - 50, 2) + MathPow (ind.genes [2] - 50, 2));
}

// Сортировка популяции по убыванию приспособленности
void SortPopulation ()
{
  ArraySort (population, WHOLE_ARRAY, 0, MODE_DESCEND);
}

// Мутация популяции по концепции Швефеля
void Mutate ()
{
  for (int i = 1; i < PopulationSize; i++)  // Начинаем с 1, чтобы сохранить лучшее решение
  {
    for (int j = 0; j < 3; j++)
    {
      // Мутация размера шага
      population [i].stepSizes [j] *= MathExp (0.2 * MathRandom () - 0.1);

      // Мутация гена
      population [i].genes [j] += population [i].stepSizes [j] * NormalRandom ();

      // Ограничение значений генов
      population [i].genes [j] = MathMax (0, MathMin (100, population [i].genes [j]));
    }
  }
}

// Вспомогательная функция для вывода информации об индивиде
void PrintIndividual (Individual &ind)
{
  Print ("Гены: ", ind.genes [0], ", ", ind.genes [1], ", ", ind.genes [2]);
  Print ("Размеры шага: ", ind.stepSizes [0], ", ", ind.stepSizes [1], ", ", ind.stepSizes [2]);
  Print ("Приспособленность: ", ind.fitness);
}

Давайте рассмотрим метод по частям:

1. Структура и входные параметры.

Вначале мы определяем входные параметры для алгоритма и структуру "Individual", которая представляет отдельное решение в популяции. Каждый индивид имеет гены (параметры стратегии), размеры шага для мутации и значение приспособленности.

input int PopulationSize = 50;   // Размер популяции
input int Generations = 100;     // Число поколений
input double InitialStepSize = 1.0; // Начальный размер шага

struct Individual
{
  double genes[3];  // Параметры стратегии
  double stepSizes[3];  // Размеры шага для каждого параметра
  double fitness;   // Приспособленность
};

2. Инициализация.

В функции "OnInit()" мы создаем популяцию и инициализируем ее. Функция "InitializePopulation()" заполняет популяцию случайными значениями генов и устанавливает начальные размеры шага.

int OnInit ()
{
  ArrayResize (population, PopulationSize);
  InitializePopulation ();
  return (INIT_SUCCEEDED);
}

void InitializePopulation ()
{
  for (int i = 0; i < PopulationSize; i++)
  {
    for (int j = 0; j < 3; j++)
    {
      population [i].genes [j] = MathRand () / 32767.0 * 100;
      population [i].stepSizes [j] = InitialStepSize;
    }
  }
}

3. Основной цикл.

В функции "OnTick ()" происходит управление процессом оптимизации. Она оценивает популяцию, сортирует ее, выполняет мутацию и переходит к следующему поколению.

datetime lastOptimizationTime = 0;

void OnTick ()
{
  datetime currentTime = TimeCurrent ();

  // Проверяем, прошли ли сутки с последней оптимизации
  if (currentTime - lastOptimizationTime >= 86400) // 86400 секунд в сутках
  {
    Optimize ();
    lastOptimizationTime = currentTime;
  }

  // Здесь код для торговли с текущими оптимальными параметрами
  TradingLogic ();
}

void Optimize ()
{
  // Код оптимизации (текущее содержимое OnTick)
}

void TradingLogic ()
{
  // Реализация торговой логики с использованием оптимизированных параметров
}

4. Оценка и сортировка популяции.

Эти функции оценивают приспособленность каждого индивида и сортируют популяцию по убыванию приспособленности. Функция "CalculateFitness()" в данном примере простая, однако в реальном применении в ней должна быть целевая функция для оценки торговой стратегии.

void EvaluatePopulation ()
{
  for (int i = 0; i < PopulationSize; i++)
  {
    population [i].fitness = CalculateFitness (population [i]);
  }
}

double CalculateFitness (Individual &ind)
{
  return -(MathPow (ind.genes [0] - 50, 2) + MathPow (ind.genes [1] - 50, 2) + MathPow (ind.genes [2] - 50, 2));
}

void SortPopulation ()
{
  ArraySort (population, WHOLE_ARRAY, 0, MODE_DESCEND);
}

5. Мутация.

Это ключевая часть, реализующая концепцию Швефеля. Для каждого индивида (кроме лучшего) мы:

  • Мутируем размер шага, умножая его на экспоненту случайного числа.
  • Мутируем ген, добавляя к нему нормально распределенное случайное число, умноженное на размер шага.
  • Ограничиваем значения генов в диапазоне [0, 100].

Базовая реализация концепции Швефеля для оптимизации параметров. В реальном применении необходимо адаптировать целевую функцию под конкретную торговую стратегию.

void Mutate ()
{
  for (int i = 1; i < PopulationSize; i++)
  {
    for (int j = 0; j < 3; j++)
    {
      population [i].stepSizes [j] *= MathExp (0.2 * MathRandom () - 0.1);
      population [i].genes [j] += population [i].stepSizes [j] * NormalRandom ();
      population [i].genes [j] = MathMax (0, MathMin (100, population [i].genes [j]));
    }
  }
}

Отдельно стоит отметить реализацию функции "NormalRandom()", которая является частью концепции Швефеля для адаптивной мутации и реализует метод Бокса-Мюллера для генерации случайных чисел с нормальным (гауссовым) распределением. Давайте разберем эту функцию:

1. Генерация равномерно распределенных чисел. Генерируем два независимых случайных числа  "u1" и "u2", равномерно распределенных в интервале [0, 1].

double u1 = u.RNDfromCI(0, 1);
double u2 = u.RNDfromCI(0, 1); 

2. Преобразование в нормальное распределение. Формула преобразования Бокса-Мюллера преобразует равномерно распределенные числа в нормально распределенные.

return MathSqrt(-2 * MathLog(u1)) * MathCos(2 * M_PI * u2);

Важно отметить, что это половина реализации преобразования Бокса-Мюллера и генерирует одно нормально распределенное число. Полное преобразование генерирует два нормально распределенных числа:

z0 = MathSqrt(-2 * MathLog(u1)) * MathCos(2 * M_PI * u2);
z1 = MathSqrt(-2 * MathLog(u1)) * MathSin(2 * M_PI * u2);

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

Характеристики генерируемых чисел:

1. Распределение: Нормальное (гауссово) распределение
2. Среднее значение: 0
3. Стандартное отклонение: 1

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

  • Около 68% генерируемых чисел будут находиться в диапазоне [-1, 1].
  • Около 95% чисел будут в диапазоне [-2, 2].
  • Около 99.7% чисел будут в диапазоне [-3, 3].

Очень редко могут генерироваться числа, выходящие за пределы [-4, 4], вероятность этого крайне мала.

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

Напишем скрипт для проверки функции "NormalRandom()":

#property version   "1.00"
#property script_show_inputs

input int NumSamples = 10000; // Количество генерируемых чисел

double NormalRandom ()
{
  double u1 = (double)MathRand () / 32767.0;
  double u2 = (double)MathRand () / 32767.0;
  return MathSqrt (-2 * MathLog (u1)) * MathCos (2 * M_PI * u2);
}

void OnStart ()
{
  double sum = 0;
  double sumSquared = 0;
  double min = DBL_MAX;
  double max = DBL_MIN;

  int histogram [];
  ArrayResize (histogram, 20);
  ArrayInitialize (histogram, 0);

  // Генерация и анализ случайных чисел
  for (int i = 0; i < NumSamples; i++)
  {
    double value = NormalRandom ();
    sum += value;
    sumSquared += value * value;

    if (value < min) min = value;
    if (value > max) max = value;

    // Заполнение гистограммы
    int index = (int)((value + 4) / 0.4); // Разбиваем диапазон [-4, 4] на 20 интервалов
    if (index >= 0 && index < 20) histogram [index]++;
  }

  // Расчет статистики
  double mean = sum / NumSamples;
  double variance = (sumSquared - sum * sum / NumSamples) / (NumSamples - 1);
  double stdDev = MathSqrt (variance);

  // Вывод результатов
  Print ("Статистика для ", NumSamples, " сгенерированных чисел:");
  Print ("Среднее значение: ", mean);
  Print ("Стандартное отклонение: ", stdDev);
  Print ("Минимальное значение: ", min);
  Print ("Максимальное значение: ", max);

  // Вывод гистограммы
  Print ("Гистограмма распределения:");
  for (int i = 0; i < 20; i++)
  {
    string bar = "";
    for (int j = 0; j < histogram [i] * 50 / NumSamples; j++) bar += "*";
    PrintFormat ("[%.1f, %.1f): %s", -4 + i * 0.4, -3.6 + i * 0.4, bar);
  }
}

Проверочный скрипт делает следующее:

1. Определяет функцию "NormalRandom()".
2. Генерирует заданное количество случайных чисел (по умолчанию 10000).
3. Вычисляет основные статистические характеристики: среднее значение, стандартное отклонение, минимальное и максимальное значения.
4. Создает гистограмму распределения, разбивая диапазон [-4, 4] на 20 интервалов.
5. Выводит результаты в журнал терминала MetaTrader.

А теперь протестируем скрипт, возьмем 20000 значений. Распечатка работы скрипта для проверки метода преобразования Бокса-Мюллера:

2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    Статистика для 20000 сгенерированных чисел:
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    Среднее значение: -0.003037802901958332
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    Стандартное отклонение: 0.9977477093538349
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    Минимальное значение: -3.865371560675546
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    Максимальное значение: 3.4797509297243994
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    Гистограмма распределения:
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    [-4.0, -3.6):
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    [-3.6, -3.2):
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    [-3.2, -2.8):
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    [-2.8, -2.4):
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    [-2.4, -2.0):
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    [-2.0, -1.6): *
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    [-1.6, -1.2): **
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    [-1.2, -0.8): ****
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    [-0.8, -0.4): ******
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    [-0.4, 0.0): *******
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    [0.0, 0.4): *******
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    [0.4, 0.8): ******
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    [0.8, 1.2): ****
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    [1.2, 1.6): ***
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    [1.6, 2.0): *
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    [2.0, 2.4):
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    [2.4, 2.8):
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    [2.8, 3.2):
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    [3.2, 3.6):
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    [3.6, 4.0):

Из распечатки видно, что метод работает верно, стандартное отклонение практически равно 1, среднее значение в 0, разброс соотвествует интервалу [-4;4].

Далее переходим к расчету адаптивных параметров мутации и написанию функции:

//——————————————————————————————————————————————————————————————————————————————
void AdaptiveMutation (double &Cg, double &Cs, double &Cn)
{
  Cg *= MathExp (tau_prime * NormalRandom() + tau * NormalRandom());
  Cs *= MathExp (tau_prime * NormalRandom() + tau * NormalRandom());
  Cn *= MathExp (tau_prime * NormalRandom() + tau * NormalRandom());
}
//——————————————————————————————————————————————————————————————————————————————

Адаптивные параметры Cg, Cs и Cn рассчитываются с использованием стратегии самоадаптивной мутации, основанной на концепции, предложенной Швефелем. Формулы для расчета этих параметров:

1. Инициализация:

  • Популяция из N решений, где каждое решение представлено в виде пары векторов (pi, σi), где i ∈ {0, 1, 2} соответствует трем параметрам Cg, Cs и Cn.
  • Начальные значения компонентов pi выбираются случайно в соответствии с равномерным распределением в предполагаемом пространстве решений.
  • Начальные значения σi устанавливаются в некоторое фиксированное значение.

2. Генерация потомков:

  • Для каждого родителя (pi, σi) генерируется потомок (pi', σi') по следующим формулам:
σ'i (j) = σi (j) * exp (τ' * N (0,1) + τ * Nj (0,1))
p'i (j) = pi (j) + σi (j) * N (0,1)

Где pi (j), p'i (j), σi (j), σ'i (j) - j-я компонента векторов pi, p'i, σi, σ'i соответственно.

  • N (0,1) - случайное число, взятое из нормального распределения со средним 0 и стандартным отклонением 1.
  • Nj (0,1) - случайное число, взятое из нормального распределения со средним 0 и стандартным отклонением 1, где j - счетчик.
  • τ и τ' - масштабирующие факторы, установленные в (√(2√n))^-1 и (√(2n))^-1 соответственно, где n - размерность задачи.

Таким образом, адаптивные параметры Cg, Cs и Cn мутируют в соответствии с этой самоадаптивной стратегией, позволяя им динамически подстраиваться в процессе оптимизации.

Как можно увидеть ниже из распечатки получения значений коэффициентов Cg, Cs и Cn, в некоторых отдельных случаях эти коэффициенты приобретают слишком большие значения. Это связано с тем, что в уравнении для обновления стратегических параметров σ новое значение получается путём умножения на предыдущее. Это позволяет адаптировать параметры к сложной поверхности целевой функции, но в то же время может приводить к нестабильности и резким скачкам значений.

Давайте посмотрим, какие значения принимают Cg, Cs и Cn:

1.3300705071425474 0.0019262948596068497 0.00015329292900983978
1.9508542383574192 0.00014860860614036015 7007.656113084095
52.13323602205895 1167.5425054524449 0.0008421503214593158
1.0183156975753507 0.13486291437434697 0.18290674582014257
0.00003239513683361894 61.366694225534175 45.11710564761292
0.0004785111900713668 0.4798661298436033 0.007932035893070274
2712198854.6325 0.00003936758705232012 325.9282730206205
0.0016658142882911 22123.863582276706 1.6844067196638965
2.0422888701555126 0.007999762224016285 0.02460292446531715
7192.66545492709 0.000007671729921045711 0.3705939923291289
0.0073279981653727785 3237957.2544339723 1.6750241266497745e-07
550.7433921368721 13.512466529311943 223762.44571145615
34.571961515974785 0.000008292503593679501 0.008122937723317175
0.000002128739177639208 63.17654973794633 128927.83801094144
866.7293481660888 1260.0820389718326 1.8496629497788273
0.000008459817609364248 25.623751292511788 0.0013478840638847347
27.956286711833616 0.0006967869388129299 0.0006885039945210606
66.6928872126239 47449.76869262452 8934.079392419668
0.15058617433681198 0.003114981958516487 7.703748428996011e-07
0.22147618633450808 265.4903003920267 315.20318731505455
0.0000015873778483580056 1134.6304274682934 0.7883024873065534

Когда параметры Cg, Cs и Cn принимают очень большие значения в результате самоадаптивной мутации по методу Швефеля, может быть необходимо принять меры для контроля и ограничения этих значений. Это важно для сохранения стабильности и эффективности алгоритма. Возможны несколько подходов, которыми можно руководствоваться для ограничения численных значений коэффициентов:

1. Ограничение значений.
Установить верхний и нижний пределы для Cg, Cs и Cn. Например: 

void LimitParameters (double &param, double minValue, double maxValue)
{
  param = MathMax (minValue, MathMin (param, maxValue));
}

// Применение:
LimitParameters (Cg, 0.0, 2.0);
LimitParameters (Cs, 0.0, 2.0);
LimitParameters (Cn, 0.0, 2.0); 

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

void NormalizeParameters (double &Cg, double &Cs, double &Cn)
{
  double sum = Cg + Cs + Cn;
  if (sum > 0)
  {
    Cg /= sum;
    Cs /= sum;
    Cn /= sum;
  }
  else
  {
    // Если сумма равна 0, установите равные значения
    Cg = Cs = Cn = 1.0 / 3.0;
  }
}

3. Логарифмическое масштабирование.
Применить логарифмическое масштабирование для сглаживания больших значений:

double ScaleParameter (double param)
{
  if (param == 0) return 0;

  double sign = (param > 0) ? 1 : -1;
  return sign * MathLog (1 + MathAbs (param));
}

4. Адаптивное масштабирование шага мутации.
Уменьить размер шага мутации, если параметры становятся слишком большими:

 void AdaptMutationStep(double &stepSize, double paramValue)
{
  if(MathAbs(paramValue) > threshold)
  {
    stepSize *= 0.9; // Уменьшаем размер шага
  }
}

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

void ResetParameters(int generationCount)
{
  if(generationCount % resetInterval == 0)
  {
    Cg = initialCg;
    Cs = initialCs;
    Cn = initialCn;
  }
}

6. Использование экспоненциальной функции.
Применить экспоненциальную функцию для ограничения роста параметров:

double LimitGrowth(double param)
{
  return 2.0 / (1.0 + MathExp(-param)) - 1.0; // Ограничивает значения в диапазоне [-1, 1]
}

7. Мониторинг и адаптация.
Следить за значениями параметров и адаптировать стратегию мутации, если они часто выходят за допустимые пределы:

void MonitorAndAdapt(double &param, int &outOfBoundsCount)
{
  if(MathAbs(param) > maxAllowedValue)
  {
    outOfBoundsCount++;
    if(outOfBoundsCount > threshold)
    {
      // Адаптация стратегии мутации
      AdjustMutationStrategy();
    }
  }
}

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

//——————————————————————————————————————————————————————————————————————————————
void C_AO_ASBO::AdaptiveMutation (S_ASBO_Agent &ag)
{
  ag.Cg *= MathExp (tau_prime * u.GaussDistribution (0, -1, 1, 1) + tau * u.GaussDistribution (0, -1, 1, 8));
  ag.Cs *= MathExp (tau_prime * u.GaussDistribution (0, -1, 1, 1) + tau * u.GaussDistribution (0, -1, 1, 8));
  ag.Cn *= MathExp (tau_prime * u.GaussDistribution (0, -1, 1, 1) + tau * u.GaussDistribution (0, -1, 1, 8));
}
//——————————————————————————————————————————————————————————————————————————————

Как мы можем увидеть ниже из распечатки получения значений коэффициентов Cg, Cs и Cn, после применения моей функции большие значения встречаются намного реже, чем с применением метода Бокса-Мюллера.

0.025582051880112085 0.6207719272290446 0.005335225840354781
0.9810075068811726 0.16583946164135704 0.01016969794039794
0.006133031813953609 17.700790930206647 0.3745475117676483
1.4547663270710334 0.3537259667123157 0.08834618264409702
0.11125695415944291 0.28183794982955684 0.09051405673590024
0.06340035225180855 0.16270375413207716 0.36885953030567936
0.008575136469231127 2.5881627332149053 0.11237602809318312
0.00001436227841400286 0.02323530434501054 10.360403964016376
0.936476760121053 0.017321731852758693 0.40372788912091845
0.009288586536835293 0.0000072639468670123115 15.463139841665908
0.15092489031689466 0.02160456749606 0.011008504295160867
0.0100807047494077 0.4592091893288436 0.0343285901385665
0.010014652012224212 0.0014577046664934706 0.006484475820059919
0.0002654495048564554 0.0005018788250576451 1.8639207859646574
5.972802450172414 0.10070170017416721 0.9226557559293936
0.011441827382547332 14.599641192191408 0.00007257778906744059
0.7249805357484554 0.000004301248511125035 0.2718776654314797
5.019113547774523 0.11351424171113386 0.02129848352762841
0.023978285994614518 0.041738711812672386 1.0247944259605422
0.0036842456260203237 12.869472963773408 1.5167205157941646
0.4529181577133935 0.0000625576761842319 30.751931508050227
0.5555092369559338 0.9606330180338433 0.39426099685543164
0.036106836251057275 2.57811344513881 0.042016638784243526
3.502119772985753 128.0263928713568 0.9925745499516576
279.2236061102195 0.6837013166327449 0.01615639677602729
0.09687457825904996 0.3812813151133578 0.5272720937749686

Теперь, когда мы разобрались с концепцией Швефеля, а также с адаптивными значениями коэффициентов, перейдем к методу определения ближайших соседей в алгоритме. Для определения координат ближайших соседей "Nc" используется следующий подход:
1. Для каждого индивидуума в популяции определяются его ближайшие соседи.
2. Ближайшими соседями считаются три индивидуума, имеющие ближайшие значения целевой функции (фитнеса) к данному индивидууму.
3. Координаты центра группы, образованной данным индивидуумом и его ближайшими соседями, вычисляются как среднее арифметическое координат этих трех соседей.

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

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

//——————————————————————————————————————————————————————————————————————————————
void C_AO_ASBO::FindNeighborCenter (const S_ASBO_Agent &ag, double &center [])
{
// Создаем массивы для индексов и разниц фитнеса
  int indices [];
  double differences [];
  ArrayResize (indices, popSize - 1);
  ArrayResize (differences, popSize - 1);

// Заполняем массивы
  int count = 0;
  for (int i = 0; i < popSize; i++)
  {
    if (&a [i] != &ag)  // Исключаем текущего агента
    {
      indices [count] = i;
      differences [count] = MathAbs (a [i].fitness - ag.fitness);
      count++;
    }
  }

// Сортируем массивы по разнице фитнеса (пузырьковая сортировка)
  for (int i = 0; i < count - 1; i++)
  {
    for (int j = 0; j < count - i - 1; j++)
    {
      if (differences [j] > differences [j + 1])
      {
        // Обмен разниц
        double tempDiff = differences [j];
        differences [j] = differences [j + 1];
        differences [j + 1] = tempDiff;

        // Обмен индексов
        int tempIndex = indices [j];
        indices [j] = indices [j + 1];
        indices [j + 1] = tempIndex;
      }
    }
  }

// Инициализируем центр
  ArrayInitialize (center, 0.0);

// Вычисляем центр на основе трех ближайших соседей
  for (int j = 0; j < coords; j++)
  {
    for (int k = 0; k < 3; k++)
    {
      int nearestIndex = indices [k];
      center [j] += a [nearestIndex].c [j];
    }
    center [j] /= 3;
  }
}
//——————————————————————————————————————————————————————————————————————————————

Объяснение метода:

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

Давайте проанализируем эту функцию:

1. Направление сортировки:
Сортировка осуществляется по возрастанию разницы фитнеса.

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

2. Функция выполняет следующие шаги:

  • Исключает текущего агента из рассмотрения.
  • Вычисляет разницу фитнеса между текущим агентом и всеми остальными.
  • Сортирует агентов по этой разнице.
  • Выбирает трех ближайших соседей (с наименьшей разницей фитнеса)
  • Вычисляет центр группы на основе координат этих трех соседей
void C_AO_ASBO::FindNeighborCenter(int ind, S_ASBO_Agent &ag[], double &center[])
{
  int indices[];
  double differences[];
  ArrayResize(indices, popSize - 1);
  ArrayResize(differences, popSize - 1);

  int count = 0;
  for (int i = 0; i < popSize; i++)
  {
    if (i != ind)
    {
      indices[count] = i;
      differences[count] = MathAbs(ag[ind].f - ag[i].f);
      count++;
    }
  }

// Сортировка по возрастанию разницы фитнеса
  for (int i = 0; i < count - 1; i++)
  {
    for (int j = 0; j < count - i - 1; j++)
    {
      if (differences[j] > differences[j + 1])
      {
        double tempDiff = differences[j];
        differences[j] = differences[j + 1];
        differences[j + 1] = tempDiff;

        int tempIndex = indices[j];
        indices[j] = indices[j + 1];
        indices[j + 1] = tempIndex;
      }
    }
  }

  ArrayInitialize(center, 0.0);

  int neighborsCount = MathMin(3, count);  // Защита от случая, когда агентов меньше 3
  for (int j = 0; j < coords; j++)
  {
    for (int k = 0; k < neighborsCount; k++)
    {
      int nearestIndex = indices[k];
      center[j] += ag[nearestIndex].c[j];
    }
    center[j] /= neighborsCount;
  }
}

Эта версия функции более устойчива к ошибкам и правильно обрабатывает случаи с малым количеством (меньше 3) агентов в популяции. Мы познакомились с основными логическими методами в алгоритме, теперь можем переходить к рассмотрению структуры самого алгоритма. Алгоритм ASBO (Adaptive Social Behavior Optimization) состоит из следующих основных шагов:

1. Инициализация:

  • Определяется начальная популяция решений, где каждое решение представлено в явном виде (не в закодированном формате).
  • Для каждого решения вычисляется значение целевой функции, которое определяет его пригодность.
  • Решение с максимальным значением пригодности объявляется глобальным лидером на данный момент времени.
  • Для каждого решения определяется группа ближайших соседей, имеющих следующие наибольшие значения пригодности.

2. Адаптивная мутация параметров:

  • Для каждого решения определяется вектор из трех адаптивных параметров: Cg, Cs и Cn, отвечающих за влияние глобального лидера, личного лучшего решения и центра группы соседей соответственно.
  • Применяется самоадаптивная стратегия мутации Швефеля для обновления значений этих параметров.

3. Обновление положения решений:

  • Для каждого решения вычисляется изменение его положения используя текущие значения параметров Cg, Cs и Cn.
  • Новое положение решения вычисляется путем добавления изменения к текущему положению.

4. Двухфазный процесс:

  • Фаза 1: Создается M независимых популяций, каждая из которых обрабатывается алгоритмом ASBO в течение фиксированного числа итераций. Сохраняются значения пригодности и параметров для каждого решения из финальных популяций.
  • Фаза 2: Из всех финальных популяций выбираются лучшие решения по пригодности, и их параметры используются для формирования новой популяции. Над полученной новой популяцией применяется основная логика алгоритма ASBO для получения окончательного решения.

Итак, подчеркнём ключевые особенности алгоритма ASBO:

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


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


Прикрепленные файлы |
Изучение MQL5 — от новичка до профи (Часть III): Сложные типы данных и подключаемые файлы Изучение MQL5 — от новичка до профи (Часть III): Сложные типы данных и подключаемые файлы
Статья является третьей в серии материалов об основных аспектах программирования на MQL5. Здесь описываются сложные типы данных, которые не были описаны в предыдущей статье, включая структуры, объединения, классы и тип данных "функция". Также рассказано, как добавить модульности нашей программе с помощью директивы препроцессора #include.
Модифицированный советник Grid-Hedge в MQL5 (Часть III): Оптимизация простой хеджирующей стратегии (I) Модифицированный советник Grid-Hedge в MQL5 (Часть III): Оптимизация простой хеджирующей стратегии (I)
В третьей части мы вернемся к советникам Simple Hedge и Simple Grid, разработанным ранее. Теперь мы займемся совершенствованием советника Simple Hedge с помощью математического анализа и подхода грубой силы (brute force) с целью оптимального использования стратегии. Эта статья углубляется в математическую оптимизацию стратегии, закладывая основу для будущего исследования оптимизации на основе кода в последующих частях.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Парадигмы программирования (Часть 2): Объектно-ориентированный подход к разработке советника на основе ценовой динамики Парадигмы программирования (Часть 2): Объектно-ориентированный подход к разработке советника на основе ценовой динамики
В этой статье мы поговорим о парадигме объектно-ориентированного программирования и ее применении в коде MQL5. Это вторая статья в серии. В ней мы познакомимся с особенностями объектно-ориентированного программирования и рассмотрим практические примеры. В прошлый раз мы написали советник на основе ценовой динамики (Price Action), используя индикатор EMA и свечные данные. Сейчас мы преобразуем его процедурный код в объектно-ориентированный.