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)
ロスは順調に下がったようです。それでは、ポイントとなる。ネットワーク構築の部分のcbow.py
の class 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
第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
多値分類を二値分類に近似するには、まず、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
ミニバッチに対応するために、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)
まずは、most_similar
メソッド(common/util.py)を使った、単語の類似性の確認です。you に最も近いものは we, そして i, they, your と人称代名詞が続きます。これは、各単語の類似性を下記のコサイン類似度で計算した結果です。
# analogy task
analogy('king', 'man', 'queen', word_to_id, id_to_word, word_vecs)
今度は、alalogy
メソッド(common/util.py)を使った、有名な king - man + woman = queen 問題の確認です。確かに、その通りになっていますね。
これは、「man → woman」ベクトルに対して**「king → x」ベクトルが出来るだけ近くなるような単語 x** を探すというタスクを解くことになります。