Introdução à Biblioteca Jenetics

Introdução à Biblioteca Jenetics

1. Introdução

The aim of this series é para explicar a ideia de algoritmos genéticos e mostrar as implementações mais conhecidas.

Neste tutorial, vamosdescribe a very powerful Jenetics Java library that can be used for solving various optimization problems.

Se você acha que precisa aprender mais sobre algoritmos genéticos, recomendamos começar comthis article.

2. Como funciona?

De acordo com seuofficial documents, Jenetics é uma biblioteca baseada em um algoritmo evolutivo escrito em Java. Os algoritmos evolutivos têm suas raízes na biologia, pois usam mecanismos inspirados na evolução biológica, como reprodução, mutação, recombinação e seleção.

Jenetics é implementado usando a interface JavaStream, portanto, funciona perfeitamente com o resto da API JavaStream.

As principais características são:

  • frictionless minimization - não há necessidade de alterar ou ajustar a função de aptidão; podemos apenas alterar a configuração da classeEngine e estamos prontos para iniciar nosso primeiro aplicativo

  • dependency free - não há bibliotecas de terceiros em tempo de execução necessárias para usar o Jenetics

  • Java 8 ready - suporte total paraStreame expressões lambda

  • multithreaded - etapas evolutivas podem ser executadas em paralelo

Para usar o Jenetics, precisamos adicionar a seguinte dependência em nossopom.xml:


    io.jenetics
    jenetics
    3.7.0

A versão mais recente pode ser encontradain Maven Central.

3. Casos de Uso

Para testar todos os recursos do Jenetics, vamos tentar resolver vários problemas de otimização bem conhecidos, começando com o algoritmo binário simples e terminando com o problema da mochila.

3.1. Algoritmo Genético Simples

Vamos supor que precisamos resolver o problema binário mais simples, onde precisamos otimizar as posições dos bits 1 no cromossomo consistindo em 0 e 1. Primeiro, precisamos definir a fábrica adequada para o problema:

Factory> gtf = Genotype.of(BitChromosome.of(10, 0.5));

Criamos oBitChromosome com um comprimento de 10 e a probabilidade de ter 1 no cromossomo igual a 0,5.

Agora, vamos criar o ambiente de execução:

Engine engine
  = Engine.builder(SimpleGeneticAlgorithm::eval, gtf).build();

O métodoeval() retorna a contagem de bits:

private Integer eval(Genotype gt) {
    return gt.getChromosome().as(BitChromosome.class).bitCount();
}

Na etapa final, iniciamos a evolução e coletamos os resultados:

Genotype result = engine.stream()
  .limit(500)
  .collect(EvolutionResult.toBestGenotype());

O resultado final será semelhante a este:

Before the evolution:
[00000010|11111100]
After the evolution:
[00000000|11111111]

Conseguimos otimizar a posição de 1 no gene.

3.2. Problema de soma de subconjunto

Outro caso de uso para Jenetics é resolver osubset sum problem. Em resumo, o desafio de otimizar é que, dado um conjunto de números inteiros, precisamos encontrar um subconjunto não vazio cuja soma é zero.

Existem interfaces predefinidas no Jenetics para resolver esses problemas:

public class SubsetSum implements Problem, EnumGene, Integer> {
    // implementation
}

Como podemos ver, implementamos oProblem<T, G, C>, que possui três parâmetros:

  • <T> - o tipo de argumento da função de adequação do problema, em nosso caso, uma sequênciaInteger imutável, ordenada e de tamanho fixoISeq<Integer>

  • <G> - o tipo de gene com o qual o mecanismo de evolução está trabalhando, neste caso,Integer genes contáveisEnumGene<Integer>

  • <C> - o tipo de resultado da função de adequação; aqui está umInteger

Para usar a interfaceProblem<T, G, C>, precisamos substituir dois métodos:

@Override
public Function, Integer> fitness() {
    return subset -> Math.abs(subset.stream()
      .mapToInt(Integer::intValue).sum());
}

@Override
public Codec, EnumGene> codec() {
    return codecs.ofSubSet(basicSet, size);
}

No primeiro, definimos nossa função de adequação, enquanto o segundo é uma classe que contém métodos de fábrica para criar codificações de problemas comuns, por exemplo, para encontrar o melhor subconjunto de tamanho fixo de um determinado conjunto básico, como no nosso caso.

Agora podemos avançar para a parte principal. No começo, precisamos criar um subconjunto para usar no problema:

SubsetSum problem = of(500, 15, new LCG64ShiftRandom(101010));

Observe que estamos usando o geradorLCG64ShiftRandom fornecido pelo Jenetics. Na próxima etapa, estamos construindo o mecanismo de nossa solução:

Na próxima etapa, estamos construindo o mecanismo de nossa solução:

Engine, Integer> engine = Engine.builder(problem)
  .minimizing()
  .maximalPhenotypeAge(5)
  .alterers(new PartiallyMatchedCrossover<>(0.4), new Mutator<>(0.3))
  .build();

Tentamos minimizar o resultado (otimamente o resultado será 0) definindo a idade do fenótipo e os alteradores usados ​​para alterar a prole. Na próxima etapa, podemos obter o resultado:

