Rubyで形態素解析
最近そこそこに忙しくてなかなか自由時間がとれないのでコード書きたい欲が大分溜まっています。
そんなところに大学の自然言語処理を扱う授業の輪講の順番が回ってきたので、スライドを作るついでにデモプログラムを書くことにしました。
どうも自然言語処理の分野ではPythonが強くRubyにはあまりライブラリが充実していないらしいのですが、父親から授けられた「まつもとゆきひろ コードの世界」が本棚からオーラを放っていたのでRubyで書いてみることに。
やっていること
形態素解析。辞書データをもとに文章をばらばらにします。
NAIST辞書
http://sourceforge.jp/projects/naist-jdic/
から単語のデータをいただいて、見出し語と単語コストだけ抜き出して辞書ファイルを作りました。
それをHashに読み込んで使っています。
以下メソッドの説明。
- longestMatch(string)
最長一致法。その名の通り文中で一番長い単語から確定していく手法。
再帰で実装。
- smallestCost(string)
接続コスト最小法の似非実装。
単語の接続にかかるコストの和が最小になるように分割します。
NAIST辞書から文法の情報をごっそり抜き取ってるので品詞間のコストを全く考えていません。
DPを使って実装している、はず。
これらの手法について詳しくは輪講の資料をご覧ください。
http://www.slideshare.net/domitry/8-22801849
問題点
- 未知語への対応をほとんど考えていない
最長一致法では未知語には一応かっこをつけていますが、接続コスト最小法に至っては適当に1文字で分割しているだけです。
しかしこれの対策だけでひとつ分野があるくらいなので今回はパス。
- 動詞の活用展開ができない
これは実用面で致命的。辞書データを変換するときについでに展開しておくべきだった?
使い方
辞書をカレントフォルダに置いて適当にNltkのインスタンスを作ってつっこんでもらえれば動きます。
辞書データは[見出し語,コスト]の順番。
class Nltk def initialize file = open(File.expand_path("../normal.dic",__FILE__),"r:UTF-8") @dic = Hash.new file.each{|line| line.chomp! line =~ /(\W+),(\d+)/ @dic[Regexp.last_match(1)] = Regexp.last_match(2).to_i } end def longestMatch(string) if string.empty? return [] end max_len = string.length<5 ? string.length : 5 max_len.downto(1) {|len| for seek in 0..(string.length-len) do tmp = string[seek,len] if @dic.key?(tmp.encode("UTF-8")) prefix = longestMatch(string[0,seek]) safix = longestMatch(string[seek+len,string.length-seek-len]) return prefix + [tmp] + safix end end } #not found in dic return ["("+string+")"] end def getAllWords(string) #get all words contained in string words = Array.new(string.length) for seek in 0..string.length-1 do words[seek] = Array.new max_len = string.length-seek < 5 ? string.length-seek : 5 max_len.downto(1){|len| tmp = string[seek,len] if @dic.key?(tmp.encode("UTF-8")) words[seek].push(tmp) end } end return words end def smallestCost(string) words = getAllWords(string) #get all combinations combs = Array.new(string.length) (string.length-1).downto(0){|seek| combs[seek]=Array.new #unresistered word if words[seek].empty? words[seek].push(string[seek]) end words[seek].each do |word| cost = @dic[word.encode("UTF-8")] if cost == nil cost = 0 end if seek + word.length == string.length combs[seek].push([word,cost]) else combs[seek+word.length].each do |comb| tmp = [word]+comb cost_sum = tmp.pop + cost tmp.push(cost_sum) combs[seek].push(tmp) end end end } return combs[0] end end