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
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 Lehrveranstaltung finden Sie im zugehörigen GitHub-Repository.
1 Einführung in algorithmisches Denken & LLMs¶
Willkommen zu “Fortgeschrittene Mathematik und Computergestützte Algorithmen”! In diesem Kurs werden wir gemeinsam einige fortgeschrittene mathematische Konzepte und algorithmische Ansätze zur Lösung komplexer Probleme erkunden.
Nach dem Studium dieser Einheit sollten Sie zumindest folgende drei der wichtigsten Punkte hier verstehen:
- Was ein Algorithmus ist
- Was Large-Language Models (LLMs) sind und was man derzeit von ihnen erwarten kann
- Wie Sie ein LLM einsetzen, um Algorithmen zu implementieren und damit konkrete Probleme zu lösen
1.1 Was ist ein Algorithmus?¶
Algorithmen sind etwas, das wir ständig verwenden. Ob im täglichen Leben oder in speziellen Situationen wie z.B. beim Lösen konkreter Probleme.
1.1.1 Leicht verständliche Beispiele für Algorithmen:¶
Simple Beispiele für Algorithmen sind
- Wie man zwei Zahlen miteinander multipliziert
- Wie man eine Tasse Tee trinkt
- Ein Kochrezept
- Eine Montageanleitung für Möbel
- Eine Wegbeschreibung von Punkt A nach Punkt B
- Wie Sie ein Jupyter notebook aus unserem Moodle-Kurs herunterladen können
- Wie Sie Ihr Studium beenden können/werden
1.1.2 Wesentliche Eigenschaften von Algorithmen:¶
Was alle Algorithmen gemeinsam haben, sind folgende Eigenschaften:
- Endlichkeit: Ein Algorithmus muss nach einer endlichen Anzahl von Schritten terminieren
- Bestimmtheit: Jeder Schritt muss präzise definiert sein, damit man in der Lage ist, alles genau nachzuvollziehen bzw. richtig zu implementieren
- Eingabe: Ein Algorithmus nimmt null oder mehr Eingaben entgegen
- Ausgabe: Er produziert eine oder mehrere Ausgaben
- Effektivität: Wie vollständig, korrekt, präzise und robust ein Algorithmus ein Problem löst, und wie gut er generell für solche Probleme anwendbar ist
- Effizienz: Wie gut oder schnell ein Algorithmus ein Problem lösen kann, z.B. einen bestimmten Grad an Komplexität
Im Folgenden werden wir uns ein einfaches Beispiel ansehen (und gleichzeitig den diesbezüglichen Umgang mit LLMs üben). Sie sollen dabei unter anderem ein Gefühl dafür bekommen, was mit dem Begriff “Algorithmus” gemeint ist und wie er mit einem Computerprogramm zusammenhängt.
1.1.3 Möglichkeiten, Algorithmen zu formulieren bzw. aufzuschreiben:¶
Einen Algorithmus zu formulieren kann man sich einfach machen, aber auch komplizierter. Wichtig dabei ist, dass die verwendete Methode für die Anfordernisse der Situation ausreichen. Was man hier verwenden kann ist z.B.:
- Natürliche Sprache, so wie Sie jemandem erklären würden, was zu tun ist
- Pseudocode
- Flussdiagramme (Flowcharts)
- Computer-Code über die Verwendung von Programmiersprachen wie z.B. Python
1.1.4. Iterative vs. Direkte Algorithmen¶
Algorithmen können nach ihrer Herangehensweise grob in zwei Kategorien eingeteilt werden: iterative und direkte Algorithmen. Diese Unterscheidung ist fundamental für das Verständnis algorithmischer Problemlösungsstrategien.
Direkte Algorithmen (auch als “Closed-Form” oder “Analytical Solutions” bezeichnet) berechnen das Ergebnis in einem einzigen Durchlauf ohne Wiederholungen oder Annäherungen. Sie liefern das exakte Ergebnis direkt durch eine mathematische Formel oder einen definierten Ablauf.
Haupteigenschaften direkter Algorithmen:
- Sie erfordern eine vollständige Problemanalyse vor der Implementierung
- Sie liefern das Ergebnis in einer vorherbestimmbaren, konstanten Anzahl von Schritten, vielleicht sogar in einem einzigen Schritt
- Man benötigt allerdings typischerweise ein tieferes mathematisches Verständnis des Problems, um eine direkte Lösung zu finden oder zu implementieren
Beispiele für direkte Algorithmen:
- Berechnung der Lösungen einer quadratischen Gleichung mit der bekannten Formel
- Matrixinversion für ein lineares Gleichungssystem mit der Cramer’schen Regel
- Berechnung der Signumfunktion von $(-1)^N$
Iterative Algorithmen
Iterative Algorithmen nähern sich der Lösung schrittweise durch wiederholte Berechnungen an. Sie führen eine Folge von Operationen aus, wobei jeder Schritt auf den Ergebnissen des vorherigen aufbaut, bis eine Abbruchbedingung erreicht ist.
Eigenschaften iterativer Algorithmen:
- Sie lösen das Problem durch wiederholte Anwendung einer Regel oder Transformation
- Sie benötigen grundsätzlich eine Abbruchbedingung (meist ein Konvergenzkriterium)
- Sie sind für komplexe Probleme meist wesentlich einfacher zu implementieren
- Und sie eignen sich naturgemäß insbesondere für Probleme, bei denen keine direkten Lösungsformeln bekannt sind
Beispiele für iterative Algorithmen:
- Newton-Raphson-Verfahren zur Nullstellenberechnung einer Funktion
- Die iterative Berechnung der Fibonacci-Zahlen $F_{(i+1)}=F_i+F_{(i-1)}$
Hybride Ansätze
In der Praxis verwenden viele moderne Algorithmen hybride Ansätze, die Elemente beider Kategorien kombinieren. Die Unterscheidung zwischen iterativen und direkten Algorithmen bietet also in erster Linie eine nützliche konzeptuelle Grundlage für die Analyse und Entwicklung von Problemlösungsstrategien. Letztlich ist die Wahl des geeigneten Ansatzes aber stark problemabhängig und berücksichtigt Faktoren wie Genauigkeitsanforderungen, Berechnungsressourcen, Problemgröße und Komplexität.
1.2 Problemlösungsparadigmen¶
Es gibt mehrere gängige Ansätze zum Entwerfen von Algorithmen:
Brute Force: Alle möglichen Lösungen werden ausprobiert, bis eine passende gefunden wird
- Einfach, aber meist ineffizient, oft (öfter als einem lieb ist) praktisch unmöglich durchführbar
- Beispiel: Überprüfen aller möglichen Kombinationen bei einem Zahlenschloss
Teile und Herrsche (Divide and Conquer):
- Ein Problem wird in kleinere Teilprobleme zerlegt
- Jedes Teilproblem wird dann unabhängig von den anderen Teilen gelöst
- Die Lösungen werden anschließend, wenn nötig, wieder kombiniert
- Beispiel: Binäre Suche nach der Position einer bestimmten Zahl in einer geordneten Liste von Zahlen
Greedy-Ansatz:
- Bei jedem Schritt wird die lokal optimale Wahl getroffen, das heißt, man tut so, als ob man sich bereits in der Nähe der tatsächlichen Lösung befände
- Die Hoffnung dabei ist, damit das globale Optimum zu finden (den Unterschied sehen wir später, wenn es um Optimierung geht)
- Beispiel: Eine bestimmte Summe Wechselgeld aus möglichst wenigen Münzen mit bestimmten Werten zusammensetzen
Dynamische Programmierung:
- Ein Problem wird in überlappende Teilprobleme zerlegt
- Jedes Teilproblem wird separat und einmal gelöst und alle Teillösungen werden gespeichert
- Beispiel: Finden des kürzesten Pfades zwischen zwei Knotenpunkten in einem Graphen
1.2.1 Visualisierung von Divide and Conquer¶
# Implementiert mit Claude 3.7 Sonnet
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Rectangle, FancyArrowPatch
from matplotlib.animation import FuncAnimation
import matplotlib.gridspec as gridspec
import time
def binary_search(arr, target):
"""
Implementiert die binäre Suche als Divide-and-Conquer Algorithmus.
Args:
arr: Sortierte Liste von Elementen
target: Gesuchter Wert
Returns:
int: Index des gesuchten Werts oder -1, wenn nicht gefunden
list: Schritte der Suche für Visualisierung
"""
schritte = []
def binary_search_recursive(links, rechts):
# Basis-Fall: Leerer Suchbereich
if links > rechts:
schritte.append({'links': links, 'rechts': rechts, 'mitte': None, 'gefunden': False})
return -1
# Berechne die Mitte des aktuellen Suchbereichs (Divide)
mitte = (links + rechts) // 2
schritte.append({'links': links, 'rechts': rechts, 'mitte': mitte, 'gefunden': False})
# Prüfe, ob der gesuchte Wert in der Mitte liegt
if arr[mitte] == target:
schritte[-1]['gefunden'] = True
return mitte
# Wenn der gesuchte Wert kleiner ist, suche in der linken Hälfte (Conquer)
elif arr[mitte] > target:
return binary_search_recursive(links, mitte - 1)
# Wenn der gesuchte Wert größer ist, suche in der rechten Hälfte (Conquer)
else:
return binary_search_recursive(mitte + 1, rechts)
# Starte die rekursive Suche
result = binary_search_recursive(0, len(arr) - 1)
return result, schritte
def binary_search_iterative(arr, target):
"""
Implementiert die binäre Suche iterativ für den Vergleich.
Args:
arr: Sortierte Liste von Elementen
target: Gesuchter Wert
Returns:
int: Index des gesuchten Werts oder -1, wenn nicht gefunden
list: Schritte der Suche für Visualisierung
"""
schritte = []
links, rechts = 0, len(arr) - 1
while links <= rechts:
mitte = (links + rechts) // 2
schritte.append({'links': links, 'rechts': rechts, 'mitte': mitte, 'gefunden': False})
if arr[mitte] == target:
schritte[-1]['gefunden'] = True
return mitte, schritte
if arr[mitte] > target:
rechts = mitte - 1
else:
links = mitte + 1
schritte.append({'links': links, 'rechts': rechts, 'mitte': None, 'gefunden': False})
return -1, schritte
def visualisiere_array(schritte, schritt_idx, arr, target, ax):
"""Visualisiert das Array und den aktuellen Suchbereich."""
ax.clear()
ax.set_title(f"Binäre Suche für Zielwert {target}")
ax.set_yticks([])
# Aktueller Schritt
schritt = schritte[schritt_idx] if schritt_idx < len(schritte) else schritte[-1]
# Zeichne alle Elemente des Arrays
for i, wert in enumerate(arr):
farbe = 'white'
# Markiere den Suchbereich
if schritt['links'] <= i <= schritt['rechts']:
farbe = 'lightskyblue'
# Markiere die aktuelle Mitte
if schritt['mitte'] is not None and i == schritt['mitte']:
farbe = 'yellow' if not schritt['gefunden'] else 'lightgreen'
# Zeichne ein Rechteck für das Element
rect = Rectangle((i-0.4, 0), 0.8, 0.8, color=farbe, ec='black')
ax.add_patch(rect)
# Füge den Wert hinzu
ax.text(i, 0.4, str(wert), ha='center', va='center')
# Setze die Achsenbegrenzungen
ax.set_xlim(-0.5, len(arr) - 0.5)
ax.set_ylim(0, 1)
# Beschriftungen für die Indizes
ax.set_xticks(range(len(arr)))
ax.set_xticklabels(range(len(arr)))
# Status-Text
status_text = ""
if schritt['mitte'] is not None:
if schritt['gefunden']:
status_text = f"Gefunden an Position {schritt['mitte']}!"
else:
vergleich = ">" if arr[schritt['mitte']] > target else "<"
status_text = f"Mitte ({schritt['mitte']}): {arr[schritt['mitte']]} {vergleich} {target}"
if schritt_idx + 1 < len(schritte):
naechster_schritt = schritte[schritt_idx + 1]
if naechster_schritt['links'] > naechster_schritt['rechts']:
status_text += "\nElement nicht im Array"
elif naechster_schritt['links'] > schritt['links']:
status_text += f"\nSuche in rechter Hälfte [{schritt['mitte'] + 1}, {schritt['rechts']}]"
else:
status_text += f"\nSuche in linker Hälfte [{schritt['links']}, {schritt['mitte'] - 1}]"
else:
status_text = "Element nicht im Array gefunden"
ax.text(len(arr) / 2, -0.2, status_text, ha='center', va='top')
def visualisiere_binaere_suche(arr, target):
"""Visualisiert den Ablauf der binären Suche als Divide-and-Conquer."""
# Führe die binäre Suche durch und sammle die Schritte
ergebnis, schritte = binary_search(arr, target)
# Erstelle die Figur und Subplots
fig = plt.figure(figsize=(12, 8))
gs = gridspec.GridSpec(2, 1, height_ratios=[1, 2])
ax_array = plt.subplot(gs[0])
# ax_baum = plt.subplot(gs[1])
# Initialisiere die Visualisierung
schritt_idx = 0
visualisiere_array(schritte, schritt_idx, arr, target, ax_array)
plt.tight_layout()
# Funktion für die Animation
def update(frame):
nonlocal schritt_idx
schritt_idx = min(frame, len(schritte) - 1)
visualisiere_array(schritte, schritt_idx, arr, target, ax_array)
# Erstelle die Animation und speichere sie in einer Variable
ani = FuncAnimation(fig, update, frames=len(schritte) + 2, interval=1000, repeat=False)
# Füge Titel und Informationen hinzu
plt.suptitle(f"Binäre Suche als Divide-and-Conquer für {target} in {arr}", fontsize=14)
plt.tight_layout()
plt.subplots_adjust(top=0.9, bottom=0.1)
# plt.show()
# Alternative: Animation als GIF speichern
ani.save('binary_search_animation.gif', writer='pillow', fps=1)
# Gib das Ergebnis, die Schritte und die Animation zurück
return ergebnis, schritte, ani
# Beispiel: Visualisiere die binäre Suche
if __name__ == "__main__":
# Beispiel-Array und Zielwert
arr = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29]
target = 3 # Element an Index 2
print(f"Binäre Suche für {target} in {arr}:")
ergebnis, schritte, animation = visualisiere_binaere_suche(arr, target)
print(f"Ergebnis: {ergebnis} (gefunden an Index {ergebnis})")
print(f"Benötigte Schritte: {len(schritte)}")
# Speichere die Animation in einer Variable im globalen Scope,
# um zu verhindern, dass sie vom Garbage Collector gelöscht wird
_animation_reference = animation
Binäre Suche für 3 in [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29]: Ergebnis: 1 (gefunden an Index 1) Benötigte Schritte: 3

