CRFsuite の正則化パラメータ最適化
例えば CRFsuite で l2sgd を使う場合、正則化パラメータとして c2 を指定できます。デフォルトは 1 です。
% crfsuite learn -H -a l2sgd
CRFSuite 0.12.2 Copyright (c) 2007-2013 Naoaki Okazaki
PARAMETERS for l2sgd (crf1d):
float feature.minfreq = 0.000000;
The minimum frequency of features.
int feature.possible_states = 0;
Force to generate possible state features.
int feature.possible_transitions = 0;
Force to generate possible transition features.
float c2 = 1.000000;
Coefficient for L2 regularization.
int max_iterations = 1000;
The maximum number of iterations (epochs) for SGD optimization.
(snip)
正則化パラメータは小さすぎると過学習するし、大きすぎると精度が悪くなるので、グリッドサーチなどで適切な値を選ぶことが多いかと思います。1
というわけで、グリッドサーチするための Ruby のクラスを書いてみたという話です。
コマンドラインで頑張ってみる
crfsuite コマンドには cross validation 用に -x オプションがあるので、分割数を -g オプションで指定することで、各試行の結果がわかります。
なので、以下のように c2 の値を変えて何度も実行することで、最適な値を確認することができます。
% for c2 in 0.001 0.01; do
for> crfsuite learn -a l2sgd -p c2=$c2 -g3 -x /path/to/input
for> done
が、目視で確認するのは辛すぎるので出力結果をパースするスクリプトが必須と思われます。
Ruby で頑張る
というわけで、本題です。次のコードは grid search するクラスの定義です。
crfsuite gem のインストールや、この後使う train.crfsuite.txt の作成については「CRFsuite を Ruby から実行してみた」を参照してください。
# Copyright 2015- Takeshi Arabiki
# License: MIT License (http://opensource.org/licenses/MIT)
require 'crfsuite'
require 'tempfile'
module Crfsuite
class Parameter
def initialize(name, algorithm_name, algorithm_value)
@name = name
@trainer = Trainer.new
@trainer.select(algorithm_name, algorithm_value)
end
def grid_search(xseqs, yseqs, param_values, cv: 10)
dataset = xseqs.zip(yseqs)
scores_by_param_value = Hash.new{ |h, k| h[k] = [] }
n = (dataset.size / cv.to_f).ceil
dataset_groups = dataset.each_slice(n).to_a
dataset_groups.size.times do |i|
@trainer.clear
groups = dataset_groups.dup
test_data = groups.delete_at(i)
groups.each do |group|
group.each do |xseq, yseq|
@trainer.append(xseq, yseq, 0)
end
end
param_values.each do |param_value|
@trainer.set(@name, param_value.to_s)
@trainer.train(temp_model_file, -1)
tagger = Crfsuite::Tagger.new
tagger.open(temp_model_file)
correct_count = test_data.count do |xseq, yseq|
tagger.set(xseq)
predicted_yseq = tagger.viterbi
predicted_yseq.to_a == yseq.to_a
end
scores_by_param_value[param_value] << correct_count.to_f / test_data.size
end
end
scores_by_param_value.map do |param_value, scores|
GridScore.new(param_value, scores)
end
end
private
def temp_model_file
@temp_model_file ||= Tempfile.new('model').tap { |f|
f.close
}.path
end
class GridScore
include Comparable
attr_reader :param_value, :scores
def initialize(param_value, scores)
@param_value = param_value
@scores = scores
end
def mean_score
@mean_score ||= scores.reduce(&:+) / scores.size
end
def <=>(other)
mean_score <=> other.mean_score
end
end
end
end
次のように使います。instances の定義はこれです。
require 'crfsuite'
require 'crfsuite/parameter'
trainer = Crfsuite::Trainer.new
xseqs = []
yseqs = []
# Read training instances from STDIN, and set them to trainer.
instances($stdin).each do |xseq, yseq|
xseqs << xseq
yseqs << yseq
end
parameter = Crfsuite::Parameter.new('c2', 'l2sgd', 'crf1d')
grid_scores = parameter.grid_search(xseqs, yseqs, [0.001, 0.01, 0.1, 1, 10], cv: 3)
grid_scores.each do |score|
scores = score.scores.map{ |s| s.round(3) }
mean_score = score.mean_score.round(3)
puts "param_value: #{score.param_value}, mean_score: #{mean_score}, scores: #{scores}"
end
puts "Best param value: #{grid_scores.max.param_value}"
実行してみます。
% ruby -I. sample_train.rb < train.crfsuite.txt
param_value: 0.001, mean_score: 0.572, scores: [0.54, 0.602, 0.574]
param_value: 0.01, mean_score: 0.573, scores: [0.543, 0.602, 0.574]
param_value: 0.1, mean_score: 0.573, scores: [0.543, 0.602, 0.574]
param_value: 1, mean_score: 0.564, scores: [0.533, 0.599, 0.559]
param_value: 10, mean_score: 0.497, scores: [0.47, 0.534, 0.485]
Best param value: 0.1
出力結果から、c2 の値は 0.1 が良いということがわかりますね。なんとなく 0.01 と 0.1 の間にありそうなので、この間で再度 grid search を行うとより良い値が見つかりそうです。