CRFsuite を Ruby から実行してみた

CRFsuite は最新バージョン1から SWIG のインタフェースが提供されており、SWIG がサポートしている言語であれば手軽に利用することができます。

というわけで、Ruby から実行してみたという話です。

環境

以下の環境で動作確認していますが、Mac を前提に説明していきます。

  • Mac OS X 10.10.2 with SWIG 3.0.5 and Ruby 2.0.0p598
  • Debian 6.0.5 with SWIG 3.0.5 and Ruby 2.0.0p451
  • Debian 6.0.5 with SWIG 2.0.4 and Ruby 2.0.0p451

CRFsuite のインストール

libLBFGS も一緒にインストールしてくれて楽なので Homebrew からインストールします。

% brew install crfsuite
% crfsuite -h
CRFSuite 0.12  Copyright (c) 2007-2011 Naoaki Okazaki

USAGE: crfsuite <COMMAND> [OPTIONS]
    COMMAND     Command name to specify the processing
    OPTIONS     Arguments for the command (optional; command-specific)

COMMAND:
    learn       Obtain a model from a training set of instances
    tag         Assign suitable labels to given instances by using a model
    dump        Output a model in a plain-text format

For the usage of each command, specify -h option in the command argument.

Ruby バインディングの作成

本家に 3 つ pull request を送ってるんですが、全部取り込んであるやつが https://github.com/abicky/crfsuite/tree/archive/20150412 なのでこちらを使います。

% git clone https://github.com/abicky/crfsuite.git
% cd ./crfsuite/
% git checkout archive/20150412
% cd ./swig/ruby/
% ./prepare.sh --swig
% ruby extconf.rb
% make  # インストールまでする場合は make install

サンプルコードの実行

サンプルデータの作成

チュートリアルで使われている学習データを使うことにします。

% cd /path/to/crfsuite/swig/ruby/
% curl -LO http://www.cnts.ua.ac.be/conll2000/chunking/train.txt.gz
% gzcat train.txt | ../../example/chunking.py > train.crfsuite.txt

モデルの学習

swig/ruby/ 以下に sample_train.py を Ruby に移植したコードを用意したのでそれを実行してみます。

% cd /path/to/crfsuite/swig/ruby/
% ruby -I. sample_train.rb CoNLL2000.model < train.crfsuite.txt
0.12
feature.minfreq 0.000000 The minimum frequency of features.
feature.possible_states 0 Force to generate possible state features.
feature.possible_transitions 0 Force to generate possible transition features.
c2 1.000000 Coefficient for L2 regularization.
max_iterations 1000 The maximum number of iterations (epochs) for SGD optimization.
period 10 The duration of iterations to test the stopping criterion.
delta 0.000001 The threshold for the stopping criterion; an optimization process stops when
the improvement of the log likelihood over the last ${period} iterations is no
greater than this threshold.
calibration.eta 0.100000 The initial value of learning rate (eta) used for calibration.
calibration.rate 2.000000 The rate of increase/decrease of learning rate for calibration.
calibration.samples 1000 The number of instances used for calibration.
calibration.candidates 10 The number of candidates of learning rate.
calibration.max_trials 20 The maximum number of trials of learning rates for calibration.
Feature generation
type: CRF1d
feature.minfreq: 0.000000
feature.possible_states: 0
feature.possible_transitions: 0
0....1....2....3....4....5....6....7....8....9....10
Number of features: 452755
Seconds required: 2.969

Stochastic Gradient Descent (SGD)
c2: 0.100000
max_iterations: 1000
period: 10
delta: 0.000001

Calibrating the learning rate (eta)
calibration.eta: 0.100000
calibration.rate: 2.000000
calibration.samples: 1000
calibration.candidates: 10
calibration.max_trials: 20
Initial loss: 73427.713480
Trial #1 (eta = 0.100000): 6582.519447
Trial #2 (eta = 0.200000): 6730.455920
Trial #3 (eta = 0.400000): 9512.830812
Trial #4 (eta = 0.800000): 18893.911403
Trial #5 (eta = 1.600000): 37728.232850
Trial #6 (eta = 3.200000): 80183.389088 (worse)
Trial #7 (eta = 0.050000): 7630.312307
Trial #8 (eta = 0.025000): 9621.753885
Trial #9 (eta = 0.012500): 12638.018534
Trial #10 (eta = 0.006250): 16982.966482
Trial #11 (eta = 0.003125): 23074.675027
Trial #12 (eta = 0.001563): 31343.061039
Trial #13 (eta = 0.000781): 41540.646706
Trial #14 (eta = 0.000391): 52696.836433
Trial #15 (eta = 0.000195): 61945.207748
Trial #16 (eta = 0.000098): 67498.204541
Best learning rate (eta): 0.100000
Seconds required: 2.101

***** Epoch #1 *****
Loss: 33583.103783
Feature L2-norm: 63.611729
Learning rate (eta): 0.098039
Total number of feature updates: 8936
Seconds required for this iteration: 1.047

(snip)

***** Epoch #154 *****
Loss: 3524.448997
Improvement ratio: -0.000047
Feature L2-norm: 149.987601
Learning rate (eta): 0.024510
Total number of feature updates: 1376144
Seconds required for this iteration: 1.051

SGD terminated with the stopping criteria
Loss: 3523.303519
Total seconds required for training: 168.976

Storing the model
Number of active features: 452755 (452755)
Number of active attributes: 335674 (335674)
Number of active labels: 22 (22)
Writing labels
Writing attributes
Writing feature references for transitions
Writing feature references for attributes
Seconds required: 1.099

コマンドで実行しても同じような結果になります。

% crfsuite learn -a l2sgd -p c2=0.1 -m CoNLL2000_via_command.model train.crfsuite.txt
CRFSuite 0.12  Copyright (c) 2007-2011 Naoaki Okazaki

Start time of the training: 2015-04-12T02:56:35Z

Reading the data set(s)
[1] train.crfsuite.txt
0....1....2....3....4....5....6....7....8....9....10
Number of instances: 8937
Seconds required: 6.221

Statistics the data set(s)
Number of data sets (groups): 1
Number of instances: 8936
Number of items: 211727
Number of attributes: 335674
Number of labels: 22

Feature generation
type: CRF1d
feature.minfreq: 0.000000
feature.possible_states: 0
feature.possible_transitions: 0
0....1....2....3....4....5....6....7....8....9....10
Number of features: 452755
Seconds required: 3.517

Stochastic Gradient Descent (SGD)
c2: 0.100000
max_iterations: 1000
period: 10
delta: 0.000001

Calibrating the learning rate (eta)
calibration.eta: 0.100000
calibration.rate: 2.000000
calibration.samples: 1000
calibration.candidates: 10
calibration.max_trials: 20
Initial loss: 73427.713480
Trial #1 (eta = 0.100000): 6582.519447
Trial #2 (eta = 0.200000): 6730.455920
Trial #3 (eta = 0.400000): 9512.830812
Trial #4 (eta = 0.800000): 18893.911404
Trial #5 (eta = 1.600000): 37980.324345
Trial #6 (eta = 3.200000): 79542.856610 (worse)
Trial #7 (eta = 0.050000): 7630.312307
Trial #8 (eta = 0.025000): 9621.753885
Trial #9 (eta = 0.012500): 12638.018534
Trial #10 (eta = 0.006250): 16982.966482
Trial #11 (eta = 0.003125): 23074.675027
Trial #12 (eta = 0.001563): 31343.061039
Trial #13 (eta = 0.000781): 41540.646706
Trial #14 (eta = 0.000391): 52696.836433
Trial #15 (eta = 0.000195): 61945.207748
Trial #16 (eta = 0.000098): 67498.204541
Best learning rate (eta): 0.100000
Seconds required: 1.967

***** Epoch #1 *****
Loss: 33583.103783
Feature L2-norm: 63.611729
Learning rate (eta): 0.098039
Total number of feature updates: 8936
Seconds required for this iteration: 0.960

(snip)

***** Epoch #154 *****
Loss: 3524.448997
Improvement ratio: -0.000047
Feature L2-norm: 149.987601
Learning rate (eta): 0.024510
Total number of feature updates: 1376144
Seconds required for this iteration: 0.981

SGD terminated with the stopping criteria
Loss: 3523.303519
Total seconds required for training: 155.202

Storing the model
Number of active features: 452755 (452755)
Number of active attributes: 335674 (335674)
Number of active labels: 22 (22)
Writing labels
Writing attributes
Writing feature references for transitions
Writing feature references for attributes
Seconds required: 1.115

End time of the training: 2015-04-12T02:59:30Z

ただ、Ruby でモデルファイルを作成すると推定のためにモデルを読み込むことはできるんですが、dump しようとすると SEGV になるという・・・2

推定

swig/ruby/ 以下に sample_tag.py を Ruby に移植したコードを用意したのでそれを実行してみます。

% cd /path/to/crfsuite/swig/ruby/
% ruby -I. sample_tag.rb CoNLL2000.model < train.crfsuite.txt > result
% head -39 result
0.7205747288807243
B-NP:0.991185
B-PP:0.999874
B-NP:0.999506
I-NP:0.999689
B-VP:0.999957
I-VP:0.983551
I-VP:0.992489
I-VP:0.993622
I-VP:0.999727
B-NP:0.999940
I-NP:0.999648
I-NP:0.998855
B-SBAR:0.984406
B-NP:0.998340
I-NP:0.996686
B-PP:0.999381
B-NP:0.999211
O:0.999081
B-ADJP:0.994262
B-PP:0.973796
B-NP:0.963802
B-NP:0.904103
O:0.999290
B-VP:0.971081
I-VP:0.962726
I-VP:0.999455
B-NP:0.999884
I-NP:0.999874
I-NP:0.999750
B-PP:0.999940
B-NP:0.999992
I-NP:0.934700
I-NP:0.935800
B-NP:0.999914
I-NP:0.999763
I-NP:0.999913
O:0.999981

コマンドで実行しても同様の結果になります。

% crfsuite tag -p -i -m CoNLL2000_via_command.model train.crfsuite.txt > result_via_command
% head -n 39 result_via_command
@probability    0.720575
B-NP:0.991185
B-PP:0.999874
B-NP:0.999506
I-NP:0.999689
B-VP:0.999957
I-VP:0.983551
I-VP:0.992489
I-VP:0.993622
I-VP:0.999727
B-NP:0.999940
I-NP:0.999648
I-NP:0.998855
B-SBAR:0.984406
B-NP:0.998340
I-NP:0.996686
B-PP:0.999381
B-NP:0.999211
O:0.999081
B-ADJP:0.994262
B-PP:0.973796
B-NP:0.963802
B-NP:0.904103
O:0.999290
B-VP:0.971081
I-VP:0.962726
I-VP:0.999455
B-NP:0.999884
I-NP:0.999874
I-NP:0.999750
B-PP:0.999940
B-NP:0.999992
I-NP:0.934700
I-NP:0.935800
B-NP:0.999914
I-NP:0.999763
I-NP:0.999913
O:0.999981

以上、簡単ですね!!

苦労話

「こんなのいちいちエントリー書くほどの内容じゃないでしょ。」と思われるとあれなので意外に大変だったんですよ!!!!!!って話です。

Python のサンプルコードを正常に実行するまでの道のりが長かった

まず、SWIG 3.0.5 で swig を実行するとエラーになるのでググってみたら export.i の「%template」の上に「%include」を移動しましょうと説明があったのでその通りにしたらエラーを回避できました。

cf. http://stackoverflow.com/questions/22776173/swig-error-c-for-python-director-use

次に、「python setup.py build_ext」でエラーが出たので setup.py を修正することで回避できました。

cf. https://github.com/chokkan/crfsuite/issues/11

更に、サンプルコードを実行すると次のようなエラーが出たのでコードを読んで修正して回避しました。

Traceback (most recent call last):
  File "sample_train.py", line 54, in <module>
    trainer.append(xseq, yseq, 0)
  File "/path/to/crfsuite/swig/python/crfsuite.py", line 130, in append
    def append(self, *args): return _crfsuite.Trainer_append(self, *args)
TypeError: in method 'Trainer_append', argument 3 of type 'CRFSuite::StringList const &'

cf. https://github.com/chokkan/crfsuite/pull/40

ところが、サンプルを実行してもメッセージが表示されませんでした。ちゃんと crfsuite.Trainer#message を override しているのに。
デフォルトの関数にログを埋め込んでみたらメッセージが表示されるので、どうも override ができてない模様。
そもそも Python 側で override なんかできるのか?ということで SWIG の S の字もよくわからない状態から ドキュメントを眺めつつ単純なサンプルを動かしてみたりしていたらdirectorsという存在を知り、最初の export.i の変更が原因で override できなくなっていることを突き止めたわけです。

cf. https://github.com/chokkan/crfsuite/pull/39

Ruby と Python で実行結果が違っていた

コマンドで実行したり Python のサンプルコードだとすぐ学習が終わるのに、Ruby だと 100 倍ぐらいかかって結果も悲惨なものでした。
サンプルコードを見比べても違いがわからなかったので SWIG のバグなんじゃないかと思いつつ CRFsuite にログを埋め込んでどこからおかしくなっているか調べました。

結局は for 文のブロックの範囲を一箇所間違えていただけなんですが・・・。これは僕のポカミスですね・・・

  1. といっても 4 年ぐらい前にリリースされたやつですが 

  2. Homebrew からインストールしたものではなく自前でビルドすれば大丈夫っぽいです