myfinderの技術や周辺的活動のblog

2008年12月23日火曜日

人工無脳(chatbot)を実装してみる -第四回-

第四回は、ユーザの入力を形態素解析してキーワードを抽出し、キーワードと入力をパターン辞書に学習させる機能を追加します。
例によって「恋するプログラム」の例に倣ってやっています。

今回形態素を解析するのに、Rubyバインディングの存在するMeCabを利用しました。
下記はMeCabをラップするRubyのクラスです。

require 'MeCab'

class Morph

# 初期化
def initialize
@mecab = MeCab::Tagger.new("-Osimple")
end

def parse(text)
@mecab.parse(text)
end

def parse_nbest(n, text)
@mecab.parse_nbest(n, text)
end

def analyze(text)
parsed_text = parse(text)

# EOS部分がnilとなって返るのを防ぐ
parsed_data = []
parsed_text.split(/\n/).map do |line|
if line != "EOS"
parsed_data.push(line.split(/\t/))
end
end
return parsed_data
end

private :parse, :parse_nbest

def keyword?(part)
return /名詞-(一般|固有名詞|サ変接続|形容動詞語幹)/ =~ part
end
end

これで、Morphのインスタンスを取得すれば、形態素解析やキーワード判定が行なえるようになります。
入力を形態素解析するのは、無脳クラスのdialogueメソッドが適当と考えられるので、そこに下記のように追記します。

parts = @morph.analyze(input)

analyzeメソッドは、MeCabのparseを呼び出すラッパメソッドです。
戻り値には、表層系と品詞分類の配列の配列(二次元配列)が返ります。

ここまでで形態素の解析は出来ました、引き続きこれをパターン辞書に学習させる実装を行ないます。
学習させるということで、今回形態素解析した結果を引き渡す先はDictionaryクラスのstudyメソッドが適当と思われますので、そこに解析結果を引き渡します。

@dictionary.study(input, parts)

これまでのstudyメソッドは、単純にランダム辞書に入力を書き込んでいただけでしたが、パターン辞書への登録も受け付けられるように改修します。

def study(input, parts)
study_random(input)
study_pattern(input, parts)
end

study_randomは以前のままで、今回新規にstudy_patternというメソッドを追加しました。
このメソッドに、ユーザの入力と形態素解析結果を引き渡して、パターン辞書に学習させます。

def study_pattern(input, parts)
parts.each do |word, part|
next unless @morph.keyword?(part)
duped = @pattern.find{|pattern_item| pattern_item.pattern == word}
if duped
duped.add_phrase(input)
else
@pattern.push(PatternItem.new(word, input))
end
end
end

形態素解析の結果について処理を行なっています。
keyword?メソッドで、品詞情報が「名詞-(一般|固有名詞|サ変接続|形容動詞語幹)」という正規表現にマッチするかどうかを判断し、マッチすればキーワード登録の対象として次の処理に渡します。
単純に「名詞」だけを対象にすると、あまりに多くのワードを引っ掛けてしまうため、名詞として処理する対象を絞るため、このようにしています。
また、そのまま登録すると、キーワードが重複してしまう可能性があるため、重複チェックを行なっています。
重複チェックし、すでに存在するキーワードであれば、そのキーワードに入力を追記し、存在しなければ新規キーワードとして追加するのです。

最後に、学習したパターン辞書を保存する処理を追加します。

open('dicts/pattern.txt', 'w') do |file|
@pattern.each do |pattern_item|
file.puts(pattern_item.make_line)
end
end

ランダム辞書は単純にファイルの最後に追記するだけでしたが、パターン辞書はパターン辞書フォーマットに合わせた文字列に変換する必要があります。
そこで、PatternItemクラスに変換メソッドを用意しました。

def make_line
# 必要機嫌値##応答例
pattern = @modify.to_s + "##" + @pattern
# pattern\t必要機嫌値##応答例
phrases = @phrases.map { |phrase|
phrase["need"].to_s + "##" + phrase["phrase"]
}
return pattern + "\t" + phrases.join("|")
end

パターン辞書フォーマットは「必要機嫌値##パターン\t必要機嫌値##応答例(|応答例)」といった形式でした。
今回はそれに沿った形で文字列をreturnする実装としています。

ここまでで、名詞を抽出してパターン辞書に登録する処理を実装することができました。
が、MeCabも万能ではないので、キーワードとして不適切なものが登録されてしまう可能性は多いにあり得ます。

例えば「攻殻機動隊」をMeCabで形態素解析すると

攻殻機動隊
攻 名詞,固有名詞,人名,名,*,*,攻,オサム,オサム
殻 名詞,一般,*,*,*,*,殻,カラ,カラ
機動 名詞,一般,*,*,*,*,機動,キドウ,キドー
隊 名詞,接尾,一般,*,*,*,隊,タイ,タイ

となってしまい、現在の仕様のままでは、全く関係ない語である「攻」「殻」「機動」が下記のようにそれぞれ別のキーワードとして登録されてしまいます。

0##攻 0##攻殻機動隊のDVD
0##殻 0##攻殻機動隊のDVD
0##機動 0##攻殻機動隊のDVD


これらは、パターン辞書のメンテナンスや、MeCab辞書のメンテナンスで補うしかないのが現状です。
でもでも、これでさらに会話のバリエーションが充実する&使われれば使われるほどボキャブラリが増えていくようになってきました。

今回はここまで。

0 件のコメント: