Stimulator

機械学習とか好きな技術話とかエンジニア的な話とかを書く

Rustによるlindera、neologd、fasttext、XGBoostを用いたテキスト分類

- はじめに -

RustでNLP機械学習どこまでできるのか試した時のメモ。

Pythonどこまで脱却できるのか見るのも兼ねて。

コードは以下に全部置いてある。
GitHub - vaaaaanquish/rust-text-analysis: rust-text-analysis

- 形態素解析 -

Rustの形態素解析実装を調べると、lindera-morphology/lindera を使うのが有力候補となりそうである。sorami/sudachi.rsagatan/yoinnakagami/awabi のような実装もあるがメンテは止まっている様子である。

linderaメンテナのブログ。
Rust初心者がRust製の日本語形態素解析器の開発を引き継いでみた - Qiita

neologd

linderaはipadic-neologd含む辞書作成ツール等もRustプロジェクト内で作成されている。まず、ipadic-neologd辞書を取得し、linderaで扱えるようにする。

以下ツールを使う。
github.com

READMEの通りに進める。

# neologdの辞書をlindera用に
$ cargo install lindera-ipadic-neologd-builder
$ curl -L https://github.com/neologd/mecab-ipadic-neologd/archive/master.zip > ./mecab-ipadic-neologd-master.zip
$ unzip -o mecab-ipadic-neologd-master.zip
$ ./mecab-ipadic-neologd-master/bin/install-mecab-ipadic-neologd --create_user_dic -p $(pwd)/mecab-ipadic-neologd-master/tmp -y
$ IPADIC_VERSION=$(find ./mecab-ipadic-neologd-master/build/mecab-ipadic-*-neologd-* -type d | awk -F "-" '{print $6"-"$7}')
$ NEOLOGD_VERSION=$(find ./mecab-ipadic-neologd-master/build/mecab-ipadic-*-neologd-* -type d | awk -F "-" '{print $NF}')
$ lindera-ipadic-neologd ./mecab-ipadic-neologd-master/build/mecab-ipadic-${IPADIC_VERSION}-neologd-${NEOLOGD_VERSION} lindera-ipadic-${IPADIC_VERSION}-neologd-${NEOLOGD_VERSION}

# lindera-cli で検証
$ cargo install lindera-cli
$ echo "すもももももももものうち" | lindera
すもも	名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
の	助詞,連体化,*,*,*,*,の,ノ,ノ
うち	名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS
$ echo "すもももももももものうち" | lindera -d ./lindera-ipadic-2.7.0-20070801-neologd-20200910
すもももももももものうち	名詞,固有名詞,一般,*,*,*,すもももももももものうち,スモモモモモモモモノウチ,スモモモモモモモモノウチ
EOS

neologdが利用可能な状態になっている。ここで作成した./lindera-ipadic-2.7.0-20070801-neologd-20200910なる辞書データは、後でRustスクリプト上で使う。

lindera

CLIではなくRustからneologdを呼ぶ。本体は以下のrepoになる。
github.com

linderaのREADMEに先程作成したneologdの辞書を流す例。

use lindera::tokenizer::Tokenizer;
use lindera_core::core::viterbi::Mode;

fn main() -> std::io::Result<()> {
    let mut tokenizer = Tokenizer::new(Mode::Normal, "./lindera-ipadic-2.7.0-20070801-neologd-20200910");
    let tokens = tokenizer.tokenize("すもももももももものうち");
    for token in tokens {
        println!("{}", token.text);
    }
    Ok(())
}
$ cargo run
すもももももももものうち

形態素解析はこれで一旦良さそう。トークナイズ結果は、mecabフォーマット等でも出力できるので、自然にPythonから移行したりできそうである。

- Text Processing、Embedding -

NLPでよく使うような特徴量、Count Vector、TF-IDF、fasttext、BERT、…辺りを扱いたい。

PyTorchのRust Bindingが一番活発に開発されていて、候補として最も良さそう。
github.com

tch-rsに依存した、guillaume-be/rust-bertなどもある。


TF-IDFは、 ferristseng/rust-tfidfafshinm/tf-idf など野良の実装が見つかるが、メンテも止まっていてあまり使いやすい実装のクレートは現状見当たらなかった。


今回は、facebookresearch/fastTextの公式実装のRust Bindingを実装している以下を使う。
github.com

fasttextの実装も含むのでbuildにcmake必須。macならbrewで入れておく。

$ brew install cmake
extern crate csv;
use lindera::tokenizer::Tokenizer;
use lindera_core::core::viterbi::Mode;
use fasttext::FastText;
use fasttext::Args;

...

    // 一度csvに書き込む
    let mut tokenizer = Tokenizer::new(Mode::Normal, "./lindera-ipadic-2.7.0-20070801-neologd-20200910");
    let mut file = File::create("./data/train_fasttext.csv")?;
    for (sentence, label) in zip(&train_sentences, &train_labels){
        let row = tokenizer.tokenize(&sentence).iter().map(|x| x.text).collect::<Vec<&str>>().join(" ");
        write!(file, "__label__{}, {}\n", label, row)?;
    }
    file.flush()?;

    // 先程作成した __label__1, bar の形式のcsvを入力としてbinを生成
    let mut fasttext_args = Args::default();
    fasttext_args.set_input("./data/train_fasttext.csv");
    let mut model = FastText::default();
    let _ = model.train(&fasttext_args);
    let _ = model.save_model("./data/fasttext.bin");

公式実装のBindingなので、パラメータは以下を見れば良い。
List of options · fastText

このfasttext model自体supervisedに分類を解いてtrainさせれば、この時点でテキスト分類のpredictはできる。

- XGBoost -

一般的にfasttextだけで問題が解ける場合は少ないので、特徴量を追加してxgboostのようなモデルを挟む事になる。

gbdtに関連したものだと、以下が最も更新されていて良さそう。
github.com

今回は、公式実装のRust Bindingなのでこちらを選ぶ(最終更新が2年前でxgboost 0.8を使っていて作者も音沙汰がない様子ではあるが、PythonC++から触っている馴染みのxgboostのパラメータやAPIを使いたい)。
github.com

先程のfasttextモデルで文字列をEmbeddingして、DMatrixを作成しBoosterに入れる。更新がなくREADMEに書かれたExampleは動かないので、実装を見比べるか、repo内のExamplesを参考にする。

extern crate xgboost;
use xgboost::{DMatrix, Booster};
use xgboost::parameters::{self, tree, learning::Objective};

...

    // python binding同様にDMatrixを作る
    let train_data_size = train_sentences.len();
    let test_data_size = test_sentences.len();
    let mut train_dmat = DMatrix::from_dense(train_ft_vector_flatten, train_data_size).unwrap();
    train_dmat.set_labels(&train_labels).unwrap();
    let mut test_dmat = DMatrix::from_dense(test_ft_vector_flatten, test_data_size).unwrap();
    test_dmat.set_labels(&test_labels).unwrap();

    // parameter群を設定し多クラス分類でtrain
    let uniq_label = train_labels.unique().len() as u32;
    let eval_sets = &[(&train_dmat, "train"), (&test_dmat, "test")];
    let learning_params = parameters::learning::LearningTaskParametersBuilder::default().objective(Objective::MultiSoftmax(uniq_label)).build().unwrap();
    let tree_params = tree::TreeBoosterParametersBuilder::default().eta(0.1).max_depth(6).build().unwrap();
    let booster_params = parameters::BoosterParametersBuilder::default().booster_type(parameters::BoosterType::Tree(tree_params)).learning_params(learning_params).build().unwrap();
    let training_params = parameters::TrainingParametersBuilder::default().dtrain(&train_dmat).booster_params(booster_params).boost_rounds(5).evaluation_sets(Some(eval_sets)).build().unwrap();
    let bst = Booster::train(&training_params).unwrap();

    // predict
    let mut preds = bst.predict(&test_dmat).unwrap();

特徴量化からmodelによるpredictまでができた。

- 実験 -

実際にlivedoorニュースコーパスに対して、ニュース本文からメディアを分類するタスクを実施する。

以下からデータをダウンロードした。
ダウンロード - 株式会社ロンウイット

RUN wget https://www.rondhuit.com/download/ldcc-20140209.tar.gz
RUN tar -zxvf ldcc-20140209.tar.gz

このデータを以下スクリプトで雑にtrain、testデータに分割した。
https://github.com/vaaaaanquish/rust-text-analysis

Dockerに収めたので、以下をbuildすれば、上記データがダウンロードされtrain、testデータが作成される。
github.com


実際にlinderaで形態素解析し、fasttextでtrain、embedding、XGBoostで分類タスクを実施した。

testに対するclassification_reportは以下のようになった。

              precision    recall  f1-score   support

           0       0.73      0.72      0.72       251
           1       0.73      0.58      0.64       147
           2       0.91      0.94      0.93       251
           3       0.91      0.92      0.92       283
           4       0.80      0.77      0.79       251
           5       0.84      0.80      0.82       232
           6       0.82      0.94      0.88       254
           7       0.78      0.78      0.78       263
           8       0.81      0.83      0.82       278

   micro avg       0.82      0.82      0.82      2210
   macro avg       0.81      0.81      0.81      2210
weighted avg       0.82      0.82      0.82      2210
 samples avg       0.82      0.82      0.82      2210

悪くないので一旦ここまで。

- おわりに -

割と悪くない所までできた。

LightGBMもcoreのBindingを書いてあげれば使えそうだし、もう少しmetricとかpreprocessingのML関連クレート書いていけば良さそう。

fasttexやPyTorchがWebAssenblyに対応しているので、次の記事ではその検証を書く。
WebAssembly module · fastText
WebAssemblyでの機械学習モデルデプロイの動向 · tkat0.github.io

Python脱却にはまだまだ遠い(クレートやメンテナの数が違いすぎるし、EDAなど型なしでやりたい作業も多い)が、期待した結果が得られたので良かった。