Besseres Feature Engineering mit Spark, Solr und Lucene Analyzers

In diesem Blogbeitrag geht es um neue Funktionen im Lucidworks spark-solr Open Source Toolkit. Eine Einführung in das spark-solr Projekt…

In diesem Blogbeitrag geht es um neue Funktionen im Lucidworks spark-solr Open Source Toolkit. Eine Einführung in das spark-solr Projekt finden Sie unter Solr als Apache Spark SQL DataSource

Durchführen von Textanalysen in Spark

Das Open-Source-Toolkit spark-solr von Lucidworks enthält jetzt Tools, mit denen Sie den gesamten Text mithilfe des Textanalyse-Frameworks von Lucene in Wörter, so genannte Token, zerlegen können. Die Lucene-Textanalyse wird von Solr im Verborgenen verwendet, wenn Sie Dokumente indizieren, um Suche, Facettierung, Sortierung usw. zu ermöglichen. Aber die Textanalyse außerhalb von Solr kann Prozesse vorantreiben, die nicht direkt in Suchindizes einfließen, wie z.B. die Erstellung von Modellen für maschinelles Lernen. Darüber hinaus kann die Extra-Solr-Analyse es ermöglichen, teure Textanalyseprozesse getrennt von der Indizierung der Dokumente in Solr zu skalieren.

Lucene Textanalyse, über LuceneTextAnalyzer

Das Lucene-Textanalyse-Framework, eine Java-API, kann direkt in Code verwendet werden, den Sie auf Spark ausführen, aber der Prozess der Erstellung einer Analyse-Pipeline und deren Verwendung zur Extraktion von Token kann ziemlich komplex sein. Das spark-solr LuceneTextAnalyzer Klasse zielt darauf ab, den Zugriff auf diese API über eine optimierte Schnittstelle zu vereinfachen. Alle Methoden von analyze*() erzeugen nur Text-Token, d.h. es werden keine der mit den Token verbundenen Metadaten (so genannte „Attribute“) ausgegeben, die vom Lucene-Analyse-Framework erzeugt werden: Inkrement und Länge der Token-Position, Offset für Anfangs- und Endzeichen, Token-Typ usw. Wenn diese für Ihren Anwendungsfall wichtig sind, lesen Sie den Abschnitt „Extra-Solr Textanalyse“ weiter unten.

LuceneTextAnalyzer verwendet ein reduziertes JSON-Schema mit zwei Abschnitten: Der Abschnitt analyzers konfiguriert eine oder mehrere benannte Analysepipelines und der Abschnitt fields ordnet Feldnamen den Analysatoren zu. Wir haben uns dafür entschieden, ein vom Solr-Schema getrenntes Schema zu definieren, da viele der Schemafunktionen von Solr außerhalb eines Suchkontextes nicht anwendbar sind, z.B.: getrennte Indizierung und Abfrageanalyse, Ähnlichkeit von Abfrage zu Dokument, Nicht-Text-Felder, Spezifikation von indizierten/gespeicherten/Doc-Werten usw.

Die Lucene-Textanalyse besteht aus drei aufeinander folgenden Phasen: Zeichenfilterung – Änderung des gesamten Textes; Tokenisierung, bei der der resultierende Text in Token aufgeteilt wird; und Tokenfilterung – Änderung/Ergänzung/Entfernung der erzeugten Token.

Hier sehen Sie das Skelett eines Schemas mit zwei definierten Analysepipelines:

{ "analyzers": [{ "name": "...",
                    "charFilters": [{ "type": "...", ...}, ... ], 
                    "tokenizer": { "type": "...", ... },
                    "filters": [{ "type": "...", ... } ... ] }] },
                { "name": "...", 
                    "charFilters": [{ "type": "...", ...}, ... ], 
                    "tokenizer": { "type": "...", ... },
                    "filters": [{ "type": "...", ... }, ... ] }] } ],
  "fields": [{"name": "...", "analyzer": "..."}, { "regex": ".+", "analyzer": "..." }, ... ] }

In jedem JSON-Objekt im Array analyzers kann es vorkommen:

  • null oder mehr Zeichenfilter, konfiguriert über ein optionales charFilters Array von JSON-Objekten;
  • genau einen Tokenizer, der über das erforderliche tokenizer JSON-Objekt konfiguriert wird; und
  • null oder mehr Token-Filter, konfiguriert durch ein optionales filters Array von JSON-Objekten.

Auf Klassen, die jede dieser drei Arten von Analysekomponenten implementieren, wird über den erforderlichen Schlüssel type in den Konfigurationsobjekten dieser Komponenten verwiesen. Der Wert dieses Schlüssels ist der SPI-Name der Klasse, bei dem die Groß- und Kleinschreibung nicht beachtet wird und der einfache Name der Klasse ohne das Suffix -CharFilterFactory, -TokenizerFactory oder -(Token)FilterFactory angegeben wird. Siehe die Javadocs für Lucene’s CharFilterFactory, TokenizerFactory und TokenFilterFactory Klassen für eine Liste von Unterklassen, deren Javadocs eine Beschreibung der Konfigurationsparameter enthalten, die als Schlüssel/Wertpaare in den JSON-Objekten der Konfiguration der Analysekomponente im Schema angegeben werden können.

Nachfolgend finden Sie ein Scala-Snippet zur Anzeige der Anzahl der 10 häufigsten Wörter aus der obersten Ebene von spark-solr README.adoc Datei, unter Verwendung von LuceneTextAnalyzer, konfiguriert mit einem Analysator bestehend aus StandardTokenizer (das die Wortumbruchregeln aus dem Unicode-Standard UAX#29 implementiert) und LowerCaseFiltereinen Filter, um die extrahierten Token zu verkleinern. Wenn Sie zu Hause mitspielen möchten: Klonen Sie den spark-solr Quellcode von Github; wechseln Sie in das Stammverzeichnis des Projekts; erstellen Sie das Projekt (über mvn -DskipTests package); starten Sie die Spark-Shell (über $SPARK_HOME/bin/spark-shell --jars target/spark-solr-2.1.0-SNAPSHOT-shaded.jar); geben Sie paste: in die Shell ein; und fügen Sie schließlich den unten stehenden Code in die Shell ein, nachdem sie // Entering paste mode (ctrl-D to finish) ausgegeben hat:

import com.lucidworks.spark.analysis.LuceneTextAnalyzer
val schema = """{ "analyzers": [{ "name": "StdTokLower",
               |                  "tokenizer": { "type": "standard" },
               |                  "filters": [{ "type": "lowercase" }] }], 
               |  "fields": [{ "regex": ".+", "analyzer": "StdTokLower" }] }
             """.stripMargin
val analyzer = new LuceneTextAnalyzer(schema)
val file = sc.textFile("README.adoc")
val counts = file.flatMap(line => analyzer.analyze("anything", line))
                 .map(word => (word, 1))
                 .reduceByKey(_ + _)
                 .sortBy(_._2, false) // descending sort by count
println(counts.take(10).map(t => s"${t._1}(${t._2})").mkString(", "))

Die obersten 10 Token(count) Tupel werden ausgedruckt:

the(158), to(103), solr(86), spark(77), a(72), in(44), you(44), of(40), for(35), from(34)

Im obigen Schema werden alle Feldnamen über die Zuordnung "regex": ".+" im Abschnitt fields auf den Analyzer StdTokLower abgebildet – deshalb wird beim Aufruf von analyzer.analyze() "anything" als Feldname verwendet.

Die Ergebnisse enthalten viele Präpositionen („to“, „in“, „of“, „for“, „from“) und Artikel („the“ und „a“) – es wäre schön, diese aus unserer Top-10-Liste auszuschließen. Lucene enthält einen Token-Filter namens StopFilter der Wörter entfernt, die mit einer schwarzen Liste übereinstimmen, und er enthält einen Standardsatz englischer Stoppwörter, der mehrere Präpositionen und Artikel enthält. Fügen wir unserem Schema einen weiteren Analyzer hinzu, der auf unserem ursprünglichen Analyzer aufbaut, indem wir StopFilter hinzufügen:

import com.lucidworks.spark.analysis.LuceneTextAnalyzer
val schema = """{ "analyzers": [{ "name": "StdTokLower",
               |                  "tokenizer": { "type": "standard" },
               |                  "filters": [{ "type": "lowercase" }] },
               |                { "name": "StdTokLowerStop",
               |                  "tokenizer": { "type": "standard" },
               |                  "filters": [{ "type": "lowercase" },
               |                              { "type": "stop" }] }], 
               |  "fields": [{ "name": "all_tokens", "analyzer": "StdTokLower" },
               |             { "name": "no_stopwords", "analyzer": "StdTokLowerStop" } ]}
             """.stripMargin
val analyzer = new LuceneTextAnalyzer(schema)
val file = sc.textFile("README.adoc")
val counts = file.flatMap(line => analyzer.analyze("no_stopwords", line))
                 .map(word => (word, 1))
                 .reduceByKey(_ + _)
                 .sortBy(_._2, false)
println(counts.take(10).map(t => s"${t._1}(${t._2})").mkString(", "))

Im obigen Schema werden nicht alle Felder dem ursprünglichen Analyzer zugeordnet, sondern nur das Feld all_tokens dem Analyzer StdTokLower und das Feld no_stopwords unserem neuen Analyzer StdTokLowerStop.

spark-shell wird gedruckt:

solr(86), spark(77), you(44), from(34), source(32), query(25), option(25), collection(24), data(20), can(19)

Wie Sie sehen können, enthält die obige Liste mehr wichtige Token aus der Datei.

Weitere Einzelheiten über das Schema finden Sie in dem kommentierten Beispiel in den LuceneTextAnalyzer scaladocs.

LuceneTextAnalyzer verfügt über mehrere andere Analysemethoden: analyzeMV(), um die Analyse von mehrwertigen Eingaben durchzuführen, und analyze(MV)Java() Komfortmethoden, die Java-freundliche Datenstrukturen akzeptieren und ausgeben. Es gibt einen überladenen Satz dieser Methoden, die eine auf Feldnamen basierende Map mit zu analysierenden Textwerten aufnehmen. Diese Methoden geben eine Map von Feldnamen zu ausgegebenen Token-Sequenzen zurück.

Extrahieren von Textmerkmalen in spark.ml Pipelines

Die spark.ml Bibliothek für maschinelles Lernen enthält eine begrenzte Anzahl von Transformatoren, die eine einfache Textanalyse ermöglichen, aber keiner unterstützt mehr als eine Eingabespalte und keiner unterstützt mehrwertige Eingabespalten.

Das spark-solr Projekt umfasst LuceneTextAnalyzerTransformerdie LuceneTextAnalyzer und das oben beschriebene Schemaformat verwendet, um Tokens aus einer oder mehreren DataFrame-Text-Spalten zu extrahieren, wobei die Analysekonfiguration jeder Eingabespalte durch das Schema festgelegt ist.

Wenn Sie kein Schema angeben (z.B. über die Methode setAnalysisSchema() ), verwendet LuceneTextAnalyzerTransformer das Standardschema (siehe unten), das alle Felder auf die gleiche Weise analysiert: StandardTokenizer gefolgt von LowerCaseFilter:

{ "analyzers": [{ "name": "StdTok_LowerCase",
                  "tokenizer": { "type": "standard" }, "filters": [{ "type": "lowercase" }] }],
  "fields": [{ "regex": ".+", "analyzer": "StdTok_LowerCase" }] }

LuceneTextAnalyzerTransformer fügt alle aus allen Eingabespalten extrahierten Token in eine einzige Ausgabespalte ein. Wenn Sie das Vokabular jeder Spalte von dem anderer Spalten unterscheiden möchten, können Sie den Token die Eingabespalte voranstellen, aus der sie stammen, z.B. word aus column1 wird column1=word – diese Option ist standardmäßig deaktiviert.

Sie können LuceneTextAnalyzerTransformer in Aktion sehen in der spark-solr MLPipelineScala Beispiel, das zeigt, wie man mit LuceneTextAnalyzerTransformer Textmerkmale extrahiert, um ein Klassifizierungsmodell zu erstellen, das anhand des Textes eines Artikels vorhersagt, in welcher Newsgroup dieser gepostet wurde. Wenn Sie dieses Beispiel ausführen möchten, das die Indizierung der 20 Newsgroups-Daten in einer Solr-Cloud-Sammlung erwartet, folgen Sie den Anweisungen in der scaladoc des NewsgroupsIndexer Beispiel, dann folgen Sie den Anweisungen in der scaladoc des MLPipelineScala Beispiel.

Das Beispiel MLPipelineScala erstellt einen Naive Bayes-Klassifikator, indem es eine K-fache Kreuzvalidierung mit einer Suche nach Hyperparametern durchführt. Dabei werden neben mehreren anderen Parameterwerten auch die Frage, ob den Token die Spalte, aus der sie extrahiert wurden, vorangestellt werden soll oder nicht, sowie 2 verschiedene Analyseschemata berücksichtigt:

  val WhitespaceTokSchema =
    """{ "analyzers": [{ "name": "ws_tok", "tokenizer": { "type": "whitespace" } }],
      |  "fields": [{ "regex": ".+", "analyzer": "ws_tok" }] }""".stripMargin
  val StdTokLowerSchema =
    """{ "analyzers": [{ "name": "std_tok_lower", "tokenizer": { "type": "standard" },
      |                  "filters": [{ "type": "lowercase" }] }],
      |  "fields": [{ "regex": ".+", "analyzer": "std_tok_lower" }] }""".stripMargin
[...]
  val analyzer = new LuceneTextAnalyzerTransformer().setInputCols(contentFields).setOutputCol(WordsCol)
[...]
  val paramGridBuilder = new ParamGridBuilder()
    .addGrid(hashingTF.numFeatures, Array(1000, 5000))
    .addGrid(analyzer.analysisSchema, Array(WhitespaceTokSchema, StdTokLowerSchema))
    .addGrid(analyzer.prefixTokensWithInputCol)

Wenn ich MLPipelineScala ausführe, besagt die folgende Protokollausgabe, dass der std_tok_lower Analyzer den ws_tok Analyzer übertrifft und dass das Voranstellen der Eingabespalte an Token besser funktioniert:

2016-04-08 18:17:38,106 [main] INFO  CrossValidator  - Best set of parameters:
{
	LuceneAnalyzer_9dc1a9c71e1f-analysisSchema: { "analyzers": [{ "name": "std_tok_lower", "tokenizer": { "type": "standard" },
                  "filters": [{ "type": "lowercase" }] }],
  "fields": [{ "regex": ".+", "analyzer": "std_tok_lower" }] },
	hashingTF_f24bc3f814bc-numFeatures: 5000,
	LuceneAnalyzer_9dc1a9c71e1f-prefixTokensWithInputCol: false,
	nb_1a5d9df2b638-smoothing: 0.5
}

Extra-Solr Textanalyse

Solr’s PreAnalyzedField Feldtyp ermöglicht es, die Ergebnisse einer außerhalb von Solr durchgeführten Textanalyse zu übergeben und so zu indizieren/zu speichern, als ob die Analyse in Solr durchgeführt worden wäre.

Zum Zeitpunkt der Erstellung dieses Artikels hängt das spark-solr Projekt von Solr 5.4.1 ab. Vor Solr 5.5.0 wurde die Abfrage von Feldern des Typs PreAnalyzedField nicht vollständig unterstützt – siehe Solr JIRA issue SOLR-4619 für weitere Informationen.

Es gibt einen Zweig im spark-solr Projekt, der noch nicht an Master übergeben oder freigegeben wurde, der die Fähigkeit hinzufügt, JSON zu erzeugen, das von Solrs PreAnalyzedField geparst, dann indiziert und optional gespeichert werden kann.

Nachfolgend finden Sie ein Scala-Snippet zur Erzeugung von voranalysiertem JSON für einen kleinen Textabschnitt unter Verwendung von LuceneTextAnalyzer, konfiguriert mit einem Analyzer bestehend aus StandardTokenizer+LowerCaseFilter. Wenn Sie dies zu Hause ausprobieren möchten: Klonen Sie den spark-solr-Quellcode von Github; wechseln Sie in das Stammverzeichnis des Projekts; checken Sie den Zweig aus (über git checkout SPAR-14-LuceneTextAnalyzer-PreAnalyzedField-JSON); bauen Sie das Projekt (über mvn -DskipTests package); starten Sie die Spark-Shell (über $SPARK_HOME/bin/spark-shell --jars target/spark-solr-2.1.0-SNAPSHOT-shaded.jar); geben Sie paste: in die Shell ein; und fügen Sie schließlich den unten stehenden Code in die Shell ein, nachdem sie // Entering paste mode (ctrl-D to finish) ausgegeben hat:

import com.lucidworks.spark.analysis.LuceneTextAnalyzer
val schema = """{ "analyzers": [{ "name": "StdTokLower",
               |                  "tokenizer": { "type": "standard" },
               |                  "filters": [{ "type": "lowercase" }] }], 
               |  "fields": [{ "regex": ".+", "analyzer": "StdTokLower" }] }
             """.stripMargin
val analyzer = new LuceneTextAnalyzer(schema)
val text = "Ignorance extends Bliss."
val fieldName = "myfield"
println(analyzer.toPreAnalyzedJson(fieldName, text, stored = true))

Es wird Folgendes ausgegeben (Leerzeichen hinzugefügt):

{"v":"1","str":"Ignorance extends Bliss.","tokens":[
  {"t":"ignorance","s":0,"e":9,"i":1},
  {"t":"extends","s":10,"e":17,"i":1},
  {"t":"bliss","s":18,"e":23,"i":1}]}

Wenn wir den Wert der Option stored zu false machen, dann wird der Schlüssel str mit dem ursprünglichen Text als Wert nicht in die JSON-Ausgabe aufgenommen.

Zusammenfassung

LuceneTextAnalyzer vereinfacht die Lucene-Textanalyse und ermöglicht die Verwendung des PreAnalyzedField von Solr. LuceneTextAnalyzerTransformer ermöglicht eine bessere Extraktion von Textmerkmalen durch die Nutzung der Lucene-Textanalyse.

You Might Also Like

B2B-KI-Benchmarkstudie 2025: Was wir in den Schützengräben sehen

Laden Sie die B2B-KI-Benchmark-Highlights 2025 von Lucidworks herunter. Sehen Sie sich die...

Read More

Vom Suchunternehmen zum praktischen KI-Pionier: Unsere Vision für 2025 und darüber hinaus

CEO Mike Sinoway gibt Einblicke in die Zukunft der KI und stellt...

Read More

Wenn KI schief geht: Fehlschläge in der realen Welt und wie man sie vermeidet

Lassen Sie nicht zu, dass Ihr KI-Chatbot einen 50.000 Dollar teuren Tahoe...

Read More

Quick Links