北野坂備忘録

主にインストールやプログラミングのメモを載せています。

言語処理100本ノック 2015年版 (30~39)

第4章は形態素解析。しかし、どちらかというと matplotlibが問題、それも操作ではなくてインストールとの闘いだと思われます。

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

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

まずもってmecabとその結果の話をしないと分からないでしょう。
以下のような形態素解析結果があるとします。
(辞書は NAIST Japanese Dictionary を使用)

生れ    動詞,自立,*,*,一段,連用形,生れる,ウマレ,ウマレ,うまれ/生まれ/生れ,

素性は以下のとおりです(素性とは何か、というのがまたややこしいんですが割愛します)。

表層形 生れ
品詞 動詞
品詞細分類1 自立
品詞細分類2 *
品詞細分類3 *
活用型 一段
活用形 連用形
基本形 生れる
読み ウマレ
発音 ウマレ

つまり、「表層形 生れ」「基本形 生れる」「品詞 動詞」「品詞細分類1 自立」を抽出し、マッピング型に格納しろ、ということです。マッピング型というのは他の言語を念頭にしたものいいで、pythonには辞書型しかありません。辞書型のキーは指定されています。

#!/usr/bin/env python

import codecs
import re

fin = codecs.open('neko.txt.mecab', 'r', 'utf_8')
fmap = {}
string = []
src = []

def getfeature(line):
  l = re.sub(r"\t",",",line)
  l = re.split(",",l)
  fmap['surface'] = l[0]
  fmap['base'] = l[7]
  fmap['pos'] = l[1]
  fmap['pos1'] = l[2]
  return fmap

if __name__ == "__main__":
  for line in fin:
    if re.search("EOS",line):
      continue
    b = getfeature(line)
    string.append(b.copy())
    if b['surface'] == "。" or b['surface'] == "!" or b['surface'] == "?" or b['surface'] == "」":
      src.append(string.copy())
      string = []
  print(src)

結果(最終2文)

[{'base': '南無阿弥陀仏', 'surface': '南無阿弥陀仏', 'pos1': '一般', 'pos': '名詞'}, {'base': '南無阿弥陀仏', 'surface': '南無阿弥陀仏', 'pos1': '一般', 'pos': '名詞'}, {'base': '。', 'surface': '。', 'pos1': '句点', 'pos': '記号'}], 
[{'base': 'ありがたい', 'surface': 'ありがたい', 'pos1': '自立', 'pos': '形容詞'}, {'base': 'ありがたい', 'surface': 'ありがたい', 'pos1': '自立', 'pos': '形容詞'}, {'base': '。', 'surface': '。', 'pos1': '句点', 'pos': '記号'}]]

一文の判定には 。!?」 を使用。
copy()を使わないとpythonの参照渡しの罠にひっかかってすべて同じオブジェクトになってしまいます。

31. 動詞

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

 getfeature関数に2行足すだけ。使わない部分を削除。

#!/usr/bin/env python

import codecs
import re

fin = codecs.open('neko.txt.mecab', 'r', 'utf_8')
fmap = {}

def getfeature(line):
  l = re.sub(r"\t",",",line)
  l = re.split(",",l)
  fmap['surface'] = l[0]
  fmap['base'] = l[7]
  fmap['pos'] = l[1]
  fmap['pos1'] = l[2]
  if fmap['pos'] == "動詞":
    print(fmap['surface'])
  return fmap

if __name__ == "__main__":
  for line in fin:
    if re.search("EOS",line):
      continue
    b = getfeature(line)

結果(最後)

得
切り落し
し
入る
死ぬ
死ん
得る
死な
得
られ
32. 動詞の原形

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

 31から1行変えるだけ。

#!/usr/bin/env python

import codecs
import re

fin = codecs.open('neko.txt.mecab', 'r', 'utf_8')
fmap = {}

def getfeature(line):
  l = re.sub(r"\t",",",line)
  l = re.split(",",l)
  fmap['surface'] = l[0]
  fmap['base'] = l[7]
  fmap['pos'] = l[1]
  fmap['pos1'] = l[2]
  if fmap['pos'] == "動詞":
    print(fmap['base'])
  return fmap

if __name__ == "__main__":
  for line in fin:
    if re.search("EOS",line):
      continue
    b = getfeature(line)

結果(最後)

得る
切り落す
する
入る
死ぬ
死ぬ
得る
死ぬ
得る
られる
33. サ変名詞

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

 条件を変更するだけ。

#!/usr/bin/env python

import codecs
import re

fin = codecs.open('neko.txt.mecab', 'r', 'utf_8')
fmap = {}

def getfeature(line):
  l = re.sub(r"\t",",",line)
  l = re.split(",",l)
  fmap['surface'] = l[0]
  fmap['base'] = l[7]
  fmap['pos'] = l[1]
  fmap['pos1'] = l[2]
  if fmap['pos'] == "名詞" and fmap['pos1'] == "サ変接続":
    print(fmap['base'])
  return fmap

if __name__ == "__main__":
  for line in fin:
    if re.search("EOS",line):
      continue
    b = getfeature(line)

結果(最後)

行水
行水
行水
拷問
抵抗
見当
判然
*

 一部の空白と * がサ変接続名詞扱いになっていることに気づきましたが、MeCab側の問題なので置いておきます。

34. 「AのB」

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

 名詞句用のキュー型辞書nounを準備しておき、上記の条件に適合すると表示します。

#!/usr/bin/env python

import codecs
import re

fin = codecs.open('neko.txt.mecab', 'r', 'utf_8')
fmap = {}
string = []
src = []
noun = [{'surface':0,'base':0,'pos':0,'pos1':0},{'surface':0,'base':0,'pos':0,'pos1':0},{'surface':0,'base':0,'pos':0,'pos1':0}]

def getfeature(line):
  l = re.sub(r"\t",",",line)
  l = re.split(",",l)
  fmap['surface'] = l[0]
  fmap['base'] = l[7]
  fmap['pos'] = l[1]
  fmap['pos1'] = l[2]
  return fmap

if __name__ == "__main__":
  for line in fin:
    if re.search("EOS",line):
      continue
    b = getfeature(line)
    string.append(b.copy())
#nounに追加
    noun.pop(0)
    noun.append(b.copy())
    if noun[0]['pos'] == "名詞" and noun[1]['surface']=="の" and noun[2]['pos'] == "名詞":
      print(noun[0]['surface'] + noun[1]['surface'] + noun[2]['surface'])
    if b['surface'] == "。" or b['surface'] == "!" or b['surface'] == "?" or b['surface'] == "」":
      src.append(string.copy())
      string = [""]

結果(最後)

水の面
甕の縁
爪のかかり
甕のふち
爪のかかり
年の間
自然の力
水の中
座敷の上
不可思議の太平
35. 名詞の連接

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

 方針はさきほどと同じで名詞句 noun を使用。
 名詞が連続すれば nstring を伸ばしていき、現時点最長の mostlenstring より長ければ入れ替えます。

#!/usr/bin/env python

import codecs
import re

fin = codecs.open('neko.txt.mecab', 'r', 'utf_8')
fmap = {}
noun = [{'surface':0,'base':0,'pos':0,'pos1':0},{'surface':0,'base':0,'pos':0,'pos1':0}]
nstring=""
nstringlen=0
mostlenstring=""

def getfeature(line):
  l = re.sub(r"\t",",",line)
  l = re.split(",",l)
  fmap['surface'] = l[0]
  fmap['base'] = l[7]
  fmap['pos'] = l[1]
  fmap['pos1'] = l[2]
  return fmap

if __name__ == "__main__":
  for line in fin:
    if re.search("EOS",line):
      continue
    b = getfeature(line)
#nounに追加
    noun.pop(0)
    noun.append(b.copy())
    if noun[0]['pos'] == "名詞" and noun[1]['pos'] == "名詞":
      if nstring == "":
        nstring = noun[0]['surface'] + noun[1]['surface']
      else:
        nstring = nstring + noun[1]['surface']
    else:
      if nstring:
        if len(nstring) > nstringlen:
          print(nstring)
          print(len(nstring))
          mostlenstring = nstring
          nstringlen = len(nstring)
      nstring=""
  print(mostlenstring)
  print(len(mostlenstring))

見事に失敗していますが結果を掲載します。

結果

人間中
3
一番獰悪
4
こんな片輪
5
ぷうぷうと煙
6
その後いろいろ経験
9
大家アンドレア・デル・サルト
14
ちゃらちゃらちゃらちゃら続け様
15
一ぷくふかしているとようやく甘木先生
18
ちゃんちゃんにしたらさぞあたたかでよかろうと
22
ちゃんちゃんにしたらさぞあたたかでよかろうと
22

 なぜ失敗するかというと、MeCabの判定がおかしい。

ゃんちゃんにしたらさぞあたたかでよかろうと	名詞,一般,*,*,*,*,*

 なぜこれが名詞になるのか……。

36. 単語の出現頻度

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

カウントする単語は基本形か表層型かで話が変わります。今回は表層型で出現頻度を求めました。
辞書に放り込んでいきます。同じものがなければ'x':1にして、同じものがあれば+1していきます。

#!/usr/bin/env python

import codecs
import re

fin = codecs.open('neko.txt.mecab', 'r', 'utf_8')
fmap = {}
dic = {}

def getfeature(line):
  l = re.sub(r"\t",",",line)
  l = re.split(",",l)
  fmap['surface'] = l[0]
  fmap['base'] = l[7]
  fmap['pos'] = l[1]
  fmap['pos1'] = l[2]
  return fmap

if __name__ == "__main__":
  for line in fin:
    if re.search("EOS",line):
      continue
    b = getfeature(line)
    if b['surface'] not in dic:
      dic[b['surface']] = 1
    else:
      dic[b['surface']] += 1
  for k,v in sorted(dic.items(), key=lambda x:x[1],reverse=True):
    print(k,v)

結果(上位10位)

の 9196
。 7486
て 6799
、 6772
は 6416
に 6160
を 6047
と 5482
が 5338
た 3968
37. 頻度上位10語

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

 matplotlibのインストールでつまづく人も多いと思われますが割愛。

#!/usr/bin/env python

import codecs
import re
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm

fin = codecs.open('neko.txt.mecab', 'r', 'utf_8')
fmap = {}
dic = {}
n=0
x=[]
y=[]
prop = fm.FontProperties(fname='/usr/share/fonts/ipa-pgothic/ipagp.ttf')

def getfeature(line):
  l = re.sub(r"\t",",",line)
  l = re.split(",",l)
  fmap['surface'] = l[0]
  fmap['base'] = l[7]
  fmap['pos'] = l[1]
  fmap['pos1'] = l[2]
  return fmap

if __name__ == "__main__":
  for line in fin:
    if re.search("EOS",line):
      continue
    b = getfeature(line)
    if b['surface'] not in dic:
      dic[b['surface']] = 1
    else:
      dic[b['surface']] += 1
  for k,v in sorted(dic.items(), key=lambda x:x[1],reverse=True):
    x.append(k)
    y.append(v)
    n += 1
    if n > 9:
     break
  plt.bar([1,2,3,4,5,6,7,8,9,10],y,align="center")
  plt.xticks([1,2,3,4,5,6,7,8,9,10],x,fontproperties=prop)
  plt.show()

 fontpropertiesで設定しないと日本語が表示されず謎の四角になる。
結果
f:id:kenichia:20160101153458p:plain

38. ヒストグラム

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

 matplotlibのhistを使用。カンタン。

#!/usr/bin/env python

import codecs
import re
import matplotlib.pyplot as plt

fin = codecs.open('neko.txt.mecab', 'r', 'utf_8')
fmap = {}
dic = {}
n=0
x=[]
y=[]

def getfeature(line):
  l = re.sub(r"\t",",",line)
  l = re.split(",",l)
  fmap['surface'] = l[0]
  fmap['base'] = l[7]
  fmap['pos'] = l[1]
  fmap['pos1'] = l[2]
  return fmap

if __name__ == "__main__":
  for line in fin:
    if re.search("EOS",line):
      continue
    b = getfeature(line)
    if b['surface'] not in dic:
      dic[b['surface']] = 1
    else:
      dic[b['surface']] += 1
  for k,v in sorted(dic.items(), key=lambda x:x[1],reverse=True):
    print(k,v)
    y.append(v)
  plt.hist(y,bins=10)
  plt.show()

結果
f:id:kenichia:20160101153504p:plain
何か失敗しているように見えますが、出現頻度が1000以上のものは各ビンで1ケタしかないためです。

39. Zipfの法則

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

 まずは対数を使わないグラフを作って、最後に plt.xscale("log") と plt.yscale("log") を適用すると何をしているかよくわかると思います。

#!/usr/bin/env python

import codecs
import re
import matplotlib.pyplot as plt

fin = codecs.open('neko.txt.mecab', 'r', 'utf_8')
fmap = {}
dic = {}
x=[]
y=[]

def getfeature(line):
  l = re.sub(r"\t",",",line)
  l = re.split(",",l)
  fmap['surface'] = l[0]
  fmap['base'] = l[7]
  fmap['pos'] = l[1]
  fmap['pos1'] = l[2]
  return fmap

if __name__ == "__main__":
  for line in fin:
    if re.search("EOS",line):
      continue
    b = getfeature(line)
    if b['surface'] not in dic:
      dic[b['surface']] = 1
    else:
      dic[b['surface']] += 1
  for k,v in sorted(dic.items(), key=lambda x:x[1],reverse=True):
    y.append(v)
  plt.xscale("log")
  plt.yscale("log")
  plt.plot(y)
  plt.show()

結果
f:id:kenichia:20160101153510p:plain