English Русский 中文 Español 日本語 Português
preview
Adaptive Social Behavior Optimization (ASBO): Das Verfahren von Schwefel und Box-Muller

Adaptive Social Behavior Optimization (ASBO): Das Verfahren von Schwefel und Box-Muller

MetaTrader 5Beispiele | 5 Februar 2025, 09:29
85 0
Andrey Dik
Andrey Dik
Inhalt
  1. Einführung
  2. Implementierung der Algorithmusmethoden


1. Einführung

Das kollektive Verhalten von Lebewesen zeugt von einer unglaublichen Weisheit. Von Fischschwärmen bis zu menschlichen Gesellschaften sind Kooperation und Zusammenarbeit der Schlüssel zu Überleben und Wohlstand. Was aber, wenn wir diese Prinzipien der sozialen Struktur nutzen könnten, um neue Optimierungsalgorithmen zu entwickeln, die komplexe Probleme effizient und genau lösen können?

In der Natur gibt es viele Beispiele für Gruppenverhalten, bei denen sich Lebewesen zu Gesellschaften zusammenschließen, um ihre Überlebens- und Innovationschancen zu erhöhen. Dieses Phänomen, das im Tierreich, in der menschlichen Gesellschaft und in anderen Lebensformen zu beobachten ist, hat sich zu einem faszinierenden Untersuchungsgegenstand für Evolutionsbiologen und Sozialphilosophen entwickelt. Durch die Untersuchung solcher Gesellschaften wurde ein Computermodell entwickelt, das ihr erfolgreiches Funktionieren im Hinblick auf bestimmte Ziele simuliert. Diese Modelle, wie die Partikelschwarmoptimierung und die Ameisenkolonieoptimierung, zeigen die Effizienz der Gruppenarbeit bei der Lösung von Optimierungsproblemen.

In diesem Artikel wird das Konzept der Sozialstruktur und ihr Einfluss auf Entscheidungsprozesse in Gruppen untersucht. Außerdem stellen wir ein mathematisches Modell vor, das auf den Prinzipien des Sozialverhaltens und der Interaktion in Gesellschaften basiert und zur globalen Optimierung eingesetzt werden kann. Dieses Modell mit der Bezeichnung ASBO (Adaptive Social Behavior Optimization oder Adaptive Optimierung des Sozialverhaltens) berücksichtigt den Einfluss der Umwelt auf die Entscheidungsfindung von Gruppen, einschließlich Führung, Nachbarschaft und Selbstorganisation. Der Algorithmus wurde von Manojo Kumar Singh vorgeschlagen und 2013 in "Proceedings of ICAdC, AISC 174" veröffentlicht, herausgegeben von Aswatha Kumar M. et al.

Die Struktur der Gesellschaft und das Modell des Einflusses:

  • Eine Gesellschaft ist eine Gruppe von miteinander verbundenen Lebewesen, die durch gemeinsame Verhaltensweisen oder Merkmale vereint sind.
  • Die Vorteile des Lebens in der Gesellschaft: bessere Möglichkeiten zur Beutejagd, Fortpflanzung, Schutz vor Raubtieren, Innovation.
  • Schlüsselfaktoren, die die Entwicklung eines Individuums in der Gesellschaft beeinflussen: Führung, Nachbarschaft, Selbstwertgefühl.
  • Das vorgeschlagene ASBO-Modell basiert auf dynamischer Führung, dynamischer logischer Nachbarschaft und Selbsteinschätzung.

Die wichtigsten Grundsätze des ASBO-Algorithmus:

  • Eine Population von Lösungen, von denen jede ein Vektor in einem D-dimensionalen Raum ist.
  • Für jede Lösung werden drei Vektoren berechnet: der global beste, der persönlich beste und der Nachbarschaftsvektor.
  • Die neue Lösungsposition wird mit Hilfe von adaptiven Einflussverhältnissen berechnet.
  • Die Einflussverhältnisse werden durch eine selbstanpassende Mutationsstrategie angepasst.


2. Implementierung der Algorithmusmethoden

Bevor wir uns mit dem Algorithmus selbst beschäftigen, ist es wichtig, das grundlegende Konzept dahinter zu verstehen. Dieses Konzept ist mit Schwefels Methode verwandt, die zu den Ansätzen der selbstanpassenden Mutation gehört und in einigen Optimierungsalgorithmen, z. B. den evolutionären, verwendet wird. Seine wichtigsten Merkmale:

1. Selbstanpassung der Mutationsparameter:

  • Jedes Individuum (Lösung) hat seine eigenen Mutationsparameter (z. B. die Größe des Mutationsschritts).
  • Auch diese Mutationsparameter entwickeln sich mit den Lösungen selbst weiter.
  • So wird die Mutationsschrittweite an die Funktionslandschaft für jedes Individuum angepasst.

2. Gaußsche Mutation:

  • Die Gaußsche (normale) Verteilung wird verwendet, um neue Lösungen zu generieren.
  • Der mittlere Mutationswert entspricht der vorherigen Lösung, und die Standardabweichung wird durch die Mutationsparameter bestimmt.

3. Beziehung zwischen Lösungs- und Mutationsparametern:

  • Die Mutationsparameter (Schrittgröße) hängen vom Wert der Zielfunktion (Fitness) der Lösung ab.
  • Je besser die Lösung, desto kleiner die Mutationsschrittweite und umgekehrt.

Die Idee hinter Schwefels Konzept ist, dass die Anpassung der Mutationsparameter es dem Algorithmus ermöglicht, den Lösungsraum effizienter zu erkunden, insbesondere in den späteren Phasen der Suche, wenn eine genauere Abstimmung der Lösungen erforderlich ist.

Das folgende Beispiel demonstriert die Umsetzung des Schwefelschen Konzepts zur Optimierung der Parameter einer Handelsstrategie. Der Vorgang der Methode selbst wird im Folgenden erläutert.

Nehmen wir als Beispiel den einfachsten hypothetischen EA, bei dem eine anfängliche Population von Zufallslösungen während der Initialisierung durch OnInit erstellt wird. Ein Schritt des evolutionären Prozesses ist in OnTick abgeschlossen:

a. Die Fitness eines jeden Individuums in der Population wird bewertet.
b. Die Population wird nach der Fitness sortiert.
c. Die Mutation wird auf alle Individuen außer dem besten angewandt.
d. Der Generationenzähler wird erhöht.

Dieser Vorgang wird so lange wiederholt, bis eine bestimmte Anzahl von Generationen erreicht ist. Nach Abschluss der Optimierung wird die beste gefundene Lösung angezeigt.

// Inputs
input int    PopulationSize = 50;   // Population size
input int    Generations = 100;     // Number of generations
input double InitialStepSize = 1.0; // Initial step size

// Structure for storing an individual
struct Individual
{
    double genes [3];  // Strategy parameters
    double stepSizes [3];  // Step sizes for each parameter
    double fitness;   // Fitness
};

// Global variables
Individual population [];
int generation = 0;

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

// Main loop function
datetime lastOptimizationTime = 0;

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

  // Check if a day has passed since the last optimization
  if (currentTime - lastOptimizationTime >= 86400) // 86400 seconds in a day
  {
    Optimize ();
    lastOptimizationTime = currentTime;
  }

  // Here is the code for trading with the current optimal parameters
  TradingLogic ();
}

void Optimize ()
{
  // Optimization code (current OnTick content)
}

void TradingLogic ()
{
  // Implementing trading logic using optimized parameters
}

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

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

// Calculate the fitness of an individual (this is where you need to implement your objective function)
double CalculateFitness (Individual &ind)
{
  // Example of a simple objective function
  return -(MathPow (ind.genes [0] - 50, 2) + MathPow (ind.genes [1] - 50, 2) + MathPow (ind.genes [2] - 50, 2));
}

// Sort the population in descending order of fitness
void SortPopulation ()
{
  ArraySort (population, WHOLE_ARRAY, 0, MODE_DESCEND);
}

// Population mutation according to Schwefel's concept
void Mutate ()
{
  for (int i = 1; i < PopulationSize; i++)  // Start from 1 to keep the best solution
  {
    for (int j = 0; j < 3; j++)
    {
      // Step size mutation
      population [i].stepSizes [j] *= MathExp (0.2 * MathRandom () - 0.1);

      // Gene mutation
      population [i].genes [j] += population [i].stepSizes [j] * NormalRandom ();

      // Limiting gene values
      population [i].genes [j] = MathMax (0, MathMin (100, population [i].genes [j]));
    }
  }
}

