Texte verstehen und für Volltextsuche nutzen

Machine-Learning-Bibliothek hilft dabei Texte zu verstehen

Lukas Molzberger

© Shutterstock / all_is_magic

Volltextsuche ist ein Kernbestandteil des Internetzeitalters, nichtsdestotrotz lässt sie bis heute viel zu Wünschen übrig. Sie ist ganz hervorragend dazu geeignet, um exakte Worttreffer in einer großen Menge an Dokumenten zu finden. Was allerdings bis heute noch nicht zuverlässig funktioniert ist, ein Wort nur in seiner gewünschten Bedeutung zu finden. Genau hier kann die Java-Bibliothek Aika weiterhelfen.

Aika ist eine Java-Bibliothek, mit der sich semantische Informationen in Texten erkennen und verarbeiten lassen. Da semantische Informationen sehr häufig mehrdeutig sind, erzeugt die Bibliothek für jede dieser Bedeutungen eine eigene Interpretation und wählt zum Schluss die am höchsten gewichtete aus. Aika kombiniert dazu aktuelle Techniken und Konzepte aus dem Bereich des maschinellen Lernens und der künstlichen Intelligenz, wie etwa künstliche neuronale Netze, Frequent Pattern Mining und auf Logik basierende Expertensysteme.

Aika basiert auf der heute gängigen Architektur eines künstlichen neuronalen Netzwerks (KNN) und nutzt diese, um sprachliche Regeln und semantische Beziehungen abzubilden. Das Grundprinzip eines KNN ist dabei schnell erklärt: Ein solches Netz besteht aus Neuronen, die über Synapsen miteinander verbunden sind. Um den Aktivierungswert eines Neurons zu berechnen werden die Produkte aus dem Aktivierungswert des Eingabeneurons und dem Synapsengewicht gebildet und für alle Eingabesynapsen des Neurons aufsummiert. Dazu wird noch der Bias, also eine Art Gegengewicht, addiert und das Ergebnis dann durch eine Transfer- bzw. Aktivierungsfunktion geschickt. Der daraus resultierende Aktivierungswert ist dann auf den Wertebereich zwischen 0 und 1 beschränkt und kann als Eingabe für weitere Neuronen dienen. Wenn man nun solche KNNs auf Texte anwendet, tauchen allerdings zwei Probleme auf.

Die Knackpunkte: Relationale Struktur und zyklische Abhängigkeiten

Das erste Problem: Texte haben eine von Grund auf relationale Struktur. Die einzelnen Worte stehen über ihre Reihenfolge in einer ganz bestimmten Beziehung zueinander. Gängige Methoden, um Texte für die Eingabe in ein KNN auszuflachen, sind beispielsweise Bag-of-Words oder Sliding-Window. Mittlerweile haben sich auch rekurrente neuronale Netze etabliert, die das gesamte Netz in einer Schleife für jedes Wort des Textes mehrfach hintereinander schalten. Aika geht hier allerdings einen anderen Weg. Aika propagiert die relationalen Informationen, also den Textbereich und die Wortposition, gemeinsam mit den Aktivierungen durch das Netzwerk. Die gesamte relationale Struktur des Textes bleibt also erhalten und lässt sich jederzeit zur weiteren Verarbeitung nutzen.

Das zweite Problem ist, dass bei der Verarbeitung von Text häufig nicht klar ist, in welcher Reihenfolge einzelne Informationen verarbeitet werden müssen (Abb. 1). Wenn wir beispielsweise den Namen „Ernst Koch“ betrachten, können sowohl der Vor- als auch der Nachname in einem anderen Zusammenhang eine völlig andere Bedeutung annehmen. Ernst könnte sich auch auf das allgemeinsprachliche Wort ‚Ernst der Lage‘ oder auf die Gemeinde Ernst an der Mosel beziehen. Und genauso könnte Koch eben auch den Beruf des Kochs meinen. Einfache Regeln, um hier dennoch den Vor- und den Nachnamen zu erkennen, wären: „Wenn das nachfolgende Wort ein Nachname ist, handelt es sich bei Ernst um einen Vornamen“ und „Wenn das vorherige Wort ein Vorname ist, dann handelt es sich bei Koch um einen Nachnamen“. Das Problem dabei ist nur, dass unsere Regeln nun eine zyklische Abhängigkeit beinhalten. Aber ist das wirklich so schlimm? Aika erlaubt es, genau solche Feedback-Schleifen abzubilden. Wobei die Schleifen natürlich sowohl positive, als auch negative Gewichte haben können. Der Trick dabei ist es nun zunächst nur Annahmen zu treffen, also etwa dass es sich bei dem Wort „Koch“ um den Beruf handelt und zu schauen wie das Netzwerk auf diese Annahme reagiert. Es bedarf also einer Evaluationsfunktion und einer Suche, die die Annahmen immer weiter variiert, bis schließlich eine optimale Interpretation des Textes gefunden ist. Genau wie schon der Textbereich und die Wortposition werden nun auch die Annahmen gemeinsam mit den Aktivierungen durch das Netzwerk propagiert.

Abb. 1: Bei der Verarbeitung von Text häufig ist nicht klar, in welcher Reihenfolge einzelne Informationen verarbeitet werden müssen

Lesen Sie auch: Mit Sensordaten und Maschinellem Lernen Bewegungen erkennen

Die zwei Ebenen von Aika

Aber wie lassen sich diese Informationen mit den Aktivierungen durch das Netzwerk propagieren, wo doch der Aktivierungswert eines Neurons für gewöhnlich nur eine Fließkommazahl ist? Genau hier liegt der Grund, weshalb Aika unter der neuronalen Ebene mit ihren Neuronen und kontinuierlich gewichteten Synapsen noch eine diskrete Ebene besitzt, in der es eine Darstellung aller Neuronen in boolscher Logik gibt. Aika verwendet als Aktivierungsfunktion die obere Hälfte der Tanh-Funktion (Abb. 2).

Abb. 2: Aika verwendet als Aktivierungsfunktion die obere Hälfte der Tanh-Funktion

Alle negativen Werte werden auf 0 gesetzt und führen zu keiner Aktivierung des Neurons. Es gibt also einen klaren Schwellenwert, der zwischen aktiven und inaktiven Neuronen unterscheidet. Anhand dieses Schwellenwertes lassen sich die Gewichte der einzelnen Synapsen in boolsche Logik übersetzen und entlang der Gatter dieser Logik kann nun ein Aktivierungsobjekt mit den Informationen durch das Netzwerk propagiert werden. So verbindet Aika seine diskrete bzw. symbolische Ebene mit seiner subsymbolischen Ebene aus kontinuierlichen Synapsen-Gewichten.

Ein Beispiel

Aber wie lässt sich Aika nun ganz konkret in der Praxis anwenden? Das möchte ich nun am Beispiel „Ernst Koch“ Schritt für Schritt erläutern. Zunächst wird ein leeres Modell und ein Iteration-Objekt angelegt, das für gewöhnlich der Verarbeitung eines Dokumentes dient. Da wir hier die Regeln aber manuell anlegen, benötigen wir erst einmal kein Dokument und übergeben einfach null.

Model m = new Model();
Iteration t = m.startIteration(null, 0);

Die folgenden drei Neuronen dienen nur dazu, die Wortposition innerhalb des Textes mitzuzählen. Das Startsignal verweist dabei auf den Anfang des Textes und das Leerzeichen dient als Takt, um die Wortposition hochzuzählen. Zu beachten ist, dass die create-Funktionen also etwa createCounterNeuron nur Hilfsfunktionen sind, die reguläre Neuronen erzeugen, die dann über Synapsen miteinander verknüpft werden.

InputNeuron spaceN = t.createOrLookupInputSignal("SPACE");
InputNeuron startSignal = t.createOrLookupInputSignal("START-SIGNAL");
Neuron ctNeuron = t.createCounterNeuron(new Neuron("RID Zähler"),
	spaceN, // Taktsignal
	false, // Richtung des Taktsignals (Bereichsende zählt)
	startSignal, // Startsignal
	true, // Richtung des Startsignals (Bereichsanfang zählt)
	false // Die Richtung in der die Worte durchgezählt werden sollen
);

Die folgenden beiden Maps enthalten die Wort-Eingabeneuronen. Die Neuronen der ersten Map enthalten zunächst noch keine Wortpositionen, die Neuronen der Zweiten dann aber schon.

// Wort String -> Wort Neuron
Map<String, InputNeuron> inputNeurons = new HashMap<String, InputNeuron>();
Map<String, Neuron> relNeurons = new HashMap<String, Neuron>();

Hier werden die Wort-Eingabeneuronen nun angelegt. Über die Hilfsfunktion createRelationalNeuron werden den Wortneuronen die jeweiligen Wortpositionen zugeordnet.

for(String word: new String[] {"Ernst", "Koch"}) {
	InputNeuron in = t.createOrLookupInputSignal("W-" + word);
	Neuron rn = t.createRelationalNeuron(
		new Neuron("WR-" + word),
		ctNeuron, // RID Zählerneuron
		in, // Eingabeneuron
		false // Richtung des Eingabeneurons
	);

	inputNeurons.put(word, in);
	relNeurons.put(word, rn);
}

Hier werden drei Kategorieneuronen für die spätere Verwendung vorbereitet. Diese dienen zunächst nur als Platzhalter und werden erst später mit ihren Synapsen verkabelt.

Neuron forenameCategory = new Neuron("C-Vorname");
Neuron surnameCategory = new Neuron("C-Nachname");
Neuron suppressingN = new Neuron("SUPPR");

Kommen wir nun zu der entscheidenden Stelle dieses Beispiels, denn hier wird das erste Entitätsneuron angelegt und mit seinen Eingabeneuronen verknüpft. Das Entitätsneuron steht für eine konkrete Bedeutung eines bestimmten Wortes. In diesem Falle also der Bedeutung des Wortes „Koch“ als Nachname. Was sich hier beobachten lässt, ist, dass die Eingaben, die später in Synapsen übersetzt werden, viel mehr Eigenschaften aufweisen als man das aus einem herkömmlichen neuronalen Netz gewohnt ist. Das liegt daran, dass die Synapsen in Aika ja nicht nur einen Aktivierungswert weiterpropagieren, sondern auch strukturelle Informationen wie den Textbereich und die Wortposition. Die Gewichte, die hier gesetzt werden, werden unverändert in die zu erzeugenden Synapsen übernommen. Sie sind auch für die Auswahl der richtigen Interpretation später von Bedeutung. Um wie hier bei einer Und-Verknüpfung den Bias nicht selbst berechnen zu müssen, unterstützt uns die Hilfsfunktion createAndNeuron dabei. Sie muss dazu aber die Mindestaktivierung jedes Eingabeneurons (MinInput) kennen.

Das Entitätsneuron wird hier jedenfalls über Synapsen mit drei Eingabeneuronen verbunden. Die Aufgabe der ersten Eingabe ist es nur zu prüfen ob das Wort „Koch“ überhaupt vorkommt. Die zweite Eingabe stellt eine positive Feedback-Schleife dar und prüft ob das vorherige Wort der Kategorie „Vorname“ angehört. Die dritte Eingabe ist eine negative Feedback-Schleife, die verhindert, dass zu einem Wort gleichzeitig mehrere Bedeutungen aktiv werden. Die restlichen Entitätsneuronen werden analog zu diesem erzeugt, daher sind sie hier nicht aufgeführt.

Neuron kochSurnameEntity = t.createAndNeuron(
		new Neuron("E-Koch (Nachname)"),
		0.5, // Anpassung des Bias
		new Input() // Prüft ob das Wort 'Koch' vorhanden ist
			.setNeuron(relNeurons.get("Koch"))
			.setWeight(10.0)
			.setMinInput(0.9)

			// Referenziert das aktuelle Wort
			.setRelativeRid(0)
			.setRecurrent(false),

		new Input() // Prüft ob das vorherige Wort ein Vorname ist
			.setNeuron(forenameCategory)
			.setWeight(10.0)
			.setMinInput(0.9)

			// Referenziert das vorherige Wort
			.setRelativeRid(-1)

			// Bei dieser Eingabe handelt es sich um eine positive Feedback-Schleife
			.setRecurrent(true)

			// Der Textbereich braucht nicht übereinzustimmen
			.setMatchRange(false)

			// Der Textbereich dieser Eingabe soll nicht an die Ausgabe weiterpropagiert werden
			.setStartVisibility(RangeVisibility.NONE)
			.setEndVisibility(RangeVisibility.NONE),

		new Input()
			.setNeuron(suppressingN)
			.setWeight(-20.0)
			.setMinInput(1.0)

			// Bei dieser Eingabe handelt es sich um eine negative Feedback-Schleife
			.setRecurrent(true)
);

Die folgenden beiden Neuronen bilden die Kategorien Vorname und Nachname ab. Sie bestehen lediglich aus einer Oder-Verknüpfung aller Vor- und Nachnamen-Entitäten.

t.createOrNeuron(forenameCategory,
		new Input() // Hier könnten durchaus auch mehrere Vornamen verknüpft sein.
			.setNeuron(ernstForenameEntity)
			.setWeight(10.0)
			.setRelativeRid(0)
);
t.createOrNeuron(surnameCategory,
		new Input()
			.setNeuron(kochSurnameEntity)
			.setWeight(10.0)
			.setRelativeRid(0)
);

Das letzte Neuron dient der Unterdrückung sich gegenseitiger ausschließender Bedeutungen. Prinzipiell wäre dies auch ohne ein weiteres Neuron möglich. Dann müsste aber jede Entität mit jeder anderen verknüpft sein. Daher ist es effizienter, ein solches vermittelndes Neuron dazwischen zu schalten. Zu beachten ist, dass Neuronen sich niemals selbst unterdrücken.

t.createOrNeuron(suppressingN,
	new Input().setNeuron(kochProfessionEntity).setWeight(10.0),
	new Input().setNeuron(kochSurnameEntity).setWeight(10.0),
	new Input().setNeuron(ernstCityEntity).setWeight(10.0),
	new Input().setNeuron(ernstCommonWordEntity).setWeight(10.0),
	new Input().setNeuron(ernstForenameEntity).setWeight(10.0)
);

Da jetzt das Modell vollständig aufgebaut ist, können wir es nun endlich nutzen, um einen Text zu verarbeiten.

Document doc = Document.create("Ernst Koch war ein deutscher Baumeister");

t = m.startIteration(doc, 0);

// Das Startsignal setzt die Anfangsposition für das Zählen der Wortpositionen
startSignal.addInput(t, 0, 1, 0);  // iteration, begin, end, relational id

int i = 0;
for(String w: doc.getContent().split(" ")) {
	int j = i + w.length();

	// Das Leerzeichen wird hier als Takt für das hochzählen der Wortposition genutzt.
	spaceN.addInput(t, j, j + 1);

	// Hier werden die einzelnen Wörter als Eingaben in das Netzwerk gefüttert.
	inputNeurons.get(w).addInput(t, i, j);

	i = j + 1;
}

// Die Suche nach der besten Interpretation für diesen Text.
t.process();

Der nachfolgende Code dient nur der Ausgabe der von Aika im Text erkannten Informationen. Die Schleife iteriert dabei beispielhaft über die Aktivierungen der Nachnamenkategorie. Analog kann so aber auch auf die Aktivierungen aller anderen Neuronen zugegriffen werden.

System.out.println(t.networkStateToString(true, true));
System.out.println();

System.out.println("Finale Interpretation: " + t.doc.selectedOption.toString());
System.out.println();

System.out.println("Aktivierungen der Kategorie Nachname:");
for(Activation act: surnameCategory.node.getActivations(t)) {
	System.out.print(act.key.r + " "); // Der Textbereich
	System.out.print(act.key.rid + " "); // Die Wortposition
	System.out.print(act.key.o + " "); // Die Annahme
	System.out.print(act.key.n.neuron.label + " "); // Label des Neurons
	System.out.print(act.finalState.value); // Der Aktivierungswert
}

t.clearActivations();

Die Ausgabe zu diesem Beispiel sieht dann wie folgt aus:

// Textbereich - Annahme - Neuron Label - Relationale Id (Wortposition)
// - Aktivierungswert, Gewicht dieser Annahme, Normierungswert zu dieser Annahme

(0,1)  - ()               - START-SIGNAL               - Rid:0    - FV:1.0  FW:0.0  FN:0.0
(0,5)  - ()               - W-Ernst                    - Rid:null - FV:1.0  FW:0.0  FN:0.0
(0,6)  - (2[(1)])         - E-Ernst (Gemeinde)         - Rid:0    - FV:0.0  FW:0.0  FN:3.07

In der Liste steht dabei jede Zeile für die Aktivierung eines Neurons. An zweiter Stelle einer Zeile steht die Annahme, die, falls sie Teil des Ergebnisses ist, auch unten in der finalen Interpretation aufgeführt ist. Die Annahmen sind so zu interpretieren, dass jede Annahme für sich eine ID besitzt. In den eckigen Klammern hinter dieser ID steht dann eine oder-verknüpfte Liste aller Annahmen von denen die aktuelle abhängig ist. Die Abkürzung RID steht für die relationale ID, die in diesem Besipiel nur zur Darstellung der Wortposition verwendet wird. Die Aktivierungswerte der einzelnen Neuronen sind die mit FV gelabelte Werte, die bei allen Neuronen, die nicht Teil der finalen Interpretation sind, einfach auf 0 gesetzt sind. Die mit FW gelabelten Werte sind die Gewichte, die der Auswahl der finalen Interpretation dienen. Sie werden aufsummiert sowie normiert und ergeben so das Gesamtgewicht (NW) einer Interpretation. Die FW-Gewichte ergeben sich aus dem Zusammenspiel von rekurrenten und nicht rekurrenten Eingabesynapsen. Sie sind notwendig, denn wenn einfach die Summe der Aktivierungswerte gebildet würde, Interpretationen mit vielen Neuronen unfair bevorzugt würden.

Lesen Sie auch: Bilderkennung mit Big Data und Deep Learning

Fazit

Wie in dem Beispiel zu sehen ist, lassen sich sprachliche Konstrukte wie Worte, Phrasen, Entitäten oder Kategorien leicht mit Aika abbilden. Die Bibliothek erlaubt es, diese Elemente zueinander in Beziehung zu setzen. Treten dabei Konflikte auf, ist Aika in der Lage, verschiedene Interpretationen eines Textes zu generieren, die sich gegenseitig ausschließen. Aika bewertet diese und startet dann eine Suche nach der sinnvollsten Interpretation. Das Ergebnis lässt sich dann indexieren und somit für eine semantische Volltextsuche nutzen. Die einzelnen Neuronen können leicht aus Listen von Städten, Berufen, Vor- und Nachnamen sowie sonstigen Entitäten erzeugt werden.

Geschrieben von
Lukas Molzberger
Lukas Molzberger
Lukas Molzberger hat an der Albert-Ludwigs-Universität in Freiburg Informatik mit Schwerpunkt Machine Learning studiert. Er arbeitet derzeit als Senior Software Developer für die meinestadt.de GmbH.
Kommentare
  1. Sven C.2017-06-23 10:49:17

    So ganz ist mir irgendwie nicht klar, was Aika kann. Das Beispiel jedenfalls macht das nicht deutlich.
    Ich füttere also alle Wörter + Bedeutungen, die ich interpretiert haben will hinein und muss aber für jedes Neuron (Wort+Bedeutung) die Gewichte und Konfiguration selbst festlegen.
    Dann gebe ich den Text hinein und erhalte Wahrscheinlichkeiten für die Bedeutungen + Wörter. Ich modelliere also ein komplexes System komplett selbst und muss JEDES vorkommende Wort und jede Bedeutung selbst anlegen. Das heißt ich muss nicht nur die Architektur des Netzwerks festlegen, sondern sogar die Gewichte und jede Relation explizit modellieren.
    Klar wenn ich eine Keywordsuche baue ist das sinnvoll, aber wo ist da jetzt das maschinelle Lernen? Habe ich das falsch verstanden oder etwas übersehen?

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.