Teil 2: Wider die Mehrdeutigkeit

Machine Learning Tutorial: Natürliche Datentypen und sprachnatürlicher Polymorphismus

Masoud Amri

© iStockphoto.com/RinArte

Wer sich mit Entwicklern über Mehrdeutigkeit unterhält, kommt schnell auf die Polymorphie der objektorientierten Programmierung zu sprechen. Mehrdeutigkeit in Zusammenhang mit natürlicher Sprache wird in der Regel als Poesie und kunstvoll empfunden. Doch nicht nur poetische Sätze können mehrdeutig sein, auch alltägliche Konversationen enthalten oft Mehrdeutigkeiten, die wir meist unbewusst aus deren inhaltlicher Bedeutung erschließen können. Für sprachverarbeitende Anwendungen ist jedoch die Ableitung der möglichen Bedeutungen eine große Herausforderung. Diese Problematik lässt sich in Cogniology durch den Ansatz der natürlichen Datentypen und des sprachnatürlichen Polymorphismus teilweise lösen.

Bevor wir uns mit Mehrdeutigkeit, Polymorphie und natürlichen Datentypen beschäftigen, werfen wir kurz einen Blick auf die Vorgehensweise. Da wir es bei der Entwicklung der Digitalassistenten mit natürlichen Sprachen zu tun haben, ist die Denkweise, wie wir die Anforderungen der Anwender aufnehmen, sie analysieren, Lösungen dafür finden und diese umsetzen, besonders am Anfang komplex und anders als der Umgang mit typischen herkömmlichen Anwendungen. Beispielsweise beziehen sich die User Stories im Zusammenhang mit natürlichen Sprachen nicht auf die Benutzeroberflächen, sondern auf Szenarien, die sich in einer Konversation abspielen bzw. auf Sätze, die in einer Konversation eine Rolle spielen.

Eine systematische Vorgehensweise kann uns dabei helfen, solche Projekte qualitativ abzuarbeiten. Ein solcher systematischer Vorgang ist uns aus den 90er-Jahren unter dem Begriff „Softwareentwicklungszyklus“ bekannt. Dabei wird die Softwareentwicklung in einem inkrementellen Zyklus, bestehend aus den Schritten Analyse, Design, Implementierung, Testen und Lieferung, durchgeführt, um die Komplexität der Softwareentwicklung signifikant zu reduzieren. Dieser Vorgang wird heute nicht mehr explizit praktiziert, jedoch implizit von jedem Java-Entwickler durchgeführt, indem er die User Stories in Klassen, Objekte und Logik umsetzt. Wir werden in diesem Artikel diese Schritte durchlaufen, wobei der Fokus mehr auf Analyse, Design und Implementierung liegt.

Artikelserie
Teil 1: Sentence Component
Teil 2: Polymorphismus in natürlicher Sprache
Teil 3: Maschinelle Schlussfolgerung
Teil 4: Maschinelles Lernen und Verstehen
Teil 5: Programmierung in natürlicher Sprache I
Teil 6: Programmierung in natürlicher Sprache II

In unserem letzten Artikel haben wir uns mit der Realisierung eines Digitalassistenten für Zahnarztpraxen beschäftigt. Im ersten Schritt (Analyse) haben wir ein Szenario für die Konversation zwischen Zahnarzt, Zahnarzthelferin und den Patienten vorgestellt. Wir haben festgestellt, dass der Satz „Ich könnte nächsten Montagmorgen kommen – ginge das?“ für die Terminvereinbarung relevant ist.

Im zweiten Schritt (Design) haben wir den Satz genauer unter die Lupe genommen und festgestellt, dass „nächsten Montagmorgen“ ein Datum ist. Im letzten Schritt (Implementierung) haben wir eine Sentence Component mit der Annotation @Sentence(„Ich komme an irgendeinem Datum.“) entwickelt. Dabei haben wir einen Teil des in der Annotation enthaltenen Satzes als Parameter definiert und ihm als natürlichen Datentyp die Kategorie Datum zugewiesen.

public class Terminvereinbarung
{
  Date datum = null;

  @Sentence("Ich komme an irgendeinem Datum.")
  public String createCalenderEntry()
  {
return "Wir haben Ihnen in unserem Terminkalender einen Termin für „ + 
datum.toString() +
" eingetragen.";
  }

  @NaturalTypeMethod(naturalName = "an irgendeinem Datum", naturalType = "Datum")
  public void setDatum(Date datum){
    this.datum = datum;
  }
}

Natürliche Datentypen lösen Mehrdeutigkeit auf

Diese Sentence Component haben wir dann in unserer Plattform hochgeladen und in ihrem Texteingabefeld den Text „Ich könnte nächsten Montagmorgen kommen“ eingegeben. In Ausgabefeld erschien dann „Wir haben Ihnen in unserem Terminkalender einen Termin für 09.10.2017 08:30 eingetragen“. Wir sind danach kurz auf die natürlichen Datentypen eingegangen und haben erläutert, wie man sie einsetzt. In diesem Artikel werden wir uns mit den natürlichen Datentypen intensiver beschäftigen und zeigen, wie sie sich realisieren lassen, und welche Rolle die natürlichen Datentypen bei der Auflösung der Mehrdeutigkeit und Polymorphismus spielen.

Bevor wir auf die Technik eingehen, möchten wir die Definition der natürlichen Datentypen erläutern: Ein natürlicher Datentyp ist eine endliche, klassifizierte Gruppe in natürlicher Sprache, die sich und ihre Merkmale durch ein Codesegment in einen technischen Datentyp konvertieren lässt.

Python Summit 2018
Oz Tiram

Advanced Flow Control in Python

mit Oz Tiram (derico – web development & consulting))

Um zwischen natürlichen Datentypen und Java-Datentypen unterscheiden zu können, verwenden wir den Begriff technischer Datentyp für Java-Datentypen. Somit ist ein technischer Datentyp ein einfacher Java-Datentyp (Integer, String) oder ein komplexer Datentyp (Class, Enum). Eine endliche, klassifizierte Gruppe ist ein Ausdruck in natürlicher Sprache, der für die Ordnung von Dingen und Wissen nach einheitlichen Kriterien steht. Beispiele dafür sind Autos, Getränke, Schmetterlinge oder ein Datum. Demnach ist eine endliche, klassifizierte Gruppe eine Kategorie, eine Klasse oder Gruppe, die wir täglich oft verwenden. So verstehen wir unter dem Begriff Getränk die Gruppe aller zum Trinken zubereiteten Flüssigkeiten. Mit dem Codesegment ist eine Java-Methode gemeint, die die Merkmale einer Kategorie in technischen Datentypen konvertiert. Um alle relevanten Themen rund um den natürlichen Datentyp plausibel darzustellen, möchten wir einen einfachen Digitalassistenten für einen Getränkelieferservice entwickeln.

Konversation zwischen Verkäufer und Kunde analysieren

Wir notieren zuerst Szenarien bzw. Sätze, die in der Konversation zwischen dem Verkäufer und dem Kunden vorkommen können. Diese Szenarien können Sätze zur Begrüßung oder ähnliches beinhalten, auf die wir hier aber verzichten. Stattdessen werden wir uns auf die Sätze konzentrieren, die für die Bestellungsannahme relevant sind. Dabei fangen wir mit einfachsten Sätzen an:

  • Ich möchte eine Cola.
  • Ich bestelle einen Orangensaft.
  • Ich trinke Wasser.
  • Ein Bier, bitte.

Wir müssen herausfinden, ob die Sätze Kategorien, Klassen, Gruppen oder allgemeingültige Begriffe beinhalten. Aus obigen Sätzen erkennen wir, dass Cola, Orangensaft, Wasser und Bier Getränke sind.

Element der Gruppe „Getränke“ speichern

Im nächsten Schritt müssen wir in den meisten Fällen eine neue Java-Klasse schreiben, die die in einer Gruppe enthaltenen Daten speichert. Für die Gruppe Getränke reicht zur Speicherung deren Elemente (Cola, Orangensaft, Wasser und Bier) ein einfacher Datentyp String.

„@NaturalTypeConverter“ implementieren

Die natürlichen Datentypen werden als eine Komponente realisiert, damit sie sich in anderen Konversationen ebenfalls verwenden lassen. Dafür erstellen wir ein neues Projekt und eine neue Klasse, z. B. mit dem Namen Getraenke. Dann markieren wir diese Klasse mit der Annotation @NaturalType und implementieren die Schnittstelle NaturalTypeMatcher. In der Annotation @NaturalType geben wir den Namen des natürlichen Typs ein, der in einer Sentence Component angewendet werden kann, in diesem Fall Getränke. Das Interface NaturalTypeMatcher definiert eine einzige Methode matches, die als Argument einen Text enthält und verifizieren muss, ob im Text die entsprechenden Elemente des Typs vorkommen. Darüber hinaus muss die Klasse mindestens eine Methode mit der Annotation @NaturalTypeConverter implementieren.

@NaturalType( "Getränke" )
public class Getraenke implements NaturalTypeMatcher {

private String drink;

@Override
public boolean matches( String text )
{
if ( text.contains(„Cola") )
{
drink = „Cola";
return true;
}
else if ( text.contains („Orangensaft") )
{
drink = „ Orangensaft";
return true;
}
else if ( text.contains („Wasser") )
{
drink = „Wasser";
return true;
}
else if ( text.contains („Bier") )
{
drink = „Bier";
return true;
}

return false;
}

@NaturalTypeConverter
public String getDring() {
return drink ;
}
}

Die Elemente einer Gruppe können auch Eigenschaften aufweisen. Ein rotes Auto oder ein viertüriges Cabriolet dienen als Beispiele für die Gruppe Auto. Wir erweitern unsere Sätze um einige Eigenschaften:

  • Ich möchte eine eiskalte Cola.
  • Ich bestelle einen frischen Orangensaft.
  • Ich trinke stilles Mineralwasser mit Eiswürfeln.
  • Ich nehme einen Kaffee ohne Milch.
  • Ein kaltes Bier, bitte.

Java-Klasse mit drei Variablen erstellen

Für die Speicherung der Daten erstellen wir eine neue Java-Klasse mit drei Variablen und nennen sie Drink. Die Variablen lauten properties zur Speicherung von kalt, still, drink für das Getränk selbst und with für mit, ohne. Letztendlich fügen wir sideOrder zur Speicherung von Eiswürfel, Milch hinzu. Zudem schreiben wir, falls sinnvoll, einige Hilfsmethoden (Listing 3).

public class Drink {
  private String properties;
  private String drink;
  private String with;
  private sideOrder;
  /* Die Getter- und Setter-Methoden wurden nicht dargestellt. */

  public Boolean istKalt()
  {
        if ( properties. equals( "kalt" ) )
    {
      return true;
    }
    return false;
  }

  public Boolean istFrisch()
  {
    /* Analog zu istKalt. */
  }

Mit Hilfe von „RegEx“ umfangreiche Daten extrahieren

Die Prüfung des Texts und das Herausnehmen der Eigenschaften der Gruppe ist leider mit einer einfachen Funktion contains() nicht mehr möglich. In den meisten Fällen haben wir mit der Extrahierung umfangreicher Daten zu tun. Zum Glück bieten Java und die meisten Programmiersprachen die regulären Ausdrücke (RegEx). Damit sieht die Methode matches() wie in Listing 4 aus.

@Override
public boolean matches( String text )
{
Pattern pattern = Pattern.compile( "  (?i)\\s*\\b(cola|orangensaft|wasser|kaffee|bier)\\b\s*"  );
return pattern.matcher(text).find();
}

Wir notieren immer ganz am Anfang unseres regulären Ausdrucks die Regel (?i); damit werden die Groß- und Kleinschreibung ignoriert. Dann schreiben wir in zwei runden Klammern alle Elemente der Gruppe Getränke mit Kleinbuchstaben und trennen sie mit |.

(cola|orangensaft|wasser|kaffee|bier)

Der senkrechte Strich wird in regulären Ausdrücken als ODER interpretiert. Mit \\b vor und nach runden Klammern geben wir an, dass die in runden Klammern eingegebenen Wörter nur als Ganzes vorkommen dürfen. Damit wird verhindert, dass zum Beispiel Wassermelone als Wasser interpretiert wird.

\\b(cola|orangensaft|wasser|kaffee|bier)\\b

Letztendlich wird durch \\s* angegeben, dass zwischen Wörtern keine, eines oder mehrere Leerzeichen stehen können.

\\s*\\b(cola|orangensaft|wasser|kaffee|bier)\\b\\s*

Die Methode matches() sieht durch reguläre Ausdrücke übersichtlicher aus, dafür erscheint aber die Methode getDrink() komplizierter. Bevor wir mit der Entwicklung der Methode getDrink() starten, möchten wir noch einen weiteren Vorteil der regulären Ausdrücke erwähnen.

Reguläre Ausdrücke in Java unterstützen einen für uns sehr interessanten Ansatz. Durch die Verwendung runder Klammern im Suchmuster lässt sich ein Teil des Ausdrucks gesondert benennen. Wenn wir beispielweise auf den Satz „Ich trinke stilles Wasser mit Eiswürfeln“ den regulären Ausdruck (?<drink>cola|wasser) anwenden, wird die Methode getGroup(„drink“) den Wert Wasser zurückgeben. Somit können wir jeden Teilausdruck mit einem Namen versehen und Teilausdrücke miteinander kombinieren.

Pattern pattern = Pattern.compile("(?cola| wasser)" );
Matcher matcher = pattern.matcher( „Ich trinke stilles Wasser mit Eiswürfeln" );
if ( matcher.find() )
{
  System.out.println(    "Das Getränk ist: "  +  matcher.getGroup( "drink" )    );
  // Ausgabe: Das Grtränk ist Wasser
}

Aus dem bisher Gelernten soll nun ein Schema gebildet werden, das wir immer wieder verwenden und damit den Umgang mit den regulären Ausdrücken erleichtern können. Wie schon erwähnt, notieren wir immer ganz am Anfang unseres regulären Ausdrucks die Regel (?i); damit werden im kompletten regulären Ausdruck die Groß- und Kleinschreibung ignoriert. Zudem muss der ganze reguläre Ausdruck immer mit \\s* anfangen und mit \\s* enden.

Schema: (?i) \\s*… \\s*
Beispiel: (?i)\\s*(wasser|kaffee|bier)\\s*

Wir benennen jeden Teilausdruck und fügen vor und nach dem runden Klammer immer \\b hinzu:

Schema: \\b(?<name> … )\\b
Beispiel: \\b(?<drink>wasser|kaffee|bier)\\b

Hierbei werden die drei Punkte durch Wörter ersetzt, die wir in einer Zeichenkette herausfinden möchten. Die Wörter, die hintereinander vorkommen müssen, verbinden wir mit \\s+

Schema: \\b(?<name> … )\\b\\s+\\b(?<name> … )\\b
Beispiel: \\b(?<with>mit|ohne)\\b\\s+\\b(?<sideOrder>eiswürfel|milch)\\b

Wenn die Wörter optional sind (z. B. kaltes in kaltes Wasser), ergänzen wir ein Fragezeichen nach der runden Klammer. Zudem fügen wir \\s* vor und nach dem Ausdruck ein. Damit geben wir an, dass vor und nach dem Wort kein, ein oder mehrere Leerzeichen vorkommen können.

Schema: \\s*\\b(?<name> … )?\\b\\s*
Beispiel: \\s*\\b(?<properties>still|frisch|kalt)?\\b\\s*

Wenn ein Ausdruck, der aus mehreren Teilausdrücken besteht, optional ist (z. B. mit Milch im Satz Ich möchte einen Kaffee mit Milch), dann setzen wir den kompletten Ausdruck in ein rundes Klammerpaar und fügen nach der schließenden runden Klammer ein Fragezeichen hinzu.

Schema: \\s*(\\b(?<name> … )\\b\\s+\\b(?<name> … )\\b)?\\s*
Beispiel: \\s*(\\b(?<with>mit|ohne)\\b\\s+\\b(?<sideOrder>eiswürfel|milch)\\b)?\\s*

Mit diesem Schema sollten die meisten Fälle zur Erfassung der natürlichen Datentypen abgedeckt sein. Nun kann die Methode getDrink() mithilfe der regulären Ausdrücke realisiert werden, wie in Listing 9 zu sehen.

@NaturalTypeConverter
public String getDring() {
  StringBuffer sideOrder  = new StringBuffer();
  expression.append("(?i)\s*");

  expression.append("\\b(");
  expression.append(    "(?still|frisch|kalt) [emnrs]{1,2}");
  expression.append(")?\\b");

  expression.append("\\s*");

  expression.append("\\b");
  expression.append(    "(?cola|orangensaft|wasser|kaffee|bier)");
  expression.append("\\b");

  expression.append("\\s*");

  expression.append(    "(\\b(?mit|ohne)\\b\\s+\\b(?eiswürfel|milch)\\b)?");

  expression.append("\\s*");

  Pattern pattern = Pattern.compile(   expression.toString()   );

  Drink drink = new Drink();
  Matcher matcher = pattern.matcher( text );
  if ( matcher.find() )
  {
    drink.setProperties( matcher.getGroup( "properties" )  );
    drink.setDrink(  matcher.getGroup( "drink" )  );
    drink.setWith(  matcher.getGroup( "with" )  );
  }
  return drink;
}

Die Eigenschaftswörter der Getränke werden durch den Ausdruck (?<properties>) erfasst. Einige Eigenschaftswörter bzw. Adjektive richten ihre Endung nach dem zugehörigen Nomen. Diese Endungen sind in der Grammatik sehr wichtig, spielen jedoch meist in der Bedeutung für unsere Zwecke keine Rolle. Diese und Wörter wie schon, freilich, halt, eben, ja bezeichnet man als grammatische Partikel. Wir lassen die grammatischen Partikel immer weg.

Mit den [emnrs]{1,2} wird angegeben, dass am Ende eines Worts ein oder zwei der Buchstaben aus der Menge emnrs in allen Kombinationen vorkommen können. Zum Beispiel stilles, stillen, stille usw.

(?<properties>still|frisch|kalt) [emnrs]{1,2}

Wenn wir nun an unserem Satz „Ich trinke stilles Wasser mit Eiswürfeln“ den obigen regulären Ausdruck anwenden, wird die Methode getGroup(„properties“) den Wert still zurückgeben. Ohne Endungen ist es viel leichter, die Eigenschaftswörter z. B. in einer if-Anweisung zu prüfen.

if( getGroup(„properties“).equals(„still“)) {…}

Andernfalls müssten wir für jede Kombination die if-Anweisung anpassen:

if( getGroup(„properties“).equals(„stilles“) || getGroup(„properties“).equals(„stille“)) {…}

Reguläre Ausdrücke sind eine mächtige Sprache, wenn es um Zeichenketten geht. Für die Erstellung der natürlichen Datentypen reicht es in den meisten Fällen aus, sich an unserem Schema zu orientieren. Es ist jedoch empfehlenswert, sich mit den regulären Ausdrücken vertraut zu machen.

Nachdem wir gelernt haben, was mit natürlichen Datentypen gemeint ist und wie sie realisiert werden, können wir für unseren natürlichen Datentyp eine Sentence Component schreiben und uns mit dem eigentlichen Thema dieses Artikels, nämlich Mehrdeutigkeit und sprachnatürliche Polymorphie, beschäftigen.

public class Bestellung
{
  Drink drink = null;

  @Sentence("Ich nehme irgendein Getränk")
  public String bestellen()
  {
    if ( drink.equals("Kaffee") )
    {
      return „Ich bringe Ihnen gleich einen Kaffee";
    }
    else if ( drink.equals("Bier") )
    {
      return „Hier ist ein Kaffeehaus, keine Kneipe!";
    }
    else
    {
      return „Wir haben nur Kaffee!";
    }
  }

  @NaturalTypeMethod(naturalName = "irgendein Getränk", naturalType = "Getränke")
  public void setDrink(Drink drink){
    this.drink = drink;
  }
}


Mit der Annotation @Sentence("Ich nehme irgendein Getränk") realisieren wir eine einfache Sentence Component. Zudem markieren wir „irgendein Getränk“ als Parameter und weisen ihn als natürlichen Datentyp Getränke zu. Über die Methode setDrink(Drink drink) erhält die Sentence Component von der Plattform ein Objekt der Klasse Drink.

Wenn wir diese Sentence Component in der Plattform hochladen und den Text „Ich nehme einen Kaffee“ in das Eingabefeld eingeben, bekommen wir als Antwort: „Ich bringe Ihnen gleich einen Kaffee“. Wenn wir in Eingabefeld „Ich nehme ein Bier“ eingeben, erhalten wir als Antwort „Hier ist ein Kaffeehaus, keine Kneipe!

Diese Sentence Component funktioniert offensichtlich nur in einem Kaffeehaus und eignet sich nicht für eine Kneipe. Für Kneipen müssen wir eine neue Sentence Component entwickeln.

public class KneipeBestellung
{
// Unverändert

@Sentence("Ich nehme irgendein Getränk")
public String bestellen()
{
if ( drink.equals("Bier") )
{
return „Ich bringe Ihnen gleich ein Bier";
}
else if ( drink.equals("Cocktail") )
{
return „Hier ist eine Kneipe, keine Cocktailbar!";
}
else
{
return „Wir haben nur Bier!";
}
}

@NaturalTypeMethod(naturalName = "irgendein Getränk", naturalType = "Getränke")
public void setDrink(Drink drink){
// Unverändert
}
}

Wenn wir diese Sentence Component in der Plattform hochladen und den Text „Ich nehme ein Bier“ in das Eingabefeld eingeben, muss die Plattform nun entscheiden, welche der beiden Sentence Components ausgeführt werden soll. Die Plattform kennt die Schnittstellen der Sentence Components, aber nicht deren innere Implementierung. In diesem Fall sind die Schnittstellen beider Sentence Components gleich. Beide implementieren den gleichen @Sentence("Ich nehme irgendein Getränk") und haben die gleiche @NaturalTypeMethod(naturalName = "irgendein Getränk", naturalType = "Getränke").

Zur Lösung dieses Problems könnte die Plattform anhand der Geokoordinaten oder anhand eines Kalendereintrags und der aktuellen Uhrzeit herausfinden, ob der Benutzer sich gerade in einer Kneipe oder einem Café befindet. Die Möglichkeit, aus der Örtlichkeit den Kontext herauszufinden, kann in diesem Beispiel eine gute Lösung sein. Aber in den meisten Fällen können uns solche Möglichkeiten nicht helfen, das Problem der Mehrdeutigkeit in der natürlichen Sprache zu lösen.

Effektiv ist es, aus den inhaltlichen Bedeutungen zu schlussfolgern, welche Sentence Component in welchem Kontext ausgeführt werden soll. Diesem Ansatz möchten wir nachgehen und aus dem Zusammenspiel der Sentence Components und der natürlichen Datentypen über die inhaltlichen Bedeutungen entscheiden, welche der beiden Sentence Components ausgeführt werden soll. Hierfür erweitern wir unseren natürlichen Datentyp Getränke, indem wir sinnvolle Unterkategorien schaffen (Abb. 1).

Datentyp Getränke und Unterkategorien

Um zu präzisieren, welche Informationen unter jeder Kategorie zu entnehmen sind, schreiben wir einige passende Sätze: „Ich nehme schwarzen Kaffee mit Milch und Zucker.“ „Ich nehme Blue Ocean Cocktail mit Wodka und Limette.“ „Ich nehme trockenen Wein.“ Als Nächstes erstellen wir für jede Unterkategorie eine Klasse zur Speicherung der Eigenschaften der jeweiligen Kategorie. Ob diese Klassen von Drink abgeleitet werden sollen, mag jeder selbst entscheiden (Abb. 2).

Klasse zur Speicherung der Eigenschaften der jeweiligen Unterkategorie

 

Somit ändert sich die Klasse Getraenke wie in Listing 9 dargestellt.

@NaturalType( "Getränke" )
public class Getraenke implements NaturalTypeMatcher {
 
  // Unverändert

  @Override
  public boolean matches( String text )
  {
    Pattern pattern = Pattern.compile("(?i)\\s*(cola|wasser|bier|" +
      "kaffee|cocktail|cabernet\\s+sauvignon)\\s*" );
    return pattern.matcher(text).find();
 }

  @NaturalTypeConverter
  public Drink getDring() { … }

  @NaturalTypeConverter
  public Coffee getCoffee() { … }

  @NaturalTypeConverter
  public Wine getWine() { … }

  @NaturalTypeConverter
  public Cocktail getCocktail() { … }

  @NaturalTypeConverter
  public Beer getBeer() { … }
}

Dabei haben wir die Methode matches(String text) um den regulären Ausdruck kaffee|cocktail|wine erweitert, damit die Kategorien Kaffee, Cocktail und Wein mit dem Eingabetext abgestimmt werden können. Dazu haben wir noch vier weitere Methoden programmiert, deren Implementierungen sich nach dem schon besprochenen Schema richten, weshalb wir in diesem Artikel auf die weitere Beschreibung der inneren Implementierung der Methoden verzichten.

Mit der Erweiterung der natürlichen Datentypen Getränke ändern sich auch unsere beiden Sentence Components.

public class Bestellung
{
  private Coffee coffee = null;

  @Sentence("Ich nehme irgendein Getränk")
  public String bestellen()
  {
    if ( coffee.getWith() == null )
    {
      return „Wollen Sie Milch dazu?";
    }
    // und weitere sinnvolle Bearbeitung des Objects coffee.
  }

  @NaturalTypeMethod(naturalName = "irgendein Getränk", naturalType = "Getränke")
  public void setDrink(Coffee coffee){
    this.coffee = coffee;
  }
}


public class KneipeBestellung
{
  private Beer bier = null;

  @Sentence("Ich nehme irgendein Getränk ")
  public String bestellen()
  {
    if ( bier.istVomFass()  ==  null )
    {
      return „Wollen Sie Flaschenbier oder vom Fass?";
    }
    // und weitere sinnvolle Bearbeitung des Objects Bier.
  }

  @NaturalTypeMethod(naturalName = "irgendein Getränk", naturalType = "Getränke")
  public void setDrink(Beer bier){
    this.beer = bier;
  }
}

 

Wie Listing 10 zu entnehmen ist, passt sich die Implementierung der Methode bestellen() an den entsprechenden Kontext an und gibt jeweils die richtige Antwort zurück. Wichtig ist in diesem Zusammenhang, dass sich die Annotationen @Sentence("Ich nehme irgendein Getränk ") und @NaturalTypeMethod(naturalName = "irgendein Getränk", naturalType = "Getränke") nicht geändert haben. Zusätzlich sind durch die Methode setDrink() beide Sentence Components mit der passenden Klasse, nämlich Coffee und Beer, definiert.

Sobald wir die beiden Sentence Components in der Plattform hochgeladen haben, analysiert die Plattform die Annotationen @Sentence und @NaturalTypeMethod und trägt die Satzglieder, das Prädikat und Parameter der Sentence Components in einer sogenannten Grammatical Function ein.

  • Subjekt (er, ich, Herr Dr. Schwaiger)
  • Prädikat (Verb zusammen mit weiteren Bestandteilen, z. B. ansehen, Zeit haben)
  • Objekt (mich, dir, Herrn Dr. Schwaiger)
  • Signatur der Parameter in Form eines Tripples, bestehend aus Parametername, natürlichem Datentyp, technischem Datentyp z. B. (irgendein Getränk, Getränke, Coffee)

Subjekt, Prädikat und Objekt sind aus der Grammatik der meisten Sprachen schon bekannt. Die Signatur eines Parameters ermöglicht die eindeutige Zuordnung eines Teils des Satzes zu einem natürlichen Datentyp. Mit technischem Datentyp in diesem Tripel ist der Rückgabedatentyp einer Converter-Methode gemeint, die in der Implementierung eines natürlichen Datentyps durch @NaturalTypeConverter annotiert ist. Somit werden, wie in Listing 11 zu sehen, zwei Grammatical Functions für unsere beiden Sentence Components in einer Registry eingetragen.

Bestellung:
{
  Subject: "drittePerson",
  Predicate: "nehmen",
  Object: null,
  Signatures: [
    {
      name: „irgendein Getränk",
      naturalType: „Getränke",
      technicalType: Coffee
    }
  ] 
}

KneipeBestellung:
{
  Subject: "drittePerson",
  Predicate: "nehmen",
  Object: null,
  Signatures: [
    {
      name: „irgendein Getränk",
      naturalType: „Getränke",
      technicalType: Beer
    }
  ] 
}

 

Wie den beiden Grammatical Functions zu entnehmen ist, liegt der einzige Unterschied der beiden Grammatical Functions in den technischen Datentypen. Jeder technische Datentyp muss als Rückgabewert einer Methode in einer Klasse mit der Annotation @NaturalType definiert sein. Die Implementierung der natürlichen Datentypen Getränke haben wir in Listing 9 vorgestellt; zur Erinnerung sehen Sie in Listing 12 noch einmal einen Ausschnitt daraus.

@NaturalType( "Getränke" )
public class Getraenke implements NaturalTypeMatcher {
  …	
    @NaturalTypeConverter
    public Coffee getCoffee() { … }

    @NaturalTypeConverter
    public Beer getBeer() { … }
  …
}

Nun sind die beiden Sentence Components in der Plattform registriert und können ausgeführt werden. Wir geben erneut den Text „Ich nehme ein Bier“ in das Eingabefeld der Plattform ein. Die Plattform analysiert daraufhin den Eingabetext und erstellt dafür, wie oben schon beschrieben, eine Grammatical Function, jedoch ohne Signaturen (Listing 13).

Sentence: "Ich nehme ein Bier"
{
  Subject: "drittePerson",
  Predicate: "nehmen",
  Object: null,
}

Im nächsten Schritt vergleicht die Plattform Subjekt, Prädikat und Objekt des Eingabetexts mit jeder einzelnen in der Registry registrierten Grammatical Function, um zum Eingabetext passende Sentence Components zu finden. In diesem Fall stimmen die beiden gerade registrierten Sentence Components mit dem Eingabetext überein:

Bestellung Kaffee: { … naturalType: „Getränke“, technicalType: Coffee}]}
Bestellung Bier: { … naturalType: „Getränke“, technicalType: Beer}]}

Die Plattform erstellt, basierend auf der innerhalb der Variable naturalType angegebenen Zeichenkette, eine Instanz der Klasse Getraenke und ruft deren Methode matches() auf. Ergibt das Ergebnis des Aufrufs TRUE, wird damit der Plattform mitgeteilt, dass es in dem Eingabetext mindestens ein Getränk gibt, das durch eine Converter-Methode dieser Klasse in einen technischen Datentyp konvertiert werden kann. Im Fall der ersten Sentence Component wird die Methode getCoffee() der Klasse Getraenke aufgerufen. Der Aufruf der Methode getCoffee() erfolgt nicht aufgrund ihres Namens, sondern wegen ihres Rückgabedatentyps Coffee.

Getraenke getraenke = new Getraenke();
bolean result = getraenke.matches("Ich nehme ein Bier");
if( result == true)
{
  Bestellung bestellung = new Bestellung();
  Coffee coffee = bestellung.getCoffee();
  if ( coffee != null )
  {
    String answer = coffee .bestellen();
    return answer;
}
}

Gibt die Methode getraenke.getCoffee() als Ergebnis NULL zurück, geht die Plattform davon aus, dass es im Eingabetext keine Zeichenkette gibt, die zum technischen Datentyp Coffee passt. Ist dies der Fall, wird die Plattform das gleiche Verfahren mit der nächsten Sentence Component wiederholen. Die nächste Sentence Component hat als technischen Datentyp Beer. Dieser Aufruf gibt einen Wert ungleich NULL zurück, was zur Ausführung der bestellung.getBeer() führt.

Getraenke getraenke = new Getraenke();
bolean result = getraenke.matches("Ich nehme ein Bier");
if( result == true)
{
  BestellungBier bestellung = new BestellungBier ();
  Beer beer = bestellung.getBeer();
  if ( beer != null )
  {
    String answer = beer.bestellen();
    return answer;
}
}

Als Letztes möchten wir die Implementierung einer Sentence Component mit der gleichen Annotation @Sentence("Ich nehme irgendein Getränk") im Kontext eines Restaurants, das die beiden Getränke Kaffee und Bier anbietet, implementieren. Damit wird die Polymorphie in natürlicher Sprache deutlicher. Hierzu fügen wir beide Methoden getBeer() und getCoffee() in der Klasse RestaurantBestellung hinzu und passen die Methode bestellung() so an, dass beide Getränke bearbeitet werden können.

public class RestaurantBestellung
{	
  private Coffee coffee = null;
  private Beer beer = null;

  @Sentence("Ich nehme irgendein Getränk ")
  public String bestellen()
  {
    if ( coffee != null )
    {
      return „Restaurant: Wollen Sie Milch dazu?";
    }
    if ( beer != null )
    {
      return „Restaurant: Wollen Sie Flaschenbier oder vom Fass?";
    }
  }

  @NaturalTypeMethod(naturalName = "irgendein Getränk", naturalType = "Getränke")
  public void setCoffee(Coffee coffee){
    this.coffee = coffee;
  }

  @NaturalTypeMethod(naturalName = "irgendein Getränk", naturalType = "Getränke")
  public void setBeer(Beer beer){
    this.beer = beer;
  }
}

Nach dem Hochladen dieser Sentence Component geben wir in das Eingabefeld den Text „Ich nehme ein Bier“ ein; als Antwort wird „Wollen Sie Flaschenbier oder vom Fass?“ ausgegeben. Geben wir aber den Text „Zum Essen nehme ich ein Bier, danach einen Kaffee“ ein, sehen wir im Ausgabefeld als Antwort: „Restaurant: Wollen Sie Flaschenbier oder vom Fass?“ sowie „Restaurant: Wollen Sie Milch dazu?“. In diesem Fall stimmt der Eingabetext mit Kaffee und Bier überein. Daher hat die Plattform die Sentence Component RestaurantBestellung ausgeführt. Also hat die Plattform nicht die einzelnen Sentence Components zur Bestellung von Bier und Kaffee einzeln aufgerufen.

Mit dem Beispiel „Ich nehme irgendein Getränk“ wird deutlich, wie dieser Satz abhängig von seiner Anwendung aus dem Zusammenspiel der Sentence Components und der natürlichen Datentypen unterschiedliche Verhaltensweisen aufweist. Diese Vielgestaltigkeit wird in der Cogniologie als sprachnatürlicher Polymorphismus bezeichnet. Mit dem sprachnatürlichen Polymorphismus wird das Problem der Mehrdeutigkeit teilweise gelöst.

Fazit

Im ersten Artikel haben wir uns mit der Entwicklung eines Digitalassistenten für Zahnärzte beschäftigt und eine Sentence Component für die Terminvereinbarung entwickelt. Dabei sind wir kurz auf den natürlichen Datentyp Datum eingegangen. In diesem Artikel haben wir gezeigt, wie sich die natürlichen Datentypen entwickeln lassen. Wir haben ebenfalls gezeigt, wie Mehrdeutigkeit entsteht und wie aus dem Zusammenspiel der Sentence Component und natürlichen Datentypen (Polymorphismus) das Problem der Mehrdeutigkeit in natürlicher Sprache und kontextbezogene Verhaltenweisen teilweise gelöst werden können. Dies wird in KI als eine große Problemstellung angesehen. Im Wikipedia-Eintrag zu Mehrdeutigkeit [1] findet sich folgende Beschreibung:

„Eines der schwierigsten Probleme bei der automatischen Verarbeitung natürlicher Sprachen ist es, die Mehrdeutigkeit sprachlicher Zeichen auf eine Interpretation hin aufzulösen. Menschen gelingt dies – ebenso wie die Unterscheidung zwischen gewollter und ungewollter Mehrdeutigkeit – leicht. Sprachverarbeitende Programme scheitern noch oft daran.“

Lesen Sie auch: Kubeflow: Maschinelles Lernen für Kubernetes

Im nächsten Artikel werden wir uns auch weiterhin mit Polymorphismus und Mehrdeutigkeit beschäftigen. Wir werden anschließend die Themen Kohärenz und Schlussfolgerung aufgreifen und erklären, wie die Sätze eines Texts zusammenhängen und sich dadurch ein Sinn erschließen lässt. Abschließend zeigen wir, wie die Kontextdaten im Kurz- und Langzeitgedächtnis gespeichert und gesucht werden.

Geschrieben von
Masoud Amri
Masoud Amri ist Informatiker mit über zwanzig Jahren Erfahrung als Softwareentwickler und Softwarearchitekt bei namhaften Unternehmen (IBM, Mercedes, Volksbank, Fraunhofer etc.) Er arbeitet seit 2012 an der Theorie und Umsetzung der Cogniology.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: