Prototyp für ein Robot-Control-System

Die wichtigste Frage bei der Entwicklung eines Robot-Control-System ist nicht etwa wie es gemacht wird, sondern welche Methode nicht funktioniert. Ein wenig konnte ich das Thema bereits eingrenzen. Als dysfunktional haben sich herausgestellt:
– Neuronale Netze (nicht im Stande komplette Turing-Maschinen zu lernen)
– C++ als Implementierungssprache (zu viele Pointer, maximal 5x schneller als Python, zu viel Boilerplate-Code)
– WillowGarage ROS (allein die Installation ist eine Qual)

Es bleiben einige Techniken übrig, die sich als praktikabel hergestellt haben:
– Python, Eclipse, pydev, pygame, Box2D (ideale Entwicklungsumgebung, schnell programmierter Code)
– Finite-State-Maschines, Behavior Tree (gut geeignet um komplexe Abläufe zu spezifizieren)
– Python Threads (um einen Controller als Background Prozess zu starten)

Offen geblieben ist bisher die Frage, wie man eine Finite-State-Maschine so hochskaliert, dass sich damit komplexe Steuerungsprobleme lösen lassen. Bisher wurde lediglich ein Pick&Place Problem implementiert und bereits dort war das Schreiben der Motion Primitive extrem aufwendig. Eine Methode das zu beschleunigen gibt es nicht. Aber wie kann man herausfinden wo möglicherweise der Flaschenhals liegt? Gibt es womöglich eine Technologie die noch fehlt auf der Liste?

Um das herauszufinden bietet es sich an, mit Prototypen zu arbeiten, also mit nicht funktionalen Mockups, die schnell erstellt sind und die das Prinzip als solche erläutern. Um einen Mockup für Behavior Trees zu erstellen, lässt man die Details einfach weg und betrachtet die Sache auf rein linguistischer Seite. Es geht darum, nur die Abfolge der Motion Primitive zu ermitteln und ein kleines Python Programm zu haben, mit dem man herumspielen kann. Im folgenden dazu der Sourcecode:

'''
Created on 20.02.2017
Prototyp BehaviorTree
@author: hp
'''
import time, sys, random, math, numpy, threading, logging
logging.basicConfig(
  stream=sys.stdout,
  level=logging.DEBUG,
  format='%(message)s'
)
log = logging.getLogger(__name__)

def taskpause():
  time.sleep(1)
def main():
  log.info('main')
  walk()
  pickplace()
def pickplace():
  log.info('pickplace')
  grasp()
  move()
  ungrasp()
  grasp()
  move()
  place()
def walk():
  log.info('walk')
  centertoright()
  footup()
  footforward()
  centertoleft()
  footup()
  footforward()
  centertoright()
def footup():
  log.info('--footup')
  taskpause()
def footforward():
  log.info('--footforward')
  taskpause()
def centertoleft():
  log.info('--centertoleft')
  taskpause()
def centertoright():
  log.info('--centertoright')
  taskpause()
def place():
  log.info('-place')
  taskpause()
def ungrasp():
  log.info('-ungrasp')
  down()
  opengripper()
  up()
def grasp():
  log.info('-grasp')
  grasptype()
  move()
  opengripper()
  down()
  closegripper()
def grasptype():
  log.info('--grasptype')
  taskpause()
def move():
  log.info('--move')
  taskpause()
def opengripper():
  log.info('--opengripper')
  taskpause()
def closegripper():
  log.info('--closegripper')
  taskpause()
def down():
  log.info('--down')
  taskpause()
def up():
  log.info('--up')
  taskpause()
  
if __name__ == "__main__":
  main()

Das Programm kann man ganz normal ausführen und auf dem Bildschirm werden dann die Logausgaben angezeigt. Durch den Pause-Befehl ist das ganze verzögert, so dass man schön in Ruhe mitlesen kann. Es handelt sich dabei um einen BehaviorTree der aus hierarchichen Subfunktionen besteht ohne dass er etwas tut. Vielmehr ist es ein reines Mockup-Projekt. Es zeigt, wie man aus einfachen Befehlen komplexe Befehle zusammenbaut. So besteht der Task „grasp“ aus einer Abfolge von weiteren Befehlen, die nacheinander aufgerufen werden. Obwohl programmiertechnisch das ganze harmlos aussieht, ist diese Form der Programmierung relativ selten. Denn normalerweise werden die KI-Bots anders programmiert und zwar als Gametree Search, wo also die CPU belastet wird und irgendwas durchprobiert wird. Im obigen Fall hingegen wurde alles manuell programmiert, es ist eine hierarchiche Finite-State-Maschine die linear durchläuft.

Leider ist es nicht gelungen, den Mockup an Grenzen zu führen, also aufzuzeigen in welchen Fällen er versagt. Man kann zwar ein wenig mit dem Pause-Paremter herumspielen um das Programm schneller oder langsamer zu machen, an dem Behavior tree ändert sich jedoch nichts. Es handelt sich um eine Art von Authoring-Werkzeug um beliebig komplexe Sachverhalte zu gliedern. Man kann noch weitere Funktionen erstellen, die entweder vorhandene Motion Primitive neu anordnen, oder sogar neue Motion Primitive benötigen. Um soetwas wie einen Fail zu versuchen müsste man den Prototypen um eine Game-Engine erweitern. Also gegen eine Baseline Programmieren wo ein Task entweder erfolgreich ausgeführt wird oder eben nicht.

VERBESSERUNGSMÖGLICHKEITEN
Die nächste Stufe bei der Entwicklung eines Prototypen besteht darin, nicht nur ein text-log auszugeben, sondern die Aktionen grafisch zu visualisieren. Dazu wird ein vereinfachter Roboter verwendet, der in der Luft hängt und selbst nicht laufen kann. Sondern es werden einfach die Kommandos an die Gliedmaßen gesendet, die bewegen sich dann auch, aber irgendein Ziel ist damit nicht verbunden. Es geht also darum, die textuelle Ausgabe durch eine Visualisierung zu ersetzen. Es würde dann auf dem Bildschirm nicht nur dastehen „footforward“, sondern auf dem Bildschirm bewegt sich der Fuß dann tatsächlich. Allerdings noch ohne Kollisionsabfrage oder ähnliches.

1

Wenn man diese Idee in Source realisiert ergibt sich die obige Abbildung. Programmtechnisch war es etwas anspruchsvoller als die reine Textausgabe, aber es ist noch immer ein Mockup. Das heißt, der Roboter auf dem Bildschirm bewegt sich nicht wirklich, sondern es sind einfach nur 4 Motoren die in der Luft hängen und über ein Script gesteuert werden. Neu ist jedoch, dass man jetzt nicht nur Textbotschaften auf der Komamndozeile durchlaufen sieht, sondern die beiden Füße visualieren kann. Man drückt auf Start und sieht dann eine synchron ablaufende Bewegung. Das interessante daran ist, dass sie anfangs noch Fehler enthält, die man dadurch fixt, dass man das Script verbessert. Man kann sich aus den Motion Primitiven dann höherwertige Tasks zusammenbauen. Eine Baseline gegen die man programmiert gibt es ähnlich wie in dem rein textuellen Mockup nicht. Das heißt, der Roboter muss nicht wirklich vorwärtsgehen oder eine Punktzahl erreichen. Vielmehr ist das ganze eine simple Demonstration, also wie man über ein Script eine längere Animation erstellt.

Das interessante daran ist, dass sich nur mittels Finite-State-Maschine sehr komplexe Abläufe scripten lassen. Auf der Lowlevel-Ebene hat man nur den Befehl rotate zur Verfügung um eine Gliedmaße auf eine Sollposition zu bewegen. Aber daraus kann man sich dann Befehle erstellen wie „Footleftup“ oder „Rest“, wodurch dann mehrere Subaktionen ausgeführt werden.

Das Konzept unterscheidet sich grundsätzlich von Reinforcement Learning oder DeepLearning wie es häufiger in der Literatur diskutiert wird. Im Kern gibt es eine manuell programmierte Finite State Maschine die in einem Authoring-Prozess erweitert wird. Es ist also zwingend ein Man-in-the-loop nötig um die Bewegungsabfolge zu scripten. Die Software findet also nicht durch Suche im Problemraum von allein die Lösung. Der Clou ist, dass das Scripten jedoch simpel ist, weil man mit selbst definierten High-Level Kommandos arbeitet.

Da das Gesamtprogramm wegen der Pygame und Box2D Bibliothek umfangreich ausfällt im folgenden nur die eigentliche Behavior Engine um die Füße zu animieren:

  def task2(self):
    self.taskrest()
    self.taskrightup1()
    self.taskleftup1()
    self.taskrightup2()
    self.taskleftup2()
    self.taskrightdown1()
    self.taskleftdown1()
    self.taskrightdown2()
    self.taskleftdown2()
  def taskrest(self):
    log.info('rest')
    self.taskrotate(0,0)
    self.taskrotate(1,0)
    self.taskrotate(2,0)
    self.taskrotate(3,0)
    self.taskpause()
  def taskwalkstart(self):
    log.info('walkstart')
    self.taskrotate(1,-90)
    self.taskrotate(3,-90)
    self.taskpause()
  def taskrightdown1(self):
    log.info('rightdown1')
    self.taskrotate(0,0)
    self.taskrotate(1,-90)
    self.taskpause()
  def taskrightdown2(self):
    log.info('rightdown2')
    self.taskrotate(0,90)
    self.taskrotate(1,0)
    self.taskpause()
  def taskleftdown1(self):
    log.info('leftdown1')
    self.taskrotate(2,0)
    self.taskrotate(3,-90)
    self.taskpause()
  def taskleftdown2(self):
    log.info('leftdown2')
    self.taskrotate(2,90)
    self.taskrotate(3,0)
    self.taskpause()

  def taskrightup1(self):
    log.info('rightup1')
    self.taskrotate(1,-90)
    self.taskpause()
  def taskrightup2(self):
    log.info('rightup2')
    self.taskrotate(0,-90)
    self.taskrotate(1,-180)
    self.taskpause()
  def taskleftup1(self):
    log.info('leftup1')
    self.taskrotate(3,-90)
    self.taskpause()
  def taskleftup2(self):
    log.info('leftup2')
    self.taskrotate(2,-90)
    self.taskrotate(3,-180)
    self.taskpause()

Wie man sieht sind auf der untersten Werte absolute Winkelangaben verwendet worden, die dann zu komplexeren Befehlen aggregiert werden. Wenn man das etwas schneller abspielt ergibt sich eine Fließende Bewegung die man hintereinanderweg ausführen kann. Es ist ähnlich als wenn man eine Kurvenscheibe verwendet, nur dass man als Motion Primitive eine Finite-State-Machine nutzt. Im Hinblick auf die vorhandene Robotik-Literatur kann man das als vereinfachte Kopie von Marc Raiberts Arbeit betrachten, die in den 1980’er veröffentlicht wurde. Nach diesem Prinzip lassen sich biped Walking Roboter konstruieren.

Man kann jetzt im Detail debattieren ob man die Winkelangaben über einen Solver automatisch bestimmt und welche Formeln man noch einbaut, um die Stabilität des Walking-Zyklus zu gewährleisten. Die Idee als solche jedoch mittels Finite-State-Machine einen Controller zu realisieren ist jedoch konstant.

