言語処理100本ノック2015 をRubyでやる【第4章】
コードは GitHub に随時上げていきます。この記事では省略した長い出力も output/ ディレクトリに置いてます。
今回は第 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 はいらないので **
でまとめて拾ってやります。
出力(一部省略)
生れ つか し 泣い し いる 始め 見 聞く 捕え ...
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 使ってみたかったんですよ! で、チュートリアルでグラフ描画してたので参考にしてみました。
自分の環境だと matplotlib が動かなくて数時間悩んだんですが、この行が必要だったみたいです。
mp.rcParams[:backend] = 'TkAgg' if mp.rcParams[:backend] == 'MacOSX'
出力
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()
出力
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()
出力
もっと良い書き方あるよ〜などあれば issue とかで教えてもらえるとうれしいです!