Introduction à la bibliothèque Jenetics

Introduction à la bibliothèque de Jenetics

1. introduction

The aim of this series est d'expliquer l'idée d'algorithmes génétiques et de montrer les implémentations les plus connues.

Dans ce tutoriel, nous allonsdescribe a very powerful Jenetics Java library that can be used for solving various optimization problems.

Si vous pensez avoir besoin d'en savoir plus sur les algorithmes génétiques, nous vous recommandons de commencer parthis article.

2. Comment ça marche?

Selon sesofficial documents, Jenetics est une bibliothèque basée sur un algorithme évolutif écrit en Java. Les algorithmes évolutifs ont leurs racines dans la biologie, car ils utilisent des mécanismes inspirés par l'évolution biologique, tels que la reproduction, la mutation, la recombinaison et la sélection.

Jenetics est implémenté à l'aide de l'interface JavaStream, donc il fonctionne correctement avec le reste de l'API JavaStream.

Les principales caractéristiques sont:

  • frictionless minimization - il n'est pas nécessaire de modifier ou d'ajuster la fonction de remise en forme; nous pouvons simplement changer la configuration de la classeEngine et nous sommes prêts à démarrer notre première application

  • dependency free - il n'y a pas de bibliothèques tierces d'exécution nécessaires pour utiliser Jenetics

  • Java 8 ready - prise en charge complète des expressionsStream et lambda

  • multithreaded - les étapes évolutives peuvent être exécutées en parallèle

Pour utiliser Jenetics, nous devons ajouter la dépendance suivante dans nospom.xml:


    io.jenetics
    jenetics
    3.7.0

La dernière version peut être trouvéein Maven Central.

3. Cas d'utilisation

Pour tester toutes les fonctionnalités de Jenetics, nous allons essayer de résoudre divers problèmes d'optimisation bien connus, en commençant par l'algorithme binaire simple et en terminant par le problème Knapsack.

3.1. Algorithme génétique simple

Supposons que nous ayons besoin de résoudre le problème binaire le plus simple, où nous devons optimiser les positions des bits 1 dans le chromosome constitué de 0 et de 1. Tout d’abord, nous devons définir l’usine adaptée au problème:

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

Nous avons créé lesBitChromosome avec une longueur de 10, et la probabilité d'avoir des 1 dans le chromosome égale à 0,5.

Maintenant, créons l'environnement d'exécution:

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

La méthodeeval() renvoie le nombre de bits:

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

Dans la dernière étape, nous commençons l’évolution et rassemblons les résultats:

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

Le résultat final ressemblera à ceci:

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

Nous avons réussi à optimiser la position des 1 dans le gène.

3.2. Problème de somme de sous-ensemble

Un autre cas d'utilisation de Jenetics est de résoudre lessubset sum problem. En résumé, le défi à optimiser est que, pour un ensemble d’entiers, nous devons trouver un sous-ensemble non vide dont la somme est égale à zéro.

Jenetics propose des interfaces prédéfinies pour résoudre ces problèmes:

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

Comme nous pouvons le voir, nous implémentons leProblem<T, G, C>, qui a trois paramètres:

  • <T> - le type d'argument de la fonction de remise en forme du problème, dans notre cas une séquenceInteger de taille fixe immuable, ordonnée et fixeISeq<Integer>

  • <G> - le type de gène avec lequel le moteur d'évolution travaille, dans ce cas, les gènesInteger dénombrablesEnumGene<Integer>

  • <C> - le type de résultat de la fonction de fitness; ici c'est unInteger

Pour utiliser l'interfaceProblem<T, G, C>, nous devons remplacer deux méthodes:

@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);
}

Dans la première, nous définissons notre fonction de fitness, tandis que la seconde est une classe contenant des méthodes fabriques pour créer des codages de problèmes communs, par exemple, pour trouver le meilleur sous-ensemble de taille fixe à partir d'un ensemble de base donné, comme dans notre cas.

Nous pouvons maintenant passer à la partie principale. Au début, nous devons créer un sous-ensemble à utiliser dans le problème:

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

Veuillez noter que nous utilisons le générateurLCG64ShiftRandom fourni par Jenetics. Dans la prochaine étape, nous construisons le moteur de notre solution:

Dans la prochaine étape, nous construisons le moteur de notre solution:

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

Nous essayons de minimiser le résultat (de manière optimale, le résultat sera 0) en définissant l'âge du phénotype et les modificateurs utilisés pour modifier la progéniture. Dans l'étape suivante, nous pouvons obtenir le résultat:

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

Veuillez noter que nous utilisonsbySteadyFitness() qui retourne un prédicat, qui tronquera le flux d'évolution si aucun meilleur phénotype n'a pu être trouvé après le nombre de générations donné et collectera le meilleur résultat. Si nous avons de la chance et qu'il existe une solution à l'ensemble créé au hasard, nous verrons quelque chose de similaire à ceci:

Si nous avons de la chance et qu'il existe une solution à l'ensemble créé au hasard, nous verrons quelque chose de similaire à ceci:

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

Sinon, la somme du sous-ensemble sera différente de 0.

3.3. Problème de premier ajustement du sac à dos

La bibliothèque Jenetics nous permet de résoudre des problèmes encore plus sophistiqués, tels que lesKnapsack problem. En bref, dans ce problème, nous avons un espace limité dans notre sac à dos et nous devons décider quels articles mettre à l’intérieur.

Commençons par définir la taille du sac et le nombre d'articles:

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

Dans l'étape suivante, nous allons générer un tableau aléatoire contenant les objetsKnapsackItem (définis par les champssize etvalue) et nous placerons ces éléments au hasard dans le sac à dos, en utilisant le Méthode d'ajustement:

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

Ensuite, nous devons créer lesEngine:

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();

Il y a quelques points à noter ici:

  • la taille de la population est de 500

  • la progéniture sera choisie parmi les sélections du tournoi et de la roue de roulette

  • comme nous l'avons fait dans la sous-section précédente, nous devons également définir les alternateurs pour la nouvelle progéniture

There is also one very important feature of Jenetics. We can easily collect all statistics and insights from the whole simulation duration. Nous allons faire cela en utilisant la classeEvolutionStatistics:

EvolutionStatistics statistics = EvolutionStatistics.ofNumber();

Enfin, exécutons les simulations:

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

Veuillez noter que nous mettons à jour les statistiques d’évaluation après chaque génération, ce qui est limité à 7 générations permanentes et à un maximum de 100 générations au total. Plus en détail, deux scénarios sont possibles:

  • nous avons 7 générations stables, puis la simulation s'arrête

  • nous ne pouvons pas obtenir 7 générations stables en moins de 100 générations, donc la simulation s'arrête en raison du deuxièmelimit()

Il est important de limiter le nombre maximum de générations, sinon les simulations risquent de ne pas s'arrêter dans un délai raisonnable.

Le résultat final contient beaucoup d'informations:

+---------------------------------------------------------------------------+
|  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                              |
+---------------------------------------------------------------------------+

Cette fois-ci, nous avons pu placer des articles d’une valeur totale de 716,68 dans le meilleur scénario. Nous pouvons également voir les statistiques détaillées de l'évolution et du temps.

Comment tester?

C'est un processus assez simple - il suffit d'ouvrir le fichier principal lié au problème et d'exécuter d'abord l'algorithme. Une fois que nous avons une idée générale, nous pouvons alors commencer à jouer avec les paramètres.

4. Conclusion

Dans cet article, nous avons présenté les fonctionnalités de la bibliothèque Jenetics basées sur de vrais problèmes d’optimisation.

Le code est disponible en tant que projet Maven surGitHub. Veuillez noter que nous avons fourni les exemples de code pour d'autres défis d'optimisation, tels que les problèmesSpringsteen Record (oui, cela existe!) Et les problèmes de voyageur de commerce.

Pour tous les articles de la série, y compris d'autres exemples d'algorithmes génétiques, consultez les liens suivants: