morikomorou’s blog

自分が学んだことなどの備忘録的なやつ

【python】言語処理100本ノック2020を解く(第4章)


はじめに

前回の続きで言語処理100本ノック解いていきたいと思います。

今回は第4章です。形態素解析の章やっていきます。




第4章: 形態素解析

夏目漱石の小説『吾輩は猫である』に形態素解析器を適用し,小説中の単語の統計を求めます.

事前準備

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

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

まずはMeCabというソフトを使って形態素解析を行っていきます。
pythonでMeCabを使えるバインダがあるのでそれを使ってやってみます。
インストール、使用法については下記記事が大変参考になりました。

self-development.info

今回はpythonが64bit版なのでMeCabも64bitのものを使用しないといけません。
次の章でCaboChaというものを使用するのですが、これは困ったことに32bit版しかありません。しかもCaboChaでの解析ではMeCabも使用するため、MeCabも32bitである必要があります。
つまりはpythonも32bit版を再インストールしなきゃいけない感じになります。

なので、pythonでMeCab, CaboChaを動かすのはお勧めしません
素直にMeCab, CaboChaどちらも32bit版をインストールし、pythonを介さずコマンドラインからそれぞれ直接実行したほうがいいです。

今回はpythonでMeCabを動かす方法でやりますが、次章ではやめます。

import MeCab

raw_file = './input/section4/neko.txt'
file = './input/section4/neko.txt.mecab'
tagger = MeCab.Tagger()

with open(raw_file, 'r', encoding='utf-8') as f1, \
                open(file, 'w', encoding='utf-8') as f2:
    for row in f1:
        f2.write(tagger.parse(row))

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

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

形態素解析結果は以下のような形で出力されますので、必要な形態素をそれぞれ抜き出すだけですね。

表層形\t品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音

1文の終わりは'EOS\n'で表されるので、それをトリガーに文を区切ります。

def read_mecab(file):
    results = []
    morphs = []
    with open(file, 'r', encoding='utf-8') as f:
        for row in f:
            if row == 'EOS\n':
                if len(morphs):
                    results.append(morphs)
                    morphs = []
            else:
                cols1 = row.split('\t')
                cols2 = cols1[1].split(',')
                morph = {'surface': cols1[0],
                         'base': cols2[6],
                         'pos': cols2[0],
                         'pos1': cols2[1],
                        }
                morphs.append(morph)
    if len(morphs):
        results.append(morphs)
        morphs = []
    return results

results = read_mecab(file)
results[:3]

以下出力です。

[[{'surface': '一', 'base': '一', 'pos': '名詞', 'pos1': '数'}],
 [{'surface': '\u3000', 'base': '\u3000', 'pos': '記号', 'pos1': '空白'},
  {'surface': '吾輩', 'base': '吾輩', 'pos': '名詞', 'pos1': '代名詞'},
  {'surface': 'は', 'base': 'は', 'pos': '助詞', 'pos1': '係助詞'},
  {'surface': '猫', 'base': '猫', 'pos': '名詞', 'pos1': '一般'},
  {'surface': 'で', 'base': 'だ', 'pos': '助動詞', 'pos1': '*'},
  {'surface': 'ある', 'base': 'ある', 'pos': '助動詞', 'pos1': '*'},
  {'surface': '。', 'base': '。', 'pos': '記号', 'pos1': '句点'}],
 [{'surface': '名前', 'base': '名前', 'pos': '名詞', 'pos1': '一般'},
  {'surface': 'は', 'base': 'は', 'pos': '助詞', 'pos1': '係助詞'},
  {'surface': 'まだ', 'base': 'まだ', 'pos': '副詞', 'pos1': '助詞類接続'},
  {'surface': '無い', 'base': '無い', 'pos': '形容詞', 'pos1': '自立'},
  {'surface': '。', 'base': '。', 'pos': '記号', 'pos1': '句点'}]]

31. 動詞

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

先ほど作成したresultsというリストからposが動詞となっている形態素を抜き出すだけです。

ans = set()
for morphs in results:
    for morph in morphs:
        if morph['pos'] == '動詞':
            ans.add(morph['surface'])

print('動詞の表層形の種類: {}'.format(len(ans)))
print('===10個表示===')
for v in list(ans)[:10]:
    print(v)

出力は以下。

動詞の表層形の種類: 3893
===10個表示===
踞る
しまう
隠れる
はいり
挟まっ
作り上げ
磨っ
甘える
読み直し
兼ねる

32. 動詞の基本形

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

これも先ほどとほぼ同じです。

ans = set()
for morphs in results:
    for morph in morphs:
        if morph['pos'] == '動詞':
            ans.add(morph['base'])

print('動詞の基本形の種類: {}'.format(len(ans)))
print('===10個表示===')
for v in list(ans)[:10]:
    print(v)

出力は以下。

動詞の表層形の種類: 2300
===10個表示===
踞る
しまう
隠れる
拡げる
甘える
すかす
達す
転じる
兼ねる
聳える




