[RUBY] Coopération rubis / rouille (6) Extraction de morphologie

Série d'articles

introduction

J'ai progressivement appris à faire fonctionner Ruby et Rust ensemble, et c'est devenu plus intéressant. Jusqu'à présent ((3) - (5)) J'ai essayé les calculs numériques, alors essayons le traitement de texte maintenant. Très bien, du coup, j'utiliserai la bibliothèque d'analyse morphologique de Rust pour extraire uniquement les éléments morphologiques d'une partie spécifique du texte, comme uniquement la nomenclature appropriée, toute la nomenclature, les adjectifs et les adjoints, etc.

L'auteur a vécu une longue et mince vie de Ruby, mais Rust est un amateur, et l'analyse morphologique n'est qu'un petit jeu avec MeCab en Ruby. Je ne sais pas à quel point c'est difficile.

politique

Lindera est utilisé comme bibliothèque d'analyse morphologique réalisée par Rust. Ceci est un fork de @mosuka à partir d'une bibliothèque créée expérimentalement appelée kuromoji-rs. Il prend la forme d'un fork, mais il a repris le développement sous un autre nom. Voir l'article de @ mosuka ci-dessous pour plus de détails. Les débutants de Rust ont repris le développement de l'analyseur morphologique japonais de Rust --Qiita

Quant au mécanisme de coopération entre Ruby et Rust, Rutie est utilisé comme dans (4) et (5).

La répartition des rôles entre Ruby et Rust est pensée comme ça. Dans Rust, créez une classe Ruby également appelée extracteur morphologique (Rutie peut écrire une classe Ruby dans Rust). Donnez une liste des pièces à récupérer à l'initialisation (prenez toutes les pièces de la liste). Si vous créez une instance d'un extracteur d'éléments morphologiques et que vous lui donnez du texte, les éléments morphologiques correspondants sont renvoyés sous forme de tableau de chaînes de caractères (dans l'ordre d'apparition, il y a des doublons).

Dans l'exemple de programme côté Ruby, une table de fréquences est créée à partir de la liste des éléments morphologiques renvoyés et affichée dans l'ordre de celle avec la fréquence la plus élevée.

Caractéristiques de Lindera

Pour un aperçu de Lindera, voir le lien, mais ici, je voudrais souligner uniquement les points suivants.

Je suis reconnaissant que le dictionnaire soit inclus depuis le début. C'est un peu pénible de télécharger le dictionnaire de quelque part, de taper une commande et de placer le fichier quelque part, même si je ne fais que l'essayer.

De plus, IPADIC est extrêmement à court de mots pour gérer les phrases qui passent à travers divers médias tels que SNS, mais je suis reconnaissant qu'un grand dictionnaire tel que IPADIC-NEologd puisse être facilement utilisé.

Pour ajouter un mot utilisateur, placez simplement un fichier CSV et spécifiez son chemin.

Motivation

Dans cet article, je voudrais présenter "un code qui n'est pas pratique, mais suffisamment simple pour imaginer un chemin vers un code pratique" afin que d'autres puissent facilement s'y référer.

Dans Ruby, les gemmes pour l'utilisation de l'analyseur morphologique MeCab et JUMAN ++ sont natto et jumanpp_ruby. Il y a respectivement [^ gem].

[^ gem]: Il semble y en avoir d'autres, mais je ne les connais pas.

Alors pourquoi s'embêter à écrire du code qui appelle Rust de Ruby? L'hypothèse décrite dans Je veux éviter GC.

Lorsque vous utilisez MeCab etc. de Ruby, une grande quantité de données de chaîne de caractères est amenée du côté Ruby pour chaque morphologie. La plupart d'entre eux deviennent des déchets, et lorsqu'ils s'accumulent dans une certaine mesure, ils deviennent la cible de la collecte des déchets. Il semble que ce soit inefficace [^ ef].

[^ ef]: Il ne peut pas être dit sans une expérimentation appropriée pour savoir s'il est vraiment inefficace et quelle quantité de texte doit être manipulée pour affecter les performances.

Pour des tâches telles que l'extraction de midi, il serait efficace si seule la nomenclature était extraite du côté Rust et que seules les chaînes de caractères souhaitées par le côté Ruby étaient renvoyées. Le garbage collection ne se produit pas du côté de la rouille. Les variables qui sont hors de portée disparaissent à ce moment.

Mise en œuvre: côté rouille

Jusqu'à la modification de Cargo.toml

Premier

cargo new phoneme_extractor --lib

Et. phonème signifie élément morphologique. Je ne sais pas si le mot japonais extracteur morphologique est approprié, et si l'anglais est vraiment un extracteur de phonèmes.

Donc, dans 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"]

Écrire.

(Ajout 2020-10-01) La version de Rutie était "0.7.0", mais elle a été remplacée par la dernière version "0.8.1". Cela élimine l'avertissement émis dans Rust 1.46. Faites-moi savoir s'il y a une personne qui dit "J'ai pu compiler avec 0.7.0 mais pas avec 0.8.1".

lindera est une caisse d'analyse morphologique qui est la clé de cette tâche. rutie est une caisse qui relie Ruby et Rust. lazy_static est la caisse nécessaire pour créer une classe avec Rutie.

Je ne connaissais pas un bon moyen de transmettre des informations de Ruby à Rust, telles que les mots de partie à extraire, j'ai donc décidé d'utiliser une chaîne au format JSON. Pour ce faire, utilisez serde et serde_json.

code

C'est tout le code du côté Rust.

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);
    });
}

Ci-dessous, j'ajouterai une petite explication.

RustPhoneneExtractor

Utilisez Rutie pour créer une classe appelée PhonemeExtractor for Ruby. Tout d'abord, créez une structure appelée RustPhonemeExtractor et enveloppez-la pour créer PhonemeExtractor.

C'est la définition de RustPhonemeExtractor.

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

Oh, je n'ai pas dit que Lindera avait deux "modes", "normal" et "décomposer". En gros, «décomposer» est un mode de décomposition de mots composés. En d'autres termes, "décomposer" est plus fin que "normal". Autorisez cela à être spécifié avec mode. D'autre part, ʻallowed_poss` a une liste de pièces à récupérer sous la forme d'un vecteur. «poss» est un nom assez approprié, mais comme le mot anglais pour «part of speech» est «part of speech», il est abrégé en «pos». Je l'ai fait «poss» au pluriel (?) (Poses est déroutant avec la troisième personne du singulier de la forme présente de la pose).

PhonenemeExtractor

Ensuite, créez un PhonenemeExtractor de classe Ruby.

Pour envelopper RustPhonemeExtractor pour créer PhonemeExtractor

wrappable_struct!(RustPhonemeExtractor, PhonemeExtractorWrapper, PHONEME_EXTRACTOR_WRAPPER);

Écrire. L'explication est la dernière fois Lien Ruby / Rust (5) Calcul numérique avec Rutie ② Bezier --Qiita Je veux que tu voies.

Et faire un cours

class!(PhonemeExtractor);

Écrire.

Méthode PhonenemeExtractor

Ensuite, écrivez la méthode PhonenemeExtractor avec la macro methods!. Les deux méthodes suivantes sont décrites.

méthode phoneme_extractor_new

Telle est la définition.

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 est le type de Rust (défini dans Rutie) qui correspond à la classe Ruby String. params est une chaîne de caractères qui représente le mode d'initialisation de Lindera et la liste de pièces à récupérer au format JSON.

C'est donc une partie intéressante, mais le processus de création de la valeur de la structure RustPhonemeExtractor basée sur la chaîne de caractères JSON contenue dans params est

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

C'est juste fait.

C'est la partie incroyable de la caisse appelée Serde (je ne sais pas). Il interprète JSON selon la définition de la structure. Si une chaîne JSON qui ne correspond pas à la définition de la structure est donnée, le programme plantera au moment de ʻunwrap () `. Si vous souhaitez créer une bibliothèque pratique, vous devez gérer l'erreur correctement.

Au fait, je m'attends à recevoir une telle chaîne de caractères JSON.

{
  "mode": "normal",
  "allowed_poss": [
    "nom,Général",
    "nom,固有nom",
    "nom,Avocat possible",
    "nom,Changer de connexion",
    "nom,Racine du verbe adjectif",
    "nom,Nai adjectif radical"
  ]
}

Les paroles de la partie seront décrites plus loin dans une autre section.

méthode d'extraction

Il s'agit d'une méthode d'instance de la classe PhonemeExtractor.

Lorsque la définition est extraite, cela ressemble à ceci.

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
}

Étant donné le texte d'entrée sous forme de RString (correspondant à la Ruby String), une liste d'éléments de morphologie est renvoyée sous la forme d'un Array of String.

rtself est donné au deuxième argument de la macro methods!, et semble correspondre à une instance de la classe Ruby PhonemeExtractor (?). La variable ʻextractorest une instance deRustPhonemeExtractor`.

Si vous ne souhaitez pas ajouter de dictionnaire utilisateur, générez un tokenizer avec Tokenizer :: new. Le premier argument est la chaîne de caractères du mode décrit ci-dessus, et le second argument donne le chemin du répertoire du dictionnaire à utiliser. Si vous donnez une chaîne de caractères vide au deuxième argument, l'IPADIC par défaut est utilisé.

Lorsque vous utilisez un dictionnaire utilisateur, utilisez Tokenizer :: new_with_userdic et donnez le chemin du dictionnaire utilisateur (format CSV) au troisième argument.

Si vous donnez du texte à la méthode tokenize du tokenizer, la chaîne de jeton sera renvoyée sous forme de vecteur. Une morphologie correspond à un jeton.

Le jeton est

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

Il est défini comme.

«texte» est la morphologie décomposée elle-même. Dans le cas de «écrivons un code», les quatre «code», «o», «écrire» et «u» sont applicables. detail est un vecteur de chaîne qui stocke collectivement des informations sur un élément de morphologie récupéré. L'ordre des informations dépend du dictionnaire utilisé. Dans le cas de l'IPADIC par défaut, les index 0 à 3 font partie des informations sur les paroles, et en plus, des informations telles que le type d'utilisation / prototype de formulaire d'utilisation et la lecture sont incluses.

L'essence de cette fonction est de vérifier si l'élément morphologique extrait correspond à l'un des mots de partie spécifiés, mais comme il est nécessaire d'expliquer d'abord le système de ligne de partie, il est mis une fois sur le dos. Quoi qu'il en soit, il jette l'élément morphologique correspondant text dans le tableau Ruby result et renvoie le dernier result.

Affectation de classes et de méthodes Ruby

Le reste est

#[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);
    });
}

seulement. La classe Ruby PhonemeExtractor et sa méthode singulière new et la méthode d'instance ʻextract sont affectées aux méthodes définies par la macro methods!`. Voir l'article précédent.

Système de pièces détachées

Dans le cas d'IPADIC, les paroles des parties semblent suivre ce qu'on appelle le "système de lignes de parties IPA" composé de quatre couches. Je n'avais aucune idée de l'endroit où se trouvaient les informations principales de ce système, mais elles sont écrites sur la page suivante pour le moment. Fait partie de l'outil d'analyse morphologique

D'après cela, par exemple, il semble que ce soit comme suit.

(Image des 4 premiers éléments du jeton détail extraite)

Il convient de noter que la longueur (nombre d'éléments) de "detail" est fondamentalement de 9 dans IPADIC, mais que "detail" est "[" UNK "]" uniquement pour les éléments morphologiques jugés "mots inconnus". Ce sera un vecteur de longueur 1.

Désignation et jugement des mots partiels

En fonction de l'application, vous voudrez peut-être ramasser tous les 0e éléments de l'information de partie de discours qui sont des «substantifs», ou les 0e et 1er éléments sont la «nomenclature» et la «nomenclature propriétaire» (3ème et 4ème éléments, respectivement). Cela n'a pas d'importance). En d'autres termes, cela dépend du niveau de détail que vous souhaitez spécifier.

Comment cela doit-il être spécifié et comment doit-il être jugé? Je veux le faire le plus simplement possible, j'ai donc décidé de faire ce qui suit.

La spécification est une chaîne de caractères qui sépare les informations de partie du discours jusqu'à la profondeur requise, telle que «« nomenclature »» ou «« nomenclature, nomenclature appropriée »».

De plus, pour les éléments morphologiques trouvés, detail est une chaîne de caractères séparés par des virgules (c'est-à-direjoin (",")).

Ensuite, si le premier existe au début du second est déterminé par la méthode starts_with de String. Faire.

Cependant, plusieurs désignations de mot partiel doivent être données, et chacune d'elles doit être applicable. C'est cette partie:

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` est exactement comme Ruby Enumerable # any?.

Notez que RString :: new_utf8 crée une Ruby String à partir de la chaîne Rust.

compiler

Comme d'habitude

cargo build --release

Et. L'artefact peut être dans le chemin target / release / libmy_rutie_math.dylib (l'extension dépend de la cible).

Implémentation: côté rubis

C'est le seul script Ruby. Comme d'habitude, ce script décrit le chemin de la bibliothèque de Rust, en supposant qu'il existe dans le répertoire racine du projet de Rust.

# 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": [
      "nom,Général",
      "nom,固有nom",
      "nom,Avocat possible",
      "nom,Changer de connexion",
      "nom,Racine du verbe adjectif",
      "nom,Nai adjectif radical"
    ]
  }
JSON

text = <<EOT
"Route" Kotaro Takamura
Il n'y a aucun moyen devant moi
Il y a une route derrière moi
Oh, c'est naturel
Père
Le vaste père qui m'a fait rester seul
Garde un œil sur moi et protège
Remplis-moi toujours de l'esprit de mon père
À cause de ce voyage lointain
À cause de ce voyage lointain
EOT


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

résultat:

3 père
3 voyage
2 voies
1 avant
1 derrière
1 Nature
1 Debout
1 vaste
1er
1
1 Takamura
1 Kotaro

Hmm, je suis fatigué.

en conclusion

Si vous essayez d'ajouter une explication, cela deviendra de plus en plus long, et si vous continuez à élaborer, vous ne finirez jamais d'écrire. Je suis désolé, mais la qualité de l'article n'est peut-être pas bonne. Les questions sont les bienvenues, veuillez donc demander quoi que ce soit. Je répondrai si je comprends.

Recommended Posts

Coopération rubis / rouille (6) Extraction de morphologie
Extraction du double hash "ruby" * Avis
Coopération Ruby / Rust (3) Calcul numérique avec FFI
Liaison Ruby / Rust (4) Calcul numérique avec Rutie
Bases de Ruby
Coopération Ruby / Rust (5) Calcul numérique avec Rutie ② Veggie
Coopération rubis / rouille (1) Objectif
définition de la méthode ruby
Coopération Rubis / Rouille (2) Moyens
[Ruby] Liste des commandes de base