EasyNLU ist eine in Java geschriebene Natural Language Understanding (NLU)-Bibliothek für mobile Apps. Da es auf der Grammatik basiert, eignet es sich gut für Domänen, die eng sind, aber eine strenge Kontrolle erfordern.
Das Projekt verfügt über eine Beispiel-Android-Anwendung, die Erinnerungen anhand natürlicher Spracheingaben planen kann:
EasyNLU ist unter Apache 2.0 lizenziert.
Im Kern ist EasyNLU ein CCG-basierter semantischer Parser. Eine gute Einführung in das semantische Parsen finden Sie hier. Ein semantischer Parser kann eine Eingabe wie die folgende analysieren:
Gehe morgen um 17 Uhr zum Zahnarzt
in eine strukturierte Form:
{task: "Go to the dentist", startTime:{offset:{day:1}, hour:5, shift:"pm"}}
EasyNLU stellt die strukturierte Form als rekursive Java-Karte bereit. Diese strukturierte Form kann dann in ein aufgabenspezifisches Objekt aufgelöst werden, das „ausgeführt“ wird. Beispielsweise wird im Beispielprojekt das strukturierte Formular in ein Reminder
Objekt aufgelöst, das über Felder wie task
, startTime
und repeat
verfügt und zum Einrichten eines Alarms mit dem AlarmManager-Dienst verwendet wird.
Im Allgemeinen sind die folgenden allgemeinen Schritte zum Einrichten der NLU-Fähigkeit aufgeführt:
Bevor wir Regeln schreiben, sollten wir den Umfang der Aufgabe und die Parameter der strukturierten Form definieren. Nehmen wir als Spielzeugbeispiel an, dass unsere Aufgabe darin besteht, Telefonfunktionen wie Bluetooth, WLAN und GPS ein- und auszuschalten. Die Felder sind also:
Ein Beispiel für ein strukturiertes Formular wäre:
{feature: "bluetooth", action: "enable" }
Außerdem ist es hilfreich, ein paar Beispieleingaben zu haben, um die Variationen zu verstehen:
Wenn wir das Spielzeugbeispiel fortsetzen, können wir sagen, dass wir auf der obersten Ebene eine Einstellungsaktion haben, die eine Funktion und eine Aktion haben muss. Anschließend verwenden wir Regeln, um diese Informationen zu erfassen:
Rule r1 = new Rule ( "$Setting" , "$Feature $Action" );
Rule r2 = new Rule ( "$Setting" , "$Action $Feature" );
Eine Regel enthält mindestens ein LHS und ein RHS. Konventionell stellen wir einem Wort ein „$“ voran, um eine Kategorie anzugeben. Eine Kategorie stellt eine Sammlung von Wörtern oder anderen Kategorien dar. In den obigen Regeln stellt $Feature
Wörter wie Bluetooth, BT, WLAN usw. dar, die wir mithilfe „lexikalischer“ Regeln erfassen:
List < Rule > lexicals = Arrays . asList (
new Rule ( "$Feature" , "bluetooth" ),
new Rule ( "$Feature" , "bt" ),
new Rule ( "$Feature" , "wifi" ),
new Rule ( "$Feature" , "gps" ),
new Rule ( "$Feature" , "location" ),
);
Um Variationen in Feature-Namen zu normalisieren, strukturieren wir $Features
in Unterfeatures:
List < Rule > featureRules = Arrays . asList (
new Rule ( "$Feature" , "$Bluetooth" ),
new Rule ( "$Feature" , "$Wifi" ),
new Rule ( "$Feature" , "$Gps" ),
new Rule ( "$Bluetooth" , "bt" ),
new Rule ( "$Bluetooth" , "bluetooth" ),
new Rule ( "$Wifi" , "wifi" ),
new Rule ( "$Gps" , "gps" ),
new Rule ( "$Gps" , "location" )
);
Ähnliches gilt für $Action
:
List < Rule > actionRules = Arrays . asList (
new Rule ( "$Action" , "$EnableDisable" ),
new Rule ( "$EnableDisable" , "?$Switch $OnOff" ),
new Rule ( "$EnableDisable" , "$Enable" ),
new Rule ( "$EnableDisable" , "$Disable" ),
new Rule ( "$OnOff" , "on" ),
new Rule ( "$OnOff" , "off" ),
new Rule ( "$Switch" , "switch" ),
new Rule ( "$Switch" , "turn" ),
new Rule ( "$Enable" , "enable" ),
new Rule ( "$Disable" , "disable" ),
new Rule ( "$Disable" , "kill" )
);
Beachten Sie das „?“ in der dritten Regel; das bedeutet, dass die Kategorie $Switch
optional ist. Um festzustellen, ob eine Analyse erfolgreich ist, sucht der Parser nach einer speziellen Kategorie, der sogenannten Stammkategorie. Konventionell wird es als $ROOT
bezeichnet. Wir müssen eine Regel hinzufügen, um diese Kategorie zu erreichen:
Rule root = new Rule ( "$ROOT" , "$Setting" );
Mit diesem Regelwerk sollte unser Parser in der Lage sein, die obigen Beispiele zu analysieren und sie in sogenannte Syntaxbäume umzuwandeln.
Das Parsen nützt nichts, wenn wir die Bedeutung des Satzes nicht extrahieren können. Diese Bedeutung wird durch die strukturierte Form (logische Form im NLP-Jargon) erfasst. In EasyNLU übergeben wir einen dritten Parameter in der Regeldefinition, um zu definieren, wie die Semantik extrahiert wird. Dazu verwenden wir die JSON-Syntax mit speziellen Markern:
new Rule ( "$Action" , "$EnableDisable" , "{action:@first}" ),
@first
weist den Parser an, den Wert der ersten Kategorie der Regel RHS auszuwählen. In diesem Fall wird es je nach Satz entweder „aktivieren“ oder „deaktivieren“ sein. Weitere Marker sind:
@identity
: Identitätsfunktion@last
: Wählen Sie den Wert der letzten RHS-Kategorie@N
: Wählen Sie den Wert der N- ten RHS-Kategorie, z. B. wählt @3
die dritte aus@merge
: Werte aller Kategorien zusammenführen. Nur benannte Werte (z. B. {action: enable}
) werden zusammengeführt@append
: Werte aller Kategorien in eine Liste einfügen. Die resultierende Liste muss benannt werden. Es sind nur benannte Werte zulässigNach dem Hinzufügen semantischer Marker lauten unsere Regeln:
List < Rule > rules = Arrays . asList (
new Rule ( "$ROOT" , "$Setting" , "@identity" ),
new Rule ( "$Setting" , "$Feature $Action" , "@merge" ),
new Rule ( "$Setting" , "$Action $Feature" , "@merge" ),
new Rule ( "$Feature" , "$Bluetooth" , "{feature: bluetooth}" ),
new Rule ( "$Feature" , "$Wifi" , "{feature: wifi}" ),
new Rule ( "$Feature" , "$Gps" , "{feature: gps}" ),
new Rule ( "$Bluetooth" , "bt" ),
new Rule ( "$Bluetooth" , "bluetooth" ),
new Rule ( "$Wifi" , "wifi" ),
new Rule ( "$Gps" , "gps" ),
new Rule ( "$Gps" , "location" ),
new Rule ( "$Action" , "$EnableDisable" , "{action: @first}" ),
new Rule ( "$EnableDisable" , "?$Switch $OnOff" , "@last" ),
new Rule ( "$EnableDisable" , "$Enable" , "enable" ),
new Rule ( "$EnableDisable" , "$Disable" , "disable" ),
new Rule ( "$OnOff" , "on" , "enable" ),
new Rule ( "$OnOff" , "off" , "disable" ),
new Rule ( "$Switch" , "switch" ),
new Rule ( "$Switch" , "turn" ),
new Rule ( "$Enable" , "enable" ),
new Rule ( "$Disable" , "disable" ),
new Rule ( "$Disable" , "kill" )
);
Wenn der Semantikparameter nicht angegeben wird, erstellt der Parser einen Standardwert, der dem RHS entspricht.
Um den Parser auszuführen, klonen Sie dieses Repository und importieren Sie das Parser-Modul in Ihr Android Studio/IntelliJ-Projekt. Der EasyNLU-Parser benötigt ein Grammar
-Objekt, das die Regeln enthält, ein Tokenizer
-Objekt zum Konvertieren des Eingabesatzes in Wörter und eine optionale Liste von Annotator
-Objekten zum Kommentieren von Entitäten wie Zahlen, Datumsangaben, Orten usw. Führen Sie nach dem Definieren der Regeln den folgenden Code aus:
Grammar grammar = new Grammar ( rules , "$ROOT" );
Parser parser = new Parser ( grammar , new BasicTokenizer (), Collections . emptyList ());
System . out . println ( parser . parse ( "kill bt" ));
System . out . println ( parser . parse ( "wifi on" ));
System . out . println ( parser . parse ( "enable location" ));
System . out . println ( parser . parse ( "turn off GPS" ));
Sie sollten die folgende Ausgabe erhalten:
23 rules
[{feature=bluetooth, action=disable}]
[{feature=wifi, action=enable}]
[{feature=gps, action=enable}]
[{feature=gps, action=disable}]
Probieren Sie andere Variationen aus. Wenn eine Analyse für eine Beispielvariante fehlschlägt, erhalten Sie keine Ausgabe. Anschließend können Sie die Regeln hinzufügen oder ändern und den Grammatikentwicklungsprozess wiederholen.
EasyNLU unterstützt jetzt Listen in strukturierter Form. Für die obige Domäne kann es Eingaben wie verarbeiten
Standort-BT-GPS deaktivieren
Fügen Sie diese drei zusätzlichen Regeln zur obigen Grammatik hinzu:
new Rule("$Setting", "$Action $FeatureGroup", "@merge"),
new Rule("$FeatureGroup", "$Feature $Feature", "{featureGroup: @append}"),
new Rule("$FeatureGroup", "$FeatureGroup $Feature", "{featureGroup: @append}"),
Führen Sie eine neue Abfrage aus wie:
System.out.println(parser.parse("disable location bt gps"));
Sie sollten diese Ausgabe erhalten:
[{action=disable, featureGroup=[{feature=gps}, {feature=bluetooth}, {feature=gps}]}]
Beachten Sie, dass diese Regeln nur ausgelöst werden, wenn die Abfrage mehr als ein Feature enthält
Annotatoren erleichtern den Zugriff auf bestimmte Arten von Token, die sonst umständlich oder gar nicht über Regeln zu handhaben wären. Nehmen Sie zum Beispiel die NumberAnnotator
-Klasse. Alle Zahlen werden als $NUMBER
erkannt und mit Anmerkungen versehen. Sie können dann in Ihren Regeln direkt auf die Kategorie verweisen, z. B.:
Rule r = new Rule("$Conversion", "$Convert $NUMBER $Unit $To $Unit", "{convertFrom: {unit: @2, quantity: @1}, convertTo: {unit: @last}}"
EasyNLU verfügt derzeit über einige Annotatoren:
NumberAnnotator
: Kommentiert ZahlenDateTimeAnnotator
: Kommentiert einige Datumsformate. Bietet außerdem eigene Regeln, die Sie mit DateTimeAnnotator.rules()
hinzufügen können.TokenAnnotator
: Kommentiert jedes Token der Eingabe als $TOKEN
PhraseAnnotator
: Kommentiert jede zusammenhängende Phrase der Eingabe als $PHRASE
Um Ihren eigenen benutzerdefinierten Annotator zu verwenden, implementieren Sie die Annotator
-Schnittstelle und übergeben Sie sie an den Parser. Sehen Sie sich die integrierten Annotatoren an, um eine Vorstellung davon zu bekommen, wie man einen Annotator implementiert.
EasyNLU unterstützt das Laden von Regeln aus Textdateien. Jede Regel muss in einer separaten Zeile stehen. Die LHS, RHS und die Semantik müssen durch Tabulatoren getrennt werden:
$EnableDisable ?$Switch $OnOff @last
Achten Sie darauf, keine IDEs zu verwenden, die Tabulatoren automatisch in Leerzeichen umwandeln
Wenn dem Parser weitere Regeln hinzugefügt werden, werden Sie feststellen, dass der Parser für bestimmte Eingaben mehrere Parses findet. Dies ist auf die allgemeine Mehrdeutigkeit menschlicher Sprachen zurückzuführen. Um festzustellen, wie genau der Parser für Ihre Aufgabe ist, müssen Sie ihn anhand beschrifteter Beispiele ausführen.
Wie die Regeln zuvor verwendet EasyNLU im Klartext definierte Beispiele. Jede Zeile sollte ein separates Beispiel sein und den Rohtext und die strukturierte Form getrennt durch einen Tabulator enthalten:
take my medicine at 4pm {task:"take my medicine", startTime:{hour:4, shift:"pm"}}
Es ist wichtig, dass Sie eine akzeptable Anzahl von Variationen in der Eingabe abdecken. Sie erhalten mehr Abwechslung, wenn Sie diese Aufgabe von verschiedenen Personen ausführen lassen. Die Anzahl der Beispiele hängt von der Komplexität der Domäne ab. Der in diesem Projekt bereitgestellte Beispieldatensatz enthält mehr als 100 Beispiele.
Sobald Sie die Daten haben, können Sie diese in einen Datensatz umwandeln. Der Lernteil wird vom trainer
übernommen; Importieren Sie es in Ihr Projekt. Laden Sie einen Datensatz wie diesen:
Dataset dataset = Dataset . fromText ( 'filename.txt' )
Wir evaluieren den Parser, um zwei Arten von Genauigkeiten zu bestimmen
Um die Auswertung durchzuführen, verwenden wir die Model
Klasse:
Model model = new Model(parser);
model.evaluate(dataset, 2);
Der zweite Parameter der Auswertungsfunktion ist die Ausführlichkeitsstufe. Je höher die Zahl, desto ausführlicher die Ausgabe. Die Funktion evaluate()
führt den Parser durch jedes Beispiel und zeigt fehlerhafte Analysen an, um schließlich die Genauigkeit anzuzeigen. Wenn Sie beide Genauigkeiten im hohen 90er-Bereich erreichen, ist kein Training erforderlich, und Sie könnten diese wenigen schlechten Analysen wahrscheinlich mit der Nachbearbeitung bewältigen. Wenn die Oracle-Genauigkeit hoch, die Vorhersagegenauigkeit jedoch gering ist, hilft das Training des Systems. Wenn die Oracle-Genauigkeit selbst gering ist, müssen Sie mehr Grammatik-Engineering betreiben.
Um die richtige Analyse zu erhalten, bewerten wir sie und wählen diejenige mit der höchsten Punktzahl aus. EasyNLU verwendet ein einfaches lineares Modell zur Berechnung der Ergebnisse. Das Training wird mittels Stochastic Gradient Descent (SGD) mit einer Scharnierverlustfunktion durchgeführt. Eingabefunktionen basieren auf Regelanzahlen und Feldern im strukturierten Formular. Die trainierten Gewichte werden dann in einer Textdatei gespeichert.
Sie können einige der Modell-/Trainingsparameter optimieren, um eine bessere Genauigkeit zu erzielen. Für die Erinnerungsmodelle wurden folgende Parameter verwendet:
HParams hparams = HParams . hparams ()
. withLearnRate ( 0.08f )
. withL2Penalty ( 0.01f )
. set ( SVMOptimizer . CORRECT_PROB , 0.4f );
Führen Sie den Trainingscode wie folgt aus:
Experiment experiment = new Experiment ( model , dataset , hparams , "yourdomain.weights" );
experiment . train ( 100 , false );
Dadurch wird das Modell für 100 Epochen mit einer Zugtestaufteilung von 0,8 trainiert. Es zeigt die Genauigkeiten des Testsatzes an und speichert die Modellgewichte im angegebenen Dateipfad. Um das gesamte Dataset zu trainieren, setzen Sie den Bereitstellungsparameter auf „true“. Sie können einen interaktiven Modus ausführen und Eingaben von der Konsole entgegennehmen:
experiement.interactive();
HINWEIS: Es kann nicht garantiert werden, dass das Training auch bei einer großen Anzahl von Beispielen eine hohe Genauigkeit liefert. In bestimmten Szenarien sind die bereitgestellten Funktionen möglicherweise nicht differenziert genug. Wenn Sie in einem solchen Fall nicht weiterkommen, melden Sie bitte ein Problem, damit wir zusätzliche Funktionen finden können.
Die Modellgewichte sind eine reine Textdatei. Bei einem Android-Projekt können Sie es im assets
-Ordner ablegen und mit dem AssetManager laden. Weitere Informationen finden Sie in ReminderNlu.java. Sie könnten Ihre Gewichte und Regeln sogar in der Cloud speichern und Ihr Modell drahtlos aktualisieren (jemand Firebase?).
Sobald der Parser die Eingabe in natürlicher Sprache gut in eine strukturierte Form umwandelt, möchten Sie diese Daten wahrscheinlich in einem aufgabenspezifischen Objekt haben. Für einige Domänen wie das obige Spielzeugbeispiel kann es ziemlich einfach sein. Für andere müssen Sie möglicherweise Verweise auf Dinge wie Daten, Orte, Kontakte usw. auflösen. Im Erinnerungsbeispiel sind die Daten häufig relativ (z. B. „morgen“, „nach 2 Stunden“ usw.) und müssen in absolute Werte umgewandelt werden. Bitte sehen Sie sich ArgumentResolver.java an, um zu sehen, wie die Auflösung durchgeführt wird.
TIPP: Beschränken Sie die Auflösungslogik beim Definieren von Regeln auf ein Minimum und erledigen Sie den Großteil davon in der Nachbearbeitung. Dadurch bleiben die Regeln einfacher.