Ruby で Crfsuite::ItemSequence を作成する処理を 2 倍以上速くした
CRFsuite は柔軟な素性を指定できることが特徴の一つですが、それ故に CRF++ と違って素性を全て与えてやる必要があり、オンラインで推定しようとするとこの処理がボトルネックになります。
というわけで、Ruby 側でどう書いたら少しでも速くなるかという話です。
環境は次のとおりです。
- Mac OS X 10.10.3 (2.6 GHz Intel Core i5, 16 GB 1600 MHz DDR3)
- Ruby 2.0.0p598
- CRFsuite 0.12.2 を少しいじったもの
データはチュートリアルで使われている学習データのみ使用します。
% 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']
ポイントは
- Crfsuite::Item#push, Crfsuite::ItemSequence#push を呼ばずに初期化時に配列を渡す(14 秒ぐらい速くなる)
- 属性(素性)名は予め作っておく(7 秒ぐらい速くなる)
- 値の配列を用意して結合するのではなく値の文字列を用意して文字列を結合していく(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 側でこれ以上速くするのはかなり大変そうです・・・