HTW Dresden > GaFrame
 

Nutzung von GaFrame

Note
Die zur Nutzung von GaFrame notwendige Bibliothek wird im Downloadbereich zur Verfügung gestellt.

Im folgenden Abschnitt wird ein grundlegendes "Kochrezept" zur Erstellung eines eigenen Genetischen Algorithmus vorgestellt.

Dabei geht es zuerst nur um die lokale Nutzung, die Möglichkeiten der parallelen Nutzung bzw. der Nutzung in einem Cluster werden auf eigenen Seiten erläutert.

Um die Bibliothek in einer eigenen Anwendung nutzen zu können, muss ga-frame.jar im Classpath enthalten sein. Weitere Konfigurationen sind nicht nötig, werden die Features zum Speichern/Laden von Populationen genutzt, muss auch die Bibliothek JDOM im Classpath liegen.

ga-frame.jar enthält neben den class-Dateien auch die Quelldateien des Projekts, so dass der Quellcode (und damit auch javadoc) zugänglich sind. Details zum Design des Frameworks sind auch aus Design und Javadoc zu entnehmen.

Das Rucksackproblem

Ein Beispiel für einen einfachen Genetischen Algorithmus ist die Klasse de.htwdd.ga.examples.KnapSack, diese ist direkt ausführbar und modelliert ein klassisches Beispiel Genetischer Algorithmen, das Rucksackproblem.
Das Problem besteht darin, aus einer Menge verschiedener Objekte (z.B. Zubehör für eine Bergwanderung) diejenigen auszuwählen, die in einen Rucksack gepackt werden sollen. Jedem Gegenstand ist dabei ein Nutzwert und ein Gewicht zugeordnet, der Rucksack hat ein maximal zulässiges Gesamtgewicht. Die optimale Kombination ist zu ermitteln.

Anhand dieses Beispiels wird die Nutzung von GaFrame deutlich, die wichtigsten Elemente des Frameworks werden hierzu im nächsten Abschnitt erläutert.

Note
Eine einfache Art und Weise, einen eigenen Genetischen Algorithmus zu erstellen, ist also, die genannte Klasse zu kopieren und sich durch Modifikationen mit dem Framework vertraut zu machen.

Kochrezept für einen Genetischen Algorithmus

Das folgende Diagramm zeigt das grundsätzliche Vorgehen, um mit GaFrame einen eigenen Genetischen Algorithmus zu implementieren. Die einzelnen Punkte werden in den folgenden Unterabschnitten weiter erläutert.

Kochrezept

Grundlegende Elemente von GaFrame

Chromosom

Ein Chromosom besteht aus einer Folge von Bits, in GaFrame wird es durch ein java.util.BitSet repräsentiert.

Vor der Nutzung eines Genetischen Algorithmus muss also festgelegt werden, wie sich das vorliegende Problem am sinnvollsten codieren lässt. Eine sinnvolle Codierung des Rucksackproblems ist, für jeden Gegenstand ein Bit zu reservieren, das festlegt, ob der Gegenstand im Rucksack enthalten ist oder nicht.

Der folgende Code gibt eine "Rucksackpopulation" auf der Konsole aus. Jeder im Rucksack enthaltene Gegenstand wird mit einem x markiert.

private static void displayPopulation(Individual[] population)
{
	StringBuffer out = new StringBuffer();
	for (Individual individual : population)
	{
		BitSet b = individual.getChromosome();
		
		out.append("individual ");
		for (int i = 0; i < KnapsackFitness.NUM_ITEMS; i++)
		{
			if (b.get(i))
				out.append("x ");
			else
				out.append("- ");
		}
		out.append("\n");
	}
	System.out.println(out.toString());
}
							

Bei der Arbeit mit einem BitSet kann entweder direkt auf die einzelnen Bits zugegriffen werden (wie im Beispiel), oder es kann die Klasse de.htwdd.ga.util.BitSetUtil genutzt werden, um Ganz- und Gleitkommazahlen in bestimmte Teile des BitSets zu schreiben bzw. wieder auszulesen.

Der folgende Code extrahiert eine Ganzzahl aus den Bits 3-12 des BitSets chromosome:

BitSetUtil.bitSetToInt(chromosome, 3, 10); 

An dieser Stelle wollen wir auch darauf hinweisen, dass standardmäßig keine Gray-Codierung verwendet wird. Diese kann allerdings leicht durch die eben schon genutzte Klasse de.htwdd.ga.util.BitSetUtil nachgereicht werden. Hierzu muss man nur nach dem Setzen der Bitkombination BitSetUtil.doGrayCoding(bitSet) aufrufen. Diese statische Methode transformiert das übergebene BitSet (in binärer Darstellung) in einen Gray-Code.

Die Rücktransformation kann auf gleiche Weise mittels BitSetUtil.doInvGrayCoding(BitSet bitSet) erfolgen.

Individuen/Populationen

Ein Individuum (de.htwdd.ga.Individual) wird duch sein Chromosom repräsentiert, eine Population besteht aus einem Array von Individuen.

Die Klasse de.htwdd.ga.util.GaXMLUtil bietet Methoden, um eine Population in einer XML-Datei zu persistieren oder sie wieder zu extrahieren.

Fitnessfunktion

Um die Fitness eines Individuums zu bestimmen, benötigt der Genetische Algorithmus eine auf das Problem zugeschnittene Fitnessfunktion. Diese muss das Interface de.htwdd.ga.FitnessFunction implementieren.

Die Fitness eines Rucksackindividuums wird duch die aufsummierten Nutzwerte der enthaltenen Gegenstände bestimmt, die Nutzwerte werden aus einer Tabelle ausgelesen. Ein zu schwerer Rucksack hat die Fitness 0. Das Array knapSackArray enthält Nutzwert und Gewicht der einzelnen Gegenstände, der zweite Gegenstand hat z.B. das Gewicht 7 und den Nutzwert 5.

public class KnapsackFitness implements FitnessFunction
{
    public static final int NUM_ITEMS   = 9;
    private static final int MAX_WEIGHT	= 58;
    public static long delay            = 0;
	
	private int	knapSackArray[][] = {
	{ 3, 3 }, { 7, 5 }, { 4, 2 }, { 12, 11 },
	{ 8, 4 }, { 10, 6 }, { 9, 2 }, { 14, 15 },
	{ 10, 12 }, { 12, 9 } };



	public double computeFitness(BitSet chromosome)
	{
		int weightSum = 0, valueSum = 0;
		for (int iGene = 0; iGene < NUM_ITEMS; ++iGene)
		{
			if (chromosome.get(iGene))
			{
				weightSum += knapSackArray[iGene][0];
				valueSum += knapSackArray[iGene][1];
			}
		}

		if (weightSum <= MAX_WEIGHT)
			return valueSum;
		else
			return 0;
	}
}
							
Genetischer Algorithmus

Die Klasse de.htwdd.ga.GeneticAlgorithm ist die zentrale Klasse von GaFrame, sie steuert den Ablauf des Genetischen Algorithmus. Um einen Genetischen Algorithmus zu nutzen, muss diese Klasse instanziiert werden. Dabei ist anzugeben:

  • chromosomeLength - die Länge des Chromosoms aller Individuen
  • maxCycles - die maximale Anzahl von Generationen, die erzeugt werden sollen
  • terminationFitness - sobald mindestens ein Individuum der Population diese Fitness erreicht, wird der Algorithmus beendet.
  • populationSize - die Anzahl von Individuen pro Population

Im Weiteren muss dem Genetischen Algorithmus eine Fitnessfunktion (siehe unten) angegeben werden. Optional können außerdem die einzelnen Bestandteile (Initialisierungsfunktion, Ersetzungsschema, ...) definiert werden, falls von den Standardeinstellungen abweichende Verfahren verwendet werden sollen.

run() startet den Genetischen Algorithmus.

Der folgende Code zeigt die entsprechende Lösung im Rucksackproblem:

GeneticAlgorithm algorithm = new GeneticAlgorithm(KnapsackFitness.NUM_ITEMS, 100, 66, 20);
algorithm.setFitnessFunction(KnapsackFitness.class);
algorithm.setCrossoverMethod(new TwoPointCrossover());
algorithm.setMutationRate(0.1);
algorithm.setSelectionStrategy(new RankingSelection(.3, .4));

algorithm.run();
							

Optionale Konfigurationsmöglichkeiten

GaFrame individualisieren

GaFrame kann leicht individualisiert werden, um z.B. Log-Ausgaben oder das automatische Speichern der Populationen nach jedem Durchlauf zu realisieren. Zu diesem Zweck muss lediglich die Klasse de.htwdd.ga.GeneticAlgorithm abgeleitet werden. Alle nicht als final deklarierten Methoden lassen sich leicht überschreiben, um individuelle Erweiterungen zu realisieren. Dabei ist darauf zu achten, dass im Verlauf der Methode super aufgerufen wird.

Im folgenden Beispiel wird die Methode runReplacement() überschrieben, um eine Logausgabe zu realisieren und die Population zu speichern.

public class MyGA extends GeneticAlgorithm
{
	public MyGA(int chromosomeLength, int numCycles, int populationSize)
	{
		super(chromosomeLength, numCycles, 0.98, populationSize);
	}

	@Override
	protected void runReplacement()
	{
		super.runReplacement();

		// Fitnesswerte aller Individuen ausgeben
		StringBuffer buffer = new StringBuffer();
		buffer.append("##population fitness after cycle ");
		buffer.append(cycle + 1);
		IndividualSorter.sortIndividuals(population);
		for (Individual individual : population)
		{
			buffer.append(String.format(" %1.4f", individual.getFitness()));
		}
		System.out.println(buffer.toString());

		//Population speichern
		GaXmlUtil.writeToFile(population, "data/" + cycle + ".xml");
	}
}
							

Neben der Möglichkeit, GeneticAlgorithm zu überschreiben, können auch alle weiteren austauschbaren Komponenten durch individuelle Implementierungen (z.B. eine eigene Ersetzungsstrategie) ersetzt werden.

Mutationsrate

Die Mutationsrate bestimmt, mit welcher Wahrscheinlichkeit ein Individuum mutiert, also sein Erbmaterial zufällig verändert. Mutationen werden nach der Ersetzung durchgeführt (siehe Ablaufmodellierung).

Beispiel:

GeneticAlgorithm algorithm;
...
algorithm.setMutationRate(0.2);
							
Initialisierungsfunktion

Die Initialisierungsfunktion erzeugt eine Startpopulation, mit der der Genetische Algorithmus im Folgenden arbeiten wird (de.htwdd.ga.InitializationFunction). Standardmäßig werden die hier erzeugten Individuen mit einem zufällig erzeugten Chromosom ausgestattet. Weiterhin ist es möglich, eine Überpopulation erzeugen zu lassen, deren "fitteste" Individuen dann die initiale Population bilden. Standardmäßig wird keine Überpopulation erzeugt.

Das folgende Beispiel erzeugt eine Überpopulation von 50%:

GeneticAlgorithm algorithm;
...
algorithm.getInitializationFunction().setInitializationCoeff(1.5);
							

Die Klasse de.htwdd.ga.FileInitialization erlaubt es, eine aus einer XML-Datei extrahierte Population als Startpopulation zu nutzen, weitere eigene Implementierungen sind denkbar.

Beispiel:

GeneticAlgorithm algorithm;
...
FileInitialization init = new FileInitialization("alte_Population.xml");
algorithm.setInitializationFunction(init);
							
Selektionsverfahren

Das Selektionsverfahren bestimmt, welche Individuen einer aktuellen Population sich vermehren dürfen, Basisklasse für Selektionsverfahren ist de.htwdd.ga.SelectionStrategy.

Vorhandene Selektionsverfahren:

  • zufällige Auswahl von Heiratskandidaten: de.htwdd.ga.RandomSelection
  • rangbasierte Auswahl: de.htwdd.ga.RankingSelektion

Beispiel:

GeneticAlgorithm algorithm;
...
algorithm.setSelectionStrategy(new RankingSelection(.3, .4));
							
Kreuzungsverfahren

Um aus einer vorhandenen Population von Individuen Nachkommen zu gewinnen, werden jeweils zwei Individuen aus dem Heiratspool mittels eines Kreuzungsverfahrens rekombiniert.

Grundsätzlich bedeutet dies, dass das "Erbgut" (also das Chromosom) der Eltern in geeigneter Weise gespalten wird, wodurch man (üblicherweise) zwei neue Individuen, die Kinder, erhält.

GaFrame enthält zwei grundlegenden Kreuzungsverfahren:

  • de.htwdd.ga.SinglePointCrossover: Bei diesem Verfahren wird zufällig eine Stelle des Chromosoms ausgewählt, die Chromosomen der beiden Eltern werden an dieser Stelle aufgeteilt. Durch Vertauschen der jeweils zweiten Hälften der Chromsomen entstehen zwei neue Individuen.
    Ein-Punkt-Kreuzung
  • de.htwdd.ga.TwoPointCrossover: Aus dem Namen wird bereits klar, dass bei diesem Verfahren zwei Stellen des Chromosoms ausgewählt werden. So wird aus der Mitte der Chromosomen der Eltern ein Teil herausgetrennt, durch Kreuzung erhält man auch hier zwei neue Individuen.
    Zwei-Punkt-Kreuzung

Beispiel:

GeneticAlgorithm algorithm;
...
algorithm.setCrossoverMethod(new TwoPointCrossover());