Einleitung
ChatGPT kann Fehler machen. Daher ist es wichtig, wichtige Informationen zu überprüfen. Wir haben uns daran gewöhnt, dass ChatGPT stoisch alles Mögliche erfindet, von Daten bis hin zu ganzen Referenzen. Aber wie sieht es mit grundlegendem Denken aus? In diesem Artikel werden wir anhand einer einfachen Turm-Umordnungsaufgabe aus den Anfängen der künstlichen Intelligenz (KI) zeigen, wie große Sprachmodelle (LLM) ihre Grenzen erreichen und die Planning Domain Definition Language (PDDL) und symbolische Solver einführen, um diese Grenzen zu überwinden. Da LLMs grundsätzlich wahrscheinlichkeitsbasiert sind, ist es wahrscheinlich, dass solche Tools in zukünftigen Versionen von KI-Agenten integriert werden, die allgemeines Wissen und scharfes Denken kombinieren. Um das Beste aus diesem Artikel herauszuholen, richten Sie Ihre eigene PDDL-Umgebung mit der PDDL-Erweiterung für VS Code und der planutils Planer-Schnittstelle ein und arbeiten Sie mit den Beispielen.
Die Grenzen von LLMs
In einem großen Sprachmodell (LLM) wird jedes Zeichen wörtlich auf der Grundlage aller vorherigen Zeichen seiner Antwort sowie der Eingabeaufforderung des Benutzers bedingt. Mit fast allem, was jemals geschrieben wurde, sind LLMs nicht nur allwissend, sondern auch geistreich geworden. Es dauert jedoch in der Regel nicht lange, um festzustellen, dass LLMs sehr faul sind und sich weigern, wirklich über ein Problem nachzudenken. Dies kann anhand eines einfachen Beispiels aus der „Blockwelt“ veranschaulicht werden, einem Problemfeld, das so alt ist wie das Feld der „künstlichen Intelligenz“. Betrachten Sie eine einfache Blockstapelaufgabe, wie in der folgenden Abbildung gezeigt:
Hier ist die Aufgabe, den Turm auf der linken Seite in den Turm auf der rechten Seite umzuwandeln. Wir gehen davon aus, dass der Roboter die folgenden Funktionen hat:
aufheben <farbe>
: hebt einen Block mit der Farbe<farbe>
überall auf dem Tisch aufabsetzen <farbe>
: setzt einen Block mit der Farbe<farbe>
überall auf dem Tisch abentstapeln <farbe1> <farbe2>
: hebt einen Block mit der Farbe<farbe1>
von einem Turm auf, der<farbe2>
oben hatstapeln <farbe1> <farbe2>
: setzt einen Block mit der Farbe<farbe1>
auf einen Turm, der<farbe2>
oben hat
Wie schlägt sich ChatGPT3.5 mit einer solchen Aufgabe? Hier ist die Eingabeaufforderung:
ChatGPT antwortet sofort mit einer Sequenz von Aktionen mit der erforderlichen Syntax. Unglaublich! Aber Moment, nur die ersten drei Aktionen sind die richtigen! Der Roboter bewegt zunächst den blauen Block von der Spitze des Turms an eine beliebige Stelle auf dem Tisch und tut dasselbe mit dem grünen Block. Wir haben nun alle Blöcke auf dem Tisch, wie in der folgenden Abbildung gezeigt:
ChatGPT schlägt nun vor, den blauen Block aufzuheben und auf den roten zu stapeln. Das ist absolut nicht das, was wir wollten: Der blaue Block sollte unten sein. Außerdem ist der Turm nicht vollständig. Versuchen wir, zu helfen:
ChatGPT nimmt den Hinweis auf und fügt ihn einfach am Ende hinzu. Jetzt ist der grüne Block auf dem blauen, der auf dem roten ist. Obwohl es möglich sein könnte, die Ausgabe durch Bereitstellung zusätzlicher Hinweise in die richtige Form zu massieren, haben wir alle gesehen, wie ChatGPT tatsächlich anfängt, sich im Kreis zu drehen, indem er zwischen zwei gleichermaßen falschen Lösungen hin- und herspringt.
ChatGPT4o macht einen etwas besseren Job und produziert die folgende Ausgabe für dieselbe Eingabeaufforderung:
entstapeln blau grün
absetzen blau
entstapeln grün rot
absetzen grün
aufheben blau
stapeln grün blau
aufheben rot
stapeln rot grün
Dies ist tatsächlich richtig, außer für „aufheben blau“ in der Mitte. Nach einer Beschwerde „Sie können ’stapeln grün blau‘ nicht nach ‚aufheben blau‘ ausführen, da der Roboter immer noch ‚blau‘ hält“, gehorcht ChatGPT4o widerwillig:
entstapeln blau grün
absetzen blau
entstapeln grün rot
absetzen grün
aufheben blau
absetzen blau (falls erforderlich, sonst überspringen)
aufheben grün
stapeln grün blau
aufheben rot
stapeln rot grün
Es stellt sich heraus, dass ChatGPT4o tatsächlich ein großer Schritt nach vorne gegenüber ChatGPT3 ist und das Hinzufügen von mehr Informationen zur anfänglichen Eingabeaufforderung auch hilft. Es ist jedoch sehr schwierig, ChatGPT dazu zu bringen, die optimale Sequenz von Aktionen zu produzieren, die oben gezeigt wird, und es ist unmöglich, dies für kompliziertere Einschränkungen wie „Sie können nur zwei Blöcke auf dem Tisch platzieren“ oder die Verwendung von zwei Armen in einer humanoiden Montageaufgabe zu tun.
Zu Ehren von LLMs könnte man auch argumentieren, dass das Problem tatsächlich unterdefiniert ist. Tatsächlich führt das sorgfältige Nachdenken über die oben genannten Aktionen wahrscheinlich zu immer mehr Missverständnissen. Beispielsweise haben wir ChatGPT nie gesagt, dass das Aufheben eines Blocks den Roboter daran hindert, einen anderen Block aufzuheben, bevor der erste nicht abgesetzt wurde.
Planning Domain Definition Language
Wie können wir ein solches Problem formal beschreiben, um die letzten Unsicherheiten zu beseitigen? Ein Ansatz ist die Planning Domain Definition Language (PDDL), die zu einem Standard in der KI-Planungsgemeinschaft geworden ist. PDDL gibt es seit 1998 und hat seinen Umfang ständig erweitert und zu einer Vielzahl von Solvern geführt, die sich alle auf verschiedene Aspekte des Planungsproblems spezialisiert haben. Werfen Sie einen Blick auf die Aufgaben der Internationalen Planungswettbewerbs 2023, um eine Vorstellung davon zu bekommen, mit welchen Problemen sich die Planungsgemeinschaft derzeit beschäftigt.
Eine PDDL-Definition besteht aus zwei Dateien:
- Ein Domain
- Ein Problem
Beginnen wir mit dem Problem, wobei wir das Turmbeispiel verwenden:
(define (problem blocks)
(:domain blocksworld)
(:objects
red green blue - block
)
(:init
(ontable red) ; Block red
(on green red) ; Block green
(on blue green)(clear blue) ; Block blue
(handempty)
)
(:goal (and
(on green blue)
(on red green)
))
)
PDDL verwendet S-Ausdrücke, die einige Gewöhnung erfordern, insbesondere für diejenigen, die mit LISP nicht vertraut sind. Kurz gesagt, anstatt „A + B“ zu sagen, kommt der Operator „+“ zuerst, was zu (+ A B) führt.
In dem obigen Beispiel können Sie dies am Ende sehen, wo das „Ziel“ definiert ist. Die Aussage (on green blue)
liest sich „grün auf blau“ und (on red green)
liest sich „rot auf grün“. Ähnlich verhält es sich mit dem „und“-Operator, der sicherstellt, dass beide Aussagen wahr sein müssen, d.h. „rot auf grün und grün auf blau“. Die Klammern sind ein weiterer Rückruf aus der Vergangenheit: LISP steht für „List Processor“ mit Listen, die ähnlich wie Tupel in Python definiert werden. Beispielsweise nimmt (:goal <list>)
eine weitere Liste auf, die wiederum zwei Listen enthält. Hier ist (and <list>)
ein integrierter Operator, aber (on <list>)
ist es nicht. Dies ist, wo die Domain-Datei ins Spiel kommt, die wir später einführen werden.
Zusätzlich zu einem „Ziel“ erfordert ein PDDL-Problem auch eine Liste von Anfangsbedingungen. Diese werden in Form von Prädikaten bereitgestellt, Aussagen, die nur „wahr“ oder „falsch“ sein können. PDDL sammelt diese Prädikate in einer Liste, die (:init <list>)
vorangestellt ist, und hier haben wir:
(ontable red)
(on green red)
(on blue green)
(clear blue)
(handempty)
Es ist verlockend, über diese Prädikate als Variablen oder Funktionen nachzudenken. Sie sind weder das noch das. Sie sind einfach Aussagen. Wenn beispielsweise eine Aussage (handempty)
zum Problem hinzugefügt wurde, ist sie wahr. Um sie auf „falsch“ zu setzen, muss sie aus dem Problem entfernt werden. Daher sind es keine Variablen, die einen der beiden Werte enthalten können. Wenn es definiert ist, ist es wahr. Wenn es nicht definiert ist, ist es falsch.
Hinweis: Prädikate sind weder Variablen noch Funktionen, es sind Aussagen, die wahr sind.
Alle anderen Prädikate sehen auf den ersten Blick wie Funktionen aus, sind aber auch nur Aussagen. (ontable red)
wird verwendet, um dem Solver mitzuteilen, dass der rote Block auf dem Tisch liegt. (on green red)
wird verwendet, um dem Solver mitzuteilen, dass der grüne Block auf dem roten Block liegt, (on blue green)
, dass der blaue Block auf dem grünen Block liegt, und (clear blue)
zeigt an, dass der blaue Block aufgehoben werden kann.
PDDL wäre ziemlich nutzlos, wenn diese Prädikate nicht parametrisierbar wären. Die :objects
-Liste definiert jedoch alle Objekte, die die Prädikate bilden können. In diesem Beispiel ist -block
ein Hinweis darauf, dass red
, green
und blue
alle vom Typ block
sind. Dies macht es ein wenig strenger und verhindert logische Fehler, da alle oben genannten Prädikate nur mit Objekten des Typs block
funktionieren. Dies erleichtert auch die Suche nach einer Lösung, da nur Objekte des richtigen Typs für eine Aktion berücksichtigt werden müssen.
Die Prädikate werden in der Domain-Beschreibung definiert. Der Name der Domain wird in der :domain
-Liste in der Problem-Beschreibung angegeben. Der Domain-Name wird auch in der ersten Zeile der Domain-Datei angezeigt:
(define (domain blocksworld)
(:requirements :typing :negative-preconditions)
(:types block)
(:predicates
(on ?a ?b - block)
(clear ?a - block)
(holding ?a - block)
(handempty)
(ontable ?x - block)
)
...
)
Stellen Sie sicher, dass Sie die beiden Dateien als problem.pddl
und domain.pddl
speichern. Dadurch sollte es Ihnen möglich sein, mit der PDDL-Erweiterung für VS Code einen Plan zu generieren, wie in Abbildung 1 gezeigt, oder Sie können den Online-PDDL-Editor verwenden und den LAMA-Solver verwenden.
Die Typen und Prädikate für die Blockwelt-Domäne werden sehr früh in der Domain-Datei definiert:
(:types block)
(:predicates
(on ?a ?b - block)
(clear ?a - block)
(holding ?a - block)
(handempty)
(ontable ?x - block)
)
Die verwendeten Typen (nur block
) werden der :types
-Liste bereitgestellt. Die Prädikate werden in :predicates
eingegeben und verwenden Platzhalter, die durch ein vorangestelltes ?
gekennzeichnet sind. Sie können die Problem-Definition oben überprüfen, um zu bestätigen, dass alle Prädikate dem obigen Muster folgen.
Hinweis: Die Typisierung ist bereits eine nicht standardmäßige Funktion von PDDL. Um dem Parser und dem Solver mitzuteilen, dass Typen verwendet werden, wird :typing
in der :requirements
-Liste am Anfang der Domain bereitgestellt.
Prädikate können nun mit Aktionen manipuliert werden. Eine :action
ist eine Liste, die aus Parametern, Vorbedingungen und Auswirkungen besteht. Eine Aktion wird nur ausgeführt, wenn alle ihre Vorbedingungen wahr sind. Die Auswirkung einer Aktion ist die Erstellung oder Löschung von Prädikaten. Hier verwenden wir den (not <list>)
-Operator, der durch Hinzufügen von :negative-preconditions
zu :requirements
aktiviert wurde.
(:action pickup ; this action is only for picking from table
:parameters (?x - block)
:precondition (and (ontable ?x)
(handempty)
(clear ?x)
)
:effect (and (holding ?x)
(not (handempty))
(not (clear ?x))
(not (ontable ?x))
)
)
Die Aktion pickup
hat einen einzelnen Parameter, ein Block-Objekt. Hier verwenden wir ?x
, wann immer wir dasselbe Objekt meinen. Damit es ausgeführt wird, müssen drei Prädikate wahr sein:
(ontable ?x)
(handempty)
(clear ?x)
Das Objekt muss auf dem Tisch liegen, der Greifer muss leer sein und das Objekt muss aufgehoben werden können. Nochmals, ontable ?x
und clear ?x
sind keine Funktionen. Diese sind Prädikate, die entweder als Teil der :init
-Liste im Problem bereitgestellt werden mussten oder von einer Aktion zur Laufzeit erstellt wurden.
Dies ist, wo die :effect
ins Spiel kommt. Diese Liste enthält Prädikate, die wahr sein werden, sobald die Aktion ausgeführt wurde. Diese sind:
(holding ?x)
(not (handempty))
(not (clear ?x))
(not (ontable ?x))
Das heißt, das erfolgreiche Ausführen der Aktion (pickup ?x)
führt dazu, dass der Roboter ?x
hält und die Prädikate (handempty)
, (clear ?x)
und (ontable ?x)
löscht.
Diese Einrichtung liefert auch einen ersten Hinweis darauf, wie wir einen Plan erstellen können: Wir können entweder durch die Auswirkungen suchen, die zu einem gewünschten Ergebnis führen, und dann die Voraussetzungen auf die gleiche Weise befriedigen, bis wir den Anfangszustand erreichen, oder wir können alle möglichen Aktionen auf die Menge der Anfangsbedingungen und auf die daraus resultierenden Zustände anwenden, bis wir das Ziel finden. Beide sind schwierige Probleme, die das Planen zu einem NP-schweren Problem machen.
Wir können uns nun die verbleibenden Aktionen ansehen. Zunächst entfernt unstack
den Block ?x
von Block ?y
. Dazu ist erforderlich, dass ?x
auf ?y
liegt, die Hand leer ist und ?x
klar ist.
(:action unstack ; only suitable for picking from block
:parameters (?x ?y - block)
:precondition (and (on ?x ?y)
(handempty)
(clear ?x)
)
:effect (and (holding ?x)
(not (handempty))
(not (clear ?x))
(clear ?y)
(not (on ?x ?y))
)
)
Das Ergebnis ist, dass der Roboter ?x
hält und die Prädikate (handempty)
, (clear ?x)
und (on ?x ?y)
gelöscht werden. Außerdem ist (clear ?y)
jetzt wahr, d.h. Block ?y
kann jetzt aufgehoben werden. Die Aktionen putdown
und stack
folgen dem gleichen Muster.
Ein Planungsproblem lösen
Das hier beschriebene Planungsproblem ist so alt wie das Feld der KI, bleibt aber ein aktives Forschungsgebiet. Spätere Versionen von PDDL umfassen die Fähigkeit, über Zeit und Mengen zu raisonnieren, und bestimmte Planungsprobleme können mit einigen Algorithmen besser gelöst werden als mit anderen. Einer der am häufigsten verwendeten Planer ist als „FastDownward“ bekannt (https://www.fast-downward.org/), der die meisten Funktionen von PDDL 2.1 unterstützt. Eine ziemlich umfassende Liste von Planern finden Sie unter https://planning.wiki/ref/planners/atoz, und die meisten von ihnen listen die Anforderungen auf, die sie unterstützen.
Zusätzlich zur Formalisierung der Problem-Beschreibung hat die KI-Gemeinschaft auch die Planer-Schnittstelle vereinheitlicht, und das planutils Projekt stellt einen Docker-Container bereit, der Ihnen die Auswahl aus einer großen Anzahl von Planern ermöglicht und diese über einen lokalen Webserver verfügbar macht. Dieser Ansatz ist in die PDDL-Erweiterung für VS Code integriert. Neben der Syntaxhervorhebung ermöglicht das Tool die Visualisierung von Anfangs- und Zielzuständen sowie des resultierenden Plans. Dies kann durch Ihre eigene JavaScript-Visualisierung ergänzt werden, wie z.B. die Blockaufgabe rechts. Sie finden dieses Beispiel hier.
Die Einrichtung von planutils in einem Docker-Container unter Linux, Mac oder Windows ist mit den Anweisungen in der VS Code-Erweiterung unkompliziert. Stellen Sie jedoch sicher, dass „Host-Netzwerk“ aktiviert ist, um auf den Flask-Server im Docker-Container zugreifen zu können.
Planning und ChatGPT
Im Gegensatz zu ChatGPT liefert ein Planer immer eine korrekte Sequenz von Aktionen. Obwohl es mühsam erscheint, alle Prädikate und Aktionen für ein bestimmtes Problem einzurichten, ist es nicht unvernünftig, dies in einer Produktionsumgebung zu tun. Wenn Sie eine Software-Agenten erstellen, um komplexe Reiseplanungen zu erstellen, eine lange Bestellung zu erfüllen oder einen humanoiden Roboter zu erstellen, der Ihre Lebensmittel einkauft oder eine komplexe Montage durchführt, sollten Sie eine gute Vorstellung davon haben, welche APIs Ihnen zur Verfügung stehen, was sie benötigen, um zu funktionieren (die Voraussetzungen) und was passiert, wenn sie erfolgreich ausgeführt werden. Die Ausführung des Plans erfordert dann (1) das Auffüllen der Liste der Prädikate auf der Grundlage des Zustands der Welt, (2) das tatsächliche Aufrufen aller Aktionen, die der Planer ausspuckt, nacheinander und (3) optional das erneute Planen, wenn sich die Umgebung während der Planausführung ändert. Sobald dieser Rahmen implementiert ist, kann ChatGPT leicht dazu aufgefordert werden, geeignete Ziele zu generieren:
Während dieser Ansatz letztendlich auf die gleichen Grenzen stoßen wird, ist die Erstellung geeigneter Ziele viel handhabbarer als eine lange Liste von Aktionen, insbesondere für ein Ziel wie „Auto zusammenbauen“, das Tausende von Aktionen erfordern könnte. Beachten Sie auch, dass ChatGPT einen Schuss darauf genommen hat, wie die Symbole benannt werden sollen. Er hat sich dafür entschieden, einfach die Farbe zu verwenden, was möglicherweise darauf zurückzuführen ist, dass die Blockwelt-Beispiel sehr gut dokumentiert ist.
Pläne mit Zählern und Zeit
Bisher haben wir nur über boolesche Prädikate nachgedacht. Spätere Versionen von PDDL unterstützen auch Mengen und Zeit. Beispielsweise verwendet das folgende Problem eine Variable counter
, die mit null initialisiert ist und drei erreichen muss.
(define (problem say-hello-3-times)
(:domain counter-test)
(:init
(= (counter) 0)
)
(:goal
(and
(= (counter) 3)
)
)
)
Hier ist eine Domain, die eine Aktion bereitstellt, um dies zu bewirken:
(define (domain counter-test)
(:requirements :strips :fluents)
(:functions
(counter)
)
(:action hello-world
:parameters ()
:precondition ()
:effect (and
(increase (counter) 1)
)
)
)
Die Funktion counter
wird in :functions
definiert, die über die Anforderung :fluents
verfügbar ist. Die Aktion hello-world
(die möglicherweise „Hallo Welt“ ausgibt) bietet dann die Auswirkung (increase (counter) 1)
. PDDL-Fluents – Zustandsvariablen, die sich im Laufe der Zeit ändern können – sind mehr als Zähler. Sie ermöglichen auch die Berücksichtigung des Kraftstoffstands, des Energieverbrauchs oder der Anzahl von Passagieren oder Waren in einem E-Commerce-Warenkorb.
Wenn Sie bisher Schritt für Schritt vorgegangen sind, werden Sie feststellen, dass der FastDownward-Solver die Anforderung :fluents
nicht unterstützen kann. Ein Planer, der dies tun kann und über planutils verfügbar ist, ist der Expressive Numeric Heuristic Search Planner (ENHSP). Sie können es mit planutils install enhsp-2020
in dem planutils Docker-Container installieren oder den EHSP-Solver im Online-Editor verwenden.
Sie können auch jeder Aktion eine Zeit zuordnen, wodurch Sie die Planung in die Zeitdomäne erweitern können. Dies wird in diesem Tutorial des Herstellers der PDDL-Erweiterung für VS Code gut erklärt:
Bedingte Auswirkungen
Bedingte Auswirkungen ermöglichen es Ihnen, unterschiedliche Ergebnisse auf der Grundlage bestimmter Bedingungen anzugeben, indem Sie das Schlüsselwort when
verwenden (erfordert :conditional-effects
). In einem Beispiel für das Laden von LKWs wird das Prädikat requires-caution
nur dann auf „wahr“ gesetzt, wenn ein Paket zerbrechlich ist.
(define (domain truck)
(:requirements :strips :conditional-effects)
(:predicates
(in ?package ?truck)
(empty ?truck)
(requires-caution ?truck)
(fragile ?object)
)
(:action load-truck
:parameters (?package ?truck)
:precondition (and
(empty ?truck))
:effect (and
(in ?package ?truck)
(when (fragile ?package)
(requires-caution ?truck))))
)
)
Ein Beispiel-Problem, das zu dieser Domain gehört, ist:
(define (problem load-truck)
(:domain truck)
(:objects truck77 shipment123)
(:init
(empty truck77)
(fragile shipment123))
(:goal (and (in shipment123 truck77)))
)
Sie können dieses Problem mit FastDownward lösen. Es ist verlockend, when
als Möglichkeit zu betrachten, Unsicherheiten zu modellieren. Tatsächlich ist es möglich, zwei gegenseitig ausschließende when
-Anweisungen zu verwenden, und die Modellierung von Roboterfehlern auf diese Weise ist ein häufiges Beispiel im Internet. Dies macht jedoch keinen Sinn, da der Solver die Prädikate nicht in Echtzeit auswertet. Es gibt auch eine probabilistische Version von PDDL, bekannt als PPDDL, die es Ihnen ermöglicht, den verschiedenen Zweigen einer Aktion Wahrscheinlichkeiten zuzuordnen. PPDDL hat jedoch das gleiche Problem und ermöglicht es Ihnen einfach, über Probleme auf probabilistische Weise zu raisonnieren, ähnlich wie ein Markov-Entscheidungsproblem.
Zusammenfassung
Die Fähigkeit symbolischer Planer, eine korrekte Sequenz von Aktionen zu finden, indem systematisch alle Möglichkeiten ausgewertet werden, unter Berücksichtigung von Einschränkungen für Mengen und Zeit, steht im Gegensatz zu den Denkfähigkeiten eines LLM wie ChatGPT. Während beide Systeme möglicherweise schließlich dasselbe Problem lösen können, modelliert diese Dichotomie auch die menschliche Erfahrung: Sobald unsere geistigen Fähigkeiten ihre Grenzen erreichen, greifen wir zu einem Abakus, einem Stift und Papier oder einem anderen Werkzeug und halten uns an einen Algorithmus, um die Dinge herauszufinden.
Es ist unwahrscheinlich, dass ChatGPT jemals ein allgemeiner Problemlöser wird, indem es trainiert wird. Nicht nur sind die Beispiele, die wir bereitstellen müssen, um jeden möglichen Bereich zu erfassen, einfach zu viel, sondern LLMs bieten keine Möglichkeit, die Richtigkeit durchzusetzen. Dies ist besonders wichtig für ein Produktionssystem wie eine Geschäftsanwendung oder einen Roboter, der in der realen Welt arbeitet. Leider unterstützen nicht alle Solver alle Funktionen von PDDL, und die Suche nach der richtigen Kombination aus PDDL-Definition und Solver kann schwierig sein.
Zusätzlich zur Ergänzung von ChatGPT mit PDDL gibt es auch eine riesige Chance für eine viel engere Integration von Open-World-Denken und symbolischem Planen. LLMs könnten beispielsweise verwendet werden, um die Suche in eine Richtung gegenüber der anderen zu priorisieren, indem allgemeines Wissen verwendet wird. Ähnlich könnten PDDL-Symbole und -Prädikate in natürlicher Sprache verwendet werden, um Bilder in der realen Welt mit Tools wie OWL-ViT zu erkennen.