
Algoritmo de comportamento social adaptativo — Adaptive Social Behavior Optimization (ASBO): Método de Schwefel, Box-Muller
1. Introdução
Há uma sabedoria incrível no comportamento coletivo dos organismos vivos. Desde cardumes de peixes até sociedades humanas, a cooperação e a interação são fundamentais para a sobrevivência e o sucesso. E se pudéssemos utilizar esses princípios de estrutura social para criar novos algoritmos de otimização capazes de resolver tarefas complexas de maneira eficiente e precisa?
Na natureza, há muitos exemplos de comportamento em grupo, no qual os organismos se unem em sociedades para aumentar suas chances de sobrevivência e inovação. Esse fenômeno, observado no reino animal, nas sociedades humanas e em outras formas de vida, tornou-se objeto de estudos fascinantes para biólogos evolucionistas e filósofos sociais. Como resultado do estudo dessas sociedades, foi desenvolvido um modelo de computação que imita seu funcionamento bem-sucedido em relação a determinados objetivos. Esses modelos, como a otimização por enxame de partículas e a otimização por colônia de formigas, demonstram a eficácia do trabalho em grupo na resolução de problemas de otimização.
Neste artigo, examinamos o conceito de estrutura social e sua influência nos processos de tomada de decisão em grupos. Apresentamos também um modelo matemático baseado nos princípios de comportamento social e interação em sociedades, que pode ser aplicado para alcançar a otimização global. Denominado ASBO (Adaptive Social Behavior Optimization), esse modelo considera a influência do ambiente na tomada de decisões em grupo, incluindo liderança, vizinhança e autoorganização. O algoritmo "Adaptive Social Behavior Optimization" (ASBO) foi proposto por Manojo Kumar Singh e publicado em 2013 nos anais da conferência Proceedings of ICAdC, AISC 174, sob a edição de Aswatha Kumar M. et al.
Estrutura da sociedade e modelo de influência:
- A sociedade é um grupo de seres vivos interconectados, unidos por comportamentos ou características comuns.
- Viver em sociedade traz vantagens como o aumento das oportunidades de obtenção de recursos, reprodução, proteção contra predadores e inovações.
- A sociedade é um grupo de seres vivos interconectados por comportamentos ou características comuns.
- O modelo ASBO proposto se baseia em liderança dinâmica, vizinhança lógica dinâmica e autoavaliação.
Os princípios fundamentais incorporados ao algoritmo ASBO são:
- Uma população de soluções, cada uma representando um vetor em um espaço D-dimensional.
- Para cada solução, calculam-se três vetores: o melhor global, o melhor pessoal e o centro dos vizinhos.
- A nova posição da solução é calculada utilizando coeficientes adaptativos de influência.
- Os coeficientes de influência são ajustados através de uma estratégia de mutação autoadaptativa.
2. Implementação dos métodos do algoritmo
Antes de nos aprofundarmos no estudo do próprio algoritmo, porém, é importante compreender o conceito central que o sustenta. Esse conceito está relacionado ao uso do método de Schwefel, uma abordagem de mutação autoadaptativa utilizada em alguns algoritmos de otimização, como os algoritmos evolucionários. Suas principais características são:
1. Autoadaptação dos parâmetros de mutação:
- Cada indivíduo (solução) possui seus próprios parâmetros de mutação (como o tamanho do passo de mutação).
- Esses parâmetros de mutação também evoluem junto com as próprias soluções.
- Assim, o tamanho do passo de mutação é ajustado ao terreno da função para cada indivíduo.
2. Mutação Gaussiana:
- Um novo conjunto de soluções é gerado utilizando uma distribuição Gaussiana (normal).
- O valor médio da mutação é igual à solução anterior, enquanto o desvio padrão é determinado pelos parâmetros de mutação.
3. Relação entre a solução e os parâmetros de mutação:
- Os parâmetros de mutação (tamanho do passo) dependem do valor da função objetivo (adaptabilidade) da solução.
- Quanto melhor a solução, menor o tamanho do passo de mutação, e vice-versa.
A ideia central do conceito de Schwefel é que a adaptação dos parâmetros de mutação permite que o algoritmo explore o espaço de soluções de maneira mais eficiente, especialmente nas etapas finais da busca, quando são necessários ajustes mais precisos.
O exemplo a seguir demonstra a implementação do conceito de Schwefel para a otimização de parâmetros de uma estratégia de negociação. Abaixo, será feita uma análise detalhada do funcionamento do método.
Para ilustrar, consideremos um EA (Expert Advisor) hipotético mais simples, no qual uma população inicial de soluções aleatórias é criada durante a inicialização ("OnInit"). No evento "OnTick", é executada uma etapa do processo evolucionário:
a. A adaptabilidade de cada indivíduo na população é avaliada.
b. A população é ordenada por adaptabilidade.
c. A mutação se aplica a todos os indivíduos, exceto ao melhor.
d. O contador de gerações é incrementado.
Esse processo é repetido até que o número predeterminado de gerações seja atingido. Ao concluir a otimização, a melhor solução encontrada é apresentada.
// Входные параметры input int PopulationSize = 50; // Размер популяции input int Generations = 100; // Число поколений input double InitialStepSize = 1.0; // Начальный размер шага // Структура для хранения индивида struct Individual { double genes [3]; // Параметры стратегии double stepSizes [3]; // Размеры шага для каждого параметра double fitness; // Приспособленность }; // Глобальные переменные Individual population []; int generation = 0; // Функция инициализации int OnInit () { ArrayResize (population, PopulationSize); InitializePopulation (); return (INIT_SUCCEEDED); } // Функция основного цикла datetime lastOptimizationTime = 0; void OnTick () { datetime currentTime = TimeCurrent (); // Проверяем, прошли ли сутки с последней оптимизации if (currentTime - lastOptimizationTime >= 86400) // 86400 секунд в сутках { Optimize (); lastOptimizationTime = currentTime; } // Здесь код для торговли с текущими оптимальными параметрами TradingLogic (); } void Optimize () { // Код оптимизации (текущее содержимое OnTick) } void TradingLogic () { // Реализация торговой логики с использованием оптимизированных параметров } // Инициализация популяции void InitializePopulation () { for (int i = 0; i < PopulationSize; i++) { for (int j = 0; j < 3; j++) { population [i].genes [j] = MathRand () / 32767.0 * 100; // Случайные значения от 0 до 100 population [i].stepSizes [j] = InitialStepSize; } } } // Оценка приспособленности популяции void EvaluatePopulation () { for (int i = 0; i < PopulationSize; i++) { population [i].fitness = CalculateFitness (population [i]); } } // Расчет приспособленности индивида (здесь нужно реализовать вашу целевую функцию) double CalculateFitness (Individual &ind) { // Пример простой целевой функции return -(MathPow (ind.genes [0] - 50, 2) + MathPow (ind.genes [1] - 50, 2) + MathPow (ind.genes [2] - 50, 2)); } // Сортировка популяции по убыванию приспособленности void SortPopulation () { ArraySort (population, WHOLE_ARRAY, 0, MODE_DESCEND); } // Мутация популяции по концепции Швефеля void Mutate () { for (int i = 1; i < PopulationSize; i++) // Начинаем с 1, чтобы сохранить лучшее решение { for (int j = 0; j < 3; j++) { // Мутация размера шага population [i].stepSizes [j] *= MathExp (0.2 * MathRandom () - 0.1); // Мутация гена population [i].genes [j] += population [i].stepSizes [j] * NormalRandom (); // Ограничение значений генов population [i].genes [j] = MathMax (0, MathMin (100, population [i].genes [j])); } } } // Вспомогательная функция для вывода информации об индивиде void PrintIndividual (Individual &ind) { Print ("Гены: ", ind.genes [0], ", ", ind.genes [1], ", ", ind.genes [2]); Print ("Размеры шага: ", ind.stepSizes [0], ", ", ind.stepSizes [1], ", ", ind.stepSizes [2]); Print ("Приспособленность: ", ind.fitness); }
Vamos analisar o método por partes:
1. Estrutura e parâmetros de entrada.
Primeiramente, definimos os parâmetros de entrada para o algoritmo e a estrutura "Individual", que representa uma solução individual na população. Cada indivíduo possui genes (parâmetros da estratégia), tamanhos de passo para mutação e um valor de adaptabilidade.
input int PopulationSize = 50; // Размер популяции input int Generations = 100; // Число поколений input double InitialStepSize = 1.0; // Начальный размер шага struct Individual { double genes[3]; // Параметры стратегии double stepSizes[3]; // Размеры шага для каждого параметра double fitness; // Приспособленность };
2. Inicialização.
Na função "OnInit()", criamos e inicializamos a população. A função "InitializePopulation()" preenche a população com valores aleatórios para os genes e define os tamanhos iniciais dos passos de mutação.
int OnInit () { ArrayResize (population, PopulationSize); InitializePopulation (); return (INIT_SUCCEEDED); } void InitializePopulation () { for (int i = 0; i < PopulationSize; i++) { for (int j = 0; j < 3; j++) { population [i].genes [j] = MathRand () / 32767.0 * 100; population [i].stepSizes [j] = InitialStepSize; } } }
3. Ciclo principal.
A função "OnTick()" controla o processo de otimização. Ela avalia a população, a classifica, realiza a mutação e avança para a próxima geração.
datetime lastOptimizationTime = 0; void OnTick () { datetime currentTime = TimeCurrent (); // Проверяем, прошли ли сутки с последней оптимизации if (currentTime - lastOptimizationTime >= 86400) // 86400 секунд в сутках { Optimize (); lastOptimizationTime = currentTime; } // Здесь код для торговли с текущими оптимальными параметрами TradingLogic (); } void Optimize () { // Код оптимизации (текущее содержимое OnTick) } void TradingLogic () { // Реализация торговой логики с использованием оптимизированных параметров }
4. Avaliação e classificação da população.
Essas funções avaliam a adaptabilidade de cada indivíduo e classificam a população em ordem decrescente de adaptabilidade. A função "CalculateFitness()" neste exemplo é simplificada, mas, em uma aplicação real, deve conter a função objetivo para avaliar a estratégia de negociação.
void EvaluatePopulation () { for (int i = 0; i < PopulationSize; i++) { population [i].fitness = CalculateFitness (population [i]); } } double CalculateFitness (Individual &ind) { return -(MathPow (ind.genes [0] - 50, 2) + MathPow (ind.genes [1] - 50, 2) + MathPow (ind.genes [2] - 50, 2)); } void SortPopulation () { ArraySort (population, WHOLE_ARRAY, 0, MODE_DESCEND); }
5. Mutação.
Esta é a parte-chave que implementa o conceito de Schwefel. Para cada indivíduo (exceto o melhor), realizamos as seguintes etapas:
- Mutamos o tamanho do passo, multiplicando-o por um expoente de um número aleatório.
- Mutamos o gene, adicionando a ele um número aleatório normalmente distribuído, multiplicado pelo tamanho do passo.
- Limitamos os valores dos genes ao intervalo [0, 100].
Esta é uma implementação básica do conceito de Schwefel para otimização de parâmetros. Em aplicações reais, é necessário adaptar a função objetivo para uma estratégia de trading específica.
void Mutate () { for (int i = 1; i < PopulationSize; i++) { for (int j = 0; j < 3; j++) { population [i].stepSizes [j] *= MathExp (0.2 * MathRandom () - 0.1); population [i].genes [j] += population [i].stepSizes [j] * NormalRandom (); population [i].genes [j] = MathMax (0, MathMin (100, population [i].genes [j])); } } }
Uma implementação notável é a da função "NormalRandom()", que faz parte do conceito de mutação adaptativa de Schwefel e utiliza o método Box-Muller para gerar números aleatórios com distribuição normal (gaussiana). Vamos detalhar essa função:
1. Geração de números uniformemente distribuídos. Geram-se dois números aleatórios independentes "u1" e "u2", distribuídos uniformemente no intervalo [0, 1].
double u1 = u.RNDfromCI(0, 1); double u2 = u.RNDfromCI(0, 1);
2. Transformação para distribuição normal. A fórmula de transformação de Box-Muller converte números uniformemente distribuídos em números normalmente distribuídos.
return MathSqrt(-2 * MathLog(u1)) * MathCos(2 * M_PI * u2);
É importante destacar que essa é metade da implementação da transformação de Box-Muller e gera um número normalmente distribuído. A transformação completa gera dois números normalmente distribuídos
z0 = MathSqrt(-2 * MathLog(u1)) * MathCos(2 * M_PI * u2); z1 = MathSqrt(-2 * MathLog(u1)) * MathSin(2 * M_PI * u2);
Nossa implementação utiliza apenas o cosseno, o que resulta em um único número normalmente distribuído. Isso é perfeitamente aceitável se apenas um número for necessário por chamada. Se ambos os números forem necessários, é possível adicionar o cálculo com o seno.
Essa implementação é eficiente e amplamente utilizada para gerar números aleatórios com distribuição normal em várias aplicações, incluindo algoritmos evolucionários e otimização estocástica.
Características dos números gerados:
1. Distribuição: Distribuição normal (gaussiana)
2. Valor médio: 0
3. Desvio padrão: 1
Intervalo dos números gerados: teoricamente, a distribuição normal pode gerar números no intervalo de menos infinito a mais infinito. Na prática:
- Cerca de 68% dos números gerados estarão no intervalo [-1, 1].
- Cerca de 95% dos números estarão no intervalo [-2, 2].
- Cerca de 99,7% dos números estarão no intervalo [-3, 3].
Raramente, números fora do intervalo [-4, 4] são gerados; a probabilidade disso é extremamente baixa.
O método Box-Muller é usado para gerar números aleatórios com distribuição normal, o que é fundamental para a implementação de mutação autoadaptativa no algoritmo baseado no conceito de Schwefel. Tal distribuição permite gerar pequenas alterações com maior frequência, mas ocasionalmente admite mutações mais significativas, contribuindo para a exploração eficaz do espaço de soluções. Vamos verificar e avaliar o método Box-Muller na prática.
Escreveremos um script para testar a função "NormalRandom()":
#property version "1.00" #property script_show_inputs input int NumSamples = 10000; // Количество генерируемых чисел double NormalRandom () { double u1 = (double)MathRand () / 32767.0; double u2 = (double)MathRand () / 32767.0; return MathSqrt (-2 * MathLog (u1)) * MathCos (2 * M_PI * u2); } void OnStart () { double sum = 0; double sumSquared = 0; double min = DBL_MAX; double max = DBL_MIN; int histogram []; ArrayResize (histogram, 20); ArrayInitialize (histogram, 0); // Генерация и анализ случайных чисел for (int i = 0; i < NumSamples; i++) { double value = NormalRandom (); sum += value; sumSquared += value * value; if (value < min) min = value; if (value > max) max = value; // Заполнение гистограммы int index = (int)((value + 4) / 0.4); // Разбиваем диапазон [-4, 4] на 20 интервалов if (index >= 0 && index < 20) histogram [index]++; } // Расчет статистики double mean = sum / NumSamples; double variance = (sumSquared - sum * sum / NumSamples) / (NumSamples - 1); double stdDev = MathSqrt (variance); // Вывод результатов Print ("Статистика для ", NumSamples, " сгенерированных чисел:"); Print ("Среднее значение: ", mean); Print ("Стандартное отклонение: ", stdDev); Print ("Минимальное значение: ", min); Print ("Максимальное значение: ", max); // Вывод гистограммы Print ("Гистограмма распределения:"); for (int i = 0; i < 20; i++) { string bar = ""; for (int j = 0; j < histogram [i] * 50 / NumSamples; j++) bar += "*"; PrintFormat ("[%.1f, %.1f): %s", -4 + i * 0.4, -3.6 + i * 0.4, bar); } }
O script de teste realiza as seguintes etapas:
1. Define a função "NormalRandom()".
2. Gera uma quantidade predeterminada de números aleatórios (por padrão, 10.000).
3. Calcula as principais características estatísticas: valor médio, desvio padrão, valores mínimo e máximo.
4. Cria um histograma da distribuição, dividindo o intervalo [-4, 4] em 20 intervalos.
5. Exibe os resultados no log do terminal MetaTrader.
Agora, testaremos o script com 20.000 valores. Imprimiremos os resultados do script para verificar o método de transformação Box-Muller:
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) Estatísticas para 20.000 números gerados:
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) Média: -0.003037802901958332
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) Desvio padrão: 0.9977477093538349
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) Valor mínimo: -3.865371560675546
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) Valor máximo: 3.4797509297243994
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) Histograma de distribuição:
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) [-4.0, -3.6):
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) [-3.6, -3.2):
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) [-3.2, -2.8):
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) [-2.8, -2.4):
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) [-2.4, -2.0):
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) [-2.0, -1.6): *
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) [-1.6, -1.2): **
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) [-1.2, -0.8): ****
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) [-0.8, -0.4): ******
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) [-0.4, 0.0): *******
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) [0.0, 0.4): *******
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) [0.4, 0.8): ******
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) [0.8, 1.2): ****
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) [1.2, 1.6): ***
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) [1.6, 2.0): *
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) [2.0, 2.4):
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) [2.4, 2.8):
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) [2.8, 3.2):
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) [3.2, 3.6):
2024.07.12 13:11:05.437 checkRND (.US500Cash,M5) [3.6, 4.0):
A partir dos resultados impressos, é evidente que o método funciona corretamente: o desvio padrão é praticamente igual a 1, o valor médio é próximo de 0 e a dispersão corresponde ao intervalo [-4, 4].
Agora avançamos para o cálculo dos parâmetros adaptativos de mutação e a implementação da função:
//—————————————————————————————————————————————————————————————————————————————— void AdaptiveMutation (double &Cg, double &Cs, double &Cn) { Cg *= MathExp (tau_prime * NormalRandom() + tau * NormalRandom()); Cs *= MathExp (tau_prime * NormalRandom() + tau * NormalRandom()); Cn *= MathExp (tau_prime * NormalRandom() + tau * NormalRandom()); } //——————————————————————————————————————————————————————————————————————————————
Os parâmetros adaptativos (Cg, Cs e Cn) são calculados utilizando uma estratégia de mutação autoadaptativa baseada no conceito proposto por Schwefel. As fórmulas para o cálculo desses parâmetros são as seguintes:
1. Inicialização:
- Uma população de N soluções, em que cada solução é representada por um par de vetores (pi, σi), em que i ∈ {0, 1, 2} e corresponde aos três parâmetros Cg, Cs e Cn.
- Os valores iniciais dos componentes pi são escolhidos aleatoriamente de acordo com uma distribuição uniforme no espaço de soluções previsto.
- Os valores iniciais de σi são definidos como um valor fixo.
2. Geração de descendentes:
- Para cada pai (pi, σi), um descendente (pi', σi') é gerado pelas seguintes fórmulas:
σ'i (j) = σi (j) * exp (τ' * N (0,1) + τ * Nj (0,1))
p'i (j) = pi (j) + σi (j) * N (0,1)
Onde pi (j), p'i (j), σi (j), σ'i (j) representam a j-ésima componente dos vetores pi, p'i, σi, σ'i, respectivamente.
- O número aleatório N(0,1) é extraído de uma distribuição normal com média 0 e desvio padrão 1.
- O número aleatório extraído de uma distribuição normal com média 0 e desvio padrão 1, onde "j" é o contador, é chamado de Nj (0,1).
- Os fatores de escalonamento são definidos como τ и τ' e (√(2√n))^-1 e (√(2n))^-1 , respectivamente, onde n é a dimensionalidade do problema.
Dessa forma, os parâmetros adaptativos Cg, Cs e Cn sofrem mutações de acordo com essa estratégia autoadaptativa, permitindo que eles se ajustem dinamicamente durante o processo de otimização.
Como mostrado abaixo, a partir dos valores obtidos para os coeficientes Cg, Cs e Cn, esses coeficientes atingem valores extremamente altos em alguns casos específicos. Isso ocorre porque, na equação de atualização dos parâmetros estratégicos σ, o novo valor é obtido multiplicando o valor anterior. Embora essa abordagem permita que os parâmetros se adaptem a superfícies complexas da função objetivo, ela também pode levar a instabilidades e variações bruscas nos valores.
Valores de Cg, Cs e Cn:
1.3300705071425474 0.0019262948596068497 0.00015329292900983978
1.9508542383574192 0.00014860860614036015 7007.656113084095
52.13323602205895 1167.5425054524449 0.0008421503214593158
1.0183156975753507 0.13486291437434697 0.18290674582014257
0.00003239513683361894 61.366694225534175 45.11710564761292
0.0004785111900713668 0.4798661298436033 0.007932035893070274
2712198854.6325 0.00003936758705232012 325.9282730206205
0.0016658142882911 22123.863582276706 1.6844067196638965
2.0422888701555126 0.007999762224016285 0.02460292446531715
7192.66545492709 0.000007671729921045711 0.3705939923291289
0.0073279981653727785 3237957.2544339723 1.6750241266497745e-07
550.7433921368721 13.512466529311943 223762.44571145615
34.571961515974785 0.000008292503593679501 0.008122937723317175
0.000002128739177639208 63.17654973794633 128927.83801094144
866.7293481660888 1260.0820389718326 1.8496629497788273
0.000008459817609364248 25.623751292511788 0.0013478840638847347
27.956286711833616 0.0006967869388129299 0.0006885039945210606
66.6928872126239 47449.76869262452 8934.079392419668
0.15058617433681198 0.003114981958516487 7.703748428996011e-07
0.22147618633450808 265.4903003920267 315.20318731505455
0.0000015873778483580056 1134.6304274682934 0.7883024873065534
Quando os parâmetros Cg, Cs e Cn alcançam valores muito altos devido à mutação autoadaptativa pelo método de Schwefel, pode ser necessário adotar medidas para controlá-los e limitá-los. Isso é essencial para manter a estabilidade e a eficiência do algoritmo. Algumas abordagens possíveis para limitar os valores numéricos dos coeficientes incluem:
1. Limitação de valores.
Definir limites superiores e inferiores para Cg, Cs e Cn. Por exemplo:
void LimitParameters (double ¶m, double minValue, double maxValue) { param = MathMax (minValue, MathMin (param, maxValue)); } // Применение: LimitParameters (Cg, 0.0, 2.0); LimitParameters (Cs, 0.0, 2.0); LimitParameters (Cn, 0.0, 2.0);
2. Normalização.
Normalizar os valores dos parâmetros após a mutação, para que sua soma seja sempre igual a 1:
void NormalizeParameters (double &Cg, double &Cs, double &Cn) { double sum = Cg + Cs + Cn; if (sum > 0) { Cg /= sum; Cs /= sum; Cn /= sum; } else { // Если сумма равна 0, установите равные значения Cg = Cs = Cn = 1.0 / 3.0; } }
3. Escalonamento logarítmico.
Aplicar escalonamento logarítmico para suavizar valores elevados:
double ScaleParameter (double param) { if (param == 0) return 0; double sign = (param > 0) ? 1 : -1; return sign * MathLog (1 + MathAbs (param)); }
4. Escalonamento adaptativo do passo de mutação.
Reduzir o tamanho do passo de mutação se os parâmetros se tornarem excessivamente grandes:
void AdaptMutationStep(double &stepSize, double paramValue) { if(MathAbs(paramValue) > threshold) { stepSize *= 0.9; // Уменьшаем размер шага } }
5. Redefinição periódica.
Redefinir periodicamente os parâmetros para seus valores iniciais ou para o valor médio da população:
void ResetParameters(int generationCount) { if(generationCount % resetInterval == 0) { Cg = initialCg; Cs = initialCs; Cn = initialCn; } }
6. Uso de função exponencial.
Aplicar uma função exponencial para limitar o crescimento dos parâmetros:
double LimitGrowth(double param) { return 2.0 / (1.0 + MathExp(-param)) - 1.0; // Ограничивает значения в диапазоне [-1, 1] }
7. Monitoramento e adaptação.
Acompanhar os valores dos parâmetros e ajustar a estratégia de mutação se eles frequentemente ultrapassarem os limites permitidos:
void MonitorAndAdapt(double ¶m, int &outOfBoundsCount) { if(MathAbs(param) > maxAllowedValue) { outOfBoundsCount++; if(outOfBoundsCount > threshold) { // Адаптация стратегии мутации AdjustMutationStrategy(); } } }
É importante lembrar que restringir excessivamente os parâmetros pode reduzir a capacidade do algoritmo de explorar o espaço de soluções. Portanto, é necessário encontrar o equilíbrio correto entre controle e flexibilidade. Esses métodos podem ser aplicados em futuras pesquisas por entusiastas da otimização. No entanto, utilizei a função que desenvolvi, "GaussDistribution", descrita neste artigo.
//—————————————————————————————————————————————————————————————————————————————— void C_AO_ASBO::AdaptiveMutation (S_ASBO_Agent &ag) { ag.Cg *= MathExp (tau_prime * u.GaussDistribution (0, -1, 1, 1) + tau * u.GaussDistribution (0, -1, 1, 8)); ag.Cs *= MathExp (tau_prime * u.GaussDistribution (0, -1, 1, 1) + tau * u.GaussDistribution (0, -1, 1, 8)); ag.Cn *= MathExp (tau_prime * u.GaussDistribution (0, -1, 1, 1) + tau * u.GaussDistribution (0, -1, 1, 8)); } //——————————————————————————————————————————————————————————————————————————————
Como mostrado abaixo, nos resultados obtidos para os coeficientes Cg, Cs e Cn, após aplicar minha função, os valores elevados ocorrem com muito menos frequência do que com o uso do método Box-Muller.
0.025582051880112085 0.6207719272290446 0.005335225840354781
0.9810075068811726 0.16583946164135704 0.01016969794039794
0.006133031813953609 17.700790930206647 0.3745475117676483
1.4547663270710334 0.3537259667123157 0.08834618264409702
0.11125695415944291 0.28183794982955684 0.09051405673590024
0.06340035225180855 0.16270375413207716 0.36885953030567936
0.008575136469231127 2.5881627332149053 0.11237602809318312
0.00001436227841400286 0.02323530434501054 10.360403964016376
0.936476760121053 0.017321731852758693 0.40372788912091845
0.009288586536835293 0.0000072639468670123115 15.463139841665908
0.15092489031689466 0.02160456749606 0.011008504295160867
0.0100807047494077 0.4592091893288436 0.0343285901385665
0.010014652012224212 0.0014577046664934706 0.006484475820059919
0.0002654495048564554 0.0005018788250576451 1.8639207859646574
5.972802450172414 0.10070170017416721 0.9226557559293936
0.011441827382547332 14.599641192191408 0.00007257778906744059
0.7249805357484554 0.000004301248511125035 0.2718776654314797
5.019113547774523 0.11351424171113386 0.02129848352762841
0.023978285994614518 0.041738711812672386 1.0247944259605422
0.0036842456260203237 12.869472963773408 1.5167205157941646
0.4529181577133935 0.0000625576761842319 30.751931508050227
0.5555092369559338 0.9606330180338433 0.39426099685543164
0.036106836251057275 2.57811344513881 0.042016638784243526
3.502119772985753 128.0263928713568 0.9925745499516576
279.2236061102195 0.6837013166327449 0.01615639677602729
0.09687457825904996 0.3812813151133578 0.5272720937749686
Agora que compreendemos o conceito de Schwefel, bem como os valores adaptativos dos coeficientes, avancemos para o método de determinação dos vizinhos mais próximos no algoritmo. Para determinar as coordenadas dos vizinhos mais próximos, "Nc", é utilizada a seguinte abordagem:
1. Para cada indivíduo na população, determinam-se os seus vizinhos mais próximos.
2. Consideram-se como vizinhos mais próximos os três indivíduos com valores de função objetivo (fitness) mais próximos ao do indivíduo em questão.
3. As coordenadas do centro do grupo formado por esse indivíduo e seus vizinhos mais próximos são calculadas como a média aritmética das coordenadas desses três vizinhos.
Assim, as coordenadas "Nc" não são simplesmente escolhidas como três coordenadas aleatórias, mas calculadas como o centro do grupo dos vizinhos mais próximos em termos do valor da função objetivo. Isso permite utilizar informações sobre o entorno imediato do indivíduo para determinar sua próxima posição.
O ponto central é que os vizinhos mais próximos são determinados não pela proximidade geográfica, mas pela proximidade em valores da função objetivo. Isso corresponde a uma proximidade lógica, e não geográfica, na estrutura social.
//—————————————————————————————————————————————————————————————————————————————— void C_AO_ASBO::FindNeighborCenter (const S_ASBO_Agent &ag, double ¢er []) { // Создаем массивы для индексов и разниц фитнеса int indices []; double differences []; ArrayResize (indices, popSize - 1); ArrayResize (differences, popSize - 1); // Заполняем массивы int count = 0; for (int i = 0; i < popSize; i++) { if (&a [i] != &ag) // Исключаем текущего агента { indices [count] = i; differences [count] = MathAbs (a [i].fitness - ag.fitness); count++; } } // Сортируем массивы по разнице фитнеса (пузырьковая сортировка) for (int i = 0; i < count - 1; i++) { for (int j = 0; j < count - i - 1; j++) { if (differences [j] > differences [j + 1]) { // Обмен разниц double tempDiff = differences [j]; differences [j] = differences [j + 1]; differences [j + 1] = tempDiff; // Обмен индексов int tempIndex = indices [j]; indices [j] = indices [j + 1]; indices [j + 1] = tempIndex; } } } // Инициализируем центр ArrayInitialize (center, 0.0); // Вычисляем центр на основе трех ближайших соседей for (int j = 0; j < coords; j++) { for (int k = 0; k < 3; k++) { int nearestIndex = indices [k]; center [j] += a [nearestIndex].c [j]; } center [j] /= 3; } } //——————————————————————————————————————————————————————————————————————————————
Explicação do método:
- Determina os vizinhos mais próximos com base na proximidade dos valores da função objetivo (fitness).
- Utiliza três vizinhos mais próximos.
- Calcula o centro do grupo como a média aritmética das coordenadas desses três vizinhos.
Vamos analisar esta função:
1. Direção da ordenação:
A ordenação é feita em ordem crescente da diferença de fitness.
Se o elemento atual for maior que o próximo, eles trocam de lugar. Assim, após a ordenação, o array "differences" estará organizado do menor para o maior, e os índices correspondentes no array "indices" refletirão essa organização.
2. A função executa os seguintes passos:
- Exclui o agente atual da consideração.
- Calcula a diferença de fitness entre o agente atual e todos os outros.
- Ordena os agentes com base nessa diferença.
- Seleciona os três vizinhos mais próximos (com a menor diferença de fitness).
- Calcula o centro do grupo com base nas coordenadas desses três vizinhos.
void C_AO_ASBO::FindNeighborCenter(int ind, S_ASBO_Agent &ag[], double ¢er[]) { int indices[]; double differences[]; ArrayResize(indices, popSize - 1); ArrayResize(differences, popSize - 1); int count = 0; for (int i = 0; i < popSize; i++) { if (i != ind) { indices[count] = i; differences[count] = MathAbs(ag[ind].f - ag[i].f); count++; } } // Сортировка по возрастанию разницы фитнеса for (int i = 0; i < count - 1; i++) { for (int j = 0; j < count - i - 1; j++) { if (differences[j] > differences[j + 1]) { double tempDiff = differences[j]; differences[j] = differences[j + 1]; differences[j + 1] = tempDiff; int tempIndex = indices[j]; indices[j] = indices[j + 1]; indices[j + 1] = tempIndex; } } } ArrayInitialize(center, 0.0); int neighborsCount = MathMin(3, count); // Защита от случая, когда агентов меньше 3 for (int j = 0; j < coords; j++) { for (int k = 0; k < neighborsCount; k++) { int nearestIndex = indices[k]; center[j] += ag[nearestIndex].c[j]; } center[j] /= neighborsCount; } }
Essa versão da função é mais robusta contra erros e processa corretamente casos com um número reduzido (menos de três) de agentes na população. Após compreender os principais métodos lógicos do algoritmo, podemos avançar para sua estrutura. O algoritmo ASBO (Adaptive Social Behavior Optimization) consiste nos seguintes passos principais:
1. Inicialização:
- Define-se a população inicial de soluções, onde cada solução é representada explicitamente (não em formato codificado).
- Calcula-se o valor da função objetivo para cada solução, determinando sua adaptabilidade.
- A solução com o maior valor de adaptabilidade é declarada como o líder global no momento.
- Determina-se para cada solução o grupo de vizinhos mais próximos, com os maiores valores de adaptabilidade.
2. Mutação adaptativa dos parâmetros:
- Para cada solução, define-se um vetor de três parâmetros adaptativos: Cg, Cs e Cn, que representam a influência do líder global, da melhor solução pessoal e do centro do grupo de vizinhos, respectivamente.
- Aplica-se a estratégia de mutação autoadaptativa de Schwefel para atualizar os valores desses parâmetros.
3. Atualização da posição das soluções:
- Para cada solução, calcula-se a alteração em sua posição utilizando os valores atuais dos parâmetros Cg, Cs e Cn.
- A nova posição da solução é calculada somando a alteração à posição atual.
4. Processo bifásico:
- Fase 1: são criadas M populações independentes, cada uma processada pelo algoritmo ASBO por um número fixo de iterações. Os valores de adaptabilidade e os parâmetros de cada solução nas populações finais são armazenados.
- Fase 2: entre todas as populações finais, selecionam-se as melhores soluções em termos de adaptabilidade, e seus parâmetros são usados para formar uma nova população. Essa nova população é submetida à lógica principal do algoritmo ASBO para gerar a solução final.
Destacamos as principais características do algoritmo ASBO:
- Aplicação de uma estratégia de mutação autoadaptativa para atualização dinâmica dos parâmetros.
- Uso de conceitos de liderança e influência de vizinhos para modelar o comportamento social.
- Processo bifásico para manter a diversidade das soluções e acelerar a convergência.
Neste artigo, exploramos o conceito de Schwefel, o método Box-Muller, que utiliza a distribuição normal, a aplicação de coeficientes de mutação autoadaptativos e a função de determinação de vizinhos mais próximos com base na adaptabilidade. Abordamos a estrutura do ASBO e, no próximo artigo, analisaremos detalhadamente o processo bifásico de evolução artificial, concluiremos a formação do algoritmo, realizaremos testes com funções de referência e apresentaremos considerações sobre sua eficácia.
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/15283
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