Algoritmo de otimização multi-enxame em Java
1. Introdução
Neste artigo, daremos uma olhada em um algoritmo de otimização Multi-swarm. Como outros algoritmos da mesma classe, seu objetivo é encontrar a melhor solução para um problema, maximizando ou minimizando uma função específica, denominada função de condicionamento físico.
Vamos começar com alguma teoria.
2. Como funciona a otimização Multi-Swarm
O Multi-swarm é uma variação do algoritmoSwarm. Como o nome sugere,the Swarm algorithm solves a problem by simulating the movement of a group of objects in the space of possible solutions. Na versão multi-enxame, existem vários enxames em vez de apenas um.
O componente básico de um enxame é chamado de partícula. A partícula é definida por sua posição atual, que também é uma solução possível para o nosso problema, e sua velocidade, usada para calcular a próxima posição.
A velocidade da partícula muda constantemente, inclinando-se para a melhor posição encontrada entre todas as partículas em todos os enxames com um certo grau de aleatoriedade para aumentar a quantidade de espaço coberto.
Em última análise, isso leva a maioria das partículas a um conjunto finito de pontos que são mínimos ou máximos locais na função de adequação, dependendo se estamos tentando minimizar ou maximizar.
Embora o ponto encontrado seja sempre um mínimo ou máximo local da função, não é necessariamente global, uma vez que não há garantia de que o algoritmo explorou completamente o espaço de soluções.
Por esta razão, o multi-enxame é considerado uma metaheurística -the solutions it finds are among the best, but they may not be the absolute best.
3. Implementação
Agora que sabemos o que é um enxame múltiplo e como funciona, vamos dar uma olhada em como implementá-lo.
Para nosso exemplo, vamos tentar resolverthis real-life optimization problem posted on StackExchange:
Em League of Legends, a Vida Efetiva de um jogador ao se defender contra danos físicos é dada porE=H(100+A)/100, ondeH é a saúde eA é a armadura.
Saúde custa 2,5 ouro por unidade e Armadura custa 18 ouro por unidade. Você tem 3600 ouro e precisa otimizar a eficáciaE de sua saúde e armadura para sobreviver o maior tempo possível contra os ataques do time inimigo. Quanto de cada um você deve comprar?
3.1. Partícula
Começamos modelando nossa construção básica, uma partícula. O estado de uma partícula inclui sua posição atual, que é um par de valores de saúde e armadura que resolvem o problema, a velocidade da partícula nos dois eixos e a pontuação de aptidão da partícula.
Também armazenaremos a melhor posição e pontuação de condicionamento físico que encontrarmos, pois precisaremos deles para atualizar a velocidade das partículas:
public class Particle {
private long[] position;
private long[] speed;
private double fitness;
private long[] bestPosition;
private double bestFitness = Double.NEGATIVE_INFINITY;
// constructors and other methods
}
Escolhemos usar matrizeslong para representar a velocidade e a posição porque podemos deduzir a partir da declaração do problema que não podemos comprar frações de armadura ou saúde, portanto, a solução deve estar no domínio inteiro.
Não queremos usarint porque isso pode causar problemas de estouro durante os cálculos.
3.2. Enxame
Em seguida, vamos definir um enxame como uma coleção de partículas. Mais uma vez, também armazenaremos a melhor posição histórica e pontuação para cálculos posteriores.
O enxame também precisará cuidar da inicialização de suas partículas, atribuindo uma posição e velocidade iniciais aleatórias a cada uma.
Como podemos estimar aproximadamente um limite para a solução, adicionamos esse limite ao gerador de números aleatórios.
Isso reduzirá a potência computacional e o tempo necessário para executar o algoritmo:
public class Swarm {
private Particle[] particles;
private long[] bestPosition;
private double bestFitness = Double.NEGATIVE_INFINITY;
public Swarm(int numParticles) {
particles = new Particle[numParticles];
for (int i = 0; i < numParticles; i++) {
long[] initialParticlePosition = {
random.nextInt(Constants.PARTICLE_UPPER_BOUND),
random.nextInt(Constants.PARTICLE_UPPER_BOUND)
};
long[] initialParticleSpeed = {
random.nextInt(Constants.PARTICLE_UPPER_BOUND),
random.nextInt(Constants.PARTICLE_UPPER_BOUND)
};
particles[i] = new Particle(
initialParticlePosition, initialParticleSpeed);
}
}
// methods omitted
}
3.3. Multiswarm
Finalmente, vamos concluir nosso modelo criando uma classe Multiswarm.
Da mesma forma que o enxame, vamos acompanhar uma coleção de enxames e a melhor posição de partícula e aptidão encontrada entre todos os enxames.
Também armazenaremos uma referência à função de fitness para uso posterior:
public class Multiswarm {
private Swarm[] swarms;
private long[] bestPosition;
private double bestFitness = Double.NEGATIVE_INFINITY;
private FitnessFunction fitnessFunction;
public Multiswarm(
int numSwarms, int particlesPerSwarm, FitnessFunction fitnessFunction) {
this.fitnessFunction = fitnessFunction;
this.swarms = new Swarm[numSwarms];
for (int i = 0; i < numSwarms; i++) {
swarms[i] = new Swarm(particlesPerSwarm);
}
}
// methods omitted
}
3.4. Função Fitness
Vamos agora implementar a função de fitness.
Para desacoplar a lógica do algoritmo deste problema específico, apresentaremos uma interface com um único método.
Este método toma uma posição de partícula como argumento e retorna um valor indicando como é bom:
public interface FitnessFunction {
public double getFitness(long[] particlePosition);
}
Desde que o resultado encontrado seja válido de acordo com as restrições do problema, medir a adequação é apenas uma questão de retornar a saúde efetiva calculada que queremos maximizar.
Para o nosso problema, temos as seguintes restrições de validação específicas:
-
soluções devem ser apenas números inteiros positivos
-
soluções devem ser viáveis com a quantidade fornecida de ouro
Quando uma dessas restrições é violada, retornamos um número negativo que indica a que distância estamos do limite de validade.
Este é o número encontrado no primeiro caso ou a quantidade de ouro indisponível no último:
public class LolFitnessFunction implements FitnessFunction {
@Override
public double getFitness(long[] particlePosition) {
long health = particlePosition[0];
long armor = particlePosition[1];
if (health < 0 && armor < 0) {
return -(health * armor);
} else if (health < 0) {
return health;
} else if (armor < 0) {
return armor;
}
double cost = (health * 2.5) + (armor * 18);
if (cost > 3600) {
return 3600 - cost;
} else {
long fitness = (health * (100 + armor)) / 100;
return fitness;
}
}
}
3.5. Loop principal
O programa principal iterará entre todas as partículas em todos os enxames e fará o seguinte:
-
calcular a aptidão das partículas
-
se uma nova melhor posição foi encontrada, atualize o histórico de partículas, enxame e multiswarm
-
calcular a nova posição da partícula adicionando a velocidade atual a cada dimensão
-
calcular a nova velocidade das partículas
Por enquanto, deixaremos a velocidade de atualização para a próxima seção, criando um método dedicado:
public void mainLoop() {
for (Swarm swarm : swarms) {
for (Particle particle : swarm.getParticles()) {
long[] particleOldPosition = particle.getPosition().clone();
particle.setFitness(fitnessFunction.getFitness(particleOldPosition));
if (particle.getFitness() > particle.getBestFitness()) {
particle.setBestFitness(particle.getFitness());
particle.setBestPosition(particleOldPosition);
if (particle.getFitness() > swarm.getBestFitness()) {
swarm.setBestFitness(particle.getFitness());
swarm.setBestPosition(particleOldPosition);
if (swarm.getBestFitness() > bestFitness) {
bestFitness = swarm.getBestFitness();
bestPosition = swarm.getBestPosition().clone();
}
}
}
long[] position = particle.getPosition();
long[] speed = particle.getSpeed();
position[0] += speed[0];
position[1] += speed[1];
speed[0] = getNewParticleSpeedForIndex(particle, swarm, 0);
speed[1] = getNewParticleSpeedForIndex(particle, swarm, 1);
}
}
}
3.6. Atualização de velocidade
É essencial que a partícula mude sua velocidade, pois é assim que ela consegue explorar diferentes soluções possíveis.
A velocidade da partícula precisará fazer com que a partícula se mova para a melhor posição encontrada por si mesma, por seu enxame e por todos os enxames, atribuindo um certo peso a cada um deles. Chamaremos esses pesos decognitive weight, social weight and global weight, respectivamente.
Para adicionar alguma variação, vamos multiplicar cada um desses pesos por um número aleatório entre 0 e 1. We’ll also add an inertia factor to the formula que incentiva a partícula a não desacelerar muito:
private int getNewParticleSpeedForIndex(
Particle particle, Swarm swarm, int index) {
return (int) ((Constants.INERTIA_FACTOR * particle.getSpeed()[index])
+ (randomizePercentage(Constants.COGNITIVE_WEIGHT)
* (particle.getBestPosition()[index] - particle.getPosition()[index]))
+ (randomizePercentage(Constants.SOCIAL_WEIGHT)
* (swarm.getBestPosition()[index] - particle.getPosition()[index]))
+ (randomizePercentage(Constants.GLOBAL_WEIGHT)
* (bestPosition[index] - particle.getPosition()[index])));
}
Os valores aceitos para inércia, pesos cognitivos, sociais e globais são 0,729, 1,49445, 1,49445 e 0,3645, respectivamente.
4. Conclusão
Neste tutorial, analisamos a teoria e a implementação de um algoritmo de enxame. Também vimos como projetar uma função de condicionamento físico de acordo com um problema específico.
Se você quiser ler mais sobre este tópico, dê uma olhada emthis bookethis article, que também foram usados como fontes de informação para este artigo.
Como sempre, todo o código do exemplo está disponívelover on the GitHub project.