Olá pessoal, tudo certo? No post de hoje temos um desafio interessante para um algoritmo evolucionário: dado uma determinada imagem e um número fixo de polígonos semi-transparentes, será que conseguimos aproximar a imagem de maneira satisfatória, em um tempo razoável? Como você deve ter adivinhado, a resposta é sim, podemos! Observe as imagens abaixo:
Após 2386 gerações:
Imagem original:
Nas imagens acima vemos que mesmo com poucas gerações, obtemos bons resultados. Por exemplo, a imagem do centro (linha 2, coluna 2) foi formada em 8094 gerações, tomando pouco mais de 5 minutos de execução no meu MacBook Pro (2.4 GHz, 4 GB). Neste post vou mostrar como fazer isso da maneira mais simples possível, então mãos a obra!
Estratégia
A minha primeira tentativa para resolver esse problema foi usar uma GA simples, e os resultados não foram muito animadores: os polígonos demoravam muito para evoluir, e mesmo assim as imagens resultantes não eram muito boas. Provavelmente o crossover atua deteriorando os melhores indivíduos, o que torna a evolução muito lenta ou quase impossível. Claro, bastou eliminar o crossover para obter resultados muito melhores.
Como estratégia, vamos usar uma espécie de 'Hill Climbing estocástico', de acordo com o pseudo-código abaixo:
solucao := cria cromossomo randômico
repete
novaSolucao := mutaciona(criaCopia(solucao))
se novaSolucao.qualidade > solucao.qualidade
solucao := novaSolucao
ate atingir a qualidade desejada
retorna solucao
A qualidade de cada solucao é simplesmente uma comparação pixel a pixel da imagem alvo e da solução atual. Apesar de não ser muito eficiente (depende do tamanho da imagem), essa foi a maneira mais simples que encontrei.
Para facilitar o decoding dos cromossomos, fixamos um valor V para o número de vértices de cada polígono. Quanto as cores, usaremos os 4 componentes RGBA e restringimos o valor de alpha numa determinada faixa [alphMin, alphaMax], o que nos permite 'cortar' um pouco o espaço de busca. Dessa forma, representamos um polígono de V vértices como poly(V) = [x1, y1, x2, y2, ..., xv, yv, red, green, blue, alpha].
Um cromossomo é apenas uma sequência de polígonos: cromo(N, V) = [poly1(V), poly2(V), ..., polyN(V)]. Como de costume, usaremos uma string de 1's e 0's para codificar nosso problema. Vamos começar criando uma classe que faça esse encoding/decoding, continue lendo!
Classe BinaryIntChromosome
O papel dessa classe é simples: codificar um vetor de inteiros em um vetor de booleans, e ter a capacidade de fazer a operação reversa: decodificar o vetor de booleans em um vetor de inteiros. No construtor passamos os vetores minVals e maxVals, que representam respectivamente os valores mínimos e máximos que as variáveis podem assumir, e opcionalmente passamos um objeto java.util.Random, que irá percorrer o cromossomo e setar os bits de maneira randômica.
A 'qualidade' de cada cromossomo é representada pela variável de instância fitness, e obviamente é setada pelo cliente da classe.O método mutatepercorre o cromossomo e inverte cada bit com a probabilidade fornecida. Por fim, o método decodefaz o que realmente interessa: decodifica o cromossomo e seta a variável de instância fenotype, que representa as variáveis do problema em questão. Segue a classe:
import java.util.Random;
public class BinaryIntChromosome implements Comparable<BinaryIntChromosome>
{
protected int[] minVals;
protected int[] maxVals;
protected int[] bitsPerVar;
protected boolean[] genotype;
protected int[] fenotype;
protected float fitness;
protected int totalBits;
public BinaryIntChromosome(int[] minVals, int[] maxVals, Random random)
{
this.minVals = minVals;
this.maxVals = maxVals;
this.bitsPerVar = new int[minVals.length];
for (int i = 0; i < minVals.length; i++)
{
int nBits = maxVals[i] - minVals[i] + 1;
double bits = Math.log(nBits)/Math.log(2);
bitsPerVar[i] = (int) Math.ceil(bits);
this.totalBits += bitsPerVar[i];
}
this.genotype = new boolean[totalBits];
this.fenotype = new int[minVals.length];
if (random != null)
this.randomizeGenotype(random);
}
protected void randomizeGenotype(Random random)
{
for (int i = 0; i < totalBits; i++)
genotype[i] = random.nextBoolean();
}
protected static int binToDec(boolean[] string, int startIndex, int endIndex)
{
int val = 0;
int slot = 0;
for (int i = startIndex; i <= endIndex; i++)
{
if (string[i])
val += (int) Math.pow(2, slot);
slot++;
}
return val;
}
protected void mutate(Random random, float mutationRate)
{
for (int i = 0; i < genotype.length; i++)
{
float f = random.nextFloat();
if (f < mutationRate)
genotype[i] = !genotype[i];
}
}
protected BinaryIntChromosome duplicate()
{
int[] lb = new int[minVals.length];
int[] ub = new int[maxVals.length];
for (int i = 0; i < minVals.length; i++)
{
lb[i] = minVals[i];
ub[i] = maxVals[i];
}
BinaryIntChromosome theCopy = new BinaryIntChromosome(lb, ub, null);
for (int i = 0; i < this.genotype.length; i++)
theCopy.genotype[i] = this.genotype[i];
for (int i = 0; i < this.fenotype.length; i++)
theCopy.fenotype[i] = this.fenotype[i];
return theCopy;
}
protected void decode()
{
int bitIndex = 0;
for (int var = 0; var < bitsPerVar.length; var++)
{
int bitsToRead = bitsPerVar[var];
int startIndex = bitIndex;
int endIndex = startIndex + bitsToRead - 1;
int decVal = BinaryIntChromosome.binToDec(this.genotype, startIndex, endIndex);
int n = (int)(Math.pow(2, bitsToRead) - 1);
int range = maxVals[var] - minVals[var];
this.fenotype[var] = this.minVals[var] + (range * decVal) / n;
bitIndex += bitsToRead;
}
}
public int compareTo(BinaryIntChromosome other)
{
if (this.fitness > other.fitness)
return 1;
else if (this.fitness < other.fitness)
return -1;
return 0;
}
}
Interface FitnessDelegate
Essa interface define o método getFitness, onde a qualidade do indivíduo será determinada passando-se o fenótipo como argumento:
public interface FitnessDelegate
{
float getFitness(int[] fenotype);
}
Classe HillClimb
Agora que já sabemos como codificar/decodificar nossas variáveis, é uma boa hora para implementar o HC. O construtor da classe recebe os vetores de mínimo e máximo e também a taxa de mutação que será aplicada ao cromossomo. O objeto passado em delegate implementa a interface FitnessDelegate, sendo responsável pelo cálculo de fitness.
O método evolve simplesmente avança uma geração, podendo ser executado em uma Thread separada e notificar outras Threads, caso necessário. Por fim, getBest nos retorna a melhor solução obtida até então, que no caso do HC é sempre igual a solução atual, já que se trata de um algoritmo 'greedy'. Segue a classe:
import java.util.Random;
import java.util.concurrent.CountDownLatch;
public class HillClimb implements Runnable
{
protected BinaryIntChromosome current;
protected BinaryIntChromosome best;
protected float mutationRate;
protected FitnessDelegate delegate;
protected Random random = new Random();
protected CountDownLatch signal;
public HillClimb(int[] lower, int[] upper, FitnessDelegate delegate, float mutationRate, CountDownLatch signal, boolean randomizeStart)
{
this.delegate = delegate;
this.mutationRate = mutationRate;
this.signal = signal;
current = new BinaryIntChromosome(lower, upper, randomizeStart ? random : null);
current.decode();
current.fitness = delegate.getFitness(this.current.fenotype);
best = current;
}
public void run()
{
evolve();
if (signal != null)
signal.countDown();
}
public void evolve()
{
BinaryIntChromosome copyIndiv = current.duplicate();
copyIndiv.mutate(random, mutationRate);
copyIndiv.decode();
copyIndiv.fitness = delegate.getFitness(copyIndiv.fenotype);
if (copyIndiv.fitness > current.fitness)
{
current = copyIndiv;
best = copyIndiv;
}
}
public BinaryIntChromosome getBest()
{
return best;
}
}
Testando
Para finalizar este post, vamos testar as duas classes escritas e maximizar a função y = (x5 * x4 * x3) / (x2 * x1 * x0), cujo domínio está entre xmin = [1, 2, 3, 4, 5, 6] e xmax = [10, 20, 30, 40, 50, 60]. Como você deve ter notado, a solução é trivial: S = [1, 2, 3, 40, 50, 60], porém o computador não sabe disso!
public class TestHC
{
public static void main (String[] args)
{
int[] min = new int[]{1, 2, 3, 4, 5, 6};
int[] max = new int[]{10, 20, 30, 40, 50, 60};
float mutationRate = 0.05f;
int generations = 600;
FitnessDelegate delegate = new FitnessDelegate()
{
public float getFitness(int[] fenotype)
{
return (1.0f * fenotype[5] * fenotype[4] * fenotype[3]) / (fenotype[2] * fenotype[1] * fenotype[0]);
}
};
HillClimb hc = new HillClimb(min, max, delegate, mutationRate, null, true);
for (int i = 0; i < generations; i++)
hc.evolve();
BinaryIntChromosome best = hc.getBest();
System.out.println("Best solution: ");
for (int i = 0; i < best.fenotype.length; i++)
System.out.println("x[" + i + "] : " + best.fenotype[i]);
}
}
Na próximo post colocaremos os polígonos a prova, até mais!










Nenhum comentário:
Postar um comentário