33. 「AのB」

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

名詞の後に「の」が来たらフラグを立てて、フラグが立ってる状態で次に名詞が来たら集合に加えるという形で組んでます。

ans = set()
for morphs in results:
    word = ''
    flg = 0
    for morph in morphs:
        if flg:
            if morph['pos'] == '名詞':
                ans.add(word + morph['surface'])
                flg = 0
                word = morph['surface']
            else:
                flg = 0
                word = ''
        elif morph['pos'] == '名詞':
            word = morph['surface']
        elif word and morph['surface'] == 'の':
            flg = 1
            word += 'の'
        else:
            word = ''
            continue

print('名詞句の種類: {}'.format(len(ans)))
print('===10個表示===')
for v in list(ans)[:10]:
    print(v)

出力は以下です。まぁちゃんと抽出できてそうな気はします。

名詞句の種類: 4924
===10個表示===
風来坊のよう
個性の発達
かたのよう
寒村の百姓
所の三
先生の方
ベランメーの刺
六の背
宣戦の大詔
女の方

34. 名詞の連接

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

これも先ほどと同じ感じでやれますね。

ans = set()
for morphs in results:
    word = []
    for morph in morphs:
        if morph['pos'] != '名詞':
            if len(word) > 1:
                ans.add(''.join(word))
            word = []
        else:
            word.append(morph['surface'])
    if len(word) > 1:
        ans.add(''.join(word))

print('名詞の連接の種類: {}'.format(len(ans)))
print('===10個表示===')
for v in list(ans)[:10]:
    print(v)
名詞の連接の種類: 4457
===10個表示===
幾何学的
迷惑はさ
三日処置
理学士
積極的
候敬具
十四五
——両君
多々良さんせんだって
先生大

35. 単語の出現頻度

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

collections.Counterが便利すぎます。これで一発です。

from collections import Counter

words = []
for morphs in results:
    for morph in morphs:
        if morph['pos'] != '記号':
            words.append(morph['base'])

c = Counter(words)
ans = c.most_common()

for w in ans[:10]:
    print(w)
('の', 9194)
('て', 6848)
('は', 6420)
('に', 6243)
('を', 6071)
('だ', 5975)
('と', 5508)
('が', 5337)
('た', 4267)
('する', 3657)

36. 頻度上位10語

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

グラフの文字化け対策にフォントを日本語対応のフォントに変えときます。
35ですでに出現頻度順にしているansを使用します。

import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'Meiryo'

top10_words = [w[0] for w in ans[:10]]
top10_words_count = [w[1] for w in ans[:10]]

fig, ax = plt.subplots()
ax.bar(top10_words, top10_words_count)
ax.set_xlabel('頻出単語')
ax.set_ylabel('出現頻度')
plt.show()


37. 「猫」と共起頻度の高い上位10語

「猫」とよく共起する(共起頻度が高い)10語とその出現頻度をグラフ(例えば棒グラフなど)で表示せよ.

ちょっと問題の意味が分からなかったんですが、1文の中に猫という単語が出てきた際の他の単語が「猫」と共起する単語ということと解釈してます。

words = []
for morphs in results:
    if '猫' in [x['surface'] for x in morphs]:
        for morph in morphs:
            if morph['pos'] != '記号' and morph['surface'] != '猫':
                words.append(morph['base'])

c = Counter(words)
ans = c.most_common()

top10_words = [w[0] for w in ans[:10]]
top10_words_count = [w[1] for w in ans[:10]]

fig, ax = plt.subplots()
ax.bar(top10_words, top10_words_count)
ax.set_xlabel('頻出単語')
ax.set_ylabel('出現頻度')
plt.show()




38. ヒストグラム

単語の出現頻度のヒストグラムを描け.ただし,横軸は出現頻度を表し,1から単語の出現頻度の最大値までの線形目盛とする.縦軸はx軸で示される出現頻度となった単語の異なり数(種類数)である.

普通に出現頻度でヒストグラムを書くだけです

words = []
for morphs in results:
    for morph in morphs:
        if morph['pos'] != '記号':
            words.append(morph['base'])

c = Counter(words)
ans = c.values()

fig, ax = plt.subplots()
ax.hist(ans, bins=100)
ax.set_xlabel('出現頻度')
ax.set_ylabel('単語の異なり数')
plt.show()


39. Zipfの法則

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

出現頻度順に並べ替えて順位付けして、散布図をかきます。
ここで同じ出現頻度のものを同じ順位にするとサンプルとちょっと違うグラフになったので止めました

ans = sorted(list(ans), reverse=True)
order = [i + 1 for i in range(len(ans))]

fig, ax = plt.subplots()
ax.scatter(order, ans)
ax.set_xscale('log')
ax.set_yscale('log')
ax.set_title('Zipfの法則')
ax.set_xlabel('出現頻度順位')
ax.set_ylabel('出現頻度')
plt.show()


おわりに

4章終わりです。
形態素解析なかなか強力そうなツールですね