[RUBY] Rubin / Rost-Kooperation (6) Extraktion der Morphologie

Artikelserie

Einführung

Nach und nach lernte ich, wie Ruby und Rust zusammenarbeiten, und es wurde interessanter. Bisher ((3) - (5)) habe ich numerische Berechnungen versucht, also versuchen wir jetzt die Textverarbeitung. Okay, plötzlich werde ich Rusts morphologische Analysebibliothek verwenden, um nur die morphologischen Elemente eines bestimmten Teils des Textes zu extrahieren, wie nur die richtige Nomenklatur, alle Nomenklaturen, Adjektive und Zusätze und so weiter.

Der Autor hat ein langes und dünnes Ruby-Leben geführt, aber Rust ist ein Amateur, und die morphologische Analyse ist nur ein kleines Spiel mit MeCab in Ruby. Ich weiß nicht, wie schwierig es ist.

Politik

Lindera wird als morphologische Analysebibliothek von Rust verwendet. Dies ist eine Abzweigung von @mosuka aus einer experimentell erstellten Bibliothek namens kuromoji-rs. Es hat die Form einer Gabel, hat aber die Entwicklung unter einem anderen Namen übernommen. Weitere Informationen finden Sie im Artikel von @ mosuka unten. Rust-Anfänger übernahmen die Entwicklung des japanischen morphologischen Analysators von Rust - Qiita

Für den Mechanismus der Zusammenarbeit zwischen Ruby und Rust wird Rutie wie in (4) und (5) verwendet.

Die Rollenverteilung zwischen Ruby und Rust wird so gedacht. Erstellen Sie in Rust eine Ruby-Klasse, die auch als morphologischer Extraktor bezeichnet wird (Rutie kann in Rust eine Ruby-Klasse schreiben). Geben Sie eine Liste der Teile an, die bei der Initialisierung aufgenommen werden sollen (nehmen Sie alle Teile in der Liste auf). Wenn Sie eine Instanz eines Extraktors für morphologische Elemente erstellen und ihm Text geben, werden die entsprechenden morphologischen Elemente als Array von Zeichenfolgen zurückgegeben (in der Reihenfolge ihres Auftretens gibt es Duplikate).

Im Beispielprogramm auf der Ruby-Seite wird eine Häufigkeitstabelle aus der Liste der zurückgegebenen morphologischen Elemente erstellt und in der Reihenfolge derjenigen mit der höchsten Häufigkeit angezeigt.

Eigenschaften von Lindera

Eine Übersicht über Lindera finden Sie unter dem Link, aber hier möchte ich nur auf die folgenden Punkte hinweisen.

Ich bin dankbar, dass das Wörterbuch von Anfang an enthalten ist. Es ist etwas schmerzhaft, das Wörterbuch von irgendwoher herunterzuladen, einen Befehl einzugeben und die Datei irgendwo abzulegen, obwohl ich es gerade ausprobiere.

Darüber hinaus verfügt IPADIC über eine überwiegend unzureichende Anzahl von Wörtern, um Sätze zu verarbeiten, die durch verschiedene Medien wie SNS fliegen. Ich bin jedoch dankbar, dass ein großes Wörterbuch wie IPADIC-NEologd problemlos verwendet werden kann.

Um ein Benutzerwort hinzuzufügen, platzieren Sie einfach eine CSV-Datei und geben Sie deren Pfad an.

Motivation

In diesem Artikel möchte ich "einen Code vorstellen, der nicht praktisch ist, aber einfach genug, um sich einen Pfad zu einem praktischen Code vorzustellen", damit andere leicht darauf verweisen können.

In Ruby sind Edelsteine für die Verwendung des morphologischen Analysators MeCab und JUMAN ++ natto und jumanpp_ruby. Es gibt jeweils [^ gem].

[^ gem]: Es scheint andere zu geben, aber ich kenne sie nicht.

Warum also Code schreiben, der Rust von Ruby aus aufruft? Hintergrund hierfür ist die in Ich möchte GC vermeiden beschriebene Hypothese.

Bei Verwendung von MeCab usw. von Ruby wird für jede Morphologie eine große Menge von Zeichenfolgendaten auf die Ruby-Seite gebracht. Die meisten von ihnen werden zu Müll, und wenn sie sich zu einem gewissen Grad ansammeln, werden sie zum Ziel der Müllabfuhr. Es scheint, dass es ineffizient ist [^ ef].

[^ ef]: Ohne richtiges Experimentieren kann nicht gesagt werden, ob es wirklich ineffizient ist und wie viel Text behandelt werden sollte, um die Leistung zu beeinträchtigen.

Für Aufgaben wie die Mittags-Extraktion wäre es effizient, wenn nur die Nomenklatur auf der Rost-Seite extrahiert und nur die von der Ruby-Seite gewünschten Zeichenketten zurückgegeben würden. Die Speicherbereinigung erfolgt nicht auf der Rostseite. Variablen, die außerhalb des Gültigkeitsbereichs liegen, verschwinden in diesem Moment.

Implementierung: Rostseite

Bis zur Bearbeitung von Cargo.toml

Zuerst

cargo new phoneme_extractor --lib

Und. Phonem bedeutet morphologisches Element. Ich weiß nicht, ob das japanische Wort morphologischer Extraktor angemessen ist und ob das Englische wirklich ein Phonemextraktor ist.

Also, in Cargo.toml

Cargo.toml


[dependencies]
lindera = "0.5.1"
lazy_static = "1.4.0"
rutie = "0.8.1"
serde = "1.0.115"
serde_json = "1.0.57"

[lib]
crate-type = ["cdylib"]

Schreiben.

(Ergänzung 2020-10-01) Die Version von Rutie war "0.7.0", wurde jedoch auf die neueste Version "0.8.1" geändert. Dadurch wird die in Rust 1.46 ausgegebene Warnung entfernt. Bitte lassen Sie mich wissen, wenn es eine Person gibt, die sagt "Ich konnte mit 0.7.0 kompilieren, aber nicht mit 0.8.1".

Lindera ist eine morphologische Analysekiste, die der Schlüssel zu dieser Aufgabe ist. Rutie ist eine Kiste, die Ruby und Rust verbindet. lazy_static ist die Kiste, die benötigt wird, um mit Rutie eine Klasse zu erstellen.

Ich kannte keine gute Möglichkeit, Informationen von Ruby an Rust zu übermitteln, z. B. welche Teilwörter extrahiert werden sollten, und entschied mich daher für eine JSON-formatierte Zeichenfolge. Verwenden Sie dazu serde und serde_json.

Code

Dies ist der gesamte Code auf der Rust-Seite.

src/lib.rs


#[macro_use]
extern crate rutie;

#[macro_use]
extern crate lazy_static;

use serde::{Deserialize};

use rutie::{Object, Class, RString, Array};

use lindera::tokenizer::Tokenizer;

#[derive(Deserialize)]
pub struct RustPhonemeExtractor {
    mode: String,
    allowed_poss: Vec<String>,
}

wrappable_struct!(RustPhonemeExtractor, PhonemeExtractorWrapper, PHONEME_EXTRACTOR_WRAPPER);

class!(PhonemeExtractor);

methods!(
    PhonemeExtractor,
    rtself,

    fn phoneme_extractor_new(params: RString) -> PhonemeExtractor {
        let params = params.unwrap().to_string();
        let rpe: RustPhonemeExtractor = serde_json::from_str(&params).unwrap();

        Class::from_existing("PhonemeExtractor").wrap_data(rpe, &*PHONEME_EXTRACTOR_WRAPPER)
    }

    fn extract(input: RString) -> Array {
        let extractor = rtself.get_data(&*PHONEME_EXTRACTOR_WRAPPER);
        let input = input.unwrap();
        let mut tokenizer = Tokenizer::new(&extractor.mode, "");
        let tokens = tokenizer.tokenize(input.to_str());

        let mut result = Array::new();
        for token in tokens {
            let detail = token.detail;
            let pos: String = detail.join(",");
            if extractor.allowed_poss.iter().any(|s| pos.starts_with(s)) {
                result.push(RString::new_utf8(&token.text));
            }
        }

        result
    }
);

#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" fn Init_phoneme_extractor() {
    Class::new("PhonemeExtractor", None).define(|klass| {
        klass.def_self("new", phoneme_extractor_new);
        klass.def("extract", extract);
    });
}

Im Folgenden werde ich eine kleine Erklärung hinzufügen.

RustPhoneneExtractor

Verwenden Sie Rutie, um eine Klasse namens PhonemeExtractor for Ruby zu erstellen. Erstellen Sie zunächst eine Struktur mit dem Namen RustPhonemeExtractor und schließen Sie sie an, um PhonemeExtractor zu erstellen.

Dies ist die Definition von RustPhonemeExtractor.

#[derive(Deserialize)]
pub struct RustPhonemeExtractor {
    mode: String,
    allowed_poss: Vec<String>,
}

Oh, ich habe nicht gesagt, dass Lindera zwei "Modi" hat, "normal" und "zerlegen". Grob gesagt ist "Zerlegen" ein Modus zum Zerlegen zusammengesetzter Wörter. Mit anderen Worten, "Zerlegen" ist feiner als "Normal". Lassen Sie dies mit mode angeben. Andererseits hat allow_poss eine Liste von Teilen, die in Form eines Vektors aufgenommen werden sollen. Der Name "Poss" ist durchaus angemessen, aber da das englische Wort für "Teil der Sprache" "Teil der Sprache" ist, wird es als "pos" abgekürzt. Ich habe es im Plural (?) "Poss" gemacht (Posen sind verwirrend mit der singulären gegenwärtigen Posenform der dritten Person).

PhonenemeExtractor

Erstellen Sie als Nächstes einen Ruby-PhonenemeExtractor der Klasse.

Um WustPhonemeExtractor zu verpacken, um PhonemeExtractor zu erstellen

wrappable_struct!(RustPhonemeExtractor, PhonemeExtractorWrapper, PHONEME_EXTRACTOR_WRAPPER);

Schreiben. Die Erklärung ist das letzte Mal Ruby / Rust-Verknüpfung (5) Numerische Berechnung mit Rutie ② Bezier --Qiita Ich möchte dich sehen.

Und um eine Klasse zu machen

class!(PhonemeExtractor);

Schreiben.

PhonenemeExtractor-Methode

Schreiben Sie als Nächstes die PhonenemeExtractor-Methode mit dem Makro Methoden!. Die folgenden zwei Methoden werden beschrieben.

phoneme_extractor_new Methode

Dies ist die Definition.

fn phoneme_extractor_new(params: RString) -> PhonemeExtractor {
    let params = params.unwrap().to_string();
    let rpe: RustPhonemeExtractor = serde_json::from_str(&params).unwrap();

    Class::from_existing("PhonemeExtractor").wrap_data(rpe, &*PHONEME_EXTRACTOR_WRAPPER)
}

RString ist der Rusttyp (in Rutie definiert), der der Ruby String-Klasse entspricht. params ist eine Zeichenfolge, die den Lindera-Initialisierungsmodus und die im JSON-Format zu erfassende Teileliste darstellt.

Dies ist also ein interessanter Teil, aber der Prozess zum Erstellen des Werts der RustPhonemeExtractor-Struktur basierend auf der in params enthaltenen JSON-Zeichenfolge ist

serde_json::from_str(&params).unwrap()

Es ist nur gemacht.

Dies ist der erstaunliche Teil der Kiste namens Serde (ich weiß nicht). Es interpretiert JSON gemäß der Definition der Struktur. Wenn eine JSON-Zeichenfolge angegeben wird, die nicht der Definition der Struktur entspricht, stürzt das Programm bei "unwrap ()" ab. Wenn Sie eine praktische Bibliothek erstellen möchten, sollten Sie den Fehler ordnungsgemäß behandeln.

Ich erwarte übrigens eine solche JSON-Zeichenfolge.

{
  "mode": "normal",
  "allowed_poss": [
    "Substantiv,Allgemeines",
    "Substantiv,固有Substantiv",
    "Substantiv,Anwalt möglich",
    "Substantiv,Verbindung ändern",
    "Substantiv,Adjektiv Verbstamm",
    "Substantiv,Nai Adjektivstamm"
  ]
}

Der Teiletext wird später in einem anderen Abschnitt beschrieben.

Extraktionsmethode

Dies ist eine Instanzmethode der PhonemeExtractor-Klasse.

Wenn die Definition extrahiert wird, sieht es so aus.

