In diesem Artikel wird erläutert, wie wir den von Yandex entwickelten Tomita-Parser in unser System integriert, in eine dynamische Bibliothek umgewandelt, uns mit Java angefreundet, Multithreading durchgeführt und damit das Problem der Textklassifizierung für die Immobilienbewertung gelöst haben.

Erklärung des Problems
Bei der Immobilienbewertung ist die Analyse von Verkaufsankündigungen von großer Bedeutung. Aus der Ankündigung können Sie alle notwendigen Informationen über die Immobilie erhalten, einschließlich Informationen über den Zustand der Reparatur in der Wohnung. Normalerweise sind diese Informationen im Anzeigentext enthalten. Dies ist bei der Beurteilung sehr wichtig, da eine gute Reparatur den Quadratmeterpreis um mehrere Tausend erhöhen kann.
Wir haben also einen Anzeigentext, der je nach Reparaturzustand in der Wohnung in eine der Kategorien eingeteilt werden muss (unvollendet, fair, durchschnittlich, gut, ausgezeichnet, exklusiv). Bei Reparaturen kann eine Anzeige ein oder zwei Sätze, ein paar Wörter oder nichts enthalten. Daher ist es nicht sinnvoll, den Text vollständig zu klassifizieren. Aufgrund der Spezifität des Textes und der begrenzten Anzahl von Wörtern im Zusammenhang mit dem Reparaturkontext bestand die einzig sinnvolle Lösung darin, alle erforderlichen Informationen aus dem Text zu extrahieren und zu klassifizieren.

Jetzt müssen wir lernen, wie wir aus dem Text alle Fakten über den Zustand der Dekoration extrahieren können. Insbesondere, was direkt mit der Reparatur zusammenhängt, sowie alles, was indirekt über den Zustand der Wohnung sprechen kann - das Vorhandensein von abgehängten Decken, Einbaugeräten, Kunststofffenstern, einem Whirlpool, die Verwendung teurer Veredelungsmaterialien usw.
In diesem Fall müssen wir nur Informationen über Reparaturen in der Wohnung selbst extrahieren, da uns der Zustand der Eingänge, Keller und Dachböden nicht interessiert. Es ist auch notwendig zu berücksichtigen, dass der Text in natürlicher Sprache mit all seinen inhärenten Fehlern, Tippfehlern, Abkürzungen und anderen Merkmalen geschrieben ist. Ich persönlich habe drei Schreibweisen der Wörter „Linoleum“ und „Laminat“ und fünf Schreibweisen des Wortes „endgültig“ gefunden. Einige Leute verstehen nicht, warum Leerzeichen zwischen Wörtern benötigt werden, während andere nichts von Kommas gehört haben. Daher wurde der Parser mit kontextfrei freien Grammatiken zur einfachsten und vernünftigsten Lösung.
Als die Entscheidung getroffen wurde, wurde eine zweite große und interessante Aufgabe gebildet - zu lernen, wie alle ausreichenden und notwendigen Informationen über die Reparatur aus der Ankündigung extrahiert werden können, nämlich eine schnelle syntaktische und morphologische Analyse des Textes bereitzustellen, die im Bibliotheksmodus unter Last parallel arbeiten kann.
Fahren Sie mit der Lösung fort
Von den verfügbaren Mitteln zum Extrahieren von Fakten aus Text basierend auf kontextfreien Grammatiken, die mit der russischen Sprache arbeiten können, wurde unsere Aufmerksamkeit auf Tomita-Parser und die Yagry-Bibliothek in Python gelenkt. Yagry wurde sofort abgelehnt, da es vollständig in Python geschrieben und kaum gut optimiert ist. Und Tomita sah anfangs sehr attraktiv aus: Sie hatte eine detaillierte Dokumentation für den Entwickler und viele Beispiele, C ++ versprach eine akzeptable Geschwindigkeit. Es war nicht schwer, die Regeln für das Schreiben von Grammatiken zu verstehen, und die erste Version des Klassifikators mit seiner Verwendung war bereits am nächsten Tag fertig.
Beispiele für Regeln aus unserer Grammatik, die Adjektive und Verben im Zusammenhang mit dem Reparaturkontext extrahieren:
RepairW -> "" | "" | ""; StopWords -> "" | "" | "" | ""; Repair -> RepairW<gnc-agr[1]> Adj<gnc-agr[1]>+ interp (Repair.AdjGroup {weight = 0.5}); Repair -> Verb<gnc-agr[1]> Adj<gnc-agr[1]>* interp (Repair.Verb) RepairW<gnc-agr[1]> {weight = 0.5};
Regeln, mit denen sichergestellt wird, dass keine Informationen zum Status öffentlicher Räume abgerufen werden:
Repair -> StopWords Verb* Prep* Adj* RepairW; Repair -> Adj+ RepairW Prep* StopWords;
Standardmäßig ist das Gewicht der Regel 1, wobei der Regel ein kleineres Gewicht zugewiesen wird. Wir legen die Reihenfolge ihrer Ausführung fest.
Es war ein wenig peinlich, dass nur die Konsolenanwendung und eine Menge C ++ - Code in die Öffentlichkeit hochgeladen wurden. Der zweifelsfreie Vorteil war jedoch die einfache Bedienung und die schnellen Ergebnisse bei Experimenten. Daher wurde beschlossen, über die möglichen Schwierigkeiten nachzudenken, es näher an der Implementierung selbst in unser System einzuführen.
Fast sofort war es möglich, nahezu alle notwendigen Informationen über die Reparatur in hoher Qualität zu extrahieren. "Fast", weil anfangs einige Wörter unter keinen Bedingungen und Grammatiken extrahiert wurden. Es war jedoch schwierig, das Ausmaß dieses Problems sofort einzuschätzen, inwieweit es die Qualität der Lösung des gesamten Klassifizierungsproblems beeinflussen kann.
Nachdem wir sichergestellt hatten, dass Tomita uns in erster Näherung die erforderlichen Funktionen zur Verfügung stellt, stellten wir fest, dass es keine Option ist, sie als Konsolenanwendung zu verwenden: Erstens erwies sich die Konsolenanwendung aus unbekannten Gründen als instabil und stürzte von Zeit zu Zeit ab, und zweitens bot sie keine die erforderliche Parsing-Last von mehreren Millionen Anzeigen pro Tag. So wurde definitiv klar, woraus man eine Bibliothek machen sollte.
Wie wir Tomitha zu einer Multithread-Bibliothek gemacht und uns mit Java angefreundet haben
Unser System ist in Java geschrieben, Tomita-Parser in C ++. Wir mussten in der Lage sein, Parsing-Anzeigentext von Java aus aufzurufen.
Die Entwicklung von Java-Bindungen für Tomita-Parser kann bedingt in zwei Komponenten unterteilt werden - die Implementierung der Möglichkeit, Tomita als gemeinsam genutzte Bibliothek zu verwenden und tatsächlich eine Integrationsschicht mit jvm zu schreiben. Die Hauptschwierigkeit betraf den ersten Teil. Tomita selbst war ursprünglich für die Ausführung in einem separaten Prozess konzipiert. Daraus folgte, dass die Haupthindernisse für die Verwendung des Parsers im Bewerbungsprozess zwei Faktoren waren.
- Der Datenaustausch wurde über verschiedene Arten von E / A durchgeführt. Es war erforderlich, die Fähigkeit zum Datenaustausch mit dem Parser über den Speicher zu implementieren. Darüber hinaus war es notwendig, dies so zu tun, dass der Code des Parsers selbst minimal beeinflusst wurde. Die Architektur von Tomita schlug eine Möglichkeit vor, das Lesen von Eingabedokumenten aus dem Speicher als Implementierung der Schnittstellen CDocStreamBase und CDocListRetrieverBase zu implementieren. Bei der Ausgabe war es schwieriger - ich musste den Code des XML-Generators berühren.
- Der zweite Faktor, der sich aus dem Prinzip „ein Parser - ein Prozess“ ergibt, ist der globale Status, der aus verschiedenen Instanzen des Parsers modifiziert wurde. Wenn Sie sich die Datei src / util / generic / singleton.h ansehen , sehen Sie den Mechanismus für die Verwendung des freigegebenen Status. Es ist leicht vorstellbar, dass bei Verwendung von zwei Parser-Instanzen im selben Adressraum eine Race-Bedingung auftritt. Um nicht den gesamten Parser neu zu schreiben, wurde beschlossen, diese Klasse zu ändern und den globalen Status durch einen lokalen Status relativ zum Thread (thread_local) zu ersetzen. Dementsprechend setzen wir diese thread_local-Variablen vor jedem Parser-Aufruf im JTextMiner-Wrapper auf die aktuelle Parser-Instanz. Danach arbeitet der Parser-Code mit den Adressen der aktuellen Parser-Instanz.
Nachdem diese beiden Faktoren beseitigt wurden, konnte der Parser in jeder Umgebung als gemeinsam genutzte Bibliothek verwendet werden. Das Schreiben von JNI-Bindemitteln und eines Java-Wrappers war nicht länger schwierig.
Der Tomita-Parser muss vor der Verwendung konfiguriert werden. Die Konfigurationsparameter ähneln denen, die beim Aufrufen des Konsolendienstprogramms verwendet werden. Das Parsen selbst besteht darin, die parse () -Methode aufzurufen, die Dokumente zum Parsen empfängt und XML als Zeichenfolge mit den Ergebnissen des Parsers zurückgibt.
Die Multithread-Version von Tomita - TomitaPooledParser verwendet zum Parsen eines Pools von TomitaParser-Objekten, die auf die gleiche Weise konfiguriert sind. Zum Parsen wird der erste freie Parser verwendet. Da die Anzahl der erstellten Parser der Anzahl der Threads im Pool entspricht, ist immer mindestens ein Parser für die Aufgabe verfügbar. Die Analysemethode analysiert asynchron die bereitgestellten Dokumente im ersten freien Parser.
Ein Beispiel für den Aufruf der Tomita-Bibliothek von Java aus:
tomitaPooledParser = new TomitaPooledParser(threadAmount, new File(configDirname), new String[]{tomitaConfigFilename}); Future<String> result = tomitaPooledParser.parse(documents); String response = result.get();
Als Antwort eine XML-Zeichenfolge mit dem Ergebnis der Analyse.
Probleme, auf die wir gestoßen sind und wie wir sie gelöst haben
Wenn die Bibliothek fertig ist, starten wir den Dienst mit seiner Verwendung für eine große Datenmenge und erinnern uns an das Problem, einige Wörter nicht zu extrahieren, und erkennen, dass dies für unsere Aufgabe sehr wichtig ist.
Unter diesen Wörtern befanden sich "vorgefiltert" sowie "erledigt", "produziert" und andere gekürzte Partizipien. Das heißt, die Wörter, die sehr oft in der Anzeige gefunden werden, und manchmal ist dies die einzige oder sehr wichtige Information über die Reparatur. Der Grund für dieses Verhalten - das Wort „vorgefiltert“ stellte sich als Wort mit unbekannter Morphologie heraus, dh Tomita kann einfach nicht bestimmen, welcher Teil der Sprache es ist, und kann es dementsprechend nicht extrahieren. Und für gekürzte Partizipien musste ich eine separate Regel schreiben, und das Problem wurde gelöst, aber es dauerte einige Zeit, um herauszufinden, dass es sich um abgekürzte Partizipien handelt, für deren Extraktion eine spezielle Regel erforderlich ist. Und für das langmütige „endgültige“ Ende musste ich eine separate Regel für ein Wort mit einer unbekannten Morphologie schreiben.
Um Analyseprobleme mithilfe von Grammatiken zu lösen, fügen wir dem Ortsverzeichnis ein Wort mit unbekannter Morphologie hinzu:
TAuxDicArticle "adjNonExtracted" { key = "" | "-" }
Für abgekürzte Partizipien verwenden wir die grammatikalischen Eigenschaften von partcp, brev.
Und jetzt können wir die Regeln für diese Fälle schreiben:
Repair -> RepairW<gnc-agr[1]> Word<gram="partcp,brev",gnc-agr[1]> interp (Repair.AdjGroup) {weight = 0.5}; Repair -> Word<kwtype="adjNonExtracted",gnc-agr[1]> interp (Repair.AdjGroup) RepairW<gnc-agr[1]> Prep* Adj<gnc-agr[1]>+;
Und das letzte der Probleme, die wir entdeckt haben, ist ein Dienst mit Multithread-Nutzung der Tomita-Bibliothek, der myStem-Prozesse erzeugt, die nicht zerstört werden und nach einiger Zeit den gesamten Speicher füllen. Die einfachste Lösung bestand darin, die maximale und minimale Anzahl von Threads in Tomcat zu begrenzen.
Ein paar Worte zur Klassifizierung
Jetzt haben wir die Reparaturinformationen aus dem Text extrahiert. Es war nicht schwierig, es mit einem der Gradientenverstärkungsalgorithmen zu klassifizieren. Wir werden hier nicht lange auf dieses Thema verzichten, es wurde viel darüber gesagt und geschrieben, und wir haben in diesem Bereich nichts radikal Neues getan. Ich werde nur die Qualitätsindikatoren der Klassifizierung angeben, die wir bei den Tests erhalten haben:
- Genauigkeit = 95%
- F1-Punktzahl = 93%
Fazit
Der implementierte Dienst, der Tomita-Parser im Bibliotheksmodus verwendet, arbeitet derzeit kontinuierlich und analysiert und klassifiziert mehrere Millionen Anzeigen pro Tag.
PS
Der gesamte Tomita-Code , den wir im Rahmen dieses Projekts geschrieben haben, wird auf den Github hochgeladen. Ich hoffe, dass dies für jemanden nützlich ist, und diese Person wird ein wenig Zeit für etwas noch Nützlicheres sparen.