はじめに
言語処理100本ノックの続き、今回は第4章をやっていきます。
過去分はこちら↓
第1章 00~09 - 第2章 10~19 - 第3章20~29
夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をMeCabを使って形態素解析し,その結果をneko.txt.mecabというファイルに保存せよ.
このファイルを用いて,以下の問に対応するプログラムを実装せよ.
なお,問題37, 38, 39はmatplotlibもしくはGnuplotを用いるとよい.
ということで、いよいよ形態素解析など、NLPっぽい内容になってきました。
最初に mecab, mecab-ipadic, crf++, cabocha をインストールしておきましょう。
Macの人はこの辺をご参照あれ。
ちなみに個人的にはipadicよりunidicのほうが好きなんですが、なんとなくipadicのほうがデファクト感あるのでipadicにしておきます。
まずは前座から
夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をMeCabを使って形態素解析し,その結果をneko.txt.mecabというファイルに保存せよ.
この内容をまずやっつけないといけないですね。
#!/usr/bin/env bash
rm neko.txt
curl -O http://www.cl.ecei.tohoku.ac.jp/nlp100/data/neko.txt
mecab < neko.txt > neko.txt.mecab
これで、neko.txt.mecab が出来上がりました。
一 名詞,数,*,*,*,*,一,イチ,イチ
EOS
EOS
記号,空白,*,*,*,*, , ,
吾輩 名詞,代名詞,一般,*,*,*,吾輩,ワガハイ,ワガハイ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
猫 名詞,一般,*,*,*,*,猫,ネコ,ネコ
で 助動詞,*,*,*,特殊・ダ,連用形,だ,デ,デ
ある 助動詞,*,*,*,五段・ラ行アル,基本形,ある,アル,アル
。 記号,句点,*,*,*,*,。,。,。
EOS
名前 名詞,一般,*,*,*,*,名前,ナマエ,ナマエ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
まだ 副詞,助詞類接続,*,*,*,*,まだ,マダ,マダ
無い 形容詞,自立,*,*,形容詞・アウオ段,基本形,無い,ナイ,ナイ
。 記号,句点,*,*,*,*,。,。,。
EOS
...(以下略)
これで用意ができたので、本編に行きましょう。
30. 形態素解析結果の読み込み
形態素解析結果(neko.txt.mecab)を読み込むプログラムを実装せよ.
ただし,各形態素は表層形(surface),基本形(base),品詞(pos),品詞細分類1(pos1)をキーとするマッピング型に格納し,1文を形態素(マッピング型)のリストとして表現せよ.第4章の残りの問題では,ここで作ったプログラムを活用せよ.
前の章では「辞書型」って読んでた気がしますが、とにかく1形態素ごとに辞書に格納して、そのリストを作りましょう、ということですね。
こういう構造のデータで読み込んでいきましょう。
neko.txt.mecab のファイル構造は、以下の通りになっています。
- 文末では
EOS
- それ以外の行は
表層形\t品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音
ただし、最後のふたつ、「読み」と「発音」は未知語のときは付与されません。例えば、「ニャーニャー」にはこれらは付きませんでした。
ニャーニャー 名詞,一般,*,*,*,*,*
泣い 動詞,自立,*,*,五段・カ行イ音便,連用タ接続,泣く,ナイ,ナイ
て 助詞,接続助詞,*,*,*,*,て,テ,テ
これを踏まえて読み込むコードがこちら。
def parse_line(line):
"""
mecabでパースされた一行を読み込みます。
>>> parse_line('吾輩\\t名詞,代名詞,一般,*,*,*,吾輩,ワガハイ,ワガハイ')
{'surface': '吾輩', 'base': '吾輩', 'pos': '名詞', 'pos1': '代名詞'}
>>> parse_line('生れ\\t動詞,自立,*,*,一段,連用形,生れる,ウマレ,ウマレ')
{'surface': '生れ', 'base': '生れる', 'pos': '動詞', 'pos1': '自立'}
"""
(surface, attr) = line.split('\t')
arr = attr.split(',')
return {
'surface': surface,
'base': arr[6],
'pos': arr[0],
'pos1': arr[1]
}
def read_mecab_file(filename) -> [[{}]]:
"""
指定されたファイルからすべての行を読み取って、
一文ごとに1つのリストにして列挙します。
各文の要素は parse_line の返り値のフォーマットです。
# 3文目を取り出して、surfaceだけを連結する。
>>> ''.join([i['surface'] for i in list(read_mecab_file('neko.txt.mecab'))[3]])
'名前はまだ無い。'
"""
sentence = []
with open(filename, mode='rt', encoding='utf-8') as f:
for line in f:
line = line.rstrip('\n')
if line == 'EOS':
yield sentence
sentence = []
else:
sentence.append(parse_line(line))
31. 動詞
動詞の表層形をすべて抽出せよ.
これは簡単ですね。read_mecab_file
で読み込んだ内容から動詞を取り出して、表層系だけを抽出して表示します。
from nlp30 import *
sentences = read_mecab_file('neko.txt.mecab')
for sentence in sentences:
for word in sentence:
if word['pos'] == '動詞':
print(word['surface'])
これを実行すると、
生れ
つか
し
泣い
し
いる
始め
見
聞く
捕え
煮
食う
...(略)
といった感じで動詞の表層系だけが抽出できました。
32. 動詞の原形
動詞の原形をすべて抽出せよ.
今度は原形です。
先程のコードをちょっとだけ手直しして・・・
from nlp30 import *
sentences = read_mecab_file('neko.txt.mecab')
for sentence in sentences:
for word in sentence:
if word['pos'] == '動詞':
print(word['base'])
で、実行すると。
生れる
つく
する
泣く
する
いる
始める
見る
聞く
捕える
煮る
食う
...(略)
33. サ変名詞
サ変接続の名詞をすべて抽出せよ.
今度はサ変名詞を取り出します。pos1まで見ろということですね。
ちなみに、ipadicが採用しているchasen の品詞体系はこちら。
個人的には「れる」「せる」などの助動詞を動詞扱いにしているのがちょっと気持ち悪いです。
from nlp30 import *
sentences = read_mecab_file('neko.txt.mecab')
for sentence in sentences:
for word in sentence:
if word['pos'] == '名詞' and word['pos1'] == 'サ変接続':
print(word['base'])
実行すると。
見当
記憶
話
装飾
突起
運転
記憶
分別
決心
我慢
餓死
訪問
始末
34. 「AのB」
2つの名詞が「の」で連結されている名詞句を抽出せよ.
ちょっとだけ言語処理っぽくなってきましたね。
「机の上」とか「頭の中」の「の」は、mecabでパースするとどういう品詞になるのでしょうか?
頭の中
頭 名詞,一般,*,*,*,*,頭,アタマ,アタマ
の 助詞,連体化,*,*,*,*,の,ノ,ノ
中 名詞,非自立,副詞可能,*,*,*,中,ナカ,ナカ
EOS
というわけで、品詞は「助詞」、副品詞は「連体化」になるようです。
そこで、これらを踏まえて「名詞」+「の(助詞-連体化」+「名詞」の並びをすべて取り出してみましょう。
from nlp30 import *
sentences = read_mecab_file('neko.txt.mecab')
for sentence in sentences:
for i in range(0, len(sentence) - 2):
if sentence[i]['pos'] == '名詞' and \
sentence[i+1]['surface'] == 'の' and \
sentence[i+1]['pos'] == '助詞' and \
sentence[i+1]['pos1'] == '連体化' and \
sentence[i+2]['pos'] == '名詞':
print('{}の{}'.format(sentence[i]['surface'], sentence[i+2]['surface']))
実行すると。
彼の掌
掌の上
書生の顔
はずの顔
顔の真中
穴の中
書生の掌
掌の裏
何の事
肝心の母親
藁の上
笹原の中
...(略)
というわけで、しっかり「AのB」に当たるような表現が取れました。
やっぱり「上」とか「中」とか「裏」とか、場所を指し示すような名詞が2つ目には来がちですね。
・・・ちょっと気になるので、後半に出現する名詞の頻度をランキング形式で集計してみましょう。
from nlp30 import *
from pprint import pprint
sentences = read_mecab_file('neko.txt.mecab')
second_nouns = {}
for sentence in sentences:
for i in range(0, len(sentence) - 2):
if sentence[i]['pos'] == '名詞' and \
sentence[i+1]['surface'] == 'の' and \
sentence[i+1]['pos'] == '助詞' and \
sentence[i+1]['pos1'] == '連体化' and \
sentence[i+2]['pos'] == '名詞':
noun = sentence[i+2]['surface']
if noun in second_nouns:
second_nouns[noun] += 1
else:
second_nouns[noun] = 1
pprint(sorted(second_nouns.items(), key=lambda kv: kv[1], reverse=True))
「AのB」という表現に出現する「B」側の名詞の出現回数をカウントして、降順にソートして表示しています。
[('よう', 228),
('上', 184),
('方', 161),
('中', 139),
('事', 139),
('ため', 76),
('うち', 59),
('前', 58),
('頭', 57),
('顔', 56),
('もの', 52),
('間', 48),
('所', 36),
('下', 35),
('家', 33),
('鼻', 33),
('眼', 31),
('生徒', 31),
('人間', 29),
('猫', 28),
('人', 28),
('主人', 27),
...(略)
いわゆる形式名詞(「よう」「ため」「事」など)や方向(「上」「下」「中」など)、それから、家とか鼻とか眼とか、とても日常に関わるであろう単語が多いことがわかります。
主人公は「猫」で、「主人」の苦沙弥先生は旧制中学で「生徒」に英語を教えているわけですから、これらの単語は、「吾輩は猫である」らしい単語なのかもしれません。暇な人は他の青空文庫のデータを落として比較してみるといいよ!
35. 名詞の連接
名詞の連接(連続して出現する名詞)を最長一致で抽出せよ.
こんどは「の」が入らないひたすら名詞が連続したものを抽出しましょう、ということです。
from nlp30 import *
from pprint import pprint
sentences = read_mecab_file('neko.txt.mecab')
compound_nouns = []
for sentence in sentences:
current_continuation = []
for word in sentence:
if word['pos'] == '名詞':
current_continuation.append(word['surface'])
else:
if len(current_continuation) >= 2:
compound_nouns.append(''.join(current_continuation))
current_continuation = []
if len(current_continuation) >= 2:
compound_nouns.append(''.join(current_continuation))
pprint(compound_nouns)
で、結果。
['人間中',
'一番獰悪',
'時妙',
'一毛',
'その後猫',
'一度',
'ぷうぷうと煙',
'邸内',
'三毛',
'書生以外',
'四五遍',
'この間おさん',
'三馬',
'御台所',
'まま奥',
'住家',
'終日書斎',
'勉強家',
'勉強家',
'勤勉家',
'二三ページ',
'主人以外',
...(略)
36. 単語の出現頻度
文章中に出現する単語とその出現頻度を求め,出現頻度の高い順に並べよ.
ここで、「単語」は品詞が違うと異なるもので、逆に活用形が違っていても同じ単語だとみなすことにします。
また、漢字とひらがなや送り仮名が違うものは区別しておきましょう。ipadicにはそれをまとめるための情報がついていないのと、きっと校正担当の編集者が有能だったからブレはないはず! 天下の朝日新聞ですし!1
品詞も含めて単語を区別するために、'{原形}\t{品詞}\t{品詞細分類1}'
という文字列をキーにしてカウントしていきます。
from nlp30 import *
import itertools
from pprint import pprint
sentences = read_mecab_file('neko.txt.mecab')
words = {}
for word in itertools.chain.from_iterable(sentences):
key = '{}\t{}\t{}'.format(word['base'], word['pos'], word['pos1'])
if key in words:
words[key] += 1
else:
words[key] = 1
pprint(sorted(words.items(), key=lambda kv: kv[1], reverse=True))
先に35の脱線で出現頻度順の集計をやってしまったので、使いまわしですね。
結果はこうです。
[('。\t記号\t句点', 7486),
('の\t助詞\t連体化', 7032),
('て\t助詞\t接続助詞', 6812),
('、\t記号\t読点', 6772),
('は\t助詞\t係助詞', 6420),
('を\t助詞\t格助詞', 6071),
('だ\t助動詞\t*', 5975),
('に\t助詞\t格助詞', 5470),
('た\t助動詞\t*', 4267),
('が\t助詞\t格助詞', 4196),
('と\t助詞\t格助詞', 3991),
('する\t動詞\t自立', 3657),
('「\t記号\t括弧開', 3231),
('」\t記号\t括弧閉', 3225),
('も\t助詞\t係助詞', 2479),
('ない\t助動詞\t*', 2049),
('*\t名詞\t一般', 1735),
('で\t助詞\t格助詞', 1646),
('の\t名詞\t非自立', 1611),
('か\t助詞\t副助詞/並立助詞/終助詞', 1529),
...(略)
うーん・・・つまらない。
そこで、品詞別に集計できるようにしてみましょう。
from nlp30 import *
import itertools
from pprint import pprint
sentences = list(read_mecab_file('neko.txt.mecab'))
def rank_words_by_pos(sentences, pos):
words = {}
for word in itertools.chain.from_iterable(sentences):
if word['pos'] != pos:
continue
key = '{}\t{}\t{}'.format(word['base'], word['pos'], word['pos1'])
if key in words:
words[key] += 1
else:
words[key] = 1
return sorted(words.items(), key=lambda kv: kv[1], reverse=True)
print('名詞上位20件')
pprint(rank_words_by_pos(sentences, '名詞')[0:20])
print('-------------')
print()
print('動詞上位20件')
pprint(rank_words_by_pos(sentences, '動詞')[0:20])
print('-------------')
print()
print('形容詞上位20件')
pprint(rank_words_by_pos(sentences, '形容詞')[0:20])
print('-------------')
print()
結果はこちら。
名詞上位20件
[('*\t名詞\t一般', 1735),
('の\t名詞\t非自立', 1611),
('事\t名詞\t非自立', 1150),
('もの\t名詞\t非自立', 978),
('主人\t名詞\t一般', 932),
('ん\t名詞\t非自立', 704),
('よう\t名詞\t非自立', 669),
('君\t名詞\t接尾', 641),
('一\t名詞\t数', 554),
('何\t名詞\t代名詞', 498),
('吾輩\t名詞\t代名詞', 481),
('これ\t名詞\t代名詞', 414),
('それ\t名詞\t代名詞', 381),
('人\t名詞\t一般', 355),
('*\t名詞\tサ変接続', 341),
('君\t名詞\t代名詞', 332),
('三\t名詞\t数', 319),
('二\t名詞\t数', 303),
('ところ\t名詞\t非自立', 293),
('時\t名詞\t非自立', 283)]
-------------
動詞上位20件
[('する\t動詞\t自立', 3657),
('いる\t動詞\t非自立', 1503),
('云う\t動詞\t自立', 1408),
('なる\t動詞\t自立', 1015),
('ある\t動詞\t自立', 959),
('見る\t動詞\t自立', 675),
('思う\t動詞\t自立', 502),
('れる\t動詞\t接尾', 450),
('聞く\t動詞\t自立', 347),
('出来る\t動詞\t自立', 324),
('出る\t動詞\t自立', 317),
('いる\t動詞\t自立', 274),
('来る\t動詞\t非自立', 245),
('行く\t動詞\t自立', 244),
('知る\t動詞\t自立', 217),
('来る\t動詞\t自立', 214),
('やる\t動詞\t自立', 209),
('見える\t動詞\t自立', 208),
('てる\t動詞\t非自立', 204),
('分る\t動詞\t自立', 173)]
-------------
形容詞上位20件
[('ない\t形容詞\t自立', 1003),
('いい\t形容詞\t自立', 201),
('面白い\t形容詞\t自立', 125),
('いい\t形容詞\t非自立', 93),
('よい\t形容詞\t自立', 92),
('早い\t形容詞\t自立', 79),
('悪い\t形容詞\t自立', 74),
('長い\t形容詞\t自立', 72),
('好い\t形容詞\t自立', 55),
('苦い\t形容詞\t自立', 52),
('善い\t形容詞\t自立', 47),
('高い\t形容詞\t自立', 47),
('わるい\t形容詞\t自立', 45),
('えらい\t形容詞\t自立', 41),
('うまい\t形容詞\t自立', 41),
('苦しい\t形容詞\t自立', 40),
('強い\t形容詞\t自立', 34),
('大きい\t形容詞\t自立', 30),
('むずかしい\t形容詞\t自立', 28),
('寒い\t形容詞\t自立', 27)]
-------------
うーん、「フーン」って感じの結果だなぁ。
なんかもうちょっと面白い分析もできそうだけれども、まあこの辺にしておきますか。
37. 頻度上位10語
出現頻度が高い10語とその出現頻度をグラフ(例えば棒グラフなど)で表示せよ.
さあ、ここでグラフ化です。
私は普段グラフ描画ツールには、世界的に有名なあのエクセル大先生を使っているので、pythonでグラフを書くのははじめての体験です。
まあ、やってみましょう。
とりあえず形容詞の上位20件で表示してみます。
from matplotlib import rcParams
import matplotlib.pyplot as plt
import numpy as np
from nlp30 import *
import itertools
sentences = list(read_mecab_file('neko.txt.mecab'))
def rank_words_by_pos(sentences, pos):
words = {}
for word in itertools.chain.from_iterable(sentences):
if word['pos'] != pos:
continue
key = '{}\t{}\t{}'.format(word['base'], word['pos'], word['pos1'])
if key in words:
words[key] += 1
else:
words[key] = 1
return sorted(words.items(), key=lambda kv: kv[1], reverse=True)
ranking = rank_words_by_pos(sentences, '形容詞')[0:20]
plt.rcdefaults()
plt.rcParams['font.family'] = 'Hiragino Maru Gothic Pro'
fig, ax = plt.subplots()
words = tuple((' ' + kv[0].split()[0] + ' ' for kv in ranking))
y_pos = np.arange(len(words))
counts = tuple((kv[1] for kv in ranking))
# 下はサンプルからとってきたまま・・・
ax.barh(y_pos, counts, align='center')
ax.set_yticks(y_pos)
ax.set_yticklabels(words)
ax.invert_yaxis() # labels read top-to-bottom
ax.set_xlabel('出現頻度')
ax.set_title('形容詞出現頻度上位20件')
plt.show()
で、出力されたグラフはこちら。
ちなみにMacでやってますが、日本語フォント指定すると微妙に表示が崩れました。
words = tuple((' ' + kv[0].split()[0] + ' ' for kv in ranking))
っていうところで前後に謎の空白をつけているのは、この表示調整のためです。。。
38. ヒストグラム
単語の出現頻度のヒストグラム(横軸に出現頻度,縦軸に出現頻度をとる単語の種類数を棒グラフで表したもの)を描け.
さっきと違って軸のラベルが数字で済むから楽そう! というのが第一印象。
そして、調べてみると matplotlib には matplotlib.pyplot.hist
というヒストグラムを書いてくれるメソッドがある様子。使ってみましょう。
from matplotlib import rcParams
import matplotlib.pyplot as plt
import numpy as np
from nlp30 import *
import itertools
sentences = list(read_mecab_file('neko.txt.mecab'))
def rank_words(sentences):
words = {}
for word in itertools.chain.from_iterable(sentences):
key = '{}\t{}\t{}'.format(word['base'], word['pos'], word['pos1'])
if key in words:
words[key] += 1
else:
words[key] = 1
return sorted(words.items(), key=lambda kv: kv[1], reverse=True)
# 出現頻度の数字だけの配列を作ります
counts = list([kv[1] for kv in rank_words(sentences)])
plt.hist(counts, bins=10)
plt.show()
実行結果がこちら。
・・・あれ?
なんかヒストグラムっぽくないぞ。
どうも件数が偏り過ぎのようなので、 plt.hist(counts, bins=10, range=(0,200))
と頻度200以下だけにフォーカスを当てる形で再度表示してみます。
これでも偏りすぎですね。
やっぱり頻度が低い単語が多いことがわかります。横軸を50までにして更にズームイン。
やっと、ヒストグラムっぽくなりました。
ほとんどの単語は5回以下しか使われていないことがわかったわけです。
39. Zipfの法則
単語の出現頻度順位を横軸,その出現頻度を縦軸として,両対数グラフをプロットせよ.
Zipfの法則 とは、
ジップの法則(ジップのほうそく、Zipf's law)あるいはジフの法則とは、出現頻度が k 番目に大きい要素が全体に占める割合が $1/k$ に比例するという経験則である。Zipf は「ジフ」と読まれることもある。また、この法則が機能する世界を「ジフ構造」と記する論者もいる。
(Wikipediaより、以下のグラフも同じ)
という法則です。Wikipediaの場合、出現する単語の頻度と順位を対数軸でプロットすると以下のようなグラフになるようです。
両対数のグラフを取ると、リンク先にあるように左下がりの線形に近いグラフになるはず、ということでやってみましょう。
matplotlibには軸を対数で取る機能もあるようなので、それを活用するとあっさり対数グラフが描けました。
import matplotlib.pyplot as plt
import math
from nlp30 import *
import itertools
sentences = list(read_mecab_file('neko.txt.mecab'))
def rank_words(sentences):
words = {}
for word in itertools.chain.from_iterable(sentences):
key = '{}\t{}\t{}'.format(word['base'], word['pos'], word['pos1'])
if key in words:
words[key] += 1
else:
words[key] = 1
return sorted(words.items(), key=lambda kv: kv[1], reverse=True)
# 出現頻度の数字だけの配列を作ります
counts = list([kv[1] for kv in rank_words(sentences)])
ranks = list(range(1, len(counts) + 1))
plt.plot(ranks, counts)
plt.yscale('log')
plt.xscale('log')
plt.show()
結果がこちら。
Wikipediaでも夏目漱石でも同じ傾向が現れるのって、不思議ですね。
というわけで今回はここまで。また次回お会いしましょう。
-
「吾輩は猫である」は朝日新聞に連載された小説でした。 ↩