Natürlich ist der obige Sourcecode nicht der erste seiner Art, das Video zum Rhex Roboter wurde schon viel früher veröffentlicht und dort ist nicht nur ein Mockup sondern ein kompletter Roboter zu sehen. Aber, Rhex wurde nicht öffentlich dokumentiert, es gibt keinen Sourcecode oder ähnliches. Das obige Programm hingegen ist auf Verständlichkeit hin ausgelegt, das heißt es wird im Detail erläutert wie der Zaubertrick funktioniert und was relevant ist und was nicht. Das Problem mit Rhex ist weniger, dass der Roboter keine Leistung erbringen würde, sondern das Problem mit RHEX ist, dass es er nicht verrät wie diese Leistung reproduziert werden kann. Aus didaktischer Sicht ist also die Arbeit im RHex Projekt und die Paper von Marc Raibert kein produktiver Beitrag gewesen für die Robotik.

Na ja, zugegeben wenn man bei Google Scholar dezidiert nach „Rhex Finite State Maschine“ sucht findet man einige Paper wo das Prinzip im Detail erläutert wird. Aber es ist eben versteckt, man muss schon sehr genau danach suchen. Wenn man nur oberflächlich in Erfahrung bringen möchte wie RHEX programmiert wurde, wird man eher als Erklärung finden, dass das dort Reinforcement Learning eingesetzt wurde. Das mag zwar der Fall sein, aber das ist nur ein Nebenproblem beim Entwickeln eines Controllers.

WALKING
Spannend wird es, wenn man den Mockup erweitert. Bisher haben sich die Beine nur in der Luft bewegt, der Roboter war quasi aufgebockt. Wenn man jedoch die Box2D Parameter so verändert, dass die Beine den Boden berühren und man dann die Finite-State-Maschine startet ergibt sich etwas erstaunliches. Und zwar kann man den Controller in echt sehen. Das heißt, es werden Motion Primitive ausgeführt und diese führen in der Box2D Engine zu einem Resultat. Natürlich nicht zu dem gewünschten, dass der Roboter graziel sich vorwärtsbewegt. Eher erinnern die Bewegungen an Zufallsbewegungen. Aber, sie kommen nicht per Zufallsgenerator zustande sondern sind das Resulat der BehaviorEngine die streng nach Taskplan durchläuft.

Und jetzt passiert etwas erstaunliches. Es ist mit ein wenig herumprobieren leicht möglich, die Finite-State-Maschine so umzuprogrammieren dass eine Vorwärtsbewegung entsteht. Dazu versucht man zunächst rein manuell die Bewegung zu erzeugen. Das heißt, man bringt ein Bein in eine bestimmte Position und danach in eine andere. Und wenn dadurch sich der Roboter bewegt, notiert man diese Aktionen einfach in die Finite State Machine. Und damit kann man jetzt das Makro beliebig oft wiederholen. Man hat einen ersten Mini-Controller der zwar nicht besonders intelligent ist, aber zumindest auf Knopfdruck den Roboter vorwärtsschiebt.

Der Form halber hier der Minimal-Controller:

  def task2(self):
    self.taskrotate(3,-90) # init
    self.taskpause()
    for i in range(10):
      self.taskrotate(2,-22.5)
      self.taskpause()
      self.taskrotate(2,0)
      self.taskpause()

Er ist stark auf die jeweilige Umgebung hin zugeschnitten. Am Anfang wird das hintere Bein in die Init Position gebracht und dann wird mehrmals hinterinander eine Abfolge ausgeführt. Auf dem Bildschirm ergibt sich eine Oszilator-ähnliche Bewegung. Das heißt, darüber wird der gesammte Roboter jeweils ein kleines Stück vorwärtsbewegt.

Natürlich ist das noch sehr provisorisch und vermutlich kann man den controller so erweitern, dass die Beweguhg schneller ist, also in der selben Zeit mehr Wegstrecke zurückgelegt wird. Doch das ganze ist nur ein Mockup, wichtig ist, dass auf Knopfdruck sich der Roboter überhaupt vorwärtsbewegt. Wenn man jetzt noch einen weiteren Motion Primitive für die Rückwärtsbewegung erstellt kann man die Makros dann auf unterschiedle Tasten legen und darüber den Roboter steuern. Ein sehr mächtiges Prinzip wie ich finde.

