Ruby で Crfsuite::ItemSequence を作成する処理を 2 倍以上速くした

CRFsuite は柔軟な素性を指定できることが特徴の一つですが、それ故に CRF++ と違って素性を全て与えてやる必要があり、オンラインで推定しようとするとこの処理がボトルネックになります。

というわけで、Ruby 側でどう書いたら少しでも速くなるかという話です。

環境は次のとおりです。

データはチュートリアルで使われている学習データのみ使用します。

% curl -L http://www.cnts.ua.ac.be/conll2000/chunking/train.txt.gz | zcat > train.txt

学習と推定にはこのスクリプトを使用します。

まず、モデルを生成します。

% ruby /path/to/script -c learn -m crfsuite.model < train.txt

そして推定です。

% time ruby /path/to/script -c tag -m crfsuite.model < train.txt
ruby /path/to/script -c tag -m crfsuite.model < train.txt  38.21s user 0.53s system 99%cpu 39.016 total

最初は 39.016 秒かかりました。

これを色々いじって次のように書き換えました。

diff --git a/script b/script
index 7dc1e06..066dbf8 100644
--- a/script
+++ b/script
@@ -28,6 +28,11 @@ FEATURE_TEMPLATES = [
   [[ 0, 1], [ 1, 1], [2, 1]],
 ].freeze
 
+FEATURE_TEMPLATES_WITH_ATTR_NAME = FEATURE_TEMPLATES.map { |template|
+  [template, template.map{ |row, col| "x[#{row},#{col}]" }.join('|')]
+}.freeze
+
+
 class Trainer < Crfsuite::Trainer
   def message(s)
     print s
@@ -74,37 +79,33 @@ def tag(model_file)
 end
 
 def build_sequence(items)
-  xseq = Crfsuite::ItemSequence.new
-  names = []
-  values = []
+  citems = []
   items.size.times do |offset|
-    item = Crfsuite::Item.new
-    FEATURE_TEMPLATES.each do |template|
-      names.clear
-      values.clear
+    attrs = []
+    FEATURE_TEMPLATES_WITH_ATTR_NAME.each do |template, attr_name|
+      values = ''
       available_feature = true
-      template.each do |field|
-        idx = offset + field[0]
+      template.each do |row, col|
+        idx = offset + row
         if idx < 0 || idx >= items.size
           available_feature = false
           break
         end
-        value = items[idx][field[1]]
+        value = items[idx][col]
         if value.nil?
           available_feature = false
           break
         end
-        names << 'x[%d,%d]' % field
-        values << value
+        values << (values.empty? ? value : "|#{value}")
       end
       next unless available_feature
-      item << Crfsuite::Attribute.new("#{names.join('|')}=#{values.join('|')}")
+      attrs << Crfsuite::Attribute.new("#{attr_name}=#{values}")
     end
-    item << Crfsuite::Attribute.new('__BOS__') if offset.zero?
-    item << Crfsuite::Attribute.new('__EOS__') if offset == items.size - 1
-    xseq << item
+    attrs << Crfsuite::Attribute.new('__BOS__') if offset.zero?
+    attrs << Crfsuite::Attribute.new('__EOS__') if offset == items.size - 1
+    citems << Crfsuite::Item.new(attrs)
   end
-  xseq
+  Crfsuite::ItemSequence.new(citems)
 end
 
 case params['command']

ポイントは

  1. Crfsuite::Item#push, Crfsuite::ItemSequence#push を呼ばずに初期化時に配列を渡す(14 秒ぐらい速くなる)
  2. 属性(素性)名は予め作っておく(7 秒ぐらい速くなる)
  3. 値の配列を用意して結合するのではなく値の文字列を用意して文字列を結合していく(1 秒ぐらい速くなる)

という感じで、手元の環境で 22 秒ぐらい速くなりました。

% time ruby /path/to/script -c tag -m crfsuite.model < train.txt
ruby crfsuite.rb -c tag -m crfsuite.model < train.txt  16.91s user 0.19s system 99% cpu 17.217 total

39.016 秒 から 17.217 秒に!!

ちなみに 15 秒ぐらいは build_sequence の処理なんですが、そのうち約 12 秒が Crfsuite::Attribute.new にかかっている時間なので、Ruby 側でこれ以上速くするのはかなり大変そうです・・・

広告
R で擬似アクセスログを作ってみた Mac で SimString を使ってみた
※このエントリーははてなダイアリーから移行したものです。過去のコメントなどはそちらを参照してください