Phenotype, Integer> result = engine.stream()
  .limit(limit.bySteadyFitness(55))
  .collect(EvolutionResult.toBestPhenotype());

Observe que estamos usandobySteadyFitness() que retorna um predicado, que truncará o fluxo de evolução se nenhum fenótipo melhor puder ser encontrado após um determinado número de gerações e coletar o melhor resultado. Se tivermos sorte e houver uma solução para o conjunto criado aleatoriamente, veremos algo semelhante a isto:

Se tivermos sorte e houver uma solução para o conjunto criado aleatoriamente, veremos algo semelhante a isto:

[85|-76|178|-197|91|-106|-70|-243|-41|-98|94|-213|139|238|219] --> 0

Caso contrário, a soma do subconjunto será diferente de 0.

3.3. Problema do primeiro ajuste da mochila

A biblioteca Jenetics nos permite resolver problemas ainda mais sofisticados, como oKnapsack problem. Resumidamente, neste problema, temos um espaço limitado em nossa mochila e precisamos decidir quais itens colocar dentro.

Vamos começar definindo o tamanho da sacola e o número de itens:

int nItems = 15;
double ksSize = nItems * 100.0 / 3.0;

Na próxima etapa, geraremos um array aleatório contendo objetosKnapsackItem (definidos pelos campossizeevalue) e colocaremos esses itens aleatoriamente dentro da mochila, usando o primeiro Método de ajuste:

KnapsackFF ff = new KnapsackFF(Stream.generate(KnapsackItem::random)
  .limit(nItems)
  .toArray(KnapsackItem[]::new), ksSize);

Em seguida, precisamos criar oEngine:

Engine engine
  = Engine.builder(ff, BitChromosome.of(nItems, 0.5))
  .populationSize(500)
  .survivorsSelector(new TournamentSelector<>(5))
  .offspringSelector(new RouletteWheelSelector<>())
  .alterers(new Mutator<>(0.115), new SinglePointCrossover<>(0.16))
  .build();

Há alguns pontos a serem observados aqui:

  • tamanho da população é 500

  • a prole será escolhida nas seleções do torneio e da roleta

  • como fizemos na subseção anterior, precisamos também definir os alteradores para a prole recém-criada

There is also one very important feature of Jenetics. We can easily collect all statistics and insights from the whole simulation duration. Faremos isso usando a classeEvolutionStatistics:

EvolutionStatistics statistics = EvolutionStatistics.ofNumber();

Finalmente, vamos rodar as simulações:

Phenotype best = engine.stream()
  .limit(bySteadyFitness(7))
  .limit(100)
  .peek(statistics)
  .collect(toBestPhenotype());

Observe que estamos atualizando as estatísticas de avaliação após cada geração, que são limitadas a 7 gerações contínuas e no máximo 100 gerações no total. Mais detalhadamente, existem dois cenários possíveis:

  • atingimos 7 gerações constantes, então a simulação para

  • não podemos obter 7 gerações constantes em menos de 100 gerações, então a simulação para devido ao segundolimit()

É importante ter um limite máximo de gerações, caso contrário, as simulações podem não parar em um tempo razoável.

O resultado final contém muitas informações:

+---------------------------------------------------------------------------+
|  Time statistics                                                          |
+---------------------------------------------------------------------------+
|             Selection: sum=0,039207931000 s; mean=0,003267327583 s        |
|              Altering: sum=0,065145069000 s; mean=0,005428755750 s        |
|   Fitness calculation: sum=0,029678433000 s; mean=0,002473202750 s        |
|     Overall execution: sum=0,111383965000 s; mean=0,009281997083 s        |
+---------------------------------------------------------------------------+
|  Evolution statistics                                                     |
+---------------------------------------------------------------------------+
|           Generations: 12                                                 |
|               Altered: sum=7 664; mean=638,666666667                      |
|                Killed: sum=0; mean=0,000000000                            |
|              Invalids: sum=0; mean=0,000000000                            |
+---------------------------------------------------------------------------+
|  Population statistics                                                    |
+---------------------------------------------------------------------------+
|                   Age: max=10; mean=1,792167; var=4,657748                |
|               Fitness:                                                    |
|                      min  = 0,000000000000                                |
|                      max  = 716,684883338605                              |
|                      mean = 587,012666759785                              |
|                      var  = 17309,892287851708                            |
|                      std  = 131,567063841418                              |
+---------------------------------------------------------------------------+

Dessa vez, conseguimos colocar itens com um valor total de 716,68 no melhor cenário. Também podemos ver as estatísticas detalhadas da evolução e do tempo.

Como testar?

É um processo bastante simples - basta abrir o arquivo principal relacionado ao problema e primeiro executar o algoritmo. Depois de termos uma ideia geral, podemos começar a brincar com os parâmetros.

4. Conclusão

Neste artigo, abordamos os recursos da biblioteca Jenetics com base em problemas reais de otimização.

O código está disponível como um projeto Maven emGitHub. Observe que fornecemos os exemplos de código para mais desafios de otimização, comoSpringsteen Record (sim, existe!) E problemas do caixeiro viajante.

Para todos os artigos da série, incluindo outros exemplos de algoritmos genéticos, confira os seguintes links: