GiNZA v3.1で言語処理100本ノック 2020をやった 第4章

# はじめに
満を持して[言語処理100本ノック 2020](https://nlp100.github.io/ja/)が公開されたので早速やってみます。

第4章ではMeCabで形態素解析します。が、せっかくなので[GiNZA](https://megagonlabs.github.io/ginza/)でやります（といっても、最初しか使いませんでしたが）。


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

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

```python:コード
import spacy
import pandas as pd
import pprint
from functools import reduce
import collections
import matplotlib.pyplot as plt
import seaborn as sns

with open('neko.txt') as f:
    raw_text = f.read()
    # 先頭の余計な文字を削除
    raw_text = raw_text.replace('一\n\n　', '')
    nlp = spacy.load('ja_ginza')
    doc = nlp(raw_text)
    with open('neko.txt.ginza', 'a') as f2:
        for sent in doc.sents:
            for token in sent:
                # 番号、表層系、基本形、品詞を出力
                f2.write(','.join([str(token.i), token.orth_, token.lemma_, token.tag_]) + '\n')
```
GiNZAはコマンドラインではMeCabと同じ出力が出せるようですが、PythonからのAPIは見つけられなかったためそのまま出力しています。


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

```python:コード
neko_df = pd.read_csv('neko.txt.ginza', header=None)

docs = []
sentence = []
for row in neko_df.itertuples():
    pos, *pos1 = row[4].split('-')
    neko_dict = {
        'surface': row[2],
        'base': row[3],
        'pos': pos,
        'pos1': pos1
    }
    sentence.append(neko_dict)
    # 。で区切る
    if row[2] == '。':
        docs.append(sentence)
        sentence = []
pprint.pprint(docs[0])
```

```:出力結果
[{'base': '我が輩', 'pos': '代名詞', 'pos1': [], 'surface': '吾輩'},
 {'base': 'は', 'pos': '助詞', 'pos1': ['係助詞'], 'surface': 'は'},
 {'base': '猫', 'pos': '名詞', 'pos1': ['普通名詞', '一般'], 'surface': '猫'},
 {'base': 'だ', 'pos': '助動詞', 'pos1': [], 'surface': 'で'},
 {'base': '有る', 'pos': '動詞', 'pos1': ['非自立可能'], 'surface': 'ある'},
 {'base': '。', 'pos': '補助記号', 'pos1': ['句点'], 'surface': '。'}]
```

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

```python:コード
surfaces = []
for sentence in docs:
    for morpheme in sentence:
        surfaces.append(morpheme['surface'])
print(surfaces[:30])
```

```:出力結果
['吾輩', 'は', '猫', 'で', 'ある', '。', '名前', 'は', 'まだ', '無い', '。', 'どこ', 'で', '生れ', 'た', 'か', 'と', 'んと', '見当', 'が', 'つか', 'ぬ', '。', '何', 'で', 'も', '薄暗い', 'じめじめ', 'し', 'た']
```

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

```python:コード
bases = []
for sentence in docs:
    for morpheme in sentence:
        bases.append(morpheme['base'])
print(bases[:30])
```

```:出力結果
['我が輩', 'は', '猫', 'だ', '有る', '。', '名前', 'は', '未だ', '無い', '。', 'どこ', 'で', '生まれる', 'た', 'か', 'と', 'うんと', '見当', 'が', '付く', 'ず', '。', '何', 'で', 'も', '薄暗い', 'じめじめ', '為る', 'た']
```

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

```python:コード
nouns = []
for sentence in docs:
    for i in range(len(sentence) - 2):
        if sentence[i]['pos'] == '名詞' and sentence[i + 1]['surface'] == 'の' and sentence[i + 2]['pos'] == '名詞':
            nouns.append(sentence[i]['surface'] + sentence[i + 1]['surface'] + sentence[i + 2]['surface'])
print(nouns[:30])
```

```:出力結果
['掌の上', '書生の顔', 'ものの見', 'はずの顔', '顔の真中', '穴の中', '書生の掌', '掌の裏', '今までの所', '藁の上', '笹原の中', '池の前', '池の上', '一樹の蔭', '垣根の穴', '隣家の三毛', '時の通路', '一刻の猶予', '家の内', '以外の人間', '前の書生', 'おさんの隙', 'おさんの三', '胸の痞', '家の主人', '主人の方', 'なしの小猫', '鼻の下', '自分の住家', '家のもの']
```

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

```python:コード
nouns2 = []
for sentence in docs:
    word = ''
    count = 0
    for morpheme in sentence:
        if morpheme['pos'] == '名詞':
            word += morpheme['surface']
            count += 1
        else:
            if count >= 2:
                nouns2.append(word)
            word = ''
            count = 0 
print(nouns2[:30]) 
```

```:出力結果
['見始', '時妙', '一毛', '後猫', '上今まで', 'うち池', '書生以外', '間おさん', '宿なし', 'まま奥', '終日書斎', 'ぎりほとんど', '時々忍び足', '二三ページ', '主人以外', '朝主人', '椽側', '一間', '神経胃弱', '時々同衾', '言語同断', '家内総がかり', '先日玉', '一部始終', 'いくら人間', '後架先生', 'そら宗盛', '一月', '月給日', '水彩絵具']
```

## 35. 単語の出現頻度

```python:コード
# 二次元リストをflatten
words = reduce(list.__add__, docs)
# 単語だけを抜き出し
words = collections.Counter(map(lambda e: e['surface'], words))
# 出現頻度を算出
words = words.most_common()
# 出現頻度順にソート
words = sorted(words, key=lambda e: e[1], reverse=True)
print(words[:30])
```

```:出力結果
[('の', 9546), ('。', 7486), ('て', 7401), ('に', 7047), ('、', 6772), ('は', 6485), ('と', 6150), ('を', 6118), ('が', 5395), ('で', 4542), ('た', 3975), ('「', 3238), ('」', 3238), ('も', 3229), ('だ', 2705), ('し', 2530), ('ない', 2423), ('から', 2213), ('か', 2041), ('ある', 1729), ('ん', 1625), ('な', 1600), ('いる', 1255), ('事', 1214), ('する', 1056), ('もの', 1005), ('へ', 998), ('です', 978), ('君', 967), ('云う', 937)]
```

## 36. 頻度上位10語

```python:コード
words_df = pd.DataFrame(words[:10], columns=['word', 'count'])
sns.set(font='AppleMyungjo')

fig = plt.figure()
ax = fig.add_subplot(111)
ax.bar(words_df['word'], words_df['count'])
plt.show()
```

出力結果
![36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227999/98655096-4eca-d85e-58a6-ad7058dede91.png)


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

```python:コード
cats = []
for sentence in docs:
    cat_list = list(filter(lambda e: e['surface'] == '猫', sentence))
    if len(cat_list) > 0:
        for morpheme in sentence:
            if morpheme['surface'] != '猫':
                cats.append(morpheme['surface'])
cats = collections.Counter(cats)
# 出現頻度を算出
cats = cats.most_common()
# 出現頻度順にソート
cats = sorted(cats, key=lambda e: e[1], reverse=True)

cats_df = pd.DataFrame(cats[:10], columns=['word', 'count'])
sns.set(font='AppleMyungjo')

fig = plt.figure()
ax = fig.add_subplot(111)
ax.bar(cats_df['word'], cats_df['count'])
# plt.show()
plt.savefig('37.png')
```

出力結果
![37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227999/9fae7f13-37e5-05d1-f400-c8d45aca6b1e.png)


## 38. ヒストグラム
単語の出現頻度のヒストグラム（横軸に出現頻度，縦軸に出現頻度をとる単語の種類数を棒グラフで表したもの）を描け．

```python:コード
hist_df = pd.DataFrame(words, columns=['word', 'count'])
sns.set(font='AppleMyungjo')

fig = plt.figure()
ax = fig.add_subplot(111)
ax.hist(hist_df['count'], range=(1, 100))
# plt.show()
plt.savefig('38.png')
```

出力結果
![38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227999/c00f1387-7458-16c9-2902-49cc4bee45b7.png)

横軸は100までに制限しています。出現回数1付近が圧倒的に多いため、制限なしにして表示すると以下のようになってしまい、まともに可視化できません。
![38_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227999/0778c9fc-655e-c457-0b92-e5e15fc6f927.png)


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

```python:コード
zipf_df = pd.DataFrame(words, columns=['word', 'count'])
sns.set(font='AppleMyungjo')

fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_yscale('log')
ax.set_xscale('log')
ax.plot(zipf_df['count'])
# plt.show()
plt.savefig('39.png')
```

出力結果
![39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227999/0546ce1a-3edd-a927-a04d-bdf3bc519170.png)

対数グラフにすることで見れるようになります。

# おわりに
4章で学べること

* 形態素解析のやり方
* matplotlibによるグラフ出力
* リスト、辞書操作