// Auxiliary function for displaying information about an individual
void PrintIndividual (Individual &ind)
{
  Print ("Genes: ", ind.genes [0], ", ", ind.genes [1], ", ", ind.genes [2]);
  Print ("Step sizes: ", ind.stepSizes [0], ", ", ind.stepSizes [1], ", ", ind.stepSizes [2]);
  Print ("Fitness: ", ind.fitness);
}

Schauen wir uns die Methode in Teilen an:

1. Struktur und Inputs.

Zunächst definieren wir die Eingaben für den Algorithmus und die Individualstruktur, die eine einzelne Lösung in der Population darstellt. Jedes Individuum hat Gene (Strategieparameter), Schrittgrößen für die Mutation und einen Fitnesswert.

input int PopulationSize = 50;   // Population size
input int Generations = 100;     // Number of generations
input double InitialStepSize = 1.0; // Initial step size

struct Individual
{
  double genes[3];  // Strategy parameters
  double stepSizes[3];  // Step sizes for each parameter
  double fitness;   // Fitness
};

2. Initialisierung.

In der Funktion OnInit() erstellen wir die Population und initialisieren sie. Die Funktion InitializePopulation() füllt die Population mit zufälligen Genwerten und setzt die anfänglichen Schrittgrößen.

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. Hauptschleife.

Der Optimierungsprozess wird durch die Funktion OnTick () gesteuert. Es wertet die Population aus, sortiert sie, führt die Mutation durch und geht zur nächsten Generation über.

datetime lastOptimizationTime = 0;

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

  // Check if a day has passed since the last optimization
  if (currentTime - lastOptimizationTime >= 86400) // 86400 seconds in a day
  {
    Optimize ();
    lastOptimizationTime = currentTime;
  }

  // Here is the code for trading with the current optimal parameters
  TradingLogic ();
}

void Optimize ()
{
  // Optimization code (current OnTick content)
}

void TradingLogic ()
{
  // Implementing trading logic using optimized parameters
}

4. Bewertung und Sortierung der Population.

Diese Funktionen bewerten die Fitness jedes Individuums und sortieren die Population in absteigender Reihenfolge der Fitness. In diesem Beispiel ist die Funktion CalculateFitness() einfach, aber in der Praxis sollte sie eine Zielfunktion zur Bewertung der Handelsstrategie haben.

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. Mutation.

Dies ist der Schlüsselteil, der das Schwefelsche Konzept umsetzt. Für jeden Einzelnen (außer den Besten):

  • Wir ändern die Schrittweite, indem wir sie mit dem Exponenten einer Zufallszahl multiplizieren.
  • Ein Gen wird mutiert, indem eine normalverteilte Zufallszahl, multipliziert mit der Schrittweite, zu dem Gen hinzugefügt wird.
  • Die Werte der Genen werden auf den Bereich [0, 100] begrenzt.

Grundlegende Umsetzung des Schwefelschen Konzepts zur Parameteroptimierung. In realen Anwendungen ist es notwendig, die Zielfunktion an eine bestimmte Handelsstrategie anzupassen.

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]));
    }
  }
}

Erwähnenswert ist auch die Implementierung der Funktion NormalRandom(), die Teil von Schwefels Konzept der adaptiven Mutation ist und die Box-Muller-Methode zur Erzeugung von Zufallszahlen mit normaler (Gaußscher) Verteilung implementiert. Wir wollen diese Funktion aufschlüsseln:

1. Generierung gleichmäßig verteilter Zahlen. Wir erzeugen zwei unabhängige Zufallszahlen u1 und u2, die gleichmäßig im Intervall [0, 1] verteilt sind.

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

2. Transformation in die Normalverteilung. Die Box-Muller-Transformationsgleichung wandelt gleichmäßig verteilte Zahlen in normal verteilte Zahlen um.

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

Es ist wichtig zu beachten, dass es sich hierbei um die Hälfte der Implementierung der Box-Muller-Transformation handelt, die eine einzige normalverteilte Zahl erzeugt. Die vollständige Transformation erzeugt zwei normalverteilte Zahlen:

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

