言語処理100本ノック 第4章: 形態素解析

# [第4章：形態素解析](https://nlp100.github.io/ja/ch04.html)
夏目漱石の小説『吾輩は猫である』の文章（neko.txt）をMeCabを使って形態素解析し，その結果をneko.txt.mecabというファイルに保存せよ．このファイルを用いて，以下の問に対応するプログラムを実装せよ．

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

## 記事の説明
- この記事は言語処理にもpythonにも素人な学生が[言語処理100本ノック 2020](https://nlp100.github.io/ja/ch04.html)を解いた結果を載せている記事です。間違いや改善点の指摘には大変喜びますので、よろしくお願いします。
- ~~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というファイルに保存せよ．

とのことですので、以下のプログラムを実行し作成します。

<details><summary>**作成したプログラム(click)**</summary><div>

```python3:pre_processing.py
# -*- 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)
```

</div></details>

#### note

- 出力された形態素をみてみると、最初の一行目の「吾輩は猫である」が固有名詞として識別されている。どうでもいいけど。

- 出力フォーマットは([mecab](https://taku910.github.io/mecab/#parse)によると)
`表層¥t品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音`
となっており要素数10になるはずが、たまに以下のような要素数8になる行があった。(要素数9はない)
` ['頸筋', '名詞', '一般', '*', '*', '*', '*', '*\n'] 
['ぎりご', '名詞', '一般', '*', '*', '*', '*', '*\n']
['韲', '名詞', '一般', '*', '*', '*', '*', '*\n']
`
省略されるのかな。

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

<details><summary>**作成したプログラム(click)**</summary><div>

```python3:k30input.py
#! /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として実行されました。")
```

</div></details>


#### note

- 他の問題でも使うので関数化しました。
- 品詞 = pos = part of speech

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

<details><summary>**作成したプログラム(click)**</summary><div>

```python3:k31verb_surface.py
# -*- 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'])

```

</div></details>

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

<details><summary>**作成したプログラム(click)**</summary><div>

```python3:k32verb_base.py
# -*- 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'])

```

</div></details>

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

<details><summary>**作成したプログラム(click)**</summary><div>

```python:k33noun_no_noun.py
# -*- 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

```

</div></details>

## 34. 名詞の連接
名詞の連接（連続して出現する名詞）を最長一致で抽出せよ．

<details><summary>**作成したプログラム(click)**</summary><div>

```python:k34nounoun_longest.py
# -*- 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 = []

```

</div></details>

#### note

- わかりやすいように名詞間に空白を挟んで出力している。
- 表層系が同じ「ようやく」でも、mecabは文脈によって副詞と判断されたり、名詞と判断されたりするのを忘れていて、戸惑った。

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

<details><summary>**作成したプログラム(click)**</summary><div>

```python:k35word_freq.py
# -*- 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)

```

</div></details>

#### note

- 表層語が同じ形態素でも、品詞が違うものは区別して数えた。
- 例えば、副詞の「ようやく」と名詞の「ようやく」を区別して数えている。

## 36. 頻度上位10語
出現頻度が高い10語とその出現頻度をグラフ（例えば棒グラフなど）で表示せよ．

<details><summary>**作成したプログラム(click)**</summary><div>

```python:k36word10_graph.py
# -*- 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()

```

</div></details>

#### note

- matplotlibのデフォルトのフォントでは、日本語が表示できないのでフォントを切り替えた。
    - 具体的には、<パッケージ保管場所>/matplotlib/mpl-data/matplotlibrcが設定ファイルなので、そこに```font.family         : Hiragino Maru Gothic Pro```を追加した。(Mac標準搭載フォント)

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

<details><summary>**作成したプログラム(click)**</summary><div>

```python:k37co_cat.py
# -*- 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()

```

</div></details>

#### 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軸で示される出現頻度となった単語の異なり数（種類数）である．

<details><summary>**作成したプログラム(click)**</summary><div>

```python:k38histogram.py
# -*- 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()

```

</div></details>

#### note

- 他のqiitaの投稿者で、この縦軸を出現頻度にしている方がいましたが、縦軸はその頻度で現れている単語数(異なり数)です。

#### 結果
![Figure_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213694/3ddc0e22-0444-0a5b-6096-c1ef74590ef0.png)

対数ヒストグラムにするとこんな感じ

![Figure_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213694/d87bf0de-8c63-6166-6bd7-4966062fa530.png)

## 39. Zipfの法則
単語の出現頻度順位を横軸，その出現頻度を縦軸として，両対数グラフをプロットせよ．

<details><summary>**作成したプログラム(click)**</summary><div>

```python:k39loglog_graph.py
# -*- 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()

```

</div></details>

#### 結果
![Figure_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213694/8efab875-0982-c9e2-6ac2-13465477b106.png)



####note

- Zipfの法則

> 出現頻度が k 番目に大きい要素が全体に占める割合が1/kに比例するという経験則である。[^3]

- この法則って自然言語だけの経験則ってわけではなく、様々な現象に成り立つらしいです。

![ウィキペディア（30ヶ国語版）における単語の出現頻度](https://upload.wikimedia.org/wikipedia/commons/a/ac/Zipf_30wiki_en_labels.png)

- ウィキペディア（30ヶ国語版）における単語の出現頻度です。似てますね。[^3]

## 感想

まだまだpythonのお勉強の範疇です。だというのに、numpy, pandas, collectionsモジュールを勉強するのがめんどくさくて、使わずに済ませた。けどもしかして使わない方が難しくないか？
あと、同じような処理は関数化してカッコよく済ませたかった。
次回に続く。（確実にやる）

[^1]: [共起 - Wikipedia](https://ja.wikipedia.org/wiki/%E5%85%B1%E8%B5%B7)
[^2]: [の - ウィクショナリー日本語版](https://ja.wiktionary.org/wiki/%E3%81%AE)
[^3]: [ジップの法則 - Wikipedia](https://ja.wikipedia.org/wiki/%E3%82%B8%E3%83%83%E3%83%97%E3%81%AE%E6%B3%95%E5%89%87)