Graphen und Bäume: die Struktur, auf der Suche und neuronale Netze stehen
Knoten und Kanten, Grade, Adjazenzmatrix, Wege und Komponenten, BFS und Dijkstra, Bäume, minimaler Spannbaum und der Berechnungsgraph eines Neurons — alles an einem Graphen mit sieben Städten.
Serie KI-Grundlagen Teil 8 von 52
- Landkarte der künstlichen Intelligenz: was was ist und wie alles zusammenpasst
- Eine kurze Geschichte der künstlichen Intelligenz: von Turing bis zu Sprachmodellen
- Daten, Modell, Training, Inferenz: die Grundbegriffe an einem einzigen Beispiel
- Mathematische Notation lesen: Symbole, Indizes, Summen und Quantoren
- Mengen und Relationen: der Baukasten, aus dem Definitionen in der KI bestehen
- Funktionen und Abbildungen: warum jedes KI-Modell eine Funktion ist
- Logik und Beweise: die Regeln, nach denen KI schließt
- Graphen und Bäume: die Struktur, auf der Suche und neuronale Netze stehen
- Lineare Algebra für maschinelles Lernen: Vektoren, Matrizen und warum KI vor allem multipliziert
- Ableitungen, Gradient und Kettenregel: wie ein Modell herausfindet, in welche Richtung es lernen soll
- Gradientenabstieg: wie ein Modell lernt
- Was ist Wahrscheinlichkeit: vom Würfelwurf zu den Kolmogorow-Axiomen
- Bedingte Wahrscheinlichkeit und der Satz von Bayes: wie du deine Einschätzung änderst, wenn du etwas Neues erfährst
- Zufallsvariable und Verteilungsfunktion: eine Funktion, die alles über eine Verteilung verrät
- Dichtefunktion und Wahrscheinlichkeitsfunktion: warum eine stetige Zufallsvariable in einem Punkt die Wahrscheinlichkeit null hat
- Erwartungswert, Varianz und Kovarianz: wo eine Verteilung liegt, wie breit sie ist und was womit zusammenhängt
- Überblick über Wahrscheinlichkeitsverteilungen: Bernoulli-, Binomial-, Poisson-, Gleich-, Normal- und Exponentialverteilung
- Gesetz der großen Zahlen und zentraler Grenzwertsatz: Warum sich Durchschnitte so vorhersagbar verhalten
- Statistik und Parameterschätzung: Maximum Likelihood, MAP und Konfidenzintervalle
- Informationstheorie: Entropie, Kreuzentropie und KL-Divergenz — woher Verlustfunktionen kommen
- Zustandsraumsuche: BFS, DFS, UCS und A* mit Animationen
- Spiele und Gegner: Minimax, Alpha-Beta-Suche und Monte Carlo Tree Search
- Constraint Satisfaction: Backtracking, Constraint-Propagierung und AC-3 an Sudoku und einer Karte tschechischer Regionen
- Logik und wissensbasierte Systeme: Vorwärts- und Rückwärtsverkettung, Resolution und Expertensysteme
- Planung: STRIPS, die Blockwelt, Heuristiken aus Relaxierung und Planung in KI-Agenten
- Lokale Suche und simulierte Abkühlung: ein Handlungsreisender durch die tschechischen Regionalhauptstädte
- Genetische Algorithmen: Population, Kreuzung, Mutation und Selektion am Rucksack für die Wanderung
- Evolutionsstrategien und genetische Programmierung: von der 1/5-Erfolgsregel zum Keplerschen Gesetz
- Ameisenkolonieoptimierung: wie eine Pheromonspur einen kurzen Weg findet
- Partikelschwarmoptimierung: Trägheit, eigene Erfahrung und die Erfahrung des Schwarms
- Neuroevolution: Evolution der Gewichte und der Gestalt neuronaler Netze und der Algorithmus NEAT
- Fuzzy-Mengen und Zugehörigkeitsfunktionen: wie man „etwas warm“ aufschreibt
- Fuzzy-Logik und Wahrscheinlichkeit: warum „etwas warm“ nicht „wahrscheinlich warm“ bedeutet
- Fuzzy-Logik: t-Normen, Implikationen und WENN–DANN-Regeln
- Fuzzy-Regelung: Mamdani und Sugeno vom Ventilator bis zum Pendel auf dem Wagen
- Fuzzy-Clustering: der Algorithmus fuzzy c-means und Punkte zwischen den Clustern
- Neuro-Fuzzy-Systeme (ANFIS): Fuzzy-Regeln, die aus Daten lernen
- Markow-Ketten: Übergangsmatrix, stationäre Verteilung und PageRank
- Hidden-Markov-Modelle: der Viterbi-Algorithmus und das unehrliche Kasino
- Bayessche Netze: warum das Gras nass ist und was das für den Rasensprenger bedeutet
- Der Kalman-Filter: wie man aus ungenauen Messungen schätzt, wo ein Auto ist und wie schnell es fährt
- Arten des maschinellen Lernens: überwacht, unüberwacht, halbüberwacht und mehr
- Prompts, Kontext und RAG: wie man einem Modell die richtigen Informationen gibt
- Variational Autoencoder (VAE): neue Bilder aus einem stetigen latenten Raum
- Generative Adversarial Networks (GAN): der Fälscher gegen den Detektiv
- Diffusionsmodelle: wie aus reinem Rauschen ein Bild wird
- Markow-Entscheidungsprozesse: Zustände, Aktionen, Belohnungen und Bellman-Gleichungen
- Q-Learning und DQN: den Wert von Aktionen lernen, von der Tabelle bis zum neuronalen Netz
- Policy Gradient und PPO: die Strategie direkt lernen
- Wie ein KI-Agent funktioniert: ein Sprachmodell in einer Schleife mit Werkzeugen
- Programmieren mit KI-Agenten: Auftrag, Tests und Kontrolle
- Bewertung und Sicherheit von KI-Systemen: wie man misst und wie man überwacht
Der ganze Lehrplan der Serie 52 von 83 Themen fertig
- Was ein Graph, ein Grad und Zusammenhang sind — und wie man einen Graphen im Computer speichert.
- Warum der Weg mit den wenigsten Kanten nicht der kürzeste sein muss und wie BFS und Dijkstra damit umgehen.
- Was ein Baum und ein minimaler Spannbaum sind und warum ein neuronales Netz ein azyklischer Graph ist, der hin und zurück berechnet wird.
Vorher gut zu wissen: Mengen und Relationen: der Baukasten, aus dem Definitionen in der KI bestehen
Eine Karte mit Städten und Straßen, ein Freundesnetzwerk, der Zustandsraum eines Spiels, ein neuronales Netz und die Berechnung, in der ein Gradient ermittelt wird — all das ist ein Graph: Punkte, verbunden durch Linien. Graphen sind die universellste Struktur der Informatik, und in der künstlichen Intelligenz begegnest du ihnen auf Schritt und Tritt. Bäume sind ihr einfachster und nützlichster Spezialfall.
Den ganzen Teil bauen wir auf einem einzigen Graphen mit sieben Städten A bis G und Straßen mit Längen auf. Nach und nach zeigen wir daran Grade, wie man einen Graphen speichert, kürzeste Wege und den minimalen Spannbaum.
1. Was ein Graph ist
Definition — Graph
Sei V eine endliche nichtleere Menge; ihre Elemente nennen wir Knoten.
- Ein ungerichteter Graph ist ein Paar G = (V, E), wobei E ⊆ {{u, v} : u, v ∈ V, u ≠ v}; die Elemente von E sind die Kanten — zweielementige Teilmengen von V. Die Knoten u, v sind benachbart genau dann, wenn {u, v} ∈ E.
- Ein gerichteter Graph ist ein Paar G = (V, E), wobei E ⊆ V × V; eine Kante ist ein geordnetes Paar (u, v) — ein Pfeil von u nach v. Ein gerichteter Graph ist also eine Relation auf V (siehe den Teil Mengen und Relationen).
- Ein gewichteter Graph ist ein Tripel G = (V, E, w), wobei (V, E) ein Graph nach 1. oder 2. ist und w : E → ℝ eine Funktion, die jeder Kante e ∈ E ein Gewicht w(e) zuordnet (Länge, Kosten, Zeit).
Sofern nicht anders gesagt, ist ein Graph in diesem Teil ungerichtet.
Knoten sind „Dinge“, Kanten „Beziehungen“ zwischen ihnen. In einem ungerichteten Graphen gilt die Beziehung in beide Richtungen (eine Straße), in einem gerichteten nur in eine (eine Einbahnstraße, „folgt in den sozialen Medien“). Das Gewicht ist eine Zahl an der Kante — wie viele Kilometer, wie viele Euro, wie viele Minuten.
2. Grad eines Knotens
Definition — Grad und Handschlaglemma
Sei G = (V, E) ein ungerichteter Graph und v ∈ V. Der Grad des Knotens v ist die Anzahl der Kanten, die ihn enthalten (die von ihm ausgehen):
deg(v) = |{e ∈ E : v ∈ e}|.
Satz (Handschlaglemma). Sei G = (V, E) ein ungerichteter Graph. Dann gilt
∑v ∈ V deg(v) = 2 |E|.
Insbesondere ist die Summe der Grade in jedem ungerichteten Graphen eine gerade Zahl.
Jede Kante hat zwei Enden. Wenn du also zusammenzählst, wie viele Kanten von jedem Knoten ausgehen, zählst du jede Kante genau zweimal. Deshalb ist die Summe der Grade immer gerade.
Die Summe der Grade ist 2 + 4 + 4 + 4 + 4 + 4 + 2 = 24, und es gibt 12 Kanten, also 24 = 2 · 12 — genau wie im Satz (Farben nach der Formel unten). Der Grad hat praktische Bedeutung: In einem sozialen Netzwerk ist er die Anzahl der Freunde, im Web die Anzahl der Links, und Knoten mit hohem Grad sind meist die wichtigsten.
∑v ∈ V deg(v) = 2 · |E|
- ∑v ∈ V
- wir gehen alle Knoten durch — wir zählen die Kanten „aus Sicht der Knoten“
- deg(v)
- bei jedem Knoten die Enden der Kanten, die ihn berühren
- 2
- jede Kante {u, v} hat zwei Enden, also trifft die Summe sie zweimal — einmal bei u, einmal bei v
- |E|
- die Anzahl der Kanten — dieselbe Zählung „aus Sicht der Kanten“
Satz — die Anzahl der Knoten mit ungeradem Grad ist gerade
Satz. Sei G = (V, E) ein ungerichteter Graph und L = {v ∈ V : deg(v) ist ungerade} die Menge seiner Knoten mit ungeradem Grad. Dann ist |L| eine gerade Zahl.
Beweis. Wir teilen die Summe der Grade in Knoten mit geradem und mit ungeradem Grad auf: ∑v ∈ V deg(v) = ∑v ∈ V ∖ L deg(v) + ∑v ∈ L deg(v). Die linke Seite ist nach dem Handschlaglemma 2|E|, also gerade; die erste Summe rechts ist eine Summe gerader Zahlen, also gerade. Daher muss auch die zweite gerade sein — und eine Summe von |L| ungeraden Zahlen ist genau dann gerade, wenn |L| gerade ist. ∎
Deshalb lässt sich kein Graph mit fünf Knoten zeichnen, von denen jeder den Grad 3 hat: Die Summe 15 wäre ungerade.
3. Wie man einen Graphen im Computer speichert
Ein Computer sieht kein Bild, er braucht Zahlen. Üblich sind zwei Formen:
- Adjazenzmatrix — eine Tabelle |V| × |V|; in der Zelle (u, v) steht das Gewicht der Kante oder 0. Schnelles „Gibt es hier eine Kante?“, aber der Speicher wächst wie |V|2.
- Adjazenzliste — zu jedem Knoten eine Liste seiner Nachbarn. Der Speicher wächst nur wie |V| + |E|, was bei dünn besetzten Graphen (soziales Netzwerk, Landkarte) einen riesigen Unterschied macht.
Definition — Adjazenzmatrix und Adjazenzliste
Sei G = (V, E, w) ein gewichteter ungerichteter Graph mit von null verschiedenen Gewichten (w(e) ≠ 0 für alle e ∈ E) und n = |V| Knoten, die wir durchnummerieren: V = {v1, …, vn}. Die Adjazenzmatrix von G ist die quadratische Matrix A ∈ ℝn×n mit den Einträgen (für i, j = 1, …, n)
Aij = w({vi, vj}), falls {vi, vj} ∈ E; sonst Aij = 0.
Die von null verschiedenen Gewichte braucht man, damit sich eine Kante von einer fehlenden Kante unterscheiden lässt. Bei einem ungewichteten Graphen schreibt man statt des Gewichts 1. Weil {vi, vj} = {vj, vi}, hat ein ungerichteter Graph Aij = Aji, also A = AT.
Für v ∈ V bezeichne N(v) die Menge seiner Nachbarn:
N(v) = {u ∈ V : {u, v} ∈ E}.
Die Adjazenzliste ist die Abbildung, die jedem Knoten v ∈ V die Menge N(v) zuordnet, bei einem gewichteten Graphen samt Gewichten: {(u, w({u, v})) : u ∈ N(v)}. Es gilt |N(v)| = deg(v).
Beispiel — wie viel Platz der Städtegraph und ein Straßennetz brauchen
Städtegraph: Die Matrix hat 7 · 7 = 49 Zellen, davon nur 24 von null verschieden (jede Straße zweimal, ober- und unterhalb der Diagonale). Die Adjazenzlisten haben zusammen ∑ deg(v) = 2 · 12 = 24 Einträge (Farben nach dem Handschlaglemma oben). Die Zeile B der Matrix ist (4, 0, 5, 3, 7, 0, 0) — vier von null verschiedene Zahlen, weil deg(B) = 4.
Ein Straßennetz mit einer Million Kreuzungen, von jeder gehen im Schnitt 3 Straßen aus: |E| = 106 · 3/2 = 1,5 · 106. Die Matrix hat (106)2 = 1012 Zellen, die Listen nur 2 · 1,5 · 106 = 3 · 106 Einträge — über dreihunderttausendmal weniger.
Die Matrix ist symmetrisch zur Diagonale — das Kennzeichen eines ungerichteten Graphen. In der KI wird die Adjazenzmatrix in graphbasierten neuronalen Netzen verwendet, die direkt auf Graphen lernen (Moleküle, soziale Netzwerke, Straßennetze).
4. Wege, Kreise und Zusammenhang
Definition — Weg, Kreis, Zusammenhang
Sei G = (V, E) ein ungerichteter Graph und u, v ∈ V.
- Ein Weg von u nach v ist eine Folge von Knoten (v0, v1, …, vk), k ≥ 0, sodass v0 = u, vk = v, vi ≠ vj für i ≠ j, {vi−1, vi} ∈ E für i = 1, …, k. Seine Länge ist k (die Anzahl der Kanten); in einem gewichteten Graphen (V, E, w) ist es die Summe der Gewichte ∑i=1k w({vi−1, vi}).
- Ein Kreis ist eine Folge von Knoten (v0, v1, …, vk) mit k ≥ 3, in der v0, …, vk−1 paarweise verschieden sind, vk = v0 und {vi−1, vi} ∈ E für i = 1, …, k — ein Weg, der nach mindestens drei Kanten zu seinem Startknoten zurückkehrt.
- G ist zusammenhängend genau dann, wenn ∀u, v ∈ V existiert ein Weg von u nach v.
- Wir definieren die Relation ∼ auf V: u ∼ v genau dann, wenn ein Weg von u nach v existiert. Sie ist eine Äquivalenz (reflexiv dank des Weges der Länge 0, symmetrisch durch Umkehren des Weges, transitiv, weil sich aus der Verkettung zweier Wege ein Weg auswählen lässt). Ihre Äquivalenzklassen sind die Zusammenhangskomponenten von G; sie bilden also eine Zerlegung von V (siehe den Teil Mengen und Relationen).
Ein Graph ist also genau dann zusammenhängend, wenn er eine einzige Zusammenhangskomponente hat.
Ein Weg ist eine Route, die nirgends zweimal durch denselben Ort führt. Ein Kreis ist ein Rundweg. Ein zusammenhängender Graph ist einer, in dem du von überall überallhin kommst; sonst zerfällt er in „Inseln“ — Komponenten.
5. Kürzeste Wege
Die Aufgabe „Wie komme ich am schnellsten von A nach G?“ ist die Suche nach einem kürzesten Weg — und genau das haben die Algorithmen aus dem Teil über die Suche im Zustandsraum getan. Es kommt aber darauf an, was wir messen.
Definition — Abstand in einem Graphen
Sei G = (V, E, w) ein gewichteter ungerichteter Graph mit nichtnegativen Gewichten, w : E → [0, ∞), und u, v ∈ V. Für einen Weg π bezeichne w(π) = ∑e ∈ π w(e) seine Länge (die Summe der Gewichte seiner Kanten). Der Abstand der Knoten u und v ist
d(u, v) = min {w(π) : π ist ein Weg von u nach v}, falls mindestens ein solcher Weg existiert; sonst d(u, v) = ∞.
Das Minimum existiert, weil es in einem endlichen Graphen nur endlich viele Wege gibt. Für einen ungewichteten Graphen nehmen wir w(e) = 1 für alle e ∈ E; der Abstand ist dann die kleinste Anzahl von Kanten eines Weges von u nach v.
d(u, v) = minπ : u → v ∑e ∈ π w(e)
- d(u, v)
- eine Zahl für ein Paar von Knoten — das, was der Algorithmus sucht
- minπ
- Wege von u nach v gibt es meist viele; uns interessiert der beste, daher das Minimum über alle
- ∑e ∈ π
- die Länge eines Weges setzt sich aus den Kanten zusammen, die auf ihm liegen — wir summieren sie
- w(e)
- das Gewicht einer Kante (Kilometer); bei der BFS ist jedes gleich 1, und die Summe zählt dann Kanten
Wenn uns die Anzahl der Kanten interessiert, genügt die Breitensuche: zuerst die Nachbarn von A, dann deren Nachbarn … Die Zahl über einem Knoten ist sein Abstand von A in Kanten:
from collections import deque
def bfs(sousede, start):
d = {start: 0}
fronta = deque([start])
while fronta:
u = fronta.popleft() # wer zuerst kommt, kommt zuerst dran
for v in sousede[u]:
if v not in d: # erster Besuch = wenigste Kanten
d[v] = d[u] + 1
fronta.append(v)
return d
Von A aus erhalten wir so die Schichten: B, C im Abstand 1; D, E, F im Abstand 2; G im Abstand 3. Jeder Knoten wird einmal aus der Warteschlange genommen und jede Kante zweimal untersucht (von beiden Enden), also läuft die BFS in einer Zeit proportional zu |V| + |E|.
Nach G führen drei Kanten (zum Beispiel A–B–E–G). Wenn wir aber die Länge der Straßen zählen, brauchen wir den Dijkstra-Algorithmus — er macht immer beim nächstgelegenen noch nicht abgeschlossenen Knoten weiter. Die Zahl über einem Knoten ist jetzt der kürzeste Abstand in Kilometern, und hervorgehoben ist der kürzeste Weg nach G:
Der kürzeste Weg A–B–D–E–G hat vier Kanten, also mehr als der Weg mit drei Kanten aus der BFS, aber nur die Länge 12. Der Weg A–B–E–G wäre 4 + 7 + 3 = 14 lang (Farben nach der Abstandsformel oben). Weniger Schritte bedeuten keinen kürzeren Weg — genau diesen Unterschied zwischen BFS und UCS haben wir im Labyrinth mit Schlamm gesehen.
Der Dijkstra-Algorithmus Schritt für Schritt
Der Algorithmus hält bei jedem Knoten eine Schätzung d[v] — die Länge des besten bisher gefundenen Weges. Am Anfang ist d[A] = 0 und überall sonst ∞. Dann wiederholt er: Nimm den nicht abgeschlossenen Knoten mit der kleinsten Schätzung, schließe ihn ab und versuche, über ihn die Schätzungen seiner Nachbarn zu verbessern. Diese Verbesserung heißt Relaxation einer Kante:
d[v] ← min( d[v], d[u] + w(u, v) )
- d[v]
- die bisher beste bekannte Länge bis v; sie bleibt, wenn der neue Weg nicht besser ist — die Schätzung wird nie schlechter
- d[u]
- die Länge bis zum gerade abgeschlossenen u — sie ist sicher schon die kürzeste, also können wir darauf aufbauen
- w(u, v)
- der letzte Abschnitt von u nach v; zusammen mit d[u] ist das die Länge des Kandidatenweges „über u“
import heapq
def dijkstra(sousede, start):
d = {v: float("inf") for v in sousede}
d[start] = 0
hotove = set()
halda = [(0, start)]
while halda:
du, u = heapq.heappop(halda) # nicht abgeschlossener Knoten mit kleinstem d
if u in hotove:
continue
hotove.add(u) # d[u] ist jetzt endgültig
for v, w in sousede[u]:
if du + w < d[v]: # Relaxation der Kante (u, v)
d[v] = du + w
heapq.heappush(halda, (d[v], v))
return d
Im Städtegraphen sieht der Ablauf von A aus so aus. Eine Zeile zeigt die Schätzungen nach dem Abschließen des Knotens in der ersten Spalte; fett sind die Abstände, die schon endgültig sind:
| Schritt | abgeschlossen | d[A] | d[B] | d[C] | d[D] | d[E] | d[F] | d[G] |
|---|---|---|---|---|---|---|---|---|
| 1 | A | 0 | 4 | 2 | ∞ | ∞ | ∞ | ∞ |
| 2 | C | 0 | 4 | 2 | 8 | ∞ | 10 | ∞ |
| 3 | B | 0 | 4 | 2 | 7 | 11 | 10 | ∞ |
| 4 | D | 0 | 4 | 2 | 7 | 9 | 10 | ∞ |
| 5 | E | 0 | 4 | 2 | 7 | 9 | 10 | 12 |
| 6 | F | 0 | 4 | 2 | 7 | 9 | 10 | 12 |
| 7 | G | 0 | 4 | 2 | 7 | 9 | 10 | 12 |
Die Farben entsprechen den Gliedern der Relaxation oben. Im dritten Schritt verbessert sich die Schätzung von D von 8 (über C: 2 + 6) auf 7 (über B: 4 + 3) und im vierten die Schätzung von E von 11 (über B: 4 + 7) auf 9 (über D: 7 + 2). G bekommt 9 + 3 = 12 und ändert sich nicht mehr: Der Weg über F wäre 10 + 5 = 15 lang.
Satz — Dijkstra ist für nichtnegative Gewichte korrekt
Satz. Sei G = (V, E, w) ein gewichteter Graph mit nichtnegativen Gewichten, w : E → [0, ∞), start ∈ V der Startknoten und d[·] die Schätzungen des Dijkstra-Algorithmus, gestartet von start (Beschreibung oben). Dann gilt für jeden Knoten u in dem Moment, in dem der Algorithmus ihn abschließt,
d[u] = d(start, u).
Warum: Jeder andere Weg nach u muss die Menge der abgeschlossenen Knoten irgendwo zum ersten Mal verlassen, und zwar über einen Knoten x, dessen Schätzung ≥ d[u] ist (sonst hätten wir x gewählt, nicht u). Der Rest des Weges von x nach u hat eine nichtnegative Länge, also ist der ganze Weg nicht kürzer als d[u]. ∎
Mit einer negativen Kante fällt diese Überlegung — „der Rest des Weges“ könnte die Länge verkürzen. Für negative Gewichte braucht man daher einen anderen Algorithmus (Bellman–Ford).
6. Bäume
Definition — Baum und Wurzelbaum
Sei G = (V, E) ein ungerichteter Graph.
- G ist ein Wald genau dann, wenn er keinen Kreis enthält (jede seiner Zusammenhangskomponenten ist dann ein Baum).
- G ist ein Baum genau dann, wenn er zusammenhängend ist und keinen Kreis enthält.
Satz. Sei G = (V, E) ein ungerichteter Graph mit n = |V| ≥ 1 Knoten. Dann sind die folgenden Aussagen äquivalent:
- G ist ein Baum;
- G ist zusammenhängend und |E| = n − 1;
- G enthält keinen Kreis und |E| = n − 1;
- ∀u, v ∈ V gibt es genau einen Weg von u nach v.
Ein Wurzelbaum ist ein Paar (T, r), wobei T = (V, E) ein Baum und r ∈ V ein ausgezeichneter Knoten ist, die Wurzel. Für v ∈ V:
- die Tiefe h(v) ist die Länge (Anzahl der Kanten) des einzigen Weges von r nach v; insbesondere h(r) = 0;
- die Kinder des Knotens v sind seine Nachbarn in Richtung weg von der Wurzel, d. h. die Knoten der Menge {u ∈ N(v) : h(u) = h(v) + 1}; v ist ihr Elternknoten;
- v ist ein Blatt genau dann, wenn er keine Kinder hat.
Ein Baum ist ein Graph ohne jeden Rundweg — zwischen zwei Orten gibt es immer genau eine Route. Ein Stammbaum, Ordner auf der Festplatte, verzweigte Entscheidungen. Nimmst du eine einzige Kante weg, zerfällt der Baum; fügst du eine hinzu, entsteht ein Kreis.
In der KI sind Bäume überall. Ein Entscheidungsbaum klassifiziert mit einer Reihe von Fragen von der Wurzel bis zu einem Blatt. Suchalgorithmen bauen einen Suchbaum — der BFS- und DFS-Baum aus dem Teil über die Suche ist genau das. Und ein Schachprogramm berechnet einen Spielbaum, in dem die Knoten Stellungen und die Kanten Züge sind.
Satz — ein Baum mit n Knoten hat n − 1 Kanten (Beweis durch Induktion)
Satz. Sei T = (V, E) ein Baum mit n = |V| ≥ 1 Knoten. Dann ist |E| = n − 1.
Lemma: Sei T ein Baum mit mindestens zwei Knoten. Dann hat T ein Blatt, d. h. einen Knoten vom Grad 1. Beweis des Lemmas: Wir nehmen einen längsten Weg im Baum; sein Endknoten kann keinen weiteren Nachbarn außerhalb des Weges haben (der Weg ließe sich verlängern) und auch keinen auf dem Weg (es entstünde ein Kreis) — er hat also den Grad 1.
Beweis des Satzes durch Induktion nach n. Induktionsanfang: Ein Baum mit 1 Knoten hat 0 Kanten. Induktionsschritt: In einem Baum mit n + 1 Knoten entfernen wir ein Blatt samt seiner einzigen Kante. Der Rest ist weiterhin zusammenhängend und ohne Kreise, also ein Baum mit n Knoten, und hat nach Induktionsvoraussetzung n − 1 Kanten. Der ursprüngliche Baum hatte eine mehr, n. ∎
Ein Suchbaum wächst schnell. Hat jeder Knoten b Kinder (Verzweigungsgrad), liegen in der Tiefe k genau bk Knoten und im ganzen Baum bis zur Tiefe d:
N(b, d) = ∑k=0d bk = bd+1 − 1/b − 1
- N(b, d)
- wie viele Knoten die Suche im schlimmsten Fall durchgehen muss — also wie viel Zeit und Speicher sie kostet
- ∑k=0d
- wir summieren Ebene für Ebene, von der Wurzel (k = 0) bis zur tiefsten Ebene d
- bk
- jeder Knoten der Ebene k − 1 hat b Kinder, also vervielfacht sich die Anzahl mit jeder Ebene um b
- bd+1 − 1/b − 1
- die Summe der geometrischen Reihe in geschlossener Form (für b ≥ 2); sie zeigt, dass die letzte Ebene alle vorherigen zusammen überwiegt
7. Minimaler Spannbaum
Definition — Spannbaum und Kruskal-Algorithmus
Sei G = (V, E, w) ein zusammenhängender gewichteter ungerichteter Graph.
- Ein Spannbaum von G ist ein Graph T = (V, F) mit F ⊆ E, der ein Baum ist — er enthält alle Knoten von G und nur einige seiner Kanten.
- Das Gewicht des Spannbaums ist w(T) = ∑e ∈ F w(e).
- Ein Spannbaum T ist minimal genau dann, wenn w(T) ≤ w(T′) für jeden Spannbaum T′ von G.
Kruskal-Algorithmus.
- Eingabe: ein zusammenhängender gewichteter ungerichteter Graph G = (V, E, w) mit m = |E| Kanten.
- Ausgabe: eine Kantenmenge F ⊆ E, sodass (V, F) ein minimaler Spannbaum von G ist.
- Sortiere die Kanten aufsteigend nach Gewicht: e1, …, em mit w(e1) ≤ … ≤ w(em).
- Setze F := ∅.
- Für i = 1, …, m: Enthält der Graph (V, F ∪ {ei}) keinen Kreis, setze F := F ∪ {ei}; sonst lass die Kante ei aus.
- Gib F zurück.
Abbruch: Der Algorithmus endet, nachdem er alle m Kanten durchgegangen ist (er darf früher enden, sobald |F| = |V| − 1). Es ist ein gieriger Algorithmus — in jedem Schritt nimmt er die leichteste zulässige Kante und ändert eine Entscheidung nie mehr — und trotzdem findet er immer einen minimalen Spannbaum (Satz unten).
Du willst alle Städte so günstig wie möglich mit Stromleitungen verbinden. Es reicht, dass man von überall überallhin kommt — Rundwege sind überflüssig. Du nimmst die kürzesten Abschnitte zuerst und lässt die aus, die einen Rundweg schließen würden.
Der Spannbaum hat 6 Kanten für 7 Knoten — genau n − 1, wie der Satz über Bäume sagt — und eine Gesamtlänge von 18. Die sechs übrigen Straßen hat der Algorithmus abgelehnt, weil sie einen Kreis gebildet hätten.
Beispiel — Kruskal im Städtegraphen Kante für Kante
Die Kanten nach Länge sortiert (angenommene fett): A–C (2), D–E (2), B–D (3), E–G (3), A–B (4), D–F (4), B–C (5), F–G (5), C–D (6), E–F (6), B–E (7), C–F (8).
Die ersten sechs Kanten werden angenommen: A–C und D–E verbinden Paare, B–D fügt B zu D–E hinzu, E–G fügt G hinzu, und A–B verbindet die Gruppe {A, C} mit der Gruppe {B, D, E, G}. D–F schließt den letzten Knoten F an. Jetzt liegen alle Knoten in einer Komponente, also würde jede weitere Kante zwei Knoten derselben Komponente verbinden — sie würde einen Kreis schließen, und der Algorithmus lehnt sie ab.
Länge des Spannbaums: 2 + 2 + 3 + 3 + 4 + 4 = 18.
def kruskal(vrcholy, hrany):
rodic = {v: v for v in vrcholy} # jeder Knoten steht vorerst für sich allein
def koren(v): # Vertreter der Komponente, in der v liegt
while rodic[v] != v:
v = rodic[v]
return v
kostra = []
for u, v, w in sorted(hrany, key=lambda h: h[2]):
ku, kv = koren(u), koren(v)
if ku == kv:
continue # u und v sind schon verbunden: die Kante würde einen Kreis schließen
rodic[ku] = kv # Vereinigung zweier Komponenten
kostra.append((u, v, w))
return kostra
Satz — warum das gierige Vorgehen funktioniert (Schnitteigenschaft)
Sei G = (V, E, w) ein zusammenhängender gewichteter ungerichteter Graph. Ein Schnitt ist eine Aufteilung der Knoten in zwei nichtleere Teile S und V ∖ S, wobei ∅ ≠ S ⊊ V. Eine Kante {u, v} ∈ E kreuzt den Schnitt genau dann, wenn |{u, v} ∩ S| = 1 (ein Ende in S, das andere außerhalb).
Satz. Sei (S, V ∖ S) ein Schnitt von G und e eine Kante, die den Schnitt kreuzt und unter allen Kanten, die diesen Schnitt kreuzen, das kleinste Gewicht hat. Dann gibt es einen minimalen Spannbaum von G, der die Kante e enthält.
Beweis: Nehmen wir einen beliebigen minimalen Spannbaum T; enthält er e, sind wir fertig. Andernfalls betrachten wir T ohne e. Durch Hinzufügen von e entsteht ein Kreis, und der muss den Schnitt noch ein weiteres Mal kreuzen, über eine Kante f. Durch Entfernen von f erhalten wir wieder einen Spannbaum, mit dem Gewicht w(T) + w(e) − w(f) ≤ w(T), weil w(e) ≤ w(f). Er ist also ebenfalls minimal und enthält e. ∎
Kruskal nimmt eine Kante nur dann an, wenn sie zwei verschiedene Komponenten verbindet — und sie ist die leichteste Kante über den Schnitt „eine dieser Komponenten gegen den Rest“, weil er alle leichteren schon verarbeitet hat.
8. Azyklische Graphen und die Berechnung eines neuronalen Netzes
Definition — DAG und topologische Sortierung
Sei G = (V, E) ein gerichteter Graph, E ⊆ V × V, mit n = |V| Knoten.
- Ein gerichteter Kreis ist eine Folge von Knoten (v0, v1, …, vk), k ≥ 1, sodass (vi−1, vi) ∈ E für i = 1, …, k und vk = v0 — entlang der Pfeile kommt man zum Startknoten zurück.
- G ist ein gerichteter azyklischer Graph (DAG) genau dann, wenn er keinen gerichteten Kreis enthält.
- Eine topologische Sortierung von G ist eine Bijektion π : V → {1, …, n} (eine Reihenfolge der Knoten), sodass ∀(u, v) ∈ E : π(u) < π(v), d. h. jede Kante führt von links nach rechts — u steht in der Reihenfolge vor v.
Satz. Sei G = (V, E) ein gerichteter Graph. Dann hat G genau dann eine topologische Sortierung, wenn er azyklisch ist.
Ein DAG ist ein Graph von Abhängigkeiten, in dem sich nichts im Kreis dreht: zuerst die Socken anziehen, dann die Schuhe. Eine topologische Sortierung ist eine Reihenfolge, in der sich alles so erledigen lässt, dass jeder Schritt schon bereit hat, wovon er abhängt.
So rechnet jedes neuronale Netz. Hier ist die Berechnung eines einzelnen Neurons ŷ = σ(w · x + b) und seines Fehlers L(ŷ, y) als Berechnungsgraph: Eingaben links, Operationen als Knoten, Pfeile zeigen, was wovon abhängt. Die Zahlen sind die topologische Reihenfolge:
Das Netz rechnet vorwärts in topologischer Reihenfolge — zuerst das Produkt, dann die Summe, die Sigmoide und den Fehler. Und wenn es dann lernt, geht es denselben Graphen rückwärts: Die Fehlerrückführung (Backpropagation) durchläuft die Knoten in umgekehrter topologischer Reihenfolge und wendet in jedem die Kettenregel an. Bibliotheken wie PyTorch bauen sich diesen Graphen bei jeder Berechnung selbst. Übrigens ist auch der Graph der Voraussetzungen der Module im Lehrplan dieser Reihe ein DAG.
Wie man die topologische Reihenfolge findet
Der Eingangsgrad eines Knotens v eines gerichteten Graphen (V, E), deg−(v) = |{u ∈ V : (u, v) ∈ E}|, ist die Anzahl der Pfeile, die nach v führen — die Anzahl der Dinge, von denen v abhängt. Der Algorithmus von Kahn nimmt wiederholt einen Knoten, der auf nichts mehr wartet:
from collections import deque
def topologicke(vrcholy, hrany):
vstupni = {v: 0 for v in vrcholy}
for u, v in hrany:
vstupni[v] += 1
fronta = deque(v for v in vrcholy if vstupni[v] == 0) # Eingaben: hängen von nichts ab
poradi = []
while fronta:
u = fronta.popleft()
poradi.append(u)
for a, b in hrany:
if a == u:
vstupni[b] -= 1 # u ist fertig, b wartet auf eine Sache weniger
if vstupni[b] == 0:
fronta.append(b)
return poradi if len(poradi) == len(vrcholy) else None # None: der Graph hat einen Kreis
Im Graphen des Neurons ergibt sich die Reihenfolge x, w, b, y, ·, +, σ, L: zuerst die vier Eingaben, dann Produkt, Summe, Sigmoide und Verlust. Hat der Graph einen Kreis, warten die Knoten auf dem Kreis gegenseitig aufeinander, ihr Eingangsgrad sinkt nie auf null, und in die Reihenfolge gelangen sie nicht — daher die letzte Zeile.
Beispiel — Durchlauf durch den Graphen des Neurons hin und zurück
Eingaben x = 2, w = 0,5, b = −1, richtige Antwort y = 1, Verlust L = (ŷ − y)2, σ(z) = 1/1 + e−z.
Vorwärts (in topologischer Reihenfolge): w · x = 1; z = 1 + (−1) = 0; ŷ = σ(0) = 0,5; L = (0,5 − 1)2 = 0,25.
Rückwärts (in umgekehrter Reihenfolge; Farben nach der Aufschlüsselung der Kettenregel unten): ∂L/∂ŷ = 2(ŷ − y) = −1; ∂ŷ/∂z = σ(z)(1 − σ(z)) = 0,25; ∂L/∂z = −1 · 0,25 = −0,25; ∂L/∂b = ∂L/∂z · 1 = −0,25; ∂L/∂w = ∂L/∂z · ∂z/∂w = −0,25 · 2 = −0,5 (∂z/∂w = x = 2).
Beide Ableitungen sind negativ: Durch Vergrößern von w wie auch b wird der Verlust kleiner — das Netz rückt näher an die Antwort 1.
∂L/∂w = ∂L/∂ŷ · ∂ŷ/∂z · ∂z/∂w
- ∂L/∂w
- wie empfindlich der Fehler auf das Gewicht reagiert — danach wird das Gewicht beim Lernen verschoben
- ∂L/∂ŷ
- Kante σ → L: um wie viel sich der Fehler ändert, wenn sich die Ausgabe ändert; wird zuerst berechnet, weil L in der Reihenfolge der letzte ist
- ∂ŷ/∂z
- Kante + → σ: wie die Sigmoide eine Änderung ihrer Eingabe weitergibt
- ∂z/∂w
- Kanten w → · → +: Änderung der Summe pro Einheit des Gewichts, also x
9. Graphen in der künstlichen Intelligenz
- Ein Zustandsraum ist ein Graph: Zustände sind Knoten, Aktionen Kanten mit Kosten. Suche ist das Finden eines Weges in einem Graphen.
- Die Berechnungsgraphen neuronaler Netze sind DAGs; Training ist ein Durchlauf hin und zurück.
- Wissensgraphen speichern Fakten als Kanten wie „Prag — ist die Hauptstadt von — Tschechien“, und Suchmaschinen setzen daraus Antworten zusammen.
- Bayessche Netze sind DAGs probabilistischer Abhängigkeiten — zu ihnen kommen wir im Modul über probabilistische Modelle.
- Graphbasierte neuronale Netze (Graph Neural Networks) lernen direkt auf Graphen: Sie sagen Eigenschaften von Molekülen, Verkehr oder Empfehlungen vorher.
10. Zusammenfassung
- Ein Graph ist eine Menge von Knoten und Kanten; Kanten können eine Richtung und ein Gewicht haben. Die Summe der Grade ist das Doppelte der Anzahl der Kanten.
- Ein Graph wird als Adjazenzmatrix oder als Adjazenzliste gespeichert — Letztere ist bei dünn besetzten Graphen viel sparsamer.
- BFS findet den Weg mit den wenigsten Kanten, Dijkstra den kürzesten nach Gewichten — und das muss nicht dasselbe sein.
- Ein Baum ist ein zusammenhängender Graph ohne Kreise mit n − 1 Kanten; einen minimalen Spannbaum findet der gierige Kruskal-Algorithmus.
- Ein neuronales Netz ist ein Berechnungs-DAG: Vorwärts wird in topologischer Reihenfolge gerechnet, der Gradient rückwärts.
Siehst du das anders? Schreib an support@coden-app.com.