Unsere Implementierung verwendet nur den Kosinus, der eine einzige normalverteilte Zahl ergibt. Dies ist durchaus akzeptabel, wenn wir nur eine Nummer pro Aufruf benötigen. Wenn beide Zahlen benötigt werden, kann eine Sinusberechnung hinzugefügt werden.
Diese Implementierung ist effizient und wird häufig zur Erzeugung normalverteilter Zufallszahlen in verschiedenen Anwendungen eingesetzt, darunter evolutionäre Algorithmen und stochastische Optimierung.

Merkmale der generierten Zahlen:

1. Verteilung: Normale (Gaußsche) Verteilung
2. Durchschnittlicher Wert: 0
3. Standardabweichung: 1

Bereich der generierten Zahlen: Theoretisch kann die Normalverteilung Zahlen im Bereich von minus unendlich bis plus unendlich erzeugen. In der Praxis:

  • Etwa 68 % der erzeugten Zahlen liegen im Bereich [-1, 1].
  • Etwa 95 % der Zahlen werden im Bereich [-2, 2] liegen.
  • Etwa 99,7 % der Zahlen liegen im Bereich [-3, 3].

In sehr seltenen Fällen kann es auch Zahlen außerhalb von [-4, 4] geben.

Die Box-Muller-Methode wird verwendet, um Zufallszahlen mit Normalverteilung zu erzeugen, was für die Implementierung der selbstanpassenden Mutation in dem auf dem Schwefelschen Konzept basierenden Algorithmus wichtig ist. Diese Verteilung ermöglicht es, dass kleinere Änderungen häufiger erzeugt werden, erlaubt aber manchmal auch größere Mutationen, was eine effiziente Erkundung des Lösungsraums erleichtert. Wir wollen die Box-Muller-Methode in der Praxis testen und bewerten.

Schreiben wir einen Skript, um die Funktion NormalRandom() zu testen:

#property version   "1.00"
#property script_show_inputs

input int NumSamples = 10000; // Amount of generated numbers

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);

  // Random number generation and analysis
  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;

    // Filling the histogram
    int index = (int)((value + 4) / 0.4); // Split the range [-4, 4] into 20 intervals
    if (index >= 0 && index < 20) histogram [index]++;
  }

  // Calculate statistics
  double mean = sum / NumSamples;
  double variance = (sumSquared - sum * sum / NumSamples) / (NumSamples - 1);
  double stdDev = MathSqrt (variance);

  // Display results
  Print ("Statistics for ", NumSamples, " generated numbers:");
  Print ("Average value: ", mean);
  Print ("Standard deviation: ", stdDev);
  Print ("Minimum value: ", min);
  Print ("Maximum value: ", max);

  // Display the histogram
  Print ("Distribution histogram:");
  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);
  }
}

Das Testskript tut Folgendes:

1. Es definiert die Funktion NormalRandom().
2. Es erzeugt eine bestimmte Anzahl von Zufallszahlen (Standard ist 10.000).
3. Es berechnet die grundlegenden statistischen Merkmale: Mittelwert, Standardabweichung, Mindest- und Höchstwerte.
4. Es erstellen ein Verteilungshistogramm, indem es den Bereich [-4, 4] in 20 Intervalle unterteilt.
5. Es zeigt die Ergebnisse im Protokoll des MetaTrader-Terminals.

Nun wollen wir das Skript testen. Wir werden 20.000 Werte nehmen. Hier der Ausdruck des Skripts, das zum Testen der Box-Muller-Transformationsmethode ausgeführt wird:

2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    Statistics for 20,000 generated numbers:
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    Average value: -0.003037802901958332
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    Standard deviation: 0.9977477093538349
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5) Mindestwert: -3.865371560675546
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    Maximum value: 3.4797509297243994
2024.07.12 13:11:05.437    checkRND (.US500Cash,M5)    Distribution histogram:
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):

Aus dem Ausdruck ist ersichtlich, dass die Methode korrekt funktioniert, die Standardabweichung ist fast gleich 1, der Durchschnittswert ist 0, während die Streuung dem Intervall [-4;4] entspricht.

Als Nächstes gehen wir zur Berechnung der adaptiven Parameter der Mutation und zum Schreiben der Funktion über:

//——————————————————————————————————————————————————————————————————————————————
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());
}
//——————————————————————————————————————————————————————————————————————————————

Die adaptiven Parameter Cg, Cs und Cn werden mit Hilfe einer selbstanpassenden Mutationsstrategie berechnet, die auf dem von Schwefel vorgeschlagenen Konzept beruht. Gleichungen zur Berechnung dieser Parameter:

1. Initialisierung:

  • Die Population von N Lösungen, wobei jede Lösung als ein Paar von Vektoren (pi, σi) dargestellt wird, wobei i ∈ {0, 1, 2} den drei Parametern Cg, Cs und Cn entspricht.
  • Die Anfangswerte der pi Komponenten werden nach dem Zufallsprinzip gemäß einer Gleichverteilung im angenommenen Lösungsraum ausgewählt.
  • Die Ausgangswerte σi werden auf einen festen Wert gesetzt.

2. Generation der Nachkommenschaft:

  • Für jedes Elternteil (pi, σi) wird ein Nachkomme (pi', σi') gemäß den folgenden Gleichungen erzeugt:
σ'i (j) = σi (j) * exp (τ' * N (0,1) + τ * Nj (0,1))
p'i (j) = pi (j) + σi (j) * N (0,1)

wobei pi (j), p'i (j), σi (j), σ 'i (j) jeweils die j-ten Komponenten der Vektoren pi, p'i, σi und σ'i sind.

  • N (0,1) - eine Zufallszahl aus einer Normalverteilung mit Mittelwert 0 und Standardabweichung 1.
  • Nj (0,1) - eine Zufallszahl aus einer Normalverteilung mit Mittelwert 0 und Standardabweichung 1, wobei j ein Zähler ist.
  • τ und τ' sind Skalierungsfaktoren, die in (√(2√n))^-1 und (√(2n))^-1 festgelegt sind, wobei n die Dimension des Problems ist.

Die adaptiven Parameter Cg, Cs und Cn verändern sich also gemäß dieser selbstanpassenden Strategie, sodass sie sich während des Optimierungsprozesses dynamisch anpassen können.

Wie aus dem Ausdruck der ermittelten Verhältnisse von Cg, Cs und Cn hervorgeht, sind diese Verhältnisse in einzelnen Fällen zu groß. Dies liegt daran, dass in der Gleichung für die Aktualisierung der strategischen Parameter σ der neue Wert durch Multiplikation mit dem vorherigen Wert ermittelt wird. Dies ermöglicht die Anpassung der Parameter an die komplexe Oberfläche der Zielfunktion, kann aber gleichzeitig zu Instabilität und starken Wertesprüngen führen.

Schauen wir einmal, welche Werte Cg, Cs und Cn annehmen:

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

Wenn Cg, Cs und Cn infolge der selbstanpassenden Mutation nach der Methode Schwefels sehr große Werte annehmen, kann es notwendig sein, Maßnahmen zur Kontrolle und Begrenzung dieser Werte zu ergreifen. Dies ist wichtig, um die Stabilität und Effizienz des Algorithmus zu erhalten. Es gibt mehrere mögliche Ansätze, um die Zahlenwerte der Verhältnisse zu begrenzen:

1. Grenzwerte.
Legen wir die Ober- und Untergrenzen für Cg, Cs und Cn fest. Zum Beispiel: 

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

// Usage:
LimitParameters (Cg, 0.0, 2.0);
LimitParameters (Cs, 0.0, 2.0);
LimitParameters (Cn, 0.0, 2.0); 

2. Normalisierung.
Wir normalisieren die Parameterwerte nach der Mutation so, dass ihre Summe immer gleich 1 ist:

void NormalizeParameters (double &Cg, double &Cs, double &Cn)
{
  double sum = Cg + Cs + Cn;
  if (sum > 0)
  {
    Cg /= sum;
    Cs /= sum;
    Cn /= sum;
  }
  else
  {
    // If sum is 0, set equal values
    Cg = Cs = Cn = 1.0 / 3.0;
  }
}

3. Logarithmische Skalierung.
Wir wenden eine logarithmische Skalierung an, um große Werte zu glätten:

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

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

4. Adaptive Skalierung des Mutationsschritts.
Wir verringern die Mutationsschrittweite, wenn die Parameter zu groß werden:

 void AdaptMutationStep(double &stepSize, double paramValue)
{
  if(MathAbs(paramValue) > threshold)
  {
    stepSize *= 0.9; // Reduce the step size
  }
}

5. Regelmäßige Rückstellung.
Wir setzen die Parameter regelmäßig auf ihre Anfangswerte oder auf den Mittelwert der Grundgesamtheit zurück:

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

6. Verwendung der Exponentialfunktion.
Wir wenden Seine Exponentialfunktion an, um das Wachstum der Parameter zu begrenzen:

double LimitGrowth(double param)
{
  return 2.0 / (1.0 + MathExp(-param)) - 1.0; // Limit values in [-1, 1]
}

7. Überwachung und Anpassung.
Wir überwachen die Parameterwerte und passen die Mutationsstrategie an, wenn sie häufig die zulässigen Grenzen überschreiten:

void MonitorAndAdapt(double &param, int &outOfBoundsCount)
{
  if(MathAbs(param) > maxAllowedValue)
  {
    outOfBoundsCount++;
    if(outOfBoundsCount > threshold)
    {
      // Adaptation of the mutation strategy
      AdjustMutationStrategy();
    }
  }
}

Es ist auch wichtig, daran zu denken, dass eine übermäßige Einschränkung der Parameter die Fähigkeit des Algorithmus, den Lösungsraum zu erkunden, einschränken kann. Daher ist es notwendig ist, das richtige Gleichgewicht zwischen Kontrolle und Flexibilität zu finden. Diese Methoden können von Optimierungsenthusiasten für weitere Forschungen verwendet werden, aber ich habe meine eigene Gauß-Verteilungsfunktion verwendet, die in dem Artikel beschrieben wird.

//——————————————————————————————————————————————————————————————————————————————
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));
}
//——————————————————————————————————————————————————————————————————————————————

Wie aus dem Ausdruck der erhaltenen Werte für die Verhältnisse Cg, Cs und Cn unten ersichtlich ist, treten nach Anwendung meiner Funktion große Werte viel seltener auf als bei Anwendung der Box-Muller-Methode.

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

Nachdem wir nun Schwefels Konzept und die adaptiven Werte der Verhältnisse verstanden haben, wollen wir uns nun der Methode zur Bestimmung der nächsten Nachbarn im Algorithmus zuwenden. Zur Bestimmung der Koordinaten der nächsten Nachbarn Nc wird die folgende Methode verwendet:
1. Für jedes Individuum in der Population werden seine nächsten Nachbarn ermittelt.
2. Als nächste Nachbarn gelten die drei Individuen, deren Werte der Zielfunktion (Fitness) einem bestimmten Individuum am nächsten kommen.
3. Die Koordinaten des Mittelpunkts der Gruppe, die aus einem bestimmten Individuum und seinen nächsten Nachbarn besteht, werden als arithmetisches Mittel der Koordinaten dieser drei Nachbarn berechnet.

Die Koordinaten Nc werden also nicht einfach als drei beliebige Koordinaten genommen, sondern als Mittelpunkt der Gruppe der nächsten Nachbarn durch den Wert der Zielfunktion berechnet. So können Informationen über die unmittelbare Umgebung eines Individuums genutzt werden, um seine nächste Position zu bestimmen.

Der entscheidende Punkt ist, dass die nächsten Nachbarn nicht durch die geografische Nähe, sondern durch die Nähe der Werte der Zielfunktion bestimmt werden. Dies entspricht eher einer logischen als einer geografischen Nähe in der Sozialstruktur.

