Deep Recurrent Q Network - DQN mit Blick in die Vergangenheit
Deep Q-Networks benötigen manchmal Informationen aus verschiedenen Zeitschritten um schnell zu konvergieren. Das Deep Recurrent Q-Network stellt eine Möglichkeit hierfür dar.
Henrik Bartsch
Einordnung
Vor kurzem wurde in einem Post die Herangehensweise und Implementierung von Deep Q-Networks erklärt. DQN ist allerdings lediglich einer der grundlegenden Algorithmen im Reinforcement Learning,
Forschung auf dem Gebiet zeigt eine Reihe möglicher Verbesserungen. Eine davon ist das Deep Recurrent Q-Network, welches Recurrent Layers verwendet, mehr Informationen gleichzeitig zu
verarbeiten und die Interaktionen verschiedener Informationen besser verarbeiten zu können.
Grundlagen
Deep Q-Networks haben eine Reihe von praktischen Beschränkungen. Im Folgenden soll die wichtigste Beschränkung vorgestellt werden, welche durch Deep Recurrent Q-Networks gelöst werden konnte:
Man nehme an, ein Algorithmus soll das Spiel Pong lernen. Bei Pong handelt es sich um ein kompetitives Zwei-Spieler Arcade-Spiel, bei welchem zwei
Scheiben verwendet werden, um mit einem Ball zu interagieren und diesen durch das Spielfeld zu bewegen. Ziel jedes Spielers ist es, den Ball so zu bewegen, dass der andere Spieler den Ball
nicht abfangen kann und den Spielrand beim Gegner verlässt; in diesem Fall bekommt der Spieler einen Punkt. Eine mögliche Modellierung hierbei ist, dem Agenten immer die aktuelle Observation
(also den Zustand des Bildschirms als RGB-Array) zu übergeben. Weiterhin nehmen wir an, wir befinden uns in einem fixen Zeitpunkt und sehen den Ball wie in dem Bild unten.
Nun stellt sich eine Frage: Wie wird sich der Ball abhängig vom aktuellen Zustand weiterbewegen? Die Antwort ist schnell klar - es gibt keine deterministische Antwort auf diese Frage. Um diese
Frage zu beantworten, benötigt ein Beobachter mindestens zwei Zeitschritte (und Informationen über die Weite des Zeitschrittes) um eine Flugtrajektorie und Geschwindigkeit zu berechnen. Also
war die Idee, ein Deep Q-Network als functional-Model zu definieren. Ein Functional Model ist hierbei ein Modell, welches mehrere In-
und Outputs akzeptiert, also nicht zwangsläufig einen klassischen Graphen abbildet, wie dies beispielsweise bei dem sequential-Model
der Fall ist.
Anwender welche mit künstlichen neuronalen Netzen bereits gearbeitet haben, sind eventuell mit dem Konzepte der Recurrent Models vertraut. Diese bieten sich hier an, da diese effizienter sind
eine solche Aufgabe zu lösen als es klassische Feedforward Models sind.
Recurrent Modells erhalten beim Input (sofern definiert) Informationen aus mehreren Zeitschritten vor dem aktuellen Zeitschritt. Intern in den Recurrent Layers wird hierbei die Interaktion
zwischen den einzelnen Zeitschritten mit modelliert, um besserer Ergebnisse bei Problemen wie Sequence Forecasting oder Time Forecasting zu erreichen.
Als häufig genutztes Beispiel für Recurrent Layer kann man das Long Short-Term Memory-Layer sehen, welches in Keras implementiert
wurde und bereits (1997 von Sepp Hochreiter vorgestellt wurde)[https://www.researchgate.net/publication/13853244_Long_Short-term_Memory]. Für neue Veröffentlichungen, welche sich mit LSTM
auseinandersetzen, siehe beispielsweise bei 1, 2.
Der Ansatz oben wurde in 3 sogar noch ein wenig komplexer implementiert. Hier wurde Pong implementiert, allerdings mit der Besonderheit, dass ab und an Frames leer (also komplett ohne Informationen)
an den Agenten übergeben wurden. Ein Algorithmus wie das Deep Q-Network, welches auf simple State-to-State-Transitions ausgelegt ist, kann hierbei keine sinnvolle Entscheidung treffen. Ein
Deep Recurrent Q-Network, welches beispielsweise fünf Zeitschritte als Input akzeptiert, kann hierbei weiterhin sinnvoll arbeiten, da es alle notwendigen Informationen besitzt und potenziell
die Position des Balls interpolieren kann. Die Autoren zeigen auch, dass sich das Recurrent Modell gegenüber dem Feed-Forward-Modell durchsetzt.
Implementierung
In dieser Version wird eine Implementierung eines Deep Recurrent Neural Networks, welches LSTM-Layer beinhaltet.
Alternative Layer für diese Art von Aufgabe sind beispielsweise die Gated Recurrent Unit (GRU) oder das Simple Recurrent
Neural Network-Layer (SimpleRNN).
Die Imports sind identisch zur Implementierung eines DQN’s:
Eine Implementierung eines ausgelagerten Experience Replays ist straight-forward:
Hinweis: Die Experience in diesem Memory wird bei der Ausgabe in Tensorflow-Tensoren umgewandelt, um das Training später als @tf.function ausführen zu können. Bei dieser Art
von Funktionalität handelt es sich um eine Umwandlung der Operation in einen Graphen, damit beinhaltete Operationen schneller
ausgeführt werden können.
Die genauere Funktionsweise von @tf.functions kann hier nachgelesen werden.
Der eigentliche Agent kann auf folgende Art implementiert werden:
Hier sind einige Änderungen im Gegensatz zum DQNAgent, welche bisher noch nicht angesprochen wurden:
Die Form des Inputs hat sich verändert. Hierbei werden nicht (wie leicht anzunehmen) die Inputs in den Dimensionen [self.num_rounds, self.observation_size] übergeben, sondern in der Form
[1, self.num_rounds, self.observation_size]. Dies hat mit den Eingängen an den Layern zu tun.
Es ist notwendig, eine Art von internem Speicher verwendet werden, um Informationen aus vorherigen Frames zwischenzuspeichern. In dem internen Speicher wird nach jedem Frame gespeichert;
hierbei wird der älteste Frame entfernt.
Der interne Speicher wird am Anfang zu 0 initialisiert; die Idee hierhinter ist, dem Algorithmus im ersten Schritt keine Informationen mitzugeben. Möglicherweise gibt es hierzu bessere
Alternativen, dies erscheint aber bisher die vielversprechendste Variante.
Bei der training_loop und evaluation_loop ist hierbei zu beachten, dass nicht einfach nur die aktuellen Informationen in den Experience Memory gespeichert werden, sondern der
komplette interne Speicher des Agenten. Dies ist notwendig, um einen sinnvollen Input für das Deep Recurrent Q-Network während der Trainingsphase vorliegen zu haben.
In diesem Teil ergeben sich keine funktionellen Änderungen mehr. Als kleine formelle Änderung muss hier noch die Anzahl der Zeitschritte übergeben werden.
Die Performance kann entsprechend an den Diagrammen unten abgelesen werden. Das erste Diagramm entspricht der Zeitschrittweite 2, das zweite Diagramm 5
und das letzte Diagramm einer Zeitschrittweite von 10 Zeitschritten.
Abschließend hier noch ein Vergleich der verschiedenen Zeitschrittweiten:
Hinweis: Trainingsperformance kann sich von Gerät zu Gerät und entsprechenden Seeds teilweise stark unterscheiden. Allgemeine Reproduzierbarkeit von solchen Ergebnissen ist im
Allgemeinen nicht garantierbar.
Weitere Informationen
Recurrent Layers können mit der Option stateful=True initialisiert werden. Hierbei erhält der entsprechende Layer einen eigenen internen Speicher, welcher nicht implementiert werden
muss. Weiterhin ist das Netzwerk damit in der Lage, grundsätzlich Inputs beliebiger Länge zu verarbeiten; hierzu muss lediglich immer der aktuellste Zeitschritt in das Modell als Input
gegeben werden.
Hieraus resultieren allerdings auch eine Reihe von Problemen und Beschränkungen, welche beachtet werden müssen. Dadurch, dass das Netzwerk nicht weiß, wann eine Episode endet, müssen
nach jeder Episode alle internen Speicher manuell zurückgesetzt werden. Weiterhin müssen in einem Experience Memory zu jedem Schritt auch die dazugehörigen internen Werte mitgespeichert
werden; alternativ würde ein Frame “aus dem Kontext” gerissen werden. Allerdings sollen sich auch die internen Werte über das Training verändern - man füttert das Netzwerk also in Teilen
mit veralteten Informationen.
Beide Arten der Umsetzung haben ihre Art von Vor- und Nachteilen, die Version, welche in diesem Post beschrieben wird, funktioniert allerdings verhältnismäßig gut.
Änderungen
[23.01.2023] Einführung interaktiver Plots und Beschreibung der Nicht-Reproduzierbarkeit von Ergebnissen