第4章:形態素解析
夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をMeCabを使って形態素解析し,その結果をneko.txt.mecabというファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ.
なお,問題37, 38, 39はmatplotlibもしくはGnuplotを用いるとよい.
記事の説明
- この記事は言語処理にもpythonにも素人な学生が言語処理100本ノック 2020を解いた結果を載せている記事です。間違いや改善点の指摘には大変喜びますので、よろしくお願いします。
-
pythonの勉強のために、pycharmのinspectionに従いまくっているので、無駄なコードが多々あるかもしれないです。- pycharm上で""が入力できない問題が解決できないので、今回はAtomを使っています。
- 1〜3章は飛ばします。
** 環境 **
- MacBook Pro (13-inch, 2016, Thunderbolt 3ポートx 2)
- macOS Catalina 10.15.5
- Python 3.8.1 (Anacondaじゃない)
事前準備
夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をMeCabを使って形態素解析し,その結果をneko.txt.mecabというファイルに保存せよ.
とのことですので、以下のプログラムを実行し作成します。
**作成したプログラム(click)**
# -*- coding: utf-8 -*-
import MeCab
from MeCab import Tagger
analyser: Tagger = MeCab.Tagger("-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd")
with open('./neko.txt', "r") as infile:
lines = infile.readlines()
with open('./neko.txt.mecab.txt', "w") as outfile:
for line in lines:
mors = analyser.parse(line)
outfile.write(mors)
note
-
出力された形態素をみてみると、最初の一行目の「吾輩は猫である」が固有名詞として識別されている。どうでもいいけど。
-
出力フォーマットは(mecabによると)
表層¥t品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音
となっており要素数10になるはずが、たまに以下のような要素数8になる行があった。(要素数9はない)
['頸筋', '名詞', '一般', '*', '*', '*', '*', '*\n'] ['ぎりご', '名詞', '一般', '*', '*', '*', '*', '*\n'] ['韲', '名詞', '一般', '*', '*', '*', '*', '*\n']
省略されるのかな。
30. 形態素解析結果の読み込み
形態素解析結果(neko.txt.mecab)を読み込むプログラムを実装せよ.ただし,各形態素は表層形(surface),基本形(base),品詞(pos),品詞細分類1(pos1)をキーとするマッピング型に格納し,1文を形態素(マッピング型)のリストとして表現せよ.第4章の残りの問題では,ここで作ったプログラムを活用せよ.
**作成したプログラム(click)**
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
import re
import sys
def input_macab(filename):
with open(filename, "r") as infile:
sentences = []
sentence =[]
for line in infile.readlines():
# 表層¥t品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音
if line == 'EOS\n':
if len(sentence) > 0:
sentences.append(sentence)
sentence =[]
continue
sline = re.split('[,\t]', line)
if len(sline) < 8:
print("### 読み込みエラー:\n", sline + "\n")
sys.exit(1)
sentence.append({'surface': sline[0], 'base': sline[7], 'pos': sline[1], 'pos1': sline[2] })
print("** 読み込み完了 **\n")
return sentences
if __name__ == '__main__':
filename = "neko.txt.mecab.txt"
ss = input_macab(filename)
print("")
print("mainとして実行されました。")
note
- 他の問題でも使うので関数化しました。
- 品詞 = pos = part of speech
31.動詞
動詞の表層形をすべて抽出せよ.
**作成したプログラム(click)**
# -*- coding: utf-8 -*-
import k30input
sentences = k30input.input_macab("neko.txt.mecab.txt")
for sentence in sentences:
for mor in sentence:
if mor['pos']=="動詞":
print(mor['surface'])
32. 動詞の原形
動詞の原形をすべて抽出せよ.
**作成したプログラム(click)**
# -*- coding: utf-8 -*-
import k30input
sentences = k30input.input_macab("neko.txt.mecab.txt")
for sentence in sentences:
for mor in sentence:
if mor['pos']=="動詞":
print(mor['base'])
33. 「AのB」
2つの名詞が「の」で連結されている名詞句を抽出せよ.
**作成したプログラム(click)**
# -*- coding: utf-8 -*-
import k30input
sentences = k30input.input_macab("neko.txt.mecab.txt")
noun_flag = 0
no_flag = 0
noun1: str
for sentence in sentences:
for mor in sentence:
if noun_flag == 0 :
if mor['pos']=="名詞":
noun_flag = 1
noun1 = mor['surface']
elif noun_flag == 1 and no_flag == 0:
if mor['surface']=="の":
no_flag = 1
else:
noun1 = ""
noun_flag = no_flag = 0
elif noun_flag == 1 and no_flag == 1:
if mor['pos']=="名詞":
print(noun1+"の"+mor['surface'])
noun_flag = no_flag = 0
34. 名詞の連接
名詞の連接(連続して出現する名詞)を最長一致で抽出せよ.
**作成したプログラム(click)**
# -*- coding: utf-8 -*-
import k30input
sentences = k30input.input_macab("neko.txt.mecab.txt")
nouns = []
for sentence in sentences:
for mor in sentence:
if mor['pos']=="名詞":
nouns.append(mor['surface'])
else:
if len(nouns) > 1:
for i in nouns:
print(i+" ", end="")
print("")
nouns = []
note
- わかりやすいように名詞間に空白を挟んで出力している。
- 表層系が同じ「ようやく」でも、mecabは文脈によって副詞と判断されたり、名詞と判断されたりするのを忘れていて、戸惑った。
35. 単語の出現頻度
文章中に出現する単語とその出現頻度を求め,出現頻度の高い順に並べよ.
**作成したプログラム(click)**
# -*- coding: utf-8 -*-
import k30input
sentences = k30input.input_macab("neko.txt.mecab.txt")
nouns = []
mor_freq = dict()
for sentence in sentences:
for mor in sentence:
# キーは(表層系, 品詞)のタプル, 値は出現数。
mor_freq.setdefault((mor['surface'], mor['pos']), 0)
mor_freq[(mor['surface'], mor['pos'])] = mor_freq[(mor['surface'], mor['pos'])] + 1
ranking = sorted(mor_freq.items(), key=lambda i: i[1], reverse=True)
for i in ranking:
print(i)
note
- 表層語が同じ形態素でも、品詞が違うものは区別して数えた。
- 例えば、副詞の「ようやく」と名詞の「ようやく」を区別して数えている。
36. 頻度上位10語
出現頻度が高い10語とその出現頻度をグラフ(例えば棒グラフなど)で表示せよ.
**作成したプログラム(click)**
# -*- coding: utf-8 -*-
from matplotlib import pyplot
import k30input
sentences = k30input.input_macab("neko.txt.mecab.txt")
nouns = []
mor_freq = dict()
for sentence in sentences:
for mor in sentence:
# キーは(表層系, 品詞)のタプル, 値は出現数。
mor_freq.setdefault((mor['surface'], mor['pos']), 0)
mor_freq[(mor['surface'], mor['pos'])] = mor_freq[(mor['surface'], mor['pos'])] + 1
ranking = sorted(mor_freq.items(), key=lambda i: i[1], reverse=True)
top10 = ranking[0:10]
x = []
y = []
for i in top10:
x.append(i[0][0])
y.append(i[1])
pyplot.bar(x, y)
#グラフタイトル
pyplot.title('頻度上位10語')
#グラフの軸
pyplot.xlabel('形態素')
pyplot.ylabel('頻度')
pyplot.show()
note
- matplotlibのデフォルトのフォントでは、日本語が表示できないのでフォントを切り替えた。
- 具体的には、<パッケージ保管場所>/matplotlib/mpl-data/matplotlibrcが設定ファイルなので、そこに
font.family : Hiragino Maru Gothic Pro
を追加した。(Mac標準搭載フォント)
- 具体的には、<パッケージ保管場所>/matplotlib/mpl-data/matplotlibrcが設定ファイルなので、そこに
37. 「猫」と共起頻度の高い上位10語
「猫」とよく共起する(共起頻度が高い)10語とその出現頻度をグラフ(例えば棒グラフなど)で表示せよ.
**作成したプログラム(click)**
# -*- coding: utf-8 -*-
from matplotlib import pyplot
import k30input
sentences = k30input.input_macab("neko.txt.mecab.txt") # neko.txt.mecab.clean.txt
nouns = []
tmp_count = dict()
co_cat_count = dict()
cat_flag = 0
for sentence in sentences:
for mor in sentence:
# キーは(表層系, 品詞)のタプル, 値は出現数。
tmp_count.setdefault((mor['surface'], mor['pos']), 0)
tmp_count[(mor['surface'], mor['pos'])] = tmp_count[(mor['surface'], mor['pos'])] + 1
if mor['surface'] == "猫":
cat_flag = 1
if cat_flag == 1:
for k, v in tmp_count.items():
co_cat_count.setdefault(k, 0)
co_cat_count[k] = co_cat_count[k] + v
cat_flag = 0
tmp_count = {}
ranking = sorted(co_cat_count.items(), key=lambda i: i[1], reverse=True)
top10 = ranking[0:10]
x = []
y = []
for i in top10:
x.append(i[0][0])
y.append(i[1])
pyplot.bar(x, y)
#グラフタイトル
pyplot.title('「猫」と共起頻度の高い上位10語')
#グラフの軸
pyplot.xlabel('形態素')
pyplot.ylabel('頻度')
pyplot.show()
note
共起(きょうき,英:Co-occurrence)は、ある単語がある文章(または文)中に出たとき、その文章(文)中に別の限られた単語が頻繁に出現すること。1
- 今回だと"猫"が含まれる文中に、よく出る単語(形態素)TOP10ということですね。
- "猫"自体は本来除外するべきですが、一つの基準として面白いのと、消そうと思えば簡単に消せるので残しました。
結果
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|
の | は | 、 | 猫 | に | を | て | 。 | と | が |
句点・助詞・助動詞も単語としてカウントしているので結果が、"猫"だからこその結果という感じがしなくて面白くないですね。つまり、どんな単語だってこれらの単語とは共起するでしょうから、面白くない。
結果2
ということで、neko.txt.mecabから品詞が句点・助詞・助動詞の形態素情報を除外したファイルneko.txt.mecab.clean.txtを用意してやってみた結果
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|
猫 | し | 事 | 吾輩 | の | いる | ある | する | 人間 | この |
- 幾分ましになったものの、まだ"猫"の特徴が捕らえられている感じはしませんね。
- 「いる、ある、する」なんてどの文にだって入っているでしょうからね。
- "の"は名詞判定されるものがあって、除外されずにランクインしていて煩わしい
- 名詞"の"ってどんなの?
- A. 「野」と形式名詞(例文: 僕のが見つからない)の場合など2
- 名詞"の"ってどんなの?
- こういった全ての単語と共起頻度の高い単語というのも計算して、除外するようにプログラムすれば、もっと意味のある共起頻度を求めることができますが、面倒なのでしません。
- 現状の共起頻度の結果から得られる意味のある情報は、名著『吾輩は猫である』では「"猫"と"人間"がよく共起する」ということぐらいでしょうか。
- 『吾輩は猫である』では、人間と猫を対比する文章が多いんでしょうかね。
- あとは猫の一人称は吾輩だろうなとかも(あまりに有名な事実)
38. ヒストグラム
単語の出現頻度のヒストグラムを描け.ただし,横軸は出現頻度を表し,1から単語の出現頻度の最大値までの線形目盛とする.縦軸はx軸で示される出現頻度となった単語の異なり数(種類数)である.
**作成したプログラム(click)**
# -*- coding: utf-8 -*-
from matplotlib import pyplot
import k30input
sentences = k30input.input_macab("neko.txt.mecab.txt") # neko.txt.mecab.clean.txt
nouns = []
tmp_count = dict()
co_cat_count = dict()
cat_flag = 0
for sentence in sentences:
for mor in sentence:
# キーは(表層系, 品詞)のタプル, 値は出現数。
tmp_count.setdefault((mor['surface'], mor['pos']), 0)
tmp_count[(mor['surface'], mor['pos'])] = tmp_count[(mor['surface'], mor['pos'])] + 1
if mor['surface'] == "猫":
cat_flag = 1
if cat_flag == 1:
for k, v in tmp_count.items():
co_cat_count.setdefault(k, 0)
co_cat_count[k] = co_cat_count[k] + v
cat_flag = 0
tmp_count = {}
ranking = sorted(co_cat_count.items(), key=lambda i: i[1], reverse=True)
x = []
for i in ranking:
x.append(i[1])
pyplot.hist(x, range=(0,ranking[0][1]))
#グラフタイトル
pyplot.title('単語の出現頻度')
#グラフの軸
pyplot.xlabel('出現頻度')
pyplot.ylabel('種類数')
pyplot.show()
note
- 他のqiitaの投稿者で、この縦軸を出現頻度にしている方がいましたが、縦軸はその頻度で現れている単語数(異なり数)です。
結果
対数ヒストグラムにするとこんな感じ
39. Zipfの法則
単語の出現頻度順位を横軸,その出現頻度を縦軸として,両対数グラフをプロットせよ.
**作成したプログラム(click)**
# -*- coding: utf-8 -*-
from matplotlib import pyplot
import k30input
import numpy as np
sentences = k30input.input_macab("neko.txt.mecab.txt") # neko.txt.mecab.clean.txt
nouns = []
tmp_count = dict()
co_cat_count = dict()
cat_flag = 0
for sentence in sentences:
for mor in sentence:
# キーは(表層系, 品詞)のタプル, 値は出現数。
tmp_count.setdefault((mor['surface'], mor['pos']), 0)
tmp_count[(mor['surface'], mor['pos'])] = tmp_count[(mor['surface'], mor['pos'])] + 1
if mor['surface'] == "猫":
cat_flag = 1
if cat_flag == 1:
for k, v in tmp_count.items():
co_cat_count.setdefault(k, 0)
co_cat_count[k] = co_cat_count[k] + v
cat_flag = 0
tmp_count = {}
ranking = sorted(co_cat_count.items(), key=lambda i: i[1], reverse=True)
y = []
for i in ranking:
y.append(i[1])
x = range(len(ranking))
print("size", len(ranking))
pyplot.title('単語の出現頻度')
pyplot.xlabel('出現頻度順位 log(y)')
pyplot.ylabel('種類数 log(x)')
# scatter(散布図)で対数メモリにする方法が分からなかったので、ここだけカンニングした。だからnumpy使ってる。
pyplot.scatter(np.log(x),np.log(y))
pyplot.show()
結果
####note
- Zipfの法則
出現頻度が k 番目に大きい要素が全体に占める割合が1/kに比例するという経験則である。3
- この法則って自然言語だけの経験則ってわけではなく、様々な現象に成り立つらしいです。
- ウィキペディア(30ヶ国語版)における単語の出現頻度です。似てますね。3
感想
まだまだpythonのお勉強の範疇です。だというのに、numpy, pandas, collectionsモジュールを勉強するのがめんどくさくて、使わずに済ませた。けどもしかして使わない方が難しくないか?
あと、同じような処理は関数化してカッコよく済ませたかった。
次回に続く。(確実にやる)