DIY: Wie man ein KI-gestütztes Suchergebnis-Ranking-System aufbaut
Content-Management-Systeme enthalten große Mengen an unterschiedlichen Informationen. Diese reichen von der technischen Konfiguration über Webseitentext bis hin zu visuellen Medien - alles an einem Ort. Sich in diesem Labyrinth von Informationen zurechtzufinden, kann eine Herausforderung sein, besonders wenn man es proaktiv tut. Es kostet viel Mühe, sich daran zu erinnern, auf welcher der Hunderte von Seiten genau der eine Satz vergraben ist, den wir zu ändern versuchen. Oder war es tatsächlich eine Seite, oder vielleicht die Beschriftung eines Bildes?
Um allen das Leben ein wenig zu erleichtern, haben wir einen einfacheren Ansatz für die Navigation in einem CMS entwickelt: Live-Suche mit Vorauswahl. Die Suchleiste ist ein zentraler Bestandteil der neuen Benutzeroberfläche in Magnolia 6. Die zugrundeliegende Engine namens Periscope kennt alle Inhalte und Konfigurationen, die durchsuchbar sind. Geben Sie ein Schlüsselwort ein, und die Suchfunktion listet sofort die passenden Elemente auf, selbst in den entlegensten Winkeln Ihres digitalen Lagers.
Idee
Ein zentrales, allwissendes Suchwerkzeug ist nur der Anfang. Die Produktivitätsgewinne, die es den Nutzern bringt, können noch weiter gesteigert werden, indem die Art und Weise, wie es die Suchergebnisse einstuft, verbessert wird, so dass die passenden Informationen dem Nutzer immer näher gebracht werden. Dieses Ziel der kontinuierlichen Verbesserung der Suchergebnisse hat uns dazu bewogen, ein intelligentes Ranking-System für das Periscope zu entwickeln.
Adaptives Ranking
Bei großen Mengen von Inhalten wird nicht alles gleich häufig genutzt. Tatsächlich suchen die Menschen oft immer wieder nach denselben Dingen. Die naheliegendste Möglichkeit, die am häufigsten gesuchten Begriffe zu ermitteln, ist die Beobachtung echter Nutzer, idealerweise derjenigen, für die wir optimieren wollen. Daher wäre es wünschenswert, dass das Suchwerkzeug aus den von den Nutzern ausgewählten Ergebnissen lernt und diese näher an sie heranrückt.
Rang aufgrund von Wortfragmenten
Ein naheliegender Ansatz für ein adaptives Ergebnis-Ranking wäre es, einfach zu zählen, wie oft ein Nutzer auf welches Ergebnis geklickt hat, und die Ergebnisse auf der Grundlage der Klickzahl zu ordnen. Wir wollten jedoch noch einen Schritt weiter gehen und eine Rangliste erstellen, die auf Suchanfragen basiert und bei ähnlichen Anfragen stabil bleibt. Beim Eintippen sollte es keinen großen Unterschied machen, ob das aktuelle Wortfragment ein Zeichen mehr oder weniger hat. Wenn zum Beispiel jemand in der Vergangenheit "Cargo" eingegeben hat und oft ein bestimmtes Ergebnis ausgewählt hat, wie zum Beispiel eine Seite über "Cargo Pants", sollte das System intelligent genug sein, dieses Ergebnis auch hoch zu listen, wenn nur ein Teil des Suchbegriffs eingegeben wird, wie "carg...".
Stellen Sie sich nun das Szenario vor, dass ein Nutzer beginnt, eine Suchanfrage einzugeben. Während der Suchbegriff wächst, sollte die Suchmaschine fortlaufend Ergebnisse auflisten und diejenigen an die Spitze setzen, die angesichts der Teilinformationen zu diesem Zeitpunkt am relevantesten erscheinen. Zum Beispiel könnte "c..." "Kochen" vorschlagen, während die Eingabe von "car..." "Autofußmatten" ganz oben auflisten würde, und "carg..." schließlich "Cargo Pants".
Benutzerspezifisches Ranking
Verschiedene Nutzer können unterschiedliche Präferenzen haben. Diesem Umstand tragen wir Rechnung, indem wir mehrere Instanzen unseres Bewertungssystems verwenden, die jeweils die Eigenheiten von Nutzergruppen oder Einzelpersonen berücksichtigen. Dies ist zwar technisch nicht schwierig, spielte aber eine wichtige Rolle bei unseren Entscheidungen über die Intensität der Hardwareressourcen, wie später in diesem Beitrag erläutert wird.
Umsetzung
Um die Funktion darzustellen, die einen Suchbegriff einem entsprechenden Ergebnis-Ranking zuordnet, verwenden wir ein künstliches neuronales Netz. Genauer gesagt, bildet unser neuronales Netz eine Suchanfrage auf eine Reihe von Punkten ab, wobei jeder Punkt eines der Ergebnisse darstellt und höhere Punkte eine höhere Relevanz für diese Anfrage bedeuten.
Die Architektur des Netzes wurde so gestaltet, dass sie die oben beschriebenen gewünschten Eigenschaften so gut wie möglich unterstützt. Schauen wir uns das mal Stück für Stück an.
Zweidimensionale Eingabeschicht
Unsere Eingabeschicht hat eine zweidimensionale Form, bestehend aus 15 × 128 Einheiten. Jede Spalte steht für ein Zeichen der Eingabe, d.h. wir berücksichtigen maximal 15 Zeichen des Suchbegriffs für das Ranking, die folgenden werden abgeschnitten. Dies ist dadurch gerechtfertigt, dass die Eingabe von 15 Zeichen selten notwendig sein dürfte, um einen gesuchten Artikel zu finden. Wenn es trotzdem passiert, wird das Ranking nicht komplett abgebrochen, sondern nur auf die ersten 15 Zeichen angewendet.
Die Suchbegriffe werden zunächst in ASCII umgewandelt, indem entweder Zeichen mit diakritischen Zeichen (wie Akzente) durch ihre reinen Gegenstücke ersetzt werden, oder, wo dies nicht möglich ist, werden sie vollständig entfernt. Jedes reine ASCII-Zeichen wird dann mit Hilfe einer One-Hot-Kodierung in ein Array der Größe 128 umgewandelt. Das resultierende Array besteht größtenteils aus Nullen, mit Ausnahme des Index des ASCII-Codes des Zeichens, wo eine 1 eingefügt wird.
Anfängliche Faltung
Der erste Satz von Verbindungen nach der Eingabeschicht bildet eine Faltung mit der Kernelgröße 3 × 128, die jeweils drei Spalten zu einer Einheit in der zweiten Schicht faltet. Da jede Spalte der Eingabeschicht ein Zeichen der ursprünglichen Eingabezeichenfolge darstellt, hat diese Faltung den semantischen Effekt, dass drei benachbarte Zeichen miteinander verbunden werden, während unsere Schichten gleichzeitig abgeflacht werden. Folglich ist die zweite Schicht eindimensional und besteht aus 15 Einheiten.
Vollständig verknüpfte verborgene Schichten
Im Kern unseres Netzes haben wir zwei Schichten der Größe 200 bzw. 100 angeordnet, die jeweils vollständig mit beiden Seiten verbunden sind. Sie dienen dazu, einen gewissen "Spielraum" für die Annäherung komplexer Muster in der Beziehung zwischen der Suchanfrage auf der Eingabeseite und der Ergebnisrelevanz auf der Ausgabeseite zu schaffen.
Die Wahl der Anzahl und der Größe dieser versteckten "Brute-Force"-Schichten ist ein Kompromiss zwischen Genauigkeit, Berechnungsaufwand und Speicherbedarf. Um den "Sweet Spot" zu finden, haben wir zunächst unser gewünschtes Verhalten mit Hilfe von Unit-Tests skizziert und dann die Größe so weit wie möglich reduziert, wobei wir sichergestellt haben, dass die Ergebnisse immer noch gültig sind. Das bedeutet, dass wir eine Reihe von Szenarien mit Beispiel-Ergebnislisten haben, in denen wir simulieren, dass ein Benutzer nach bestimmten Begriffen sucht und bestimmte Ergebnisse auswählt, und dann behaupten, dass die Ergebnisse später in einer gewünschten Weise neu gemischt werden.
Ausgabeschicht und Zuordnung zu den Ergebnissen
Unsere Ausgabeschicht besteht aus n Einheiten, wobei n die maximale Anzahl von Ergebnissen ist, die von unserem Bewertungssystem gleichzeitig gespeichert werden können. Jede Ausgabeeinheit repräsentiert potenziell ein Element im gesamten (bekannten, gespeicherten) Ergebnisraum, und ihr Aktivierungswert stellt die Relevanz für das Ranking dar. Während einer Suche ordnet ein Vorwärtsdurchlauf des neuronalen Netzes jedem Ergebnis eine Punktzahl zu, so dass wir sie nach dieser Punktzahl oder, semantisch gesprochen, nach der Benutzerrelevanz sortieren können.
Training
Die Trainingsdaten werden von Nutzern gewonnen, die ein Ergebnis auswählen, nachdem sie eine Suche mit einem Suchbegriff gestartet haben. Nachdem ein Nutzer ein Ergebnis angeklickt hat, wird der aktuelle Suchbegriff für die Eingabeschicht mit Hilfe der obigen Abbildung kodiert, um später dem neuronalen Netz für einen Anpassungsschritt zugeführt zu werden. Die entsprechende Ausgabe ist ein "One-Hot"-codiertes Array des ausgewählten Ergebnisses unter allen bekannten Ergebnissen. Intuitiv gesprochen übertreiben wir die Relevanz eines bestimmten Datentupels auf einmal, um die Präferenzen des Benutzers etwas besser zu kennen. Im Laufe vieler solcher Iterationen, die durch die Suchanfragen der Benutzer ausgelöst werden, konvergiert das Netzwerk schließlich zu einem Mittelwert zwischen diesen Hinweisen und erzeugt entsprechend höhere oder niedrigere Relevanzwerte für bestimmte Ergebnisse.
The size limit problem
Die Periscope-API ist steckbar und die Ergebnislieferanten entsprechend generisch. Somit ist der Raum aller möglichen Ergebnisse de facto unbegrenzt, oder zumindest offen. Dies ist etwas problematisch, da wir eine Eins-zu-eins-Beziehung zwischen den Einheiten der Ausgabeschicht und den Ergebnissen anstreben. Künstliche neuronale Netze lassen sich in der Regel nicht so leicht in ihrer Struktur verändern - zumindest ist es nicht üblich, ein Modell zu ändern, nachdem es bereits trainiert wurde, da die Auswirkungen auf die nachfolgenden Durchläufe schwer vorherzusagen sind.
Ein weiteres Problem bei einem ständig wachsenden Pool von Ergebnissen ist der Bedarf an Ressourcen. Jede Ausgabeeinheit ist mit 100 Einheiten im untersten Hidden verbunden und hat daher 100 Gewichte, die verarbeitet und im Speicher gehalten werden müssen. Selbst bei einer relativ moderaten Nutzung einer Magnolia-Installation, die nur wenige Inhalte enthält, geht die Anzahl der vollständig angezeigten Ergebnisse leicht in die Hunderte oder Tausende. In Anbetracht der Tatsache, dass wir getrennte Ranker für verschiedene Benutzer oder Gruppen verwenden und dass eine reale Installation um ein Vielfaches mehr Inhalt haben kann, würde der Versuch, alles im Speicher zu halten, schließlich nicht nur die CPU und den Hauptspeicher belasten, sondern auch den durch Backups belegten Festplattenplatz.
Um diese Probleme zu vermeiden und den Ressourcenverbrauch des Rankings berechenbar zu halten, haben wir ein Limit für die Anzahl der gespeicherten Ergebnisse eingeführt. Sobald diese Grenze erreicht ist, wird ein altes Ergebnis zugunsten des neuen vergessen. Dies stellt uns jedoch vor zwei neue Probleme: Erstens muss entschieden werden, welches Ergebnis verworfen werden soll, und zweitens muss die entsprechende Ausgabeeinheit des neuronalen Netzes neu auf das neu hinzugefügte Ergebnis abgebildet werden und sollte alle zuvor gelernten Rangfolgen vergessen.
Dazu verwenden wir eine spezielle Form eines LRU-Puffers (Least-Recently-Use), der in der Lage ist, Indizes zu verfolgen und denselben Index für ein Element während seines gesamten Lebenszyklus in diesem Puffer zu behalten. Im Kern besteht es aus einer klassischen LRU-Map mit Ergebnissen als Schlüssel, die auf Indizes der Ausgabeschicht des neuronalen Netzes als Werte verweisen. Immer wenn der Benutzer ein Ergebnis auswählt, wird es zum neuen, zuletzt verwendeten Ergebnis im Puffer gemacht.
Eine zusätzliche Herausforderung besteht darin, dass das neuronale Netz auch von allen gelernten Verzerrungen befreit werden muss, wenn ein Ergebniseintrag im Ringpuffer ersetzt wird. Wir erreichen dies, indem wir alle Eingangsgewichte vor der entsprechenden Einheit der Ausgabeschicht zurücksetzen. Dabei ist zu beachten, dass das vorherige Training mit dem jetzt nicht mehr vorhandenen Ergebnis höchstwahrscheinlich auch die Gewichte vor den anderen versteckten Schichten beeinflusst hat. Die letzte versteckte Schicht kann jedoch als eine Merkmalsrepräsentation von Eingaben interpretiert werden, die sich über viele Trainings entwickelt hat und daher unabhängig von dem Ergebnis, das derzeit in der Ausgabeschicht repräsentiert wird, relevant und gültig sein kann, so dass hier kein Schaden angerichtet wird.
Learning rate
Unter den Hyperparametern unseres neuronalen Netzes sticht die Lernrate im Vergleich zu anderen gängigen Deep-Learning-Szenarien besonders hervor. Unsere Datenquelle für das Training ist relativ spärlich. Ein Datenpunkt wird immer dann erzeugt, wenn ein Nutzer nach der Suche ein Ergebnis auswählt. Damit das System nützlich ist, möchten wir, dass es bereits nach einer Handvoll Suchvorgängen sinnvolle Vorschläge liefert. Daher haben wir eine besonders hohe Lernrate von 0,01 gewählt.
Bei spärlichen Daten in einem Online-Lernsystem wie diesem ist die Wahl der Lernrate ein Kompromiss zwischen dem Ziel, so schnell wie möglich aussagekräftige Ergebnisse zu erhalten, und dem Ziel, nicht durch Überanpassung über das Ziel hinauszuschießen, was bedeutet, dass bereits gelernte Informationen schnell durch neue überschrieben werden und eine Generalisierung kaum stattfindet.
Persistence
Ein lernendes, personalisiertes Ranking-System wird mit der Zeit immer nützlicher, da es immer mehr Daten erhält. Daher ist es wichtig, dass der komplette Status über Benutzersitzungen und Anwendungsläufe hinweg erhalten bleibt. Ein solider, sauberer Persistenzplan macht es außerdem einfach, diesen Zustand zwischen verschiedenen Installationen zu übertragen, um Aktualisierungen und andere Aufgaben nahtloser zu gestalten.
Der vollständige Zustand unseres Bewertungssystems umfasst zwei Hauptteile: Das neuronale Netz mit seinem Modell und den trainierten Parametern und der Ergebnispuffer, der die Ausgabeschicht des Netzes kommentiert. Beide sind einfach zu serialisieren, weshalb wir hier nicht auf technische Details eingehen werden.
Allein die Anzahl der Parameter des neuronalen Netzes, vor allem die Eingangsgewichte der Einheiten, führt jedoch zu einem erheblichen Ressourcenverbrauch. Das Speichern des Netzes kann je nach Einrichtung und vorhandener Hardware bis zu mehreren Sekunden dauern. Bei intensiver Nutzung, insbesondere wenn viele Nutzer dieselbe Ranking-Instanz verwenden, kann die Häufigkeit, mit der Suchvorgänge ausgeführt und Trainingsmuster gewonnen werden, leicht höher sein als die Geschwindigkeit, mit der der Status gespeichert werden kann. Der Versuch, den Status nach jedem Trainingsschritt aufrechtzuerhalten, würde dazu führen, dass solche Aufgaben unnötig in die Warteschlange gestellt werden.
In einer langlaufenden Anwendung wie einem CMS ist es nicht notwendig, den Zustand unseres Rankings sofort nach jeder Mutation zu speichern. Es reicht aus, die Gewissheit zu haben, dass er innerhalb eines angemessenen Zeitrahmens persistiert wird. Um einige Ressourcen zu sparen und wachsende Warteschlangen zu vermeiden, begrenzen wir die Häufigkeit, mit der der Status der Rangliste gespeichert wird. Speicheranfragen werden zwar registriert, aber nicht sofort ausgeführt. Sobald eine neue Anfrage eintrifft, wird die vorherige überschrieben, da diese nun ohnehin veraltet ist. Einmal in einem bestimmten Zeitintervall, z. B. 30 Sekunden, wird die aktuelle Anfrage endgültig ausgeführt.
Personal takeaways
Als wir mit diesem Projekt begannen, war es unser erster realer Versuch mit Deep-Learning-Techniken. Obwohl wir eher Anwendungsentwickler als akademische Experten für maschinelles Lernen sind, glauben wir, dass es uns gelungen ist, eine vernünftige Lösung für ein wohl komplexes Problem zu finden. Dies ist zu einem großen Teil dem riesigen Angebot an Lernressourcen zu verdanken, das die Community für maschinelles Lernen bereitstellt. Es gibt mehrere großartige Bücher und unzählige Online-Tutorials zu diesem Thema, die oft kostenlos zur Verfügung gestellt werden. Die Einstiegshürden für Deep Learning sind niedriger denn je, und die Möglichkeiten sind enorm. Wir ermutigen jeden, der sich für das Thema interessiert, es einfach mal auszuprobieren!