English Русский Español Deutsch 日本語
preview
Estratégia evolutiva de adaptação da matriz de covariância, Covariance Matrix Adaptation Evolution Strategy (CMA-ES)

Estratégia evolutiva de adaptação da matriz de covariância, Covariance Matrix Adaptation Evolution Strategy (CMA-ES)

MetaTrader 5Negociação |
273 0
Andrey Dik
Andrey Dik

Conteúdo

  1. Introdução
  2. Implementação do algoritmo
  3. Resultados dos testes


Introdução

No mundo da otimização existem muitos algoritmos, porém buscamos os mais fortes para resolver as tarefas de otimização dos nossos robôs de trading. O CMA-ES (Covariance Matrix Adaptation Evolution Strategy) é um desses raros exemplos em que o rigor matemático se combina com a intuição biológica, criando um algoritmo que não apenas resolve problemas de otimização, mas aprende a compreender sua estrutura.

A história do CMA-ES começou no final da década de 1990, em laboratórios de pesquisa da Alemanha, onde Nikolaus Hansen e Andreas Ostermeier levantaram uma questão fundamental: seria possível criar um algoritmo de otimização que não apenas buscasse uma solução, mas se adaptasse à geometria do problema? Os algoritmos evolutivos tradicionais geravam descendentes em regiões esféricas ao redor dos indivíduos parentais, o que funcionava bem para funções simples, mas se mostrava ineficiente para problemas complexos e mal condicionados. Portanto, vamos analisar esse interessante algoritmo.


Implementação do algoritmo

Imagine a busca por um tesouro em uma ilha de formato irregular. A abordagem comum é procurar em todas as direções de maneira uniforme, como se a ilha fosse circular. O CMA-ES, por sua vez, estuda gradualmente o formato da ilha e passa a buscar predominantemente nas direções onde a probabilidade de encontrar o tesouro é maior. Além disso, ele memoriza rotas bem-sucedidas e utiliza essa memória para planejar buscas futuras.

Na base do CMA-ES está uma equação aparentemente simples: x_k ~ N(m, σ²C). Contudo, por trás dessa simplicidade existe uma profunda estrutura matemática. Cada símbolo aqui carrega uma informação importante sobre o estado da busca: m é a melhor estimativa atual da localização do ótimo, σ é a medida de quão longe estamos dispostos a arriscar afastar-nos do que já é conhecido, e C é a matriz de covariância, que codifica nosso entendimento da geometria da função. A única modificação justificada que introduziremos é substituir a distribuição normal por uma distribuição de potência, isto significa que a implementação seguirá a fórmula modificada: x_k ~ PowerDist(m, σ²C). Essa modificação altera o caráter da exploração do espaço, com "saltos" mais amplos, mas preserva a natureza adaptativa fundamental do algoritmo.

A matriz de covariância C é o verdadeiro coração do algoritmo Ela inicia sua existência como uma modesta matriz identidade, representando uma distribuição esférica. Porém, a cada iteração ela evolui, alongando-se nas direções de melhoria rápida e comprimindo-se onde o progresso é lento. Gradualmente, a esfera transforma-se em uma elipse e depois em um elipsoide alongado, perfeitamente orientado ao longo dos contornos da função que está sendo otimizada.

A principal inovação do CMA-ES — o conceito de caminhos evolutivos. Trata-se de uma espécie de memória genética do algoritmo, que se recorda não apenas de onde estavam os pontos bem-sucedidos, mas também de como o algoritmo chegou até eles. O caminho evolutivo acumula informações sobre passos consecutivos bem-sucedidos, criando um vetor direcionado que aponta para as regiões mais promissoras da busca. O segundo caminho evolutivo desempenha uma função mais sutil — ele controla o tamanho do passo. Se os passos consecutivos do algoritmo são correlacionados, isto é, cada passo seguinte continua a direção do anterior, o tamanho do passo aumenta — o algoritmo "sente" que está se movendo na direção correta. Se, por outro lado, os passos são aleatórios e não correlacionados, o tamanho do passo diminui — possivelmente o algoritmo já está próximo do ótimo e é necessário buscar com mais cautela.

Por trás da metáfora biológica da evolução no CMA-ES encontra-se um princípio matemático rigoroso — a maximização da verossimilhança. O algoritmo constantemente se pergunta: quais parâmetros da distribuição tornam os pontos bem-sucedidos observados os mais prováveis? Isso transforma a otimização evolutiva de uma heurística em um método estatisticamente fundamentado.

Cada atualização da matriz de covariância C consiste em dois componentes: rank-one atualização, baseada no caminho evolutivo, e rank-μ atualização, que utiliza informações de toda a população seleccionada. Rank-one atualização garante estabilidade e capacidade de captar tendências de longo prazo, enquanto rank-μ atualização permite adaptar-se rapidamente às novas informações da geração atual.

No contexto do CMA-ES é utilizada uma função de Heaviside modificada, que desempenha um papel importante no mecanismo de detecção de estagnação do algoritmo. A função compara o comprimento do caminho evolutivo com o comprimento esperado, se o caminho for muito curto — isso é um sinal de "andar em círculos". Condições de desativação (hsig = 0): o algoritmo "vaga" aleatoriamente e os passos se anulam mutuamente, nesse caso provavelmente o tamanho do passo é grande demais. Em situação de estagnação, reduz-se a influência da rank-one atualização e passa-se a confiar mais nas informações de toda a população. Condições de ativação (hsig = 1): o algoritmo progride em uma determinada direção, os passos consecutivos são correlacionados, então o tamanho do passo é adequado à situação atual.

Uma das propriedades mais profundas do CMA-ES é sua invariância às transformações do espaço de busca. O algoritmo resolve com a mesma eficiência a função f(x) e a função f(Ax + b), onde A é qualquer matriz não degenerada e b é qualquer vetor de deslocamento. Isso significa que o CMA-ES não depende da escolha do sistema de coordenadas. Essa invariância não é acidental — ela é consequência direta do uso do princípio da máxima verossimilhança e da adaptação da matriz de covariância. O algoritmo detecta automaticamente o sistema natural de coordenadas do problema, no qual os eixos coincidem com as direções principais de variação da função.

A beleza da teoria deve estar combinada com a aplicabilidade prática. O CMA-ES requer O(n²) de memória para armazenar a matriz de covariância e O(n³) de cálculos para sua própria decomposição, o que torna o algoritmo aplicável a tarefas com dimensionalidade de até algumas centenas de variáveis. Para dimensões maiores foram desenvolvidas modificações especializadas, sep-CMA-ES limita-se a matrizes de covariância diagonais, o VkD-CMA utiliza dimensionalidade variável e o LM-CMA aplica princípios de memória limitada, a implementação desses métodos por enquanto está fora do escopo do nosso artigo, possivelmente em artigos posteriores haverá a oportunidade de retornar a eles.

cmaes-algorithm

Figura 1. Na ilustração é mostrada a evolução ao longo das gerações,

três imagens do estado do algoritmo, gerações 1, 5 e 15, demonstrando como a população converge gradualmente para o ótimo,

Adaptação da matriz de covariância, as elipses azuis mostram como a forma da distribuição se altera quando a geração é: 1, circular, matriz identidade, 5, alongada e rotacionada, 15, ajustada com precisão à geometria local.
Componentes do algoritmo: pontos vermelhos, toda a população, λ descendentes, pontos verdes, as melhores soluções, μ pais, ponto preto m, média da população, círculos tracejados, isolinhas da função objetivo.

A fórmula-chave na parte inferior com a indicação da modificação para a distribuição de potência. A ilustração demonstra claramente como o algoritmo adapta sua estratégia de busca ao relevo da função otimizada, estreitando gradualmente a região de busca e aproximando-se do ótimo.

Passamos à escrita do pseudocódigo do algoritmo.

Inicialização

  1. Definir os parâmetros do algoritmo:
    • Tamanho da população, lambda = 50
    • Número de pais, mu = 25
    • Taxa de aprendizado para atualização rank-1, c1 = 0.01
    • Taxa de aprendizado para atualização rank-μ, cμ = 0.8
    • Amortecimento do tamanho do passo = 0.6
    • Tamanho inicial do passo, sigma = 0.3
  2. Calcular os pesos de recombinação:
    • Para cada um dos μ pais calcular o peso como log(mu + 0.5) - log(i + 1)
    • Normalizar os pesos de modo que sua soma seja igual a 1
    • Calcular a massa efetiva de seleção μ_eff
  3. Inicializar os parâmetros da estratégia:
    • Calcular as taxas de aprendizado para os caminhos evolutivos, cs, cc
    • Definir a norma esperada da distribuição normal padrão, chiN
    • Determinar o intervalo para a decomposição em autovalores
  4. Inicializar as estruturas de dados:
    • Matriz de covariância C = matriz identidade
    • Matriz de autovetores B = matriz identidade
    • Vetor de autovalores D = vetor unitário
    • Caminhos evolutivos pc e ps = vetores nulos
    • Valor médio da população m = ponto aleatório no espaço de busca
  5. Criar a população inicial:
    • Gerar lambda pontos aleatórios no espaço de busca

Ciclo principal da evolução

Repetir até que o critério de parada seja atingido:

Etapa 1: Geração de descendentes 

  1. Para cada um dos lambda descendentes:
    • Gerar um vetor aleatório z a partir da distribuição normal padrão
    • Aplicar a transformação: y = B × D × z
    • Criar o descendente: x_k = m + σ × y
    • Aplicar as restrições do espaço de busca a x_k

Etapa 2: Avaliação e atualização 

  1. Avaliar o fitness de todos os descendentes
  2. Ordenar a população:
    • Organizar os descendentes em ordem decrescente de fitness
    • Atualizar a melhor solução encontrada, se necessário
  3. Atualizar o valor médio da população:
    • Salvar a média antiga m_old
    • Calcular a nova média como soma ponderada dos μ melhores descendentes: m_new = Σ(w_i × x_i) para i de 1 até μ
  4. Atualizar os caminhos evolutivos:
    • Calcular o deslocamento da média: y_w = (m_new - m_old) / σ
    • Calcular C^(-1/2) × y_w por meio da decomposição em autovalores
    • Atualizar o caminho evolutivo para o tamanho do passo: ps = (1 - cs) × ps + √(cs × (2 - cs) × μ_eff) × C^(-1/2) × y_w
    • Verificar a condição de estagnação, função de Heaviside
    • Atualizar o caminho evolutivo para a matriz de covariância: pc = (1 - cc) × pc + indicador_estagnação × √(cc × (2 - cc) × μ_eff) × y_w
  5. Atualizar a matriz de covariância:
    • Preparar a atualização rank-1 a partir do produto externo de pc
    • Preparar a atualização rank-μ a partir da soma ponderada dos desvios dos melhores descendentes
    • Atualizar C: C = (1 - c1 - cμ) × C + c1 × (pc × pc^T) + cμ × Σ(w_i × y_i × y_i^T)
    • Garantir a simetria da matriz
  6. Atualizar o tamanho do passo:
    • Calcular o coeficiente de adaptação com base no comprimento do caminho ps
    • Atualizar σ: σ = σ × exp((cs/damps) × (||ps||/chiN - 1))
    • Limitar σ a valores razoáveis para estabilidade numérica
  7. Decomposição em autovalores, se necessário:
    • Se tiverem passado iterações suficientes desde a última decomposição:
      • Executar a decomposição C = B × D² × B^T pelo método de Jacobi
      • Ordenar os autovalores e autovetores em ordem decrescente
      • Garantir a definição positiva da matriz
  8. Verificação e correção da matriz de covariância:
    • Verificar periodicamente a definição positiva de C
    • Se necessário, adicionar um pequeno valor à diagonal
    • Garantir a simetria da matriz

Agora vamos escrever a classe "C_AO_CMAES", derivada da classe "C_AO", que implementará o algoritmo de otimização CMA-ES.

Campos públicos, método:

  • SetParams () - define os valores dos parâmetros do CMA-ES a partir do array "params".
  • Init () - inicializa o algoritmo. Recebe os valores mínimos, máximos e o tamanho do passo para cada variável, assim como o número de épocas.
  • Moving () - implementa o ciclo principal do algoritmo, responsável pela geração de novas soluções.
  • Revision () - avaliação das novas soluções e atualização do estado do algoritmo.

Campos privados:

  • Parâmetros do CMA-ES - variáveis, que contêm os parâmetros específicos do algoritmo CMA-ES: número de pais (mu), tamanho do passo (sigma), taxas de aprendizado (learningRateC1, learningRateCMu), fator de amortecimento (stepSizeDamping) e outros necessários para o funcionamento do algoritmo.
  • Estruturas de dados do CMA-ES - arrays utilizados para armazenar os pesos de recombinação (weights), a matriz de covariância (covMatrix), autovetores (B), autovalores (D), caminhos evolutivos (pc, ps), assim como outros dados auxiliares. As variáveis "mu_eff", "counteval", "eigeneval", "chiN", "eigenInterval" são utilizadas para controle e gerenciamento do andamento do algoritmo.
  • Variáveis para otimização de desempenho destinadas a acelerar os cálculos durante a execução do algoritmo, como as taxas de aprendizado e o amortecimento.
  • Arrays auxiliares, utilizados como armazenamentos temporários para vetores e matrizes durante a execução de diferentes operações.
  • Métodos auxiliares, executam etapas individuais do algoritmo CMA-ES, como a inicialização da distribuição, atualização da distribuição, cálculo da decomposição própria, ordenação da população, cálculo dos pesos, atualização do valor médio, verificação da definição positiva e garantia da definição positiva.

De modo geral, a classe "C_AO_CMAES" encapsula a lógica do algoritmo CMA-ES, fornecendo uma interface para inicialização, configuração de parâmetros e execução da otimização. Ela contém os parâmetros, arrays de dados e métodos necessários para a implementação do algoritmo. A separação entre campos públicos e privados garante o controle de acesso aos componentes internos da classe.

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

O método "Init" da classe "C_AO_CMAES" realiza a inicialização do algoritmo CMA-ES. Ele recebe como entrada os valores mínimos e máximos dos parâmetros, o tamanho do passo e o número de épocas. Primeiramente é chamado o método "StandardInit" para executar a inicialização padrão. O sinalizador "has_cached" é inicializado com "false" para indicar que não existe valor armazenado em cache para a geração de números aleatórios pelo método de Box-Muller. Em seguida, o tamanho do passo "sigma" é inicializado com o valor inicial igual a 0.3, 30% do intervalo de busca. É chamado o método "ComputeWeights" para calcular os pesos de recombinação e o método "GetChiN" para calcular a norma esperada de N(0, I).

São calculados os parâmetros "cs", "cc", "damps" e "hsig_threshold" com base em "mu_eff", massa efetiva, e no número de coordenadas, coords. Esses parâmetros são utilizados para adaptar a estratégia do CMA-ES durante a execução. É calculado e definido "eigenInterval", que determina a frequência de execução da decomposição em autovalores. Esse parâmetro é ajustado para otimizar o desempenho.

É alocada memória para os arrays específicos do CMA-ES, como a matriz de covariância "covMatrix", os autovetores "B", os autovalores "D", os caminhos evolutivos "pc" e "ps", os vetores temporários "y_vec", "arindex", "arfitness", "temp_vec" e "invsqrtC_times_yw". Os arrays são recriados apenas se for necessário alterar a dimensionalidade do problema.

Os arrays dos caminhos evolutivos "pc" e "ps" são inicializados com zeros, a matriz de covariância "covMatrix" e a matriz de autovetores "B" com a matriz identidade, e o array de autovalores "D" com uns. É chamado o método "InitDistribution" para inicializar a distribuição inicial. Em seguida, os contadores de avaliações da função "counteval" e "eigeneval" são zerados. O sinalizador "revision" é definido como "true" para indicar que é necessário recalcular os valores de fitness. Retorna-se "true" em caso de inicialização bem-sucedida.

Em conclusão, o método "Init" realiza toda a inicialização necessária para o funcionamento do algoritmo CMA-ES, incluindo a definição dos parâmetros, alocação de memória para os arrays e inicialização dos valores iniciais. 

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

O método "Moving" na classe "C_AO_CMAES" é responsável pela geração de novos descendentes (soluções) na população. Transformação y = B * D * z: dentro do laço sobre a população há um laço aninhado que percorre todas as coordenadas, "coords", de cada indivíduo. Em cada coordenada "i" é calculado o valor "y_vec[i]". Isso é feito por meio de multiplicação matricial. Na prática, trata-se da multiplicação do vetor de números aleatórios "z" pela matriz "B * D", onde "B" são os autovetores da matriz de covariância, "D" são os autovalores da matriz de covariância, e "z" são números aleatórios gerados utilizando a função "u.PowerDistribution". 

Geração do descendente: para cada coordenada "i" de cada descendente "k" é calculado o valor (a [k].c [i]). Isso é feito adicionando ao valor médio atual "cB [i]" (centro da distribuição atual) o vetor escalado "y_vec [i]". O escalonamento é realizado multiplicando "y_vec [i]" pelo tamanho do passo "sigma". Assim, a [k].c [i] = cB [i] + sigma * y_vec [i].

A função "u.SeInDiSp" garante que os valores das coordenadas dos descendentes permaneçam dentro dos limites permitidos definidos por "rangeMin [i]", "rangeMax [i]" e "rangeStep [i]". Ela pode truncar os valores, refletí-los nas fronteiras e quantizá-los para o valor de passo permitido mais próximo. Após a geração de todos os descendentes, o sinalizador "revision" é definido como "true", os valores da função de fitness (qualidade) para os descendentes gerados devem ser recalculados.

Em resumo, o método gera uma nova população de soluções com base na distribuição atual (média "cB" e matriz de covariância representada por "B" e "D") e no tamanho do passo "sigma". Ele utiliza números aleatórios para criar pequenas variações em torno da média atual, a fim de explorar o espaço de busca. A função "SeInDiSp" garante que as novas soluções permaneçam dentro dos limites permitidos, e o sinalizador "revision" assegura que a qualidade dessas soluções seja avaliada.

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

O método "Revision" da classe "C_AO_CMAES" é responsável pela atualização dos parâmetros do algoritmo após a avaliação da função de fitness para a nova população. Inicialmente, o método verifica o valor do sinalizador "revision".  Em seguida é chamado o método "SortPopulation ()", que ordena a população atual de modo que os melhores indivíduos sejam colocados no início. O método "UpdateDistribution ()" atualiza os parâmetros da distribuição do CMA-ES (valor médio "cB" do vetor de estratégia, matriz de covariância, tamanho do passo "sigma", caminhos evolutivos) com base nas informações dos melhores indivíduos da população. A atualização da distribuição é a etapa-chave do CMA-ES, pois permite que o algoritmo adapte sua estratégia de busca com base nos resultados obtidos. 

Em síntese, o método "Revision" representa o ciclo principal de adaptação do CMA-ES. Ele ordena a população, atualiza os parâmetros da distribuição com base nos melhores indivíduos e incrementa o contador de avaliações. 

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

O método "InitDistribution" da classe "C_AO_CMAES" destina-se à inicialização da distribuição inicial de busca de soluções. O método define o valor médio inicial (cB) para cada coordenada no espaço de busca. Para cada coordenada "i", o valor "cB [i]" é definido aleatoriamente no intervalo entre "rangeMin [i]" e "rangeMax [i]" com o auxílio da função "u.RNDfromCI ()", o valor médio inicial é posicionado no centro do intervalo permitido para cada variável. O laço externo percorre toda a população (popSize indivíduos). Dentro desse laço há um laço interno que percorre todas as coordenadas (coords) de cada indivíduo. Para cada descendente, suas coordenadas são geradas. Inicialmente elas são selecionadas aleatoriamente dentro do intervalo especificado por "RNDfromCI", e em seguida são arredondadas para o passo de discretização utilizando "SeInDiSp".

A função "u.RNDfromCI" gera um número aleatório com distribuição uniforme dentro de um intervalo especificado. A função "u.SeInDiSp" garante que as coordenadas geradas para cada indivíduo estejam dentro dos limites permitidos definidos rangeMin [i], "rangeMax [i]" e rangeStep [i].

O método inicializa a população inicial random soluções distribuídas uniformemente no espaço de busca. O centro da distribuição (cB) também é escolhido aleatoriamente. Isso fornece o ponto de partida para as subsequentes iterações do CMA-ES, nas quais o algoritmo adaptará a estratégia de busca para encontrar a solução ótima.

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

Método "GetChiN" calcula o valor esperado da norma da distribuição normal padrão N (0,I) para um determinado número de coordenadas (coords). O resultado é aproximado com base na dimensionalidade do espaço de busca.

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

Método "SortPopulation" destina-se a ordenar a população de soluções de acordo com os valores da função objetivo (fitness) a fim de determinar a melhor solução. Inicialmente, o método copia os valores de fitness de cada indivíduo da população para o array auxiliar "arfitness". Também é criado o array "arindex", que inicialmente contém os índices dos indivíduos. Isso é necessário para que, após a ordenação pelo fitness, seja possível restaurar a correspondência entre o fitness ordenado e o indivíduo original na população.

É utilizado o algoritmo de ordenação por inserção (insertion sort). Ele percorre o array "arfitness" e, para cada elemento, insere-o na posição correta da parte já ordenada do array, deslocando os elementos maiores para a direita. É importante observar que a ordenação é realizada por ordem decrescente (a condição arfitness [j] < tempFitness no laço while), pois o objetivo é maximize a função objetivo (fitness). Juntamente com "arfitness", o array "arindex" é ordenado de forma sincronizada para acompanhar a posição dos índices originais dos indivíduos.

Após a ordenação, o método verifica se o fitness do melhor indivíduo é superior ao melhor valor atual e, em caso afirmativo, "fB" é atualizado e a melhor solução é substituída pelas coordenadas do melhor indivíduo da população. Dessa forma, o método não apenas ordena a população por fitness, mas também acompanha a melhor solução encontrada até o momento.

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

Método "UpdateMean" atualiza o valor médio da população (cB) por meio da recombinação ponderada dos melhores indivíduos. Para cada coordenada, a nova média é calculada como a soma ponderada dos valores das coordenadas dos melhores indivíduos, onde os pesos são definidos no array "weights".

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

O método "ComputeWeights" calcula os pesos para a recombinação ponderada, aloca o array "weights" com tamanho "mu", calcula log (mu + 0.5) para utilizá-lo na fórmula dos pesos. Para cada "i" de "0" até "mu-1" atribui o peso como a diferença entre log (mu + 0.5) e log (i+1); somando esses pesos, o método divide todos os pesos pela soma para que o total seja igual a 1, e calcula a soma dos quadrados dos pesos, determina "mu_eff", a eficiência do uso dos pesos, o que é importante para o ajuste da taxa e da dispersão no algoritmo.

