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