ゼロから作るDeep Learning❷で素人がつまずいたことメモ:2章

# はじめに
ふと思い立って勉強を始めた[「ゼロから作るDeep Learning❷ーー自然言語処理編」](https://www.oreilly.co.jp/books/9784873118369/)の2章で私がつまずいたことのメモです。

実行環境はmacOS Catalina + Anaconda 2019.10、Pythonのバージョンは3.7.4です。詳細は[このメモの1章](https://qiita.com/segavvy/items/91be1d4fc66f7e322f25)をご参照ください。

（このメモの他の章へ：[1章](https://qiita.com/segavvy/items/91be1d4fc66f7e322f25) / 2章 / [3章](https://qiita.com/segavvy/items/1510d8c264edd291e0ef) / [4章](https://qiita.com/segavvy/items/a286143dbcc5a93100b6) / [5章](https://qiita.com/segavvy/items/3e6a9f6bff103824598f) / [6章](https://qiita.com/segavvy/items/fba1595cf64a2462b415) / [7章](https://qiita.com/segavvy/items/883671d07cd7ab26fbe0) / [8章](https://qiita.com/segavvy/items/69ca97ff2d4e8a7144fd) / [まとめ](https://qiita.com/segavvy/items/0f2980ad746d797dd8c1)）

:::note info
この記事は個人で作成したものであり、内容や意見は所属企業・部門見解を代表するものではありません。
:::

# 2章 自然言語処理と単語の分散表現

この章から自然言語処理の話が始まります。

## 2.1 自然言語処理

自然言語処理とは「私たちの言葉をコンピュータに理解させるための技術（分野）」とありますが、「コンピュータに理解させる」と聞くとイメージが膨らんでしまってドラえもんみたいなものを想像してしまうので、私は「コンピュータで処理可能にする」くらいの表現が良いのではないかと思います。

数値データであれば、合計したり、平均取って比べたり、グラフで可視化したり、時系列データで先を予測したり、etc.と簡単に処理できます。前巻で学んだディープラーニングも活用できます。でも、自然言語のデータではそうはいきません。それを可能にするための技術ということです。

あと、NLPという略語は神経言語プログラミング（Neuro-Linguistic Programming）と同じで、「NLP」でググると神経言語プログラミングの話が最初にでてきます。分野が違うので混乱することはないかとは思いますが、ディープラーニングの勉強中に出てくると一瞬関係するかも？と思ってしまうかも知れませんのでご注意ください。

## 2.2 シソーラス

本では英語の話しか出てこないので、日本語のシソーラスについてメモしておきます。

- WordNetには日本語版の日本語WordNetがあります。ただし、本の「付録B WordNet を動かす」のようにNLTKで使えるのかは未確認です。
- プログラムから利用可能なデータが公開されている訳ではないのですが、国立研究開発法人 科学技術振興機構（JST）が構築しているシソーラスも有名のようです。こちらは[JSTシソーラスmap](https://thesaurus-map.jst.go.jp/jisho/fullIF/index.html)という用語検索サイトが用意されていて、用語を検索すると本の図2-2のようなグラフが表示されます。シソーラスだけでなく文献における共起頻度の統計情報なども使われていて、例えば「自動車」などで検索すると、スクロールしないと見渡せない規模の壮大なグラフが表示されます。ダブルクリックで用語をたどっていけます。

## 2.3 カウントベースの手法

カウントベースの手法は、3年ほど前に[言語処理100本ノック2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)で勉強していたので、そのおさらいをする形になりました。この100本ノックの[第9章: ベクトル空間法 (I)](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch8)が、この本の「2.3 カウントベースの手法」と「2.4 カウントベースの手法の改善」に相当するので、この後の特異値分解以外では特につまずく部分はなかったです。

## 2.4 カウントベースの手法の改善

「2.4.2 次元削減」の図2-8が、やや分かりにくいかもしれません。この図でイメージが湧かなかった方は、[@aya_taka](https://qiita.com/aya_taka)さんの[30分でわかる機械学習用語「次元削減(Dimensionality Reduction)」](https://qiita.com/aya_taka/items/4d3996b3f15aa712a54f)の冒頭にある身長と体重の例が分かりやすくて良いかと思います。

特異値分解（SVD）についてはつまずきました。実はもう3回目の勉強になるのですが（4年くらい前にやったCourseraのオンライン講座の[Machine Learning](https://www.coursera.org/learn/machine-learning)と前述の[言語処理100本ノック2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)）、イメージは理解できるものの計算の中身までは相変わらず良くわかりません。ちょっとググったくらいでは説明の意味がわからず、行列の勉強をきっちりやり直さないとダメそうです。NumPy（および次に出てくるscikit-learn）が計算してくれるということで、今回もそれに感謝しつつ先に進むことにしました:sweat: 

「2.4.4 PTBデータセット」では大きなコーパスとして英語のPTBコーパスを使うのですが、日本語大好きな私は日本語で試してみることにしました。日本語は英語と違って単語の境界に空白がないので、空白を入れる分かち書きの処理が必要ですが、今回はそれが済んでいる[青空文庫の分かち書き済みテキスト](https://github.com/segavvy/wakatigaki-aozorabunko)を使います。

本ではPTBコーパスを `dataset/ptb.py`で利用する形になっていますが、これを改造して`dataset/aozorabunko.py`を作りました。以下、そのソースコードですが、その前にいくつか注意点です。

- 対象データは3名の著者の作品を適当に13作品選んで連結しただけなので、かなりの偏りがあります。手法のベンチマークに使えるような代物ではなく、あくまでも「やってみた」なのでご了承ください。
- `ptb.load_data()`では引数で`'train'`、`'test'`、`'valid'`が選べるのですが、まだ今回使う`'train'`しか対応していません。同じ著者で今回使っていない作品を、そのうち持ってこようかと考えています。
- PTBコーパスの`len(corpus)`は929,589でしたが、今回の青空文庫のデータでは873,028でした。やや少ないです。
- [前巻を勉強した時のメモ](https://qiita.com/segavvy/items/4e8c36cac9c6f3543ffd)ではソースコードをほとんど一から書いていたのですが、本のコードをベースにされている方にとっては参考にしにくいため、今回は本のコードを最大限に流用する形に変えました。主な改造部分に`★`を入れています。（Visual Studio Codeに入れてるLinterの[flake8](https://pypi.org/project/flake8/)が赤波線だらけにしてしまうのがやや辛いですが:disappointed:）

```python:dataset/aozorabunko.py
# coding: utf-8
import sys
import os
sys.path.append('..')
try:
    import urllib.request
except ImportError:
    raise ImportError('Use Python3!')
import pickle
import numpy as np

# ★このURLはGitHubにアップしてある分かち書き済みの青空文庫作品詰め合わせのダウンロードURL
#  詳細は https://github.com/segavvy/wakatigaki-aozorabunko を参照してください。
url_base = 'https://github.com/segavvy/wakatigaki-aozorabunko/raw/master/'
key_file = {
    'train': '20200516merge.txt',
    'test': '',  # ★まだ使わないので準備していない
    'valid': ''  # ★まだ使わないので準備していない
}
save_file = {
    'train': 'aozorabunko.train.npy',
    'test': 'aozorabunko.test.npy',
    'valid': 'aozorabunko.valid.npy'
}
vocab_file = 'aozorabunko.vocab.pkl'

dataset_dir = os.path.dirname(os.path.abspath(__file__))


def _download(file_name):
    file_path = dataset_dir + '/' + file_name
    if os.path.exists(file_path):
        return

    print('Downloading ' + file_name + ' ... ')

    try:
        urllib.request.urlretrieve(url_base + file_name, file_path)
    except urllib.error.URLError:
        import ssl
        ssl._create_default_https_context = ssl._create_unverified_context
        urllib.request.urlretrieve(url_base + file_name, file_path)

    print('Done')


# ★テキストの分割が2箇所で使われるため関数化。実装は超その場しのぎ……
def _split_data(text):
    return text.replace('\n', '<eos> ').replace('。', '<eos> ').strip().split()


def load_vocab():
    vocab_path = dataset_dir + '/' + vocab_file

    if os.path.exists(vocab_path):
        with open(vocab_path, 'rb') as f:
            word_to_id, id_to_word = pickle.load(f)
        return word_to_id, id_to_word

    word_to_id = {}
    id_to_word = {}
    data_type = 'train'
    file_name = key_file[data_type]
    file_path = dataset_dir + '/' + file_name

    _download(file_name)

    words = _split_data(open(file_path).read())

    for i, word in enumerate(words):
        if word not in word_to_id:
            tmp_id = len(word_to_id)
            word_to_id[word] = tmp_id
            id_to_word[tmp_id] = word

    with open(vocab_path, 'wb') as f:
        pickle.dump((word_to_id, id_to_word), f)

    return word_to_id, id_to_word


def load_data(data_type='train'):
    '''
        :param data_type: データの種類：'train' or 'test' or 'valid (val)'
        :return:
    '''
    if data_type == 'val': data_type = 'valid'
    save_path = dataset_dir + '/' + save_file[data_type]

    word_to_id, id_to_word = load_vocab()

    if os.path.exists(save_path):
        corpus = np.load(save_path)
        return corpus, word_to_id, id_to_word

    file_name = key_file[data_type]
    file_path = dataset_dir + '/' + file_name
    _download(file_name)

    words = _split_data(open(file_path).read())
    corpus = np.array([word_to_id[w] for w in words])

    np.save(save_path, corpus)
    return corpus, word_to_id, id_to_word


if __name__ == '__main__':
    for data_type in ('train', 'val', 'test'):
        load_data(data_type)
```

このファイルを`dataset`ディレクトリの中に置いて、`ptb.py`の代わりに`aozorabunko.py`をimportして、`ptb.load_data()`の代わりに`aozorabunko.load_data()`すれば、PTBコーパスの代わりに青空文庫のデータが使えます。

なお、「2.4.5 PTBデータセットでの評価」の説明で「sklearnモジュールをインストールする必要があります」とありますが、このsklearnは[scikit-learn](https://scikit-learn.org/stable/)というPythonの機械学習ライブラリのことで、Anacondaに同梱されています。そのため、前巻の1章の手順でAnacondaをインストールされている場合は、特に何もせずに利用できます。

あと、PPMIの計算にもかなり時間がかかります。私の環境では数時間かかってしまうので、一度計算したらファイルにキャッシュするように変えました。また、クエリをいろいろ試してみたくなったため、標準入力できるようにしています。

以下、改造した`ch02/count_method_big.py`です。主な改造部分に`★`を入れています。

```python:ch02/count_method_big.py
# coding: utf-8
import sys
sys.path.append('..')
import numpy as np
from common.util import most_similar, create_co_matrix, ppmi
from dataset import aozorabunko  # ★青空文庫のコーパスを利用するように変更
import os   # ★PPMIの計算結果をキャッシュするため追加
import pickle   # ★PPMIの計算結果をキャッシュするため追加

window_size = 2
wordvec_size = 100

corpus, word_to_id, id_to_word = aozorabunko.load_data('train')  # ★コーパス変更
vocab_size = len(word_to_id)
print('counting  co-occurrence ...')
C = create_co_matrix(corpus, vocab_size, window_size)

# ★PPMIの計算は時間がかかるので前回結果をキャッシュし、Cが同じなら再利用するように変更
print('calculating PPMI ...')
W = None
ppmi_path = os.path.dirname(os.path.abspath(__file__)) + '/' + 'ppmi.pkl'
if os.path.exists(ppmi_path):
    # ★キャッシュの読み込み
    with open(ppmi_path, 'rb') as f:
        cache_C, cache_W = pickle.load(f)
    if np.array_equal(cache_C, C):
        W = cache_W  # Cの中身が同じなので再利用
if W is None:
    W = ppmi(C, verbose=True)
    with open(ppmi_path, 'wb') as f:
        pickle.dump((C, W), f)  # キャッシュとして保存

print('calculating SVD ...')
try:
    # truncated SVD (fast!)
    from sklearn.utils.extmath import randomized_svd
    U, S, V = randomized_svd(W, n_components=wordvec_size, n_iter=5,
                             random_state=None)
except ImportError:
    # SVD (slow)
    U, S, V = np.linalg.svd(W)

word_vecs = U[:, :wordvec_size]

# ★クエリを標準入力する形に変更
while True:
    query = input('\nquery? ')
    if not query:
        break
    most_similar(query, word_to_id, id_to_word, word_vecs, top=5)
```

以下、クエリーをいくつか試してみた結果です。まずは、本のクエリーの日本語訳から。

```
[query] あなた
 奥さん: 0.6728986501693726
 妻: 0.6299399137496948
 Ｋ: 0.6205178499221802
 父: 0.5986840128898621
 私: 0.5941839814186096

[query] 年
 反: 0.8162745237350464
 百: 0.8051895499229431
 分: 0.7906433939933777
 八: 0.7857747077941895
 円: 0.7682645320892334
 
[query] 車
 ドア: 0.6294019222259521
 ドアー: 0.6016885638237
 自動車: 0.5859153270721436
 門: 0.5726617574691772
 カーテン: 0.5608214139938354

トヨタ is not found
```

「あなた」はいい感じですね。「年」は単位としての類義語が得られている感じです。「車」はコーパスに使っている作品にほとんど出てこないからかイマイチです。「トヨタ」はそもそも存在しない時代なので仕方がありません。

他にもいくつか試してみたものを載せておきます。前半は比較的上手くいっているもので、後半は良くわらかないものです。

```
[query] 朝
 晩: 0.7267987132072449
 ごろ: 0.660172164440155
 昼: 0.6085118055343628
 夕方: 0.6021789908409119
 翌: 0.6002975106239319
 
[query] 学校
 東京: 0.6504884958267212
 高等: 0.6290650367736816
 中学校: 0.5801640748977661
 大学: 0.5742003917694092
 下宿: 0.5358142852783203
 
[query] 座敷
 書斎: 0.6603355407714844
 椽側: 0.6362787485122681
 室: 0.6142982244491577
 部屋: 0.6024710536003113
 台所: 0.6014574766159058
 
[query] 着物
 髯: 0.5216895937919617
 黒: 0.5200990438461304
 服: 0.5096032619476318
 洋服: 0.48781922459602356
 帽子: 0.4869200587272644
 
[query] 吾輩
 主人: 0.6372452974319458
 余: 0.5826579332351685
 金田: 0.4684762954711914
 彼等: 0.4676626920700073
 迷亭: 0.4615904688835144
 
[query] 犯人
 怪人: 0.6609077453613281
 賊: 0.6374931931495667
 団員: 0.6308270692825317
 あいつ: 0.6046633720397949
 潜航: 0.5931873917579651
 
[query] 注文
 話: 0.6200630068778992
 相談: 0.5290789604187012
 多忙: 0.5178924202919006
 親切: 0.5033778548240662
 講釈: 0.4894390106201172
 
[query] 無鉄砲
 陳腐: 0.7266454696655273
 古風: 0.6771457195281982
 鋸: 0.6735808849334717
 鼻息: 0.6516652703285217
 無知: 0.650424063205719
 
[query] 猫
 南無: 0.6659030318260193
 信女: 0.5759447813034058
 墨: 0.5374482870101929
 身分: 0.5352671146392822
 普通: 0.5205280780792236
 
[query] 酒
 書物: 0.5834404230117798
 茶: 0.469807893037796
 休ん: 0.4605821967124939
 食う: 0.44864168763160706
 棒: 0.4349029064178467
 
[query] 料理
 かせぎ: 0.5380040407180786
 落款: 0.5214874744415283
 原: 0.5175281763076782
 法: 0.5082278847694397
 屋: 0.5001937747001648
```

ちなみに対象データの著者は、夏目漱石、宮沢賢治、江戸川乱歩です。コーパスがちょっと偏りすぎですが、それはそれで面白いので、よろしければいろいろ試してみてください。



## 2.5 まとめ

おさらいが多かったので比較的スムーズに読めました。次章からが本番になりそうです。

この章は以上です。誤りなどありましたら、ご指摘いただけますとうれしいです。

（このメモの他の章へ：[1章](https://qiita.com/segavvy/items/91be1d4fc66f7e322f25) / 2章 / [3章](https://qiita.com/segavvy/items/1510d8c264edd291e0ef) / [4章](https://qiita.com/segavvy/items/a286143dbcc5a93100b6) / [5章](https://qiita.com/segavvy/items/3e6a9f6bff103824598f) / [6章](https://qiita.com/segavvy/items/fba1595cf64a2462b415) / [7章](https://qiita.com/segavvy/items/883671d07cd7ab26fbe0) / [8章](https://qiita.com/segavvy/items/69ca97ff2d4e8a7144fd) / [まとめ](https://qiita.com/segavvy/items/0f2980ad746d797dd8c1)）