Classe base de algoritmos populacionais como alicerce para otimização eficiente
Conteúdo:
1. Introdução. Perspectivas e possibilidades ao utilizar uma classe base de algoritmos populacionais
2. Implementando a classe base para algoritmos populacionais
3. Código dos algoritmos derivados
4. Código para um banco de testes universal aplicável a todos os algoritmos
5. Incorporando funções de teste amplamente conhecidas
6. Construção de funções de teste 3D
7. Considerações finais
1. Introdução. Perspectivas e possibilidades ao utilizar uma classe base de algoritmos populacionais
No mundo da computação moderna e da inteligência artificial, uma classe base projetada para integrar algoritmos de otimização em soluções de software finitas é um elemento fundamental que abre horizontes infinitos de possibilidades técnicas para os desenvolvedores. Herdar dessa classe base no contexto dos algoritmos populacionais não só facilita e melhora desenvolvimento de novos métodos de otimização, mas também expande as perspectivas para a criação de algoritmos híbridos, capazes de se adaptar às mais diversas tarefas.
A união dos algoritmos de otimização dentro da classe base abre portas para desenvolver soluções inovadoras que combinam as melhores características de diferentes métodos. Os algoritmos híbridos, resultantes dessa abordagem, são capazes de superar eficazmente as limitações de métodos individuais e alcançar novos patamares na resolução de tarefas complexas de otimização.
Além disso, a classe base para algoritmos populacionais garante que os algoritmos desenvolvidos sejam simples de utilizar e testar em conjuntos de funções de teste padrão. Isso permite que pesquisadores e desenvolvedores avaliem rapidamente a eficácia dos novos métodos de otimização, comparando seu desempenho com soluções já estabelecidas.
Vamos pensar no mundo da otimização e busca de soluções como um incrível mundo culinário, onde cada método de otimização é como um ingrediente especial que dá seu toque único ao prato. A hibridização, nesse contexto, é como misturar habilmente diferentes ingredientes para criar novos pratos, mais saborosos e interessantes.
Existem muitos métodos de otimização disponíveis, como algoritmos genéticos, estratégias evolucionárias, algoritmos de formigas, otimização por enxame de partículas e muitos outros. Cada um desses métodos tem suas próprias vantagens e limitações.
É aí que a hibridização se torna importante! Você pode escolher o melhor de cada método e combiná-los de maneira única, como um chef experiente. Assim, os métodos de otimização híbridos podem aproveitar as vantagens de diferentes abordagens, compensando suas fraquezas e resultando em ferramentas mais poderosas e eficazes para encontrar soluções ótimas.
Pense na combinação de um algoritmo genético com uma busca local como a mistura ideal de pimenta picante e mel doce em um prato, dando-lhe um sabor profundo e rico. Assim como na culinária, combinar diferentes algoritmos populacionais permite criar métodos inovadores que encontram soluções ótimas de maneira rápida e precisa em diversas áreas, como engenharia, análise financeira ou inteligência artificial.
Assim, a hibridização na otimização não se resume apenas a misturar métodos; é a habilidade de criar novas abordagens que potencializam cada método e alcançam resultados notáveis. Em última análise, graças à hibridização, podemos desenvolver métodos de otimização mais eficazes, inovadores e poderosos, capazes de resolver problemas complexos e impulsionar descobertas em diversas áreas.
Além disso, uma classe base unificada permite aos usuários integrar elementos individuais de cada algoritmo em projetos, facilitando o design de novos métodos de otimização únicos e robustos.
2. Implementando a classe base para algoritmos populacionais
No contexto do artigo sobre a herança da classe base para algoritmos populacionais e a criação de métodos de otimização híbridos, podemos considerar alguns exemplos interessantes de combinações:
- Algoritmo genético com busca local reforçada. Nessa combinação, o algoritmo genético busca a solução ótima em escala global, enquanto a busca local é usada para aprimorar a solução dentro de um entorno próximo. Isso permite combinar as vantagens da busca global e local, aumentando a precisão e a velocidade de convergência do algoritmo.
- Estratégia evolucionária com algoritmo de formigas. Aqui, a estratégia evolucionária pode ser usada para alterar os parâmetros do modelo, enquanto o algoritmo de formigas busca o caminho ótimo no espaço de parâmetros. Essa combinação pode ser eficaz para otimizar tarefas complexas, onde é necessário encontrar a combinação ideal de parâmetros.
- Otimização por enxame de partículas com programação genética. Nessa combinação, o enxame de partículas pode ser usado para explorar o espaço de soluções, enquanto a programação genética evolui as estruturas de programas que resolvem a tarefa de otimização. Isso permite explorar de forma eficaz tanto o espaço de parâmetros quanto as estruturas de soluções.
- Busca por recozimento simulado com algoritmo genético. Aqui, o recozimento simulado pode ser usado para explorar o espaço de soluções levando em consideração o regime de temperatura, enquanto o algoritmo genético busca soluções ótimas no espaço definido. Essa combinação pode garantir uma exploração mais profunda do espaço de soluções e melhorar a convergência do algoritmo.
Podemos também considerar as seguintes direções como exemplo de uso das possibilidades unificadas em uma única classe base de algoritmos populacionais:
- Método de abordagem metaheurística combinada. Nesse método, podemos combinar vários algoritmos metaheurísticos diferentes, como algoritmos genéticos, algoritmos de formigas, algoritmos de otimização por enxame de partículas, recozimento simulado, etc. Esses algoritmos podem operar em paralelo ou sequencialmente, trocando informações e combinando seus pontos fortes para uma busca mais eficaz da solução ótima.
- Método híbrido com gestão adaptativa de estratégias de busca. Nessa abordagem, é possível utilizar mecanismos de gestão adaptativa para combinar dinamicamente diferentes estratégias de otimização dependendo das características do problema. Por exemplo, pode-se ajustar os pesos ou parâmetros de cada método com base na eficácia do seu desempenho na fase atual da otimização.
- Método híbrido com redes neurais artificiais. Nessa abordagem, redes neurais artificiais podem ser usadas para ajustar dinamicamente os parâmetros e estratégias de otimização dos algoritmos populacionais. As redes neurais podem aprender em tempo real, adaptando-se às mudanças no espaço de busca e recomendando os parâmetros ideais para cada método de otimização.
- Método de otimização conjunta e aprendizado por reforço. Nessa abordagem, é possível combinar algoritmos populacionais com métodos de aprendizado por reforço para criar um sistema híbrido capaz de explorar e otimizar eficazmente espaços de soluções complexas. Agentes de aprendizado por reforço podem aprender com os resultados dos algoritmos populacionais e vice-versa, criando uma interação entre diferentes métodos de otimização.
A variação na quantidade de parâmetros externos em cada algoritmo de otimização pode criar problemas na herança e aplicação unificada. Para resolver isso, optou-se por definir os parâmetros externos dos algoritmos como padrão no construtor, com base nos resultados de testes extensivos. Ainda assim, há a possibilidade de alterar esses parâmetros antes da inicialização dos algoritmos. Assim, o objeto de cada algoritmo representará uma solução final pronta para uso. Anteriormente, os algoritmos e seus parâmetros eram tratados como entidades separadas.
Vamos começar com a configuração dos parâmetros dos algoritmos. Cada parâmetro é descrito de forma conveniente por uma estrutura "S_AlgoParam", que contém o nome e o valor do parâmetro. Assim, um array contendo objetos dessa estrutura representa o conjunto de parâmetros externos dos algoritmos.
struct S_AlgoParam { double val; string name; };
Em cada algoritmo de otimização, há um agente de busca – a unidade elementar e participante indispensável da estratégia de busca, seja ele uma delicada criatura – um vaga-lume no algoritmo de vaga-lumes, ou uma abelha trabalhadora no algoritmo de abelhas, uma formiga laboriosa no algoritmo de formigas, etc. Eles são artistas únicos no labirinto da otimização, revelando a magnificência da arte de buscar e descobrir caminhos ótimos para o sucesso. Seus esforços e aspirações, como magia, transformam o caos dos dados em harmonia de soluções, iluminando o caminho para novos horizontes de otimização ideal.
Hmmm, sobre o que eu estava falando... ah sim, sobre os agentes de otimização. :)
Assim, cada agente representa uma solução específica para o problema de otimização e possui duas propriedades obrigatórias mínimas: coordenadas no espaço de busca (parâmetros a serem otimizados) e qualidade da solução (função de adaptabilidade ou fitness). Para possibilitar a expansão da funcionalidade e das capacidades e a implementação das propriedades específicas dos algoritmos, organizaremos o agente na forma da classe "C_AO_Agent", que poderá ser herdada posteriormente.
class C_AO_Agent { public: ~C_AO_Agent () { } double c []; //coordinates double f; //fitness };
A maioria das operações lógicas nos algoritmos de otimização se repete e pode ser estruturada separadamente na forma de um conjunto de funções da classe "C_AO_Utilities", cujo objeto pode ser usado tanto nas classes dos algoritmos quanto nos agentes.
A classe "C_AO_Utilities" contém os seguintes métodos:
- Scale: Método sobrecarregado que executa a escala do valor de entrada "In" do intervalo [InMIN, InMAX] para o intervalo [OutMIN, OutMAX]. Também é possível realizar a escala inversa, se o parâmetro "revers" estiver configurado como "true".
- RNDfromCI: Gera um número real aleatório dentro do intervalo especificado [min, max].
- RNDintInRange: Gera um número inteiro aleatório dentro do intervalo especificado [min, max].
- RNDbool: Gera um valor lógico aleatório (true/false).
- RNDprobab: Gera uma probabilidade aleatória (número real de 0 a 1).
- SeInDiSp: Calcula o valor considerando um passo especificado "Step" dentro do intervalo [InMin, InMax].
- Métodos "DecimalToGray", "IntegerToBinary", "GrayToDecimal", "BinaryToInteger", "GetMaxDecimalFromGray": Executam conversões entre números decimais, números binários e códigos Gray.
- Métodos "GaussDistribution" e "PowerDistribution": Realizam cálculos para a distribuição normal e distribuição de potência, respectivamente.
- Método "Sorting" (método template): Classifica o array "p" do tipo "T" em ordem decrescente.
- Estrutura "S_Roulette": Contém os campos "start" e "end" para representar o intervalo.
- Métodos "PreCalcRoulette" e "SpinRoulette": "PreCalcRoulette" calcula os intervalos para objetos do tipo "T" e os salva no array "roulette". "SpinRoulette" executa a operação de "girar" a roleta com base no tamanho da população "aPopSize".
//—————————————————————————————————————————————————————————————————————————————— class C_AO_Utilities { public: //-------------------------------------------------------------------- double Scale (double In, double InMIN, double InMAX, double OutMIN, double OutMAX); double Scale (double In, double InMIN, double InMAX, double OutMIN, double OutMAX, bool revers); double RNDfromCI (double min, double max); int RNDintInRange (int min, int max); bool RNDbool (); double RNDprobab (); double SeInDiSp (double In, double InMin, double InMax, double Step); void DecimalToGray (ulong decimalNumber, char &array []); void IntegerToBinary (ulong number, char &array []); ulong GrayToDecimal (const char &grayCode [], int startInd, int endInd); ulong BinaryToInteger (const char &binaryStr [], const int startInd, const int endInd); ulong GetMaxDecimalFromGray (int digitsInGrayCode); double GaussDistribution (const double In, const double outMin, const double outMax, const double sigma); double PowerDistribution (const double In, const double outMin, const double outMax, const double p); //---------------------------------------------------------------------------- template<typename T> void Sorting (T &p [], T &pTemp [], int size) { int cnt = 1; int t0 = 0; double t1 = 0.0; int ind []; double val []; ArrayResize (ind, size); ArrayResize (val, size); for (int i = 0; i < size; i++) { ind [i] = i; val [i] = p [i].f; } while (cnt > 0) { cnt = 0; for (int i = 0; i < size - 1; i++) { if (val [i] < val [i + 1]) { t0 = ind [i + 1]; t1 = val [i + 1]; ind [i + 1] = ind [i]; val [i + 1] = val [i]; ind [i] = t0; val [i] = t1; cnt++; } } } for (int u = 0; u < size; u++) pTemp [u] = p [ind [u]]; for (int u = 0; u < size; u++) p [u] = pTemp [u]; } //---------------------------------------------------------------------------- struct S_Roulette { double start; double end; }; S_Roulette roulette []; template<typename T> void PreCalcRoulette (T &agents []) { int aPopSize = ArraySize (agents); roulette [0].start = agents [0].f; roulette [0].end = roulette [0].start + (agents [0].f - agents [aPopSize - 1].f); for (int s = 1; s < aPopSize; s++) { if (s != aPopSize - 1) { roulette [s].start = roulette [s - 1].end; roulette [s].end = roulette [s].start + (agents [s].f - agents [aPopSize - 1].f); } else { roulette [s].start = roulette [s - 1].end; roulette [s].end = roulette [s].start + (agents [s - 1].f - agents [s].f) * 0.1; } } } int SpinRoulette (int aPopSize); }; //——————————————————————————————————————————————————————————————————————————————
Como os algoritmos estocásticos de otimização são baseados na geração de números aleatórios, essa operação pode ser executada centenas ou até milhares de vezes para obter cada solução. Portanto, é sensato otimizar o processo de geração de números aleatórios dividindo as tarefas específicas. Considerando que o gerador padrão gera números inteiros, existe um potencial para acelerar esse processo.
Já nos deparamos anteriormente com o método "RNDfromCI", que gera um número real aleatório dentro do intervalo ["min", "max"]:
double C_AO_Utilities ::RNDfromCI (double min, double max) { if (min == max) return min; if (min > max) { double temp = min; min = max; max = temp; } return min + ((max - min) * rand () / 32767.0); }
Muitas vezes há a necessidade de gerar um número inteiro aleatório, por exemplo, para a seleção aleatória de um agente na população. Para isso, o método "RNDintInRange" será útil.
int C_AO_Utilities :: RNDintInRange (int min, int max) { if (min == max) return min; if (min > max) { int temp = min; min = max; max = temp; } return min + rand () % (max - min + 1); }
Obter um valor booleano aleatório com o método "RNDbool" pode ser feito muito rapidamente, em comparação com os dois métodos anteriores, por isso fazia sentido separar os números aleatórios em métodos distintos, dependendo da tarefa.
bool C_AO_Utilities :: RNDbool () { return rand () % 2 == 0; }
E ainda temos o método "RNDprobab", que permite obter um número real aleatório no intervalo [0.0, 1.0]. Ele é ideal para executar a probabilidade de realização de certas operações, como a probabilidade de crossover no algoritmo genético. Tais operações também são realizadas com bastante frequência.
double C_AO_Utilities :: RNDprobab () { return (double)rand () / 32767; }Agora, consideremos a classe base "C_AO" dos algoritmos populacionais de otimização. Esta classe descreve os atributos obrigatórios de todos os algoritmos populacionais, tais como:
- Métodos e propriedades da classe "C_AO":
- SetParams: método virtual para configurar os parâmetros do algoritmo.
- Init: método virtual para inicializar o algoritmo, passando o intervalo mínimo e máximo de busca, o passo e o número de épocas.
- Moving: método virtual para executar um passo do algoritmo.
- Revision: método virtual para realizar a revisão do algoritmo.
- GetName: método para obter o nome do algoritmo.
- GetDesc: método para obter a descrição do algoritmo.
- GetParams: método para obter os parâmetros do algoritmo em formato de string.
- - Propriedades protegidas da classe "C_AO":
- ao_name: nome do algoritmo.
- ao_desc: descrição do algoritmo.
- rangeMin, rangeMax, rangeStep: arrays para armazenar o intervalo mínimo e máximo de busca, bem como o passo.
- coords: número de coordenadas.
- popSize: tamanho da população.
- revision: flag para revisão.
- u: objeto da classe "C_AO_Utilities" para executar funções auxiliares.
#include "#C_AO_Utilities.mqh" //—————————————————————————————————————————————————————————————————————————————— class C_AO { public: //-------------------------------------------------------------------- C_AO () { } ~C_AO () { for (int i = 0; i < ArraySize (a); i++) delete a [i];} double cB []; //best coordinates double fB; //FF of the best coordinates C_AO_Agent *a []; //agents S_AlgoParam params []; //algorithm parameters virtual void SetParams () { } virtual bool Init (const double &rangeMinP [], //minimum search range const double &rangeMaxP [], //maximum search range const double &rangeStepP [], //step search const int epochsP = 0) //number of epochs { return false;} virtual void Moving () { } virtual void Revision () { } string GetName () { return ao_name;} string GetDesc () { return ao_desc;} string GetParams () { string str = ""; for (int i = 0; i < ArraySize (params); i++) { str += (string)params [i].val + "|"; } return str; } protected: //----------------------------------------------------------------- string ao_name; //ao name; string ao_desc; //ao description double rangeMin []; //minimum search range double rangeMax []; //maximum search range double rangeStep []; //step search int coords; //coordinates number int popSize; //population size bool revision; C_AO_Utilities u; //auxiliary functions bool StandardInit (const double &rangeMinP [], //minimum search range const double &rangeMaxP [], //maximum search range const double &rangeStepP []) //step search { MathSrand ((int)GetMicrosecondCount ()); //reset of the generator fB = -DBL_MAX; revision = false; coords = ArraySize (rangeMinP); if (coords == 0 || coords != ArraySize (rangeMaxP) || coords != ArraySize (rangeStepP)) return false; ArrayResize (rangeMin, coords); ArrayResize (rangeMax, coords); ArrayResize (rangeStep, coords); ArrayResize (cB, coords); ArrayCopy (rangeMin, rangeMinP, 0, 0, WHOLE_ARRAY); ArrayCopy (rangeMax, rangeMaxP, 0, 0, WHOLE_ARRAY); ArrayCopy (rangeStep, rangeStepP, 0, 0, WHOLE_ARRAY); return true; } }; //——————————————————————————————————————————————————————————————————————————————
Também neste mesmo arquivo, junto com a classe base, está a enumeração "E_AO", contendo identificadores de algoritmos de otimização e a função "SelectAO", que permite criar uma instância do algoritmo correspondente e obter seu ponteiro.
#include "AO_BGA_Binary_Genetic_Algorithm.mqh" #include "AO_(P_O)ES_Evolution_Strategies.mqh" #include "AO_DE_Differential_Evolution.mqh" #include "AO_SDSm_Stochastic_Diffusion_Search.mqh" #include "AO_ESG_Evolution_of_Social_Groups.mqh"; //—————————————————————————————————————————————————————————————————————————————— enum E_AO { AO_BGA, AO_P_O_ES, AO_SDSm, AO_ESG, AO_DE, AO_NONE }; C_AO *SelectAO (E_AO a) { C_AO *ao; switch (a) { case AO_BGA: ao = new C_AO_BGA (); return (GetPointer (ao)); case AO_P_O_ES: ao = new C_AO_P_O_ES (); return (GetPointer (ao)); case AO_SDSm: ao = new C_AO_SDSm (); return (GetPointer (ao)); case AO_ESG: ao = new C_AO_ESG (); return (GetPointer (ao)); case AO_DE: ao = new C_AO_DE (); return (GetPointer (ao)); default: ao = NULL; return NULL; } } //——————————————————————————————————————————————————————————————————————————————
3. Código dos algoritmos derivados
Como exemplo de herança da classe base, consideremos o algoritmo de busca difusiva estocástica, SDSm. O agente "C_SDS_Agent" deste algoritmo será herdado do agente base "C_AO_Agent". Note que no método de inicialização do agente estão presentes as coordenadas "c" e a adaptabilidade "f", porém na classe "C_SDS_Agent" elas não são declaradas. Isso é perfeitamente lógico, pois esses atributos são obrigatórios para todos os agentes dos algoritmos de otimização e são herdados do algoritmo base, não sendo necessário declará-los novamente.
//—————————————————————————————————————————————————————————————————————————————— class C_SDS_Agent : public C_AO_Agent { public: //-------------------------------------------------------------------- ~C_SDS_Agent () { } int raddr []; //restaurant address int raddrPrev []; //previous restaurant address double cPrev []; //previous coordinates (dishes) double fPrev; //previous fitness void Init (int coords) { ArrayResize (c, coords); ArrayResize (cPrev, coords); ArrayResize (raddr, coords); ArrayResize (raddrPrev, coords); f = -DBL_MAX; fPrev = -DBL_MAX; } }; //——————————————————————————————————————————————————————————————————————————————
A classe "C_AO_SDSm" do algoritmo SDSm é herdada da classe "C_AO". Ao declarar um objeto da classe no construtor, faremos a inicialização dos parâmetros externos do algoritmo, que posteriormente podem ser alterados pelo usuário, se desejado, e os parâmetros estarão disponíveis em formato de array, garantindo a compatibilidade com o banco de testes.
//—————————————————————————————————————————————————————————————————————————————— class C_AO_SDSm : public C_AO { public: //-------------------------------------------------------------------- ~C_AO_SDSm () { } C_AO_SDSm () { ao_name = "SDSm"; ao_desc = "Stochastic Diffusion Search"; popSize = 100; //population size restNumb = 100; //restaurants number probabRest = 0.05; //probability restaurant choosing ArrayResize (params, 3); params [0].name = "popSize"; params [0].val = popSize; params [1].name = "restNumb"; params [1].val = restNumb; params [2].name = "probabRest"; params [2].val = probabRest; } void SetParams () { popSize = (int)params [0].val; restNumb = (int)params [1].val; probabRest = params [2].val; } bool Init (const double &rangeMinP [], //minimum search range const double &rangeMaxP [], //maximum search range const double &rangeStepP [], //step search const int epochsP = 0); //number of epochs void Moving (); void Revision (); //---------------------------------------------------------------------------- int restNumb; //restaurants number double probabRest; //probability restaurant choosing C_SDS_Agent *agent []; //candidates private: //------------------------------------------------------------------- struct S_Riverbed //русло реки { double coordOnSector []; //coordinate on the sector (количество ячеек: количество секторов на координате, значение ячеек: конкретная координата на секторе) }; double restSpace []; //restaurants space S_Riverbed rb []; //riverbed void Research (const double ko, const int raddr, const double restSpace, const double rangeMin, const double rangeStep, const double pitOld, double &pitNew); }; //——————————————————————————————————————————————————————————————————————————————Em seguida, deve-se considerar especialmente o método de inicialização "Init" da classe "C_AO_SDSm". Passo a passo, o método realiza o seguinte:
1. Primeiramente, é necessário chamar o método da classe base "StandardInit" e passar para ele "rangeMinP", "rangeMaxP", "rangeStepP". Se o método retornar "false", a função "Init" também retorna "false", indicando uma inicialização malsucedida do algoritmo.
2. Exclusão de agentes usando "delete". Isso é necessário ao utilizar o objeto do algoritmo várias vezes.
3. Em seguida, os arrays "agent" do algoritmo SDSm e "a" da classe base são redimensionados para "popSize" e os tipos são convertidos.
4. Depois, são realizadas ações semelhantes ao algoritmo descrito anteriormente no artigo sobre SDSm.
//—————————————————————————————————————————————————————————————————————————————— bool C_AO_SDSm::Init (const double &rangeMinP [], //minimum search range const double &rangeMaxP [], //maximum search range const double &rangeStepP [], //step search const int epochsP = 0) //number of epochs { if (!StandardInit (rangeMinP, rangeMaxP, rangeStepP)) return false; //---------------------------------------------------------------------------- for (int i = 0; i < ArraySize (agent); i++) delete agent [i]; ArrayResize (agent, popSize); ArrayResize (a, popSize); for (int i = 0; i < popSize; i++) { a [i] = new C_SDS_Agent (); agent [i] = (C_SDS_Agent *)a [i]; agent [i].Init (coords); } ArrayResize (restSpace, coords); ArrayResize (rb, coords); for (int i = 0; i < coords; i++) { ArrayResize (rb [i].coordOnSector, restNumb); ArrayInitialize (rb [i].coordOnSector, -DBL_MAX); } for (int i = 0; i < coords; i++) { restSpace [i] = (rangeMax [i] - rangeMin [i]) / restNumb; } return true; } //——————————————————————————————————————————————————————————————————————————————
4. Código para um banco de testes universal aplicável a todos os algoritmos
Embora no momento não haja necessidade de "multiplicar" bancos de testes, ainda assim transferiremos todas as funções do banco de testes para a classe "C_TestStand". Isso permitirá encapsular convenientemente a funcionalidade do banco de testes. Como não houve alterações significativas nas funções do banco de testes, não vamos descrevê-las detalhadamente. Basta apenas observar como agora se parece esta "infraestrutura de teste":
#include <Canvas\Canvas.mqh> #include <\Math\Functions.mqh> //—————————————————————————————————————————————————————————————————————————————— class C_TestStand { public: void Init (int width, int height) { W = width; //750; H = height; //375; WscrFunc = H - 2; HscrFunc = H - 2; //creating a table --------------------------------------------------------- string canvasName = "AO_Test_Func_Canvas"; if (!Canvas.CreateBitmapLabel (canvasName, 5, 30, W, H, COLOR_FORMAT_ARGB_RAW)) { Print ("Error creating Canvas: ", GetLastError ()); return; } ObjectSetInteger (0, canvasName, OBJPROP_HIDDEN, false); ObjectSetInteger (0, canvasName, OBJPROP_SELECTABLE, true); ArrayResize (FunctScrin, HscrFunc); for (int i = 0; i < HscrFunc; i++) ArrayResize (FunctScrin [i].clr, HscrFunc); } struct S_CLR { color clr []; }; //---------------------------------------------------------------------------- public: void CanvasErase () { Canvas.Erase (XRGB (0, 0, 0)); Canvas.FillRectangle (1, 1, H - 2, H - 2, COLOR2RGB (clrWhite)); Canvas.FillRectangle (H + 1, 1, W - 2, H - 2, COLOR2RGB (clrWhite)); } //---------------------------------------------------------------------------- public: void MaxMinDr (C_Function & f) { //draw Max global------------------------------------------------------------- int x = (int)Scale(f.GetMaxFuncX(), f.GetMinRangeX(), f.GetMaxRangeX(), 1, W/2 - 1, false); int y = (int)Scale(f.GetMaxFuncY(), f.GetMinRangeY(), f.GetMaxRangeY(), 1, H - 1, true); Canvas.Circle(x, y, 12, COLOR2RGB(clrBlack)); Canvas.Circle(x, y, 13, COLOR2RGB(clrBlack)); Canvas.Circle(x, y, 14, COLOR2RGB(clrBlack)); Canvas.Circle(x, y, 15, COLOR2RGB(clrBlack)); //draw Min global------------------------------------------------------------- x = (int)Scale(f.GetMinFuncX(), f.GetMinRangeX(), f.GetMaxRangeX(), 0, W/2 - 1, false); y = (int)Scale(f.GetMinFuncY(), f.GetMinRangeY(), f.GetMaxRangeY(), 0, H - 1, true); Canvas.Circle(x, y, 12, COLOR2RGB(clrBlack)); Canvas.Circle(x, y, 13, COLOR2RGB(clrBlack)); } //---------------------------------------------------------------------------- public: void PointDr (double &args [], C_Function & f, int shiftX, int shiftY, int count, bool main) { double x = 0.0; double y = 0.0; double xAve = 0.0; double yAve = 0.0; int width = 0; int height = 0; color clrF = clrNONE; for(int i = 0; i < count; i++) { xAve += args [i * 2]; yAve += args [i * 2 + 1]; x = args [i * 2]; y = args [i * 2 + 1]; width = (int)Scale(x, f.GetMinRangeX(), f.GetMaxRangeX(), 0, WscrFunc - 1, false); height = (int)Scale(y, f.GetMinRangeY(), f.GetMaxRangeY(), 0, HscrFunc - 1, true); clrF = DoubleToColor(i, 0, count - 1, 0, 270); Canvas.FillCircle(width + shiftX, height + shiftY, 1, COLOR2RGB(clrF)); } xAve /=(double)count; yAve /=(double)count; width = (int)Scale(xAve, f.GetMinRangeX(), f.GetMaxRangeX(), 0, WscrFunc - 1, false); height = (int)Scale(yAve, f.GetMinRangeY(), f.GetMaxRangeY(), 0, HscrFunc - 1, true); if(!main) { Canvas.FillCircle(width + shiftX, height + shiftY, 3, COLOR2RGB(clrBlack)); Canvas.FillCircle(width + shiftX, height + shiftY, 2, COLOR2RGB(clrWhite)); } else { Canvas.Circle (width + shiftX, height + shiftY, 5, COLOR2RGB (clrBlack)); Canvas.Circle (width + shiftX, height + shiftY, 6, COLOR2RGB (clrBlack)); } } //---------------------------------------------------------------------------- public: void SendGraphToCanvas () { for (int w = 0; w < HscrFunc; w++) { for (int h = 0; h < HscrFunc; h++) { Canvas.PixelSet (w + 1, h + 1, COLOR2RGB (FunctScrin [w].clr [h])); } } } //---------------------------------------------------------------------------- public: void DrawFunctionGraph (C_Function & f) { double ar [2]; double fV; for (int w = 0; w < HscrFunc; w++) { ar [0] = Scale (w, 0, H, f.GetMinRangeX (), f.GetMaxRangeX (), false); for (int h = 0; h < HscrFunc; h++) { ar [1] = Scale (h, 0, H, f.GetMinRangeY (), f.GetMaxRangeY (), true); fV = f.CalcFunc (ar, 1); FunctScrin [w].clr [h] = DoubleToColor (fV, f.GetMinFunValue (), f.GetMaxFunValue (), 0, 270); } } } //---------------------------------------------------------------------------- public: void Update () { Canvas.Update (); } //---------------------------------------------------------------------------- //Scaling a number from a range to a specified range public: double Scale (double In, double InMIN, double InMAX, double OutMIN, double OutMAX, bool Revers = false) { if (OutMIN == OutMAX) return (OutMIN); if (InMIN == InMAX) return ((OutMIN + OutMAX) / 2.0); else { if (Revers) { if (In < InMIN) return (OutMAX); if (In > InMAX) return (OutMIN); return (((InMAX - In) * (OutMAX - OutMIN) / (InMAX - InMIN)) + OutMIN); } else { if (In < InMIN) return (OutMIN); if (In > InMAX) return (OutMAX); return (((In - InMIN) * (OutMAX - OutMIN) / (InMAX - InMIN)) + OutMIN); } } } //---------------------------------------------------------------------------- private: color DoubleToColor (const double In, //input value const double inMin, //minimum of input values const double inMax, //maximum of input values const int loH, //lower bound of HSL range values const int upH) //upper bound of HSL range values { int h = (int) Scale (In, inMin, inMax, loH, upH, true); return HSLtoRGB (h, 1.0, 0.5); } //---------------------------------------------------------------------------- private: color HSLtoRGB (const int h, //0 ... 360 const double s, //0.0 ... 1.0 const double l) //0.0 ... 1.0 { int r; int g; int b; if (s == 0.0) { r = g = b = (unsigned char)(l * 255); return StringToColor ((string) r + "," + (string) g + "," + (string) b); } else { double v1, v2; double hue = (double) h / 360.0; v2 = (l < 0.5) ? (l * (1.0 + s)) : ((l + s) - (l * s)); v1 = 2.0 * l - v2; r = (unsigned char)(255 * HueToRGB (v1, v2, hue + (1.0 / 3.0))); g = (unsigned char)(255 * HueToRGB (v1, v2, hue)); b = (unsigned char)(255 * HueToRGB (v1, v2, hue - (1.0 / 3.0))); return StringToColor ((string) r + "," + (string) g + "," + (string) b); } } //---------------------------------------------------------------------------- private: double HueToRGB (double v1, double v2, double vH) { if (vH < 0) vH += 1; if (vH > 1) vH -= 1; if ((6 * vH) < 1) return (v1 + (v2 - v1) * 6 * vH); if ((2 * vH) < 1) return v2; if ((3 * vH) < 2) return (v1 + (v2 - v1) * ((2.0f / 3) - vH) * 6); return v1; } //---------------------------------------------------------------------------- public: int W; //monitor screen width public: int H; //monitor screen height private: int WscrFunc; //test function screen width private: int HscrFunc; //test function screen height public: CCanvas Canvas; //drawing table private: S_CLR FunctScrin []; //two-dimensional matrix of colors }; //——————————————————————————————————————————————————————————————————————————————Agora vamos dar uma olhada no próprio código do banco de testes. Ao estudar os parâmetros de entrada do banco de testes, fica claro que agora temos a possibilidade de escolher o algoritmo de otimização e as funções de teste nas configurações. Isso permite criar conjuntos únicos de funções de teste para verificar o algoritmo. Agora é possível realizar testes apenas em funções contínuas ou apenas em funções discretas. Além disso, agora é possível escolher qualquer combinação de funções de teste, mais adequada às necessidades do usuário.
#include "PAO\#C_TestStandFunctions.mqh" #include "PAO\#C_AO.mqh" //—————————————————————————————————————————————————————————————————————————————— input string AOparam = "----------------"; //AO parameters----------- input E_AO AOexactly_P = AO_NONE; input string TestStand_1 = "----------------"; //Test stand-------------- input double ArgumentStep_P = 0.0; //Argument Step input string TestStand_2 = "----------------"; //------------------------ input int Test1FuncRuns_P = 5; //Test #1: Number of functions in the test input int Test2FuncRuns_P = 25; //Test #2: Number of functions in the test input int Test3FuncRuns_P = 500; //Test #3: Number of functions in the test input string TestStand_3 = "----------------"; //------------------------ input EFunc Function1 = Hilly; input EFunc Function2 = Forest; input EFunc Function3 = Megacity; input string TestStand_4 = "----------------"; //------------------------ input int NumbTestFuncRuns_P = 10000; //Number of test function runs input int NumberRepetTest_P = 10; //Test repets number input string TestStand_5 = "----------------"; //------------------------ input bool Video_P = true; //Show video //—————————————————————————————————————————————————————————————————————————————— //—————————————————————————————————————————————————————————————————————————————— void OnStart () { C_AO *AO = SelectAO (AOexactly_P); if (AO == NULL) { Print ("AO is not selected..."); return; } Print (AO.GetName (), "|", AO.GetDesc (), "|", AO.GetParams ()); //============================================================================ C_TestStand ST; //stand ST.Init (750, 375); double allScore = 0.0; double allTests = 0.0; C_Function *F1 = SelectFunction (Function1); C_Function *F2 = SelectFunction (Function2); C_Function *F3 = SelectFunction (Function3); if (F1 != NULL) { Print ("============================="); ST.CanvasErase (); FuncTests (AO, ST, F1, Test1FuncRuns_P, clrLime, allScore, allTests); FuncTests (AO, ST, F1, Test2FuncRuns_P, clrAqua, allScore, allTests); FuncTests (AO, ST, F1, Test3FuncRuns_P, clrOrangeRed, allScore, allTests); delete F1; } if (F2 != NULL) { Print ("============================="); ST.CanvasErase (); FuncTests (AO, ST, F2, Test1FuncRuns_P, clrLime, allScore, allTests); FuncTests (AO, ST, F2, Test2FuncRuns_P, clrAqua, allScore, allTests); FuncTests (AO, ST, F2, Test3FuncRuns_P, clrOrangeRed, allScore, allTests); delete F2; } if (F3 != NULL) { Print ("============================="); ST.CanvasErase (); FuncTests (AO, ST, F3, Test1FuncRuns_P, clrLime, allScore, allTests); FuncTests (AO, ST, F3, Test2FuncRuns_P, clrAqua, allScore, allTests); FuncTests (AO, ST, F3, Test3FuncRuns_P, clrOrangeRed, allScore, allTests); delete F3; } Print ("============================="); if (allTests > 0.0) Print ("All score: ", DoubleToString (allScore, 5), " (", DoubleToString (allScore * 100 / allTests, 2), "%)"); delete AO; } //—————————————————————————————————————————————————————————————————————————————— //—————————————————————————————————————————————————————————————————————————————— void FuncTests (C_AO &ao, C_TestStand &st, C_Function &f, const int funcCount, const color clrConv, double &allScore, double &allTests) { if (funcCount <= 0) return; allTests++; if (Video_P) { st.DrawFunctionGraph (f); st.SendGraphToCanvas (); st.MaxMinDr (f); st.Update (); } int xConv = 0.0; int yConv = 0.0; double aveResult = 0.0; int params = funcCount * 2; int epochCount = NumbTestFuncRuns_P / (int)ao.params [0].val; //---------------------------------------------------------------------------- double rangeMin [], rangeMax [], rangeStep []; ArrayResize (rangeMin, params); ArrayResize (rangeMax, params); ArrayResize (rangeStep, params); for (int i = 0; i < funcCount; i++) { rangeMax [i * 2] = f.GetMaxRangeX (); rangeMin [i * 2] = f.GetMinRangeX (); rangeStep [i * 2] = ArgumentStep_P; rangeMax [i * 2 + 1] = f.GetMaxRangeY (); rangeMin [i * 2 + 1] = f.GetMinRangeY (); rangeStep [i * 2 + 1] = ArgumentStep_P; } for (int test = 0; test < NumberRepetTest_P; test++) { //-------------------------------------------------------------------------- if (!ao.Init (rangeMin, rangeMax, rangeStep)) break; // Optimization------------------------------------------------------------- for (int epochCNT = 1; epochCNT <= epochCount && !IsStopped (); epochCNT++) { ao.Moving (); for (int set = 0; set < ArraySize (ao.a); set++) { ao.a [set].f = f.CalcFunc (ao.a [set].c, funcCount); } ao.Revision (); if (Video_P) { //drawing a population-------------------------------------------------- st.SendGraphToCanvas (); for (int i = 0; i < ArraySize (ao.a); i++) { st.PointDr (ao.a [i].c, f, 1, 1, funcCount, false); } st.PointDr (ao.cB, f, 1, 1, funcCount, true); st.MaxMinDr (f); //drawing a convergence graph--------------------------------------------- xConv = (int)st.Scale (epochCNT, 1, epochCount, st.H + 2, st.W - 3, false); yConv = (int)st.Scale (ao.fB, f.GetMinFunValue (), f.GetMaxFunValue (), 2, st.H - 2, true); st.Canvas.FillCircle (xConv, yConv, 1, COLOR2RGB (clrConv)); st.Update (); } } aveResult += ao.fB; } aveResult /= (double)NumberRepetTest_P; double score = aveResult; Print (funcCount, " ", f.GetFuncName (), "'s; Func runs: ", NumbTestFuncRuns_P, "; result: ", aveResult); allScore += score; } //——————————————————————————————————————————————————————————————————————————————
5. Incorporando funções de teste amplamente conhecidas
Às vezes me perguntam por que eu não incluí no conjunto de funções de teste aquelas amplamente conhecidas e frequentemente usadas em pesquisas e desenvolvimento de algoritmos de otimização. A razão é que todas elas pertencem à categoria de "funções de teste simples" segundo minha classificação. Mais da metade de suas superfícies está concentrada perto do extremo global, o que as torna muito "previsíveis" para um teste adequado. Mas, apesar disso, as pessoas estão acostumadas a confiar nelas e tendem a testar seus algoritmos justamente nessas funções. Portanto, decidi incluir no conjunto as funções Ackley, Goldstein-Price e Schaffer No. 2. Isso ajudará a equilibrar a escolha das funções de teste para os usuários e a garantir testes mais abrangentes e confiáveis dos algoritmos de otimização, abrindo novos horizontes para os pesquisadores e promovendo uma compreensão mais profunda de sua eficácia.
Fórmula de Ackley:
f(x, y) = -(-20 * exp(-0.2 * sqrt(0.5 * (x^2 + y^2))) - exp(0.5 * (cos(2 * pi * x) + cos(2 * pi * y))) + e + 20)
onde:
x, y são os parâmetros de entrada da função,
e é o número de Euler (aproximadamente 2.71828),
π é o número pi (aproximadamente 3.14159).
Código da função:
double Core (double x, double y) { double res1 = -20.0 * MathExp (-0.2 * MathSqrt (0.5 * (x * x + y * y))); double res2 = -MathExp (0.5 * (MathCos (2.0 * M_PI * x) + MathCos (2.0 * M_PI * y))); double res3 = -(res1 + res2 + M_E + 20.0); return Scale (res3, -14.302667500265278, 0.0, 0.0, 1.0); }
Fórmula de Goldstein-Price:
f(x, y) = -([1 + (x + y + 1)^2 * (19 - 14x + 3x^2 - 14y + 6xy + 3y^2)] * [30 + (2x - 3y)^2 * (18 - 32x + 12x^2 + 48y - 36xy + 27y^2)])
Código da função:
double Core (double x, double y) { double part1 = 1 + MathPow ((x + y + 1), 2) * (19 - 14 * x + 3 * x * x - 14 * y + 6 * x * y + 3 * y * y); double part2 = 30 + MathPow ((2 * x - 3 * y), 2) * (18 - 32 * x + 12 * x * x + 48 * y - 36 * x * y + 27 * y * y); return Scale (-part1 * part2, -1015690.2717980597, -3.0, 0.0, 1.0); }
Fórmula de Schaffer No. 2:
f(x, y) = -(0.5 + ((sin(x^2 - y2)2 - 0.5) / (1 + 0.001 * (x^2 + y2))2))
Código da função:
double Core (double x, double y) { double numerator = MathPow (MathSin (x * x - y * y), 2) - 0.5; double denominator = MathPow (1 + 0.001 * (x * x + y * y), 2); return Scale (-(0.5 + numerator / denominator), -0.9984331449753265, 0, 0, 1.0); }
Nota: os valores das funções de teste foram ajustados para o intervalo [0.0, 1.0].
6. Construção de funções de teste 3D
Às vezes, para uma compreensão mais profunda das funções de teste e seu relevo, é necessário não apenas abstrair-se dos números e fórmulas, mas vê-los visualmente. Portanto, decidi aproveitar a oportunidade de construir cenas 3D usando DirectX na plataforma MetaTrader 5. Fui inspirado pelo arquivo "...\MQL5\Experts\Examples\Math 3D Morpher\Math 3D Morpher.mq5" dos desenvolvedores, que faz parte da distribuição padrão e visualiza funções que desenvolvi anteriormente (e que planejo adicionar à lista de funções para o banco de testes). Assim, decidi criar um visualizador para as funções de teste.
Para isso, tive que expandir o arquivo "Functions.mqh", onde está armazenada a classe de funções de teste usadas na testagem dos algoritmos de otimização. As funções adicionais que adicionei permitem não apenas estudar visualmente as mudanças nas funções ao modificá-las, se necessário, mas também investigar mais profundamente suas propriedades e características.
Esse processo não só torna minha pesquisa mais envolvente e visual, como também me ajuda a entender melhor o comportamento das funções de teste. Em última análise, a visualização em 3D me permite não apenas ver números na tela, mas interagir diretamente com a forma e estrutura das funções, o que é importante ao modificá-las e analisar suas propriedades.
Código das funções adicionais para construir o modelo para a cena 3D:
//—————————————————————————————————————————————————————————————————————————————— //GenerateFunctionDataFixedSize bool GenerateFunctionDataFixedSize (int x_size, int y_size, double &data [], double x_min, double x_max, double y_min, double y_max, C_Function &function) { if (x_size < 2 || y_size < 2) { PrintFormat ("Error in data sizes: x_size=%d,y_size=%d", x_size, y_size); return (false); } double dx = (x_max - x_min) / (x_size - 1); double dy = (y_max - y_min) / (y_size - 1); ArrayResize (data, x_size * y_size); //--- for (int j = 0; j < y_size; j++) { for (int i = 0; i < x_size; i++) { double x = x_min + i * dx; double y = y_min + j * dy; data [j * x_size + i] = function.Core (x, y); } } return (true); } //—————————————————————————————————————————————————————————————————————————————— //—————————————————————————————————————————————————————————————————————————————— //GenerateDataFixedSize bool GenerateDataFixedSize (int x_size, int y_size, C_Function &function, double &data []) { if (x_size < 2 || y_size < 2) { PrintFormat ("Error in data sizes: x_size=%d,y_size=%d", x_size, y_size); return (false); } return GenerateFunctionDataFixedSize (x_size, y_size, data, function.GetMinRangeX (), function.GetMaxRangeX (), function.GetMinRangeY (), function.GetMaxRangeY (), function); } //——————————————————————————————————————————————————————————————————————————————
Adicionalmente, adicionei variantes discretas das funções que já conhecemos: SkinDiscrete e HillyDiscrete.
Função HillyDiscrete.
Função SkinDiscrete.
Função Shaffer #2.
7. Considerações finais
Continuaremos utilizando as funções de teste "Hilly", "Forest" e "Megacity", que já conquistaram sua reputação como verdadeiros desafios para algoritmos de otimização, ajudando a criar uma tabela de classificação confiável. No entanto, não devemos nos limitar apenas a essas funções, pois, afinal, a lista de funções de teste foi ampliada com sucesso! Por isso, vocês têm a liberdade de escolha: experimentem, explorem, descubram novos horizontes, pois essa é a verdadeira essência da pesquisa científica.
Depois de nossa pesquisa "culinária" sobre métodos de otimização híbridos através da herança da classe base, vemos que a criação de tal ponto de partida abre portas para possibilidades infinitas de pesquisa. A união de diferentes algoritmos populacionais em uma única classe nos permite não apenas melhorar a precisão e velocidade na busca de soluções ótimas, mas também criar uma ferramenta universal para pesquisas.
A criação de um banco de testes universal que reúne diversas funções de teste — discretas, suaves, agudas, não diferenciáveis — abre novas possibilidades para testar e comparar diferentes métodos de otimização. Essa abordagem permite que os pesquisadores não apenas usem conjuntos padrão de funções, mas também criem seus próprios bancos de testes, adaptados a tarefas e requisitos específicos.
Assim, a união em uma única classe de algoritmos populacionais e a criação de um banco de testes universal nos abre um caminho fascinante para criar novas combinações "gastronômicas" de métodos de otimização, permitindo-nos descobrir novos sabores no mundo da busca de soluções ótimas. Vamos continuar esse experimento "culinário" e criar novas obras-primas no caminho para a perfeição e inovação na área de otimização!
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/14331
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso