ゼロから作る DeepLearning 2 格闘日誌5/24 4章
ゼロから作るDeep Learning 2を友達と読んでいます.
自分用のまとめかつ,補充知識として重要なことを書いています.
読者対象はゼロから作るDeep Learning 実装が終わったくらいの方,Deep Learning 2を読んでいる方です.
4章 word2vecの高速化
CBOWモデルでは,コーパスの増加につれ計算時間コストが増大する
→word2vecの高速化に挑む
- Embeddingレイヤを導入
- 損失関数,Negative samplingを導入
において,CBOWモデルの構築を行った.
ボトルネックとなるのは,以下の処理.
- 入力層のone-hot表現と重み行列
の積による計算(4.1)W_{in} - 中間層と重み行列
の積及びSoftmaxレイヤの計算(4.2)W_{out}
4.1 word2vecの改良1(Embeddingレイヤの導入)
4.4.1 Embedding レイヤ
One-hotベクトルと重み行列
One-hotベクトル
重み行列
本来,one-hotと行列の積は実質的に「行を選ぶだけ」の操作なので計算量は:
実際の実装では,one-hotベクトルを生成せず,インデックスで重み行列から該当行(ベクトル)を抜き出す.
→Embeddingレイヤ:重みパラメータから単語IDに該当する列を抜き出すためのレイヤ
4.1.2 Embeddingレイヤの実装
import numpy as np
W = np.arange(21).reshape(7,3)
print(W)
print(W[2])
print(W[5])
[[ 0 1 2]
[ 3 4 5]
[ 6 7 8]
[ 9 10 11]
[12 13 14]
[15 16 17]
[18 19 20]]
[6 7 8]
[15 16 17]
idx = [0,2,4,6]
print(W[idx])
[[ 0 1 2]
[ 6 7 8]
[12 13 14]
[18 19 20]]
Embeddingレイヤを実装した.
class Embedding:
def __init__(self, W):
self.params = [W]
self.grads = [np.zeros_like(W)]
self.idx = None #index
def forward(self, idx):
W, = self.params
self.idx = idx
out = W[idx]
return out
def backward(self, dout):
dW, = self.grads
dW[...] = 0
dW[self.idx] = dout #悪い例
return None
さて,悪い例の問題点を考える.
同じ行に加えたいとき(同じidxの指定が起きた時),2回書き換えが起こってしまうので,加算を行うことで解決
def backward(self, dout):
dW, = self.grads
dW[...] = 0
np.add.at(dW, self.idx, dout)
#悪い例 dW[self.idx] = dout
return None
以上よりEmbeddingレイヤの実装が完了した.
4.2 word2vecの改良2(Negative Samplingなど)
中間層と
中間層ベクトルのサイズ:
この計算のオーダーは:
そして Softmax の計算も語彙数
Softmax:
Softmaxの代わりにNegative Samplingを行うことで,語彙数が多くなったとしても計算量を少なく一定に抑える.
4.2.1中間層以降の計算の問題点
- 中間層のニューロンと重み行列
の積W_{out} - Softmaxレイヤの計算→negative samplingで対象とする単語を絞る
4.2.2 多値分類から二値分類へ
Softmaxは全ての単語のスコアを基に,すべての単語の予測確率を算出→誤差を伝播していたのに対して,Negative Samplingは以下の戦略をとる.
- 二値問題に絞る(特定の単語のYes/Noに絞る)
- 間違いであると予測してほしい単語をいくつか選び,予測確率を算出→誤差を伝播することで不例についても学習させる
以下の低燃費化ストーリーが浮かんだ:
カウントベース(一気にすべての単語を学習)→推論ベース(≒Word2vec,バッチ単位で学習)→Negative Sampling(すべての単語について学習せず,複数の単語について学習)
4.2.3 シグモイド関数と交差エントロピー誤差
二値問題ならば簡単に解ける.
スコアをシグモイド関数で求め,交差エントロピー誤差によって損失を求めればいい.
シグモイド関数は
交差エントロピー誤差(対象が2つであるだけで多値分類の場合と同じ)は
Sigmoid With Loss layerとして実装する.
4.2.2 多値分類から二値分類へ(実装編)
二値分類を行うword2vec(CBOWモデル)を実装する.
まずEmbeddingDotクラスを作成する
import numpy as np
from common.layers import Embedding
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):
#ベクトルhとWのidx番目の行の内積計算
target_W = self.embed.forward(idx)
out = np.sum(target_W * h, axis=1)
self.cache = (h, target_W) #backwardのために保存
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 #先のレイヤに誤差を伝播
4.2.5 Negative Sampling
誤った答え(不例)についても学習させる(予測確率を低下させる)ために,いくつかの不例についても同様の計算を行う.
4.2.6 Negative Samplingのサンプリング手法
確率的に多く登場する単語に対する不例はそれだけ多くカバーしたい
→ランダムサンプリングではなく,コーパス統計データに基づき登場する確率の高い単語を多くサンプリングする
出現確率の低い単語についてもサンプリングされるために確率分布に0.75乗する.(数字に理論的意味はない)
(本書の方針に従い,UnigramSamplerクラスを使用し,実装は行わない)
UnigramSampler(corpus, power, sample_size)
→sample_size個分の不例を,corpusに基づいて出力してくれる
4.2.7 Negative Samplingの実装
UnigramSamplerをもちいて不例をサンプリングできる.これを利用する.
正例と不例(with UnigramSampler)を用意し,順伝播(損失関数の計算)を以下の通り実装した.
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)
#正例のforward
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)
#不例のforward
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
forwardメソッドは中間層のニューロンh, 正例のターゲットtargetを受け取り,self.samplerで不例をサンプリングし,順伝播を行う.
repeatノードの場合、逆伝播は足し合わせる.(ノードの形を見れば一目瞭然)第一章の復習が必要だと感じた.これが分かればどんなノードでも実装できそう.
以上より,Negative Samplingの実装が完了した.
4.3 改良版word2vecの学習
PTBデータセットを用いて,学習を行ってみよう.
4.3.1 CBOWモデルの実装
ここまで作成してきたEmbeddingレイヤ(分散表現を取り出すのが高速)とNegative Sampling Lossレイヤ(すべての単語についての勾配を伝播するのをやめたので高速)を用いて,CBOWモデルのword2vecを実装する.
基本的には,前章で作成したCBOWモデルを拡張する形で実装する.
class CBOW:
def __init__(self, vocab_size, hidden_size, window_size, corpus):
V,H = vocab_size, hidden_size
#init weight
W_in = 0.01 * np.random.randn(V,H).astype('f')
W_out = 0.01 * np.random.randn(V,H).astype('f') #注意!CBOWと違う形
#init layer
self.in_layers = []
for i in range(2*window_size):
layer = Embedding(W_in)
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
cbowのイニシャライザが受け取る引数は,語彙数vocab_size,中間層のニューロン数hidden_size,単語のIDリストcorpus,コンテキストサイズwindow_sizeの4つである.
Embeddingレイヤは2* Window_size分(前側と後ろ側で2倍)である.
その後,Negative Sampling Lossレイヤを作成.
4.3.2 CBOWモデルの学習コード
CBOWモデルの学習コードを実装した.
import sys
sys.path.append('..')
import numpy as np
from common import config
import pickle
from common.trainer import Trainer
from common.optimizer import Adam
from cbow import CBOW
from common.utils import create_context_target, convert_one_hot, to_cpu, to_gpu
from dataset import ptb
#hyper param
window_size = 5
hidden_size = 100
batch_size = 100
max_epoch = 10
#load data
corpus, word_to_id, id_to_word = ptb.load_data('train')
vocab_size = len(word_to_id)
contexts, target = create_context_target(corpus, window_size)
if config.GPU:
contexts, target = to_gpu(contexts), to_gpu(target)
#generate model
model = CBOW(vocab_size,hidden_size,window_size, corpus)
optimizer =Adam()
trainer = Trainer(model, optimizer)
#learning
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'
with open(pkl_file, 'wb') as f:
pickle.dump(params, f, -1)

