Einführung in die Jenetics Library

Einführung in die Jenetics Library

1. Einführung

The aim of this series soll die Idee genetischer Algorithmen erklären und die bekanntesten Implementierungen zeigen.

In diesem Tutorial werden wirdescribe a very powerful Jenetics Java library that can be used for solving various optimization problems.

Wenn Sie der Meinung sind, dass Sie mehr über genetische Algorithmen erfahren müssen, empfehlen wir, mitthis article zu beginnen.

2. Wie funktioniert es?

Jenetics ist gemäßofficial documents eine Bibliothek, die auf einem in Java geschriebenen evolutionären Algorithmus basiert. Evolutionäre Algorithmen haben ihre Wurzeln in der Biologie, da sie von der biologischen Evolution inspirierte Mechanismen wie Reproduktion, Mutation, Rekombination und Selektion verwenden.

Jenetics wird über die JavaStream-Schnittstelle implementiert, sodass es reibungslos mit dem Rest der JavaStream-API zusammenarbeitet.

Die Hauptmerkmale sind:

  • frictionless minimization - Die Fitnessfunktion muss nicht geändert oder optimiert werden. Wir können einfach die Konfiguration der KlasseEngineändern und sind bereit, unsere erste Anwendung zu starten

  • dependency free - Für die Verwendung von Jenetics sind keine Laufzeitbibliotheken von Drittanbietern erforderlich

  • Java 8 ready - volle Unterstützung fürStream und Lambda-Ausdrücke

  • multithreaded - Evolutionsschritte können parallel ausgeführt werden

Um Jenetics verwenden zu können, müssen wir unserenpom.xml die folgende Abhängigkeit hinzufügen:


    io.jenetics
    jenetics
    3.7.0

Die neueste Version finden Sie unterin Maven Central.

3. Anwendungsfälle

Um alle Funktionen von Jenetics zu testen, werden wir versuchen, verschiedene bekannte Optimierungsprobleme zu lösen, beginnend mit dem einfachen binären Algorithmus und endend mit dem Knapsack-Problem.

3.1. Einfacher genetischer Algorithmus

Nehmen wir an, wir müssen das einfachste binäre Problem lösen, bei dem wir die Positionen der 1-Bits im Chromosom, bestehend aus 0 und 1, optimieren müssen. Zuerst müssen wir die Fabrik definieren, die für das Problem geeignet ist:

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

Wir haben dieBitChromosome mit einer Länge von 10 und einer Wahrscheinlichkeit von 1 im Chromosom von 0,5 erstellt.

Erstellen wir nun die Ausführungsumgebung:

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

Die Methodeeval() gibt die Bitanzahl zurück:

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

Im letzten Schritt starten wir die Evolution und sammeln die Ergebnisse:

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

Das Endergebnis sieht ungefähr so ​​aus:

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

Es ist uns gelungen, die Position von Einsen im Gen zu optimieren.

3.2. Teilmengen-Summenproblem

Ein weiterer Anwendungsfall für Jenetics ist die Lösung dersubset sum problem. Kurz gesagt, die Herausforderung bei der Optimierung besteht darin, dass wir bei einer gegebenen Menge von Ganzzahlen eine nicht leere Teilmenge finden müssen, deren Summe Null ist.

Es gibt vordefinierte Schnittstellen in Jenetics, um solche Probleme zu lösen:

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

Wie wir sehen können, implementieren wirProblem<T, G, C>, das drei Parameter hat:

  • <T> - der Argumenttyp der Problem-Fitness-Funktion, in unserem Fall eine unveränderliche, geordneteInteger-SequenzISeq<Integer> mit fester Größe

  • <G> - der Gentyp, mit dem die Evolutionsmaschine arbeitet, in diesem Fall zählbareInteger GeneEnumGene<Integer>

  • <C> - der Ergebnistyp der Fitnessfunktion; hier ist es einInteger

Um dieProblem<T, G, C>-Schnittstelle zu verwenden, müssen zwei Methoden überschrieben werden:

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

In der ersten definieren wir unsere Fitnessfunktion, während die zweite eine Klasse ist, die Factory-Methoden zum Erstellen allgemeiner Problemcodierungen enthält, um beispielsweise wie in unserem Fall die beste Teilmenge mit fester Größe aus einer gegebenen Grundmenge zu finden.

Nun können wir mit dem Hauptteil fortfahren. Zu Beginn müssen wir eine Teilmenge erstellen, die für das Problem verwendet werden soll:

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

