Evolutionäre Strategie zur Anpassung der Kovarianzmatrix (CMA-ES)
Inhalt
Einführung
In der Welt der Optimierung gibt es viele Algorithmen, doch wir suchen nach den leistungsstärksten, um die Optimierungsprobleme unserer Handelsroboter zu lösen. CMA-ES (Covariance Matrix Adaptation Evolution Strategy) ist eines der seltenen Beispiele, bei denen mathematische Strenge mit biologischer Intuition kombiniert wird, wodurch ein Algorithmus entsteht, der nicht nur Optimierungsprobleme löst, sondern auch lernt, deren Struktur zu verstehen.
Die Geschichte von CMA-ES begann Ende der 1990er Jahre in Forschungslabors in Deutschland, wo Nikolaus Hansen und Andreas Ostermeier eine grundlegende Frage stellten: Ist es möglich, einen Optimierungsalgorithmus zu entwickeln, der nicht einfach nur nach einer Lösung sucht, sondern sich an die Geometrie des Problems anpasst? Herkömmliche evolutionäre Algorithmen erzeugten Nachkommen in kugelförmigen Bereichen um die Elternindividuen herum, was bei einfachen Funktionen gut funktionierte, sich jedoch bei komplexen, schlecht konditionierten Problemen als unwirksam erwies. Werfen wir einen Blick auf diesen interessanten Algorithmus.
Implementierung des Algorithmus
Stell dir vor, du suchst auf einer unregelmäßig geformten Insel nach einem Schatz. Üblicherweise sucht man in alle Richtungen gleichermaßen, als wäre die Insel rund. CMA-ES lernt nach und nach die Form der Insel kennen und verlagert seine Suche schrittweise in Richtungen, in denen die Wahrscheinlichkeit, einen Schatz zu finden, höher ist. Außerdem speichert es erfolgreiche Routen und nutzt diese Informationen zur Planung künftiger Suchvorgänge.
CMA-ES basiert auf einer täuschend einfachen Gleichung: x_k ~ N(m, σ²C). Hinter dieser Einfachheit verbirgt sich jedoch eine tiefgreifende mathematische Struktur. Jedes Symbol hier enthält wichtige Informationen über den Stand der Suche: m ist die derzeit beste Schätzung für die Lage des Optimums, σ ist ein Maß dafür, wie weit wir bereit sind, uns vom Bekannten zu entfernen, während C eine Kovarianzmatrix ist, die unser Verständnis der Funktionsgeometrie widerspiegelt. Die einzige Änderung, die wir rechtfertigen können, ist der Ersatz der Normalverteilung durch eine Potenzgesetzverteilung, was bedeutet, dass die Implementierung einer modifizierten Gleichung folgt: x_k ~ PowerDist(m, σ²C). Diese Änderung verändert die Art der Exploration des Suchraums (größere „Sprünge“), bewahrt jedoch den grundlegenden adaptiven Charakter des Algorithmus.
Die Kovarianzmatrix C ist das eigentliche Herzstück des Algorithmus. Sie beginnt als einfache Identitätsmatrix, die eine sphärische Verteilung darstellt. Doch mit jeder Iteration entwickelt sie sich weiter, dehnt sich in Bereichen aus, in denen rasche Verbesserungen erzielt werden, und schrumpft dort, wo der Fortschritt nur langsam voranschreitet. Allmählich verwandelt sich die Kugel in eine Ellipse, dann in ein längliches Ellipsoid, das idealerweise entlang der Konturen der zu optimierenden Funktion ausgerichtet ist.
Die wichtigste Neuerung von CMA-ES ist das Konzept der Evolutionspfade. Das ist sozusagen das genetische Gedächtnis des Algorithmus, das sich nicht nur merkt, wo die erfolgreichen Punkte lagen, sondern auch, wie der Algorithmus zu ihnen gelangt ist. Der evolutionäre Pfad sammelt Informationen über aufeinanderfolgende erfolgreiche Schritte und erzeugt einen solchen Richtungsvektor, der auf die vielversprechendsten Suchbereiche hinweist. Der zweite Evolutionspfad erfüllt eine subtilere Funktion: Er steuert die Schrittweite. Sind die aufeinanderfolgenden Schritte des Algorithmus miteinander verknüpft, das heißt, folgt jeder nachfolgende Schritt der Richtung des vorherigen, dann vergrößert sich die Schrittweite – der Algorithmus „spürt“, dass er sich in die richtige Richtung bewegt. Sind die Schritte zufällig und unkorreliert, verringert sich die Schrittweite – möglicherweise befindet sich der Algorithmus bereits nahe am Optimum und muss genauer gesucht werden.
Hinter der biologischen Metapher der Evolution in CMA-ES verbirgt sich ein strenges mathematisches Prinzip – die Maximum-Likelihood-Schätzung. Der Algorithmus fragt sich ständig: Welche Verteilungsparameter machen die beobachteten Erfolgspunkte am wahrscheinlichsten? Dadurch wird die evolutionäre Optimierung von einer heuristischen Methode zu einer statistisch fundierten Methode.
Jede Aktualisierung der C-Kovarianzmatrix besteht aus zwei Komponenten: einer Aktualisierung vom Rank-One auf der Grundlage des Evolutionspfads und einer Aktualisierung vom Rank-μ unter Verwendung von Informationen aus der gesamten ausgewählten Population. Die Aktualisierung von Rank-One sorgt für Stabilität und ermöglicht es, langfristige Trends zu erfassen, während die Aktualisierung von Rank-μ eine schnelle Anpassung an neue Informationen aus der aktuellen Generation ermöglicht.
Im Rahmen von CMA-ES wird eine modifizierte Heaviside-Funktion verwendet, die eine wichtige Rolle im Stagnationserkennungsmechanismus des Algorithmus spielt. Die Funktion vergleicht die Länge des Evolutionspfads mit der erwarteten Länge; ist der Pfad zu kurz, ist dies ein Anzeichen für ungerichtete Bewegungen. Deaktivierungsbedingungen (hsig = 0): Der Algorithmus „irrt“ zufällig umher und die Schritte heben sich gegenseitig auf; in diesem Fall ist die Schrittweite wahrscheinlich zu groß. Während einer Stagnationsphase nimmt der Einfluss der Aktualisierung von Rank-One ab, und wir stützen uns stärker auf Informationen aus der gesamten Population. Aktivierungsbedingungen (hsig = 1): Der Algorithmus macht Fortschritte in einer bestimmten Richtung, aufeinanderfolgende Schritte sind miteinander verknüpft, und die Schrittweite ist der aktuellen Situation angemessen.
Eine der grundlegendsten Eigenschaften von CMA-ES ist seine Invarianz gegenüber Transformationen des Suchraums. Der Algorithmus löst die Funktion f(x) ebenso effizient wie die Funktion f(Ax + b), wobei A eine beliebige nicht-singuläre Matrix und b ein beliebiger Verschiebungsvektor ist. Das bedeutet, dass CMA-ES unabhängig von der Wahl des Koordinatensystems ist. Diese Invarianz ist kein Zufall; sie ist eine direkte Folge der Anwendung des Maximum-Likelihood-Prinzips und der Anpassung der Kovarianzmatrix. Der Algorithmus erkennt automatisch ein natürliches Koordinatensystem für das Problem, bei dem die Achsen mit den Hauptrichtungen der Funktionsvariation zusammenfallen.
Die Schönheit der Theorie muss mit praktischer Anwendbarkeit einhergehen. CMA-ES benötigt Speicherplatz im Ausmaß von O(n²) für die Speicherung der Kovarianzmatrix und O(n³) Rechenoperationen für die Eigenwertzerlegung, wodurch sich der Algorithmus für Probleme mit bis zu mehreren Hundert Variablen eignet. Für große Dimensionen wurden spezielle Varianten entwickelt: sep-CMA-ES beschränkt sich auf diagonale Kovarianzmatrizen, VkD-CMA nutzt variable Dimensionen und LM-CMA wendet die Prinzipien des begrenzten Speicherbedarfs an. Die Umsetzung dieser Methoden würde den Rahmen unseres Artikels sprengen, doch vielleicht können wir in späteren Artikeln darauf zurückkommen.

Abb. 1. Darstellung des CMA-ES-Algorithmus
Die Abbildung zeigt die Entwicklung über mehrere Generationen hinweg – drei Momentaufnahmen des Zustands des Algorithmus (Generationen 1, 5 und 15), die verdeutlichen, wie sich die Population allmählich dem Optimum annähert,
Anpassung der Kovarianzmatrix – die blauen Ellipsen zeigen, wie sich die Form der Verteilung in den Generationen ändert: 1 – rund (Einheitsmatrix), 5 – länglich und gedreht, 15 – genau auf die lokale Geometrie abgestimmt.
Die Algorithmuskomponenten: Rote Punkte stellen die gesamte Population dar (λ Nachkommen), grüne Punkte die besten Lösungen (μ Eltern), der schwarze Punkt m ist der Populationsmittelwert, gestrichelte Kreise sind die Isolinien der Zielfunktion.
Die Schlüsselformel ist unten aufgeführt, zusammen mit einem Hinweis zur Modifikation des Potenzgesetzes. Die Abbildung veranschaulicht deutlich, wie der Algorithmus seine Suchstrategie an die Landschaft der zu optimierenden Funktion anpasst, dabei den Suchraum schrittweise eingrenzt und sich dem Optimum nähert.
Bereiten wir den Pseudocode vor.
Initialisierung
- Einstellen der Algorithmusparameter:
- Populationsgröße (Lambda) = 50
- Anzahl der Eltern (mu) = 25
- Lernrate für die Aktualisierung von Rank-One (c1) = 0,01
- Lernrate für die Aktualisierung von Rank-μ (cμ) = 0,8
- Schrittweiten-Dämpfung = 0,6
- Anfängliche Schrittweite (Sigma) = 0,3
- Berechnen der Rekombinationsgewichte:
- Berechne für jeden der μ Elternteile das Gewicht als log(μ + 0,5) – log(i + 1)
- Normalisiere die Gewichte so, dass ihre Summe 1 ergibt
- Berechne die effektive Selektionsmasse μ_eff
- Initialisieren der Strategieparameter:
- Berechne die Lernraten für den Evolutionspfad (cs, cc)
- Berechne die erwartete Norm eines Standardnormalvektors (chiN)
- Bestimme das Intervall für die Eigenwertzerlegung
- Initialisieren der Datenstrukturen:
- Kovarianzmatrix C = Einheitsmatrix
- Matrix der Eigenvektoren B = Einheitsmatrix
- Vektor der Eigenwerte D = Einheitsvektor
- Evolutionspfade von PC und PS = Nullvektoren
- Populationsmittelwert m = Zufallspunkt im Suchraum
- Erstellen einer Ausgangspopulation:
- Generiere zufällige Lambda-Punkte im Suchraum
Grundlegender Evolutionszyklus
Wiederhole dies, bis das Stoppkriterium erfüllt ist:
Schritt 1: Nachkommen zeugen
- Für jeden Nachkommen von lambda:
- Erzeuge den Zufallsvektor z aus der Standardnormalverteilung
- Wende die Transformation an: y = B × D × z
- Erzeuge einen Nachkommen: x_k = m + σ × y
- Wende Suchraum-Einschränkungen auf x_k an
Schritt 2: Überprüfen und aktualisieren
- Bewerte die Fitness aller Nachkommen
- Sortiere die Population:
- Sortiere die Nachkommen in absteigender Reihenfolge der Fitness
- Aktualisiere gegebenenfalls die gefundene beste Lösung
- Aktualisiere den Mittelwert der Population:
- Speichere den bisherigen Mittelwert m_old
- Berechne den neuen Mittelwert als gewichtete Summe der μ besten Nachkommen: m_new = Σ(w_i × x_i) für i von 1 bis μ
- Aktualisiere die Evolutionspfade:
- Berechne den Mittelwertversatz: y_w = (m_neu - m_alt) / σ
- Berechne C^(-1/2) × y_w mittels Eigenwertzerlegung
- Aktualisiere den Evolutionspfad für die Schrittweite: ps = (1 - cs) × ps + √(cs × (2 - cs) × μ_eff) × C^(-1/2) × y_w
- Überprüfe die Stagnationsbedingung (Heaviside-Funktion)
- Aktualisiere den Entwicklungsweg für die Kovarianzmatrix: pc = (1 - cc) × pc + stagnation_indicator × √(cc × (2 - cc) × μ_eff) × y_w
- Aktualisiere die Kovarianzmatrix:
- Bereite die Aktualisierung der Matrix von Rank-One aus dem Außenprodukt von pc vor
- Berechne die Aktualisierung des Rank-μ anhand der gewichteten Summe der Abweichungen des besten Nachfolgers
- Aktualisiere C: C = (1 - c1 - cμ) × C + c1 × (pc × pc^T) + cμ × Σ(w_i × y_i × y_i^T)
- Stelle die Symmetrie der Matrix sicher
- Initialisieren der Schrittweite:
- Berechne den Anpassungskoeffizienten auf der Grundlage der ps-Weglänge
- Aktualisiere σ: σ = σ × exp((cs/damps) × (||ps||/chiN - 1))
- Begrenze σ auf ein vernünftiges Maß, um die numerische Stabilität zu gewährleisten
- Eigenwertzerlegung (falls erforderlich):
- Wenn seit der letzten Zerlegung genügend Iterationen vergangen sind:
- Führe die Jacobi-Zerlegung von C = B × D² × B^T durch
- Eigenwerte und Eigenvektoren in absteigender Reihenfolge sortieren
- Stelle sicher, dass die Matrix positiv definit ist
- Wenn seit der letzten Zerlegung genügend Iterationen vergangen sind:
- Überprüfen und Korrigieren der Kovarianzmatrix:
- Überprüfe regelmäßig die positive Definitheit von C
- Falls erforderlich, füge einen kleinen Wert zur Diagonalen hinzu
- Stelle die Symmetrie der Matrix sicher
Schreiben wir nun die von der Klasse C_AO abgeleitete Klasse C_AO_CMAES, die den CMA-ES-Optimierungsalgorithmus implementieren wird.
Öffentliche Felder, Methode:
- SetParams() – Legt die CMA-ES-Parameterwerte aus dem Array params fest.
- Init() – initialisiert den Algorithmus. Akzeptiert Minimal- und Maximalwerte sowie die Schrittweite für jede Variable und die Anzahl der Epochen.
- Moving() – implementiert die Hauptschleife des Algorithmus, die für die Erzeugung neuer Lösungen zuständig ist.
- Revision() – Bewertung neuer Lösungen und Aktualisierung des Algorithmuszustands.
Private Felder:
- CMA-ES-Parameter – die Variablen, die bestimmte Parameter des CMA-ES-Algorithmus enthalten: Anzahl der Eltern (mu), Schrittweite (sigma), Lernraten (learningRateC1,learningRateCMu), Dämpfungsfaktor (stepSizeDamping) und weitere für die Funktionsweise des Algorithmus erforderliche Parameter.
- CMA-ES-Datenstrukturen –Arrays zur Speicherung von Rekombinationsgewichten, der Kovarianzmatrix covMatrix, den Eigenvektoren B, den Eigenwerten D, den Evolutionspfaden (pc,ps) sowie weiterer Hilfsdaten. Die Variablen mu_eff,counteval,eigeneval,chiN und eigenInterval dienen zur Steuerung und Verwaltung des Algorithmusverlaufs.
- Die Variablen zur Leistungsoptimierung dienen dazu, Berechnungen während der Ausführung des Algorithmus zu beschleunigen, beispielsweise Lernraten und Dämpfung.
- Die Hilfsarrays, die bei der Durchführung verschiedener Operationen als temporärer Speicher für Vektoren und Matrizen dienen.
- Die Hilfsverfahren, die einzelne Schritte des CMA-ES-Algorithmus ausführen, wie beispielsweise die Initialisierung der Verteilung, die Aktualisierung der Verteilung, die Berechnung der Eigenwertzerlegung, die Sortierung der Population, die Berechnung der Gewichte, die Aktualisierung des Mittelwerts, die Überprüfung auf positive Definitheit und die Sicherstellung der positiven Definitheit.
Insgesamt kapselt die Klasse C_AO_CMAES die Logik des CMA-ES-Algorithmus und bietet eine Schnittstelle für die Initialisierung, die Parameteranpassung und die Durchführung der Optimierung. Die Klasse enthält die Parameter, Datenfelder und Methoden, die zur Implementierung des Algorithmus erforderlich sind. Die Trennung von öffentlichen und privaten Feldern ermöglicht die Zugriffskontrolle auf die internen Komponenten der Klasse.
//———————————————————————————————————————————————————————————————————— class C_AO_CMAES : public C_AO { public: //---------------------------------------------------------- ~C_AO_CMAES () { } C_AO_CMAES () { ao_name = "CMAES"; ao_desc = "Covariance Matrix Adaptation Evolution Strategy"; ao_link = "https://www.mql5.com/de/articles/18227"; // Default parameters popSize = 50; // Default population size (lambda) mu = 25; // Number of parents (half of the population) learningRateC1 = 0.01; // Learning rate for rank-1 update learningRateCMu = 0.8; // Learning rate for rank-μ update stepSizeDamping = 0.6; // Damping for step size // Create and initialize the parameters array ArrayResize (params, 5); params [0].name = "popSize"; params [0].val = popSize; params [1].name = "mu"; params [1].val = mu; params [2].name = "learningRateC1"; params [2].val = learningRateC1; params [3].name = "learningRateCMu"; params [3].val = learningRateCMu; params [4].name = "stepSizeDamping"; params [4].val = stepSizeDamping; } void SetParams () { popSize = (int)params [0].val; mu = (int)params [1].val; learningRateC1 = params [2].val; learningRateCMu = params [3].val; stepSizeDamping = params [4].val; } bool Init (const double &rangeMinP [], // minimum values const double &rangeMaxP [], // maximum values const double &rangeStepP [], // step size const int epochsP = 0); void Moving (); void Revision (); //------------------------------------------------------------------ private: //--------------------------------------------------------- // CMA-ES specific parameters int mu; // Number of parents (selected points) double sigma; // Step size double learningRateC1; // Learning rate for rank-1 update double learningRateCMu; // Learning rate for rank-μ update double stepSizeDamping; // Damping factor for step size update // CMA-ES specific data structures double weights []; // Recombination weights double covMatrix []; // Covariance matrix (stored as a one-dimensional array) double B []; // C eigenvectors double D []; // C eigenvalues (square roots) double pc []; // Evolution path for C double ps []; // Evolution path for step-size control double mu_eff; // Effective selection mass int counteval; // Counter of function evaluations since the last decomposition int eigeneval; // Generation counter when decomposition was performed double chiN; // Expected norm N(0,I) int eigenInterval; // Interval for eigenvalue decomposition // Variables for performance optimization double cs; // Learning rate for the sigma path double cc; // Learning rate for rank-1 path double damps; // Damping for sigma double hsig_threshold; // Threshold for the Heaviside function // Auxiliary arrays double y_vec []; // Mutation vector double arindex []; // Array of indices for sorting double arfitness []; // Fitness array for sorting double temp_vec []; // Temporary vector for matrix operations double invsqrtC_times_yw []; // Temporary storage for C^(-1/2) * y_w // Caching variables for Box-Muller double cached_normal; bool has_cached; // Auxiliary methods void InitDistribution (); void UpdateDistribution (); void ComputeEigendecomposition (); double GetChiN (); void SortPopulation (); void ComputeWeights (); void UpdateMean (); bool CheckPositiveDefinite (); void EnforcePositiveDefinite (); }; //————————————————————————————————————————————————————————————————————
Die Methode „Init“ der Klasse „C_AO_CMAES“ initialisiert den CMA-ES-Algorithmus. Als Eingabe benötigt es die Minimal- und Maximalwerte der Parameter, die Schrittweite und die Anzahl der Epochen. Zunächst wird die Methode „StandardInit“ aufgerufen, um die Standardinitialisierung durchzuführen. Das Flag „has_cached“ wird auf „false“ gesetzt, um anzuzeigen, dass kein zwischengespeicherter Wert für die Box-Muller-Zufallszahlengenerierung vorliegt. Anschließend wird die Sigma-Schrittweite auf den Anfangswert 0,3 (30 % des Suchbereichs) gesetzt. Die Methode „ComputeWeights“ wird aufgerufen, um die Rekombinationsgewichte zu berechnen, und die Methode „GetChiN“ wird aufgerufen, um die erwartete Norm N(0, I) zu berechnen.
Die Parameter cs, cc, damps und hsig_threshold werden auf der Grundlage von mu_eff (effektive Masse) und der Anzahl der Koordinaten (coords) berechnet. Diese Parameter dienen dazu, die CMA-ES-Strategie während des Betriebs anzupassen. „eigenInterval“ wird berechnet und festgelegt, um zu bestimmen, wie oft die Eigenwertzerlegung durchgeführt wird. Dieser Parameter ist so konfiguriert, dass die Leistung optimiert wird.
Für CMA-ES-spezifische Arrays wie die Kovarianzmatrix „covMatrix“, die Eigenvektoren „B“, die Eigenwerte „D“, die Evolutionspfade „pc“ und „ps“, „y_vec“, „arindex“, „arfitness“, „temp_vec“ sowie die Zeitvektoren „invsqrtC_times_yw“ wird Speicher zugewiesen. Arrays werden nur neu erstellt, wenn die Problemdimension geändert werden muss.
Die Arrays „pc“ und „ps“ für den Evolutionspfad werden mit Nullen initialisiert, die Kovarianzmatrix „covMatrix“ und die Eigenvektormatrix „B“ werden mit der Einheitsmatrix initialisiert, und das Eigenwert-Array „D“ wird mit Einsen initialisiert. Die Methode „InitDistribution“ wird aufgerufen, um die Anfangsverteilung zu initialisieren. Anschließend werden die Zählerstände der Funktionen „counteval“ und „eigeneval“ zurückgesetzt. Das Flag „revision“ wird auf „true“ gesetzt, um anzuzeigen, dass die Fitnesswerte neu berechnet werden sollen. Bei erfolgreicher Initialisierung wird „true“ zurückgegeben.
Zusammenfassend lässt sich sagen, dass die Methode„Init“ alle notwendigen Initialisierungen durchführt, damit der CMA-ES-Algorithmus funktioniert, einschließlich der Einstellung von Parametern, der Zuweisung von Speicherplatz für Arrays und der Initialisierung von Startwerten.
//———————————————————————————————————————————————————————————————————— bool C_AO_CMAES::Init (const double &rangeMinP [], // minimum values const double &rangeMaxP [], // maximum values const double &rangeStepP [], // step size const int epochsP = 0) // number of epochs { if (!StandardInit (rangeMinP, rangeMaxP, rangeStepP)) return false; //------------------------------------------------------------------ // Initialize Box-Muller caching has_cached = false; // Initialize CMA-ES specific variables sigma = 0.3; // Initial step size (30% of search range) // Calculate the effective mass of the variance selection ComputeWeights (); // Expected norm N(0,I) chiN = GetChiN (); // Calculate and save strategy parameters cs = (mu_eff + 2.0) / (coords + mu_eff + 5.0); cc = (4.0 + mu_eff / coords) / (coords + 4.0 + 2.0 * mu_eff / coords); damps = 1.0 + 2.0 * MathMax (0.0, MathSqrt ((mu_eff - 1.0) / (coords + 1.0)) - 1.0) + cs; hsig_threshold = 1.4 + 2.0 / (coords + 1.0); // Set the eigenvalue decomposition interval - tuning for performance eigenInterval = (int)(coords / (10.0 * MathSqrt (learningRateC1 + learningRateCMu))); eigenInterval = MathMax (1, eigenInterval); // Allocate arrays only once ArrayResize (covMatrix, coords * coords); ArrayResize (B, coords * coords); ArrayResize (D, coords); ArrayResize (pc, coords); ArrayResize (ps, coords); ArrayResize (y_vec, coords); ArrayResize (arindex, popSize); ArrayResize (arfitness, popSize); ArrayResize (temp_vec, coords); ArrayResize (invsqrtC_times_yw, coords); // Initialize evolution paths with zeros ArrayInitialize (pc, 0); ArrayInitialize (ps, 0); // Fast initialization of the identity covariance matrix and decomposition ArrayInitialize (covMatrix, 0.0); ArrayInitialize (B, 0.0); for (int i = 0; i < coords; i++) { covMatrix [i * coords + i] = 1.0; B [i * coords + i] = 1.0; D [i] = 1.0; } // Initialize initial distribution InitDistribution (); // Reset calculation counters counteval = 0; eigeneval = 0; // Forced fitness recalculation revision = true; return true; } //————————————————————————————————————————————————————————————————————
Die Methode „Moving“ in der Klasse C_AO_CMAES ist dafür zuständig, neue Nachkommen (Lösungen) in der Population zu erzeugen. Die Umwandlung von y = B * D * z: Innerhalb der Schleife über die Population befindet sich eine verschachtelte Schleife, die alle Koordinaten jedes einzelnen Individuums durchläuft. An jedem Punkt i wird der Wert von y_vec [i] berechnet. Dies geschieht mittels Matrixmultiplikation. Tatsächlich handelt es sich um die Multiplikation eines Vektors aus z Zufallszahlen mit der Matrix „B × D“, wobei B die Eigenvektoren der Kovarianzmatrix, D die Eigenwerte der Kovarianzmatrix und z die mit der Funktion u.PowerDistribution erzeugten Zufallszahlen sind.
Die Generierung von Nachkommen: Der Wert (a [k].c [i]) wird für jede i-Koordinate jedes k-Nachfolgers berechnet. Dies geschieht, indem der skalierte Vektor y_vec[i] zum aktuellen Mittelwert cB[i] (dem Zentrum der aktuellen Verteilung) addiert wird. Die Skalierung erfolgt durch Multiplikation von y_vec[i] mit der Schrittweite „sigma“. Somit gilt: a[k].c[i] = cB[i] + σ * y_vec[i].
Die Funktion u.SeInDiSp stellt sicher, dass die Koordinatenwerte der Nachkommen innerhalb der durch rangeMin[i], rangeMax[i] und rangeStep[i] definierten gültigen Bereiche bleiben. Es kann Werte beschneiden, sie an Grenzen abprallen lassen und sie auf den nächsten gültigen Schrittwert quantisieren. Nachdem alle Nachkommen generiert wurden, wird das „Revision“-Flag auf „true“ gesetzt, und die Werte der Fitnessfunktion (Qualität) für die generierten Nachkommen sollten neu berechnet werden.
Infolgedessen erzeugt die Methode eine neue Lösungsmenge auf der Grundlage der aktuellen Verteilung (cB-Mittelwert und der Kovarianzmatrix, dargestellt durch B und D) sowie der Schrittweite σ. Sie nutzt Zufallszahlen, um kleine Abweichungen vom aktuellen Mittelwert zu erzeugen und so den Suchraum zu erkunden. Die Funktion SeInDiSp stellt sicher, dass neue Lösungen innerhalb der zulässigen Grenzen bleiben, und das „Revision“-Flag sorgt dafür, dass ihre Eignung anschließend bewertet wird.
//———————————————————————————————————————————————————————————————————— void C_AO_CMAES::Moving () { // Generate new lambda descendants for (int k = 0; k < popSize; k++) { // Apply the transformation y = B*D*z for (int i = 0; i < coords; i++) { y_vec [i] = 0.0; for (int j = 0; j < coords; j++) { y_vec [i] += B [i * coords + j] * D [j] * u.PowerDistribution (0.0, -8, 8, 20); } // Generate a descendant: x_k = m + σ * y a [k].c [i] = cB [i] + sigma * y_vec [i]; a [k].c [i] = u.SeInDiSp (a [k].c [i], rangeMin [i], rangeMax [i], rangeStep [i]); } } // Fitness recalculation mark revision = true; } //————————————————————————————————————————————————————————————————————
Die Methode „Revision“ der Klasse C_AO_CMAES ist dafür zuständig, die Algorithmusparameter nach der Auswertung der Fitnessfunktion für eine neue Population zu aktualisieren. Zunächst überprüft die Methode den Wert des Flags „revision“. Anschließend wird die Methode „SortPopulation()“ aufgerufen, die die aktuelle Population so sortiert, dass die besten Individuen an den Anfang gestellt werden. Die Methode „UpdateDistribution()“ aktualisiert die Parameter der CMA-ES-Verteilung (den cB-Mittelwert des Strategievektors, die Kovarianzmatrix, die Sigma-Schrittweite und die evolutionären Pfade) auf der Grundlage von Informationen über die besten Individuen in der Population. Die Aktualisierung der Verteilung ist ein wesentlicher Schritt bei CMA-ES, da sie es dem Algorithmus ermöglicht, seine Suchstrategie auf der Grundlage der erzielten Ergebnisse anzupassen.
Folglich stellt die Methode Revision den zentralen Anpassungszyklus von CMA-ES dar. Es sortiert die Population, aktualisiert die Verteilungsparameter auf der Grundlage der besten Individuen und erhöht den Berechnungszähler.
//———————————————————————————————————————————————————————————————————— void C_AO_CMAES::Revision () { if (!revision) return; revision = false; // Sort the population by fitness SortPopulation (); // Update distribution parameters based on selected individuals UpdateDistribution (); // Update the calculation counter counteval++; } //————————————————————————————————————————————————————————————————————
Die Methode „InitDistribution“ der Klasse „C_AO_CMAES“ dient dazu, die Anfangsverteilung für die Lösungssuche zu initialisieren. Das Verfahren ermittelt für jede Koordinate im Suchraum einen anfänglichen Mittelwert (cB). Für jede i-Koordinate wird der Wert von cB[i] mithilfe der Funktion u.RNDfromCI() zufällig im Bereich zwischen rangeMin[i] und rangeMax[i] festgelegt, wobei der anfängliche Mittelwert für jede Variable in der Mitte des gültigen Bereichs liegt. Die äußere Schleife durchläuft die gesamte Population (popSize Individuen). Innerhalb dieser Schleife befindet sich eine innere Schleife, die alle Koordinaten jedes einzelnen Elements durchläuft. Für jeden Nachkommen werden Koordinaten generiert. Zunächst werden sie zufällig aus dem vorgegebenen Bereich RNDfromCI ausgewählt und anschließend mithilfe von SeInDiSp auf den Abtastschritt gerundet.
Die Funktion u.RNDfromCI erzeugt eine gleichverteilte Zufallszahl im angegebenen Intervall. Die Funktion u.SeInDiSp stellt sicher, dass die für jede einzelne Person generierten Koordinaten innerhalb der zulässigen Grenzen liegen, die durch rangeMin[i], rangeMax[i] und rangeStep[i] definiert sind.
Die Methode initialisiert die Startpopulation mit Zufallslösungen, die gleichmäßig im Suchraum verteilt sind. Der Mittelwert (cB) wird ebenfalls zufällig ausgewählt. Dies bildet den Ausgangspunkt für nachfolgende Iterationen von CMA-ES, in denen der Algorithmus die Suchstrategie anpasst, um die optimale Lösung zu finden.
//+------------------------------------------------------------------+ //| Initialize search distribution | //+------------------------------------------------------------------+ void C_AO_CMAES::InitDistribution () { // Set the initial mean to the center of the search space for (int i = 0; i < coords; i++) { cB [i] = u.RNDfromCI (rangeMin [i], rangeMax [i]); } for (int k = 0; k < popSize; k++) { for (int i = 0; i < coords; i++) { // Generate a uniformly distributed point a [k].c [i] = u.RNDfromCI (rangeMin [i], rangeMax [i]); a [k].c [i] = u.SeInDiSp (a [k].c [i], rangeMin [i], rangeMax [i], rangeStep [i]); } } } //————————————————————————————————————————————————————————————————————
Die Methode GetChiN berechnet die erwartete Norm eines Zufallsvektors, der aus der Verteilung N(0,I) für eine gegebene Anzahl von Koordinaten (coords) gezogen wird. Das Ergebnis wird auf der Grundlage der Dimension des Suchraums approximiert.
//+------------------------------------------------------------------+ //| Calculate the expected norm N(0,I) | //+------------------------------------------------------------------+ double C_AO_CMAES::GetChiN () { double n = (double)coords; return MathSqrt (n) * (1.0 - 1.0 / (4.0 * n) + 1.0 / (21.0 * n * n)); } //————————————————————————————————————————————————————————————————————
Die Funktion SortPopulation dient dazu, eine Population von Lösungen nach ihren Zielfunktionswerten (Fitness) zu sortieren, um die beste Lösung zu ermitteln. Zunächst kopiert die Methode die Fitnesswerte jedes Individuums aus der Population in das Hilfsarray „arfitness“. Außerdem wird das Array „arindex“ angelegt, das zunächst die Indizes der Individuen enthält. Dies ist notwendig, damit nach der Sortierung nach Fitness die Zuordnung zwischen der sortierten Fitness und dem ursprünglichen Individuum in der Population wiederhergestellt werden kann.
Es wird der Einfügungssortieralgorithmus verwendet. Es durchläuft das Array „arfitness“ und fügt jedes Element an der richtigen Stelle in den sortierten Teil des Arrays ein, wobei größere Elemente nach rechts verschoben werden. Es ist wichtig zu beachten, dass die Sortierung in absteigender Reihenfolge erfolgt (arfitness [j] < tempFitness in der „while“-Schleife); das Ziel besteht darin, die Zielfunktion (Fitness) zu maximieren. Neben arfitness wird auch arindex synchron sortiert, um die Position der ursprünglichen Indizes der einzelnen Elemente zu verfolgen.
Nach dem Sortieren prüft die Methode, ob die Fitness des besten Individuums besser ist als die der derzeit besten Lösung; ist dies der Fall, wird fB aktualisiert und die beste Lösung durch die Koordinaten des besten Individuums aus der Population ersetzt. Somit sortiert das Verfahren die Population nicht nur nach der Fitness, sondern behält auch die derzeit beste gefundene Lösung im Blick.
//+------------------------------------------------------------------+ //| Sort the population by fitness | //+------------------------------------------------------------------+ void C_AO_CMAES::SortPopulation () { // Copy fitness values and indices for (int i = 0; i < popSize; i++) { arindex [i] = i; arfitness [i] = a [i].f; } for (int i = 1; i < popSize; i++) { double tempFitness = arfitness [i]; double tempIndex = arindex [i]; int j = i - 1; // Sort in descending order (for maximization) while (j >= 0 && arfitness [j] < tempFitness) { arfitness [j + 1] = arfitness [j]; arindex [j + 1] = arindex [j]; j--; } arfitness [j + 1] = tempFitness; arindex [j + 1] = tempIndex; } // Update the best solution if necessary if (arfitness [0] > fB) { fB = arfitness [0]; int bestIdx = (int)arindex [0]; ArrayCopy (cB, a [bestIdx].c, 0, 0, coords); } } //————————————————————————————————————————————————————————————————————
Die Methode UpdateMean aktualisiert den Populationsmittelwert (cB) durch gewichtete Rekombination der besten Individuen. Für jede Koordinate wird ein neuer Durchschnittswert berechnet, der sich aus der Summe der gewichteten Koordinatenwerte der besten Individuen ergibt, wobei die Gewichte im Array „weights“ festgelegt sind.
//+------------------------------------------------------------------+ //| Update the mean using weighted recombination | //+------------------------------------------------------------------+ void C_AO_CMAES::UpdateMean () { // Weighted recombination: m^(g+1) = Σ w_i * x_{i:λ}^(g+1) for (int j = 0; j < coords; j++) { double meanSum = 0.0; for (int i = 0; i < mu; i++) { int idx = (int)arindex [i]; meanSum += weights [i] * a [idx].c [j]; } cB [j] = meanSum; } } //————————————————————————————————————————————————————————————————————
Die Methode ComputeWeights berechnet Gewichte für die gewichtete Rekombination, legt das Gewichtsarray entsprechend der Größe mu an und berechnet log(mu + 0,5) zur Verwendung in der Gewichtsformel. Für jedes i von 0 bis mu-1 weist sie ein Gewicht zu, das der Differenz zwischen log(mu + 0,5) und log(i+1) entspricht; summiert diese Gewichte, dividiert alle Gewichte durch die Summe, sodass die Summe gleich 1 ist, und berechnet die Summe der Quadrate der Gewichte sowie mu_eff – die Effizienz der Verwendung der Gewichte, die für die Abstimmung von Geschwindigkeit und Varianz im Algorithmus wichtig ist.
Diese Methode liefert optimale Gewichte für die Rekombination, wobei der Beitrag der besten Lösungen berücksichtigt wird.
//+------------------------------------------------------------------+ //| Calculate weighted recombination weights | //+------------------------------------------------------------------+ void C_AO_CMAES::ComputeWeights () { // Allocate the array of weights ArrayResize (weights, mu); // Pre-calculate log(mu + 0.5) double log_mu_plus_half = MathLog (mu + 0.5); // Calculate positive weights double sum = 0.0; for (int i = 0; i < mu; i++) { weights [i] = log_mu_plus_half - MathLog (i + 1); sum += weights [i]; } // Normalize weights double sum_weights = 0.0; double sum_squares = 0.0; for (int i = 0; i < mu; i++) { weights [i] /= sum; sum_weights += weights [i]; sum_squares += weights [i] * weights [i]; } // Calculate the effective mass of the variance selection mu_eff = sum_weights * sum_weights / sum_squares; } //————————————————————————————————————————————————————————————————————
Die Methode UpdateDistribution aktualisiert die Verteilungsparameter, nämlich die Kovarianzmatrix (covMatrix) und die Schrittweite (sigma) im CMA-ES-Algorithmus. Wenn genügend Generationen vergangen sind (counteval – eigeneval > eigenInterval), wird die Eigenwertzerlegung berechnet und der aktuelle Mittelwert (cB) im temporären Array „oldMean“ gespeichert. Die Methode ruft „UpdateMean“ auf, um einen neuen Mittelwert zu berechnen, ermittelt die Differenz zwischen dem neuen und dem alten Mittelwert geteilt durch Sigma und führt zudem die Multiplikation von y_w mit C^(-1/2) durch, wobei dieser Vorgang mithilfe der B- und D-Zerlegung in mehrere Schritte unterteilt wird; das Ergebnis wird in „invsqrtC_times_yw“ gespeichert.
Die Methode aktualisiert anschließend den Evolutionspfad für die Schrittweite ps unter Verwendung von invsqrtC_times_yw, ermittelt anhand der ps-Norm und der erwarteten Länge, ob der Fortschritt als „stagnierend“ eingestuft wird, und aktualisiert dann den Evolutionspfad für die pc-Kovarianzmatrix unter Verwendung von y_w und dem Fortschrittsindikator hsig. Die Methode berechnet zunächst die Aktualisierungsmatrix c1a von Rank-One, anschließend die Aktualisierungsmatrix cmu von Rank-μ auf der Grundlage der Differenz zwischen den Einzelwerten und dem alten Mittelwert, geteilt durch Sigma unter Verwendung von „Gewichten“, aktualisiert die Kovarianzmatrix covMatrix anhand der Aktualisierungen von Rank-One- und Rank-μ und passt c1 an, wenn der Fortschritt ins Stocken gerät. Die Funktion „EnforcePositiveDefinite“ wird regelmäßig aufgerufen, um sicherzustellen, dass die Kovarianzmatrix positiv definit ist. Die Sigma-Schrittweite wird auf der Grundlage der PS-Norm aktualisiert und aus Gründen der numerischen Stabilität auf den Bereich zwischen min_sigma und max_sigma begrenzt.
Im Allgemeinen passt „UpdateDistribution“ die Verteilungsparameter (Kovarianzmatrix und Schrittweite) auf der Grundlage des Suchverlaufs (Evolutionspfad) und der Populationsdaten an, um sich an die Zielfunktionslandschaft anzupassen.
//+------------------------------------------------------------------+ //| Update distribution parameters | //+------------------------------------------------------------------+ void C_AO_CMAES::UpdateDistribution () { // Check the necessity of eigenvalue decomposition if (counteval - eigeneval > eigenInterval) { ComputeEigendecomposition (); eigeneval = counteval; } // Preserve the old average double oldMean []; ArrayResize (oldMean, coords); ArrayCopy (oldMean, cB, 0, 0, coords); // Update average UpdateMean (); // Calculate the mean shift double y_w []; ArrayResize (y_w, coords); for (int j = 0; j < coords; j++) { y_w [j] = (cB [j] - oldMean [j]) / sigma; } // Calculate C^(-1/2) * y_w // Step 1: B^T * y_w ArrayInitialize (temp_vec, 0.0); for (int i = 0; i < coords; i++) { for (int j = 0; j < coords; j++) { temp_vec [i] += B [j * coords + i] * y_w [j]; } } // Step 2: D^(-1) * (B^T * y_w) for (int i = 0; i < coords; i++) { temp_vec [i] /= D [i]; } // Step 3: B * D^(-1) * B^T * y_w ArrayInitialize (invsqrtC_times_yw, 0.0); for (int i = 0; i < coords; i++) { for (int j = 0; j < coords; j++) { invsqrtC_times_yw [i] += B [i * coords + j] * temp_vec [j]; } } // Update the evolution path for sigma double norm_ps_squared = 0.0; for (int i = 0; i < coords; i++) { ps [i] = (1.0 - cs) * ps [i] + MathSqrt (cs * (2.0 - cs) * mu_eff) * invsqrtC_times_yw [i]; norm_ps_squared += ps [i] * ps [i]; } // Heaviside function double norm_ps = MathSqrt (norm_ps_squared); double expected_length = MathSqrt (1.0 - MathPow (1.0 - cs, 2.0 * counteval)) * chiN; bool hsig = norm_ps / expected_length < hsig_threshold; // Update the evolution path for C double delta_hsig = hsig ? 1.0 : 0.0; for (int i = 0; i < coords; i++) { pc [i] = (1.0 - cc) * pc [i] + delta_hsig * MathSqrt (cc * (2.0 - cc) * mu_eff) * y_w [i]; } // Prepare rank-1 update double c1a []; ArrayResize (c1a, coords * coords); for (int i = 0; i < coords; i++) { for (int j = 0; j <= i; j++) { c1a [i * coords + j] = c1a [j * coords + i] = pc [i] * pc [j]; } } // Prepare rank-μ update double cmu []; ArrayResize (cmu, coords * coords); ArrayInitialize (cmu, 0.0); for (int k = 0; k < mu; k++) { int idx = (int)arindex [k]; // Calculate y_i = (x_i - m_old) / sigma for (int i = 0; i < coords; i++) { temp_vec [i] = (a [idx].c [i] - oldMean [i]) / sigma; } // Add the weighted outer product for (int i = 0; i < coords; i++) { for (int j = 0; j <= i; j++) { double update = weights [k] * temp_vec [i] * temp_vec [j]; cmu [i * coords + j] += update; if (i != j) cmu [j * coords + i] += update; } } } // Update C the covariance matrix double c1 = learningRateC1; double cmu_rate = learningRateCMu; // Adjust c1 if hsig is 'false' (progress stalled) if (!hsig) { c1 *= (1.0 - (1.0 - delta_hsig) * cc * (2.0 - cc)); } double one_minus_c1_cmu = 1.0 - c1 - cmu_rate; // Update C with rank-1 and rank-μ updates for (int i = 0; i < coords; i++) { for (int j = 0; j <= i; j++) { covMatrix [i * coords + j] = one_minus_c1_cmu * covMatrix [i * coords + j] + c1 * c1a [i * coords + j] + cmu_rate * cmu [i * coords + j]; // Maintain symmetry if (i != j) { covMatrix [j * coords + i] = covMatrix [i * coords + j]; } } } // Ensure positive definiteness if (counteval % (10 * eigenInterval) == 0) { EnforcePositiveDefinite (); } // Update the sigma step size double exponent = (cs / damps) * (norm_ps / chiN - 1.0); sigma *= MathExp (exponent); // Limit sigma for numerical stability double min_sigma = 1e-16; double max_eigenvalue = D [0]; // D sorted in descending order double max_sigma = 1e4 * MathMax (1.0, MathSqrt (max_eigenvalue)); if (sigma < min_sigma) sigma = min_sigma; else if (sigma > max_sigma) sigma = max_sigma; } //————————————————————————————————————————————————————————————————————
Die Funktion „ComputeEigendecomposition“ führt die Zerlegung der symmetrischen Kovarianzmatrix „covMatrix“ in Eigenwerte und Eigenvektoren mithilfe der verbesserten Jacobi-Methode durch. Dabei wird eine Kopie der „covMatrix“ erstellt, um die Originaldaten nicht zu verändern. Zunächst wird B als Identitätsmatrix gesetzt, die die Anfangsbasis darstellt. Die Iteration läuft entweder bis zur maximalen Iterationszahl oder bis die von Null verschiedenen Elemente auf den Nebendiagonalen kleiner als „tolerance“ sind.
Das größte Element außerhalb der Diagonalen finden: Finde das Element mit dem größten Absolutwert außerhalb der Diagonalen, um es für die Drehung auszuwählen. Ist der Maximalwert kleiner als „Toleranz“, wird der Zyklus unterbrochen. Die Methode berechnet den Winkel φ, um die jakobische Rotation durchzuführen, modifiziert die Elemente von C_copy durch Anwendung der Rotation und aktualisiert die Spalten der Matrix B (Eigenvektoren) entsprechend der Rotation. Nach den Iterationen ermittelt die Methode die Quadratwurzel der Diagonalelemente (wobei eine minimale Positivität von 1e-14 gewährleistet ist) und ordnet die Eigenwerte und die zugehörigen Eigenvektoren in absteigender Reihenfolge für die weitere Verwendung. Somit ermöglicht diese Methode die genaue Berechnung der Eigenwerte und Eigenvektoren der Kovarianzmatrix.
//+------------------------------------------------------------------+ //| Calculate the eigenvalue decomposition using the Jacobi method | //+------------------------------------------------------------------+ void C_AO_CMAES::ComputeEigendecomposition () { // Create a copy of the covariance matrix for decomposition double C_copy []; ArrayResize (C_copy, coords * coords); ArrayCopy (C_copy, covMatrix); // Initialize B as the identity matrix for (int i = 0; i < coords; i++) { for (int j = 0; j < coords; j++) { B [i * coords + j] = (i == j) ? 1.0 : 0.0; } } // Improved Jacobi eigenvalue decomposition int max_iterations = 10; //50 * coords; double tolerance = 0.01; //1e-14 * coords * coords; for (int iter = 0; iter < max_iterations; iter++) { // Find the largest off-diagonal element double max_val = 0.0; int p = 0, q = 1; for (int i = 0; i < coords - 1; i++) { for (int j = i + 1; j < coords; j++) { double val = MathAbs (C_copy [i * coords + j]); if (val > max_val) { max_val = val; p = i; q = j; } } } // Check convergence if (max_val < tolerance) break; // Calculate the rotation angle double app = C_copy [p * coords + p]; double aqq = C_copy [q * coords + q]; double apq = C_copy [p * coords + q]; double phi = 0.5 * MathArctan (2.0 * apq / (aqq - app + 1e-14)); double c = MathCos (phi); double s = MathSin (phi); // Update the matrix elements double app_new = c * c * app - 2 * c * s * apq + s * s * aqq; double aqq_new = s * s * app + 2 * c * s * apq + c * c * aqq; C_copy [p * coords + p] = app_new; C_copy [q * coords + q] = aqq_new; C_copy [p * coords + q] = C_copy [q * coords + p] = 0.0; // Update other elements in p and q rows/columns for (int i = 0; i < coords; i++) { if (i != p && i != q) { double aip = C_copy [i * coords + p]; double aiq = C_copy [i * coords + q]; C_copy [i * coords + p] = C_copy [p * coords + i] = c * aip - s * aiq; C_copy [i * coords + q] = C_copy [q * coords + i] = s * aip + c * aiq; } } // Update eigenvectors for (int i = 0; i < coords; i++) { double bip = B [i * coords + p]; double biq = B [i * coords + q]; B [i * coords + p] = c * bip - s * biq; B [i * coords + q] = s * bip + c * biq; } } // Extract eigenvalues and ensure positivity double min_eigenvalue = 1e-14; for (int i = 0; i < coords; i++) { D [i] = MathSqrt (MathMax (min_eigenvalue, C_copy [i * coords + i])); } // Sort eigenvalues and vectors in descending order for (int i = 0; i < coords - 1; i++) { int max_idx = i; for (int j = i + 1; j < coords; j++) { if (D [j] > D [max_idx]) max_idx = j; } if (max_idx != i) { // Exchange eigenvalues double temp = D [i]; D [i] = D [max_idx]; D [max_idx] = temp; // Exchange eigenvectors for (int k = 0; k < coords; k++) { temp = B [k * coords + i]; B [k * coords + i] = B [k * coords + max_idx]; B [k * coords + max_idx] = temp; } } } } //————————————————————————————————————————————————————————————————————
Die Methode „CheckPositiveDefinite“ prüft, ob die Kovarianzmatrix positiv definit ist. Es prüft schnell, ob die Diagonalelemente positiv sind, und vergleicht den kleinsten Eigenwert (aus dem in absteigender Reihenfolge sortierten Array „D“) mit einer kleinen positiven Zahl von der Form 1e-14. Wenn beide Prüfungen erfolgreich sind, gibt die Methode „true “ zurück.
//+------------------------------------------------------------------+ //| Check the positive definiteness of the covariance matrix | //+------------------------------------------------------------------+ bool C_AO_CMAES::CheckPositiveDefinite () { // Quick check: all diagonal elements should be positive for (int i = 0; i < coords; i++) { if (covMatrix [i * coords + i] <= 0) return false; } // Check if the minimum eigenvalue is positive double min_eigenvalue = D [coords - 1]; // D is sorted in descending order return min_eigenvalue > 1e-14; } //————————————————————————————————————————————————————————————————————
Die Methode „EnforcePositiveDefinite“ stellt die positive Definitheit der Kovarianzmatrix „covMatrix“ sicher. Sie führt mehrere Schritte durch: Es ermittelt das kleinste Element auf der Diagonalen und addiert den fehlenden „Korrekturwert“ zu den Diagonalelementen, sodass das Minimum 1e-10 beträgt; außerdem maximiert es die Symmetrie der Matrix, indem es jedes Element außerhalb der Diagonalen mit seiner Transponierten mittelt.
Ist die Matrix immer noch nicht positiv definit, wird mit „ComputeEigendecomposition“ eine Eigenwertzerlegung durchgeführt, wobei alle Eigenwerte kleiner als √1e-10 durch √1e-10 ersetzt werden, um die Positivität sicherzustellen. Die Methode berechnet dann die covMatrix unter Verwendung der B-Eigenvektoren und der korrigierten D-Eigenwerte neu, um eine korrigierte positiv definite Matrix zu erhalten. Dieser Ansatz stellt sicher, dass die Kovarianzmatrix positiv definit wird und sich für weitere Berechnungen im CMA-ES-Algorithmus eignet.
//+------------------------------------------------------------------+ //| Ensuring positive definiteness of the covariance matrix | //+------------------------------------------------------------------+ void C_AO_CMAES::EnforcePositiveDefinite () { // Method 1: Adding a small value to the diagonal double min_diag = 1e308; // A very large number for (int i = 0; i < coords; i++) { if (covMatrix [i * coords + i] < min_diag) { min_diag = covMatrix [i * coords + i]; } } if (min_diag < 1e-10) { double correction = 1e-10 - min_diag; for (int i = 0; i < coords; i++) { covMatrix [i * coords + i] += correction; } } // Method 2: Ensuring symmetry for (int i = 0; i < coords; i++) { for (int j = i + 1; j < coords; j++) { double avg = (covMatrix [i * coords + j] + covMatrix [j * coords + i]) * 0.5; covMatrix [i * coords + j] = covMatrix [j * coords + i] = avg; } } // If still not positive definite, perform the factorization and fix if (!CheckPositiveDefinite ()) { ComputeEigendecomposition (); double min_eigenvalue = 1e-10; for (int i = 0; i < coords; i++) { if (D [i] < MathSqrt (min_eigenvalue)) { D [i] = MathSqrt (min_eigenvalue); } } // Reconstruct C = B * D^2 * B^T ArrayInitialize (covMatrix, 0.0); for (int i = 0; i < coords; i++) { for (int j = 0; j <= i; j++) { double sum = 0.0; for (int k = 0; k < coords; k++) { sum += B [i * coords + k] * D [k] * D [k] * B [j * coords + k]; } covMatrix [i * coords + j] = covMatrix [j * coords + i] = sum; } } } } //————————————————————————————————————————————————————————————————————
Testergebnisse
Nun können wir uns endlich die Ergebnisse ansehen. Wie sofort erkennbar ist, bewältigt der Algorithmus mittelgroße Probleme sowie – bei bestimmten Einstellungen – Probleme mit geringer Dimension gut und schnell; die schwierigsten, mehrdimensionalen Probleme erfordern jedoch einen unverhältnismäßig hohen Zeitaufwand für die Ausführung, sodass sie praktisch aus den Ergebnissen ausgeschlossen wurden. Warum ist das so? Traditionell stellen Matrixberechnungen in MQL5 ein erhebliches Rechenproblem dar. Bei der manuellen Durchführung von Matrixoperationen zeigt der Algorithmus bei hochdimensionalen Problemen eine unbefriedigende Leistung, was seine praktische Anwendbarkeit erheblich einschränkt. Mit der Einführung integrierter Klassen für die Arbeit mit Matrizen ändert sich die Situation jedoch grundlegend. Es ist nun von entscheidender Bedeutung, für alle rechenintensiven Aufgaben die plattformnative Implementierung von Matrixoperationen zu verwenden.
=============================
5 Hilly's; Func runs: 10000; result: 0.7625797883550075
25 Hilly's; Func runs: 10000; result: 0.7208874560706138
500 Hilly's; Func runs: 10000; result: 0.0
=============================
5 Forest's; Func runs: 10000; result: 0.8205636421348295
25 Forest's; Func runs: 10000; result: 0.7961602627346933
500 Forest's; Func runs: 10000; result: 0.0
=============================
5 Megacity's; Func runs: 10000; result: 0.7584615384615383
25 Megacity's; Func runs: 10000; result: 0.49076923076923074
500 Megacity's; Func runs: 10000; result: 0.0
=============================
All score: 4.34942 (48.33%)
In der Visualisierung ist zu erkennen, dass der Algorithmus sowohl bei „großen Sprüngen“ funktioniert als auch sich auf die Extremwerte der Funktion konzentriert und diese gründlich untersucht.

