Weltmodell im Code · Teil 3 von 3

Dynamik lernen und im Traum planen

24. August 2026 · Lesezeit ca. 8 Minuten

Aus einem Zustand und einer Aktion wird eine mögliche Zukunft. Dieser Teil zeigt das Dynamiknetz, mehrstufige Vorhersagen und den Planer, der 100 Aktionsfolgen gegeneinander antreten lässt.

Vier Aktionen werden zu vier Schaltern

Der latente Zustand hat acht Werte. Die Aktion kommt als One-Hot-Vektor hinzu: Genau einer von vier Werten steht auf 1. Damit erhält das Dynamiknetz zwölf Eingabewerte.

def dynamik_eingabe(z, aktion):
    einheit = [0.0] * 4
    einheit[aktion] = 1.0
    return list(z) + einheit

# Beispiel für "links":
# [z0, z1, ... z7, 0, 0, 1, 0]

Eine einzelne Aktionsnummer wäre ebenfalls eine Zahl, würde aber eine falsche Ordnung nahelegen. „Rechts“ ist nicht doppelt so viel wie „Runter“. One-Hot-Kodierung behandelt alle vier Richtungen als getrennte Kategorien.

Zehn Ausgaben beschreiben den nächsten Moment

Das Netz gibt acht Werte für den nächsten Zustand, einen für die Belohnung und einen für das Ende der Episode aus.

Dynamiknetz: 12 → 32 → 10

Ausgabe 0 bis 7: nächster latenter Zustand
Ausgabe 8:       erwartete Belohnung
Ausgabe 9:       Wahrscheinlichkeit für Ende

Für Zustand, Belohnung und Ende gelten unterschiedliche Wertebereiche. tanh begrenzt den Zustand auf −1 bis 1. Eine Sigmoidfunktion macht aus den beiden letzten Rohwerten Wahrscheinlichkeiten zwischen 0 und 1.

def dynamik_ausgabe(roh):
    z = [math.tanh(w) for w in roh[:8]]
    belohnung = sigmoid(roh[8])
    fertig = sigmoid(roh[9])
    return z, belohnung, fertig

Das Training kennt die richtige Zukunft

Die Übergänge aus Teil 1 enthalten Bild vorher, Aktion und Bild nachher. Der fertige Encoder aus Teil 2 übersetzt beide Bilder in latente Zustände. Das Dynamiknetz soll aus dem ersten Zustand und der Aktion den zweiten Zustand treffen.

eingabe = dynamik_eingabe(
    kodiere(modell, uebergang["vorher"]),
    uebergang["aktion"])

ziel_z = kodiere(modell, uebergang["nachher"])
ziel_r = uebergang["belohnung"]
ziel_f = uebergang["fertig"]

Die Belohnung kommt nur in 1,3 Prozent der Trainingszeilen vor. Ihr Fehler wird deshalb mit 10 multipliziert. Ohne dieses Gegengewicht wäre „immer null“ eine bequeme Lösung für das Netz.

Gewichtung verändert das Lernziel. Der Faktor 10 ist eine Entscheidung dieses Versuchs. Er macht seltene Belohnungen sichtbarer, kann aber auch falsche Belohnungen verstärken. Genau solche Fehler tauchen später im Planer auf.

Ein Traumschritt fragt die Umgebung nicht

Nach dem Training braucht eine Vorhersage nur noch das Dynamiknetz:

def traumschritt(modell, z, aktion):
    x = dynamik_eingabe(z, aktion)
    roh, _ = vorwaerts(modell["dynamik"], x)
    return dynamik_ausgabe(roh)

Für einen längeren Traum wird der vorhergesagte Zustand wieder zur nächsten Eingabe. Nach dem ersten Bild bekommt das Modell keine echte Beobachtung mehr.

z = kodiere(modell, erstes_bild)

for aktion in aktionsfolge:
    z, belohnung, fertig = traumschritt(modell, z, aktion)

Dadurch wachsen Fehler mit jedem Schritt. In der Messung lag die vorhergesagte Position nach einem Schritt in 95,0 Prozent der Fälle richtig, nach zehn Schritten in 71,3 Prozent.

Der Planer probiert 100 Folgen aus

Random Shooting ist eine kurze, gut lesbare Planungsmethode. Sie erzeugt zufällige Aktionsfolgen, spielt jede im Modell durch und behält die Folge mit der höchsten vorhergesagten Belohnung.

def plane(modell, z, horizont, versuche, zufall):
    beste_folge = None
    bester_wert = -1e9

    for _ in range(versuche):
        folge = [zufall.randrange(4)
                 for _ in range(horizont)]
        zt = z
        wert = 0.0

        for k, aktion in enumerate(folge):
            zt, r, fertig = traumschritt(modell, zt, aktion)
            wert += (0.95 ** k) * r

        if wert > bester_wert:
            beste_folge, bester_wert = folge, wert

    return beste_folge[0], bester_wert

Der Faktor 0.95 ** k wertet frühe Belohnungen etwas höher als späte. Die beste Folge wird nicht vollständig in die echte Umgebung eingespielt. Hat der Planer zum Beispiel [rechts, rechts, runter, links, …] gewählt, gibt return beste_folge[0] nur rechts zurück. Die übrigen Schritte dienen allein dazu, den ersten Zug zu bewerten.

Nach jedem echten Zug wird neu geplant

Die zurückgegebene Aktion landet in aktion und wird an schritt(pos, aktion) übergeben. Der Agent führt diesen einen Zug in der echten Umgebung aus, liest das neue Bild ein und startet die Suche erneut. Die restliche Folge wird verworfen. Das begrenzt den Schaden aus länger werdenden Modellfehlern.

while not fertig:
    z = kodiere(modell, beobachtung(pos))
    aktion, wert = plane(modell, z, 6, 100, zufall)
    pos, belohnung, fertig = schritt(pos, aktion)

Dieses Vorgehen heißt Model Predictive Control. Der Horizont beträgt hier sechs Schritte, die Planung wird aber nach einem Schritt korrigiert.

Wenn ein Fehler dem Planer hilft

Das Weltmodell sagte manchmal kleine Belohnungen an Stellen voraus, an denen die Umgebung keine vergibt. In diesem Lauf lagen diese Fehler häufig auf Wegen zum Ziel. Der Planer bekam dadurch ein Gefälle, obwohl die echte Belohnung nur auf einem einzigen Feld existiert.

Ein gutes Planungsergebnis beweist kein genaues Modell. Bei sechs vorausgedachten Schritten half die erfundene Belohnungslandschaft. Bei drei vorausgedachten Schritten führte sie den Agenten in die falsche Richtung. Modellfehler und Planungsleistung müssen getrennt gemessen werden.

Die komplette Kette

Bild
  → Encoder: 49 Werte auf 8
  → Dynamik: Zustand + Aktion auf nächsten Zustand
  → Rollout: mehrere Dynamikschritte
  → Planer: 100 Rollouts vergleichen
  → erste Aktion in der echten Welt
  → neues Bild und wieder von vorn

Die Umgebung liefert Übergänge. Der Autoencoder verdichtet Bilder, das Dynamiknetz sagt ihre Folgen voraus und der Planer wählt daraus den nächsten Zug.