言語処理100本ノック俺の解答 第4章

[言語処理100本ノック第4章: UNIXコマンド](https://nlp100.github.io/ja/ch04.html)の俺の解答。[その他の章はこちら](https://qiita.com/sangoro/items/9bb6b5c39e756818cce2)。

解答に入る前に環境について。Cygwin上のPythonを使っているのだが、MeCabについては[公式Windowsバイナリ](https://taku910.github.io/mecab/#download)を普通に入れた。辞書の文字コードはUTF-8を選択。そしてWindowsの環境変数PathにC:\Program Files (x86)\MeCab\binを追加。
するとCygwinコンソールから
```
$ mecab neko.txt -o neko.txt.mecab
```
というふうに何の問題もなく使えた。

あと、PythonからMecabを使うには[fugashi](https://pypi.org/project/fugashi/)とかのラッパーがあるが、あえて使わずに。

# 30. 形態素解析結果の読み込み
```Python
import argparse
import sys

parser = argparse.ArgumentParser()
parser.add_argument('-o', help="outpuf filename")
args = parser.parse_args()

sys.stdin.reconfigure(encoding='utf-8')

sentences = []
sentence = []
for l in sys.stdin:
    l = l.rstrip()
    if l == 'EOS':
        sentences.append(sentence)
        sentence = []
    else:
        (surface, features) = l.split('\t')
        features_list = features.split(',')
        word = {}
        word['surface'] = surface
        word['base'] = features_list[6]
        word['pos'] = features_list[0]
        word['pos1'] = features_list[1]
        sentence.append(word)

f_o = open(args.o, "w", encoding="utf_8", newline='\n')
f_o.write(str(sentences))
```

# 31-35
まとめて。上記30のsentencesに対する操作として。
```Python
def is_a_no_b(w0, w1, w2):
    if w0['pos'] != '名詞':
        return False
    if w1['surface'] != 'の':
        return False
    if w1['pos'] != '助詞':
        return False
    if w2['pos'] != '名詞':
        return False
    return True

verbs = set()
a_no_b = set()
noun_phrases = set()
counts = {}
for s in sentences:
    noun_phrase = []
    for i in range(len(s)):
        if s[i]['pos'] == '動詞':
            verbs.add((s[i]['surface'], s[i]['base']))
        
        if i >= 2 and is_a_no_b(s[i-2], s[i-1], s[i]):
            a_no_b.add(s[i-2]['surface'] + 'の' + s[i]['surface'])
        
        if s[i]['pos'] == '名詞':
            noun_phrase.append(s[i]['surface'])
        else:
            if len(noun_phrase) > 1:
                noun_phrases.add(''.join(noun_phrase))
            noun_phrase = []
            
        if s[i]['surface'] not in counts:
            counts[s[i]['surface']] = 0
        counts[s[i]['surface']] += 1

f_o.write(str(verbs)+'\n')
f_o.write(str(a_no_b)+'\n')
f_o.write(str(noun_phrases)+'\n')

counts_sorted = sorted(counts.items(),
                       key=lambda x:x[1],
                       reverse = True)
f_o.write(str(counts_sorted)+'\n')
```
ところで、文中に「御前は女だけれども many a slip 'twixt the cup and the lip と云う西洋の諺くらいは心得ているだろう」みたいに英語が出てくるところがあるんですね。名詞句の出力時に、間に半角スペース入れずに出すとアルファベットがくっついちゃってなんじゃこりゃになるのですが、まぁ見なかったことにしよう。

# 36. 頻度上位10語

```Python
import argparse
import sys
from matplotlib import pyplot

parser = argparse.ArgumentParser()
parser.add_argument('-o', help="outpuf filename")
args = parser.parse_args()

sys.stdin.reconfigure(encoding='utf-8')

sentences = []
sentence = []
for l in sys.stdin:
    l = l.rstrip()
    if l == 'EOS':
        sentences.append(sentence)
        sentence = []
    else:
        (surface, features) = l.split('\t')
        sentence.append(surface)

counts = {}
for s in sentences:
    for i in range(len(s)):
        if s[i] not in counts:
            counts[s[i]] = 0
        counts[s[i]] += 1

counts_sorted = sorted(counts.items(),
                       key=lambda x:x[1],
                       reverse = True)

pyplot.rcParams['font.family'] = 'MS Gothic'
x = range(0, 10)
labels, y = zip(*counts_sorted[0:10])
pyplot.bar(x, y, tick_label=labels)
pyplot.savefig(args.o)
```

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

```Python
import argparse
import sys
from matplotlib import pyplot

parser = argparse.ArgumentParser()
parser.add_argument('-o', help="outpuf filename")
args = parser.parse_args()

sys.stdin.reconfigure(encoding='utf-8')

sentences = []
sentence = []
for l in sys.stdin:
    l = l.rstrip()
    if l == 'EOS':
        sentences.append(sentence)
        sentence = []
    else:
        (surface, features) = l.split('\t')
        sentence.append(surface)

counts = {}
for s in sentences:
    if '猫' not in s:
        continue
    for w in s:
        if w == '猫':
            continue
        if w not in counts:
            counts[w] = 0
        counts[w] += 1

counts_sorted = sorted(counts.items(),
                       key=lambda x:x[1],
                       reverse = True)

pyplot.rcParams['font.family'] = 'MS Gothic'
x = range(0, 10)
labels, y = zip(*counts_sorted[0:10])
pyplot.bar(x, y, tick_label=labels)
pyplot.savefig(args.o)
```
結局、助詞みたいな通常ストップワードにするようなものしか取れず、つまらない。

そこで、共起頻度を、文章全体の頻度で割ってみた。なお、後付けでの考察ですが、これは、[共起しやすさを計測する方法](http://user.keio.ac.jp/~rhotta/hellog/2012-10-31-1.html)のひとつであるMI（相互情報量）すなわち$log_2 \frac{C(x,y)\cdot N}{C(x)C(y)}$（[参考](https://camberbridge.github.io/2016/07/08/%E8%87%AA%E5%B7%B1%E7%9B%B8%E4%BA%92%E6%83%85%E5%A0%B1%E9%87%8F-Pointwise-Mutual-Information-PMI-%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6/)）を使うのと同等ですね。なぜならxを「猫」で固定し、かつ値の絶対値ではなく値をソートした順位だけを使うとすると、$\frac{C(猫,y)}{C(y)}$を求めれば十分なので。

ところが、実際にやってみると、その値が1.0になる単語が沢山ある。つまり、現れるときは常に猫と一緒である単語が沢山あった。そこでさらに、そのような単語について、共起頻度でソートして上位をとってみた。（下記コードは、途中までは上記コードと一緒のため省略している。）
```Python
counts = {}
counts_cat = {}
for s in sentences:
    for w in s:
        if w not in counts:
            counts[w] = 0
        counts[w] += 1
    
    if '猫' not in s:
        continue
    for w in s:
        if w == '猫':
            continue
        if w not in counts_cat:
            counts_cat[w] = 0
        counts_cat[w] += 1

# 必ず猫と共起する単語のリスト
always_with_cat = {}
for w in counts_cat:
    if counts_cat[w] == counts[w]:
        always_with_cat[w] = counts[w]

always_with_cat_sorted = sorted(always_with_cat.items(),
                                key=lambda x:x[1],
                                reverse = True)

pyplot.rcParams['font.family'] = 'MS Gothic'
x = range(0, 10)
labels, y = zip(*always_with_cat_sorted[0:10])
pyplot.bar(x, y, tick_label=labels)
pyplot.savefig(args.o)
```
その結果がこちら：
![nlp100-37-ratio-out.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2468443/fac75a78-8414-6646-3a76-6423c142a7c8.png)
ん？と思って調べてみたら、作中で、亡くなった猫に猫誉信女（みょうよしんにょ）という戒名が付けられているんですね。

# 38. ヒストグラム
下記コードも途中までは省略している。
```Python
counts = {}
for s in sentences:
    for w in s:
        if w not in counts:
            counts[w] = 0
        counts[w] += 1

max_count = max(counts.values())
x = range(1, max_count + 1)

histgram = dict.fromkeys(x, 0)
for w in counts:
    histgram[counts[w]] += 1

pyplot.rcParams['font.family'] = 'MS Gothic'
pyplot.plot(x, histgram.values())
pyplot.savefig(args.o)
```
結果は横軸・縦軸ともに0とか1に張り付いた、ほとんど直角なグラフになっちゃうけど、それが次の問題への布石ってことでしょうね。

# 39. Zipfの法則
下記コードも途中までは省略している。
```Python
counts = {}
for s in sentences:
    for w in s:
        if w not in counts:
            counts[w] = 0
        counts[w] += 1

counts_sorted = sorted(counts.values(), reverse=True)

pyplot.rcParams['font.family'] = 'MS Gothic'
pyplot.xscale('log')
pyplot.yscale('log')
pyplot.plot(range(1, len(counts_sorted) + 1), counts_sorted)
pyplot.savefig(args.o)
```