4.3.3 CBOWモデルの評価
以下のように,most_similarを用いてどのような単語と距離が近いか確認してみる.
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']
vocab_size = len(params['id_to_word'])
word_to_id = params['word_to_id']
querys = ['you', 'year', 'car', 'toyota']
for query in querys:
most_similar(query, word_to_id, id_to_word, word_vecs, top=5)
また,
analogy('king','man','queen',word_to_id,id_to_word,word_vecs)
analogy('take','took','go',word_to_id,id_to_word,word_vecs)
analogy('car','cars','child',word_to_id,id_to_word,word_vecs)
analogy('good','better','bad',word_to_id,id_to_word,word_vecs)
[query] you
we: 0.7568359375
i: 0.7109375
anything: 0.6015625
your: 0.5849609375
they: 0.5830078125
[query] year
month: 0.85205078125
week: 0.77587890625
summer: 0.76416015625
spring: 0.73388671875
decade: 0.68505859375
[query] car
truck: 0.6328125
luxury: 0.62060546875
window: 0.61767578125
auto: 0.60009765625
cars: 0.5830078125
[query] toyota
honda: 0.671875
seita: 0.654296875
engines: 0.64404296875
chevrolet: 0.64111328125
nissan: 0.60791015625
[analogy] king:man = queen:?
woman: 5.08203125
kid: 4.55859375
wife: 4.53515625
a.m: 4.453125
naczelnik: 4.4296875
[analogy] take:took = go:?
eurodollars: 4.3828125
went: 4.25390625
were: 4.24609375
began: 4.10546875
came: 3.9140625
[analogy] car:cars = child:?
a.m: 6.93359375
rape: 5.41796875
children: 5.23046875
incest: 4.94921875
adults: 4.859375
[analogy] good:better = bad:?
more: 5.71875
rather: 5.32421875
less: 5.0
greater: 3.916015625
faster: 3.81640625
car:cars = child:?
に対して
a.m: 6.93359375
rape: 5.41796875
children: 5.23046875
incest: 4.94921875
adults: 4.859375
が並んでいたり、突っ込みどころはあるがかなり良い分散表現が得られているといえるだろう.
4.4 word2vecに関する残りのテーマ
4.4.1 word2vecを使ったアプリケーションの例
転移学習:何らかの事前学習を行ったモデルを他のタスクに転用し,(ファインチューニングすること)
単語の分散表現の利点:総和を求めるなどして文章のベクトルを得ることなどができる
4.4.2 単語ベクトルの評価方法
文法/意味的な類推問題を出題し正答率による評価を行う.
アプリケーションの性能の良さとこの類推問題の成績は必ずしも相関するわけではないことに注意.
4.5 まとめ
- Embedding レイヤ 単語の分散表現の抽出を効率化
- Negative Samplingすべての単語の勾配を計算することをやめ,一部の単語を対象に計算,最適化
- 転移学習に有用
Discussion