Engineering

Textähnlichkeitssuche mit Vektorfeldern

Schon in seinen Anfangstagen als Rezeptsuchmaschine wurde Elasticsearch so konzipiert, dass es eine schnelle und leistungsfähige Volltextsuche ermöglicht. Angesichts dieser Geschichte ist die Verbesserung der Textsuche schon seit Langem ein wichtiges Motiv für unsere fortlaufende Beschäftigung mit Vektoren. In Elasticsearch 7.0 haben wir erstmals mit Feldtypen für hochdimensionale Vektoren experimentiert, und in Version 7.3 können diese Vektoren jetzt auch für das Dokument-Scoring genutzt werden.

Dieser Blogpost beschäftigt sich mit einem speziellen Verfahren namens „Textähnlichkeitssuche“. Bei dieser Art von Suche gibt der Nutzer eine kurze frei formulierte Abfrage ein und erhält dann eine Liste von Dokumenten, deren Reihenfolge sich nach ihrer Ähnlichkeit mit dem Suchtext richtet. Es gibt eine Vielzahl von Anwendungsfällen, in denen Textähnlichkeit nützlich sein kann:

  • Fragenbeantwortung: Aus einer Sammlung häufig gestellter Fragen werden diejenigen Fragen zurückgegeben, die der Frage des Nutzers am ähnlichsten sind.
  • Artikelsuche: Aus einer Sammlung von Rechercheartikeln werden diejenigen Artikel zurückgegeben, deren Titel am ehesten Bezug zur Nutzerfrage haben.
  • Bildsuche: Aus einem Satz von Bildern mit Bildtiteln werden die Bilder zurückgegeben, deren Bildtitel der Beschreibung des Nutzers am ähnlichsten sind.

Ein simpler Ansatz an die Ähnlichkeitssuche wäre es, die Dokumente einfach nur danach zu ranken, wie viele Wörter mit den Begriffen in der Abfrage übereinstimmen. Es kann aber durchaus sein, dass ein Dokument einer Abfrage ähnlich ist, ohne dass sich die verwendeten Begriffe allzu sehr decken. Für ein besseres Ergebnis von Ähnlichkeitssuchen sollten daher auch der syntaktische und der semantische Inhalt berücksichtigt werden.

Die Computerlinguistik (CL) hat mit der sogenannten „Texteinbettung“ ein Verfahren entwickelt, das Wörter und Sätze als numerische Vektoren codiert. Diese Vektordarstellungen erfassen den linguistischen Inhalt des Textes und können dann dazu verwendet werden, die Ähnlichkeit zwischen einer Abfrage und einem Dokument zu bewerten.

In diesem Blogpost geht es darum, wie Texteinbettungen und der Elasticsearch-Feldtyp dense_vector für die Ähnlichkeitssuche verwendet werden können. Zunächst geben wir einen Überblick über die Einbettungsverfahren und dann sehen wir uns einen einfachen Prototyp der Ähnlichkeitssuche mit Elasticsearch an.

Hinweis: Die Verwendung von Texteinbettungen in der Suche ist durchaus komplex und entwickelt sich ständig weiter. Wir hoffen, dass wir Ihnen mit diesem Blogpost ein paar Anregungen geben können, wie Sie mit der weiteren Untersuchung fortfahren können, ohne Ihnen eine bestimmte Sucharchitektur oder ‑implementierung zu empfehlen.

Was sind Texteinbettungen?

Lassen Sie uns als Erstes einen Blick auf die verschiedenen Arten von Texteinbettungen werfen und sie mit herkömmlichen Suchansätzen vergleichen.

Worteinbettungen

In einem Worteinbettungs-Modell wird ein Wort als ein dichter numerischer Vektor dargestellt. Ziel dieser Vektoren ist es, die semantischen Eigenschaften des Wortes zu erfassen – es wird davon ausgegangen, dass Wörter mit nahe beieinanderliegenden Vektoren Ähnlichkeiten hinsichtlich ihrer semantischen Bedeutung aufweisen. In einer guten Einbettung sind die Richtungen im Vektorraum an unterschiedliche Aspekte der Bedeutung des Wortes gebunden. So könnte beispielsweise der Vektor für „Kanada“ in der einen Richtung nahe bei „Frankreich“ und in der anderen Richtung nahe bei „Toronto“ liegen.

Das Interesse von Computerlinguisten und Suchspezialisten an der Darstellung von Wörtern in Form von Vektoren ist kein ganz neues Phänomen. In den letzten Jahren hat sich dieses Interesse – auch im Zuge der Neubetrachtung vieler herkömmlicher Aufgaben unter Zuhilfenahme neuronaler Netze – sogar verstärkt. Es wurden einige erfolgreiche Worteinbettungsalgorithmen entwickelt, wie z. B. word2vec und GloVe. Dabei wurde anhand großer Textsammlungen der Kontext untersucht, in dem jedes einzelne Wort erscheint, um so die entsprechende Vektordarstellung zu erhalten:

  • Das word2vec-Skip-Gram-Modell trainiert ein neuronales Netz, um die Kontextwörter vorhersagen zu können, die im Satz um das Wort herum erscheinen. Die Worteinbettungen ergeben sich aus den internen Gewichtungen, die das Netzwerk den einzelnen Wörtern zuordnet.
  • Bei GloVe hängt die Ähnlichkeit der Wörter davon ab, wie häufig sie mit anderen Kontextwörtern zusammen erscheinen. Dabei kommt ein einfaches lineares Modell zum Einsatz, das darauf basiert, wie oft ein Wort mit anderen Wörtern zusammen auftritt („Kookkurrenz“).

Viele Forschungsgruppen verteilen Modelle, die anhand eines großen Textkorpus wie Wikipedia oder Common Crawl vortrainiert wurden; sie können bequem heruntergeladen und in Downstream-Aufgaben integriert werden. Wenngleich vortrainierte Versionen häufig unmittelbar so eingesetzt werden, wie sie sind, kann es sich doch auszahlen, das Modell an den konkreten Zieldatensatz und die Aufgabe anzupassen. Dies lässt sich häufig durch ein wenig Feinjustierung am vortrainierten Modell erreichen.

Es hat sich gezeigt, dass Worteinbettungen recht robust und effektiv sind, sodass heute bei CL-Aufgaben, wie der maschinellen Übersetzung und der Sentimentklassifizierung, statt mit einzelnen Tokens eher mit Einbettungen gearbeitet wird.

Satzeinbettungen

In letzter Zeit ist zu beobachten, dass sich der Fokus bei der Entwicklung von Einbettungsverfahren von der Einbettung einzelner Wörter hin zur Einbettung längerer Textabschnitte verschiebt. Die neuesten Methodiken basieren auf neuronalen Netzen und beziehen beim Modelltraining mitunter gelabelte Daten mit ein, um die Erfassung semantischer Informationen zu verbessern.

Nach dem Training sind die Modelle in der Lage, aus einem Satz einen Vektor für jedes Wort in dessen Kontext sowie einen Vektor für den gesamten Satz bereitzustellen. Wie auch bei der Worteinbettung stehen für viele Modelle vortrainierte Versionen zur Verfügung, um den Nutzern den aufwendigen Trainingsprozess zu ersparen. Während der Trainingsprozess sehr ressourcenintensiv sein kann, geht das Aufrufen des Modells deutlich leichter von der Hand – Satzeinbettungsmodelle sind in der Regel so schnell, dass sie im Rahmen von Echtzeitanwendungen eingesetzt werden können.

Zu den gängigsten Satzeinbettungsverfahren gehören InferSent, Universal Sentence Encoder, ELMo und BERT. Die Forschung arbeitet aktiv an der Verbesserung der Wort- und Satzeinbettungen, sodass mit der Einführung weiterer starker Modelle zu rechnen ist.

Vergleich mit herkömmlichen Suchmethodiken

Bei herkömmlichen Verfahren zum Abrufen von Informationen erfolgt die Darstellung von Text als numerischem Vektor häufig, indem für jedes Wort im Wortschatz eine Dimension zugewiesen wird. Der Vektor für ein Textstück basiert dann darauf, wie häufig jeder einzelne Begriff im Wortschatz auftritt. Diese Art der Darstellung von Text wird oft als „Bag of Words“ bezeichnet, weil dabei einfach nur die Häufigkeit des Auftretens eines Wortes zählt, ohne dass die Satzstruktur berücksichtigt wird.

Zwischen Texteinbettungen und herkömmlichen Vektordarstellungen gibt es ein paar wichtige Unterschiede:

  • Die Vektoren sind dicht und mit üblicherweise 100 bis 1000 Dimensionen relativ niedrigdimensional. „Bag-of-Word“-Vektoren weisen dagegen eine geringe Dichte auf und können 50.000 Dimensionen und mehr umfassen. Bei der Modellierung der semantischen Bedeutung des Textes wird dieser durch die Einbettungsalgorithmen in einen Raum mit geringerer Dimensionalität codiert. Im Idealfall geht das Ganze so aus, dass synonyme Wörter und Wortgruppen im neuen Vektorraum sehr ähnlich aussehen.
  • Satzeinbettungen können bei der Festlegung der Vektordarstellung die Wortreihenfolge berücksichtigen. So kann die englische Wortgruppe „tune in“ in einer ganz anderen Vektordarstellung enden als die Wortgruppe „in tune“.
  • In der Praxis lassen sich Satzeinbettungen häufig nicht gut für große Textabschnitte generalisieren. Daher werden sie in der Regel nur bei Texten verwendet, die nicht länger als ein kurzer Absatz sind.

Verwendung von Einbettungen für die Ähnlichkeitssuche

Stellen Sie sich vor, wir hätten eine große Sammlung von Fragen und Antworten. Ein Nutzer stellt eine Frage und wir möchten jetzt in unserer Sammlung die passende Frage finden, um ihm eine Antwort präsentieren zu können.

Für das Abrufen ähnlicher Fragen könnten wir Texteinbettungen verwenden:

  • Beim Indexieren durchläuft jede Frage ein Satzeinbettungsmodell, das einen numerischen Vektor erzeugt.
  • Wenn der Benutzer eine Frage eingibt, durchläuft auch sie dasselbe Satzeinbettungsmodell und wir erhalten einen numerischen Vektor. Für das Ranking der Antworten berechnen wir die Vektorähnlichkeit zwischen jeder Frage und dem Abfragevektor. Beim Vergleich von Einbettungsvektoren wird üblicherweise die Kosinus-Ähnlichkeit herangezogen.

In diesem Repository findet sich ein einfaches Beispiel, wie dies in Elasticsearch erreicht werden kann. Das Haupt-Skript indexiert ca. 20.000 Fragen aus dem Satz von StackOverflow-Daten, woraufhin der Nutzer frei formulierte Textabfragen für die Suche in diesen Daten formulieren kann.

Wir werden uns gleich die einzelnen Teile des Skripts näher betrachten, möchten aber zunächst die Aufmerksamkeit auf ein paar Beispielergebnisse richten. In vielen Fällen ist die Methode in der Lage, auch dann Ähnlichkeit zu erfassen, wenn es keine allzu große Wortüberlagerung zwischen Abfrage und indexierter Frage gibt:

  • „zipping up files“ gibt „Compressing / Decompressing Folders & Files“ zurück
  • „determine if something is an IP“ gibt „How do you tell whether a string is an IP or a hostname“ zurück
  • „translate bytes to doubles“ gibt „Convert Bytes to Floating Point Numbers in Python“ zurück

Details zur Implementierung

Das Skript beginnt mit dem Herunterladen und Erstellen des Einbettungsmodells in TensorFlow. Wir haben uns für den Universal Sentence Encoder von Google entschieden, aber es können auch viele andere Einbettungsmethoden verwendet werden. Das Skript nutzt das Einbettungsmodell so wie es ist, also ohne zusätzliches Training oder irgendwelche Justierungen.

Als Nächstes wird der Elasticsearch-Index erstellt, der Mappings für den Titel der Frage, Tags und den als Vektor codierten Titel der Frage enthält:

"mappings": {
  "properties": {
    "title": {
      "type": "text"
    },
    "title_vector": {
      "type": "dense_vector",
      "dims": 512
    }
    "tags": {
      "type": "keyword"
    },
    ...
  }
}

Im Mapping für dense_vector müssen wir die Zahl der Dimensionen angeben, die die Vektoren enthalten werden. Wenn das Feld title_vector indexiert wird, prüft Elasticsearch, ob es dieselbe Zahl von Dimensionen hat, die auch im Mapping angegeben ist.

Zum Indexieren von Dokumenten lassen wir den Titel der Frage durch das Einbettungsmodell laufen, um ein numerisches Array zu erhalten. Dieses Array wird dem Dokument im Feld title_vector hinzugefügt.

Sobald ein Nutzer eine Abfrage eingibt, wird deren Text durch dasselbe Einbettungsmodell laufen gelassen und das Ergebnis wird im Parameter query_vector gespeichert. In Version 7.3 stellt Elasticsearch in seiner nativen Skripting-Sprache eine Kosinus-Ähnlichkeits-Funktion (cosineSimilarity) bereit. Um also Fragen anhand ihrer Ähnlichkeit mit der Nutzerfrage zu ranken, greifen wir auf eine script_score-Abfrage zurück:

{
  "script_score": {
    "query": {"match_all": {}},
    "script": {
      "source": "cosineSimilarity(params.query_vector, doc['title_vector']) + 1.0",
      "params": {"query_vector": query_vector}
    }
  }
}

Wir achten darauf, dass der Abfragevektor als Skriptparameter übergeben wird, damit das Skript nicht bei jeder neuen Abfrage neu kompiliert werden muss. Da Elasticsearch keine negativen Scores erlaubt, ist es notwendig, der Kosinus-Ähnlichkeit einen solchen hinzuzufügen.

Hinweis: Ursprünglich wurde in diesem Blogpost eine andere Syntax für Vektorfunktionen verwendet, welche in Elasticsearch 7.3 aktuell war, die aber seit der Veröffentlichung von Version 7.6 nicht mehr verfügbar ist.

Wichtige Einschränkungen

Die script_score-Abfrage dient als Hülle für eine „innere“ Abfrage und modifiziert nur die Scores der Dokumente, die von der inneren Abfrage zurückgegeben werden. Wir haben aber eine match_all-Abfrage bereitgestellt, sodass das Skript alle Dokumente im Index berücksichtigt. Dies ist eine derzeit noch existierende Einschränkung für die Vektorähnlichkeit in Elasticsearch: Vektoren können zwar zum Scoring von Dokumenten verwendet werden, aber nicht beim ersten Abrufen. Die Unterstützung für das Abrufen auf der Basis der Vektorähnlichkeit gehört zu den Themen, an denen wir arbeiten.

Um das zeitaufwendige Durchsuchen aller Dokumente zu vermeiden, kann die match_all-Abfrage durch eine selektivere Abfrage ersetzt werden. Welche Abfrage geeignet ist, hängt wahrscheinlich vom jeweiligen Anwendungsfall ab.

Dass wir oben ein paar ermutigende Beispiele gesehen haben, darf nicht darüber hinwegtäuschen, dass die Ergebnisse auch sehr verrauscht und unintuitiv sein können. So werden bei „zipping up files“ auch „Partial .csproj Files“ und „How to avoid .pyc files?“ mit hohen Scores bedacht. Und wenn die Methode unerwartete Ergebnisse erbringt, ist nicht immer offensichtlich, wie sich das Problem beheben lässt – es kommt oft vor, dass die Bedeutung einer Vektorkomponente nicht klar ist und zu keinem interpretierbaren Konzept passt. Bei herkömmlichen Scoring-Verfahren, die auf Wortübereinstimmungen basieren, liegt die Antwort auf die Frage, warum das Dokument so ein hohes Ranking erhalten hat, oft eher auf der Hand.

Wie bereits erwähnt, soll dieser Prototyp beispielhaft aufzeigen, wie Einbettungsmodelle mit Vektorfeldern zusammen verwendet werden können; er ist nicht als für den Einsatz in einer Produktionsumgebung bereite Lösung gedacht. Bei der Entwicklung einer neuen Suchstrategie ist es enorm wichtig zu testen, wie sich der Ansatz auf Ihre eigenen Daten auswirkt. Dabei sollte eine robuste Referenz, zum Beispiel eine match-Abfrage verwendet werden. Um zu einem erfolgreichen Ergebnis zu gelangen, bedarf es mitunter größerer Änderungen an der Strategie, wie Feinjustierungen des Einbettungsmodells für die Zieldaten oder Experimenten mit unterschiedlichen Methoden zur Einbeziehung von Einbettungen (Abfrageerweiterung auf Wortebene usw.).

Fazit

Einbettungsverfahren stellen eine leistungsfähige Möglichkeit dar, die linguistischen Inhalte von Texten zu erfassen. Durch das Indexieren von Einbettungen und das Scoring anhand des Abstands der Vektoren zueinander können wir Dokumente nach ihrer Ähnlichkeit ranken, ohne uns nur an die Übereinstimmungen auf Wortebene halten zu müssen.

Wir arbeiten daran, in Zukunft weitere Funktionen rund um den Vektorfeldtyp einzuführen. Die Nutzung von Vektoren für die Suche ist ein wichtiger Bereich mit vielen Nuancen und Facetten. Wie stets würden wir uns freuen, bei Github und in den Diskussionsforen von Ihren Anwendungsfällen und Ihren Erfahrungen zu hören.