Inhalt des Skriptums seit dem Sommersemester 2025
- Startseite
- Allgemeine Informationen zur Lehrveranstaltung
- Einfaches Python Setup und Wichtige Informationen
- 01 Einführung in algorithmisches Denken & LLMs
- 02 Algorithmenanalyse und Komplexitätstheorie
- 03 Vektoren, Matrizen und Vektorisierung in Python
- 04 Differenzieren und Integrieren in Python: Analytische und Numerische Methoden
- 05 Arbeiten mit Daten: Vorbereitung, Visualisierung, Analyse
- 06 Graphalgorithmen: Darstellungen, Traversierungen, Kürzeste Wege und Spannbäume
- 07 Gradient Descent, Stochastische Optimierung, Simulated Annealing, Genetische Algorithmen
- 08 Schwärme und Emergenz
- 09 Monte-Carlo-Methoden
- 10 Machine-Learning Grundlagen
Hier finden Sie die Kapitel aus den Vergangenen Jahren (legacy):
- 0. Python Einführung und Checkup
- 1. Einführung und einfache Algorithmen
- 2. Differenzieren und Integrieren
- 3. Vektoren, Matrizen und Vektorisierung in Python
- 4. Datenanalyse bzw. Datenauswertung
- 5. Grundlagen der Optimierung und Gradient Descent
- 6. Stochastische Optimierung und Genetische Algorithmen
- 7. Monte-Carlo-Methoden – Simulation und Integration
- 8. Monte-Carlo-Methoden, Teil 2 – Monte-Carlo-Integration, Teil 2 und Random Walk
- 9. Unsupervised Machine Learning: Clustering von Daten
- 10. Supervised Machine Learning: Grundlagen
- 11. Einführung in künstliche neuronale Netzwerke
Die Jupyter-Notebooks zur Legacy-Variante der Lehrveranstaltung finden Sie im zugehörigen GitHub-Repository.
8: Schwärme und Emergenz¶
Nicht nur in der Natur lassen sich oft Schwärme beobachten, die scheinbar mühelos gemeinsame Bewegungen ausführen, einen Staat organisieren oder komplexe Probleme lösen (wie z.B. Futter zu finden und in ein Nest zu transportieren). Auch in anderen Bereichen bis hin zur Optimierung kann man bewusst Schwarm-Algorithmen einsetzen um Probleme zu lösen oder Systeme von Individuen zu steuern. In dieser Einheit geht es darum, wie so etwas funktioniert und welche Regeln und Phänomene dabei eine Rolle spielen.
Nach dem Studium dieser Einheit sollten Sie zumindest folgende drei der wichtigsten Punkte hier verstehen:¶
- Was man unter einem Schwarm versteht und nach welchen Regeln er funktionieren kann.
- Welche Beispiele es für Schwarm-Verhalten gibt, und welche interessanten Dinge wir aus ihnen lernen können.
- Was Emergenz bedeutet und wodurch sie zustande kommt.
8.1. Grundbausteine emergenten Verhaltens und der Schwarm-Intelligenz¶
Man hört immer wieder von Schwarm-Intelligenz und etwas weniger oft von emergentem Verhalten. Bevor wir uns konkret mit den möglichen Ausformungen solcher Prinzipien befassen können, müssen wir zunächst verstehen, welche Mechanismen dabei zusammenwirken.
8.1.1 Grundbegriffe¶
Ein Schwarm bezeichnet eine Gruppe von Individuen, die durch lokale Interaktionen kollektives Verhalten zeigen. Im Gegensatz zu hierarchisch organisierten Gruppen existiert in einem Schwarm keine zentrale Kontrollinstanz. Der Begriff “Kollektiv” wird oft synonym verwendet, betont aber stärker den Aspekt der gemeinsamen Eigenschaft oder Handlung aller Individuen.
Die Individuen oder Agenten sind die grundlegenden Einheiten eines Schwarms. Sie können biologische Organismen (Ameisen, Vögel) oder virtuelle Entitäten (Partikel in einem Optimierungsalgorithmus, simulierte Roboter) sein. Entscheidend ist, dass jeder Agent über folgende Eigenschaften verfügt:
- Lokale Wahrnehmung seiner Umgebung
- Begrenzte Informationsverarbeitung und Entscheidungsfähigkeit
- Fähigkeit, mit anderen Agenten zu interagieren oder zu kommunizieren
- Autonomie bei der Ausführung einfacher Verhaltensregeln
Die Umgebung ist der Raum, in dem sich die Agenten bewegen und interagieren. Sie kann physisch (der dreidimensionale Raum für Vogelschwärme) oder abstrakt sein (der Lösungsraum eines Optimierungsproblems). Die Umgebung beeinflusst das Verhalten der Agenten und kann durch diese modifiziert werden – ein Prozess, der insbesondere für die Stigmergie (indirekte Kommunikation über Umgebungsveränderungen) zentral ist.
Nachbarschaft bezeichnet den begrenzten Bereich um einen Agenten, innerhalb dessen er andere Agenten wahrnimmt und mit ihnen interagiert. Die Definition der Nachbarschaft kann räumlich (alle Agenten innerhalb eines bestimmten Radius) oder topologisch (die $n$ nächsten Agenten, unabhängig von ihrer Entfernung) sein. Die Beschränkung auf lokale Nachbarschaften ist ein Schlüsselelement für die Skalierbarkeit von Schwarm-Systemen.
Emergenz beschreibt das Auftreten von Eigenschaften oder Verhaltensweisen auf Systemebene, die nicht direkt in den Regeln der einzelnen Agenten kodiert sind. Ein emergentes Phänomen lässt sich nicht durch einfache Summierung der Eigenschaften einzelner Systemkomponenten erklären, sondern entsteht aus deren komplexen Interaktionen. Beispiele sind Musterbildung, kollektive Entscheidungsfindung oder koordinierte Bewegung.
Selbstorganisation bezeichnet die Fähigkeit eines Systems, ohne externe Steuerung oder zentralen Plan Ordnung oder Struktur zu entwickeln. Sie basiert auf lokalen Interaktionen zwischen den Systemkomponenten und Rückkopplungsmechanismen. Zum Beispiel führt Selbstorganisation in Schwärmen zu adaptivem Verhalten, das dynamisch auf Umgebungsveränderungen reagieren kann.
Soviel zu den Grundbegriffen. Damit können wir uns jetzt zunächst die Mechanismen und dann diverse Phänomene ansehen, die Schwarmverhalten bestimmen und hervorbringen können.
8.1.2 Lokale Interaktionen und einfache Regeln¶
Ein Vogelschwarm, der in perfekter Synchronisation Richtungswechsel vollführt. Eine Ameisenkolonie, die ohne zentrale Koordination effiziente Wege zu Nahrungsquellen findet. Termiten, die komplexe Bauten mit ausgeklügelten Belüftungssystemen errichten. All diese beeindruckenden kollektiven Verhaltensweisen basieren auf einem überraschend einfachen Prinzip: Lokale Interaktionen zwischen gleichartigen Individuen, die simplen Regeln folgen.
Im Gegensatz etwa zu zentralisierten Systemen, in denen eine übergeordnete Instanz alle Aktivitäten koordiniert, nehmen Individuen in Schwärmen nur ihre unmittelbare Umgebung wahr. Eine Ameise “weiß” nichts über die Gesamtstruktur der Kolonie oder die globale Verteilung von Nahrungsquellen. Ein Vogel im Schwarm sieht nur bzw. achtet nur auf seine direkten Nachbarn. Diese beschränkte Wahrnehmung hat einen entscheidenden Vorteil: Sie reduziert die Komplexität der Entscheidungsfindung drastisch und ermöglicht schnelle, robuste Reaktionen auf lokale Veränderungen.
Die Entscheidungsregeln in solchen Systemen sind typischerweise sehr einfach. Als Beispiel kommen etwa folgende drei Regeln in Frage:
- Separation: Halte einen Mindestabstand zu deinen Nachbarn
- Angleichung: Bewege dich in die gleiche Richtung wie deine Nachbarn
- Zusammenhalt: Bewege dich in Richtung des Zentrums deiner lokalen Nachbarschaft
Diese Regeln sind leicht zu verstehen und auch computertechnisch einfach umzusetzen. Dennoch erzeugen sie in ihrer Kombination verblüffend realistische Schwärme, die auf Hindernisse reagieren, sich aufteilen und wieder zusammenfinden können – ohne dass dieses komplexe Verhalten explizit programmiert wurde.
Sehen wir uns das gleich einmal in einem Beispiel an. Die Code-Snippets kommen diesmal von Gemini 2.5 pro.
import os
os.environ['MKL_DISABLE_FAST_MM'] = '1'
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML # For displaying the animation in Jupyter
import matplotlib as mpl
# Set the animation embed limit to 50 MB (or your desired size in MB)
mpl.rcParams['animation.embed_limit'] = 200 # The value is in Megabytes
# Parameters for the simulation
NUM_BOIDS = 150
WIDTH = 600 # Width of the simulation area
HEIGHT = 400 # Height of the simulation area
MAX_SPEED = 5.0
MIN_SPEED = 2.0
PERCEPTION_RADIUS = 75.0 # How far a boid can see
AVOIDANCE_RADIUS = 15.0 # Radius to avoid other boids
# Weights for the three rules
COHESION_FACTOR = 0.05
ALIGNMENT_FACTOR = 0.5
SEPARATION_FACTOR = 0.15
TURN_FACTOR = 0.2 # How quickly boids can turn
# Boundaries for screen wrapping
MARGIN = 50
class Boid:
def __init__(self, x, y):
self.position = np.array([float(x), float(y)])
angle = np.random.uniform(0, 2 * np.pi)
self.velocity = np.array([np.cos(angle), np.sin(angle)]) * MAX_SPEED
self.acceleration = np.zeros(2, dtype=float)
def update(self):
# Apply acceleration
self.velocity += self.acceleration
# Limit speed
speed = np.linalg.norm(self.velocity)
if speed > MAX_SPEED:
self.velocity = (self.velocity / speed) * MAX_SPEED
elif speed < MIN_SPEED:
self.velocity = (self.velocity / speed) * MIN_SPEED
self.position += self.velocity
self.acceleration = np.zeros(2, dtype=float) # Reset acceleration
def apply_force(self, force):
self.acceleration += force
def screen_wrap(self):
if self.position[0] > WIDTH + MARGIN:
self.position[0] = -MARGIN
elif self.position[0] < -MARGIN:
self.position[0] = WIDTH + MARGIN
if self.position[1] > HEIGHT + MARGIN:
self.position[1] = -MARGIN
elif self.position[1] < -MARGIN:
self.position[1] = HEIGHT + MARGIN
def steer(self, target_direction):
desired = target_direction - self.position
desired_norm = np.linalg.norm(desired)
if desired_norm > 0:
desired = (desired / desired_norm) * MAX_SPEED
steer = desired - self.velocity
steer_norm = np.linalg.norm(steer)
if steer_norm > TURN_FACTOR: # Limit the turning force
steer = (steer / steer_norm) * TURN_FACTOR
return steer
def cohesion(self, boids):
center_of_mass = np.zeros(2, dtype=float)
count = 0
for other in boids:
if other is not self:
distance = np.linalg.norm(self.position - other.position)
if 0 < distance < PERCEPTION_RADIUS:
center_of_mass += other.position
count += 1
if count > 0:
center_of_mass /= count
return self.steer(center_of_mass) * COHESION_FACTOR
return np.zeros(2, dtype=float)
def alignment(self, boids):
avg_velocity = np.zeros(2, dtype=float)
count = 0
for other in boids:
if other is not self:
distance = np.linalg.norm(self.position - other.position)
if 0 < distance < PERCEPTION_RADIUS:
avg_velocity += other.velocity
count += 1
if count > 0:
avg_velocity /= count
# Steer towards the average velocity
desired = avg_velocity
desired_norm = np.linalg.norm(desired)
if desired_norm > 0:
desired = (desired / desired_norm) * MAX_SPEED
steer = desired - self.velocity
steer_norm = np.linalg.norm(steer)
if steer_norm > TURN_FACTOR:
steer = (steer / steer_norm) * TURN_FACTOR
return steer * ALIGNMENT_FACTOR
return np.zeros(2, dtype=float)
def separation(self, boids):
steering_force = np.zeros(2, dtype=float)
count = 0
for other in boids:
if other is not self:
distance = np.linalg.norm(self.position - other.position)
if 0 < distance < AVOIDANCE_RADIUS: # Use a smaller radius for separation
diff = self.position - other.position
steering_force += diff / (distance * distance) # Force inversely proportional to square of distance
count += 1
if count > 0:
steering_force /= count # Average the force
# Steer away
desired = steering_force
desired_norm = np.linalg.norm(desired)
if desired_norm > 0:
desired = (desired / desired_norm) * MAX_SPEED
steer = desired - self.velocity
steer_norm = np.linalg.norm(steer)
if steer_norm > TURN_FACTOR:
steer = (steer / steer_norm) * TURN_FACTOR
return steer * SEPARATION_FACTOR
return np.zeros(2, dtype=float)
def apply_rules(self, boids):
cohesion_force = self.cohesion(boids)
alignment_force = self.alignment(boids)
separation_force = self.separation(boids)
self.apply_force(cohesion_force)
self.apply_force(alignment_force)
self.apply_force(separation_force)
# Initialize boids
boids = [Boid(np.random.uniform(0, WIDTH), np.random.uniform(0, HEIGHT)) for _ in range(NUM_BOIDS)]
# Matplotlib setup
fig, ax = plt.subplots(figsize=(10, 7.5)) # Adjusted for better aspect ratio
ax.set_xlim(0, WIDTH)
ax.set_ylim(0, HEIGHT)
ax.set_xticks([]) # Remove x-axis ticks
ax.set_yticks([]) # Remove y-axis ticks
ax.set_facecolor('lightgrey') # Background color
# Using quivers to represent boids (arrows showing direction)
# We need to store the quiver objects to update them
quivers = []
for boid in boids:
# Initial quiver plot for each boid
# Scale the velocity for better arrow representation
# The head_width, head_length, etc., might need tweaking based on MAX_SPEED
q = ax.quiver(boid.position[0], boid.position[1],
boid.velocity[0], boid.velocity[1],
color='blue', scale=50, headwidth=4, headlength=6) # Adjust scale and head
quivers.append(q)
def update_frame(frame_number):
# Update positions and velocities of boids
for boid in boids:
boid.apply_rules(boids)
boid.update()
boid.screen_wrap()
# Update the quiver plots
for i, boid in enumerate(boids):
quivers[i].set_offsets(boid.position)
# Normalize velocity for direction if MAX_SPEED is large, or scale it down
# This ensures arrows don't become excessively long
vel_norm = np.linalg.norm(boid.velocity)
if vel_norm > 0:
uv = boid.velocity / vel_norm
else:
uv = np.zeros(2) # In case velocity is zero
quivers[i].set_UVC(uv[0], uv[1])
return quivers
# Create animation
# interval is the delay between frames in milliseconds.
# Adjust blit=True for potentially smoother animation if it works well on your system.
# blit=False is often more robust.
ani = FuncAnimation(fig, update_frame, frames=400, interval=50, blit=False)
# Display the animation in Jupyter Notebook
plt.close(fig) # Prevent static plot from displaying
# ani.save('boid_simulation.mp4', writer='ffmpeg', fps=30)
HTML(ani.to_jshtml()) # or use ani.to_html5_video()
8.1.3 Rückkopplungsmechanismen¶
Rückkopplungsmechanismen sind ein zentraler Baustein selbstorganisierender Systeme. Sie beschreiben, wie die Aktionen der Individuen ihre Umgebung verändern und wie diese Veränderungen wiederum das Verhalten der Individuen beeinflussen. Es gibt zwei Haupttypen: positive und negative Rückkopplung.
Positive Rückkopplung verstärkt Veränderungen und kann zu explosionsartigen Entwicklungen führen. Sie wirkt wie ein Schneeball, der beim Rollen immer größer wird. Ein klassisches Beispiel ist die Pheromonablagerung bei Ameisen: Wenn eine Ameise eine ergiebige Nahrungsquelle findet, hinterlässt sie auf dem Rückweg zum Nest eine Pheromonspur. Andere Ameisen folgen dieser Spur, finden ebenfalls Nahrung und verstärken die Pheromonspur auf ihrem Rückweg. Je mehr Ameisen den Pfad nutzen, desto attraktiver wird er für weitere Ameisen – ein sich selbst verstärkender Prozess.
Negative Rückkopplung hingegen wirkt dämpfend und stabilisierend. Sie verhindert, dass positive Rückkopplungen außer Kontrolle geraten. Im Ameisenbeispiel sorgt die Pheromonverdunstung für negative Rückkopplung: Pheromone verflüchtigen sich mit der Zeit, sodass weniger frequentierte Pfade verschwinden. Wird eine Nahrungsquelle erschöpft, legen weniger Ameisen Pheromonspuren an, und der Pfad “verschwindet” allmählich, was die Kolonie dazu bringt, neue Quellen zu erkunden. Diese Balance zwischen Verstärkung und Dämpfung ist charakteristisch für adaptive Schwarm-Systeme.
Die richtige Balance zwischen positiver und negativer Rückkopplung ist entscheidend für funktionsfähige Schwarm-Systeme. Zu viel positive Rückkopplung führt zu vorzeitiger Konvergenz und mangelnder Anpassungsfähigkeit; zu viel negative Rückkopplung verhindert die Bildung stabiler Strukturen. In natürlichen Systemen hat die Evolution diese Balance optimiert; in künstlichen Schwarm-Algorithmen ist die Einstellung dieser Balance eine der Hauptherausforderungen.
8.1.4 Emergenz von Mustern und Strukturen¶
Emergenz bedeutet, dass ein System Eigenschaften aufweist, die seine einzelnen Komponenten nicht besitzen und die nicht offensichtlich aus den Eigenschaften dieser Komponenten ableitbar sind. Kurz gesagt: “Das Ganze ist mehr als die Summe seiner Teile”. In Schwarm-Systemen bedeutet dies, dass aus den Interaktionen vieler einfacher Individuen komplexe Muster, Strukturen oder Verhaltensweisen entstehen, die nicht explizit im “Programm” der einzelnen Agenten kodiert sind.
Der Übergang von mikroskopischen Regeln zu makroskopischem Verhalten ist nicht immer intuitiv vorhersehbar. Kleine Änderungen in den lokalen Regeln können zu drastisch unterschiedlichem globalen Verhalten führen. Dies erklärt, warum das Design von Schwarm-Algorithmen oft experimentell erfolgt: Man passt die lokalen Regeln an und beobachtet, welche Muster auf globaler Ebene entstehen. Diese Nicht-Linearität ist gleichzeitig Herausforderung und Stärke von Schwarm-Systemen. Sie macht sie schwer zu analysieren, aber auch außerordentlich anpassungsfähig und vielseitig.
8.1.5 Von individueller zu kollektiver Intelligenz¶
Individuelle Agenten in einem Schwarm sind typischerweise begrenzt in ihren Fähigkeiten: Sie verfügen über limitierte Wahrnehmung, begrenzten Speicher und eingeschränkte Kommunikationsfähigkeiten. Eine einzelne Ameise kann keine komplexen Berechnungen durchführen, und ein einzelner Vogel hat keine Übersicht über optimale Migrationsrouten. Dennoch lösen Ameisenkolonien und Vogelschwärme effektiv komplexe Optimierungsprobleme. Wie ist das möglich?
Die Antwort liegt in der kollektiven Informationsverarbeitung. Information wird nicht zentralisiert, sondern verteilt über alle Agenten verarbeitet und aggregiert. In einer Bienenkolonie beispielsweise erkunden Kundschafterbienen verschiedene potenzielle Nistplätze und bewerten deren Qualität. Zurück im Schwarm führen sie einen “Schwänzeltanz” auf, dessen Intensität von der Qualität des gefundenen Platzes abhängt. Andere Bienen beobachten verschiedene Tänze und entscheiden probabilistisch, welchem Kundschafter sie folgen. Durch diesen Mechanismus “stimmt” der Schwarm effektiv für den besten Nistplatz, ohne dass eine einzelne Biene den gesamten Entscheidungsprozess überblickt.
Funktionsfähige kollektive Intelligenz basiert also auf mehreren Schlüsselprinzipien:
- Dezentralisierung: Keine zentrale Kontrolle oder Single Point of Failure
- Diversität: Unterschiedliche Perspektiven und Reaktionen
- Unabhängigkeit: Agenten treffen eigenständige Entscheidungen basierend auf lokalen Informationen
- Aggregation: Mechanismen zur Kombination individueller Beiträge
Diese Prinzipien finden sich nicht nur in biologischen Schwärmen, sondern auch in anderen kollektiv intelligenten Systemen wie Märkten, demokratischen Abstimmungen oder Online-Plattformen wie Wikipedia. Sie ermöglichen es, die Weisheit und die Problemlösungsfähigkeit von Gruppen zu nutzen, die weit über die Fähigkeiten einzelner Mitglieder hinausgehen.
8.2. Bioinspirierte Schwarm-Algorithmen¶
Die Natur bietet ein reichhaltiges Reservoir an Inspiration für Schwarm-Algorithmen:
- Ameisenkolonien und ihre Pheromonkommunikation inspirieren Optimierungsalgorithmen für Routing- und Scheduling-Probleme
- Bienenschwärme und ihre Futtersuche-/Nistplatzwahlstrategien liefern Modelle für multimodale Optimierung und Exploration-Exploitation-Balance
- Vogel- und Fischschwärme demonstrieren effiziente Bewegungskoordination, die z.B. in Roboterschwärmen angewendet wird
- Glühwürmchen und ihre Synchronisationsmechanismen inspirieren Algorithmen für Netzwerksynchronisation
Im Foglenden sehen wir uns ein paar konkrete Beispiele und Visualisierungen an.
8.2.1 Ameisenkolonie-Optimierung (ACO)¶
Die Ameisenkolonie-Optimierung ist einer der bekanntesten schwarmbasierten Algorithmen, inspiriert vom Futtersuchverhalten von Ameisen. Ameisen hinterlassen auf ihrem Weg Pheromone, chemische Substanzen, die von anderen Ameisen wahrgenommen werden können. Je mehr Ameisen einen Pfad nutzen, desto stärker wird die Pheromonspur, was wiederum mehr Ameisen anzieht. Kürzere Pfade werden häufiger traversiert, wodurch sich dort mehr Pheromone ansammeln, was zur Konvergenz auf optimale Routen führt.
Im ACO-Algorithmus repräsentieren künstliche Ameisen Lösungskandidaten, die durch den Suchraum navigieren. Die Wahrscheinlichkeit, dass eine Ameise einen bestimmten Pfad wählt, hängt von der Pheromonkonzentration und einer heuristischen Information (z.B. Distanz) ab. Nach jeder Iteration werden die Pheromonwerte aktualisiert: Erfolgreiche Pfade erhalten mehr Pheromone, während alle Pfade einer Verdunstung unterliegen, um die Erkundung neuer Lösungen zu fördern.
ACO eignet sich besonders für kombinatorische Optimierungsprobleme wie das Traveling Salesman Problem, Routenplanung oder Scheduling. An dieser Stelle sehen wir uns aber einfach einmal eine konkrete Simulation mit Visualisierung an.
# Import necessary libraries
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from IPython.display import HTML # For displaying animation in Jupyter
import random
import math
import time # To track performance if needed
import matplotlib as mpl
# Set the animation embed limit to 50 MB (or your desired size in MB)
mpl.rcParams['animation.embed_limit'] = 600 # The value is in Megabytes
# --- Constants (mostly same as pygame version) ---
WIDTH, HEIGHT = 400, 300 # Logical world dimensions
PHEROMONE_RESOLUTION_FACTOR = 5 # Pheromones calculated on a coarser grid (HIGHER value = COARSER grid = FASTER)
P_WIDTH, P_HEIGHT = WIDTH // PHEROMONE_RESOLUTION_FACTOR, HEIGHT // PHEROMONE_RESOLUTION_FACTOR
N_ANTS = 150
ANT_SPEED = 1.0
ANT_ROTATION_SPEED = 0.1 # Radians per step
SENSOR_DISTANCE = 15 # How far ahead ants "smell"
SENSOR_ANGLE = math.pi / 4 # Angle offset for side sensors (radians)
# Pheromones
EVAPORATION_RATE = 0.99 # Percentage remaining after 1 step
PHEROMONE_DROP_RATE = 2.0
MAX_PHEROMONE = 100.0 # Cap pheromone intensity
# States (using integers for easy indexing/coloring)
SEARCHING = 0
RETURNING = 1
# Colors for matplotlib plotting (Ants)
ANT_COLORS = ['red', 'blue'] # red for searching, blue for returning
# --- Helper Functions (mostly same) ---
def normalize_angle(angle):
while angle > math.pi: angle -= 2 * math.pi
while angle <= -math.pi: angle += 2 * math.pi
return angle
def world_to_pheromone_grid(pos):
x, y = pos
px = int(x / PHEROMONE_RESOLUTION_FACTOR)
py = int(y / PHEROMONE_RESOLUTION_FACTOR)
# Clamp values to be within grid bounds
px = max(0, min(P_WIDTH - 1, px))
py = max(0, min(P_HEIGHT - 1, py))
return px, py
def is_within_bounds(pos, w=WIDTH, h=HEIGHT):
x, y = pos
return 0 <= x < w and 0 <= y < h
# --- Ant Class (logic mostly same, removed drawing) ---
class Ant:
def __init__(self, nest_pos):
self.pos = np.array(nest_pos, dtype=float)
self.angle = random.uniform(0, 2 * math.pi)
self.state = SEARCHING
self.speed = ANT_SPEED
self.nest_pos = np.array(nest_pos, dtype=float)
self.color = ANT_COLORS[self.state]
def sense_pheromones(self, pheromone_grid):
sensor_main_pos = self.pos + SENSOR_DISTANCE * np.array([math.cos(self.angle), math.sin(self.angle)])
sensor_left_pos = self.pos + SENSOR_DISTANCE * np.array([math.cos(self.angle - SENSOR_ANGLE), math.sin(self.angle - SENSOR_ANGLE)])
sensor_right_pos = self.pos + SENSOR_DISTANCE * np.array([math.cos(self.angle + SENSOR_ANGLE), math.sin(self.angle + SENSOR_ANGLE)])
# Read pheromone levels only if sensors are within bounds
p_main, p_left, p_right = 0, 0, 0
if is_within_bounds(sensor_main_pos):
px, py = world_to_pheromone_grid(sensor_main_pos)
p_main = pheromone_grid[px, py]
if is_within_bounds(sensor_left_pos):
px, py = world_to_pheromone_grid(sensor_left_pos)
p_left = pheromone_grid[px, py]
if is_within_bounds(sensor_right_pos):
px, py = world_to_pheromone_grid(sensor_right_pos)
p_right = pheromone_grid[px, py]
return p_main, p_left, p_right
def steer(self, p_main, p_left, p_right):
if p_main > p_left and p_main > p_right:
pass # Go straight
elif p_left > p_right:
self.angle -= ANT_ROTATION_SPEED
elif p_right > p_left:
self.angle += ANT_ROTATION_SPEED
else:
self.angle += random.uniform(-ANT_ROTATION_SPEED * 0.5, ANT_ROTATION_SPEED * 0.5)
self.angle = normalize_angle(self.angle)
def move(self):
self.pos += self.speed * np.array([math.cos(self.angle), math.sin(self.angle)])
# Boundary bouncing
if not (0 < self.pos[0] < WIDTH):
self.angle = math.pi - self.angle
self.pos[0] = np.clip(self.pos[0], 1, WIDTH - 1)
if not (0 < self.pos[1] < HEIGHT):
self.angle = -self.angle
self.pos[1] = np.clip(self.pos[1], 1, HEIGHT - 1)
self.angle = normalize_angle(self.angle)
def update(self, pheromone_food, pheromone_home, food_sources):
original_state = self.state # Remember state before potential change
if self.state == SEARCHING:
p_main, p_left, p_right = self.sense_pheromones(pheromone_food) # Sense food trail
self.steer(p_main, p_left, p_right)
# Check for food
for food_pos, food_radius in food_sources:
if np.linalg.norm(self.pos - food_pos) < food_radius:
self.state = RETURNING
self.angle = normalize_angle(self.angle + math.pi) # Turn around
break # Found food, stop checking others
elif self.state == RETURNING:
# Drop 'food' pheromones BEFORE sensing 'home' trail
px, py = world_to_pheromone_grid(self.pos)
pheromone_food[px, py] = min(MAX_PHEROMONE, pheromone_food[px, py] + PHEROMONE_DROP_RATE)
p_main, p_left, p_right = self.sense_pheromones(pheromone_home) # Sense home trail
self.steer(p_main, p_left, p_right)
# Check for nest
if np.linalg.norm(self.pos - self.nest_pos) < 15: # Nest radius
self.state = SEARCHING
self.angle = normalize_angle(self.angle + math.pi) # Turn around
# Only move if the state didn't *just* change (prevents moving away immediately after finding food/nest)
if self.state == original_state:
self.move()
else:
# Update color immediately if state changed
self.color = ANT_COLORS[self.state]
# After moving (or state change), ants returning home deposit home pheromone at new location
if self.state == SEARCHING and original_state == RETURNING: # Just arrived at nest
px, py = world_to_pheromone_grid(self.pos)
pheromone_home[px, py] = min(MAX_PHEROMONE, pheromone_home[px, py] + PHEROMONE_DROP_RATE * 2) # Stronger drop at nest?
# --- Simulation Setup ---
# Environment elements
nest_pos = (WIDTH // 2, HEIGHT // 2)
food_sources = [
(np.array([WIDTH * 0.8, HEIGHT * 0.2]), 20),
(np.array([WIDTH * 0.2, HEIGHT * 0.8]), 20),
]
# Initialize ants
ants = [Ant(nest_pos) for _ in range(N_ANTS)]
# Pheromone grids
pheromone_food = np.zeros((P_WIDTH, P_HEIGHT), dtype=float)
pheromone_home = np.zeros((P_WIDTH, P_HEIGHT), dtype=float)
# --- Matplotlib Visualization Setup ---
fig, ax = plt.subplots(figsize=(10, 7.5)) # Adjust figsize as needed
ax.set_xlim(0, WIDTH)
ax.set_ylim(0, HEIGHT)
ax.set_aspect('equal')
ax.set_facecolor('black') # Set background color
plt.xticks([])
plt.yticks([])
plt.title("Ant Foraging Simulation (Matplotlib)")
# Create plot elements to update
# Pheromones: Use imshow with separate colormaps and alpha blending
# Normalize pheromone values for color mapping (0 to 1)
norm_ph = plt.Normalize(0, MAX_PHEROMONE * 0.5) # Adjust max value for better visibility if needed
im_food = ax.imshow(pheromone_food.T, cmap='Greens', alpha=0.6, origin='lower',
extent=[0, WIDTH, 0, HEIGHT], interpolation='bilinear', norm=norm_ph)
im_home = ax.imshow(pheromone_home.T, cmap='Blues', alpha=0.6, origin='lower',
extent=[0, WIDTH, 0, HEIGHT], interpolation='bilinear', norm=norm_ph)
# Ants: Use scatter plot. Store the scatter object to update its offsets and colors.
ant_positions = np.array([ant.pos for ant in ants])
ant_colors_array = [ant.color for ant in ants]
scatter_ants = ax.scatter(ant_positions[:, 0], ant_positions[:, 1], s=10, c=ant_colors_array) # s is marker size
# Static elements (Nest and Food)
ax.scatter(nest_pos[0], nest_pos[1], s=150, c='saddlebrown', marker='o', label='Nest') # Use s for size
for food_pos, food_radius in food_sources:
ax.scatter(food_pos[0], food_pos[1], s=food_radius*15, c='lime', marker='o', label='Food') # Scale radius for size
# --- Animation Update Function ---
def update(frame):
global pheromone_food, pheromone_home, ants
# 1. Update Pheromones (Evaporation)
pheromone_food *= EVAPORATION_RATE
pheromone_home *= EVAPORATION_RATE
# Clip minimum value (optional, prevents floating point issues)
pheromone_food[pheromone_food < 0.01] = 0
pheromone_home[pheromone_home < 0.01] = 0
# 2. Update Ants
new_positions = np.zeros((N_ANTS, 2))
new_colors = [None] * N_ANTS
for i, ant in enumerate(ants):
ant.update(pheromone_food, pheromone_home, food_sources)
new_positions[i] = ant.pos
new_colors[i] = ant.color # Update color based on state
# 3. Update Plot Elements
# Pheromones (set data for imshow objects) - Transpose needed as imshow expects (row, col) -> (y, x)
im_food.set_data(pheromone_food.T)
im_home.set_data(pheromone_home.T)
# Ants (update scatter plot offsets and colors)
scatter_ants.set_offsets(new_positions)
scatter_ants.set_facecolors(new_colors) # Use set_facecolors for individual colors
# Update title with frame number (optional)
ax.set_title(f"Ant Foraging Simulation (Frame: {frame})")
# Return list of modified plot elements
return [im_food, im_home, scatter_ants]
# --- Create and Display Animation ---
# Note: interval is the delay between frames in milliseconds.
# Setting blit=True generally improves performance but can sometimes cause issues. Try False if it glitches.
# frames = number of frames to generate. Can be set high or left None for continuous (until stopped)
ani = animation.FuncAnimation(fig, update, frames=1500, interval=30, blit=True)
# Close the initial static plot display
plt.close(fig)
# Display the animation in Jupyter using HTML
# This generates Javascript/HTML to embed the animation
print("Generating animation... (this may take a moment)")
start_time = time.time()
html_output = HTML(ani.to_jshtml())
end_time = time.time()
print(f"Animation generated in {end_time - start_time:.2f} seconds.")
# ani.save('ant_simulation.mp4', writer='ffmpeg', fps=30)
html_output # Display the animation
Generating animation... (this may take a moment)
8.2.2 Bienenschwarm-Algorithmus (ABC)¶
Der Bienenschwarm-Algorithmus (Artificial Bee Colony, ABC) ist eine stochastische Optimierungsmethode, die an das Futtersuchverhalten von Honigbienen angelehnt ist. Im Bienenstock gibt es drei Typen von Bienen: Sammelbienen (employed bees), die bekannte Nahrungsquellen und deren Umgebung erkunden, Beobachterbienen (onlooker bees), die basierend auf Informationen der Sammelbienen bekannte Nahrungsquellen auswählen und deren Umgebung erkunden wie Sammelbienen, und Kundschafterbienen (scout bees), die völlig zufällig nach neuen Nahrungsquellen suchen.
Jede Nahrungsquelle entspricht dabei einem Satz von Parametern im Suchraum, also z.B. Koordinaten. Durch eine vorgegebene Fitness-Funktion wird jeder Nahrungsquelle ein Wert (Qualität) zugeordnet. Zu jeder Zeit sind immer eine bestimmte Anzahl von Nahrungsquellen bekannt, das entspricht etwa der Population, die wir von den Genetischen Algorithmen kennen.
Der Algorithmus selbst arbeitet in Zyklen:
- Sammelbienen erkunden die Nachbarschaft ihrer zugewiesenen Nahrungsquellen und teilen die Qualitätsinformation mit den Beobachterbienen.
- Beobachterbienen wählen probabilistisch Nahrungsquellen basierend auf deren Qualität aus (bessere Quellen haben höhere Auswahlwahrscheinlichkeit).
- Wenn in der unmittelbaren Umgebung einer Nahrungsquelle nach mehreren Versuchen keine Verbesserung gefunden werden kann, wird sie aufgegeben und die entsprechende Sammelbiene wird zur Kundschafterbiene, die dann völlig zufällig eine neue Quelle sucht.
ABC balanciert effektiv zwischen Exploitation (durch Sammler- und Beobachterbienen) und Exploration (durch Kundschafterbienen). Der Algorithmus benötigt wenige Kontrollparameter und zeigt gute Leistung bei verschiedenen Optimierungsproblemen. Die Nahrungsquellen-Aufgabemechanismus verhindert, dass der Algorithmus in schlechten lokalen Optima stecken bleibt, während die probabilistische Auswahl der Beobachterbienen für Diversität in der Suche sorgt.
Insgesamt hat dieser Algorithmus zwar hauptsächlich das Setup einer zentral gesteuerten stochastischen Optimierung, man kann die Optimierungsschritte allerdings auch tatsächlich dezentralisieren und von unabhängigen Agenten ausführen lassen.
8.2.3 Glühwürmchen-Algorithmus¶
Der Glühwürmchen-Algorithmus (Firefly Algorithm) basiert auf dem Paarungsverhalten von Glühwürmchen, die durch ihr Leuchten Partner anziehen. Die Grundannahmen sind: Alle Glühwürmchen sind unisex und können sich gegenseitig anziehen, die Attraktivität ist proportional zur Helligkeit und nimmt mit der Distanz ab, und die Helligkeit wird durch die Zielfunktion (Fitness in einem Optimierungsproblem) bestimmt. Glühwürmchen bewegen sich also schrittweise zu helleren (besseren) Glühwürmchen in ihrer Umgebung hin.
Die Iteration läuft wie folgt ab:
- nach jedem Iterationsschritt über alle Glühwürmchen wird die Helligkeit aller Glühwürmchen neu berechnet
- jedes Glühwürmchen bestimmt innerhalb eines Interationsschritts der Reihe nach alle jene, die heller sind als es selbst
- für jedes hellere Glühwürmchen verändert das Glühwürmchen dann seine Position in dessen Richtung, gewichtet mit einer Funktion der Entfernung, und ergänzt durch einen kleinen zusätzlichen Schritt in zufälliger Richtung
- wenn jedes Glühwürmchen der Reihe nach auf diese Weise jeweils seinen eigenen kleinen Pfad zurückgelegt hat, ist ein Iterationsschritt beendet
Der Algorithmus eignet sich besonders für multimodale Optimierungsprobleme, da mehrere Glühwürmchen-Gruppen sich um verschiedene lokale Optima bilden können. Die automatische Unterteilung des Schwarms basierend auf lokaler Attraktivität ermöglicht die gleichzeitige Erforschung mehrerer vielversprechender Regionen. Hier sehen wir uns wieder eine Visualisierung an. Wir erkunden dabei eine neue, ebenfalls nicht ganz einfach zu optimierende Funktion (Schwefel), ähnlich zu jener Funktion, die wir bereits kennen (Rastrigin).
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
import warnings
# Suppress Matplotlib animation warning if it occurs
warnings.filterwarnings("ignore", category=UserWarning, module="matplotlib.animation")
# --- Fitness Functions (for minimization) ---
def rastrigin_2d(x, y):
"""Rastrigin function in 2D."""
return 20 + (x**2 - 10 * np.cos(2 * np.pi * x)) + \
(y**2 - 10 * np.cos(2 * np.pi * y))
def schwefel_2d(x, y):
"""Schwefel function in 2D."""
# Global minimum is at x=y=420.9687, f(x,y)=0
# To make it a minimization problem with min near 0 for typical FA brightness:
# We can use the standard form: 418.9829 * 2 - (x * sin(sqrt|x|) + y * sin(sqrt|y|))
# Or a shifted/scaled one if needed for brightness calculation,
# but for now, let's use the standard.
term1 = x * np.sin(np.sqrt(np.abs(x)))
term2 = y * np.sin(np.sqrt(np.abs(y)))
return 418.9829 * 2 - (term1 + term2)
# --- Function Configuration ---
# Choose which function to use: 'rastrigin' or 'schwefel'
SELECTED_FUNCTION = 'schwefel' #@param ['rastrigin', 'schwefel']
if SELECTED_FUNCTION == 'rastrigin':
fitness_function = rastrigin_2d
DOMAIN_MIN = -5.12
DOMAIN_MAX = 5.12
GLOBAL_MIN_POS = np.array([0.0, 0.0])
GLOBAL_MIN_VAL = 0.0
elif SELECTED_FUNCTION == 'schwefel':
fitness_function = schwefel_2d
DOMAIN_MIN = -500.0
DOMAIN_MAX = 500.0
GLOBAL_MIN_POS = np.array([420.9687, 420.9687])
GLOBAL_MIN_VAL = 0.0 # Schwefel is 0 at its global min
else:
raise ValueError("Invalid function selected")
# For scaling the random walk
DOMAIN_WIDTH = DOMAIN_MAX - DOMAIN_MIN
# Firefly Algorithm Parameters
N_FIREFLIES = 50 # Number of fireflies
MAX_GENERATIONS = 100 # Maximum number of generations
ALPHA = 0.01 # Randomness factor (for random walk part)
BETA0 = 0.01 # Attractiveness at r=0
GAMMA = 0.5 # Light absorption coefficient
# For Rastrigin with domain [-5.12, 5.12], r^2 can be up to (10.24*sqrt(2))^2 approx 200.
# If gamma=0.5, exp(-100) is tiny.
# If gamma=0.01, exp(-2) approx 0.13.
# Let's adjust gamma based on domain, or make it smaller.
# For a domain width of ~10, r_max_sq is ~200.
# For a domain width of ~1000 (Schwefel), r_max_sq is ~2,000,000
# We need gamma * r^2 to be in a reasonable range.
# Let's make gamma inversely proportional to characteristic squared length scale
CHARACTERISTIC_LENGTH_SQUARED = (DOMAIN_WIDTH)**2 / 10 # Heuristic
GAMMA = 1.0 / CHARACTERISTIC_LENGTH_SQUARED if CHARACTERISTIC_LENGTH_SQUARED > 0 else 0.1
ALPHA_DELTA = 0.97 # Optional: factor to reduce alpha over generations
# Initialize fireflies
# Each firefly is [x, y]
firefly_positions = np.random.uniform(DOMAIN_MIN, DOMAIN_MAX, (N_FIREFLIES, 2))
firefly_brightness = np.zeros(N_FIREFLIES)
firefly_fitness = np.zeros(N_FIREFLIES)
# Best solution found so far
global_best_pos = None
global_best_fitness = float('inf')
def calculate_brightness(fitness_values):
# Assuming minimization, fitness >= 0. Higher brightness = better (lower fitness)
# Add a small epsilon to avoid division by zero if fitness is 0
return 1.0 / (1.0 + fitness_values + 1e-6)
# Initial fitness and brightness calculation
for i in range(N_FIREFLIES):
x, y = firefly_positions[i]
f_val = fitness_function(x, y)
firefly_fitness[i] = f_val
if f_val < global_best_fitness:
global_best_fitness = f_val
global_best_pos = firefly_positions[i].copy()
firefly_brightness = calculate_brightness(firefly_fitness)
print(f"Using function: {SELECTED_FUNCTION}")
print(f"Domain: [{DOMAIN_MIN}, {DOMAIN_MAX}]")
print(f"Gamma: {GAMMA:.4e}")
print(f"Initial best fitness: {global_best_fitness:.4f} at {global_best_pos}")
def update_fireflies_and_get_best(positions, brightness, current_fitness,
current_global_best_pos, current_global_best_fitness):
n = positions.shape[0]
num_dims = positions.shape[1]
new_positions = np.copy(positions) # Work on a copy for this generation's new positions
# Store current brightness for comparisons within this generation
# This is I(t) based on positions(t)
current_brightness_for_comparison = np.copy(brightness)
for i in range(n):
# This firefly's position will be updated based on attractions
# It starts from its position at time t
x_i_candidate = np.copy(positions[i])
# Flag to check if firefly 'i' was attracted and moved by any 'j'
# This helps decide if it needs a standalone random walk (if it's a global best type)
attracted_and_moved = False
for j in range(n):
if i == j:
continue
# Compare based on brightness at the start of the generation
if current_brightness_for_comparison[j] > current_brightness_for_comparison[i]:
attracted_and_moved = True
# Calculate distance: r_ij
# Distance is between x_i_candidate (which evolves within this i-loop)
# and positions[j] (which is fixed at t for this generation)
r_sq = np.sum((x_i_candidate - positions[j])**2)
beta = BETA0 * np.exp(-GAMMA * r_sq)
# Random vector for this specific interaction
# Scale factor ensures random step is proportional to domain width
random_vec = ALPHA * (np.random.rand(num_dims) - 0.5) * DOMAIN_WIDTH
# Update x_i_candidate based on attraction to this j AND its own random jitter for THIS interaction
x_i_candidate += beta * (positions[j] - x_i_candidate) + random_vec
# Apply boundary conditions immediately after each micro-move
x_i_candidate = np.clip(x_i_candidate, DOMAIN_MIN, DOMAIN_MAX)
# If firefly 'i' was not attracted to any brighter firefly (e.g., it's the current global best or in a "dark" area alone)
# it performs a simple random walk.
if not attracted_and_moved:
random_vec_standalone = ALPHA * (np.random.rand(num_dims) - 0.5) * DOMAIN_WIDTH
x_i_candidate += random_vec_standalone
x_i_candidate = np.clip(x_i_candidate, DOMAIN_MIN, DOMAIN_MAX)
new_positions[i] = x_i_candidate
# Now, evaluate fitness for all new positions and update brightness
new_fitness = np.zeros(n)
updated_global_best_pos = current_global_best_pos.copy() if current_global_best_pos is not None else None
updated_global_best_fitness = current_global_best_fitness
for i in range(n):
f_val = fitness_function(new_positions[i, 0], new_positions[i, 1])
new_fitness[i] = f_val
if f_val < updated_global_best_fitness:
updated_global_best_fitness = f_val
updated_global_best_pos = new_positions[i].copy()
new_brightness = calculate_brightness(new_fitness)
return new_positions, new_brightness, new_fitness, updated_global_best_pos, updated_global_best_fitness
fig, ax = plt.subplots(figsize=(8, 6))
ax.set_xlim(DOMAIN_MIN, DOMAIN_MAX)
ax.set_ylim(DOMAIN_MIN, DOMAIN_MAX)
# Plot contour of the fitness function
x_grid = np.linspace(DOMAIN_MIN, DOMAIN_MAX, 100)
y_grid = np.linspace(DOMAIN_MIN, DOMAIN_MAX, 100)
X_mesh, Y_mesh = np.meshgrid(x_grid, y_grid)
Z_mesh = fitness_function(X_mesh, Y_mesh)
contour = ax.contourf(X_mesh, Y_mesh, Z_mesh, levels=50, cmap='viridis') # Or 'plasma', 'magma'
fig.colorbar(contour, ax=ax, label='Fitness Value')
# Plot global minimum if known
if GLOBAL_MIN_POS is not None:
ax.plot(GLOBAL_MIN_POS[0], GLOBAL_MIN_POS[1], 'X', color='red', markersize=10, label=f'Global Min ({GLOBAL_MIN_VAL:.2f})')
ax.legend(loc='upper right')
# Initial scatter plot for fireflies
# We can use brightness to modulate color or size if desired, but simple points for now
# firefly_colors = firefly_brightness / np.max(firefly_brightness + 1e-6) # Normalize for color
scatter = ax.scatter(firefly_positions[:, 0], firefly_positions[:, 1],
color='orange', edgecolor='black', s=50, zorder=3, label='Fireflies') # s=size
title = ax.set_title(f"Generation: 0, Best Fitness: {global_best_fitness:.4f}")
# Animation function
def animate(generation):
global firefly_positions, firefly_brightness, firefly_fitness, global_best_pos, global_best_fitness
firefly_positions, firefly_brightness, firefly_fitness, \
global_best_pos, global_best_fitness = update_fireflies_and_get_best(
firefly_positions,
firefly_brightness, # Pass current brightness for comparison
firefly_fitness, # Pass current fitness (for info, not direct use in update logic)
global_best_pos,
global_best_fitness
)
scatter.set_offsets(firefly_positions)
# Optionally update firefly colors/sizes based on new brightness
# norm_brightness = firefly_brightness / (np.max(firefly_brightness) + 1e-6)
# scatter.set_array(norm_brightness) # If using color to represent brightness
title.set_text(f"Generation: {generation + 1}, Best Fitness: {global_best_fitness:.4f}")
if (generation + 1) % 10 == 0 or generation == MAX_GENERATIONS -1 : # Print progress
print(f"Gen: {generation + 1}/{MAX_GENERATIONS}, Best Fit: {global_best_fitness:.4f}, Pos: {global_best_pos}")
return scatter, title,
# Create and display animation
# blit=False because title is also an artist we are returning and it might cause issues with blit=True sometimes
ani = FuncAnimation(fig, animate, frames=MAX_GENERATIONS, interval=150, blit=False, repeat=False)
plt.close(fig) # Prevent static plot from showing up before the animation
HTML(ani.to_jshtml())
# To save as mp4 (ensure ffmpeg is installed):
# ani.save('firefly_algorithm.mp4', writer='ffmpeg', fps=15)
# print("Animation saved as firefly_algorithm.mp4")
Using function: schwefel Domain: [-500.0, 500.0] Gamma: 1.0000e-05 Initial best fitness: 284.3820 at [220.08912333 404.38937103] Gen: 10/100, Best Fit: 229.5933, Pos: [ 391.09001972 -308.70720605] Gen: 20/100, Best Fit: 184.8533, Pos: [ 425.18732216 -325.30239492] Gen: 30/100, Best Fit: 156.0981, Pos: [ 415.7345098 -319.0452039] Gen: 40/100, Best Fit: 126.5209, Pos: [ 415.52602919 -308.38898899] Gen: 50/100, Best Fit: 121.5788, Pos: [ 419.42406253 -307.25790453] Gen: 60/100, Best Fit: 121.5788, Pos: [ 419.42406253 -307.25790453] Gen: 70/100, Best Fit: 121.5788, Pos: [ 419.42406253 -307.25790453] Gen: 80/100, Best Fit: 121.5788, Pos: [ 419.42406253 -307.25790453] Gen: 90/100, Best Fit: 121.5788, Pos: [ 419.42406253 -307.25790453] Gen: 100/100, Best Fit: 121.5788, Pos: [ 419.42406253 -307.25790453]
8.3. Selbstorganisation und kollektive Entscheidungsfindung¶
Nachdem wir nun verschiedene bioinspirierte Schwarm-Algorithmen kennengelernt haben, möchte ich Ihnen noch die Mechanismen der Selbstorganisation und kollektiven Entscheidungsfindung näher bringen. Diese Mechanismen sind wesentliche Aspekte von Schwarm-Algorithmen und machen deutlicher, warum und wie dezentrale Systeme ohne globale Koordination effektive Lösungen finden können.
8.3.1 Prinzipien der Selbstorganisation in biologischen Systemen¶
Selbstorganisation bezeichnet die spontane Entstehung von Ordnung und Struktur in einem System ohne externe Steuerung oder zentralen Plan. Dieser Prozess ist in der Natur allgegenwärtig – von der Bildung von Schneeflocken bis zur Entstehung komplexer Ökosysteme. In biologischen Schwärmen manifestiert sich Selbstorganisation in der Eigenschaft, komplexe kollektive Verhaltensweisen zu generieren, ohne dass diese explizit “programmiert” bzw. “vorgesehen” sind.
Die Selbstorganisation in biologischen Systemen basiert auf einigen Mechanismen und zeigt typische Phänomene. Zu den Mechanismen zählen die positive und negative Rückkopplung, die uns bereits oben begegnet sind. Diese beiden ermöglichen, dass ein System in einen geordneteren Zustand geht und diesen stabilisiert. Dazu kommt, dass solche Rückkoppelung oft zu einer Verstärkung von zufälligen Fluktuationen führen, woraus wieder Muster und neue Strukturen entstehen können, z.B. die Pheromon-Pfade im Ameisen-Beispiel oben.
Außerdem ist es wichtig, nochmals festzuhalten, dass die Wechselwirkungen zwischen Individuen/Agenten lokal sind, d.h., es genügt völlig, wenn jeder Agent nur mit seinen Nachbarn bzw. überhaupt nur mit seiner Umgebung interagiert. Dadurch steigt auch die Robustheit gegenüber Ausfällen oder Veränderungen einzelner Agenten sowie die Skalierbarkeit zu schier riesigen Schwärmen ohne dass es deshalb in einer zentralen Leitungsstelle eine Überlastung gäbe.
Die Stärke selbstorganisierender Systeme liegt also in ihrer Robustheit, Flexibilität und Skalierbarkeit. Sie können sich an veränderte Umgebungsbedingungen anpassen, bleiben funktionsfähig trotz des Ausfalls einzelner Komponenten und können mit zunehmender Größe sogar effektiver werden. Diese Eigenschaften machen biologische Selbstorganisation zu einem attraktiven Vorbild für technische Systeme, von Telekommunikationsnetzwerken bis hin zu Roboterschwärmen.
8.3.2 Emergente Koordination ohne zentrale Kontrolle¶
Ein faszinierendes Phänomen selbstorganisierender Systeme ist die emergente Koordination: Ohne zentrale Steuerung oder explizite Kommunikation über globale Ziele entwickeln Schwärme synchronisierte, koordinierte Verhaltensweisen. Diese Koordination manifestiert sich auf verschiedenen Ebenen, von einfacher Bewegungssynchronisation bis hin zu komplexer Arbeitsteilung. Dabei hängt das Auftreten solcher emergenter Phänomene oft von der “Dichte” der Agenten im System ab. Man spricht daher im Zusammenhang mit dem Auftreten emergenter Phänomene auch von Phasenübergängen in komplexen Systemen.
In künstlichen Systemen findet emergente Koordination vielfältige Anwendungen. Roboterschwärme nutzen lokale Koordinationsregeln für Formation Control oder kollaborative Objektmanipulation. Drahtlose Sensornetzwerke verwenden dezentrale Protokolle zur Selbstorganisation in effiziente Netzwerktopologien. Autonome Fahrzeuge könnten zukünftig schwarm-ähnliche Koordination für problemlosen Verkehrsfluss ohne zentrale Ampelsteuerung nutzen.
Die Herausforderung beim Design solcher Systeme liegt darin, lokale Regeln zu finden, die zuverlässig das gewünschte globale Verhalten erzeugen – ein oft nicht-triviales inverses Problem, das Expertenwissen und viele Test-Simulationen erfordert.
8.3.3 Stigmergie: Kommunikation durch Umgebungsmodifikation¶
Stigmergie ist eine besondere Form der indirekten Kommunikation in Schwärmen, bei der Individuen nicht direkt miteinander kommunizieren, sondern durch Modifikation ihrer Umgebung. Wir haben das im Ameisen-Beispiel oben bereits gesehen: Ein Agent hinterlässt (Pheromon-)Spuren in der Umgebung, die das Verhalten anderer Agenten beeinflussen. Noch konkreter hinterlässt eine Ameise, die eine Nahrungsquelle findet, auf ihrem Rückweg zum Nest eine chemische Spur. Andere Ameisen nehmen diese Spur wahr und folgen ihr mit höherer Wahrscheinlichkeit. Wenn auch sie Nahrung finden, verstärken sie die Spur auf ihrem Rückweg. Dieses indirekte Kommunikationssystem ermöglicht es Ameisenkolonien, effiziente Wege zu Nahrungsquellen zu etablieren, ohne dass einzelne Ameisen den gesamten Weg kennen oder direkt miteinander kommunizieren müssen.
Stigmergie hat mehrere bemerkenswerte Eigenschaften:
- Entkopplung von Sender und Empfänger: Die kommunizierenden Individuen müssen nicht gleichzeitig am selben Ort sein oder einander direkt wahrnehmen.
- Persistenz: Die Umgebungsmodifikationen bleiben bestehen und wirken über den Moment der Erzeugung hinaus, fungieren also als externes “Gedächtnis” des Schwarms (bis die Pheromonspur wieder “verdunstet”).
- Selbstverstärkung: Erfolgreiche Lösungen werden durch positive Rückkopplung verstärkt.
- Skalierbarkeit: Der Kommunikationsaufwand wächst nicht mit der Anzahl der Individuen, da die Kommunikation über die Umgebung erfolgt. Ebenso brauchen die Agenten selbst keinen Speicher für die Spuren (oder im Allgemeinen die in der Umgebung gespeicherten Informationen).
In der Natur finden sich zahlreiche weitere Beispiele für Stigmergie:
- Termiten bauen komplexe Nester, indem sie Bodenmaterial mit Pheromonen markieren, was andere Termiten dazu anregt, dort weiteres Material abzulegen
- Bienen nutzen die Wachsstrukturen im Bienenstock als Stimulus für weitere Bauaktivitäten
- Spinnen nutzen ihre eigenen Netze als externe Informationsspeicher, die ihnen signalisieren, wo sich Beute verfangen hat
In der Informatik hat das Konzept der Stigmergie breite Anwendung gefunden. Die Ameisenkolonie-Optimierung ist ein direktes Beispiel, bei dem virtuelle Pheromone zur Pfadoptimierung genutzt werden. Darüber hinaus finden sich stigmergische Prinzipien in:
- Verteilten Robotersystemen, bei denen Roboter RFID-Tags oder andere Marker in der Umgebung platzieren
- Empfehlungssystemen, bei denen die Nutzeraktivität die “digitale Umgebung” für andere Nutzer verändert
- Kollaborativen Filtersystemen, die auf dem aggregierten Verhalten vieler Nutzer basieren
- Versionskontrollsystemen wie Git, bei denen Entwickler Spuren ihrer Arbeit hinterlassen, die andere Entwickler beeinflussen
Die Stärke der Stigmergie liegt in ihrer Fähigkeit, Komplexität zu bewältigen, ohne komplexe Kommunikation oder Kognition (oder überhaupt Systemspeicher) zu erfordern – ein Prinzip, das sowohl in natürlichen als auch in künstlichen Schwärmen zentrale Bedeutung hat.
8.3.4 Quorum-Sensing und Konsensbildung¶
Kollektive Entscheidungsfindung ist eine der beeindruckendsten Fähigkeiten von Schwärmen. Ohne zentrale Autorität müssen Gruppen zwischen Alternativen wählen, Kompromisse finden und zu einem Konsens gelangen. Quorum-Sensing ist ein biologischer Mechanismus, der solche kollektiven Entscheidungen ermöglicht.
Ursprünglich bei Bakterien entdeckt, beschreibt Quorum-Sensing die Fähigkeit von Organismen, ihre eigene Populationsdichte wahrzunehmen und ihr Verhalten entsprechend anzupassen. Bakterien sekretieren Signalmoleküle, deren Konzentration mit der Populationsdichte steigt. Überschreitet die Konzentration einen Schwellenwert, aktivieren die Bakterien bestimmte Gene, die kollektives Verhalten koordinieren wie z.B. die Bildung eines Biofilms.
Soziale Insekten nutzen ähnliche Prinzipien für komplexere Entscheidungsprozesse. Ein klassisches Beispiel ist die Nistplatzwahl bei Honigbienen. Wenn ein Bienenschwarm eine neue Behausung suchen muss, erkunden Kundschafterbienen verschiedene potenzielle Nistplätze. Zurück im Schwarm werben sie durch Tänze für ihre Entdeckungen, wobei die Intensität des Tanzes proportional zur Qualität des gefundenen Ortes ist. Andere Kundschafter untersuchen diese Orte und beginnen ebenfalls zu werben, wenn sie überzeugt sind. Sobald für einen bestimmten Ort ein Quorum (kritische Masse an Befürwortern) erreicht ist, ändert sich das Verhalten der werbenden Bienen: Sie produzieren ein Signal, das den gesamten Schwarm zum Aufbruch veranlasst.
8.4 Anwendungen der Schwarm-Algorithmen und Schwarm-Optimierung¶
Nach all den Grundlagen und Beispielen für Schwarm-Intelligenz und selbstorganisierende Systeme möchte ich Ihnen noch ein paar weitere Gedanken und Anwendungen zeigen. Wie Sie bereits gesehen haben, taugen Schwärme nicht nur zur Simulation von komplexen Systemen in der Natur, sondern können auch zur Optimierung eingesetzt werden. Bisher haben wir die biologischen Systeme als Basis für Simulation und Visualisierung verwendet. Nun möchte ich aber einen Schritt in Richtung Abstraktion machen.
8.4.1 Partikelschwarm-Optimierung (PSO)¶
Die Partikelschwarm-Optimierung (PSO) ist einer der bekanntesten und am häufigsten eingesetzten Schwarm-Optimierungs-Algorithmen. Inspiriert vom Schwarmverhalten von Vögeln und Fischen, modelliert PSO eine Population von Lösungskandidaten (Partikel) als Punkte mit Koordinaten und Geschwindigkeiten im n-dimensionalen Suchraum, die sich kollektiv bewegen, um optimale Regionen bzw. Lösungen zu finden.
Grundprinzip und Algorithmus¶
Im PSO-Algorithmus hat jedes Partikel eine Position und eine Geschwindigkeit. Die Position repräsentiert eine potenzielle Lösung des Optimierungsproblems, während die Geschwindigkeit bestimmt, wie sich das Partikel im Suchraum bewegt. Entscheidend ist, dass jedes Partikel drei Informationsquellen für seine Bewegung nutzt:
- Trägheit: Die aktuelle Geschwindigkeit des Partikels
- Kognitive Komponente: Die beste Position, die das Partikel selbst bisher gefunden hat (persönliches Optimum)
- Soziale Komponente: Die beste Position, die von irgendjemandem im Schwarm gefunden wurde (globales Optimum)
Die Geschwindigkeitsaktualisierung erfolgt nach der Formel:
$$v_i(t+1) = w \cdot v_i(t) + c_1 \cdot r_1 \cdot (pbest_i – x_i(t)) + c_2 \cdot r_2 \cdot (gbest – x_i(t))$$
Dabei ist:
- $v_i(t)$ die aktuelle Geschwindigkeit des Partikels $i$
- $w$ das Trägheitsgewicht (typischerweise zwischen 0.4 und 0.9)
- $c_1$ und $c_2$ die Lernfaktoren (typischerweise um 2.0)
- $r_1$ und $r_2$ Zufallszahlen zwischen 0 und 1
- $pbest_i$ die beste Position, die Partikel $i$ bisher gefunden hat
- $gbest$ die beste Position im gesamten Schwarm
- $x_i(t)$ die aktuelle Position des Partikels $i$
Die Position wird dann aktualisiert durch:
$$x_i(t+1) = x_i(t) + v_i(t+1)$$
Nach jeder Positionsaktualisierung wird die Zielfunktion an dieser neuen Position ausgewertet. Ist der Wert besser als das bisherige persönliche Optimum, wird $pbest_i$ aktualisiert. Ist er besser als das globale Optimum, wird $gbest$ aktualisiert.
Varianten und Erweiterungen¶
Die Grundversion von PSO wurde vielfältig erweitert und angepasst:
- Lokale Nachbarschaften: Statt des globalen Optimums wird nur das beste Ergebnis in einer lokalen Nachbarschaft berücksichtigt, was die Diversität erhöht und vorzeitige Konvergenz vermeidet.
- Adaptive Parameter: Das Trägheitsgewicht $w$ wird dynamisch angepasst, etwa durch lineare Abnahme über die Iterationen oder basierend auf der Schwarmperformance.
- Eingeschränkte Geschwindigkeit: Maximalgrenzen für die Geschwindigkeit verhindern zu große Sprünge im Suchraum.
- Multi-Schwarm PSO: Mehrere Teil-Schwärme erkunden parallel verschiedene Regionen des Suchraums und tauschen periodisch Informationen aus.
- Hybride Ansätze: Kombination von PSO mit anderen Metaheuristiken wie genetischen Algorithmen oder lokalen Suchmethoden.
Sehen wir uns das einmal konkret an, wieder für die Rastrigin Funktion:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
import warnings
# Suppress Matplotlib animation warning if it occurs
warnings.filterwarnings("ignore", category=UserWarning, module="matplotlib.animation")
# --- Fitness Functions (for minimization) ---
def rastrigin_2d(x, y):
"""Rastrigin function in 2D."""
return 20 + (x**2 - 10 * np.cos(2 * np.pi * x)) + \
(y**2 - 10 * np.cos(2 * np.pi * y))
def schwefel_2d(x, y):
"""Schwefel function in 2D."""
term1 = x * np.sin(np.sqrt(np.abs(x)))
term2 = y * np.sin(np.sqrt(np.abs(y)))
return 418.9829 * 2 - (term1 + term2)
# --- Function Configuration ---
# Choose which function to use: 'rastrigin' or 'schwefel'
SELECTED_FUNCTION = 'rastrigin' #@param ['rastrigin', 'schwefel']
if SELECTED_FUNCTION == 'rastrigin':
fitness_function = rastrigin_2d
DOMAIN_MIN = -5.12
DOMAIN_MAX = 5.12
GLOBAL_MIN_POS_THEORETICAL = np.array([0.0, 0.0])
GLOBAL_MIN_VAL_THEORETICAL = 0.0
elif SELECTED_FUNCTION == 'schwefel':
fitness_function = schwefel_2d
DOMAIN_MIN = -500.0
DOMAIN_MAX = 500.0
GLOBAL_MIN_POS_THEORETICAL = np.array([420.9687, 420.9687])
GLOBAL_MIN_VAL_THEORETICAL = 0.0
else:
raise ValueError("Invalid function selected")
DOMAIN_WIDTH = DOMAIN_MAX - DOMAIN_MIN
NUM_DIMS = 2 # Working in 2D
# PSO Parameters
N_PARTICLES = 30
MAX_ITERATIONS = 100
# Inertia weight: linearly decreasing from W_MAX to W_MIN
W_MAX = 0.5
W_MIN = 0.1
C1 = 0.5 # Cognitive coefficient (pbest influence)
C2 = 0.5 # Social coefficient (gbest influence)
# Velocity clamping (max velocity as a fraction of domain width per dimension)
V_MAX_FACTOR = 0.01 # Max velocity is 5% of domain width
V_MAX = V_MAX_FACTOR * DOMAIN_WIDTH
V_MIN = -V_MAX
# Initial velocity range (fraction of domain width)
V_INIT_RANGE_FACTOR = 0.05
V_INIT_MAX = V_INIT_RANGE_FACTOR * DOMAIN_WIDTH
V_INIT_MIN = -V_INIT_MAX
# Initialize particles
particle_positions = np.random.uniform(DOMAIN_MIN, DOMAIN_MAX, (N_PARTICLES, NUM_DIMS))
particle_velocities = np.random.uniform(V_INIT_MIN, V_INIT_MAX, (N_PARTICLES, NUM_DIMS))
# Personal best positions and fitness
particle_pbest_positions = np.copy(particle_positions)
particle_pbest_fitness = np.full(N_PARTICLES, float('inf'))
# Global best position and fitness
gbest_position = None
gbest_fitness = float('inf')
# Initial fitness calculation and pbest/gbest update
for i in range(N_PARTICLES):
current_fitness = fitness_function(particle_positions[i, 0], particle_positions[i, 1])
particle_pbest_fitness[i] = current_fitness
# pbest_positions are already set to initial positions
if current_fitness < gbest_fitness:
gbest_fitness = current_fitness
gbest_position = particle_positions[i].copy()
print(f"Using function: {SELECTED_FUNCTION}")
print(f"Domain: [{DOMAIN_MIN}, {DOMAIN_MAX}]")
print(f"Initial global best fitness: {gbest_fitness:.4f} at {gbest_position}")
def update_pso_and_get_best(iteration, positions, velocities,
pbest_pos, pbest_fit,
gbest_pos, gbest_fit):
n_particles = positions.shape[0]
num_dims = positions.shape[1]
# Linearly decreasing inertia weight
w = W_MAX - (W_MAX - W_MIN) * (iteration / MAX_ITERATIONS)
# Update pbest and gbest based on current positions
current_gbest_pos_iter = gbest_pos.copy() if gbest_pos is not None else None
current_gbest_fit_iter = gbest_fit
new_pbest_pos = np.copy(pbest_pos)
new_pbest_fit = np.copy(pbest_fit)
for i in range(n_particles):
current_fitness = fitness_function(positions[i, 0], positions[i, 1])
if current_fitness < new_pbest_fit[i]:
new_pbest_fit[i] = current_fitness
new_pbest_pos[i] = positions[i].copy()
if current_fitness < current_gbest_fit_iter:
current_gbest_fit_iter = current_fitness
current_gbest_pos_iter = positions[i].copy()
# Update velocities and positions
new_velocities = np.zeros_like(velocities)
new_positions = np.zeros_like(positions)
for i in range(n_particles):
r1 = np.random.rand(num_dims) # Random numbers for cognitive component
r2 = np.random.rand(num_dims) # Random numbers for social component
cognitive_velocity = C1 * r1 * (new_pbest_pos[i] - positions[i])
social_velocity = C2 * r2 * (current_gbest_pos_iter - positions[i])
new_velocities[i] = w * velocities[i] + cognitive_velocity + social_velocity
# Apply velocity clamping
new_velocities[i] = np.clip(new_velocities[i], V_MIN, V_MAX)
# Update position
new_positions[i] = positions[i] + new_velocities[i]
# Apply boundary conditions (clamping)
new_positions[i] = np.clip(new_positions[i], DOMAIN_MIN, DOMAIN_MAX)
return new_positions, new_velocities, new_pbest_pos, new_pbest_fit, current_gbest_pos_iter, current_gbest_fit_iter
# --- Ensure a clean slate for Matplotlib ---
plt.close('all')
# --- Setup the new figure and axes ---
fig, ax = plt.subplots(figsize=(8, 6))
ax.set_xlim(DOMAIN_MIN, DOMAIN_MAX)
ax.set_ylim(DOMAIN_MIN, DOMAIN_MAX)
# Plot contour of the fitness function
x_grid = np.linspace(DOMAIN_MIN, DOMAIN_MAX, 100)
y_grid = np.linspace(DOMAIN_MIN, DOMAIN_MAX, 100)
X_mesh, Y_mesh = np.meshgrid(x_grid, y_grid)
Z_mesh = fitness_function(X_mesh, Y_mesh)
contour = ax.contourf(X_mesh, Y_mesh, Z_mesh, levels=50, cmap='viridis')
fig.colorbar(contour, ax=ax, label='Fitness Value')
# Plot theoretical global minimum if known
if GLOBAL_MIN_POS_THEORETICAL is not None:
ax.plot(GLOBAL_MIN_POS_THEORETICAL[0], GLOBAL_MIN_POS_THEORETICAL[1], 'X', color='red',
markersize=10, label=f'Global Min ({GLOBAL_MIN_VAL_THEORETICAL:.2f})')
# Initial scatter plot for particles
scatter = ax.scatter(particle_positions[:, 0], particle_positions[:, 1],
color='orange', edgecolor='black', s=30, zorder=3, label='Particles')
ax.legend(loc='upper right')
title_artist = ax.set_title(f"Iteration: 0, Best Fitness: {gbest_fitness:.4f}")
# Animation function
def animate(iteration):
global particle_positions, particle_velocities, \
particle_pbest_positions, particle_pbest_fitness, \
gbest_position, gbest_fitness
# Update particle states using PSO logic
particle_positions, particle_velocities, \
particle_pbest_positions, particle_pbest_fitness, \
gbest_position, gbest_fitness = update_pso_and_get_best(
iteration,
particle_positions,
particle_velocities,
particle_pbest_positions,
particle_pbest_fitness,
gbest_position,
gbest_fitness
)
scatter.set_offsets(particle_positions) # Update particle positions on the plot
title_artist.set_text(f"Iteration: {iteration + 1}, Best Fitness: {gbest_fitness:.4f}")
if (iteration + 1) % 10 == 0 or iteration == MAX_ITERATIONS - 1:
print(f"Iter: {iteration + 1}/{MAX_ITERATIONS}, Best Fit: {gbest_fitness:.4f}, Pos: {gbest_position}")
return scatter, title_artist,
# Ensure initial state for title before animation starts
if gbest_position is not None:
title_artist.set_text(f"Iteration: 0, Best Fitness: {gbest_fitness:.4f} at ({gbest_position[0]:.2f}, {gbest_position[1]:.2f})")
else:
title_artist.set_text(f"Iteration: 0, Best Fitness: {gbest_fitness:.4f}")
# Create and display animation
ani = FuncAnimation(fig, animate, frames=MAX_ITERATIONS, interval=150, blit=False, repeat=False)
animation_html = HTML(ani.to_jshtml())
plt.close(fig)
animation_html
# To save as mp4 (ensure ffmpeg is installed):
# ani.save('pso_algorithm.mp4', writer='ffmpeg', fps=15)
# print("Animation saved as pso_algorithm.mp4")
Using function: rastrigin Domain: [-5.12, 5.12] Initial global best fitness: 1.3614 at [-0.98262003 0.04128456] Iter: 10/100, Best Fit: 0.9990, Pos: [-0.99198636 0.00336854] Iter: 20/100, Best Fit: 0.9989, Pos: [-0.99186596 0.00319872] Iter: 30/100, Best Fit: 0.9989, Pos: [-0.99185077 0.00318149] Iter: 40/100, Best Fit: 0.2639, Pos: [-0.03212817 0.01736696] Iter: 50/100, Best Fit: 0.1165, Pos: [-0.024209 0.00142669] Iter: 60/100, Best Fit: 0.0241, Pos: [0.00830773 0.00723772] Iter: 70/100, Best Fit: 0.0033, Pos: [-0.00142538 0.00383087] Iter: 80/100, Best Fit: 0.0033, Pos: [-0.00142535 0.00383087] Iter: 90/100, Best Fit: 0.0033, Pos: [-0.00142535 0.00383087] Iter: 100/100, Best Fit: 0.0033, Pos: [-0.00142535 0.00383087]
8.4.2 Konkrete Optimierungsprobleme¶
Schwarm-Algorithmen haben sich bei der Lösung verschiedener klassischer und praktischer Optimierungsprobleme bewährt. Hier sei zunächst das Problem des Handlungsreisenden (TSP) erwähnt, das wir schon kennen. Hier hat sich die Ameisenkolonie-Optimierung bewährt: Virtuelle Ameisen konstruieren Touren durch probabilistische Entscheidungen, die von Pheromonspuren und heuristischen Informationen (typischerweise Distanzen) beeinflusst werden. Nach jeder Iteration werden die Pheromonspuren aktualisiert, wobei kürzere Touren mehr Pheromone erhalten.
Eine weiter Anwendung sind Scheduling-Probleme. Diese treten in der Fertigungsplanung, Projektplanung und Ressourcenallokation auf. Sie umfassen die zeitliche Planung von Aufgaben unter Berücksichtigung von Ressourcenbeschränkungen, Prioritäten und Abhängigkeiten. Hier kann man z.B. Bienenschwarm-Optimierung gut einsetzen.
Und dann sollen noch Routing-Probleme in Netzwerken erwähnt werden. Diese betreffen die Bestimmung optimaler Pfade für Daten in Kommunikationsnetzwerken oder für Fahrzeuge in Transportnetzwerken. Auch hier hilft der Ameisen-Kolonie-Algorithmus. Die Stärke dieses Ansatzes liegt dabei in seiner Anpassungsfähigkeit: Das Routing passt sich kontinuierlich an veränderte Netzwerkbedingungen an, verteilt den Verkehr auf multiple Pfade und reagiert robust auf Ausfälle.
Die Anwendung ähnlicher Prinzipien auf Verkehrsrouting führt zu adaptiven Navigationssystemen, die nicht nur statische kürzeste Wege berechnen, sondern Echtzeit-Verkehrsbedingungen berücksichtigen und die kollektive “Intelligenz” vieler Fahrzeuge nutzen, um Staus zu vermeiden und den Gesamtverkehrsfluss zu optimieren.
8.4.3 Anwendungen in der Robotik: Schwarmrobotik, kollektive Exploration, Formationskontrolle¶
Die Übertragung von Schwarmkonzepten in die Robotik hat ein neues Forschungs- und Anwendungsfeld eröffnet: Schwarmrobotik oder Multi-Roboter-Systeme. Im Gegensatz zu komplexen Einzelrobotern setzt die Schwarmrobotik auf eine Vielzahl einfacherer, kostengünstigerer Roboter, die durch kollektives Verhalten komplexe Aufgaben lösen.
Schwarmrobotik basiert auf denselben Grundprinzipien wie biologische Schwärme und hat auch die gleichen Vorteile. Ein Hauptanwendungsgebiet für Roboterschwärme ist die Exploration und Kartierung unbekannter oder gefährlicher Umgebungen – von Katastrophengebieten bis zu extraterrestrischen Oberflächen.
Die Formationskontrolle bezeichnet dabei die Koordination der Bewegung eines Roboterschwarms, um spezifische geometrische Muster zu bilden und aufrechtzuerhalten. Anwendungen reichen von der Überwachung großer Gebiete über koordinierte Objektmanipulation bis hin zu energieeffizienter Fortbewegung.
Reale Implementierungen von Schwarmrobotik müssen zahlreiche praktische Herausforderungen bewältigen:
- Hardware-Limitationen: Batterielaufzeit, Sensoren, Rechenleistung, physische Interaktionen
- Kommunikation: Zuverlässigkeit, Bandbreite, Latenz, Reichweite
- Lokalisierung: Präzision in GPS-losen Umgebungen, relative Positionierung
- Heterogenität: Integration verschiedener Robotertypen mit unterschiedlichen Fähigkeiten
Hier zeigt sich schließlich, was in einer Softwarelösung eher nicht auffällt, nämlich wie konkrete Vorteile und Nachteile des Agenten-basierten Ansatzes von Schwarm-Robotik reale Potentiale bzw. Grenzen des Systems aufzeigen.
8.4.4 Grenzen und Herausforderungen bei realen Anwendungen¶
Trotz der beeindruckenden Fortschritte und vielversprechenden Anwendungen stoßen Schwarm-Algorithmen und -systeme in der Praxis auf verschiedene Herausforderungen und Grenzen, die ihre breite Anwendung bisher einschränken.
Parametrisierung und Konfiguration¶
Eine fundamentale Herausforderung bei Schwarm-Algorithmen ist die Parameterwahl. Leistung und Verhalten hängen oft kritisch von der richtigen Einstellung mehrerer Parameter ab, beispielsweise:
- Schwarmgröße
- Lernraten und Gewichtungsfaktoren (z.B. in PSO)
- Verdunstungsraten (in ACO)
- Kommunikationsradius in Roboterschwärmen
Die optimale Konfiguration ist typischerweise problemspezifisch und erfordert oft empirisches Tuning oder Meta-Optimierung. Dies steht im Widerspruch zur ursprünglichen biologischen Inspiration, da natürliche Schwärme keine externe “Parameteroptimierung” benötigen (bzw. diese bereits durch die Evolution über lange Zeiträume passiert ist).
Skalierbarkeit und Rechenaufwand¶
Obwohl Schwarm-Algorithmen theoretisch hochskalierbar sind, stoßen praktische Implementierungen auf Grenzen:
- Rechenaufwand: Jede Iteration erfordert die Auswertung der Zielfunktion für jedes Individuum
- Speicherbedarf: In großen Problemen (z.B. TSP mit tausenden Städten) können Pheromonmatrizen oder Nachbarschaftsinformationen erheblichen Speicher benötigen
- Konvergenzgeschwindigkeit: Bei komplexen Problemen kann die Konvergenz langsam sein und viele Iterationen erfordern
In der Robotik kommen weitere Herausforderungen hinzu:
- Kommunikationsoverhead: Die Bandbreite begrenzt die Menge an austauschbaren Informationen
- Energieverbrauch: Kommunikation und Berechnung verbrauchen wertvolle Batteriereserven
- Physische Skalierbarkeit: Mehr Roboter bedeuten mehr potenzielle Kollisionen und “Verkehrsstaus”
Vorhersagbarkeit und Zuverlässigkeit¶
Eine inhärente Eigenschaft emergenter Systeme ist ihre begrenzte Vorhersagbarkeit. Dies stellt eine Herausforderung für Anwendungen dar, die hohe Zuverlässigkeit erfordern:
- Stochastische Natur: Schwarm-Algorithmen nutzen Zufallselemente, was zu variablen Ergebnissen führt
- Keine Optimalitätsgarantien: Schwarm-Algorithmen können erst einmal nicht garantieren, dass das globale Optimum gefunden wird
- Emergentes Verhalten kann je nach Systemparameterwerten vorhanden sein oder nicht: Kleine Änderungen in den Regeln oder Bedingungen können zu qualitativ anderen globalen Verhaltensweisen führen
Diese Eigenschaften machen formale Verifikation und Validierung schwierig – ein kritischer Aspekt für sicherheitsrelevante Anwendungen wie autonome Fahrzeuge oder medizinische Robotik.
Ansätze zur Verbesserung umfassen:
- Hybride Systeme, die Schwarm-Algorithmen mit klassischen, determinstischen Methoden kombinieren
- Statistische Garantien durch multiple Läufe oder Ensemble-Ansätze
- Formale Methoden zur Verifikation bestimmter Schwarm-Eigenschaften
Insgesamt ist dieses Gebiet jedoch eines der interessantesten, vielversprechendsten und zukunftsweisenden Bereiche, die wir in unseren Einheiten gesehen haben und sehen werden.
8.5 Übungsaufgabe: Conway’s Game of Life¶
Dieses berühmte Beispiel für Zelluläre Automaten wird uns heute die Basis für die Übungsaufgabe liefern. Hier geht es vielleicht nicht so sehr um Schwärme, sondern eher um das Phänomen der Emergenz. Wenn Sie dieses System noch nicht kennen, dann lassen Sie es sich vom LLM Ihrer Wahl kurz erklären. Einfach gesagt geht es darum, auf einer Art Bildschirm die Pixel nach bestimmten Regeln, die nur über die jeweiligen Nachbarn bestimmt werden, bei jedem Schritt in einer Iteration der Anzeige entweder aus- oder einzuschalten bzw. gleich zu lassen, wie sie sind.
Dadurch kann man viele sehr langweilige Konfigurationen erzeugen, aber auch einige faszinierende Dinge erleben, z.B. emergente Muster, die sich stabil über den Bildschirm bewegen.
8.5.1 Konkrete Schritte:¶
- Implementieren Sie mit Hilfe Ihres LLMs den Bildschirm und die Regeln für Conway’s Game of Life. Begrenzen Sie die Anzeige des Bildschirms auf eine vernünftige Größe von Pixeln
- Erstellen Sie verschiedene Anfangskonfigurationen und sehen Sie sich an, was damit passiert
- Erstellen Sie eine Anfangsposition mit einem “Gleiter” und beobachten Sie dessen Bewegung
- Experimentieren Sie nach Herzenslust!
Viel Vergnügen!