Esse método garante pesos ótimos para a recombinação, levando em consideração a contribuição das melhores soluções.

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

O método "UpdateDistribution" atualiza os parâmetros da distribuição, especificamente a matriz de covariância (covMatrix) e o tamanho do passo (sigma) no algoritmo CMA-ES. Se tiverem passado gerações suficientes (counteval − eigeneval > eigenInterval), então é calculada a decomposição em autovalores, o valor médio atual "cB" é salvo no array temporário "oldMean". O método chama "UpdateMean" para calcular o novo valor médio, calcula a diferença entre a nova e a antiga média dividida por "sigma", e também realiza a multiplicação de "y_w" por C^(−1/2), dividida em várias etapas utilizando a decomposição "B" e "D", armazenando o resultado em "invsqrtC_times_yw".

Em seguida, o método atualiza o caminho evolutivo para o tamanho do passo "ps" utilizando "invsqrtC_times_yw", determina se o progresso deve ser considerado "estagnado" com base na norma de "ps" e no comprimento esperado, depois atualiza o caminho evolutivo para a matriz de covariância "pc" utilizando "y_w" e o indicador de progresso "hsig". Inicialmente o método calcula a matriz de atualização "rank-1", "c1a", depois a matriz de atualização "rank-μ", "cmu", com base na diferença entre os indivíduos e a média antiga, dividida por "sigma", utilizando os pesos "weights"; realiza a atualização da matriz de covariância "covMatrix" com o uso das atualizações "rank-1" e "rank-μ" e ajusta "c1" em caso de estagnação do progresso. Periodicamente é chamado "EnforcePositiveDefinite" para garantir a definição positiva da matriz de covariância. O tamanho do passo "sigma" é atualizado com base na norma de "ps" e limitado dentro de "min_sigma" e "max_sigma" para garantir estabilidade numérica.

De modo geral, "UpdateDistribution" ajusta os parâmetros da distribuição (matriz de covariância e tamanho do passo) com base no histórico da busca (caminhos evolutivos) e nos dados da população, para adaptar-se ao relevo da função objetivo.

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

O método "ComputeEigendecomposition" realiza a decomposição da matriz de covariância simétrica "covMatrix" em autovalores e autovetores por meio do método de Jacobi aprimorado; o método cria uma cópia de "covMatrix" para não modificar os dados originais. Inicialmente "B" é definida como matriz identidade, representando a base inicial. Executa um número máximo de iterações ou até que os elementos fora da diagonal se tornem menores que "tolerance". 

Busca do maior elemento fora da diagonal: encontra o elemento com maior valor absoluto fora da diagonal para selecionar a rotação. Em seguida, se o valor máximo for menor que "tolerance", o laço é interrompido. O método calcula "phi" para realizar a rotação de Jacobi, modifica os elementos de "C_copy" aplicando a rotação, atualiza as colunas da matriz "B" (autovetores) de acordo com a rotação. Após as iterações, o método extrai a raiz quadrada dos elementos da diagonal (garantindo positividade mínima de 1e-14) e ordena em ordem decrescente os autovalores e os autovetores correspondentes para uso posterior. Assim, o método permite calcular com precisão os autovalores e autovetores da matriz de covariância.

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

Método "CheckPositiveDefinite" verifica se a matriz de covariância é definida positiva. Ele realiza uma verificação rápida da positividade dos elementos da diagonal e compara o menor autovalor (do array "D", ordenado em ordem decrescente) com um pequeno número positivo 1e-14. Se ambas as verificações forem satisfeitas, o método retorna "true".

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

Método "EnforcePositiveDefinite" garante a definição positiva da matriz de covariância "covMatrix". Ele executa várias etapas: encontra o menor elemento na diagonal e adiciona a quantidade necessária "correction" aos elementos da diagonal para que o menor seja igual a 1e-10, maximiza a simetria da matriz calculando a média de cada elemento fora da diagonal com o seu valor transposto.

Se a matriz ainda não for definida positiva, é realizada a decomposição em autovalores por meio de "ComputeEigendecomposition", todos os autovalores menores que √1e-10 são substituídos por √1e-10 para garantir positividade. Em seguida, o método recalcula "covMatrix" utilizando os autovetores "B" e os autovalores corrigidos "D", a fim de obter uma matriz ajustada e definida positiva. Essa abordagem garante que a matriz de covariância se torne definida positiva e adequada para os cálculos subsequentes no algoritmo CMA-ES.

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


Resultados dos testes