VERALLGEMEINERUNG
Um auf das Eingangsstatement zurückzukommen. Der Blogpost hat zunächst in Python-Behavior Tree erstellt, bei man man mitlesen konnte welche Motion Primitive ausgeführt wurden. Das textuelle Konzept wurde erweitert um eine visuelle Komponente und diese zu einem richtigen Roboter. Dafür wurde dann ein mini-Vorwärts-Controller programmiert der den Roboter bewegt. Spannend ist, dass der Roboter einerseits eine Bewegung ausführt man aber gleichzeitig in der Statusleiste mitlesen kann, welche Motion Primitive aktuell aufgerufen werden. Das heißt, man sieht durchlaufen, dass Joint 2 auf Positon -22,5 und dann auf Position 0 gebraucht wird, genauso wie es im Taskplan drinsteht. Man hat zwar noch nicht automatisch den perfekten Controller, aber man hat eine Entwicklungsumgebung worüber man ihn programmieren kann.

Hier mal ein Beispiel für einen etwas komplexeren Controller bei dem Subfunktionen verwendet wurden. Damit bewegt sich der Roboter 5 Schritte nach rechts und dann 5 Schritte nach links:

  def task2(self):
    log.info('init')
    self.taskrotate(3,-90) # init
    self.taskpause()
    for i in range(5):
      self.taskright()
    for i in range(5):
      self.taskleft()
  def taskright(self):
    log.info('-- right')
    self.taskrotate(2,-22.5)
    self.taskpause()
    self.taskrotate(2,0)
    self.taskpause()
  def taskleft(self):
    log.info('-- left')
    self.taskrotate(0,22.5)
    self.taskpause()
    self.taskrotate(0,0)
    self.taskpause()

Über den log.info Befehl wird parallel noch ausgegeben, in welchem Mode der Roboter gerade ist. Man erhält so eine sehr übersichtliche Statusanzeige und kann den Controller bugfixen wie ein normales Computerprogramm. Der Witz ist, dass man diesen höherwertigen Controller dann erneut wieder in einen noch höherwertigeren Controller packen kann. Also eine Funktion programmieren, die den Roboter zu einer bestimmten Zielfunktion bringt und dafür dann die Submakros einsetzt. Im Grunde ist das die Realisierung der Subsumption Architektur wie sie Brooks beschrieben hat. Ja, sie funktioniert, ja man kann damit Roboter-Controller bauen.

WIE WEITER?
Das vorgestellte Prinzip lässt sich in sehr viele Richtungen erweitern. Beispielsweise könnte man ein Bein #3 hinzufügen. Dadurch könnte man dann immer 1 Bein in der Luft lassen wodurch sich neue Makros ergeben, die neue Fortbewegungen ermöglichen. Man würde sich zuerst ein Motion Primitive bauen um ein bestimmtes Bein anzuheben und mit diesem Motion Primitive könnte man dann einen Walkzyklus bauen. Ich glaube, der entscheidene Clou in dem Verfahren ist, dass man viele Dinge verzichtet die normalerweise in der Literatur einen hohen Stellenwert haben, insbesondere die Illusion man könnte über Reinforcement Learning und genetische Algorithmen irgendwie automatisch den Motion Controller synthetisieren. Viel effektiver ist es, in einem Trial & Error Verfahren sich selber die Motion Primitive zu basteln und diese zu High-Level-Controllern zu aggregieren. Rein theoretisch wäre es vielleicht möglich, mittels genetic Programming das auch automatisch zu machen — nur wozu? Was man möchte ist im Normalfall einen Controller der noch Bugs enthält und die man fixen kann. Beispielsweise kann man sich den oben abgedruckten Controller ansehen und Verbesserungsvorschläge unterbreiten und die dann umsetzen. Das geht bei Genetic Progrmaming nicht.

Advertisements

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s