pythonによるword2vec等によるテキストマイニング（【高等学校情報科 情報Ⅱ】教員研修用教材）

# はじめに
過去の記事で青空文庫の作品を題材に簡単なテキストマイニングを行いました。
https://qiita.com/ereyester/items/7c220a49c15073809c33
今回は、Word2vecを使って、単語の類似度を探りたいと思います。
Word2vecについては、ほかにたくさん記事がありますので具体的な説明は省略します。
[Word2Vecを理解する](https://qiita.com/g-k/items/69afa87c73654af49d36 "Word2Vecを理解する")
[【Python】Word2Vecの使い方](https://qiita.com/kenta1984/items/93b64768494f971edf86 "【Python】Word2Vecの使い方")
今回は、gensimのgensim.models 内のword2vec.Word2Vec()の関数を中心にまとめていきたいと思います。
[絵で理解するWord2vecの仕組み](https://qiita.com/Hironsan/items/11b388575a058dc8a46a "絵で理解するWord2vecの仕組み")

# 教材
[高等学校情報科「情報Ⅱ」教員研修用教材(本編)：文部科学省](https://www.mext.go.jp/a_menu/shotou/zyouhou/detail/mext_00742.html "高等学校情報科「情報Ⅱ」教員研修用教材(本編)：文部科学省")
[第3章 情報とデータサイエンス 後半 (PDF:7.6MB) ](https://www.mext.go.jp/content/20200609-mxt_jogai01-000007843_007.pdf "第3章 情報とデータサイエンス 後半 (PDF:7.6MB) ")

# 環境
- ipython
- [Colaboratory - Google Colab](https://colab.research.google.com/notebooks/welcome.ipynb "Colaboratory - Google Colab")

# 教材内で取り上げる箇所
> 学習18 テキストマイニングと画像認識：「2.MeCabを利用したテキストマイニング」

#pythonでの実装例と結果

##準備
pythonにて、Word2vecで機械学習を行うために、gensimというパッケージを読み込みます。

```python
!pip install gensim
```

次に後で行う感情分析のための感情辞書をダウンロードしておきます。
感情分析の際にWord2vecで感情を示す用語の主な用語との距離を求めることにより感情分析が可能ですが、ここでは日本語辞書として，東京工業大学のPN Tableを使用して感情分析を行います。

```python
import urllib.request
import pandas as pd

#PN tableのリンク
url = 'http://www.lr.pi.titech.ac.jp/~takamura/pubs/pn_ja.dic'

#ファイル保存名
file_path = 'pn_ja.dic'

with urllib.request.urlopen(url) as dl_file:
    with open(file_path, 'wb') as out_file:
        out_file.write(dl_file.read())

# 辞書を読み込みます
dic = pd.read_csv('/content/pn_ja.dic', sep = ':', encoding= 'shift_jis', names = ('word','reading','Info1', 'PN'))

print(dic)
```

実行結果は以下になります。

```console
       word reading Info1        PN
0       優れる    すぐれる    動詞  1.000000
1        良い      よい   形容詞  0.999995
2        喜ぶ    よろこぶ    動詞  0.999979
3       褒める     ほめる    動詞  0.999979
4      めでたい    めでたい   形容詞  0.999645
...     ...     ...   ...       ...
55120    ない      ない   助動詞 -0.999997
55121    酷い     ひどい   形容詞 -0.999997
55122    病気    びょうき    名詞 -0.999998
55123    死ぬ      しぬ    動詞 -0.999999
55124    悪い     わるい   形容詞 -1.000000

[55125 rows x 4 columns]
```

次に、Mecabをインストールしておきます。

（2020/09/18　19:00追記）
mecab-python3の作者様から、mecab-python3をインストールする前にaptitudeでmecab、libmecab-dev、ipadicのインストールは不要だという指摘を受けましたので修正いたします。

```python
!pip install mecab-python3
!pip install unidic-lite
```

## Word2vecによるモデル構築とテキストの分析

word2vecで学習をさせるために、分析対象の文章を分かち書きのテキストに変換して保存します。
以下のステップで処理を行います。
①夏目漱石の「坊ちゃん」に対してテキスト分析を行うため「坊ちゃん」のテキストデータをダウンロードし読み込みを行います。
②ルビ、注釈などの除去を行います。
③「坊ちゃん」の本文から、名詞、形容詞、動詞を取り出し、数や非自立語等を取り除き、「分かち書き」に変換して、tf.txtというファイルに変換する。

```python
from collections import Counter
import MeCab    #MeCabを読み出す
import zipfile
import os.path,glob
import re

#「坊ちゃん」のURLを指定
url = 'https://www.aozora.gr.jp/cards/000148/files/752_ruby_2438.zip'

#zipファイル保存名
file_path = 'temp.zip'

# 坊ちゃんのファイルを開き、読み込んだファイルは削除する
with urllib.request.urlopen(url) as dl_file:
    with open(file_path, 'wb') as out_file:
        out_file.write(dl_file.read())
        with zipfile.ZipFile(file_path) as zf:
            listfiles = zf.namelist()
            zf.extractall()

os.remove(file_path)

# shift_jisで読み込み
with open(listfiles[0], 'rb') as f:
    text = f.read().decode('shift_jis')

# ルビ、注釈などの除去
text = re.split(r'\-{5,}', text)[2]
text = re.split(r'底本：', text)[0]
text = re.sub(r'《.+?》', '', text)
text = re.sub(r'［＃.+?］', '', text)
text = text.strip()

#MeCabを使えるように準備
tagger = MeCab.Tagger()

# 初期化しないとエラーになる
tagger.parse("")

# NMeCabで形態素解析
node = tagger.parseToNode(text)
word_list_raw = []
result_dict_raw = {}
# 名詞、形容詞、動詞は取り出す
wordclass_list = ['名詞','形容詞','動詞']
# 数、非自立、代名詞、接尾は除外
not_fine_word_class_list = ["数", "非自立", "代名詞","接尾"]

while node:
    #詳細情報を取得
    word_feature = node.feature.split(",")
    #単語を取得（原則、基本形）
    word = node.surface
    #品詞を取得
    word_class = word_feature[0]
    fine_word_class = word_feature[1]
    #品詞から取り出すものと除外するものを指定する
    if ((word not in ['', ' ','\r', '\u3000']) \
        and (word_class in wordclass_list) \
        and (fine_word_class not in not_fine_word_class_list)):
        #wordリスト
        word_list_raw.append(word)
        result_dict_raw[word] = [word_class, fine_word_class]
    #次の単語に進める
    node = node.next
print(word_list_raw)

wakachi_text = ' '.join(word_list_raw);

#wakachiファイル保存名
file2_path = 'tf.txt'

with open(file2_path, 'w') as out_file:
    out_file.write(wakachi_text)

print(wakachi_text)
```

実行結果は以下になります。

```console
['一', '親譲り', '無鉄砲', '小供', '時', '損', 'し', 'いる', '学校',…
一 親譲り 無鉄砲 小供 時 損 し いる 学校 居る 時分 学校 二 階 飛び降り…
```

次に、今回の記事のメインであるword2vecによるモデルの構築です。

```python
from gensim.models import word2vec
import logging

logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
sentence_data = word2vec.LineSentence('tf.txt')
model_bochan = word2vec.Word2Vec(sentence_data,
                         sg=1,        # Skip-gram
                         size=100,    # 次元数
                         min_count=5, # min_count回未満の単語を破棄
                         window=12,   # 文脈の最大単語数
                         hs=0,        # 階層ソフトマックス(ネガティブサンプリングするなら0)
                         negative=5,  # ネガティブサンプリング
                         iter=10      # Epoch数
                         )

model_bochan.save('test.model')
```


gensimモジュールのword2vecをインポートし、Word2Vecでモデルの構築を行います。

word2vecでは、二つの学習モデルが使用できます。
- CBOW(continuous bag-of-words)
- skip-gram

この二つの説明は割愛しますが、教材ではskip-gramを使用しているので、そちらにあわせてskip-gramを使用しています。（一般的には、CBOWとskip-gramでは、skip-gramのほうがよい性能を示すとのこと）


単語ベクトルの次元数は、デフォルトと同じですが100にしています。
単語を破棄条件は、出現回数が5回未満の単語は無視して処理するようにしています。
コンテクストとして認識する前後の最大単語数は12としています。

学習を高速化するアルゴリズムとして二つあります。
-Hierarchical Softmax
-Negative Sampling
この二つの説明も割愛します。ここでは、Negative Samplingを使用しています。

この説明は、
http://tkengo.github.io/blog/2016/05/09/understand-how-to-learn-word2vec/
が詳しいです。

コーパスの反復回数は10を指定しています。これは、一つの訓練データをニューラルネットワークで何回学習させるかを示すエポック数のことです。

これにより、モデルを構築することができました。

次に教材同様、単語の類似度をみてみます。
赤という単語で調べてみましょう。

```python
model   = word2vec.Word2Vec.load('/content/test.model')
results = model.most_similar(positive=['赤'], topn=100)

for result in results:
    print(result[0], '\t', result[1])
```

実行結果は以下になりました。

```console
:
2020-09-16 12:30:12,986 : INFO : precomputing L2-norms of word weight vectors
シャツ 	 0.9854607582092285
迷惑 	 0.9401918053627014
名 	 0.9231084585189819
ゴルキ 	 0.9050831198692322
知っ 	 0.8979452252388
優しい 	 0.897865891456604
賛成 	 0.8932155966758728
露西亜 	 0.8931306004524231
マドンナ 	 0.890703558921814
：
```

上記のように、登場人物の赤シャツなどが上位に来ることがわかりました。
次に、モデルの要素の引き算の例として、「マドンナ」から「シャツ」を引いてみます。

```python
model = word2vec.Word2Vec.load('/content/test.model')

results = model.most_similar(positive=['マドンナ'], negative=['シャツ'], topn=100)
for result in results:
    print(result[0], '\t', result[1])
```

実行結果は以下になりました。

```console
:
INFO : precomputing L2-norms of word weight vectors
声 	 0.2074282020330429
芸者 	 0.1831434667110443
団子 	 0.13945674896240234
娯楽 	 0.13744047284126282
天麩羅 	 0.11241232603788376
バッタ 	 0.10779635608196259
先生 	 0.08393052220344543
精神 	 0.08120302855968475
親切 	 0.0712042897939682
:
```
マドンナからシャツという要素を引くと、声や先生、芸者、親切などの要素を抽出できました。

word2vecによる要素の足し引きについて、以下が詳しいです。
https://www.pc-koubou.jp/magazine/9905

## PN tableによる簡単な感情分析

感情分析は、Word2Vecでも感情の主要用語との距離を求めることにより、感情分析が可能であるが、ここでは教材通り前述で読み込んだ感情辞書(PN Table)で分析を行ってみたいと思います。

まず、辞書をdataframe型からdict型に変換し扱いやすい形にしておきます。

```python
dic2 = dic[['word', 'PN']].rename(columns={'word': 'TERM'})

# PN Tableをデータフレームからdict型に変換しておく
word_list = list(dic2['TERM'])
pn_list = list(dic2['PN'])  # 中身の型はnumpy.float64

pn_dict = dict(zip(word_list, pn_list))

print(pn_dict)

```

実行結果は以下になりました。

```console
{'優れる': 1.0, '良い': 0.9999950000000001, '喜ぶ': 0.9999790000000001, '褒める': 0.9999790000000001, 'めでたい': 0.9996450000000001,…
```

ポジティブな用語は1に近い値が設定され、ネガティブな用語については-1に近い値が設定されています。

次に、「坊ちゃん」から名詞と形容詞を取り出し、数や接尾語を取り除きます。単語の頻度表と感情辞書を組み合わせて、ポジティブな言葉とネガティブな言葉を表示してみます。

```python
#MeCabを使えるように準備
tagger = MeCab.Tagger()

# 初期化しないとエラーになる
tagger.parse("")

# NMeCabで形態素解析
node = tagger.parseToNode(text)
word_list_raw = []
extra_result_list = []
# 名詞、形容詞、動詞は取り出す
wordclass_list = ['名詞','形容詞']
# 数、非自立、代名詞、接尾は除外
not_fine_word_class_list = ["数","接尾", "非自立"]

while node:
    #詳細情報を取得
    word_feature = node.feature.split(",")
    #単語を取得（原則、基本形）
    word = node.surface
    #品詞を取得
    word_class = word_feature[0]
    fine_word_class = word_feature[1]
    #品詞から取り出すものと除外するものを指定する
    if ((word not in ['', ' ','\r', '\u3000']) \
        and (word_class in wordclass_list) \
        and (fine_word_class not in not_fine_word_class_list)):
        #wordリスト
        word_list_raw.append(word)
    #次の単語に進める
    node = node.next

freq_counterlist_raw = Counter(word_list_raw)
dict_freq_raw = dict(freq_counterlist_raw)

extra_result_list = []
for k, v in dict_freq_raw.items():
    if k in pn_dict:
        extra_result_list.append([k, v, pn_dict[k]])

extra_result_pn_sorted_list = sorted(extra_result_list, key=lambda x:x[2], reverse=True)
print("ポジティブな単語")
display(extra_result_pn_sorted_list[:10])
print("ネガティブな単語")
display(extra_result_pn_sorted_list[-10:-1])
```

実行結果は以下になりました。

```console
ポジティブな単語
[['めでたい', 1, 0.9996450000000001],
 ['善い', 2, 0.9993139999999999],
 ['嬉しい', 1, 0.998871],
 ['仕合せ', 1, 0.998208],
 ['手柄', 2, 0.997308],
 ['正義', 1, 0.9972780000000001],
 ['感心', 10, 0.997201],
 ['恐悦', 1, 0.9967889999999999],
 ['奨励', 1, 0.9959040000000001],
 ['剴切', 1, 0.995553]]
ネガティブな単語
[['乱暴', 13, -0.9993340000000001],
 ['狭い', 7, -0.999342],
 ['寒い', 1, -0.999383],
 ['罰', 5, -0.9994299999999999],
 ['敵', 3, -0.9995790000000001],
 ['苦しい', 1, -0.9997879999999999],
 ['下手', 6, -0.9998309999999999],
 ['ない', 338, -0.9999969999999999],
 ['病気', 6, -0.9999979999999999]]
```

listの要素の2つ目が出現頻度（回数）、3つ目がポジティブかネガティブかわかる値となっております。
最後に、「坊ちゃん」全体としては、ポジティブ・ネガティブどちらの単語がよく使われているかをみてみる。（ただし、教材通り単語の出現頻度は使用していない）

```python
pos_n = sum(x[2] > 0 for x in extra_result_pn_sorted_list)

print(pos_n)

neg_n = sum(x[2] < 0 for x in extra_result_pn_sorted_list)

print(neg_n)

```

実行結果は以下になりました。

```console
182
1914
```

「坊ちゃん」ではネガティブな単語がよくつかわれていることがわかりました。


##コメント

word2vecに関する処理に関してはpythonとRで似たような結果が出なかったので、今後余裕があったら原因を探っていこうと思います。

##ソースコード

https://gist.github.com/ereyester/101ae0da17e747b701b67fe9fe137b84