Agora, finalmente, podemos observar os resultados. O algoritmo, o que pode ser destacado de imediato, lida de forma excelente e rápida com tarefas de dimensionalidade média, sob determinadas configurações também com tarefas de baixa dimensionalidade, porém as tarefas mais difíceis e de alta dimensionalidade exigem um tempo de execução desproporcional aos recursos, por isso foram efetivamente excluídas dos resultados. A que isso se deve? Tradicionalmente, os cálculos matriciais em MQL5 representam um sério desafio computacional. Com a implementação manual das operações matriciais o algoritmo demonstra desempenho insatisfatório em tarefas de alta dimensionalidade, o que limita significativamente sua aplicabilidade prática. Contudo, com o surgimento das classes integradas para trabalho com matrizes, a situação muda radicalmente. Agora é criticamente importante utilizar a implementação nativa das operações matriciais da plataforma para todas as tarefas computacionalmente intensivas.

CMAES|Covariance Matrix Adaptation Evolution Strategy|50.0|25.0|0.01|0.8|1.0|
=============================
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%)

Na visualização podemos notar que o algoritmo trabalha tanto com "grandes saltos" quanto se concentra nos extremos da função, explorando-os cuidadosamente.

Hilly

CMA-ES na função de teste Hilly

Forest

CMA-ES na função de teste Forest

Megacity

CMA-ES na função de teste Megacity

Ao final da execução, o algoritmo CMA-ES ocupa a 38ª posição no ranking dos algoritmos populacionais de otimização mais fortes.

#
AO
Description
Hilly
Hilly
Final
Forest
Forest
Final
Megacity (discrete)
Megacity
Final
Final
Result
% of
MAX
10 p (5 F) 50 p (25 F) 1000 p (500 F) 10 p (5 F) 50 p (25 F) 1000 p (500 F) 10 p (5 F) 50 p (25 F) 1000 p (500 F)
1 ANS across neighbourhood search 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 algorithm (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 animal migration ptimization 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) evolution strategies 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 comet tail algorithm (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 time evolution travel algorithm (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 stochastic diffusion search 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 billiards optimization algorithm 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 archery algorithm 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 evolution of social groups (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 simulated isotropic annealing (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 biogeography based optimization 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 artificial cooperative search 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 dialectical algorithm 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 black hole algorithm 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 anarchy society optimization 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 royal flush optimization (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 atomic orbital search 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 turtle shell evolution algorithm (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 differential 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 successful restaurateur algorithm (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 chemical reaction optimization 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 blood inheritance optimization (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 bird swarm algorithm 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 harmony search 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 saplings sowing and growing 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 bacterial chemotaxis optimization 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 african buffalo optimization 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) evolution strategies 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 Fractal-Based Algorithm 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 search 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 brain storm optimization 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 wale optimization algorithm 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 artificial electric field algorithm 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 artificial ecosystem-based optimization algorithm 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 camel algorithm 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 ant colony optimization 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 covariance_matrix_adaptation_evolution_strategy 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 bacterial foraging optimization - 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 simple optimization algorithm 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 artificial bee hive algorithm 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 atmospheric cloud model optimization 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 moment estimation 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 chaos game optimization 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 coral reefs optimization 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


Considerações finais

O CMA-ES é capaz de adaptar a estratégia de busca à geometria local da função objetivo, o que o torna praticamente indispensável para tarefas complexas, mal condicionadas e com estrutura desconhecida. As caudas pesadas da distribuição de potência permitem que o algoritmo realize "saltos longos", o que é criticamente importante para escapar de ótimos locais. A complexidade computacional O(n²) em memória e O(n³) em tempo limita rigidamente a aplicabilidade do algoritmo. Para tarefas de alta dimensionalidade (n > 100), o consumo de recursos torna-se desproporcional às vantagens obtidas. O tempo de execução em funções multidimensionais cresce tão rapidamente que torna o algoritmo praticamente inaplicável para grandes valores de n.

O CMA-ES funciona em funções com ruído, lida com funções objetivo descontínuas, é eficaz em paisagens com múltiplos extremos e o segredo dessa universalidade reside na filosofia fundamental do algoritmo: em vez de fazer suposições sobre a estrutura da função, ele a estuda cuidadosamente, adaptando sua estratégia de busca com base na experiência acumulada. O CMA-ES transforma o panorama da computação evolutiva, demonstrando que algoritmos inspirados biologicamente podem ter fundamentos matemáticos rigorosos e que a adaptação do algoritmo não é apenas ajuste de parâmetros, mas um princípio fundamental de aprendizado da estrutura do problema.

O algoritmo é excelente para tarefas de dimensionalidade média, é uma ferramenta especializada que se apresenta como uma alternativa digna em seu nicho. O algoritmo combina elegância matemática com eficiência prática, mas exige aplicação consciente levando em conta suas limitações computacionais. Em casos como esse, para o cálculo de matrizes, os desenvolvedores da linguagem MQL5 recentemente introduziram operações matriciais rápidas no MQL5, nesta implementação do algoritmo foram utilizados cálculos sequenciais convencionais.

tab

Figura 2. Graduação de cores dos algoritmos nos testes correspondentes

chart

Figura 3. Histograma dos resultados dos testes dos algoritmos (na escala de 0 a 100, quanto maior, melhor, onde 100 é o resultado teórico máximo possível; no arquivo há um script para calcular a tabela de classificação)

Vantagens e desvantagens do algoritmo CMAES:

Vantagens:

  1. Boa convergência em funções de dimensionalidade média.

Desvantagens:

  1. Grande variação de resultados em funções de baixa dimensionalidade.
  2. Elevado consumo de recursos em funções de alta dimensionalidade.

Um arquivo compactado com as versões atualizadas dos códigos dos algoritmos está anexado ao artigo. O autor do artigo não se responsabiliza pela precisão absoluta na descrição dos algoritmos canônicos, pois muitos deles foram modificados para melhorar as capacidades de busca. As conclusões e julgamentos apresentados nos artigos baseiam-se nos resultados dos experimentos realizados.


Programas utilizados no artigo

# Nome Tipo Descrição
1 #C_AO.mqh
Arquivo de inclusão
Classe base dos algoritmos populacionais de otimização
2 #C_AO_enum.mqh
Arquivo de inclusão
Enumeração dos algoritmos populacionais de otimização
3 TestFunctions.mqh
Arquivo de inclusão
Biblioteca de funções de teste
4
TestStandFunctions.mqh
Arquivo de inclusão
Biblioteca de funções do ambiente de testes
5
Utilities.mqh
Arquivo de inclusão
Biblioteca de funções auxiliares
6
CalculationTestResults.mqh
Arquivo de inclusão
Script para cálculo dos resultados na tabela comparativa
7
Testing AOs.mq5
Script Ambiente de testes unificado para todos os algoritmos populacionais de otimização
8
Simple use of population optimization algorithms.mq5
Script
Exemplo simples de uso de algoritmos populacionais de otimização sem visualização
9
Test_AO_CMAES.mq5
Script Ambiente de testes para CMAES

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/18227

Arquivos anexados |
CMAES.zip (230.9 KB)
Construindo um Indicador Keltner Channel com Gráficos Canvas Personalizados em MQL5 Construindo um Indicador Keltner Channel com Gráficos Canvas Personalizados em MQL5
Neste artigo, construímos um indicador Keltner Channel com gráficos canvas personalizados em MQL5. Detalhamos a integração de médias móveis, cálculos de ATR e visualização aprimorada do gráfico. Também abordamos o backtesting para avaliar o desempenho do indicador e obter insights práticos de trading.
Redes neurais em trading: Pipeline inteligente de previsões (Mistura esparsa de especialistas) Redes neurais em trading: Pipeline inteligente de previsões (Mistura esparsa de especialistas)
Propomos conhecer a implementação prática do bloco de mistura esparsa de especialistas para séries temporais no ambiente computacional OpenCL. No artigo, é analisado passo a passo o funcionamento da convolução multi-janela mascarada, bem como a organização do aprendizado por gradiente em condições de múltiplos fluxos de informação.
Criando um Painel Administrador de Trading em MQL5 (Parte IX): Organização de Código (II): Modularização Criando um Painel Administrador de Trading em MQL5 (Parte IX): Organização de Código (II): Modularização
Nesta discussão, damos um passo adiante ao dividir nosso programa MQL5 em módulos menores e mais gerenciáveis. Esses componentes modulares serão então integrados ao programa principal, melhorando sua organização e capacidade de manutenção. Essa abordagem simplifica a estrutura do programa principal e torna os componentes individuais reutilizáveis em outros Expert Advisors (EAs) e no desenvolvimento de indicadores. Ao adotar esse design modular, criamos uma base sólida para melhorias futuras, beneficiando tanto nosso projeto quanto a comunidade mais ampla de desenvolvedores.
Introdução à diversificação (en. diversification) de estruturas fractais de mercado com o auxílio de machine learning Introdução à diversificação (en. diversification) de estruturas fractais de mercado com o auxílio de machine learning
No presente artigo é feita uma tentativa de examinar séries temporais financeiras sob a perspectiva de estruturas fractais autossimilares. Como temos muitas analogias que confirmam a possibilidade de considerar as cotações de mercado como fractais autossimilares, podemos formar uma compreensão sobre os horizontes de previsão dessas estruturas.