Alex Kirsch
Independent Scientist
DE | EN

Blog

Die Sprachen der KI

Lisp und Prolog sind klassische Vertreter von KI-Programmiersprachen. Aber ist künstliche Intelligenz überhaupt an die Sprache gebunden? Ich erkläre in dieser Blog-Serie warum manche Sprachen bevorzugt für KI genutzt wurden und werden.
 

Teil 1: Die feinen Unterschiede

05.02.2018
Lisp und Prolog sind klassische Vertreter von KI-Programmiersprachen. Aber ist künstliche Intelligenz überhaupt an die Sprache gebunden? Ich erkläre in dieser Blog-Serie warum manche Sprachen bevorzugt für KI genutzt wurden und werden.

Was macht eine Programmiersprache besonders geeignet für KI Projekte? Aus Sicht der theoretischen Informatik sind alle gängigen Programmiersprachen Turing-vollständig. Das heißt, sie haben die gleiche Ausdruckskraft, können also den vollen Funktionsumfang eines Computers ausschöpfen.

Unterschiede gibt es im Abstraktionsniveau: Maschinensprache bzw. Assembler verfügen über wenige Kommandos, die quasi vom Prozessorhersteller mitgeliefert wurden. Häufige Kombinationen von Assembler-Kommandos werden in abstrakten Sprachen zu kürzeren und besser lesbaren Kommandos zusammen gefasst. Will man beispielsweise zwei Zahlen multiplizieren, sieht das in einer Assemblersprache in etwa so aus (die Syntax folgt dem Little Man Computer): lda 16 sto 52 in sto 50 in sto 51 lda 51 brz 14 sub 17 sto 51 lda 52 add 50 sto 52 br 6 lda 52 out cob dat 1

Das sind 18 Zeilen für das Einlesen von zwei Zahlen, ihre Multiplikation und Ausgabe. In einer prozeduralen Sprache erreicht man das gleiche mit 4 Zeilen: x := read() y := read() r = x * y output(r)

Abstrakte Sprachen unterscheiden sich wiederum durch ihren Stil. So wie bei der Malerei fließende Farbübergänge mit Ölfarben einfacher zu bewerkstelligen sind als mit Acrylfarben, sind bestimmte Operationen in einigen Sprachen einfacher, in anderen komplizierter. Beispielsweise sind Listen das zentrale Konzept in Lisp (die Abkürzung für LISt Processor). In Lisp ist deshalb das Erzeugen und Bearbeiten von Listen eine sehr einfache Angelegenheit: (map + '(1 2 3) '(5 7 9)) addiert jeweils die Elemente der gegebenen Listen (1 2 3) und (5 7 9), und liefert die neue Liste (6 9 12).

Im Gegensatz dazu stammt Java aus der objektorientiert-prozeduralen Welt, wo die typische Datenstruktur der Array ist, der eine direkte Abbildung des Speichers darstellt. Für das Additionsbeispiel ist es egal, ob man Listen oder Arrays verwendet, doch auch mit Arrays ist diese Operation in Java komplizierter (weitere Varianten zum Addieren von zwei Arrays werden hier diskutiert): int[] a1 = {1, 2, 3}; int[] a2 = {5, 7, 9}; int[] result = new int[a1.length]; for (int ii = 0; i < result.length; ++ii) { result[ii] = a1[ii] + a2[ii]; } return(result);

Bei der Wahl der Programmiersprache sollte man also einerseits auf den Abstraktionsgrad allgemein achten, damit man in wenigen Code-Zeilen viel ausdrücken kann, und andererseits auf die Passung der Programmiersprache zur Aufgabe. In den folgenden Teilen zeige ich, welche Anforderungen in der KI dazu geführt haben, dass Lisp und Prolog lange Zeit bevorzugt wurden. Im letzten Teil diskutiere ich die aktuellen Anforderungen, insbesondere in Bezug auf maschinelles Lernen und die daraus hervorgehenden neuen Stars in der KI: Python und Clojure.

 

Teil 2: Lisp – Der Klassiker

12.02.2018
Lisp ist seit 60 Jahren die Sprache der KI. Dieser Beitrag erklärt, was Lisp einzigartig macht.
If you give someone Fortran, he has Fortran.
If you give someone Lisp, he has any language he pleases.
Guy Steele

Lisp wurde 1958 von John McCarthy am MIT entwickelt, ein Jahr nach Fortran. Sprachen wie Fortran (prozedurale Sprachen) gehen von der „mitgelieferten“ Sprache der Maschine aus und abstrahieren oft genutzte Operationen, wie bei dem Beispiel der Multiplikation in Teil 1 dieser Blog-Serie. Lisp stützt sich dagegen auf ein mathematisches Konzept (das Lambda-Kalkül), bei dem die Funktionalität durch Funktionen beschrieben wird. Diese Art von Sprachen bezeichnet man als deklarativ, weil man mehr beschreibt, was das Ergebnis sein soll, und weniger in welchen Schritten die Maschine dieses errechnen soll.

Ein deklaratives Konzept, das in der KI immer eine große Rolle gespielt hat, ist Logik. Man möchte vielleicht etwas ausdrücken wie „Der Laptop ist auf dem Tisch“. Mathematisch lässt sich das in Prädikatenlogik ausdrücken durch auf(Laptop,Tisch) (Hier wird angenommen, das Laptop und Tisch Konstanten sind, es gibt also nur diesen einen Laptop und diesen einen Tisch.)

Computer sind aber nunmal Rechenmaschinen und können originär nur Zahlen repräsentieren. Aber man kann die Nullen und Einsen in einem Speicher statt als Binärzahl als Repräsentation für eine Konstante wie Tisch auffassen. Dies fällt leichter, wenn die Programmiersprache diese Umwandlung bereits vornimmt und man als Programmierer nur noch mit Symbolen wie Tisch und Laptop arbeiten muss. Lisp bietet ausgezeichnete Unterstützung für solche symbolische Repräsentationen. Anders als Prolog, das wir im nächsten Teil genauer ansehen, ist Lisp aber nicht auf Symbole beschränkt, sondern unterstützt genausogut „normale“ Berechnungen. Sie bietet damit eine enorme Vielfalt an Repräsentationsmöglichkeiten und Herangehensweisen zur KI. Denn bis heute kann man sich nicht darauf einigen, ob KI (rein) symbolisch arbeitet oder ob subsymbolische Prozesse (also Rechnen) Intelligenz ausmachen.

Am Rande sei erwähnt, dass die Idee der symbolischen Repräsentation bereits in der (KI-) Sprache IPL vorhanden war. Im Gegensatz zu Lisp war diese spezifisch für symbolische Verarbeitung und ihr Abstraktionsgrad erinnert eher an Assembler als an eine moderne Programmiersprache. Deshalb wurde sie sehr schnell von Lisp abgelöst.

Neben der Allgemeinheit war Lisp in vielen Aspekten ihrer Zeit voraus. Konzepte wie Rekursion, Funktionen höherer Ordnung (d.h. dass man Funktionen als Wert übergeben kann) und automatische Speicherverwaltung, sind mittlerweile vor allem in funktionalen Sprachen vorhanden und finden schrittweise ihren Weg in Mainstream-Sprachen wie Java. Ein Beispiel, das zeigt, wie weit Lisp seiner Zeit voraus war: Funktionen höherer Ordnung kamen bei Java mit Version 8, im Jahr 2014, und damit 56 Jahre später als in Lisp.

Ein Merkmal, das Lisp bis heute einmalig macht, ist die schmale Grenze zwischen Daten und Code. Beispielsweise ist (+ 1 2 3) die Aufforderung die drei gegebenen Zahlen zu addieren, das Ergebnis ist 6. Dagegen wird die Zeile '(+ 1 2 3) als reine Daten interpretiert, genaugenommen als Liste. Man kann z.B. das erste Element dieser Liste abfragen: (first '(+ 1 2 3)) und erhält das Symbol (!) + zurück. Auch folgende Zeile akzeptiert Lisp als Liste von Zahlen und +-Symbolen: '(+ 1 + 2 + 3) Interpretiert man diese Liste jedoch als Code (+ 1 + 2 + 3) dann liefert Lisp einen Fehler zurück (nur das erste Element bezeichnet ein Funktionssymbol, der Rest wird als Argumente interpretiert und + ist kein sinnvolles Argument für eine Addition). Man kann also durch ein Hochkomma aus Code Daten machen. Anders herum geht das auch: die Funktion eval macht aus Daten Code: (eval '(+ 1 2 3)) liefert als Ergebnis 6.

Das klingt vielleicht erstmal nach Spielerei, und tatsächlich verwendet man eval höchst selten. Das Konzept Daten in Code und andersherum verwandeln zu können, erlaubt jedoch einen Mechanismus, um eigene Sprachen zu definieren. Auch wieder mit deutlicher Verspätung zu Lisp, wird so etwas heute als Domain Specific Language bezeichnet. In meiner Dissertation habe ich diesen Mechanismus verwendet, um die Sprache ROLL (Robot Learning Language) zu implementieren, die Lernverfahren für Roboterprogramme so zur Verfügung stellt, dass Roboter aufgrund eigener Erfahrungen ständig weiterlernen können. Dazu musste ich keinen eigenen Compiler schreiben, ich konnte den vollen Funktionsumfang von Lisp nutzen und ihn für meine Zwecke erweitern. Eine schöne Erklärung zur Besonderheit von Lisp Macros bietet dieses Diskussionsforum.

Lisp verdankt ihre Beliebtheit also ihren modernen Programmierkonzepten und ihrer Universalität bezüglich symbolischer und numerischer Verarbeitung, sowie der Möglichkeit aus ihr Spezialsprachen zu definieren.

 

Teil 3: Prolog – Logik pur

19.02.2018
Prolog war die Sprache des letzten KI Hypes. Vor allem in Europa, Kanada und Japan hat man große Hoffnungen auf sie gesetzt.

Prolog ist die zweite klassische Sprache für KI Anwendungen. Sie wurde 1972 von Alain Colmerauer und Philippe Roussel entwickelt. Reines Prolog verwendet eine eigene Syntax, aber frühe und auch einige heutige Implementierungen verwenden das Makrosystem von Lisp, sie definieren Prolog also als Spezialsprache unter Verwendung von Lisp.

Prolog steht für PROgrammation en LOGique und setzt damit schon im Namen ein klares Ziel: Programme in Prädikatenlogik zu schreiben. Das Ziel leitet sich von der Annahme ab, dass Wissen gut in Prädikatenlogik repräsentierbar ist (wie das Beispiel aus Teil 2: auf(Laptop,Tisch)) und dass intelligentes Verhalten aus logischen Schlussfolgerungen resultiert.

Diese Ansicht war im letzten KI Hype der 80er Jahre weit verbreitet. Insbesondere im japanischen Fifth Generation Computing Project spielte Prolog eine wichtige Rolle. Eine der großen Aufgaben zu dieser Zeit war die Sprachverarbeitung. Diese basierte damals vor allem auf der Idee der Universalgrammatik von Noam Chomsky. Dieser nahm an, dass alle Sprachen - egal ob natürlich oder formal - auf dem gleichen Prinzip beruhen. Prolog unterstützt diese Art der Sprachverarbeitung mit einem eingebauten Parser und einer speziellen Syntax zum Definieren von Grammatiken.

Satz --> Nominalphrase, Verbalphrase. Nominalphrase --> Name. Nominalphrase --> Artikel, Substantiv. Verbalphrase --> IntransitivesVerb. Aus dieser einfachen Grammatik kann man bereits eine Menge Sätze generieren, wenn man noch ein keines Wörterbuch hinzufügt (die Wörter sind alle klein geschrieben, da Großbuchstaben für Prolog Variablen sind):

  • Name: asterix, obelix
  • Artikel: der, die
  • Substantiv: ente, tisch
  • IntransitivesVerb: steht, schläft
Daraus entstehen Sätze wie „obelix schläft“, „asterix steht“, „die ente schläft“, „der tisch steht“. Allerdings auch „die tisch schläft“ oder „der ente steht“.

Prolog kann solche Sätze – mit gegebener Grammatik und Wörtern – sowohl generieren als auch parsen, also testen ob sie grammatikalisch korrekt sind. Prolog ermöglicht es auch, solche Grammatiken mit weiteren Syntaxprüfungen zu erweitern (z.B. die Kongruenz zwischen Artikel und Substantiv sicher zu stellen) und die Satzstruktur mit Bedeutung zu verknüpfen, sodass beispielsweise ein Satz wie „wer ist der freund von asterix“ mit der Antwort „obelix ist der freund von asterix“ beantwortet wird.

Ich habe Prolog in 4 Semestern in Vorlesungen verwendet und die Studierenden einfache Chatbots und sprachbasierte Computerspiele damit implementieren lassen. Daran lässt sich wunderbar ein Dilemma der KI aufzeigen: Man kann mit geeigneten Werkzeugen sehr einfach Programme erstellen, die beeindruckende Ergebnisse liefern; ein einfacher Chatbot ist mit Prolog in 100 Code-Zeilen zu bekommen. Aber der Schritt von einem einfachen ersten Aufschlag zu einem nützlichen Programm, das auch mit unerwarteten Fragen zurecht kommt, ist eine komplett andere Aufgabe. Da genügt es nicht, aus 100 Zeilen 150 Zeilen Code zu machen. Vielleicht schafft man es mit sehr viel Nutzertesten, einer guten Story außenrum und viel Handarbeit einen verwendbaren Chatbot für eine Spezielaufgabe daraus zu entwickeln; dann ist aber auch Schluss.

Ein Programm, das auf jede beliebige Frage zumindest sinnvoll antwortet, ist eine andere Geschichte. Man kann kaum erwarten, dass eine Programm auf jede beliebige Frage eine Antwort weiß. Da Menschen aber meist von ihrem eigenen Funktionsumfang ausgehen, erwartet man intuitiv zumindest, dass ein Chatbot eine sinnvolle Frage („Welchen Umfang hat die Erde“) von einer reinen Wortfolge („Umfang Erde“) oder komplettem Unsinn („asdf“) unterscheiden kann; oder dass es mitbekommt, wenn es beleidigt wird (ein Schicksal das Chatbots häufig widerfährt, Beispiele erspare ich der Leserschaft). Ich bezweifle, ob so ein Programm überhaupt mit den (Standard-)Mitteln von Prolog erzeugbar ist.

Prolog ist also eine Spezialsprache zur Verarbeitung von Wissen und natürlicher Sprache nach einem bestimmten Paradigma, nämlich Logik. Programme, die auf logischem Schlussfolgern beruhen, kann man mit Prolog deutlich eleganter und in kürzerer Zeit schreiben als mit jeder anderen Sprache. Die Frage ist aber, ob Intelligenz tatsächlich durch Logik entsteht. Die aktuelle Mode setzt jedenfalls auf datengetriebene Verfahren – so ziemlich das Gegenteil von Logik. Damit ist Prolog nicht automatisch tot. Einerseits kann die Mode sich wieder ändern, und andererseits kann Logik als Bestandteil eines komplexeren Programms dienen. Moderne Prolog Implementierungen bieten deshalb Schnittstellen zu Mainstream-Sprachen wie Java oder C.

 

Teil 4: Die aktuellen Champions

24.02.2018
Clojure und Python sind meine aktuellen Champions in der KI. Das Label „KI Sprache“ prangt nicht so deutlich wie an Lisp und Prolog, aber durch einen schnellen Entwicklungszyklus und Unterstützung durch Bibliotheken, zum Beispiel für statistisches maschinelles Lernen, sind sie die modernen Sprachen der KI.

Prolog ist aus der Mode gekommen, weil seine grundlegende Annahme der logischen Informationsverarbeitung an Ansehen verloren hat. Wie steht es mit Lisp, das nichts von seiner Flexibilität und Mächtigkeit verloren hat?

Lisp gilt immer noch als die klassiche KI Sprache. 1994 wurde ein ANSI Standard für Common Lisp eingeführt, der in verschiedenen kommerziellen und freien Implementierungen umgesetzt wird. In den letzten 60 Jahren haben viele Programmiersprachen in Sachen Programmierkonzepte aufgeholt. Funktionale Sprachen implementieren jetzt die Konzepte des Lambda Kalküls – und das oft in „reinerer“ Form als Lisp; Mainstream-Sprachen haben der Reihe nach Konzepte von Lisp übernommen – sogar Java unterstützt jetzt Funktionen höherer Ordnung. Und das sind die Sprachen, die üblicherweise an Universitäten in Grundvorlesungen gelehrt werden. In einer KI Vorlesung steht man als Dozent dann vor der Wahl, einen guten Teil des Semesters zu investieren um Lisp als Sprache einzuführen, oder auf Java oder eine sonstige bekannte Sprache auszuweichen und sich stärker auf die Algorithmen zu konzentrieren (auch wenn deren Implementierung in Java oft etwas unbequemer ist als in Lisp).

Außerdem hat sich die KI als Feld in viele kleine Einzelfelder aufgespalten. Jedes dieser Gebiete hat andere Ansprüche und kann spezifische Sprachen verwenden. So verwendet man bei der Bildverarbeitung maschinennahe Sprachen, die schnelle und optimierte Matrizenrechnungen erlauben.

All dies hat dazu beigetragen, dass es weniger starke Sprachpräferenzen gibt als in den 80er und 90er Jahren. Trotzdem möchte ich zwei Sprachen herausheben: Clojure als moderne Variante von Lisp und Python als fast-schon-Standard für datengetriebene Verfahren.

Clojure ist eine Neuimplementierung von Lisp auf der Java Virtual Machine. Es ist keine Umsetzung von ANSI Common Lisp, sondern fügt neue Konzepte hinzu. Beispielsweise sind die klassischen Assoc-Listen aus Lisp zu Hashmaps geworden, die auch in Java verbreitet sind. Clojure hat sämtliche Vorteile von Lisp inklusive des Makro-Mechanismus zur Definition von Spezialsprachen, und erweitert diese um

  • einen stärkeren Fokus auf funktionales Programmieren durch unveränderbare (immutable) Datenstrukturen;
  • die Unterstützung von paralleler Verarbeitung durch explizite, abstrakte Konzepte zur synchronen und asynchronen Prozesskommunikation;
  • volle Kompatibilität zu Java, man kann also alle Java-Bibliotheken nutzen. Dies ist besonders nützlich wenn man graphische Nutzeroberflächen erstellen möchte, wofür die Möglichkeiten in Lisp sehr eingeschränkt sind. Auch die Verwendung der gut ausgebauten Software WEKA zum maschinellen Lernen ist damit einfach möglich.

Besonders außerhalb der Spezialgebiete der KI, dort wo Leute weiterhin versuchen intelligente Gesamtsysteme zu bauen (heute meist unter dem Stichwort „Kognitive Systeme“), ist Clojure die Sprache der Wahl.

Python ist der neue Star unter den KI Sprachen. Python gilt als Skriptsprache, also eine Sprache, in der man nicht unbedingt große Programm schreibt, sondern kleine Schnipsel, die schnell kleinere oder auch mal größere Aufgaben erledigen. Wie auch Clojure ist Python eine interpretierte Sprache. Man kann also in einer Python-Konsole einfach lostippen mit 5 * 3 und bekommt als Antwort 15. In Java oder C ist das nicht möglich. Dort braucht man ein vollständiges Programm, das erst compiliert und dann ausgeführt wird. Der Entwicklungszyklus in interpretierten Sprachen ist daher viel kürzer als in compilierten Sprachen.

Nehmen wir an, ich möchte in Python eine Funktion schreiben, die die Fakultätsfunktion berechnet (der Klassiker aus jeder Informatik-Vorlesung: man multipliziert alle Werte bis zum gegebenen Wert n, also fak(3) = 1 * 2 * 3 = 6). Ich tippe los: def fak(n): if (n == 1): 1 else: n * fak(n-1) Das sieht soweit gut aus für mich, also probiere ich es aus: fak(2) Python ist weniger überzeugt von meiner Funktion und antwortet mit einem Fehler: Traceback (most recent call last): File "", line 1, in File "", line 3, in fak TypeError: unsupported operand type(s) for *: 'int' and 'NoneType' Ach ja, in Clojure gedacht, dort wird der letzte Wert automatisch als Rückgabewert interpretiert. In Python braucht man dafür ein explizites return: def fak(n): if (n == 1): return(1) else: return(n * fak(n-1)) Der Umbau hat mich maximal eine halbe Minute gekostet und schon kann ich wieder ausprobieren: fak(3) Diesmal antwortet Python wie gewollt mit 6. Jetzt könnte ich die Funktion kopieren und in ein größeres Programm einfügen. Gesamte Entwicklungszeit für diese Funktion: unter 5 Minuten. Wenn ich das gleiche in Java probieren würde, würde ich die 5 Minuten allein dafür brauchen ein Projekt anzulegen, mir einen nichtssagenden Klassennamen auszudenken (in Java muss alles in eine Klasse), eine main-Methode anzulegen, und die Funktion zu schreiben. Obendrein müsste ich noch überlegen, wie ich das ganze testen möchte: der einfachste Fall wäre, einen Wert für n fest einzuprogrammieren und zu sehen, was das Programm ausgibt. Allerdings muss ich dann für jeden Testwert das Programm neu compilieren. Ich könnte in der main-Methode auch eine Schleife schreiben, die einen Eingabewert einliest und daraus die Fakultät berechnet. Das kostet mich wieder Zeit und wenn ein Fehler auftritt, kann dieser ebenso in meiner Fakultätsfunktion sein wie in der Schleife zum Einlesen der Werte.

Interpretierte Sprachen erlauben also einen viel schnelleren Trial-and-Error Zyklus. Jetzt könnte man einwenden, dass Programmieren doch kein Ausprobieren sein sollte, sondern ein wohl überlegter Vorgang. Diese Denkweise stammt aus der Zeit des Wasserfallmodells, wo man daran glaubte, dass man Programme abstrakt vorkonzipieren kann und wenn man das ordentlich erledigt hat, der Code nur noch heruntergeschrieben werden muss. Dieses Vorgehen hat nie funktioniert und kann es auch gar nicht. Programmieren ist ein kreativer Prozess und der lebt vom Ausprobieren (oder eleganter ausgedrückt Prototyping, besser noch Rapid Prototyping). Agile Entwicklungsmethoden setzen deswegen auf kurze Entwicklungszyklen, in denen man schnell Fehler machen und diese genauso schnell korrigieren kann.

In der KI ist diese Anforderung noch höher, nehmen wir als Beispiel maschinelles Lernen: Der Erfolg beim maschinellen Lernen resultiert in erster Linie von menschlicher Ingenieurskunst. Es gibt kaum theoretische Grundlagen, die sagen würden, welche und wie viele Daten man benötigt, wie man diese vorverarbeiten und repräsentieren muss. Diese Parameter kommen aus menschlicher Erfahrung und schnellem Ausprobieren, was insbesondere durch geeignete Visualisierung vereinfacht wird.

Hingegen sind die Algorithmen an sich weniger entscheidend. Man hat keinen Grund, einen Lernalgorithmus selbst zu implementieren, das haben schon andere erledigt. Wichtig ist der Zugriff auf entsprechende Bibliotheken. Und hier liegt Python gerade ganz vorn. Es gibt nicht nur Bibliotheken für statistische Analyse und maschinelles Lernen, sondern auch welche zum Visualisieren von Daten. Lernbibliotheken sind nicht unbedingt in Python geschrieben, aber sie bieten fast immer eine Schnittstelle dazu an (z.B. TensorFlow von Google).

Bei modernen KI-Sprachen zählt also vor allem ein schneller Entwicklungszyklus und die Verfügbarkeit von Bibliotheken. Sowohl Clojure als auch Python bieten beides und sind damit meine Favoriten.