深層学習／ゼロから作るDeep Learning２　第４章メモ

#1.はじめに
　名著、**「ゼロから作るDeep Learning２」**を読んでいます。今回は４章のメモ。
　コードの実行はGithubからコード全体をダウンロードし、ch04の中で jupyter notebook にて行っています。

#2.高速版CBOWモデル
　第４章のテーマは、第３章で実装した Word2vec のCBOWモデルを高速化し実用的なモデルにすることです。ch04/train.py を実行し、順番に内容を見て行きます。

　なお、データセットは [Penn Tree Bank](https://corochann.com/penn-tree-bank-ptb-dataset-introduction-1456.html)を使っていて、語彙数は10,000個,  train のコーパスサイズは約90万語です。

```python
import sys
sys.path.append('..')
from common import config
# GPUで実行する場合は、下記のコメントアウトを消去（要cupy）
# ===============================================
# config.GPU = True
# ===============================================
from common.np import *
import pickle
from common.trainer import Trainer
from common.optimizer import Adam
from cbow import CBOW
from skip_gram import SkipGram
from common.util import create_contexts_target, to_cpu, to_gpu
from dataset import ptb

# ハイパーパラメータの設定
window_size = 5
hidden_size = 100
batch_size = 100
max_epoch = 10

# データの読み込み
corpus, word_to_id, id_to_word = ptb.load_data('train')
vocab_size = len(word_to_id)

# コンテクストとターゲットの取得
contexts, target = create_contexts_target(corpus, window_size)
if config.GPU:
    contexts, target = to_gpu(contexts), to_gpu(target)

# ネットワーク構築
model = CBOW(vocab_size, hidden_size, window_size, corpus)

# 学習、ロス推移グラフ表示
optimizer = Adam()
trainer = Trainer(model, optimizer)
trainer.fit(contexts, target, max_epoch, batch_size)
trainer.plot()

# 後ほど利用できるように、必要なデータを保存
word_vecs = model.word_vecs
if config.GPU:
    word_vecs = to_cpu(word_vecs)
params = {}
params['word_vecs'] = word_vecs.astype(np.float16)
params['word_to_id'] = word_to_id
params['id_to_word'] = id_to_word
pkl_file = 'cbow_params.pkl'  # or 'skipgram_params.pkl'
with open(pkl_file, 'wb') as f:
    pickle.dump(params, f, -1)
```
![スクリーンショット 2020-05-13 16.27.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/bfe139a8-fd40-0485-4cf7-aae25aa74f72.png)

　ロスは順調に下がったようです。それでは、ポイントとなる。ネットワーク構築の部分の`cbow.py`の `class CBOW` を見てみましょう。


```python
# --------------- from cbow.py ---------------
class CBOW:
    def __init__(self, vocab_size, hidden_size, window_size, corpus):
        V, H = vocab_size, hidden_size

        # 重みの初期化
        W_in = 0.01 * np.random.randn(V, H).astype('f')
        W_out = 0.01 * np.random.randn(V, H).astype('f')

        # レイヤの生成
        self.in_layers = []
        for i in range(2 * window_size):
            layer = Embedding(W_in)  # Embeddingレイヤを使用
            self.in_layers.append(layer)
        self.ns_loss = NegativeSamplingLoss(W_out, corpus, power=0.75, sample_size=5)

        # すべての重みと勾配をリストにまとめる
        layers = self.in_layers + [self.ns_loss]
        self.params, self.grads = [], []
        for layer in layers:
            self.params += layer.params
            self.grads += layer.grads

        # メンバ変数に単語の分散表現を設定
        self.word_vecs = W_in
```
　高速化のポイントの１つは、**Embedding レイヤ**の採用です。common/layers.pyを見てみましょう。

#3.Embedding レイヤ

```python
# --------------- from common/layers.py --------------
class Embedding:
    def __init__(self, W):
        self.params = [W]
        self.grads = [np.zeros_like(W)]
        self.idx = None

    def forward(self, idx):
        W, = self.params
        self.idx = idx
        out = W[idx]  # idx で指定された行を出力
        return out

    def backward(self, dout):
        dW, = self.grads
        dW[...] = 0
        if GPU:
            np.scatter_add(dW, self.idx, dout)
        else:
            np.add.at(dW, self.idx, dout)  # idx で指定された行へデータを加算
        return None
```
![スクリーンショット 2020-05-14 15.56.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/76cfed43-9b2b-bc02-b059-72b21c385747.png)


　第３章では **MatMul レイヤ**を使って、ベクトルと重み行列の内積を求めていたわけですが、考えてみればワンホットベクトルと重み行列の内積なので、**重み行列$W_{in}$の行指定**をするだけで良いわけです。これが、**Embed レイヤ**です。

　そうすると、逆伝播も前から伝えられるデータで該当する行を更新するだけで済みます。但し、ミニバッチ学習では、たまたま同じ行に複数のデータが戻って来て重なる場合も考えられるので、置き換えではなく、**データを加算**する形にしています。

#4.Negative Sampling
　高速化の２つ目のポイントは、**Negative Sampling** です。第３章の様に、語彙数分の出力からSoftmaxで分類するというのは、非現実的です。ではどうするか。**多値分類問題を二値分類問題に近似して解く**というのが、その答えです。

　`negative_sampling_layer.py` にある `class NegativeSamplingLoss` を見てみましょう。

```python
# ------------- form negative_sampling_layer.py --------------
class NegativeSamplingLoss:
    def __init__(self, W, corpus, power=0.75, sample_size=5):
        self.sample_size = sample_size
        self.sampler = UnigramSampler(corpus, power, sample_size)
        self.loss_layers = [SigmoidWithLoss() for _ in range(sample_size + 1)]
        self.embed_dot_layers = [EmbeddingDot(W) for _ in range(sample_size + 1)]

        self.params, self.grads = [], []
        for layer in self.embed_dot_layers:
            self.params += layer.params
            self.grads += layer.grads

    def forward(self, h, target):
        batch_size = target.shape[0]
        negative_sample = self.sampler.get_negative_sample(target)

        # 正例のフォワード
        score = self.embed_dot_layers[0].forward(h, target)
        correct_label = np.ones(batch_size, dtype=np.int32)
        loss = self.loss_layers[0].forward(score, correct_label)

        # 負例のフォワード
        negative_label = np.zeros(batch_size, dtype=np.int32)
        for i in range(self.sample_size):
            negative_target = negative_sample[:, i]
            score = self.embed_dot_layers[1 + i].forward(h, negative_target)
            loss += self.loss_layers[1 + i].forward(score, negative_label)

        return loss

    def backward(self, dout=1):
        dh = 0
        for l0, l1 in zip(self.loss_layers, self.embed_dot_layers):
            dscore = l0.backward(dout)
            dh += l1.backward(dscore)

        return dh
```
![スクリーンショット 2020-05-13 17.36.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/8207538f-2539-660b-4a8e-62f661aea63c.png)
　多値分類を二値分類に近似するには、まず、you(0)とgoodbye(2)に挟まれた単語の答えについて、 say(1)が正解となる確率を出来るだけ大きくします(正例)。しかし、これだけでは不十分です。

　そこで、適当に選んだ hello(5)やI(4)が不正解となる確率を出来るだけ大きくすることを付け加えます(負例)。

　この手法を、**Negative Sampling** と言います。適当に選ぶ負例の数は、コードでは、sample_size = 5 になっています。

　ここで、`Embedding_dot_layers` が出て来ますので、これも見ておきます。同じく、　`negative_sampling_layer.py` にあります。

```python
# ------------- form negative_sampling_layer.py --------------
class EmbeddingDot:
    def __init__(self, W):
        self.embed = Embedding(W)
        self.params = self.embed.params
        self.grads = self.embed.grads
        self.cache = None

    def forward(self, h, idx):
        target_W = self.embed.forward(idx)
        out = np.sum(target_W * h, axis=1)

        self.cache = (h, target_W)
        return out

    def backward(self, dout):
        h, target_W = self.cache
        dout = dout.reshape(dout.shape[0], 1)

        dtarget_W = dout * h
        self.embed.backward(dtarget_W)
        dh = dout * target_W
        return dh
```
![スクリーンショット 2020-05-14 10.56.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/fa8f3df4-4f86-89e7-9ca4-4c7f92825df7.png)
　ミニバッチに対応するために、idx, h が複数の場合にも計算出来るように、最後に target_w*h の合計を取るようにしています。

#5.モデル評価
　最初に、ch04/train.py を動かしましたので、学習したパラメータが `cbow_params.pkl` に保存されています。これを使って、単語の分散表現が上手くできているか `eval.py` で確認します。

```python
import sys
sys.path.append('..')
from common.util import most_similar, analogy
import pickle

pkl_file = 'cbow_params.pkl'  # ファイル名指定

# 各パラメータの読み込み
with open(pkl_file, 'rb') as f:  
    params = pickle.load(f)  
    word_vecs = params['word_vecs']
    word_to_id = params['word_to_id']
    id_to_word = params['id_to_word']

# most similar task
querys = ['you']
for query in querys:
    most_similar(query, word_to_id, id_to_word, word_vecs, top=5)
```
![スクリーンショット 2020-05-14 15.41.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/5e3001e0-8ab3-61d2-250f-25d8e026ec82.png)


　まずは、`most_similar`メソッド(common/util.py)を使った、単語の類似性の確認です。you に最も近いものは we, そして i, they, your と人称代名詞が続きます。これは、各単語の類似性を下記のコサイン類似度で計算した結果です。
![スクリーンショット 2020-05-14 15.25.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/81e0159b-6722-e164-efdc-a4310dcce682.png)

```python
# analogy task
analogy('king', 'man', 'queen',  word_to_id, id_to_word, word_vecs)
```
![スクリーンショット 2020-05-14 15.40.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/1a6a4906-f0be-093e-e86a-cd431a46a341.png)

　今度は、`alalogy` メソッド(common/util.py)を使った、有名な **king - man + woman = queen** 問題の確認です。確かに、その通りになっていますね。

　これは、**「man →　woman」ベクトル**に対して**「king → x」ベクトル**が出来るだけ近くなるような**単語 x** を探すというタスクを解くことになります。