CMA-ES mit der Testfunktion Hilly

CMA-ES mit der Testfunktion Forest

CMA-ES mit der Testfunktion Megacity
Aufgrund der Ergebnisse belegt der CMA-ES-Algorithmus Platz 38 in der Rangliste der leistungsstärksten populationsbasierten Optimierungsalgorithmen.
# | AO | Beschreibung | Hilly | Hilly Final | Forest | Forest Final | Megacity (discrete) | Megacity Final | Final Result | % of MAX | ||||||
| 10 p (5 F) | 50 p (25 F) | 1000 p (500 F) | 10 p (5 F) | 50 p (25 F) | 1000 p (500 F) | 10 p (5 F) | 50 p (25 F) | 1000 p (500 F) | ||||||||
| 1 | ANS | Suche über die gesamte Nachbarschaft | 0.94948 | 0.84776 | 0.43857 | 2.23581 | 1.00000 | 0.92334 | 0.39988 | 2.32323 | 0.70923 | 0.63477 | 0.23091 | 1.57491 | 6.134 | 68.15 |
| 2 | CLA | Code-Lock-Algorithmus (joo) | 0.95345 | 0.87107 | 0.37590 | 2.20042 | 0.98942 | 0.91709 | 0.31642 | 2.22294 | 0.79692 | 0.69385 | 0.19303 | 1.68380 | 6.107 | 67.86 |
| 3 | AMOm | Optimierung der Tierwanderung M | 0.90358 | 0.84317 | 0.46284 | 2.20959 | 0.99001 | 0.92436 | 0.46598 | 2.38034 | 0.56769 | 0.59132 | 0.23773 | 1.39675 | 5.987 | 66.52 |
| 4 | (P+O)ES | (P+O) Entwicklungsstrategien | 0.92256 | 0.88101 | 0.40021 | 2.20379 | 0.97750 | 0.87490 | 0.31945 | 2.17185 | 0.67385 | 0.62985 | 0.18634 | 1.49003 | 5.866 | 65.17 |
| 5 | CTA | Kometenschweif-Algorithmus (joo) | 0.95346 | 0.86319 | 0.27770 | 2.09435 | 0.99794 | 0.85740 | 0.33949 | 2.19484 | 0.88769 | 0.56431 | 0.10512 | 1.55712 | 5.846 | 64.96 |
| 6 | TETA | Zeitentwicklungs-Reisealgorithmus (joo) | 0.91362 | 0.82349 | 0.31990 | 2.05701 | 0.97096 | 0.89532 | 0.29324 | 2.15952 | 0.73462 | 0.68569 | 0.16021 | 1.58052 | 5.797 | 64.41 |
| 7 | SDSm | stochastische Diffusionssuche M | 0.93066 | 0.85445 | 0.39476 | 2.17988 | 0.99983 | 0.89244 | 0.19619 | 2.08846 | 0.72333 | 0.61100 | 0.10670 | 1.44103 | 5.709 | 63.44 |
| 8 | BOAm | Billard-Optimierungsalgorithmus M | 0.95757 | 0.82599 | 0.25235 | 2.03590 | 1.00000 | 0.90036 | 0.30502 | 2.20538 | 0.73538 | 0.52523 | 0.09563 | 1.35625 | 5.598 | 62.19 |
| 9 | AAm | Algorithmus für das Bogenschießen M | 0.91744 | 0.70876 | 0.42160 | 2.04780 | 0.92527 | 0.75802 | 0.35328 | 2.03657 | 0.67385 | 0.55200 | 0.23738 | 1.46323 | 5.548 | 61.64 |
| 10 | ESG | Entwicklung sozialer Gruppen (joo) | 0.99906 | 0.79654 | 0.35056 | 2.14616 | 1.00000 | 0.82863 | 0.13102 | 1.95965 | 0.82333 | 0.55300 | 0.04725 | 1.42358 | 5.529 | 61.44 |
| 11 | SIA | simuliertes isotropes Tempern (joo) | 0.95784 | 0.84264 | 0.41465 | 2.21513 | 0.98239 | 0.79586 | 0.20507 | 1.98332 | 0.68667 | 0.49300 | 0.09053 | 1.27020 | 5.469 | 60.76 |
| 12 | BBO | biogeografisch basierte Optimierung | 0.94912 | 0.69456 | 0.35031 | 1.99399 | 0.93820 | 0.67365 | 0.25682 | 1.86867 | 0.74615 | 0.48277 | 0.17369 | 1.40261 | 5.265 | 58.50 |
| 13 | ACS | künstliche kooperative Suche | 0.75547 | 0.74744 | 0.30407 | 1.80698 | 1.00000 | 0.88861 | 0.22413 | 2.11274 | 0.69077 | 0.48185 | 0.13322 | 1.30583 | 5.226 | 58.06 |
| 14 | DA | dialektischer Algorithmus | 0.86183 | 0.70033 | 0.33724 | 1.89940 | 0.98163 | 0.72772 | 0.28718 | 1.99653 | 0.70308 | 0.45292 | 0.16367 | 1.31967 | 5.216 | 57.95 |
| 15 | BHAm | Schwarzes-Loch-Algorithmus M | 0.75236 | 0.76675 | 0.34583 | 1.86493 | 0.93593 | 0.80152 | 0.27177 | 2.00923 | 0.65077 | 0.51646 | 0.15472 | 1.32195 | 5.196 | 57.73 |
| 16 | ASO | Optimierung einer anarchischen Gesellschaft | 0.84872 | 0.74646 | 0.31465 | 1.90983 | 0.96148 | 0.79150 | 0.23803 | 1.99101 | 0.57077 | 0.54062 | 0.16614 | 1.27752 | 5.178 | 57.54 |
| 17 | RFO | Optimierung des Royal Flush (joo) | 0.83361 | 0.73742 | 0.34629 | 1.91733 | 0.89424 | 0.73824 | 0.24098 | 1.87346 | 0.63154 | 0.50292 | 0.16421 | 1.29867 | 5.089 | 56.55 |
| 18 | AOSm | Suche nach Atomorbitalen M | 0.80232 | 0.70449 | 0.31021 | 1.81702 | 0.85660 | 0.69451 | 0.21996 | 1.77107 | 0.74615 | 0.52862 | 0.14358 | 1.41835 | 5.006 | 55.63 |
| 19 | TSEA | Algorithmus der Schildkrötenpanzer-Evolution (JOO) | 0.96798 | 0.64480 | 0.29672 | 1.90949 | 0.99449 | 0.61981 | 0.22708 | 1.84139 | 0.69077 | 0.42646 | 0.13598 | 1.25322 | 5.004 | 55.60 |
| 20 | DE | differentielle Evolution | 0.95044 | 0.61674 | 0.30308 | 1.87026 | 0.95317 | 0.78896 | 0.16652 | 1.90865 | 0.78667 | 0.36033 | 0.02953 | 1.17653 | 4.955 | 55.06 |
| 21 | SRA | Algorithmus für erfolgreiche Gastronomen (joo) | 0.96883 | 0.63455 | 0.29217 | 1.89555 | 0.94637 | 0.55506 | 0.19124 | 1.69267 | 0.74923 | 0.44031 | 0.12526 | 1.31480 | 4.903 | 54.48 |
| 22 | CRO | Optimierung chemischer Reaktionen | 0.94629 | 0.66112 | 0.29853 | 1.90593 | 0.87906 | 0.58422 | 0.21146 | 1.67473 | 0.75846 | 0.42646 | 0.12686 | 1.31178 | 4.892 | 54.36 |
| 23 | BIO | Optimierung der Blutvererbung (joo) | 0.81568 | 0.65336 | 0.30877 | 1.77781 | 0.89937 | 0.65319 | 0.21760 | 1.77016 | 0.67846 | 0.47631 | 0.13902 | 1.29378 | 4.842 | 53.80 |
| 24 | BSA | Vogelschwarm-Algorithmus | 0.89306 | 0.64900 | 0.26250 | 1.80455 | 0.92420 | 0.71121 | 0.24939 | 1.88479 | 0.69385 | 0.32615 | 0.10012 | 1.12012 | 4.809 | 53.44 |
| 25 | HS | Harmoniesuche | 0.86509 | 0.68782 | 0.32527 | 1.87818 | 0.99999 | 0.68002 | 0.09590 | 1.77592 | 0.62000 | 0.42267 | 0.05458 | 1.09725 | 4.751 | 52.79 |
| 26 | SSG | Setzlinge säen und wachsen | 0.77839 | 0.64925 | 0.39543 | 1.82308 | 0.85973 | 0.62467 | 0.17429 | 1.65869 | 0.64667 | 0.44133 | 0.10598 | 1.19398 | 4.676 | 51.95 |
| 27 | BCOm | Optimierung der bakteriellen Chemotaxis M | 0.75953 | 0.62268 | 0.31483 | 1.69704 | 0.89378 | 0.61339 | 0.22542 | 1.73259 | 0.65385 | 0.42092 | 0.14435 | 1.21912 | 4.649 | 51.65 |
| 28 | ABO | Optimierung nach afrikanischen Büffeln | 0.83337 | 0.62247 | 0.29964 | 1.75548 | 0.92170 | 0.58618 | 0.19723 | 1.70511 | 0.61000 | 0.43154 | 0.13225 | 1.17378 | 4.634 | 51.49 |
| 29 | (PO)ES | (PO) Evolutionsstrategien | 0.79025 | 0.62647 | 0.42935 | 1.84606 | 0.87616 | 0.60943 | 0.19591 | 1.68151 | 0.59000 | 0.37933 | 0.11322 | 1.08255 | 4.610 | 51.22 |
| 30 | FBA | Fraktal-basierter Algorithmus | 0.79000 | 0.65134 | 0.28965 | 1.73099 | 0.87158 | 0.56823 | 0.18877 | 1.62858 | 0.61077 | 0.46062 | 0.12398 | 1.19537 | 4.555 | 50.61 |
| 31 | TSm | Tabu-Suche M | 0.87795 | 0.61431 | 0.29104 | 1.78330 | 0.92885 | 0.51844 | 0.19054 | 1.63783 | 0.61077 | 0.38215 | 0.12157 | 1.11449 | 4.536 | 50.40 |
| 32 | BSO | Brainstorming-Optimierung | 0.93736 | 0.57616 | 0.29688 | 1.81041 | 0.93131 | 0.55866 | 0.23537 | 1.72534 | 0.55231 | 0.29077 | 0.11914 | 0.96222 | 4.498 | 49.98 |
| 33 | WOAm | Wal-Optimierungsalgorithmus M | 0.84521 | 0.56298 | 0.26263 | 1.67081 | 0.93100 | 0.52278 | 0.16365 | 1.61743 | 0.66308 | 0.41138 | 0.11357 | 1.18803 | 4.476 | 49.74 |
| 34 | AEFA | Algorithmus für künstliche elektrische Felder | 0.87700 | 0.61753 | 0.25235 | 1.74688 | 0.92729 | 0.72698 | 0.18064 | 1.83490 | 0.66615 | 0.11631 | 0.09508 | 0.87754 | 4.459 | 49.55 |
| 35 | AEO | Algorithmus zur Optimierung auf der Grundlage künstlicher Ökosysteme | 0.91380 | 0.46713 | 0.26470 | 1.64563 | 0.90223 | 0.43705 | 0.21400 | 1.55327 | 0.66154 | 0.30800 | 0.28563 | 1.25517 | 4.454 | 49.49 |
| 36 | CAm | Kamel-Algorithmus M | 0.78684 | 0.56042 | 0.35133 | 1.69859 | 0.82772 | 0.56041 | 0.24336 | 1.63149 | 0.64846 | 0.33092 | 0.13418 | 1.11356 | 4.444 | 49.37 |
| 37 | ACOm | Ameisen-Kolonie-Optimierung M | 0.88190 | 0.66127 | 0.30377 | 1.84693 | 0.85873 | 0.58680 | 0.15051 | 1.59604 | 0.59667 | 0.37333 | 0.02472 | 0.99472 | 4.438 | 49.31 |
| 38 | CMAES | Anpassung der Kovarianzmatrix mittels Evolutionsstrategie | 0.76258 | 0.72089 | 0.00000 | 1.48347 | 0.82056 | 0.79616 | 0.00000 | 1.61672 | 0.75846 | 0.49077 | 0.00000 | 1.24923 | 4.349 | 48.33 |
| 39 | BFO-GA | Optimierung der bakteriellen Nahrungssuche - ga | 0.89150 | 0.55111 | 0.31529 | 1.75790 | 0.96982 | 0.39612 | 0.06305 | 1.42899 | 0.72667 | 0.27500 | 0.03525 | 1.03692 | 4.224 | 46.93 |
| 40 | SOA | einfacher Optimierungsalgorithmus | 0.91520 | 0.46976 | 0.27089 | 1.65585 | 0.89675 | 0.37401 | 0.16984 | 1.44060 | 0.69538 | 0.28031 | 0.10852 | 1.08422 | 4.181 | 46.45 |
| 41 | ABHA | Algorithmus für künstliche Bienenstöcke | 0.84131 | 0.54227 | 0.26304 | 1.64663 | 0.87858 | 0.47779 | 0.17181 | 1.52818 | 0.50923 | 0.33877 | 0.10397 | 0.95197 | 4.127 | 45.85 |
| 42 | ACMO | Optimierung atmosphärischer Wolkenmodelle | 0.90321 | 0.48546 | 0.30403 | 1.69270 | 0.80268 | 0.37857 | 0.19178 | 1.37303 | 0.62308 | 0.24400 | 0.10795 | 0.97503 | 4.041 | 44.90 |
| 43 | ADAMm | adaptive Momentenschätzung M | 0.88635 | 0.44766 | 0.26613 | 1.60014 | 0.84497 | 0.38493 | 0.16889 | 1.39880 | 0.66154 | 0.27046 | 0.10594 | 1.03794 | 4.037 | 44.85 |
| 44 | CGO | Optimierung von Chaos-Spielen | 0.57256 | 0.37158 | 0.32018 | 1.26432 | 0.61176 | 0.61931 | 0.62161 | 1.85267 | 0.37538 | 0.21923 | 0.19028 | 0.78490 | 3.902 | 43.35 |
| 45 | CROm | Korallenriff-Optimierung M | 0.78512 | 0.46032 | 0.25958 | 1.50502 | 0.86688 | 0.35297 | 0.16267 | 1.38252 | 0.63231 | 0.26738 | 0.10734 | 1.00703 | 3.895 | 43.27 |
| RW | Random Walk | 0.48754 | 0.32159 | 0.25781 | 1.06694 | 0.37554 | 0.21944 | 0.15877 | 0.75375 | 0.27969 | 0.14917 | 0.09847 | 0.52734 | 2.348 | 26.09 | |
Zusammenfassung
Die Fähigkeit von CMA-ES, die Suchstrategie an die lokale Geometrie der Zielfunktion anzupassen, macht es für komplexe, schlecht konditionierte Probleme mit unbekannter Struktur praktisch unverzichtbar. Die dickeren Verteilungsenden der Potenzgesetzverteilung ermöglichen es dem Algorithmus, „große Sprünge“ zu machen, was entscheidend ist, um lokale Optima zu überwinden. Die Rechenkomplexität von O(n²) Speicherplatz und O(n³) Zeit schränkt die Anwendbarkeit des Algorithmus erheblich ein. Bei hochdimensionalen Problemen (n > 100) steht der Ressourcenaufwand in keinem Verhältnis mehr zum erzielten Nutzen. Die Laufzeit bei mehrdimensionalen Funktionen steigt so stark an, dass der Algorithmus für große n praktisch nicht mehr anwendbar ist.
CMA-ES arbeitet mit verrauschten Funktionen, kommt mit diskontinuierlichen Zielfunktionen zurecht und ist auf Landschaften mit mehreren Extrema effektiv. Das Geheimnis dieser Vielseitigkeit liegt in der grundlegenden Philosophie des Algorithmus: Anstatt Annahmen über die Struktur der Funktion zu treffen, erkundet er diese sorgfältig und passt seine Suchstrategie auf der Grundlage gesammelter Erfahrungen an. CMA-ES verändert die Landschaft der evolutionären Berechnung, indem es zeigt, dass biologisch inspirierte Algorithmen auf strengen mathematischen Grundlagen beruhen können und die Anpassung von Algorithmen nicht bloß eine Feinabstimmung von Parametern ist, sondern ein grundlegendes Prinzip zum Erlernen der Struktur eines Problems.
Der Algorithmus eignet sich hervorragend für mittelgroße Probleme; es handelt sich um ein Spezialwerkzeug, das in seiner Nische eine vielversprechende Alternative darstellt. Der Algorithmus verbindet mathematische Eleganz mit praktischer Effizienz, muss jedoch angesichts seiner rechnerischen Grenzen sorgfältig angewendet werden. In solchen Fällen haben die Entwickler der MQL5-Sprache kürzlich schnelle MQL5-Matrixoperationen zur Berechnung von Matrizen eingeführt; in dieser Implementierung des Algorithmus wurden herkömmliche sequenzielle Berechnungen verwendet.
Abbildung 2. Farbskala der Algorithmen nach den entsprechenden Tests

Abbildung 3. Histogramm der Algorithmus-Testergebnisse (Skala von 0 bis 100, je höher, desto besser, wobei 100 das maximal mögliche theoretische Ergebnis ist; im Archiv befindet sich ein Skript zur Berechnung der Bewertungstabelle)
Vor- und Nachteile des CMAES:
Vorteile:
- Gute Konvergenz bei Funktionen mittlerer Dimension.
Nachteile:
- Streuung der Ergebnisse bei niedrigdimensionalen Funktionen.
- Hoher Ressourcenverbrauch bei hochdimensionalen Problemen.
Dem Artikel ist ein Archiv mit den aktuellen Versionen der Algorithmuscodes beigefügt. Der Autor des Artikels übernimmt keine Verantwortung für die absolute Richtigkeit der Beschreibung der kanonischen Algorithmen. An vielen von ihnen wurden Änderungen vorgenommen, um die Suchmöglichkeiten zu verbessern. Die in den Artikeln dargelegten Schlussfolgerungen und Urteile beruhen auf den Ergebnissen der Experimente.
Programme, die in diesem Artikel verwendet werden
| # | Name | Typ | Beschreibung |
|---|---|---|---|
| 1 | #C_AO.mqh | Include | Übergeordnete Klasse von Populationsoptimierungsalgorithmen |
| 2 | #C_AO_enum.mqh | Include | Enumeration der Algorithmen zur Populationsoptimierung |
| 3 | TestFunctions.mqh | Include | Bibliothek mit Testfunktionen |
| 4 | TestStandFunctions.mqh | Include | Bibliothek mit Funktionen für die Testumgebung |
| 5 | Utilities.mqh | Include | Bibliothek mit Hilfsfunktionen |
| 6 | CalculationTestResults.mqh | Include | Skript zur Berechnung der Ergebnisse in der Vergleichstabelle |
| 7 | Testing AOs.mq5 | Skript | Die einheitliche Testumgebung für alle Algorithmen zur Populationsoptimierung |
| 8 | Simple use of population optimization algorithms.mq5 | Skript | Ein einfaches Beispiel für die Verwendung von Algorithmen zur Populationsoptimierung ohne Visualisierung |
| 9 | Test_AO_CMAES.mq5 | Skript | CMAES-Testumgebung |
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/18227
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Marktsimulation (Teil 23): Erste Schritte mit SQL (VI)
Entwicklung eines Expert Advisors für mehrere Währungen (Teil 27): Komponente zur Anzeige von mehrzeiligem Text
CFTC-Datenanalyse in Python und Erstellung eines KI-Modells
CAPM-Indikator für den Forex-Markt
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.
