ようじょのおえかきちょう

ふぇぇ お医者さんにペン持ったらダメっていわれた〜〜

言語処理100本ノック2015 をRubyでやる【第4章】

コードは GitHub に随時上げていきます。この記事では省略した長い出力も output/ ディレクトリに置いてます。

github.com

今回は第 4 章「形態素解析」です。

夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をMeCabを使って形態素解析し,その結果をneko.txt.mecabというファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ.

なお,問題37, 38, 39はmatplotlibもしくはGnuplotを用いるとよい.

30. 形態素解析結果の読み込み

形態素解析結果(neko.txt.mecab)を読み込むプログラムを実装せよ.ただし,各形態素は表層形(surface),基本形(base),品詞(pos),品詞細分類1(pos1)をキーとするマッピング型に格納し,1文を形態素マッピング型)のリストとして表現せよ.第4章の残りの問題では,ここで作ったプログラムを活用せよ.

解答

# util.rb

def morphologies(filename: 'neko.txt.mecab')
  sentence_list = []

  File.open(filename) do |lines|
    sentence = []
    lines.each do |line|
      if line == "EOS\n"
        unless sentence.empty?
          sentence_list << sentence
          sentence = []
        end
      else
        elements = line.split(/[,\t]/)
        word = {
          surface: elements[0],
          base: elements[7],
          pos: elements[1],
          pos1: elements[2]
        }
        sentence << word
      end
    end
  end

  sentence_list
end

以降の問題で使うので util.rb にまとめて書いておきます。

nako.txt.mecab はこんな感じです。ここでは、EOS がきたら文章が終わるとみなします。

...

  記号,空白,*,*,*,*, , , 
吾輩  名詞,代名詞,一般,*,*,*,吾輩,ワガハイ,ワガハイ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
猫 名詞,一般,*,*,*,*,猫,ネコ,ネコ
で 助動詞,*,*,*,特殊・ダ,連用形,だ,デ,デ
ある  助動詞,*,*,*,五段・ラ行アル,基本形,ある,アル,アル
。 記号,句点,*,*,*,*,。,。,。
EOS
名前  名詞,一般,*,*,*,*,名前,ナマエ,ナマエ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
まだ  副詞,助詞類接続,*,*,*,*,まだ,マダ,マダ
無い  形容詞,自立,*,*,形容詞・アウオ段,基本形,無い,ナイ,ナイ
。 記号,句点,*,*,*,*,。,。,。
EOS
EOS

...
# 30.rb

require './util'

morphologies.each do |sentence|
  sentence.each do |word|
    puts word.values.join(' ')
  end
  puts
end

出力(一部省略)

...

    記号 空白
吾輩 吾輩 名詞 代名詞
は は 助詞 係助詞
猫 猫 名詞 一般
で だ 助動詞 *
ある ある 助動詞 *
。 。 記号 句点

名前 名前 名詞 一般
は は 助詞 係助詞
まだ まだ 副詞 助詞類接続
無い 無い 形容詞 自立
。 。 記号 句点

...

31. 動詞

動詞の表層形をすべて抽出せよ.

解答

require './util'

morphologies.each do |sentence|
  sentence.each do |surface:, pos:, **|
    puts surface if pos == "動詞"
  end
end

名前付き引数を使って surface と pos を受け取ります。後の key はいらないので ** でまとめて拾ってやります。

qiita.com

出力(一部省略)

生れ
つか
し
泣い
し
いる
始め
見
聞く
捕え

...

32. 動詞の原形

動詞の原形をすべて抽出せよ.

解答

require './util'

morphologies.each do |sentence|
  sentence.each do |base:, pos:, **|
    puts base if pos == "動詞"
  end
end

出力(一部省略)

生れる
つく
する
泣く
する
いる
始める
見る
聞く
捕える

...

33. サ変名詞

サ変接続の名詞をすべて抽出せよ.

解答

require './util'

morphologies.each do |sentence|
  sentence.each do |surface:, pos:, pos1:, **|
    puts surface if pos == "名詞" && pos1 == "サ変接続"
  end
end

出力(一部省略)

未知語が「サ変接続の名詞」として認識されたりするみたいです。—— とか。

見当
記憶
話
装飾
突起
運転
記憶
分別
決心
我慢

...

34. 「AのB」

2つの名詞が「の」で連結されている名詞句を抽出せよ.

解答

# util.rb

class Array
  def ngram(n)
    self.each_cons(n).select { |gram| gram.size == n }
  end
# 34.rb

require './util'

morphologies.each do |sentence|
  sentence.ngram(3).each do |(first, second, third)|
    if second[:surface] == "" && first[:pos] == "名詞" && third[:pos] == "名詞"
      puts [first, second, third].map(&->word { word[:surface] }).join
    end
  end
end

(first, second, third) で分割して拾うところ好きです。

出力(一部省略)

彼の掌
掌の上
書生の顔
はずの顔
顔の真中
穴の中
書生の掌
掌の裏
何の事
肝心の母親

...

35. 名詞の連接

名詞の連接(連続して出現する名詞)を最長一致で抽出せよ.

解答

require './util'

long_nouns = []

morphologies.each do |sentence|
  nouns = []
  sentence.each do |pos:, surface:, **|
    if pos == "名詞"
      nouns << surface
    else
      long_nouns << nouns if nouns.length > 1
      nouns = []
    end
  end
end

puts long_nouns.map(&:join)

出力(一部省略)

人間中
一番獰悪
時妙
一毛
その後猫
一度
ぷうぷうと煙
邸内
三毛
書生以外

...

36. 単語の出現頻度

文章中に出現する単語とその出現頻度を求め,出現頻度の高い順に並べよ.

解答

# util.rb

class Array
  def word_freq
    words = self.flatten
    words.group_by{ |surface:, **| surface }.map{ |k, v| [k, v.count] }.to_h
  end
end
# 36.rb

require './util'

freqs = morphologies.word_freq
freqs.sort_by{ |word, count| [-count, word] }.each do |word, count|
  puts "#{word} #{count}"
end

出力(一部省略)

の 9194
。 7486
て 6868
、 6772
は 6420
に 6243
を 6071
と 5508
が 5337
た 3988

...

37. 頻度上位10語

出現頻度が高い10語とその出現頻度をグラフ(例えば棒グラフなど)で表示せよ.

解答

require './util'
require 'pycall/import'
include PyCall::Import

# MacOSX backend is not usable through pycall...
pyimport 'matplotlib', as: :mp
mp.rcParams[:backend] = 'TkAgg' if mp.rcParams[:backend] == 'MacOSX'

pyimport 'matplotlib.mlab', as: 'mlab'
pyimport 'matplotlib.pyplot', as: 'plt'


sorted_freqs = morphologies.word_freq.sort_by{ |word, count| [-count, word] }

x = (0...10).to_a
y = sorted_freqs[0...10].map{ |_, count| count }
labels = sorted_freqs[0...10].map{ |word, _| word }

plt.bar(x, y, tick_label: labels)
plt.show()

PyCall 使ってみたかったんですよ! で、チュートリアルでグラフ描画してたので参考にしてみました。

qiita.com

自分の環境だと matplotlib が動かなくて数時間悩んだんですが、この行が必要だったみたいです。

mp.rcParams[:backend] = 'TkAgg' if mp.rcParams[:backend] == 'MacOSX'

github.com

出力

f:id:yamasy1549:20171231170220p:plain

38. ヒストグラム

単語の出現頻度のヒストグラム(横軸に出現頻度,縦軸に出現頻度をとる単語の種類数を棒グラフで表したもの)を描け.

解答

require './util'
require 'pycall/import'
include PyCall::Import

# MacOSX backend is not usable through pycall...
pyimport 'matplotlib', as: :mp
pyimport 'numpy', as: :np
mp.rcParams[:backend] = 'TkAgg' if mp.rcParams[:backend] == 'MacOSX'

pyimport 'matplotlib.mlab', as: 'mlab'
pyimport 'matplotlib.pyplot', as: 'plt'


sorted_freqs = morphologies.word_freq.sort_by{ |word, count| [-count, word] }

counts = sorted_freqs.map{ |_, count| count }
plt.hist(counts, bins: 30)
plt.yscale("log")
plt.show()

ほぼ Python ですね。Ruby なんですけど。

出力

f:id:yamasy1549:20171231173858p:plain

39. Zipfの法則

単語の出現頻度順位を横軸,その出現頻度を縦軸として,両対数グラフをプロットせよ.

解答

require './util'
require 'pycall/import'
include PyCall::Import

# MacOSX backend is not usable through pycall...
pyimport 'matplotlib', as: :mp
pyimport 'numpy', as: :np
mp.rcParams[:backend] = 'TkAgg' if mp.rcParams[:backend] == 'MacOSX'

pyimport 'matplotlib.mlab', as: 'mlab'
pyimport 'matplotlib.pyplot', as: 'plt'


sorted_freqs = morphologies.word_freq.sort_by{ |word, count| [-count, word] }

counts = sorted_freqs.map{ |_, count| count }
plt.xscale('log')
plt.yscale('log')
plt.plot(counts)
plt.show()

出力

f:id:yamasy1549:20171231174657p:plain


もっと良い書き方あるよ〜などあれば issue とかで教えてもらえるとうれしいです!

github.com