
Algoritmos de otimização populacionais: algoritmo genético binário (Binary Genetic Algorithm, BGA). Parte II
Conteúdo:
1. Introdução
2. Descrição do algoritmo
3. Resultados dos testes
1. Introdução
No artigo anterior, apresentamos todos os conceitos e métodos básicos importantes, utilizados não apenas em algoritmos genéticos, mas, de alguma forma, em quaisquer algoritmos populacionais de otimização. Agora, com base nesses conhecimentos, podemos examinar detalhadamente o principal assunto deste "dossiê" o algoritmo genético binário (BGA). Começando com informações gerais, passamos ao código desta, em todos os sentidos, notável estratégia de busca, e finalizando com os resultados dos testes.
O algoritmo genético (GA) pertence aos algoritmos evolutivos, que incluem várias abordagens, como algoritmos genéticos, programação genética, estratégias evolutivas e outros. Eles são baseados nas ideias de evolução e hereditariedade, onde as soluções são representadas em forma de população, e operadores genéticos, como cruzamento e mutação, são aplicados para criar novas gerações de soluções.
Os algoritmos genéticos utilizam os princípios da seleção natural e genética para resolver problemas de otimização. O algoritmo genético binário (BGA) abordado no artigo foi o primeiro de todos os tipos de algoritmos genéticos. Assim, o BGA pertence à classe dos algoritmos evolutivos e é uma variante específica do algoritmo genético que utiliza a representação binária dos dados.
Os trabalhos sobre a criação de algoritmos genéticos começaram ainda na metade do século 20. Um dos pioneiros é John Holland, que em 1975 publicou o livro "Sistemas Adaptativos em Inteligência Artificial" (Adaptation in Natural and Artificial Systems), onde apresentou pela primeira vez o algoritmo genético como uma abordagem completa para resolver problemas de otimização.
O desenvolvimento do algoritmo genético binário foi inspirado por vários fatores e ideias. Os principais são:
- Seleção natural e princípios da evolução: O BGA é baseado nos princípios da seleção natural e evolução, propostos por Charles Darwin. A ideia é que na população existe uma diversidade de soluções, e aquelas que estão melhor adaptadas ao ambiente têm mais chances de sobreviver e transmitir suas características para a próxima geração.
- Genética e hereditariedade: O BGA também utiliza conceitos de genética, como gene, cromossomo e hereditariedade. As soluções no BGA são representadas como cadeias binárias, onde grupos individuais de bits representam genes específicos, e um gene, por sua vez, representa um parâmetro a ser otimizado. Os operadores genéticos, como cruzamento e mutação, são aplicados às cadeias binárias para criar novas gerações de soluções.
No geral, o desenvolvimento do BGA foi o resultado da combinação de ideias das áreas de algoritmos evolutivos, genética e otimização. Ele foi criado para resolver problemas de otimização, utilizando os princípios da seleção natural e genética, e seu desenvolvimento continua até os dias de hoje, com a criação de um grande número de variantes de GA, bem como o amplo uso de ideias e abordagens em algoritmos genéticos como parte de híbridos, incluindo algoritmos muito complexos e sofisticados.
O algoritmo genético binário, BGA, utiliza a representação binária dos dados. Isso significa que cada indivíduo (solução) é representado como uma sequência de bits (0 e 1). Operadores genéticos, como cruzamento e mutação, são aplicados às sequências de bits para criar novas gerações de soluções.
Fato interessante: os algoritmos genéticos, incluindo o BGA, podem ser usados para criar e melhorar a inteligência artificial. Por exemplo, eles podem ser aplicados para a evolução de redes neurais, permitindo a criação de modelos de aprendizado de máquina mais eficazes.
Os algoritmos genéticos em geral, e o BGA em particular, representam uma ferramenta poderosa para resolver problemas complexos de otimização, onde métodos tradicionais podem ser insuficientemente eficazes devido à ausência de uma solução analítica. No MetaTrader 5, é utilizado um GA binário, e por isso é duplamente interessante estudar os princípios de funcionamento deste notável algoritmo.
2. Descrição do algoritmo
O algoritmo genético binário inclui os seguintes passos:
- Inicialização da população: criar uma população inicial, composta por cromossomos com valores binários aleatórios.
- Avaliação da adaptabilidade: avaliar a adaptação de cada indivíduo (cromossomo) na população descendente.
- Seleção: escolher os pais para criar a prole usando o método da roleta.
- Cruzamento: dividir os cromossomos parentais em segmentos e criar novos cromossomos descendentes com segmentos de ambos os pais.
- Inversão: dividir o cromossomo do descendente em um ponto escolhido aleatoriamente e trocar as posições dos segmentos obtidos.
- Mutação: alterar bits nos genes da prole aleatoriamente com uma probabilidade de mutação determinada.
- Avaliação da adaptabilidade da prole: avaliar a adaptabilidade de cada novo descendente.
- Formação de uma nova população: colocar a população de descendentes no final da população geral e ordenar pelo valor de adaptação.
- Critério de parada: repetir o processo a partir do passo 3 até atingir o critério de parada.
Para garantir que o BGA trabalhe apenas com números positivos, para simplificar as operações com sequências binárias e aumentar a velocidade dos cálculos, usaremos a distância entre os valores "max" e "min" dos parâmetros otimizáveis. Os valores positivos obtidos dessa forma serão representados em código binário de Gray e dispostos sequencialmente em um único array de caracteres do cromossomo, conforme mostrado na figura 1. Vale destacar que os pontos de quebra dos cromossomos ao executar o método de cruzamento são colocados em locais aleatórios do cromossomo e não necessariamente nos pontos de junção dos genes; os pontos de quebra podem estar dentro do espaço do gene.
Figura 1. Disposição dos atributos do indivíduo (parâmetros otimizáveis) no cromossomo.
Há um número significativo de parâmetros externos no algoritmo genético, portanto, é sensato considerá-los com mais detalhes. Por padrão, os parâmetros praticamente correspondem totalmente às recomendações de muitos autores de publicações científicas. Eles foram verificados por mim e selecionados de forma a garantir a máxima eficiência na maioria dos tipos de tarefas. No entanto, a divergência desses parâmetros em qualquer direção pode levar à obtenção de 100% de convergência em testes com 10 ou até 50 parâmetros otimizáveis em funções específicas, mas ao mesmo tempo reduzir significativamente a eficácia em outras tarefas. Portanto, os parâmetros padrão são ótimos para uso na maioria dos casos praticamente relevantes.
- Population_P (tamanho da população = 50): o parâmetro define o número de indivíduos descendentes em cada geração da população. Esse tamanho de população é usado na maioria dos algoritmos considerados nos testes e garante um equilíbrio entre a diversidade de soluções e a velocidade de convergência.
- ParentPopulation_P (tamanho da população parental = 50): o parâmetro define o número de indivíduos parentais selecionados para reprodução e criação da próxima geração. A diminuição desse parâmetro melhora a convergência em funções suaves (aumenta a precisão), enquanto o aumento melhora em funções discretas (aumenta a diversidade do material genético).
- CrossoverProbab_P (probabilidade de cruzamento = 1.0): o parâmetro define a probabilidade de realizar a operação de cruzamento entre dois indivíduos parentais selecionados. Uma alta probabilidade de cruzamento aumenta as possibilidades combinatórias do algoritmo, enquanto a diminuição do valor aumenta a velocidade de convergência, mas aumenta a chance de estagnação.
- CrossoverPoints_P (número de pontos de cruzamento = 3): o parâmetro define o número de pontos onde ocorre o cruzamento entre os cromossomos parentais. O aumento dos pontos leva a uma mistura mais intensa dos parâmetros entre si e, no limite, se aproxima do comportamento aleatório do algoritmo.
- MutationProbab_P (probabilidade de mutação = 0.001): o parâmetro define a probabilidade de mutação de cada bit no genótipo do cromossomo. A mutação permite fazer alterações aleatórias no material genético e adicionar novas soluções à população. Uma probabilidade baixa aumenta a velocidade de convergência do algoritmo, mas diminui a diversidade, enquanto uma probabilidade muito alta leva à perda de informações úteis.
- InversionProbab_P (probabilidade de inversão = 0.7): o parâmetro define a probabilidade de realizar a operação de inversão no cromossomo. Uma alta probabilidade de inversão aumenta a diversidade do material genético, mas uma probabilidade muito alta leva ao comportamento aleatório do algoritmo.
- DoubleDigitsInChromo_P (número de dígitos decimais no gene): o parâmetro define o número de casas decimais do número real (parâmetro otimizável) representado em formato binário no cromossomo. O aumento desse valor leva ao aumento da complexidade dos cálculos e ao aumento do tempo de otimização (sem aplicar o formato binário diretamente na solução do problema, não faz sentido definir mais de 16 na saída, ao converter para double, os dígitos serão perdidos).
Passamos à análise do código.
Na inicialização dos agentes, é necessário determinar o número de bits na representação binária do parâmetro otimizável. Suponha que precisamos considerar cinco casas decimais, o que corresponde a um determinado comprimento do código de Gray. No entanto, esse código pode codificar um número que excede o necessário. Portanto, precisamos determinar o número máximo em ponto flutuante que pode ser codificado em formato binário. Posteriormente, podemos escalar o número codificado para o intervalo requerido do parâmetro otimizável na saída. Para a parte fracionária do número em ponto flutuante, utilizamos o número especificado de dígitos (indicado nos parâmetros), e para a parte inteira, quantos forem necessários em formato binário.
Por exemplo, se o parâmetro de entrada da função "digitsInGrayCode" for 3, a função retornará o valor máximo do tipo "ulong" usando o código de Gray para 3 bits, ou seja, 7 (111 no sistema binário).
Para alcançar esse objetivo determinar o número máximo possível que pode ser codificado com um determinado número de bits para as partes fracionária e inteira do número em ponto flutuante usaremos a função "GetMaxDecimalFromGray".
//—————————————————————————————————————————————————————————————————————————————— //Calculation of the maximum possible ulong number using the Gray code for a given number of bits ulong GetMaxDecimalFromGray (int digitsInGrayCode) { ulong maxValue = 1; for (int i = 1; i < digitsInGrayCode; i++) { maxValue <<= 1; maxValue |= 1; } return maxValue; } //——————————————————————————————————————————————————————————————————————————————
Para representar cada gene no cromossomo (posição do gene no cromossomo), usaremos a estrutura "S_BinaryGene", que contém campos e métodos para trabalhar com genes em formato binário:
- "gene": array de caracteres representando o gene.
- "integPart": array de caracteres da parte inteira do gene.
- "fractPart": array de caracteres da parte fracionária do gene.
- "integGrayDigits": número de dígitos no código de Gray para a parte inteira do gene.
- "fractGrayDigits": número de dígitos no código de Gray para a parte fracionária do gene.
- "length": comprimento total do gene.
- "minDoubleRange": valor mínimo do intervalo dos números em ponto flutuante.
- "maxDoubleRange": valor máximo do intervalo dos números em ponto flutuante.
- "maxDoubleNumber": número máximo em ponto flutuante que pode ser representado usando o gene.
- "doubleFract": valor para converter a parte fracionária do gene em número em ponto flutuante.
Métodos da estrutura:
- "Init": inicializa os campos da estrutura. Aceita valores mínimos e máximos do intervalo de números em ponto flutuante, bem como o número de dígitos na parte fracionária do gene. Dentro do método, são calculados os valores para o número máximo em ponto flutuante, partes codificantes do gene usando o código de Gray.
- "ToDouble": converte o gene em número em ponto flutuante. Aceita um array de caracteres do código de Gray "chromo" (cromossomo) e o índice de início do gene "indChr". O método lê a seção do cromossomo e converte o gene lido em valor decimal, depois o escala para o intervalo especificado de números em ponto flutuante.
- "Scale": executa a escala do valor de entrada "In" do intervalo "InMIN" e "InMAX" para o intervalo de saída "OutMIN" e "OutMAX". Este é um método auxiliar usado em "ToDouble".
//—————————————————————————————————————————————————————————————————————————————— struct S_BinaryGene { char gene []; char integPart []; char fractPart []; uint integGrayDigits; uint fractGrayDigits; uint length; double minDoubleRange; double maxDoubleRange; double maxDoubleNumber; double doubleFract; //---------------------------------------------------------------------------- void Init (double min, double max, int doubleDigitsInChromo) { doubleFract = pow (0.1, doubleDigitsInChromo); minDoubleRange = min; maxDoubleRange = max - min; ulong decInfr = 0; for (int i = 0; i < doubleDigitsInChromo; i++) { decInfr += 9 * (ulong)pow (10, i); } //---------------------------------------- DecimalToGray (decInfr, fractPart); ulong maxDecInFr = GetMaxDecimalFromGray (ArraySize (fractPart)); double maxDoubFr = maxDecInFr * doubleFract; //---------------------------------------- DecimalToGray ((ulong)maxDoubleRange, integPart); ulong maxDecInInteg = GetMaxDecimalFromGray (ArraySize (integPart)); double maxDoubInteg = (double)maxDecInInteg + maxDoubFr; maxDoubleNumber = maxDoubInteg; ArrayResize (gene, 0, 1000); integGrayDigits = ArraySize (integPart); fractGrayDigits = ArraySize (fractPart); length = integGrayDigits + fractGrayDigits; ArrayCopy (gene, integPart, 0, 0, WHOLE_ARRAY); ArrayCopy (gene, fractPart, ArraySize (gene), 0, WHOLE_ARRAY); } //---------------------------------------------------------------------------- double ToDouble (const char &chromo [], const int indChr) { double d; if(integGrayDigits > 0)d = (double)GrayToDecimal(chromo, indChr, indChr + integGrayDigits - 1); else d = 0.0; d +=(double)GrayToDecimal(chromo, indChr + integGrayDigits, indChr + integGrayDigits + fractGrayDigits - 1) * doubleFract; return minDoubleRange + Scale(d, 0.0, maxDoubleNumber, 0.0, maxDoubleRange); } //---------------------------------------------------------------------------- double Scale (double In, double InMIN, double InMAX, double OutMIN, double OutMAX) { if (OutMIN == OutMAX) return (OutMIN); if (InMIN == InMAX) return (double((OutMIN + OutMAX) / 2.0)); else { if (In < InMIN) return OutMIN; if (In > InMAX) return OutMAX; return (((In - InMIN) * (OutMAX - OutMIN) / (InMAX - InMIN)) + OutMIN); } } }; //——————————————————————————————————————————————————————————————————————————————
Para descrever um agente, precisamos da estrutura "S_Agent", que representa o agente e contém, além do cromossomo, os seguintes dados:
- "c": array de valores das coordenadas do agente em ponto flutuante.
- "f": valor da adaptabilidade do agente.
- "genes": array de estruturas "S_BinaryGene", descrevendo a posição de cada gene no cromossomo e as regras para converter o código binário em número em ponto flutuante.
- "chromosome": array de caracteres do cromossomo do agente.
- "calculated": valor lógico indicando se os valores do agente foram calculados (campo presente, mas não utilizado no código).
Métodos da estrutura:
- "Init": inicializa os campos da estrutura. Aceita a quantidade de coordenadas "coords", arrays "min" e "max" com os valores mínimos e máximos para cada parâmetro otimizável, bem como "doubleDigitsInChromo" o número de dígitos da parte fracionária do número em ponto flutuante no gene. O método cria e inicializa os genes para cada coordenada, além de definir o valor inicial de adaptabilidade e a flag "calculated".
- "ExtractGenes": extrai os valores dos genes do cromossomo e os salva no array "c", utilizando o método "ToDouble" da estrutura "S_BinaryGene" para converter os genes do cromossomo em números em ponto flutuante.
//—————————————————————————————————————————————————————————————————————————————— struct S_Agent { void Init (const int coords, const double &min [], const double &max [], int doubleDigitsInChromo) { ArrayResize(c, coords); f = -DBL_MAX; ArrayResize(genes, coords); ArrayResize(chromosome, 0, 1000); for(int i = 0; i < coords; i++) { genes [i].Init(min [i], max [i], doubleDigitsInChromo); ArrayCopy(chromosome, genes [i].gene, ArraySize(chromosome), 0, WHOLE_ARRAY); } calculated = false; } void ExtractGenes () { uint pos = 0; for (int i = 0; i < ArraySize (genes); i++) { c [i] = genes [i].ToDouble (chromosome, pos); pos += genes [i].length; } } double c []; //coordinates double f; //fitness S_BinaryGene genes []; char chromosome []; bool calculated; }; //——————————————————————————————————————————————————————————————————————————————
Em seguida, o código apresenta a definição da estrutura "S_Roulette", que representa um segmento da roleta.
Campos da estrutura:
- "start": valor do ponto inicial do segmento da roleta.
- "end": valor do ponto final do segmento da roleta.
//—————————————————————————————————————————————————————————————————————————————— struct S_Roulette { double start; double end; }; //——————————————————————————————————————————————————————————————————————————————
Declaramos a classe "C_AO_BGA", implementando o algoritmo genético:
- "cB": array de valores das melhores coordenadas.
- "fB": valor da adaptabilidade das melhores coordenadas.
- "a": array de estruturas "S_Agent", representando os agentes.
- "rangeMax": array de valores máximos do intervalo de busca.
- "rangeMin": array de valores mínimos do intervalo de busca.
- "rangeStep": array de valores representando o passo de busca.
Métodos da classe:
- "Init": inicializa os campos da classe. Aceita os parâmetros: quantidade de coordenadas "coordsP", tamanho da população "popSizeP", tamanho da população parental "parentPopSizeP", probabilidade de cruzamento "crossoverProbabP", número de pontos de cruzamento "crossoverPointsP", probabilidade de mutação "mutationProbabP", probabilidade de inversão "inversionProbabP", número de dígitos decimais no gene "doubleDigitsInChromoP". O método inicializa variáveis internas e arrays necessários para a operação do algoritmo genético.
- "Moving": método principal do algoritmo genético, executa operações com a população, como cruzamento, mutação, inversão e avaliação de adaptabilidade.
- "Revision": método que revisa a população, ordenando os agentes e selecionando os melhores.
Campos e métodos privados da classe são utilizados para a implementação interna do algoritmo genético, incluindo operações com a roleta, escalonamento de valores, ordenação e outras operações.
//—————————————————————————————————————————————————————————————————————————————— class C_AO_BGA { //---------------------------------------------------------------------------- public: double cB []; //best coordinates public: double fB; //FF of the best coordinates public: S_Agent a []; //agent public: double rangeMax []; //maximum search range public: double rangeMin []; //manimum search range public: double rangeStep []; //step search public: void Init (const int coordsP, //coordinates number const int popSizeP, //population size const int parentPopSizeP, //parent population size const double crossoverProbabP, //crossover probability const int crossoverPointsP, //crossover points const double mutationProbabP, //mutation probability const double inversionProbabP, //inversion probability const int doubleDigitsInChromoP);//number of decimal places in the gene public: void Moving (); public: void Revision (); //---------------------------------------------------------------------------- private: int coords; //coordinates number private: int popSize; //population size private: int parentPopSize; //parent population size private: double crossoverProbab; //crossover probability private: int crossoverPoints; //crossover points private: double mutationProbab; //mutation probability private: double inversionProbab; //inversion probability private: int doubleDigitsInChromo; //number of decimal places in the gene private: bool revision; private: S_Agent parents []; //parents private: int ind []; //temporary array for sorting the population private: double val []; //temporary array for sorting the population private: S_Agent pTemp []; //temporary array for sorting the population private: char tempChrome []; //temporary chromosome for inversion surgery private: uint lengthChrome; //length of the chromosome (the length of the string of characters according to the Gray code) private: int pCount; //indices of chromosome break points private: uint poRND []; //temporal indices of chromosome break points private: uint points []; //final indices of chromosome break points private: S_Roulette roulette []; //roulette private: void PreCalcRoulette (); private: int SpinRoulette (); private: double SeInDiSp (double In, double InMin, double InMax, double Step); private: double RNDfromCI (double min, double max); private: void Sorting (S_Agent &p [], int size); private: double Scale (double In, double InMIN, double InMAX, double OutMIN, double OutMAX); }; //——————————————————————————————————————————————————————————————————————————————
Em seguida, o código apresenta a implementação do método "Init" da classe "C_AO_BGA". Este método inicializa os campos da classe e arrays necessários para a operação do algoritmo genético.
Parâmetros de entrada do método:
- "coordsP": quantidade de coordenadas.
- "popSizeP": tamanho da população.
- "parentPopSizeP": tamanho da população parental.
- "crossoverProbabP": probabilidade de cruzamento.
- "crossoverPointsP": número de pontos de cruzamento.
- "mutationProbabP": probabilidade de mutação.
- "inversionProbabP": probabilidade de inversão.
- "doubleDigitsInChromoP": número de dígitos decimais no gene.
O método "Init" é usado para inicializar os campos da classe e arrays antes de executar o algoritmo genético. Ele define os valores dos campos da classe, verifica e corrige os valores de alguns parâmetros, além de redimensionar arrays, alocando memória para eles.
//—————————————————————————————————————————————————————————————————————————————— void C_AO_BGA::Init (const int coordsP, //coordinates number const int popSizeP, //population size const int parentPopSizeP, //parent population size const double crossoverProbabP, //crossover probability const int crossoverPointsP, //crossover points const double mutationProbabP, //mutation probability const double inversionProbabP, //inversion probability const int doubleDigitsInChromoP) //number of decimal places in the gene { MathSrand ((int)GetMicrosecondCount ()); // reset of the generator fB = -DBL_MAX; revision = false; coords = coordsP; popSize = popSizeP; parentPopSize = parentPopSizeP; crossoverProbab = crossoverProbabP; crossoverPoints = crossoverPointsP; pCount = crossoverPointsP; mutationProbab = mutationProbabP; inversionProbab = inversionProbabP; doubleDigitsInChromo = doubleDigitsInChromoP; if (crossoverPoints < 1) crossoverPoints = 1; if (pCount < 1) pCount = 1; ArrayResize (poRND, pCount); ArrayResize (points, pCount + 2); ArrayResize (ind, parentPopSize + popSize); ArrayResize (val, parentPopSize + popSize); ArrayResize (pTemp, parentPopSize + popSize); ArrayResize (a, popSize); ArrayResize (parents, parentPopSize + popSize); ArrayResize (roulette, parentPopSize); ArrayResize (rangeMax, coords); ArrayResize (rangeMin, coords); ArrayResize (rangeStep, coords); ArrayResize (cB, coords); } //——————————————————————————————————————————————————————————————————————————————
Todas as principais operações com o material genético dos agentes são executadas pelo método "Moving" da classe "C_AO_BGA". O método "Moving" executa um passo do algoritmo genético, incluindo a seleção de indivíduos parentais, cruzamento, inversão e mutação dos cromossomos, bem como a aplicação de operações aos genes e coordenadas dos indivíduos.
O método é logicamente dividido em várias partes:
1. "if (!revision)" se "revision" for "false", a inicialização dos indivíduos da população é realizada:
- O método "Init" é chamado para inicializar o indivíduo com os parâmetros dados.
- Um cromossomo aleatório é gerado preenchendo cada gene com um valor aleatório de 0 ou 1.
- O método "ExtractGenes" é chamado para extrair os genes do cromossomo.
- As coordenadas do indivíduo "c" são ajustadas para o intervalo usando a função "SeInDiSp".
- O valor de adaptabilidade de cada indivíduo "f" é definido como "-DBL_MAX".
- "lengthChrome = ArraySize(a[0].chromosome)" a duração do cromossomo do indivíduo é determinada (todos os indivíduos têm a mesma duração).
- "ArrayResize(tempChrome, lengthChrome)" o tamanho do array temporário "tempChrome" é ajustado para "lengthChrome".
2. Para cada indivíduo na população:
- O cálculo preliminar da roleta de seleção dos indivíduos parentais é realizado usando o método "PreCalcRoulette".
- A seleção do indivíduo parental é realizada usando o método "SpinRoulette".
- O cromossomo do indivíduo parental selecionado é copiado para o cromossomo do indivíduo atual.
- A operação de cruzamento é realizada com a probabilidade "crossoverProbab".
- Um segundo indivíduo parental é selecionado.
- Os pontos de quebra do cromossomo são determinados.
- O cruzamento dos cromossomos dos indivíduos parentais é realizado.
- A operação de inversão é realizada com a probabilidade "inversionProbab".
- Um ponto de quebra aleatório do cromossomo é determinado.
- As partes do cromossomo são trocadas.
- A operação de mutação é realizada para cada gene do cromossomo com a probabilidade "mutationProbab".
- O método "ExtractGenes" é chamado para extrair os genes do cromossomo.
- As coordenadas do indivíduo "c" são ajustadas para o intervalo usando a função "SeInDiSp".
//—————————————————————————————————————————————————————————————————————————————— void C_AO_BGA::Moving () { //---------------------------------------------------------------------------- if (!revision) { for (int i = 0; i < popSize; i++) { a [i].Init (coords, rangeMin, rangeMax, doubleDigitsInChromo); int r = 0; for (int len = 0; len < ArraySize (a [i].chromosome); len++) { r = MathRand (); //[0,32767] if (r > 16384) a [i].chromosome [len] = 1; else a [i].chromosome [len] = 0; } a [i].ExtractGenes (); for (int c = 0; c < coords; c++) a [i].c [c] = SeInDiSp (a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); a [i].f = -DBL_MAX; a [i].calculated = true; } lengthChrome = ArraySize (a [0].chromosome); ArrayResize (tempChrome, lengthChrome); for (int i = 0; i < parentPopSize + popSize; i++) { parents [i].Init (coords, rangeMin, rangeMax, doubleDigitsInChromo); parents [i].f = -DBL_MAX; } revision = true; return; } //---------------------------------------------------------------------------- int pos = 0; double r = 0; uint p1 = 0; uint p2 = 0; uint p3 = 0; uint temp = 0; for (int i = 0; i < popSize; i++) { PreCalcRoulette (); //selection, select and copy the parent to the child------------------------ pos = SpinRoulette (); ArrayCopy (a [i].chromosome, parents [pos].chromosome, 0, 0, WHOLE_ARRAY); //crossover----------------------------------------------------------------- r = RNDfromCI (0.0, 1.0); if (r < crossoverProbab) { //choose a second parent to breed with------------------------------------ pos = SpinRoulette (); //determination of chromosome break points-------------------------------- for (int p = 0; p < pCount; p++) { poRND [p] = (int)RNDfromCI (0.0, lengthChrome); if (poRND [p] >= lengthChrome) poRND [p] = lengthChrome - 1; } ArraySort (poRND); ArrayCopy (points, poRND, 1, 0, WHOLE_ARRAY); points [0] = 0; points [pCount + 1] = lengthChrome - 1; r = RNDfromCI (0.0, 1.0); int startPoint = r > 0.5 ? 0 : 1; for (int p = startPoint; p < pCount + 2; p += 2) { if (p < pCount + 1) { for (uint len = points [p]; len < points [p + 1]; len++) a [i].chromosome [len] = parents [pos].chromosome [len]; } } } //perform an inversion------------------------------------------------------ //(break the chromosome, swap the received parts, connect them together) r = RNDfromCI (0.0, 1.0); if (r < inversionProbab) { p1 = (int)RNDfromCI (0.0, lengthChrome); if (p1 >= lengthChrome) p1 = lengthChrome - 1; //copying the second part to the beginning of the temporary array for (uint len = p1; len < lengthChrome; len++) tempChrome [len - p1] = a [i].chromosome [len]; //copying the first part to the end of the temporary array for (uint len = 0; len < p1; len++) tempChrome [lengthChrome - p1 + len] = a [i].chromosome [len]; //copying a temporary array back for (uint len = 0; len < lengthChrome; len++) a [i].chromosome [len] = tempChrome [len]; } //perform mutation--------------------------------------------------------- for (uint len = 0; len < lengthChrome; len++) { r = RNDfromCI (0.0, 1.0); if (r < mutationProbab) a [i].chromosome [len] = a [i].chromosome [len] == 1 ? 0 : 1; } a [i].ExtractGenes (); for (int c = 0; c < coords; c++) a [i].c [c] = SeInDiSp (a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); a [i].calculated = true; } } //——————————————————————————————————————————————————————————————————————————————
O método "Revision" realiza a revisão da população, ordena os indivíduos pelo valor de sua função de adaptabilidade e atualiza a adaptabilidade da melhor solução "fB" e as coordenadas da melhor solução "cB". Antes de ordenar a população, a população descendente é copiada para o final da população geral.
//—————————————————————————————————————————————————————————————————————————————— void C_AO_BGA::Revision () { //---------------------------------------------------------------------------- for (int i = parentPopSize; i < parentPopSize + popSize; i++) { parents [i] = a [i - parentPopSize]; } Sorting (parents, parentPopSize + popSize); if (parents [0].f > fB) { fB = parents [0].f; ArrayCopy (cB, parents [0].c, 0, 0, WHOLE_ARRAY); } } //——————————————————————————————————————————————————————————————————————————————
O código de pré-calculação da roleta representa o método "PreCalcRoulette". Este método calcula previamente os valores dos intervalos para a roleta de seleção de indivíduos com base em sua função de adaptabilidade.
//—————————————————————————————————————————————————————————————————————————————— void C_AO_BGA::PreCalcRoulette () { roulette [0].start = parents [0].f; roulette [0].end = roulette [0].start + (parents [0].f - parents [parentPopSize - 1].f); for (int s = 1; s < parentPopSize; s++) { if (s != parentPopSize - 1) { roulette [s].start = roulette [s - 1].end; roulette [s].end = roulette [s].start + (parents [s].f - parents [parentPopSize - 1].f); } else { roulette [s].start = roulette [s - 1].end; roulette [s].end = roulette [s].start + (parents [s - 1].f - parents [s].f) * 0.1; } } } //——————————————————————————————————————————————————————————————————————————————
O método "SpinRoulette" é usado para impor a probabilidade de escolher a posição do pai.
//—————————————————————————————————————————————————————————————————————————————— int C_AO_BGA::SpinRoulette () { double r = RNDfromCI (roulette [0].start, roulette [parentPopSize - 1].end); for (int s = 0; s < parentPopSize; s++) { if (roulette [s].start <= r && r < roulette [s].end) return s; } return 0; } //——————————————————————————————————————————————————————————————————————————————
3. Resultados dos testes
Impressão do funcionamento do algoritmo genético binário (BGA) no ambiente de teste:
C_AO_BGA:50:50:1.0:3:0.001:0.7:3
=============================
5 Hilly's; Func runs: 10000; result: 0.9999191151339382
25 Hilly's; Func runs: 10000; result: 0.994841435673127
500 Hilly's; Func runs: 10000; result: 0.5048331764136147
=============================
5 Forest's; Func runs: 10000; result: 1.0
25 Forest's; Func runs: 10000; result: 0.9997457419655973
500 Forest's; Func runs: 10000; result: 0.32054251149158375
=============================
5 Megacity's; Func runs: 10000; result: 0.9066666666666668
25 Megacity's; Func runs: 10000; result: 0.9640000000000001
500 Megacity's; Func runs: 10000; result: 0.23034999999999997
=============================
All score: 6.92090 (76.90%)
A visualização do processo de otimização do BGA demonstra claramente a alta convergência do algoritmo. É interessante notar que, no processo de otimização, uma parte da população permanece distante do extremo global, o que indica a exploração de novas áreas desconhecidas do espaço de busca, mantendo a diversidade de soluções na população. No entanto, o algoritmo enfrenta certas dificuldades ao trabalhar com a função discreta "Megacity", que é problemática para a maioria dos algoritmos. Apesar de certa dispersão nos resultados ao trabalhar com essa função complexa, o BGA se destaca significativamente melhor que outros algoritmos.
BGA na função de teste Hilly.
BGA na função de teste Forest.
BGA na função de teste Megacity.
O BGA ocupou o primeiro lugar na tabela de classificação, demonstrando os melhores resultados na maioria dos testes para todas as três funções de teste. Os resultados do BGA foram especialmente impressionantes na função discreta Megacity, superando outros algoritmos.
|
Considerações finais
Neste artigo, examinamos a versão clássica do BGA, um caso específico da classe geral dos algoritmos genéticos (GA), e todas as conclusões se referem a ele. Apesar da antiga ideia de representar soluções de forma binária, a abordagem utilizando código binário permanece relevante até hoje. Ela integra dimensões espaciais independentes do problema de otimização em uma única entidade, codificando informações em um único cromossomo, algo que é difícil de realizar com a codificação tradicional em ponto flutuante, destacando assim este algoritmo entre outros algoritmos de otimização.
Embora eu entenda perfeitamente a matemática e a lógica da estratégia BGA, ainda fico maravilhado com o que acontece com o cromossomo. É como comparar isso a um caleidoscópio mágico. Quando giramos o caleidoscópio, diversas formas e cores se combinam em padrões únicos, criando uma imagem impressionante. Da mesma forma, o operador de cruzamento no BGA corta aleatoriamente o cromossomo em várias partes, incluindo áreas internas dos parâmetros. Essas partes são então reunidas, semelhante à mistura de fragmentos do caleidoscópio. Esse processo permite combinar as melhores características de diferentes soluções e criar uma nova combinação mais otimizada. Como no caso do caleidoscópio, os resultados do cruzamento no BGA podem ser surpreendentes e inesperados, transformando cromossomos simples em verdadeiros "diamantes" de soluções ótimas.
Estou confiante de que a informação nas duas partes do artigo sobre métodos e ferramentas usadas em algoritmos genéticos ajudará você a expandir seu conhecimento e alcançar novos patamares em seu trabalho e pesquisas. Na natureza, na engenharia e na mente humana, a força da evolução se manifesta continuamente, e o BGA é um dos muitos algoritmos incríveis que nos ajudam a alcançar novos patamares e conquistas.
O BGA resolve eficientemente diversas tarefas, seja em superfícies suaves, problemas discretos ou até mesmo problemas de grande dimensionalidade, inclusive utilizando uma abordagem estocástica, abrindo novas possibilidades onde as soluções analíticas são limitadas.
Figura 2. Graduação de cores dos algoritmos nos testes correspondentes. Resultados iguais ou superiores a 0.99 são destacados em branco.
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, o script para cálculo da tabela de classificação está no arquivo).
Vantagens e desvantagens do algoritmo genético binário (BGA), referem-se exclusivamente à implementação apresentada:
Vantagens:
- Alta eficiência na resolução de diversas tarefas.
- Resistência a ficar preso em soluções locais.
- Altos resultados tanto em funções suaves quanto em funções discretas complexas.
- Alta convergência.
Desvantagens:
- Grande quantidade de parâmetros externos.
- Implementação do algoritmo bastante complexa.
- Alta complexidade computacional.
Um arquivo com versões atualizadas e atuais dos códigos dos algoritmos descritos em artigos anteriores está anexado ao artigo. O autor do artigo não se responsabiliza pela precisão absoluta na descrição dos algoritmos canônicos, pois muitas modificações foram feitas para melhorar as capacidades de busca. As conclusões e julgamentos apresentados nos artigos são baseados nos resultados dos experimentos realizados.
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/14040
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.






- 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