1.2.2 Implementierung und Visualisierung eines Greedy-Ansatzes¶
Wir sehen uns hier das obige Beispiel des Wechselgeldes mit möglichst wenigen Münzen an.
Problemstellung: Gegeben ist ein Betrag X und eine Menge von Münzwerten (z.B. 1, 2, 5, 10, 20, 50 Cent, 1 Euro, 2 Euro). Ziel ist es, den Betrag X mit möglichst wenigen Münzen zurückzugeben. Gesucht ist also eine mathematische Lösung, die abzählt, wie oft welche Münze herausgegeben werden muss.
Greedy-Strategie:
- Beginne mit der größtmöglichen Münze, die nicht größer als der verbleibende Betrag ist
- Füge diese Münze zur Lösung hinzu (die Anzahl dieses Münzwertes wird um 1 erhöht)
- Reduziere den verbleibenden Betrag entsprechend um den verwendeten Münzwert
- Wiederhole diese Schritte, bis der verbleibende Betrag 0 ist (das ist also ein iterativer Algorithmus)
Beispiel: Für einen Betrag von 3,67€ mit dem Euro-Münzsystem:
Wähle 2€ (Rest: 1,67€) Wähle 1€ (Rest: 0,67€) Wähle 50 Cent (Rest: 0,17€) Wähle 10 Cent (Rest: 0,07€) Wähle 5 Cent (Rest: 0,02€) Wähle 2 Cent (Rest: 0€)
Das sind insgesamt 6 Münzen.
Sehen wir uns nun nochmal an, warum dieser Algorithmus “greedy” ist:
- Lokale Optimierung: In jedem Schritt wird die aktuell bestmögliche Entscheidung getroffen (größtmögliche Münze)
- Keine Rücknahme: Einmal getroffene Entscheidungen werden nicht revidiert
- Keine Vorausschau: Der Algorithmus berücksichtigt nur den aktuellen Zustand
Interessant ist hier folgendes: Für das europäische und US-amerikanische Münzsystem führt der Greedy-Algorithmus tatsächlich zur optimalen Lösung. Dies liegt an der speziellen Eigenschaft dieser Münzsysteme, und zwar dass jede Münze mindestens doppelt so viel wert ist wie die Summe aller kleineren Münzen (kanonisches Münzsystem). Bei anderen Münzsystemen (z.B. mit Münzwerten 1, 3, 4) führt der Greedy-Ansatz nicht immer zur optimalen Lösung. Für 6 Geldeinheiten würde der Greedy-Algorithmus 4+1+1 (3 Münzen) wählen, während 3+3 (2 Münzen) optimal wäre.
# Implementiert mit Claude 3.7 Sonnet
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Circle
import matplotlib.colors as mcolors
def greedy_change(betrag, muenzen):
"""
Implementiert den Greedy-Algorithmus für das Wechselgeld-Problem.
Args:
betrag: Zu wechselnder Betrag (in Cent)
muenzen: Liste der verfügbaren Münzwerte (in Cent), absteigend sortiert
Returns:
verwendete_muenzen: Liste der verwendeten Münzen
"""
# Sortiere Münzen in absteigender Reihenfolge
muenzen = sorted(muenzen, reverse=True)
verwendete_muenzen = []
rest = betrag
# Greedy-Algorithmus
for muenze in muenzen:
while rest >= muenze:
verwendete_muenzen.append(muenze)
rest -= muenze
return verwendete_muenzen
def optimal_change(betrag, muenzen):
"""
Implementiert einen dynamischen Programmieransatz für das Wechselgeld-Problem.
Args:
betrag: Zu wechselnder Betrag (in Cent)
muenzen: Liste der verfügbaren Münzwerte (in Cent)
Returns:
min_muenzen: Minimale Anzahl der Münzen
verwendete_muenzen: Liste der verwendeten Münzen
"""
# Initialisiere die dynamische Programmierung Tabelle
# dp[i] = minimale Anzahl Münzen für Betrag i
dp = [float('inf')] * (betrag + 1)
dp[0] = 0
# Speichere für jeden Betrag, welche Münze als letztes verwendet wurde
letzte_muenze = [0] * (betrag + 1)
# Berechne die minimale Anzahl Münzen für jeden Teilbetrag
for i in range(1, betrag + 1):
for muenze in muenzen:
if muenze <= i and dp[i - muenze] + 1 < dp[i]:
dp[i] = dp[i - muenze] + 1
letzte_muenze[i] = muenze
# Rekonstruiere die verwendeten Münzen
verwendete_muenzen = []
rest = betrag
while rest > 0:
muenze = letzte_muenze[rest]
verwendete_muenzen.append(muenze)
rest -= muenze
return dp[betrag], verwendete_muenzen
def visualisiere_muenzen(betrag, muenzen_system1, muenzen_system2):
"""
Visualisiert die Ergebnisse des Greedy- und des optimalen Algorithmus für zwei Münzsysteme.
Args:
betrag: Zu wechselnder Betrag
muenzen_system1: Erstes Münzsystem (z.B. Euro)
muenzen_system2: Zweites Münzsystem (alternatives System)
"""
# Berechne Wechselgeld für System 1 (Euro-System)
greedy_muenzen_system1 = greedy_change(betrag, muenzen_system1)
opt_anzahl_system1, opt_muenzen_system1 = optimal_change(betrag, muenzen_system1)
# Berechne Wechselgeld für System 2 (alternatives System)
greedy_muenzen_system2 = greedy_change(betrag, muenzen_system2)
opt_anzahl_system2, opt_muenzen_system2 = optimal_change(betrag, muenzen_system2)
# Erstelle die Visualisierung
fig, axs = plt.subplots(2, 2, figsize=(14, 8))
fig.suptitle(f'Wechselgeld für {betrag/100:.2f} Geldeinheiten', fontsize=16)
# Definiere Farben für die Münzen basierend auf ihrem Wert
farben = list(mcolors.TABLEAU_COLORS)
muenzfarben1 = {muenze: farben[i % len(farben)] for i, muenze in enumerate(set(muenzen_system1))}
muenzfarben2 = {muenze: farben[i % len(farben)] for i, muenze in enumerate(set(muenzen_system2))}
# System 1 - Greedy
visualisiere_muenzen_set(axs[0, 0], greedy_muenzen_system1, muenzfarben1)
axs[0, 0].set_title(f'System 1 - Greedy: {len(greedy_muenzen_system1)} Münzen')
# System 1 - Optimal
visualisiere_muenzen_set(axs[0, 1], opt_muenzen_system1, muenzfarben1)
axs[0, 1].set_title(f'System 1 - Optimal: {opt_anzahl_system1} Münzen')
# System 2 - Greedy
visualisiere_muenzen_set(axs[1, 0], greedy_muenzen_system2, muenzfarben2)
axs[1, 0].set_title(f'System 2 - Greedy: {len(greedy_muenzen_system2)} Münzen')
# System 2 - Optimal
visualisiere_muenzen_set(axs[1, 1], opt_muenzen_system2, muenzfarben2)
axs[1, 1].set_title(f'System 2 - Optimal: {opt_anzahl_system2} Münzen')
# Füge Legenden hinzu
for i, (muenzfarben, system) in enumerate([(muenzfarben1, muenzen_system1), (muenzfarben2, muenzen_system2)]):
handles = []
labels = []
for muenze in sorted(set(system), reverse=True):
circle = Circle((0, 0), 0.4, color=muenzfarben[muenze])
handles.append(circle)
labels.append(f'{muenze/100:.2f}')
fig.legend(handles, labels, loc=f'upper right', bbox_to_anchor=(0.25+i*.5, 0.9),
title=f'System {i+1} Münzwerte')
plt.tight_layout(rect=[0, 0, 0.85, 0.9])
plt.show()
def visualisiere_muenzen_set(ax, muenzen, muenzfarben):
"""Visualisiert einen Satz von Münzen in einem Subplot."""
# Sortiere Münzen absteigend
muenzen = sorted(muenzen, reverse=True)
# Bestimme maximale Anzahl von Münzen pro Zeile
max_pro_zeile = 10
# Größe der Münzen
radius = 0.4
# Zeichne jede Münze
for i, muenze in enumerate(muenzen):
zeile = i // max_pro_zeile
spalte = i % max_pro_zeile
x = spalte * 2 * radius * 1.1 + radius
y = -zeile * 2 * radius * 1.1 - radius
circle = Circle((x, y), radius, color=muenzfarben[muenze])
ax.add_patch(circle)
# Füge Wert als Text hinzu
ax.text(x, y, f"{muenze/100:.2f}", ha='center', va='center', fontsize=9)
# Setze Achsenbegrenzungen
zeilen = (len(muenzen) - 1) // max_pro_zeile + 1
ax.set_xlim(0, max_pro_zeile * 2 * radius * 1.1)
ax.set_ylim(-zeilen * 2 * radius * 1.1, 0)
# Entferne Achsenticks
ax.set_xticks([])
ax.set_yticks([])
ax.set_aspect('equal')
# Beispiel: Vergleich zweier Münzsysteme
if __name__ == "__main__":
# System 1: Euro-System (in Cent)
euro_system = [200, 100, 50, 20, 10, 5, 2, 1]
# System 2: Alternatives System (in Cent)
alternatives_system = [400, 300, 100, 50, 10, 1]
# Betrag zum Wechseln (in Cent)
betrag = 600 # 6,00 Geldeinheiten
# Visualisiere die Ergebnisse
visualisiere_muenzen(betrag, euro_system, alternatives_system)
# Zeige textuelle Zusammenfassung für beide Systeme
print("System 1 (Euro-System):")
greedy_euro = greedy_change(betrag, euro_system)
print(f" Greedy-Lösung: {len(greedy_euro)} Münzen: {[m/100 for m in greedy_euro]}")
opt_anzahl_euro, opt_euro = optimal_change(betrag, euro_system)
print(f" Optimale Lösung: {opt_anzahl_euro} Münzen: {[m/100 for m in opt_euro]}")
print("\nSystem 2 (Alternatives System):")
greedy_alt = greedy_change(betrag, alternatives_system)
print(f" Greedy-Lösung: {len(greedy_alt)} Münzen: {[m/100 for m in greedy_alt]}")
opt_anzahl_alt, opt_alt = optimal_change(betrag, alternatives_system)
print(f" Optimale Lösung: {opt_anzahl_alt} Münzen: {[m/100 for m in opt_alt]}")
System 1 (Euro-System): Greedy-Lösung: 3 Münzen: [2.0, 2.0, 2.0] Optimale Lösung: 3 Münzen: [2.0, 2.0, 2.0] System 2 (Alternatives System): Greedy-Lösung: 3 Münzen: [4.0, 1.0, 1.0] Optimale Lösung: 2 Münzen: [3.0, 3.0]
1.3. Kritische Beurteilung von Algorithmen¶
Bei der Untersuchung von Algorithmen sollten wir immer Folgendes berücksichtigen:
- Korrektheit des Algorithmus: Produziert er für alle gültigen Eingaben die richtige Ausgabe?
- Effizienz des Algorithmus: Wie gut nutzt er die Rechenressourcen? Insbesondere interessant sind hier die
- Zeitkomplexität: Wie lange dauert die Ausführung?
- Raumkomplexität: Wie viel Speicher wird benötigt?
- Eleganz des Algorithmus: Ist die Lösung klar implementierbar und einfach wartbar?
- Robustheit des Algorithmus: Wie geht er mit unerwarteten Eingaben oder Grenzfällen um? Leidet in solchen Fällen die Effizienz?
Wir werden uns in der nächsten Einheit intensiver mit diesen Eigenschaften und Themen beschäftigen. Für den Moment halten wir einfach die Augen offen und sind achtsam, falls etwas Unerwartetes oder Ungeheures passiert.
1.4 Einführung in die Verwendung von Large Language Models (LLMs) zur algorithmischen Lösung von Problemen¶
In diesem Kurs werden wir grundsätzlich Large Language Models (LLMs) als Werkzeuge zur Implementierung von Algorithmen, verstehen der Details, und zum Schreiben von Python-Code verwenden. Dieser Abschnitt ist ein ultra-Crash-Course, der Ihnen unmittelbar die nötigen Werkzeuge in die Hand geben soll, um erfolgreich Code zu generieren, aber auch sinnvoll damit zu arbeiten.
1.4.1 Was sind LLMs?¶
LLMs sind moderne KI-Systeme, die auf enormen Mengen von Textdaten trainiert wurden. Bei den Trainingsdaten handelt es sich z.B. um verschiedene Inhalte aus dem Internet, ganze Sammlungen von Codebeispielen (GitHub), Diskussionsforen (Reddit), und andere Quellen, in denen Konversationen vorkommen. Der Grund dafür ist, dass diese Modelle trainiert wurden, auf einen User-Input, den sogenannten Prompt, den bestmöglichen Output zu erzeugen. Oder, noch konkreter: Ein LLM will mit Ihnen die Konversation führen, die Sie sich höchstwahrscheinlich wünschen.
Lassen Sie diesen Gedanken einmal kurz sacken, denn dieses Verständnis wird Ihnen im Umgang mit LLMs viel Unverständnis ersparen. Je genauer Sie sich darauf einstellen, einen Gesprächspartner zu haben (und nicht ein allwissendes Dingsbums), desto bessere Resultate werden Sie erzielen.
LLMs können, ganz kurz gesagt und für unsere Zwecke:
- Menschliche Sprache verarbeiten und erzeugen
- Code basierend auf Beschreibungen schreiben
- Konzepte und Algorithmen erklären
- Code debuggen und optimieren
- Sich auf Englisch oder Deutsch (oder auch in anderen Sprachen) mit Ihnen unterhalten
LLMs können nicht, wieder ganz kurz gesagt und für unsere Zwecke:
- Wissen, was Sie denken, wenn Sie das nicht klar formulieren oder formuliert haben
- Logisch denken (das funktioniert nur mit zusätzlichen Schichten im Modell, wie sie seit Ende 2024 angeboten werden)
1.4.2 Wie wir LLMs in diesem Kurs nutzen werden:¶
Die Nutzung von LLMs ist inzwischen sehr einfach geworden. Bei allen Anbietern gibt es einen kostenlosen Zugang zu grundlegenden Funktionen, der allerdings meist eine Registrierung voraussetzt. Andererseits gibt Ihnen eine Registrierung auch die Möglichkeit, Ihre Chats mit dem LLM zu organisieren und auf früher geführte Konversationen zurückzugreifen. Im Prinzip reicht inzwischen ein kostenloser Zugang jedenfalls aus, um gut mit dem LLM Ihrer Wahl an den Kursaufgaben und -Inhalten arbeiten zu können.
Wir werden mit Hilfe der LLMs:
- Den Hintergrund und die Details der präsentierten Probleme verstehen lernen
- Algorithmen basierend auf unseren Beschreibungen implementieren
- Verschiedene Implementierungsansätze erkunden und vergleichen
- Code und Algorithmen erklären lassen und deren Details und Möglichkeiten unter die Lupe nehmen, wenn wir das wollen
- Lösungen optimieren und Code debuggen
- Ergebnisse visualisieren und analysieren
Dieser Ansatz ermöglicht es uns hauptsächlich, uns auf das Verständnis algorithmischer Prinzipien und deren Lösungspotential zu konzentrieren anstatt mit Syntaxdetails oder dem zeilenweisen Schreiben von Code zu kämpfen.
1.4.3 Konkrete Vorteile und Einschränkungen von LLMs für die Algorithmenimplementierung¶
Hier kurz die wichtigsten Vorteile und Einschränkungen in der Arbeit mit LLMs bei der Implementierung von Algorithmen. Sie werden allerdings bereits nach kurzer Zeit selbst ein ziemlich gutes Gefühl dafür haben, was besser funktioniert und was nicht so gut.
Vorteile:
- Schnelle Implementierung komplexer Algorithmen
- Mehrere Implementierungsansätze zum Vergleichen
- Fokus auf Konzepte statt Syntax
- Kennenlernen verschiedener Programmierstile und -techniken (optional)
Einschränkungen:
- Liefern nicht immer optimale Lösungen
- Können Fehler im logischen Denken machen
- Können “halluzinieren” oder nicht existierende Funktionen erfinden
- Begrenzt auf Wissen aus ihren Trainingsdaten
Grundlegende Interaktionsrichtlinien:
- Seien Sie klar und spezifisch in Ihren Beschreibungen
- Liefern Sie wenn möglich den Kontext zu dem Problem, das Sie lösen wollen
- Geben Sie, soweit bekannt, Eingabe- und Ausgabeformate und gegebenenfalls auch Einschränkungen an
- Fragen Sie bei Bedarf nach Erklärungen zur Implementierung
- Überprüfen Sie die Korrektheit der Lösungen durch gezieltes Testen, Ausprobieren und den Einsatz bekannter Grenz- oder Spezialfälle
- Iterieren und verfeinern Sie Ihre Prompts, bis Sie zufrieden sind
1.5 Arbeiten mit LLMs beim Lösen algorithmischer Probleme und beim Schreiben von Code¶
Hier folgen jetzt einige Muster für das Schreiben von Prompts, die Sie höchstwahrscheinlich im Verlauf des Kurses gut brauchen können. Zuvor nur noch ein paar kurze Hinweise:
- Dokumentieren Sie grundsätzlich, welches LLM Sie an welcher Stelle eingesetzt haben (z.B. in Kommentaren im Code oder in Markdown-Zellen). Damit stellen Sie sicher, dass Sie oder ich sich in Ihrem Code zurechtfinden bzw. orientieren könnten, was dessen Herkunft betrifft (und zwar auch noch nach Wochen oder Monaten).
- Sollten Sie außer den Prompts noch andere Einstellungen am LLM vorgenommen haben oder Materialien zugrunde gelegt haben (z.B. über eine Projektumgebung), dann dokumentieren Sie auch das ausreichend.
- Versuchen Sie immer wieder, die Implementierungen und Code-Erklärungen bis ins Detail zu verstehen. Das ist zwar nicht der primäre Fokus der Lehrveranstaltung, aber es hilft Ihnen auf dem Weg zu einem tiefen Verständnis, und das lohnt sich!
- Bleiben Sie kritisch, was die Antworten Ihres LLMs betrifft. Auch wenn die Erklärungen ausführlich und solide klingen, müssen sie nicht im Detail korrekt sein. Nutzen Sie im Zweifelsfall einen Dienst wie z.B. Perplexity oder ein LLM mit Internet-Zugang für einen gründlichen Fact-Check.
1.5.1 Wie Sie mehr über ein bestimmtes mathematisches Problem oder einen Algorithmus lernen¶
Verwenden Sie das folgende Promptmuster und setzen Sie geeignete Textbausteine für Ihren Anwendungsfall ein. Verändern Sie auch den Prompt, wenn Ihnen gute Modifikationen einfallen, die bessere Outputs versprechen. Ich habe den Prompt in eine separate Jupyter-Zelle gesetzt, damit Sie in einfacher kopieren können. Diese Vorgehensweise gilt auch für die nachfolgenden Prompt-Muster.
Ich möchte mehr über [Name des Algorithmus/mathematischen Problems] lernen. Bitte erkläre:
- Die grundlegende Idee und den Zweck
- Die mathematischen Grundlagen und Voraussetzungen
- Die wichtigsten Anwendungsfälle
- Die Stärken und Einschränkungen
- Die Komplexität (Zeit und Speicher)
- Wie sich dieser Algorithmus von ähnlichen Ansätzen unterscheidet
Bitte erkläre die Konzepte so, dass sie ich als UniversitätsstsudentIn im zweiten Jahr mit grundlegenden Mathematikkenntnissen und einem breit gefächerten naturwissenschaftlichen Basishintergrund gut verstehen kann.
1.5.2 Wie Sie herausfinden, welche Algorithmen sich zur Lösung eines bestimmten Problems eignen¶
Ich arbeite an folgendem Problem:
[Detaillierte Problembeschreibung]
Meine Eingabedaten haben folgende Eigenschaften:
- [Größe, Format, Struktur]
- [Besondere Merkmale oder Einschränkungen]
Mein Ziel ist:
[Beschreibung des gewünschten Ergebnisses]
Welche Algorithmen oder Verfahren würdest du für die Lösung dieses Problems empfehlen? Bitte erkläre bei jedem Vorschlag:
- Warum er für dieses Problem geeignet ist
- Wie die Umsetzung grob aussehen würde
- Vor- und Nachteile gegenüber anderen möglichen Ansätzen
- Gegebenenfalls notwendige Datenstrukturen
1.5.3 Wie Sie Python-Code schreiben lassen, der eine bestimmte Lösungsstrategie für ein Problem implementiert¶
Ich möchte [Name des Algorithmus] implementieren, um folgendes Problem zu lösen:
[Problembeschreibung]
Die Eingabe hat folgendes Format:
[Beschreibung des Eingabeformats]
Die Ausgabe sollte folgendes Format haben:
[Beschreibung des Ausgabeformats]
Bitte schreibe Python-Code für diese Implementierung mit:
- Ausführlichen Kommentaren zur Erklärung der einzelnen Schritte
- Effizienter Implementierung unter Berücksichtigung der Komplexität
- Klarer Struktur und verständlichen Variablennamen
- Verwendung geeigneter Python-Bibliotheken wie NumPy, wenn sinnvoll
1.5.4 Wie Sie nach Möglichkeiten fragen, den gerade erzeugten Code zu testen¶
Ich habe den von Dir generierten Python-Code nun zur verfügung und er läuft. Wie kann ich diesen Code gründlich testen? Bitte schlage vor:
- Verschiedene Testfälle (normale Fälle, Grenzfälle, Sonderfälle)
- Wie ich die Korrektheit des Ergebnisses verifizieren kann
- Mögliche Unit-Tests oder Testfunktionen
- Wie ich die Leistung und Effizienz messen könnte
- Ideen für visuelle Verifikation der Ergebnisse, falls anwendbar
1.5.5 Wie Sie Hilfe bei der Interpretation von Lösungen oder Visualisierungen bekommen, die Ihr Code erzeugt¶
Ich habe folgendes Ergebnis/folgende Visualisierung durch Ausführung meines Codes erhalten:
[Ergebnis/Visualisierung beschreiben oder einfügen]
Ich bin mir nicht sicher, wie ich die Ergebnisse interpretieren soll. Insbesondere:
[Spezifische Frage zur Interpretation]
[Auffälligkeiten oder unerwartete Muster]
[Unklarheiten bei der Visualisierung]
Kannst du mir helfen, diese Ausgabe zu verstehen und daraus sinnvolle Schlüsse zu ziehen?
1.5.6 Wie Sie Ideen für die nächsten Schritte sammeln, entweder für ähnliche Probleme, weitere Lösungswege oder offene Fragen¶
Ich habe erfolgreich [Beschreibung des gelösten Problems] mit [verwendeter Algorithmus/Methode] mit Hilfe Deines Python Codes umgesetzt.
Was könnte ich als Nächstes tun, um mein Verständnis zu vertiefen oder die Lösung zu erweitern? Ich bin interessiert an:
- Ähnlichen, aber komplexeren Problemen, die ich versuchen könnte
- Alternative Lösungsansätze für das gleiche Problem
- Mögliche Optimierungen oder Erweiterungen
- Interessante offene Forschungsfragen in diesem Bereich
- Praktische Anwendungen dieses Algorithmus in der realen Welt
1.6 Demo: Einfache Algorithmenimplementierung mit einem LLM¶
Hier möchte ich Ihnen demonstrieren, wie man ein LLM zur Lösung eines konkreten Problems einsetzen kann. Konkret verwende ich dafür Claude 3.7 Sonnet, aber man würde auch mit ChatGPT (derzeit o3-mini-high) oder Gemini (derzeit 2.0 Flash) ähnliche Outputs erhalten.
Ich möchte gerne das automatische Gruppieren von zweidimensionalen Daten mit Hilfe von Hierarchischem Clustering besser verstehen. Bitte erkläre kurz:
- Die grundlegende Idee und den Zweck dieses Algorithmus
- Die mathematischen Grundlagen und Voraussetzungen
- Die wichtigsten Anwendungsfälle
- Die Stärken und Einschränkungen
- Die Komplexität (Zeit und Speicher)
- Wie sich dieser Algorithmus von ähnlichen Ansätzen unterscheidet
Bitte erkläre die Konzepte so, dass sie ich als Universitätsstsudent im zweiten Jahr mit grundlegenden Mathematikkenntnissen und einem breit gefächerten naturwissenschaftlichen Basishintergrund gut verstehen kann.
Antwort von Claude 3.7 Sonnet: Hierarchisches Clustering für zweidimensionale Daten¶
Grundlegende Idee und Zweck¶
Hierarchisches Clustering ist ein Algorithmus, der Datenpunkte in Gruppen (Cluster) organisiert, wobei die Ähnlichkeit zwischen Punkten innerhalb eines Clusters maximiert und zwischen verschiedenen Clustern minimiert wird. Anders als bei anderen Clustering-Verfahren entsteht eine Baumstruktur (Dendrogramm), die verschiedene Gruppierungsebenen zeigt.
Es gibt zwei Hauptansätze:
- Agglomerativ (bottom-up): Startet mit einzelnen Punkten als eigene Cluster und verschmilzt schrittweise die ähnlichsten.
- Divisiv (top-down): Beginnt mit allen Punkten in einem Cluster und teilt diesen rekursiv.
Mathematische Grundlagen¶
Distanzmatrix: Zu Beginn wird die Ähnlichkeit zwischen allen Datenpunkten berechnet. Für zweidimensionale Daten ist die euklidische Distanz üblich:
d(p, q) = √[(x₂ – x₁)² + (y₂ – y₁)²]
Linkage-Kriterien: Diese bestimmen, wie die Distanz zwischen Clustern berechnet wird:
- Single-Linkage: kürzeste Distanz zwischen zwei Punkten aus verschiedenen Clustern
- Complete-Linkage: größte Distanz zwischen zwei Punkten
- Average-Linkage: Durchschnittsdistanz zwischen allen Punktpaaren
- Ward-Methode: minimiert die Varianz innerhalb der Cluster
Dendrogramm: Die Baumstruktur visualisiert, in welcher Reihenfolge und bei welchen Distanzen Cluster zusammengeführt wurden.
Wichtigste Anwendungsfälle¶
- Bioinformatik: Gruppierung von Genen mit ähnlichen Expressionsmustern
- Marktforschung: Kundensegmentierung nach Kaufverhalten
- Bilderkennung: Identifikation ähnlicher Bildbereiche
- Geographie: Identifikation ähnlicher Regionen basierend auf Merkmalen
- Dokumentenanalyse: Gruppierung von Texten mit ähnlichem Inhalt
Stärken und Einschränkungen¶
Stärken:¶
- Benötigt keine Vorgabe der Cluster-Anzahl (im Gegensatz zu k-Means)
- Liefert eine interpretierbare hierarchische Struktur
- Funktioniert mit beliebigen Distanzmaßen
- Kann Cluster unterschiedlicher Größe und Form erkennen
Einschränkungen:¶
- Hoher Rechenaufwand für große Datensätze
- Einmal getroffene Entscheidungen sind nicht revidierbar
- Anfällig für Rauschen und Ausreißer
- Die Wahl des Linkage-Kriteriums beeinflusst stark das Ergebnis
- Keine klare Regel, wo der Baum “geschnitten” werden sollte
Komplexität¶
- Zeitkomplexität: O(n³) für naive Implementierungen, wobei n die Anzahl der Datenpunkte ist. Optimierte Versionen erreichen O(n² log n).
- Speicherkomplexität: O(n²) für die Distanzmatrix.
Diese hohe Komplexität macht den Algorithmus für sehr große Datensätze (>10.000 Punkte) unpraktisch.
Unterschiede zu ähnlichen Ansätzen¶
Im Vergleich zu k-Means:
- K-Means benötigt die Vorgabe der Cluster-Anzahl, hierarchisches Clustering nicht
- K-Means findet nur kugelförmige Cluster, hierarchisches Clustering erkennt beliebige Formen
- K-Means ist schneller (O(k·n·t) wobei k = Anzahl Cluster, t = Iterationen)
Im Vergleich zu DBSCAN:
- DBSCAN erkennt Cluster basierend auf Dichte, nicht auf hierarchischer Struktur
- DBSCAN kann Rauschen explizit identifizieren
- DBSCAN ist besser für große Datensätze geeignet (O(n log n))
Im Vergleich zu Gaussian Mixture Models:
- GMMs sind probabilistisch, hierarchisches Clustering deterministisch
- GMMs können überlappende Cluster modellieren
- GMMs treffen Annahmen über die Verteilung der Daten (Normalverteilung)
Zusammenfassend ist hierarchisches Clustering besonders nützlich, wenn Sie die Beziehungen zwischen Datenpunkten auf verschiedenen Ähnlichkeitsebenen verstehen möchten und keine Vorannahmen über die Anzahl der Cluster treffen können oder wollen.
Ich möchte Hierarchisches Agglomeratives Clustering implementieren, um folgendes Problem zu lösen:
Die automatische Gruppierung von 2D-Datenpunkten
Die Eingabe hat folgendes Format:
Eine Liste oder ein Array von N 2D-Punkten, wobei der Input-Parameter N=10000 sein oder einen geeigneten Wert haben soll. Der Code soll zu Demonstrationszwecken geeignete synthetische Daten erzeugen und verwenden.
Die Ausgabe sollte folgendes Format haben:
Visualisierung der Datenpunkte und Einfärben nach zugeordneten Clustern.
Außerdem grafische Darstellung des zugehörigen Dendrogramms
Bitte schreibe Python-Code für eine einfache Implementierung mit:
- Ausführlichen Kommentaren zur Erklärung der einzelnen Schritte
- Klarer Struktur und verständlichen Variablennamen
- Verwendung geeigneter Python-Bibliotheken wie NumPy oder scikit-learn
# Antwort von Claude 3.7 Sonnet:
# Hierarchisches Agglomeratives Clustering von 2D-Daten
# ---------------------------------------------------------
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
from scipy.cluster.hierarchy import dendrogram, linkage
from sklearn.cluster import AgglomerativeClustering
import time
# -----------------------------------------------
# 1. Generierung synthetischer 2D-Daten für das Clustering
# -----------------------------------------------
def generate_synthetic_data(n_samples=10000, n_centers=6, random_state=42):
"""
Erzeugt synthetische 2D-Datenpunkte mit mehreren Clustern.
Parameter:
- n_samples: Anzahl der zu generierenden Datenpunkte
- n_centers: Anzahl der Cluster-Zentren
- random_state: Seed für die Reproduzierbarkeit
Rückgabe:
- X: Array der Datenpunkte (n_samples, 2)
- y_true: Wahre Cluster-Labels
"""
print(f"Erzeuge {n_samples} synthetische Datenpunkte mit {n_centers} Clustern...")
# make_blobs erzeugt gaußförmige Cluster
X, y_true = make_blobs(
n_samples=n_samples,
centers=n_centers,
cluster_std=[1.0, 1.5, 0.8, 1.2, 1.3, 0.9], # Unterschiedliche Streuungen
random_state=random_state
)
# Optional: Skalieren und Verschieben einiger Cluster für interessantere Muster
X[:n_samples//3, 0] = X[:n_samples//3, 0] * 1.5 # Erste 1/3 der Punkte in x-Richtung strecken
X[n_samples//3:2*n_samples//3, 1] = X[n_samples//3:2*n_samples//3, 1] * 0.8 # Mittlere 1/3 in y-Richtung stauchen
return X, y_true
# -----------------------------------------------
# 2. Durchführung des hierarchischen Clusterings
# -----------------------------------------------
def perform_hierarchical_clustering(X, n_clusters=6, linkage_method='ward'):
"""
Führt hierarchisches agglomeratives Clustering auf den Daten durch.
Parameter:
- X: 2D-Datenarray der Form (n_samples, 2)
- n_clusters: Anzahl der zu bildenden Cluster
- linkage_method: Methode für die Berechnung der Cluster-Distanzen
('ward', 'complete', 'average', 'single')
Rückgabe:
- labels: Cluster-Labels für jeden Datenpunkt
- Z: Hierarchische Clustering-Matrix (für Dendrogramm)
"""
print(f"Starte hierarchisches Clustering mit {linkage_method}-Linkage...")
start_time = time.time()
# Hierarchisches Clustering mit scikit-learn
model = AgglomerativeClustering(
n_clusters=n_clusters,
linkage=linkage_method,
)
# Berechne die Cluster-Zuordnungen
labels = model.fit_predict(X)
# Berechne die Linkage-Matrix für das Dendrogramm
# Hinweis: Dies ist rechenintensiv für große Datensätze
Z = linkage(X, method=linkage_method)
elapsed_time = time.time() - start_time
print(f"Clustering abgeschlossen in {elapsed_time:.2f} Sekunden")
return labels, Z
# -----------------------------------------------
# 3. Visualisierung der Ergebnisse
# -----------------------------------------------
def plot_clusters(X, labels, title="Hierarchisches Clustering - Ergebnis"):
"""
Visualisiert die Datenpunkte, eingefärbt nach Cluster-Zugehörigkeit.
Parameter:
- X: 2D-Datenarray der Form (n_samples, 2)
- labels: Cluster-Labels für jeden Datenpunkt
- title: Titel für den Plot
"""
plt.figure(figsize=(10, 8))
# Jeder Cluster bekommt eine andere Farbe
unique_labels = np.unique(labels)
colors = plt.cm.rainbow(np.linspace(0, 1, len(unique_labels)))
# Plotte jeden Cluster in seiner eigenen Farbe
for label, color in zip(unique_labels, colors):
mask = labels == label
plt.scatter(
X[mask, 0],
X[mask, 1],
c=[color],
s=30, # Punktgröße
alpha=0.7, # Transparenz
label=f'Cluster {label}'
)
plt.title(title, fontsize=14)
plt.xlabel("X-Koordinate", fontsize=12)
plt.ylabel("Y-Koordinate", fontsize=12)
plt.legend(loc='upper right')
plt.grid(True, linestyle='--', alpha=0.7)
plt.tight_layout()
def plot_dendrogram(Z, title="Hierarchisches Clustering - Dendrogramm", max_samples=100):
"""
Visualisiert das Dendrogramm des hierarchischen Clusterings.
Parameter:
- Z: Linkage-Matrix aus der hierarchischen Clustering-Berechnung
- title: Titel für den Plot
- max_samples: Maximale Anzahl der im Dendrogramm gezeigten Samples (für Übersichtlichkeit)
"""
plt.figure(figsize=(14, 8))
# Wenn es zu viele Datenpunkte sind, begrenzen wir die Anzeige
if len(Z) + 1 > max_samples:
plt.title(f"{title} (gekürzt auf {max_samples} Samples)", fontsize=14)
truncate_mode = 'lastp'
else:
plt.title(title, fontsize=14)
truncate_mode = None
# Erzeuge das Dendrogramm
dendrogram(
Z,
truncate_mode=truncate_mode,
p=max_samples,
leaf_font_size=10,
orientation='top',
)
plt.xlabel("Datenpunkt-Index oder Cluster", fontsize=12)
plt.ylabel("Distanz", fontsize=12)
plt.grid(True, linestyle='--', alpha=0.7)
plt.tight_layout()
# -----------------------------------------------
# 4. Hauptfunktion
# -----------------------------------------------
def main():
"""
Hauptfunktion: Führt den gesamten Clustering-Prozess durch.
"""
# Parameter für die Datengenerierung und das Clustering
n_samples = 5000 # 10000 kann für manche Systeme zu rechenintensiv sein
n_clusters = 6
linkage_method = 'ward' # Alternativen: 'complete', 'average', 'single'
# 1. Generiere synthetische Daten
X, y_true = generate_synthetic_data(n_samples=n_samples, n_centers=n_clusters)
# 2. Führe hierarchisches Clustering durch
labels, Z = perform_hierarchical_clustering(X, n_clusters=n_clusters, linkage_method=linkage_method)
# 3. Visualisiere die Ergebnisse
plot_clusters(X, labels, title=f"Hierarchisches Clustering ({linkage_method}) - {n_clusters} Cluster")
plot_dendrogram(Z, title=f"Dendrogramm ({linkage_method}-Linkage)")
# 4. Zum Vergleich: Visualisiere die wahren Cluster
plot_clusters(X, y_true, title="Wahre Cluster der synthetischen Daten")
# 5. Zeige alle Plots
plt.show()
# -----------------------------------------------
# Programmausführung
# -----------------------------------------------
if __name__ == "__main__":
main()
print("Programm beendet.")
Erzeuge 5000 synthetische Datenpunkte mit 6 Clustern... Starte hierarchisches Clustering mit ward-Linkage... Clustering abgeschlossen in 0.68 Sekunden
Programm beendet.
Ich bin mir nicht sicher, wie ich die Ergebnisse interpretieren soll. Insbesondere habe ich gesehen, dass es Unterschiede zwischen den Berechneten Clustern und den tatsächlich in den synthetischen Daten enthaltenen Clustern gibt.
Kannst du mir helfen, diese Ausgabe zu verstehen und daraus sinnvolle Schlüsse zu ziehen?
Antwort von Claude 3.7 Sonnet: Interpretation der Hierarchischen Clustering-Ergebnisse¶
Sie haben eine wichtige Beobachtung gemacht – es gibt in der Tat Unterschiede zwischen den vom Algorithmus berechneten Clustern und den “wahren” Clustern der synthetischen Daten. Das ist ein häufiges Phänomen beim Clustering und besonders hilfreich, um die Stärken und Grenzen dieser Methoden zu verstehen.
Warum unterscheiden sich berechnete und wahre Cluster?¶
Unterschiedliche Definitionen von Ähnlichkeit:
- Die synthetischen Daten wurden mit
make_blobs
erzeugt, was normalverteilte Cluster produziert - Das hierarchische Clustering (besonders mit verschiedenen Linkage-Methoden) kann andere Ähnlichkeitsstrukturen identifizieren
- Die synthetischen Daten wurden mit
Einfluss der Linkage-Methode:
- Ward-Linkage (Standard im Code) minimiert die Varianz innerhalb der Cluster und bevorzugt daher kompakte, kugelförmige Cluster
- Single-Linkage tendiert dazu, kettenförmige Cluster zu bilden
- Complete-Linkage bevorzugt Cluster mit kleinem Durchmesser
- Average-Linkage liegt oft zwischen Single und Complete
Die Anzahl der Cluster:
- Beim hierarchischen Clustering muss man entscheiden, wo man den “Schnitt” im Dendrogramm macht
- Dieser Parameter beeinflusst stark die resultierende Cluster-Struktur
So interpretieren Sie die Ergebnisse:¶
1. Vergleich der Cluster-Plots¶
Wenn Sie die Plots der “wahren” und “berechneten” Cluster vergleichen:
- Übereinstimmende Bereiche: Cluster, die in beiden Plots ähnlich aussehen, zeigen Bereiche, wo der Algorithmus die Datenstruktur gut erfasst hat
- Unterschiedliche Bereiche: Wo sich die Cluster-Grenzen unterscheiden, könnte dies auf Mehrdeutigkeiten in den Daten hindeuten oder auf die Präferenz des Algorithmus für bestimmte Cluster-Formen
2. Interpretation des Dendrogramms¶
Das Dendrogramm zeigt:
- Die hierarchische Struktur der Daten
- Die Reihenfolge der Cluster-Fusionen (von unten nach oben)
- Die Distanzen, bei denen Cluster verschmolzen werden (vertikale Achse)
Schlüsselaspekte im Dendrogramm:
- Größere Abstände zwischen Fusionen deuten auf natürlichere Cluster-Grenzen hin
- Flache Abschnitte im Dendrogramm zeigen Bereiche, wo viele Cluster bei ähnlichen Distanzen verschmolzen werden
- Der Punkt, an dem sich die Fusionsdistanzen deutlich erhöhen, ist oft ein guter Schnittbereich
3. Vergleich verschiedener Linkage-Methoden¶
Wenn Sie die erweiterte Implementierung ausführen:
- Ward und Average-Linkage reproduzieren oft Gauß-förmige Cluster am besten
- Single-Linkage erkennt oft nicht-konvexe Strukturen wie die Halbmond-Form
- Complete-Linkage neigt dazu, Cluster ähnlicher Größe zu erzeugen
Praktische Schlussfolgerungen:¶
Wahl der Linkage-Methode:
- Verwenden Sie Ward, wenn Sie kompakte, kugelförmige Cluster erwarten
- Verwenden Sie Single-Linkage, wenn Sie nicht-konvexe Cluster oder Ausreißer-Erkennung benötigen
- Verwenden Sie Average-Linkage als guten Kompromiss für viele Anwendungen
Bestimmung der Cluster-Anzahl:
- Untersuchen Sie das Dendrogramm auf deutliche “Lücken” in den Fusionsdistanzen
- Berücksichtigen Sie domänenspezifisches Wissen (falls verfügbar)
- Experimentieren Sie mit verschiedenen Werten und vergleichen Sie die Ergebnisse
Validierung der Cluster:
- Bei synthetischen Daten: Vergleichen Sie mit den wahren Labels
- Bei realen Daten: Verwenden Sie Validierungsmaße wie Silhouette-Score oder Calinski-Harabasz-Index
Beispiel für eine konkrete Interpretation:¶
Wenn Sie bei der Ward-Methode sehen, dass einige der berechneten Cluster Teile mehrerer wahrer Cluster enthalten, könnte das bedeuten:
- Die Daten haben Überlappungen zwischen den Clustern, die für den Algorithmus schwer zu trennen sind
- Die von Ihnen gewählte Cluster-Anzahl entspricht möglicherweise nicht der natürlichen Struktur der Daten
- Die Distanzmetrik (euklidisch) oder Linkage-Methode ist möglicherweise nicht optimal für die vorliegende Datenstruktur
Durch das Experimentieren mit verschiedenen Parametern und das Vergleichen der Ergebnisse können Sie ein tieferes Verständnis dafür entwickeln, wie hierarchisches Clustering auf Ihre spezifischen Daten reagiert.
Möchten Sie den Code anpassen, um bestimmte Aspekte der Interpretation genauer zu untersuchen?
Vielen Dank für die Erklärung!
Ich habe also erfolgreich ein agglomeratives hierarchisches Clustering von 2D-Daten mit Hilfe Deines Python Codes umgesetzt.
Was könnte ich als Nächstes tun, um mein Verständnis zu vertiefen oder die Lösung zu erweitern? Ich bin interessiert an:
- Ähnlichen, aber komplexeren Problemen, die ich versuchen könnte
- Alternative Lösungsansätze für das gleiche Problem
- Mögliche Optimierungen oder Erweiterungen
- Interessante offene Forschungsfragen in diesem Bereich
- Praktische Anwendungen dieses Algorithmus in der realen Welt
Antwort von Claude 3.7 Sonnet: Nächste Schritte zur Vertiefung im Hierarchischen Clustering¶
Sie haben bereits eine gute Grundlage im hierarchischen Clustering geschaffen! Hier sind verschiedene Wege, wie Sie Ihr Verständnis vertiefen und Ihre Implementierung erweitern können:
Komplexere Probleme zum Ausprobieren¶
Hochdimensionale Daten: Erweitern Sie Ihre Implementierung auf 3D oder noch höherdimensionale Daten und visualisieren Sie die Ergebnisse mit geeigneten Dimensionsreduktions-Techniken wie PCA oder t-SNE.
Gemischte Datentypen: Implementieren Sie hierarchisches Clustering für Datensätze mit sowohl kontinuierlichen als auch kategorialen Variablen, wobei Sie geeignete Distanzmaße wie Gower-Distanz verwenden.
Zeitreihen-Clustering: Wenden Sie hierarchisches Clustering auf Zeitreihen an, unter Verwendung spezieller Distanzmaße wie Dynamic Time Warping (DTW).
Textdaten: Clustern Sie Dokumente oder Textausschnitte basierend auf semantischer Ähnlichkeit.
Alternative Lösungsansätze¶
Dichtebasiertes Clustering: Implementieren Sie DBSCAN oder OPTICS und vergleichen Sie die Ergebnisse mit hierarchischem Clustering, besonders für nicht-konvexe Cluster.
Spektrales Clustering: Implementieren Sie spektrales Clustering, das besonders gut für Daten mit komplexen Strukturen geeignet ist.
Fuzzy Clustering: Statt harter Zuordnungen können Sie mit Fuzzy C-Means arbeiten, wo Datenpunkte teilweise zu mehreren Clustern gehören können.
Self-Organizing Maps (SOM): Eine neuronale Netzwerk-basierte Methode für dimensionale Reduktion und Clustering.
Optimierungen und Erweiterungen¶
Optimierung der Skalierbarkeit: Implementieren Sie eine effizientere Version des hierarchischen Clusterings für große Datensätze:
- BIRCH (Balanced Iterative Reducing and Clustering using Hierarchies)
- HDBScan (Hierarchical Density-Based Spatial Clustering of Applications with Noise)
Automatische Bestimmung der Cluster-Anzahl: Implementieren Sie Methoden wie:
- Gap-Statistik
- Silhouette-Analyse
- Elbow-Methode
- Minimierung des Bayes’schen Informationskriteriums (BIC)
Interaktive Visualisierung: Erstellen Sie ein Tool, mit dem Benutzer interaktiv den Schnitt im Dendrogramm anpassen können, um die resultierenden Cluster in Echtzeit zu sehen.
Ensemble-Clustering: Kombinieren Sie die Ergebnisse verschiedener Clustering-Algorithmen für robustere Ergebnisse.
Interessante Forschungsfragen¶
Interpretierbarkeit: Wie können wir die Ergebnisse des hierarchischen Clusterings intuitiver und interpretierbarer machen?
Transfer Learning für Clustering: Wie können wir Wissen aus einem Clustering-Problem auf ein anderes übertragen?
Robustes Clustering: Wie können wir die Stabilität der Clustering-Ergebnisse bei kleinen Änderungen in den Daten verbessern?
Deep Hierarchical Clustering: Integration von tiefen neuronalen Netzen mit hierarchischem Clustering für verbesserte Feature-Extraktion.
Online-Hierarchisches Clustering: Wie kann hierarchisches Clustering für Streaming-Daten effizient implementiert werden?
Praktische Anwendungen¶
Bioinformatik:
- Klassifizierung von Genen basierend auf Expressionsmustern
- Identifizierung von Proteinfamilien basierend auf Sequenzähnlichkeit
- Analyse der hierarchischen Struktur biologischer Taxonomien
Kundenanalyse:
- Kundensegmentierung für zielgerichtetes Marketing
- Analyse von Kaufmustern und Produktaffinitäten
- Identifizierung von Kundengruppen mit ähnlichem Verhalten
Bildverarbeitung:
- Bildkompression durch Clustering ähnlicher Bereiche
- Objekterkennung durch Clustering von Bildmerkmalen
- Medizinische Bildanalyse zur Erkennung von Gewebetypen
Dokumentenanalyse:
- Kategorisierung von Dokumenten in hierarchische Themenstrukturen
- Erstellung hierarchischer Wissensbasen
- Plagiatserkennung durch Clustering ähnlicher Textpassagen
Geoinformatik:
- Identifizierung von Regionen mit ähnlichen geografischen oder demographischen Merkmalen
- Analyse von Verkehrsmustern
- Erkennung von Naturkatastrophen-Risikogebieten
Praktisches Projekt-Beispiel¶
Ein interessantes Projekt wäre die Entwicklung einer Pipeline für hierarchisches Clustering, die Folgendes umfasst:
- Datenvorverarbeitung: Normalisierung, Ausreißerentfernung, Behandlung fehlender Werte
- Feature Engineering: Dimensionsreduktion, Feature-Selektion
- Automatische Modellselektion: Verschiedene Linkage-Methoden und Distanzmaße testen
- Cluster-Validierung: Vergleich und Bewertung der Ergebnisse mit verschiedenen Metriken
- Interpretierbare Visualisierung: Interaktive Darstellung der Cluster-Hierarchie
- Cluster-Charakterisierung: Automatische Beschreibung der Eigenschaften jedes Clusters
Solch ein System könnte für verschiedene Anwendungsbereiche angepasst werden und wäre ein ausgezeichnetes Werkzeug für Datenanalyse und Entscheidungsunterstützung.
Gibt es einer dieser Bereiche, der Sie besonders interessiert und in dem Sie tiefer einsteigen möchten?
1.7 Übungsaufgabe: Ihre erste Algorithmenimplementierung¶
Jetzt sind Sie an der Reihe, einen einfachen Algorithmus mithilfe eines LLM zu implementieren.
Problem: Finden von Primzahlen
Implementieren Sie einen Algorithmus, um die ersten $N$ Primzahlen zu finden.
Mögliche Schritte:
- Denken Sie über das Problem nach und formulieren Sie einen Ansatz (Sie erinnern sich vielleicht an das Sieb des Eratosthenes oder andere Methoden) oder diskutieren Sie mögliche Ansätze für einen Algorithmus mit Ihrem LLM
- Beschreiben Sie Ihren Ansatz in natürlicher Sprache und diskutieren Sie mit Ihrem LLM eine konkrete und präzise Formulierung
- Verwenden Sie ein LLM, um Ihre Lösung zu implementieren
- Testen Sie die Implementierung mit verschiedenen Eingaben
- Analysieren Sie die Effizienz Ihrer Lösung
- Visualisieren Sie Ihre Lösung/Ergebnisse
# Dies ist ein Arbeitsbereich, in dem Sie Ihren vom LLM generierten Code hereinkopieren und testen können
# Beispiel-Testfälle
testfaelle = [10, 20, 50]
# Testen Sie Ihre Funktion mit diesen Werten
# Zum Beispiel:
# for n in testfaelle:
# print(f"Primzahlen bis {n}: {finde_primzahlen(n)}")
1.8 Zusammenfassung der wichtigsten Punkte dieser Einheit¶
- Algorithmen sind schrittweise Verfahren zur Lösung von Problemen mit wohldefinierten Ein- und Ausgaben und endlicher Ausführung. Es gibt verschiedene Arten von Algorithmen, die sich in spezifischer Art und Weise unterscheiden und einordnen lassen. Viele Probleme lassen sich mit verschiedenen Algorithmen (Paradigmen) angehen.
- Large Language Models (LLMs) sind mächtige Werkzeuge, die in den vergangenen Jahren zu wertvollen Assistenten beim Lernen, Diskutieren, Brainstormen und Code Schreiben herangereift sind.
- LLMs können bei der Implementierung von Algorithmen helfen, was es uns im Rahmen der VU ermöglicht, uns auf das Verständnis von Konzepten und Algorithmen statt auf Syntaxdetails oder Coden insgesamt zu konzentrieren.
- Beachtenswert bei der Arbeit mit LLMs für die Algorithmenimplementierung:
- Seien Sie klar und spezifisch in Ihren Beschreibungen
- Arbeiten Sie mit dem LLM, bis der Code funktioniert
- Überprüfen Sie die Korrektheit der Lösungen
- Analysieren Sie, wenn nötig oder interessant, die Effizienz der Implementierungen
- Analysieren und visualisieren Sie die Ergebnisse