3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

深層学習/ゼロから作るDeep Learning2 第4章メモ

Last updated at Posted at 2020-05-14

1.はじめに

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

2.高速版CBOWモデル

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

 なお、データセットは Penn Tree Bankを使っていて、語彙数は10,000個, train のコーパスサイズは約90万語です。

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

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

# --------------- 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

 高速化のポイントの1つは、Embedding レイヤの採用です。common/layers.pyを見てみましょう。

3.Embedding レイヤ

# --------------- 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

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

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

4.Negative Sampling

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

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

# ------------- 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
 多値分類を二値分類に近似するには、まず、you(0)とgoodbye(2)に挟まれた単語の答えについて、 say(1)が正解となる確率を出来るだけ大きくします(正例)。しかし、これだけでは不十分です。

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

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

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

# ------------- 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
 ミニバッチに対応するために、idx, h が複数の場合にも計算出来るように、最後に target_w*h の合計を取るようにしています。

5.モデル評価

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

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

 まずは、most_similarメソッド(common/util.py)を使った、単語の類似性の確認です。you に最も近いものは we, そして i, they, your と人称代名詞が続きます。これは、各単語の類似性を下記のコサイン類似度で計算した結果です。
スクリーンショット 2020-05-14 15.25.40.png

# analogy task
analogy('king', 'man', 'queen',  word_to_id, id_to_word, word_vecs)

スクリーンショット 2020-05-14 15.40.21.png

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

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

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?