fn extract(input: RString) -> Array {
    let extractor = rtself.get_data(&*PHONEME_EXTRACTOR_WRAPPER);
    let input = input.unwrap();
    let mut tokenizer = Tokenizer::new(&extractor.mode, "");
    let tokens = tokenizer.tokenize(input.to_str());

    let mut result = Array::new();
    for token in tokens {
        let detail = token.detail;
        let pos: String = detail.join(",");
        if extractor.allowed_poss.iter().any(|s| pos.starts_with(s)) {
            result.push(RString::new_utf8(&token.text));
        }
    }

    result
}

Wenn der Eingabetext als RString (entsprechend dem Ruby-String) angegeben wird, wird eine Liste von Morphologieelementen in Form eines String-Arrays zurückgegeben.

rtself wird dem zweiten Argument der Methoden gegeben! Macro und scheint einer Instanz der Ruby-Klasse PhonemeExtractor (?) zu entsprechen. Die Variable extractor ist eine Instanz von RustPhonemeExtractor.

Wenn Sie kein Benutzerwörterbuch hinzufügen möchten, generieren Sie einen Tokenizer mit "Tokenizer :: new". Das erste Argument ist die Zeichenfolge des oben beschriebenen Modus, und das zweite Argument gibt den Verzeichnispfad des zu verwendenden Wörterbuchs an. Wenn Sie dem zweiten Argument eine leere Zeichenfolge geben, wird die Standard-IPADIC verwendet.

Verwenden Sie bei Verwendung eines Benutzerwörterbuchs "Tokenizer :: new_with_userdic" und geben Sie den Pfad des Benutzerwörterbuchs (CSV-Format) zum dritten Argument an.

Wenn Sie der Tokenize-Methode des Tokenizers Text geben, wird die Token-Zeichenfolge als Vektor zurückgegeben. Eine Morphologie entspricht einem Token.

Token ist

#[derive(Serialize, Clone)]
pub struct Token<'a> {
    pub text: &'a str,
    pub detail: Vec<String>,
}

Es ist definiert als.

"Text" ist die zerlegte Morphologie selbst. Im Fall von "Schreiben wir einen Code" sind die vier "Code", "o", "schreiben" und "u" anwendbar. detail ist ein String-Vektor, der gemeinsam Informationen zu einem abgerufenen Morphologieelement speichert. Welche Informationen in welcher Reihenfolge vorliegen, hängt vom verwendeten Wörterbuch ab. Im Fall der Standard-IPADIC sind die Indizes 0 bis 3 Teil der Sprachinformation, und zusätzlich sind Informationen wie Nutzungstyp / Nutzungsformprototyp und Lesen enthalten.

Das Wesentliche dieser Funktion besteht darin, zu überprüfen, ob die extrahierte Morphologie für eines der angegebenen Teilwörter gilt. Da das Teilliniensystem jedoch zuerst erläutert werden muss, wird es einmal zurückgestellt. Auf jeden Fall wirft es das entsprechende morphologische Element "text" in das Ruby-Array "result" und gibt das letzte "result" zurück.

Ruby-Klassen- und Methodenzuweisung

Der Rest ist

#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" fn Init_phoneme_extractor() {
    Class::new("PhonemeExtractor", None).define(|klass| {
        klass.def_self("new", phoneme_extractor_new);
        klass.def("extract", extract);
    });
}

nur. Die Ruby PhonemeExtractor-Klasse und ihre singuläre Methode new und Instanzmethode extract werden den Methoden zugewiesen, die durch dasMethoden!Makro definiert sind. Siehe vorherigen Artikel.

Teil Teil System

Im Fall von IPADIC scheinen die Teiltexte dem sogenannten "IPA-Teilliniensystem" zu folgen, das aus vier Schichten besteht. Ich hatte keine Ahnung, wo sich die Hauptinformationen für dieses System befanden, aber sie sind vorerst auf der folgenden Seite aufgeführt. Teil des morphologischen Analysetools

Demnach scheint es zum Beispiel wie folgt zu sein.

(Bild der ersten 4 Elemente des Token "Detail" extrahiert)

Es sollte beachtet werden, dass die Länge (Anzahl der Elemente) von "Detail" in IPADIC grundsätzlich 9 beträgt, "Detail" jedoch "[" UNK "]" nur für morphologische Elemente, die als "unbekannte Wörter" beurteilt werden. Es wird ein Vektor der Länge 1 sein.

Bezeichnung und Beurteilung von Teilwörtern

Abhängig von der Anwendung möchten Sie nun möglicherweise alle 0. Elemente der partizipativen Informationen aufnehmen, die "Nomen" sind, oder das 0. und 1. Element sind die der "Nomenklatur" und der "proprietären Nomenklatur" (3. bzw. 4. Element). Es spielt keine Rolle). Mit anderen Worten, es hängt davon ab, wie detailliert Sie angeben möchten.

Wie soll dies festgelegt und wie beurteilt werden? Ich möchte es so einfach wie möglich machen, also habe ich beschlossen, Folgendes zu tun.

Die Spezifikation ist eine Zeichenkette, die Teil-der-Sprache-Informationen bis zur erforderlichen Tiefe trennt, wie z. B. "Nomenklatur" oder "Nomenklatur, richtige Nomenklatur".

Für die gefundenen morphologischen Elemente ist "Detail" eine durch Kommas getrennte Zeichenfolge (dh "join (", ")").

Ob das erstere am Anfang des letzteren existiert, wird dann durch die Methode string_with von String bestimmt. Machen.

Es sollten jedoch mehrere Teilwortbezeichnungen angegeben werden, von denen jede anwendbar sein sollte. Das ist dieser Teil:

for token in tokens {
    let detail = token.detail;
    let pos: String = detail.join(",");
    if extractor.allowed_poss.iter().any(|s| pos.starts_with(s)) {
        result.push(RString::new_utf8(&token.text));
    }
}

any ist genau wie Rubys Enumerable # any?.

Beachten Sie, dass "RString :: new_utf8" einen Ruby-String aus dem Rust-String erstellt.

kompilieren

Wie gewöhnlich

cargo build --release

Und. Das Artefakt kann sich im Pfad target / release / libmy_rutie_math.dylib befinden (Erweiterung hängt vom Ziel ab).

Implementierung: Ruby Seite

Dies ist das einzige Ruby-Skript. Wie üblich beschreibt dieses Skript den Pfad der Rust-Bibliothek, sofern er im Stammverzeichnis von Rusts Projekt vorhanden ist.

# encoding: utf-8

require "rutie"

Rutie.new(:phoneme_extractor, lib_path: "target/release").init "Init_phoneme_extractor", __dir__

pe = PhonemeExtractor.new <<JSON
  {
    "mode": "normal",
    "allowed_poss": [
      "Substantiv,Allgemeines",
      "Substantiv,固有Substantiv",
      "Substantiv,Anwalt möglich",
      "Substantiv,Verbindung ändern",
      "Substantiv,Adjektiv Verbstamm",
      "Substantiv,Nai Adjektivstamm"
    ]
  }
JSON

text = <<EOT
"Straße" Kotaro Takamura
Es gibt keinen Weg vor mir
Hinter mir liegt eine Straße
Oh, das ist natürlich
Vater
Der riesige Vater, der mich allein stehen ließ
Behalte mich im Auge und beschütze
Fülle mich immer mit dem Geist meines Vaters
Wegen dieser fernen Reise
Wegen dieser fernen Reise
EOT


pe.extract(text).tally
  .sort_by{ |word, freq| -freq }
  .each{ |word, freq| puts "%4d %s" % [freq, word] }

Ergebnis:

3 Vater
3 Reise
2-Wege
1 vor
1 dahinter
1 Natur
1 Stehend
1 riesig
1
1
1 Takamura
1 Kotaro

Hmm, ich bin müde.

abschließend

Wenn Sie versuchen, eine Erklärung hinzuzufügen, wird diese immer länger, und wenn Sie weiter darauf eingehen, werden Sie das Schreiben nie beenden. Es tut mir leid, aber die Qualität des Artikels ist möglicherweise nicht gut. Fragen sind willkommen, bitte stellen Sie alles. Ich werde antworten, wenn ich verstehe.

Recommended Posts

Rubin / Rost-Kooperation (6) Extraktion der Morphologie
Extraktion von "Ruby" Double Hash * Review
Ruby / Rust-Kooperation (3) Numerische Berechnung mit FFI
Ruby / Rust-Verknüpfung (4) Numerische Berechnung mit Rutie
Grundlagen von Ruby
Ruby / Rust-Kooperation (5) Numerische Berechnung mit Rutie ② Veggie
Rubin / Rost-Zusammenarbeit (1) Zweck
Definition der Rubinmethode
Rubin / Rost-Kooperation (2) Mittel
[Ruby] Grundlegende Befehlsliste