1. Yusuke196

    Posted

    Yusuke196
Changes in title
+言語処理100本ノック第4章で学ぶ形態素解析
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,254 @@
+### はじめに
+
+社内のメンバーを中心に進めている勉強会で[言語処理100本ノック](http://www.cl.ecei.tohoku.ac.jp/nlp100)を解いてみたのですが、そのコードや解いている過程で便利だなと思った小技のまとめです。自分で調べたり検証したりした内容が多いですが、他の勉強会メンバーが共有してくれた情報も入っています。
+
+第3章まではPythonプログラミング全般で使える内容でしたが、この章は形態素解析ということで、いよいよ言語処理らしくなってきました。
+
+### 前処理
+
+カレントディレクトリに`neko.txt`を置いてから
+
+```bash
+!mecab neko.txt -o neko.txt.mecab
+```
+
+とすると形態素解析の結果が`neko.txt.mecab`に記録されます。
+
+### 30. 形態素解析結果の読み込み
+
+```py
+import re
+import itertools
+
+def parse_text(flat=True):
+ with open('neko.txt.mecab') as file:
+ morphs = []
+ sentence = []
+
+ for line in file:
+ if re.search(r'EOS', line):
+ continue
+
+ surface, rest = line.split('\t')
+ arr = rest.split(',')
+ sentence.append({
+ 'surface': surface,
+ 'base': arr[6],
+ 'pos': arr[0],
+ 'pos1': arr[1],
+ })
+
+ if surface == ' ': # 空白は形態素と見なさない
+ sentence.pop(-1)
+ if surface in [' ', '。']: # 空白と句点を文の終わりと見なす
+ morphs.append(sentence)
+ sentence = []
+
+ if flat:
+ return list(itertools.chain.from_iterable(morphs))
+ else:
+ return morphs
+
+parse_text(flat=False)[:10]
+```
+
+この問題では「1文を形態素(マッピング型)のリストとして表現」することが求められていますが、31以降の問題でこの関数を使う際は返り値が平坦化されていた(ネストされていない)方が扱いやすいので、`flat`という仮引数を用意することにしました。この`flat`のデフォルト値は`True`としましたが、この問題では平坦化されていないリストが欲しいので`flat=False`という引数を渡しています。
+
+最後の行で`[:10]`としているのは確認のために表示する要素の数が10個ほどだとちょうどよかったからで、`10`である必然性は特にありません。
+
+### 31
+
+```py
+import numpy as np
+
+def extract_surface():
+ verbs = list(filter(lambda morph: morph['pos'] == '動詞', parse_text()))
+ return [verb['surface'] for verb in verbs]
+
+print(extract_surface()[:100])
+```
+
+返り値は`list(map(lambda morph: morph['surface'], verbs))`としても作れますが、上のように内包表記を使ったほうが簡潔なコードになります。
+
+また、これは問題文をどう解釈するかによりますが、返り値となるリストで要素の重複を許したくない場合は、関数の最後の行を
+
+```py
+return set([verb['surface'] for verb in verbs])
+```
+
+とする、などの方法があります。
+
+### 32. 動詞の原形
+
+```py
+def extract_base():
+ verbs = list(filter(lambda morph: morph['pos'] == '動詞', parse_text()))
+ return [verb['base'] for verb in verbs]
+
+print(extract_base()[:100])
+```
+
+31とほぼ同じ。
+
+### 33. サ変名詞
+
+```py
+def extract_sahens():
+ return list(filter(lambda morph: morph['pos1'] == 'サ変接続', parse_text()))
+
+extract_sahens()[:20]
+```
+
+31とほぼ同じ。
+
+### 34. 「AのB」
+
+```py
+def extract_noun_phrases():
+ morphs = parse_text()
+ phrases = []
+
+ for i, morph in enumerate(morphs):
+ if morph['surface'] == 'の' and morphs[i - 1]['pos'] == '名詞' \
+ and morphs[i + 1]['pos'] == '名詞':
+ phrases.append(
+ morphs[i - 1]['surface'] + 'の' + morphs[i + 1]['surface'])
+
+ return phrases
+
+print(extract_noun_phrases()[:100])
+```
+
+`enumerate()`を使うと、`morph`が`morphs`の中で何番目かを`i`という情報を変数として扱えるのですっきりしたコードになります。
+
+### 35. 名詞の連接
+
+```py
+def extract_continuous_nouns():
+ morphs = parse_text()
+ continuous_nouns = []
+
+ for i, morph in enumerate(morphs):
+ if morph['pos'] == '名詞' and morphs[i + 1]['pos'] == '名詞':
+ continuous_noun = morph['surface'] + morphs[i + 1]['surface']
+
+ j = 1
+ while morphs[i + 1 + j]['pos'] == '名詞':
+ continuous_noun += morphs[i + 1 + j]['surface']
+ j += 1
+
+ continuous_nouns.append(continuous_noun)
+
+ return continuous_nouns
+
+print(extract_continuous_nouns()[:100])
+```
+
+### 36. 単語の出現頻度
+
+```py
+import pandas as pd
+
+def sort_by_freq():
+ words = [morph['base'] for morph in parse_text()]
+
+ return pd.Series(words).value_counts()
+
+sort_by_freq()[:20]
+```
+
+別解としては、以下のように標準ライブラリの`collections`をインポートしておいて、
+
+```py
+from collections import Counter
+```
+
+`Counter()`オブジェクトの`most_common()`メソッドを(`pandas`の`value_counts()`の代わりに)使う方法もあります。
+
+```py
+return Counter(words).most_common()
+```
+
+この二つの解法の違いとしては`value_counts()`が`Series`を返すのに対して`most_common()`はタプルの配列を返すので、どちらを使ったらその後の処理がしやすくなるかで使い分けるといいのではないでしょうか。
+
+### 37. 頻度上位10語
+
+```py
+import japanize_matplotlib
+
+def draw_bar_graph():
+ sort_by_freq()[:10].plot.bar()
+
+draw_bar_graph()
+```
+
+![bar.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229718/a1948037-75aa-5427-f41f-f2761a5d6e80.png)
+
+日本語を表示させるのに一手間かかりますが、[こちら](https://yolo.love/matplotlib/japanese/)の記事で紹介されている`japanize_matplotlib`というライブラリを使うと簡単でした。
+
+また関数の中身について、 自分は短く書きたかったので`Series`オブジェクトに対して`.plot.bar()`としましたが、
+
+```py
+import matplotlib.pyplot as plt
+```
+
+として`matplotlib`をインポートしたうえで、以下のように書いても動作します。
+
+```py
+morph_freqs = sort_by_freq()[:10]
+plt.bar(morph_freqs.index, morph_freqs)
+```
+
+### 38. ヒストグラム
+
+```py
+import matplotlib.pyplot as plt
+
+def draw_hist():
+ plt.xlabel('出現頻度')
+ plt.ylabel('単語の種類数')
+ plt.hist(sort_by_freq(), bins=200)
+
+draw_hist()
+```
+
+![hist.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229718/9edcf147-bb39-c03d-cd50-4bbd327c8fc9.png)
+
+全体を表示するならこのようなコードになりますが、ちょっと見づらいので以下のように表示する範囲を限定するほうが現実的かもしれません。
+
+```py
+def draw_hist_2():
+ plt.xlabel('出現頻度')
+ plt.ylabel('単語の種類数')
+ plt.title('出現頻度20以下')
+ plt.hist(sort_by_freq(), bins=20, range=(1, 20))
+
+draw_hist_2()
+```
+
+![hist2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229718/18ca5e4f-0d51-bed7-d688-06870cdc7b29.png)
+
+### 39. Zipfの法則
+
+```py
+def draw_log_graph():
+ plt.xscale('log')
+ plt.yscale('log')
+ plt.xlabel('出現頻度順位')
+ plt.ylabel('出現頻度')
+ plt.scatter(range(1, len(sort_by_freq()) + 1), sort_by_freq(), s=10)
+
+draw_log_graph()
+```
+
+![zipf.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229718/4ae009bf-6388-eb12-e3c5-9bcb8d2bf72d.png)
+
+`scatter`メソッドを呼び出すとき、`s`オプションで点のサイズを指定できます。デフォルトは20ですが、それだと少し大きかったので`10`としました。
+
+### まとめ
+
+MeCabはオープンソースのソフトウェアですが、それでもこれだけいろいろなことができるということが分かって面白かったです。
+
+一方で、解析の精度はこれだけだと完璧ではないと感じる点もありました(たとえば35で出力結果として返ってくる「ぷうぷうと」は名詞ではなく副詞なのでは、など)。このあたりの問題を解決するには、いろいろな辞書を試したり自分でカスタマイズしたり、といった工夫が必要なのかなと思います。
+
+この章については以上になりますが、もし間違いなどあったらコメントいただけると助かります!