#1.はじめに
名著、**「ゼロから作るDeep Learning2」**を読んでいます。今回は3章のメモ。
コードの実行はGithubからコード全体をダウンロードし、ch03の中で jupyter notebook にて行っています。
#2.CBOWモデル
シンプルな word2vec の CBOWモデル を動かしてみます。 ch03/train.py を実行します。
import sys
sys.path.append('..') # 親ディレクトリのファイルをインポートするための設定
from common.trainer import Trainer
from common.optimizer import Adam
from simple_cbow import SimpleCBOW
from common.util import preprocess, create_contexts_target, convert_one_hot
window_size = 1
hidden_size = 5
batch_size = 3
max_epoch = 1000
# コーパス、辞書の取得
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)
# コンテキスト、ターゲットの取得
contexts, target = create_contexts_target(corpus, window_size)
# ワンホット表現化
vocab_size = len(word_to_id)
contexts = convert_one_hot(contexts, vocab_size)
target = convert_one_hot(target, vocab_size)
# ネットワーク構築
model = SimpleCBOW(vocab_size, hidden_size)
# 学習とロス推移表示
optimizer = Adam()
trainer = Trainer(model, optimizer)
trainer.fit(contexts, target, max_epoch, batch_size)
trainer.plot()
# 単語のベクトル表示
word_vecs = model.word_vecs
for word_id, word in id_to_word.items():
print(word, word_vecs[word_id])
たった7語のword2vecですが、ロスが順調に下がって各単語の5次元ベクトルが得られると嬉しいですね。それでは、コードを順番に見て行きます。
# コーパス、辞書の取得
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)
preprocess()
は、common/util.py にあるので、そこを参照します。
# -------------- from common/util.py ---------------
def preprocess(text):
text = text.lower() # 大文字を小文字に
text = text.replace('.', ' .') # ピリオドの前に空白を
words = text.split(' ') # 空白で分離して単語をリスト化
word_to_id = {}
id_to_word = {}
for word in words: # リストから1つづつ単語を word へ
if word not in word_to_id: # 単語が word_to_id になかったら
new_id = len(word_to_id) # word_to_id の登録数を id に設定
word_to_id[word] = new_id # word_to_id の登録
id_to_word[new_id] = word # id_to_word の登録
corpus = np.array([word_to_id[w] for w in words]) # corpus を id に変換
return corpus, word_to_id, id_to_word
text を単語に分解して corpus を得ます。辞書(単語→数字, 数字→単語)を作成し、その辞書を使って corpus を id に置き換えます。
corpus = [0 1 2 3 4 1 5 6]
word_to_id = {'you': 0, 'say': 1, 'goodbye': 2, 'and':3 , 'i': 4, 'hello': 5, '.': 6}
id_to_word = {0 :'you', 1 :'say', 2 :'goodbye', 3 :'and', 4 :'i', 5 :'hello', 6 : '.'}
# コンテキストとターゲットの取得
contexts, target = create_contexts_target(corpus, window_size)
create_contexts_target()
は、common/util.py にあるので、そこを参照します。
# -------------- from common/util.py ---------------
def create_contexts_target(corpus, window_size=1):
# target は corpus の前後にwindow_sizeを引いたもの
target = corpus[window_size:-window_size]
contexts = []
# target の前後t分を contexts とする
for idx in range(window_size, len(corpus)-window_size): # idx = 1 〜 6
cs = []
for t in range(-window_size, window_size + 1): # t = -1, 0, 1
if t == 0:
continue # t = 0 のときは何もしない
cs.append(corpus[idx + t]) # cs = courpus[idx-1, idx+1]
contexts.append(cs)
return np.array(contexts), np.array(target)
target は corpus の前後から window_size を引いたものです。そして、idx に target が corpus のどの位置かを入れ、t でその前後を指定することで、contexts を得ています。
contexts = [[[0 2][1 3][2 4][3 1][4 5][1 6]]]
target = [1 2 3 4 1 5]
# ワンホット表現化
vocab_size = len(word_to_id)
contexts = convert_one_hot(contexts, vocab_size)
target = convert_one_hot(target, vocab_size)
convert_one_hot()
は、common/util.py にあるので、そこを参照します。
# -------------- from common/util.py ---------------
def convert_one_hot(corpus, vocab_size):
N = corpus.shape[0]
if corpus.ndim == 1: # 1次元の場合 (target の場合)
one_hot = np.zeros((N, vocab_size), dtype=np.int32) # ゼロ行列作成
for idx, word_id in enumerate(corpus): # targetからword_idへ順次代入
one_hot[idx, word_id] = 1
elif corpus.ndim == 2: # 2次元の場合 (contexts の場合)
C = corpus.shape[1]
one_hot = np.zeros((N, C, vocab_size), dtype=np.int32) # ゼロ行列作成
for idx_0, word_ids in enumerate(corpus): # contextsからword_idsへ順次代入
for idx_1, word_id in enumerate(word_ids): # word_idsからword_idへ順次代入
one_hot[idx_0, idx_1, word_id] = 1
return one_hot
target の場合は、(N, vocab_size)でゼロ行列を作り、one_hot[idx, word_id]で指定箇所を1にしています。
contextsの場合は、2次元なので、(N, C, vocab_size)でゼロ行列を作り、one_hot[idx_0, idx_1, word_id]で指定箇所を1にしています。
# ネットワーク構築
model = SimpleCBOW(vocab_size, hidden_size)
ネットワーク構築の部分です。クラス SimpleCBOW()
がある、simple_cbow.py を順次見て行きます。
# -------------- from simple_cbow.py ---------------
class SimpleCBOW:
def __init__(self, vocab_size, hidden_size):
V, H = vocab_size, hidden_size
# 重みの初期化
W_in = 0.01 * np.random.randn(V, H).astype('f')
W_out = 0.01 * np.random.randn(H, V).astype('f')
# レイヤの生成
self.in_layer0 = MatMul(W_in)
self.in_layer1 = MatMul(W_in)
self.out_layer = MatMul(W_out)
self.loss_layer = SoftmaxWithLoss()
# すべての重みと勾配をリストにまとめる
layers = [self.in_layer0, self.in_layer1, self.out_layer]
self.params, self.grads = [], []
for layer in layers:
self.params += layer.params
self.grads += layer.grads
# メンバ変数に単語の分散表現を設定
self.word_vecs = W_in
window_size = 1なので、入力は2箇所。入力は語彙数と同じ7個のワンホットベクトル、隠れ層は5個、出力は語彙数と同じ7個。
分布仮説「単語の意味は、周囲の単語によって形成される」を基に、2つの単語に挟まれた単語は何かという穴埋め問題を解けるように学習すると、$W_{in}$が単語の分散表現になっているわけです。
最後に、word_vecs に 重みW_inを代入しています。これは学習後に、単語のベクトル表示用として使います。
# -------------- from simple_cbow.py ---------------
def forward(self, contexts, target):
h0 = self.in_layer0.forward(contexts[:, 0])
h1 = self.in_layer1.forward(contexts[:, 1])
h = (h0 + h1) * 0.5
score = self.out_layer.forward(h)
loss = self.loss_layer.forward(score, target)
return loss
layer0とlayer1の重み$W_{in}$は共用です。layer0とlayer1の信号を加算した後2で割っています。
# -------------- from simple_cbow.py ---------------
def backward(self, dout=1):
ds = self.loss_layer.backward(dout)
da = self.out_layer.backward(ds)
da *= 0.5
self.in_layer1.backward(da)
self.in_layer0.backward(da)
return None
# 学習とロス推移グラフ表示
optimizer = Adam()
trainer = Trainer(model, optimizer)
trainer.fit(contexts, target, max_epoch, batch_size)
trainer.plot()
common/trainer.py の class Trainer()
を、先程ネットワーク構築したモデル、オプチマイザーはAdamで、インスタンス化します。後は、fitで学習、plotでロス推移グラフ表示します。
# 単語のベクトル表示
word_vecs = model.word_vecs # 重みW_in(単語ベクトル)の取得
for word_id, word in id_to_word.items(): # id_to_word からインデックスと単語を取得
print(word, word_vecs[word_id]) # 単語とベクトルを表示
最後に、学習した単語ベクトルmodel.word_vecs
を呼び出し、単語とベクトルを表示します。