Série d'articles
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.
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.
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.
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.
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.
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(¶ms).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.
Ensuite, écrivez la méthode PhonenemeExtractor avec la macro methods!
.
Les deux méthodes suivantes sont décrites.
phoneme_extractor_new
(créer une instance)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(¶ms).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(¶ms).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.
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 de
RustPhonemeExtractor`.
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
.
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.
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.
" Hanako "
→ [" Nomenclature "," Nomenclature propriétaire "," Nom de la personne "," Prénom "]
" Oignon "
→ [" Nomenclature "," Général "," "," "]
(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.
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.
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).
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é.
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