おかねはだいじ

家計簿ときどき日常

すうがくめも3(numpyとネガティブサンプリング)

今回は数学というよりプログラミングな要素を含んだメモ。

はじめにnumpyでzeroベクトル(もしくは行列)について。

zeroベクトルはfor文の中で初期化した方が、ループごとに*=0するより速い可能性あり。

つまり、

for _ in range(10000):
    z_mat = np.zeros((1, 100000))

の方が↓より速い!!!

z_mat = np.zeros((1, 100000))
for _ in range(10000):
    z_mat *= 0

次にネガティブサンプリングの実装について。
まずはこれから登場する各文字たち。
V: 語彙数
H: 中間層の次元数
K: サンプル数 input_weight: (V, H) の行列
output_weight: (H, V) の行列

  1. 語彙idからinput_weightに直接アクセスして単語分散表現を抽出(注意: 内積をとるのではなく直接アクセスすること)
  2. 単語分散表現(1, H)とoutput_weightの内積を取る→ output_vec(1, V)
  3. output_vecから正解の単語idとサンプリングした単語idに対応した値をもったベクトルを用意 → sampled_vec(1, K)
    これは、長さがサンプル数+1のzeroベクトル
tmp_zero = np.zeros((1, k+1))

sampled_num = np.random.randint(0, V, K)

を用意して

sampled_vec = output_vec[(tmp_zero, sample_num)]

すればおけ
これは下記と異なり、sampled_vec = のように別の変数に代入してもよい。
4. 同様にoutput_weight(H, V)からも 正解の単語idとサンプリングした単語idに対応した場所にアクセスする
→ output_weightの[ [正解id, sample_id1, sample_id2, … ,sample_idK ] ]: 次元数は(H, K)次元

(ここで超要注意!!!

tmp = output_weight[[正解id, sample_1, sample2.,,,]] 

とすると、スライスされたことになりディープコピーになるので tmp に加算してもoutput_weightは更新されない!!!
5. 中間層の誤差は 出力層の誤差ベクトル(1, k)とoutput_weight [ [正解id, sampled_1, …] ]の転置行列(k, H) の内積をとる


h_err = np.dot(誤差ベクトル, sampled_vec)

こんな感じ。
でもこれ(ネガティブサンプリング)、実は構想の段階。

というのも、今日学んだのがたとえzeroベクトル(もしくは行列)の掛け合わせ、つまり非常にスパースなものでもその次元数が大きくなれば演算に非常に時間がかかるということ。
最初は下記のようなスパースなsampled_vecを取得しようとした。

k = 5

myvec = np.arange(100000).reshape(1,100000)

for _ in range(300000):
    z_mat = np.zeros((1, 100000)) 
    z_vec = np.zeros(k, dtype=np.int64) 
    sample_vec = np.random.randint(0, 100000, k)
    z_mat[(z_vec, sample_vec)] = 1
    sampled_vec = myvec * z_mat # ここが凄まじく時間かかってた
    z_mat = np.zeros((1, 100000))
    #a = softmax(myvec) # これは比較用

sampled_vecはoutput_vecから正解+サンプル数のところだけ1にして残り0になっているもの。
このコードでは、softmaxの8倍程度しか速度が出なかった。
(実はここでzeroベクトルの *=0 による初期化が遅いことも発覚した)  

まだまだ勉強しなければならないことはたくさんあります。