//——————————————————————————————————————————————————————————————————————————————
void C_AO_ASBO::FindNeighborCenter (const S_ASBO_Agent &ag, double &center [])
{
// Create arrays for indices and fitness differences
  int indices [];
  double differences [];
  ArrayResize (indices, popSize - 1);
  ArrayResize (differences, popSize - 1);

// Fill the arrays
  int count = 0;
  for (int i = 0; i < popSize; i++)
  {
    if (&a [i] != &ag)  // Exclude the current agent
    {
      indices [count] = i;
      differences [count] = MathAbs (a [i].fitness - ag.fitness);
      count++;
    }
  }

// Sort arrays by fitness difference (bubble sort)
  for (int i = 0; i < count - 1; i++)
  {
    for (int j = 0; j < count - i - 1; j++)
    {
      if (differences [j] > differences [j + 1])
      {
        // Exchange of differences
        double tempDiff = differences [j];
        differences [j] = differences [j + 1];
        differences [j + 1] = tempDiff;

        // Exchange of indices
        int tempIndex = indices [j];
        indices [j] = indices [j + 1];
        indices [j + 1] = tempIndex;
      }
    }
  }

// Initialize the center
  ArrayInitialize (center, 0.0);

// Calculate the center based on the three nearest neighbors
  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;
  }
}
//——————————————————————————————————————————————————————————————————————————————

Erläuterung der Methode:

  • Ermittele die nächsten Nachbarn auf der Grundlage der Nähe der Werte der Zielfunktion (Fitness).
  • Verwende drei nächstgelegene Nachbarn.
  • Berechne den Mittelpunkt der Gruppe als arithmetisches Mittel der Koordinaten dieser drei Nachbarn.

Analysieren wir diese Funktion:

1. Sortierrichtung:
Die Sortierung erfolgt in aufsteigender Reihenfolge der Fitnessdifferenz.

Wenn das aktuelle Element größer ist als das nächste, tauschen sie die Plätze. Nach der Sortierung wird das Array differences also vom kleinsten zum größten Wert sortiert, und die entsprechenden Indizes im Array indices spiegeln diese Sortierung wider.

2. Die Funktion führt die folgenden Schritte aus:

  • Schließt den aktuellen Bearbeiter von der Betrachtung aus.
  • Berechnet den Fitnessunterschied zwischen dem aktuellen Agenten und allen anderen.
  • Sortiert Agenten nach diesem Unterschied.
  • Wählt die drei nächstgelegenen Nachbarn (mit dem geringsten Fitnessunterschied)
  • Berechnet den Mittelpunkt einer Gruppe auf der Grundlage der Koordinaten dieser drei Nachbarn
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++;
    }
  }

// Sort by fitness difference ascending
  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);  // Protection against the case when there are less than 3 agents
  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;
  }
}

Diese Version der Funktion ist robuster gegenüber Fehlern und behandelt Fälle mit einer kleinen Anzahl (weniger als 3) von Agenten in der Population korrekt. Nachdem wir uns mit den grundlegenden logischen Methoden des Algorithmus vertraut gemacht haben, können wir nun zur Untersuchung der Struktur des Algorithmus selbst übergehen. Der ASBO-Algorithmus besteht aus den folgenden Hauptschritten:

1. Initialisierung:

  • Es wird eine Anfangspopulation von Lösungen definiert, wobei jede Lösung explizit dargestellt wird (nicht in verschlüsseltem Format).
  • Für jede Lösung wird der Wert der Zielfunktion berechnet, der ihre Eignung bestimmt.
  • Die Lösung mit dem höchsten Fitnesswert wird zu diesem Zeitpunkt zum globalen Spitzenreiter erklärt.
  • Für jede Lösung wird eine Gruppe von nächsten Nachbarn mit den nächsthöheren Fitnesswerten ermittelt.

2. Adaptive Mutation von Parametern:

  • Für jede Lösung wird ein Vektor von drei adaptiven Parametern bestimmt: Cg, Cs und Cn, die für den Einfluss des globalen Führers, der persönlich besten Lösung bzw. des Zentrums der Nachbargruppe verantwortlich sind.
  • Zur Aktualisierung der Werte dieser Parameter wird eine selbstanpassende Schwefelsche Mutationsstrategie verwendet.

3. Aktualisieren des Entscheidungsstatus:

  • Für jede Lösung wird die Änderung ihrer Position anhand der aktuellen Werte der Parameter Cg, Cs und Cn berechnet.
  • Die neue Lösungsposition wird berechnet, indem die Änderung zur aktuellen Position addiert wird.

4. Zweiphasiger Prozess:

  • Phase 1: Es werden M unabhängige Populationen erstellt, von denen jede vom ASBO-Algorithmus für eine feste Anzahl von Iterationen bearbeitet wird. Die Fitness- und Parameterwerte für jede Lösung aus den endgültigen Populationen werden gespeichert.
  • Phase 2: Aus allen endgültigen Populationen werden die Lösungen mit der besten Fitness ausgewählt, und ihre Parameter werden zur Bildung einer neuen Population verwendet. Die grundlegende Logik des ASBO-Algorithmus wird auf die resultierende neue Population angewandt, um die endgültige Lösung zu erhalten.

Heben wir die wichtigsten Merkmale des ASBO-Algorithmus hervor:

  • Anwendung einer selbstanpassenden Mutationsstrategie zur dynamischen Aktualisierung der Parameter.
  • Verwendung der Konzepte der Führung und des Einflusses der Nachbarn zur Modellierung des Sozialverhaltens.
  • Einsetzen eines zweistufigen Verfahrens, um die Lösungsvielfalt zu erhalten und die Konvergenz zu beschleunigen.


In diesem Artikel haben wir ein Beispiel für Schwefels Konzept, die Box-Muller-Methode, besprochen, die eine Normalverteilung, die Verwendung von selbstanpassenden Mutationsraten und eine Funktion zur Bestimmung der nächsten Nachbarn anhand ihres Fitnesswerts umfasst. Wir haben uns mit der Struktur der ASBO befasst. Im nächsten Artikel werden wir den zweistufigen Prozess der künstlichen Evolution im Detail untersuchen, die Bildung des Algorithmus abschließen, Tests mit Testfunktionen durchführen und Schlussfolgerungen über seine Effizienz ziehen.


Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/15283

Beigefügte Dateien |
Entwicklung eines Expertenberaters für mehrere Währungen (Teil 15): Den EA für den realen Handel vorbereiten Entwicklung eines Expertenberaters für mehrere Währungen (Teil 15): Den EA für den realen Handel vorbereiten
Wenn wir uns allmählich einem fertigen EA nähern, müssen wir auf Aspekte achten, die in der Phase des Testens einer Handelsstrategie zweitrangig erscheinen, aber wichtig werden, wenn wir zum echten Handel übergehen.
Neuronale Netze im Handel: Räumlich-zeitliches neuronales Netz (STNN) Neuronale Netze im Handel: Räumlich-zeitliches neuronales Netz (STNN)
In diesem Artikel werden wir über die Verwendung von Raum-Zeit-Transformationen zur effektiven Vorhersage bevorstehender Kursbewegungen sprechen. Um die numerische Vorhersagegenauigkeit in STNN zu verbessern, wird ein kontinuierlicher Aufmerksamkeitsmechanismus vorgeschlagen, der es dem Modell ermöglicht, wichtige Aspekte der Daten besser zu berücksichtigen.
Adaptive Social Behavior Optimization (ASBO): Zweiphasige Entwicklung Adaptive Social Behavior Optimization (ASBO): Zweiphasige Entwicklung
Wir beschäftigen uns weiterhin mit dem Thema des Sozialverhaltens von Lebewesen und dessen Auswirkungen auf die Entwicklung eines neuen mathematischen Modells - ASBO (Adaptive Social Behavior Optimization). Wir werden uns mit der zweiphasigen Entwicklung befassen, den Algorithmus testen und Schlussfolgerungen ziehen. So wie sich in der Natur eine Gruppe von Lebewesen zusammenschließt, um zu überleben, nutzt ASBO die Prinzipien des kollektiven Verhaltens, um komplexe Optimierungsprobleme zu lösen.
Neuronale Netze im Handel: Das „Dual-Attention-Based Trend Prediction Model“ Neuronale Netze im Handel: Das „Dual-Attention-Based Trend Prediction Model“
Wir setzen die Diskussion über die Verwendung der stückweisen, linearen Darstellung von Zeitreihen fort, die im vorherigen Artikel begonnen wurde. Heute werden wir sehen, wie diese Methode mit anderen Ansätzen der Zeitreihenanalyse kombiniert werden kann, um die Qualität der Vorhersage des Preistrend zu verbessern.