Bitte beachten Sie, dass wir den von Jenetics bereitgestelltenLCG64ShiftRandom-Generator verwenden. Im nächsten Schritt bauen wir den Motor unserer Lösung:

Im nächsten Schritt bauen wir den Motor unserer Lösung:

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

Wir versuchen, das Ergebnis zu minimieren (optimalerweise ist das Ergebnis 0), indem wir das Phänotypalter und die zur Änderung der Nachkommen verwendeten Alterungsfaktoren festlegen. Im nächsten Schritt können wir das Ergebnis erhalten:

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

Bitte beachten Sie, dass wirbySteadyFitness() verwenden, die ein Prädikat zurückgeben, das den Evolutionsstrom abschneidet, wenn nach der angegebenen Anzahl von Generationen kein besserer Phänotyp gefunden werden kann und das beste Ergebnis erzielt. Wenn wir Glück haben und es eine Lösung für die zufällig erstellte Menge gibt, sehen wir etwas Ähnliches:

Wenn wir Glück haben und es eine Lösung für die zufällig erstellte Menge gibt, sehen wir etwas Ähnliches:

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

Andernfalls unterscheidet sich die Summe der Teilmenge von 0.

3.3. Rucksack First Fit Problem

Mit der Jenetics-Bibliothek können wir noch komplexere Probleme wieKnapsack problemlösen. Kurz gesagt, bei diesem Problem haben wir einen begrenzten Platz in unserem Rucksack, und wir müssen entscheiden, welche Gegenstände hineingelegt werden sollen.

Beginnen wir mit der Definition der Taschengröße und der Anzahl der Artikel:

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

Im nächsten Schritt generieren wir ein zufälliges Array mitKnapsackItem Objekten (definiert durchsize undvalue Felder) und legen diese Elemente mit dem First zufällig in den Rucksack Anpassungsmethode:

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

Als nächstes müssen wir dieEngine erstellen:

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

Hier sind einige Punkte zu beachten:

  • Bevölkerungsgröße ist 500

  • Der Nachwuchs wird durch die Auswahl der Turnier- und Rouletteräder ausgewählt

  • Wie im vorigen Unterabschnitt müssen wir auch für die neu geschaffenen Nachkommen die Alterer definieren

There is also one very important feature of Jenetics. We can easily collect all statistics and insights from the whole simulation duration. Wir werden dies tun, indem wir die KlasseEvolutionStatistics verwenden:

EvolutionStatistics statistics = EvolutionStatistics.ofNumber();

Lassen Sie uns abschließend die Simulationen ausführen:

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

Bitte beachten Sie, dass wir die Bewertungsstatistik nach jeder Generation aktualisieren, die auf 7 stetige Generationen und maximal 100 Generationen insgesamt begrenzt ist. Im Einzelnen gibt es zwei mögliche Szenarien:

  • Wir erreichen 7 stetige Generationen, dann stoppt die Simulation

  • Wir können nicht 7 stetige Generationen in weniger als 100 Generationen erhalten, daher stoppt die Simulation aufgrund der zweitenlimit()

Es ist wichtig, ein maximales Generationslimit zu haben, da sonst die Simulationen möglicherweise nicht in angemessener Zeit beendet werden.

Das Endergebnis enthält viele Informationen:

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

Diesmal konnten wir Artikel mit einem Gesamtwert von 716,68 im besten Szenario platzieren. Wir können auch die detaillierten Statistiken der Evolution und der Zeit sehen.

Wie teste ich?

Es ist ein ziemlich einfacher Vorgang - öffnen Sie einfach die Hauptdatei, die sich auf das Problem bezieht, und führen Sie zuerst den Algorithmus aus. Sobald wir eine allgemeine Vorstellung haben, können wir mit den Parametern spielen.

4. Fazit

In diesem Artikel haben wir die Funktionen der Jenetics-Bibliothek behandelt, die auf echten Optimierungsproblemen basieren.

Der Code ist als Maven-Projekt fürGitHub verfügbar. Bitte beachten Sie, dass wir die Codebeispiele für weitere Optimierungsprobleme bereitgestellt haben, z. B. die ProblemeSpringsteen Record (ja, es gibt sie!) Und Travelling Salesman.

Überprüfen Sie für alle Artikel in der Serie, einschließlich anderer Beispiele für genetische Algorithmen, die folgenden Links: