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 LowerCaseFilter
einen 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 LuceneTextAnalyzerTransformer
die 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.