からっぽのしょこ

読んだら書く!書いたら読む!読書読読書読書♪同じ事は二度調べ(たく)ない

3.1-2:シンプルなword2vec【ゼロつく2のノート(実装)】

はじめに

 『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 本の内容を1つずつ確認しながらゆっくりと組んでいきます。

 この記事は、3.1節「推論ベースの手法とニューラルネットワーク」と3.2節「シンプルなword2vec」の内容です。CBOWモデルの順伝播を数式とPythonによるプログラムから説明します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

3.1 推論ベースの手法とニューラルネットワーク

3.1.3 ニューラルネットワークにおける単語の処理方法

・数式による確認

 前節と同じく「You say goodbye and I say hello.」の(ピリオドも含めて)7種類の単語のテキストを扱います。

 コンテキストを$\mathbf{c}$、重みを$\mathbf{W}$とし、それぞれ次の形状とします。イメージしやすいように、ここでは添字を対応する単語で表すことにします。ただし「.(ピリオド)」については「priod」とします。

$$ \mathbf{c} = \begin{pmatrix} c_{\mathrm{you}} & c_{\mathrm{say}} & c_{\mathrm{goodbye}} & c_{\mathrm{and}} & c_{\mathrm{I}} & c_{\mathrm{hello}} & c_{\mathrm{period}} \end{pmatrix} ,\ \mathbf{W} = \begin{pmatrix} w_{\mathrm{you},1} & w_{\mathrm{you},2} & w_{\mathrm{you},3} \\ w_{\mathrm{say},1} & w_{\mathrm{say},2} & w_{\mathrm{say},3} \\ w_{\mathrm{goodbye},1} & w_{\mathrm{goodbye},2} & w_{\mathrm{goodbye},3} \\ w_{\mathrm{and},1} & w_{\mathrm{and},2} & w_{\mathrm{and},3} \\ w_{\mathrm{I},1} & w_{\mathrm{I},2} & w_{\mathrm{I},3} \\ w_{\mathrm{hello},1} & w_{\mathrm{hello},2} & w_{\mathrm{hello},3} \\ w_{\mathrm{period},1} & w_{\mathrm{period},2} & w_{\mathrm{period},3} \\ \end{pmatrix} $$

 コンテキストの要素数(列数)と重みの行数が、単語の種類数に対応します。また通常通り1から単語の種類数までの通し番号を振る場合は、その値が単語IDに対応します。
 コンテキスト(単語)はone-hot表現として扱うため、例えば「you」の場合は

$$ \mathbf{c}_{\mathrm{you}} = \begin{pmatrix} 1 & 0 & 0 & 0 & 0 & 0 & 0 \end{pmatrix} $$

とすることで、単語「you」を表現できます。

 重み付き和$\mathbf{h}$は、行列の積で求められます。

$$ \begin{aligned} \mathbf{h} &= \mathbf{c}_{\mathrm{you}} \mathbf{W} \\ &= \begin{pmatrix} h_1 & h_2 & h_3 \end{pmatrix} \end{aligned} $$

 この例では$1 \times 7$のベクトルと$7 \times 3$の行列の積なので、計算結果は$1 \times 3$のベクトルになります。
 $\mathbf{h}$の1つ目の要素$h_1$の計算を詳しく見ると、次のようになります。

$$ \begin{aligned} h_1 &= c_{\mathrm{you}} w_{\mathrm{you},1} + c_{\mathrm{say}} w_{\mathrm{say},1} + c_{\mathrm{goodbye}} w_{\mathrm{goodbye},1} + c_{\mathrm{and}} w_{\mathrm{and},1} + c_{\mathrm{I}} w_{\mathrm{I},1} + c_{\mathrm{hello}} w_{\mathrm{hello},1} + c_{\mathrm{period}} w_{\mathrm{period},1} \\ &= 1 w_{\mathrm{you},1} + 0 w_{\mathrm{say},1} + 0 w_{\mathrm{goodbye},1} + 0 w_{\mathrm{and},1} + 0 w_{\mathrm{I},1} + 0 w_{\mathrm{hello},1} + 0 w_{\mathrm{period},1} \\ &= w_{\mathrm{you},1} \end{aligned} $$

 コンテキストと重みの対応する(同じ単語に関する)要素を掛けて、全ての単語で和をとります。しかしコンテキストは、$c_{\mathrm{you}}$以外の要素が0なので、対応する重みの値の影響は消えていまします。また$c_{\mathrm{you}}$は1なので、対応する重みの値$w_{\mathrm{you},1}$がそのまま中間層の1つ目のニューロンに伝播します。
 残りの2つの要素も同様に計算できるので、重み付き和

$$ \mathbf{h} = \begin{pmatrix} w_{\mathrm{you},1} & w_{\mathrm{you},2} & w_{\mathrm{you},3} \end{pmatrix} $$

は、単語「you」に関する重みの値となります。

・処理の確認

 同じことをNumPyを利用して計算してみましょう。

# NumPyをインポート
import numpy as np
# 適当にコンテキスト(one-hot表現)を指定
c = np.array([[1, 0, 0, 0, 0, 0, 0]])
print(c.shape)

# 重みをランダムに生成
W = np.random.randn(7, 3)
print(W)

# 重み付き和を計算
h = np.dot(c, W)
print(h)
print(h.shape)
(1, 7)
[[-0.23410731  0.57169613 -1.44781677]
 [-0.25681735 -1.53057848  0.28307254]
 [-2.31677149  0.74954506  0.89999068]
 [-0.6369265   1.55604891  0.64176568]
 [-0.06926048 -1.16149746 -1.18108853]
 [ 0.94972752 -0.17358602 -0.6618748 ]
 [-1.15640384 -0.20816114 -0.46683889]]
[[-0.23410731  0.57169613 -1.44781677]]
(1, 3)

 重みWの0行目の値がそのままhの値になっていますね。

 以上がword2vecの基本となる処理です。次節では、word2vecの全体像を考えます。

3.2 シンプルなword2vec

3.2.1 CBOWモデルの推論処理

・数式による確認

 前節と同じ7種類の単語によって構成されたテキストを扱って、CBOWモデルの計算を説明します。

 前項と同様に、コンテキストを

$$ \mathbf{c} = \begin{pmatrix} c_{\mathrm{you}} & c_{\mathrm{say}} & c_{\mathrm{goodbye}} & c_{\mathrm{and}} & c_{\mathrm{I}} & c_{\mathrm{hello}} & c_{\mathrm{period}} \end{pmatrix} $$

とします。単語の種類数個の要素を持つベクトルになります。またウインドウサイズの値に応じて、入力層の数が変わります。この例ではウインドウサイズが1の場合を考えます。
 コンテキストは、one-hot表現で単語を表します。例えば「you」と「goodbye」だと、それぞれ

$$ \begin{aligned} \mathbf{c}_{\mathrm{you}} &= \begin{pmatrix} 1 & 0 & 0 & 0 & 0 & 0 & 0 \end{pmatrix} \\ \mathbf{c}_{\mathrm{goodbey}} &= \begin{pmatrix} 0 & 0 & 1 & 0 & 0 & 0 & 0 \end{pmatrix} \end{aligned} $$

となります。分かりやすいように下付き添字を対応する単語としておきます。この2つのベクトルが、ターゲットを「say」とした場合のコンテキストになります。

 入力層の重みを

$$ \mathbf{W}_{\mathrm{in}} = \begin{pmatrix} w_{\mathrm{you},1} & w_{\mathrm{you},2} & w_{\mathrm{you},3} \\ w_{\mathrm{say},1} & w_{\mathrm{say},2} & w_{\mathrm{say},3} \\ w_{\mathrm{goodbye},1} & w_{\mathrm{goodbye},2} & w_{\mathrm{goodbye},3} \\ w_{\mathrm{and},1} & w_{\mathrm{and},2} & w_{\mathrm{and},3} \\ w_{\mathrm{I},1} & w_{\mathrm{I},2} & w_{\mathrm{I},3} \\ w_{\mathrm{hello},1} & w_{\mathrm{hello},2} & w_{\mathrm{hello},3} \\ w_{\mathrm{period},1} & w_{\mathrm{period},2} & w_{\mathrm{period},3} \\ \end{pmatrix} $$

とします。これも前項の重みと同じものです。ただし出力層の重みと区別するため、添字に$\mathrm{in}$と表記することにします。

 入力層を全結合層とすると、「you」に関する入力層の出力は前項と同じく

$$ \begin{aligned} \mathbf{h}_1 &= \mathbf{c}_{\mathrm{you}} \mathbf{W}_{\mathrm{in}} \\ &= \begin{pmatrix} w_{\mathrm{you},1} & w_{\mathrm{you},2} & w_{\mathrm{you},3} \end{pmatrix} \end{aligned} $$

となります。「goodbye」については

$$ \begin{aligned} \mathbf{h}_2 &= \mathbf{c}_{\mathrm{goodbye}} \mathbf{W}_{\mathrm{in}} \\ &= \begin{pmatrix} w_{\mathrm{goodbye},1} & w_{\mathrm{goodbye},2} & w_{\mathrm{goodbye},3} \end{pmatrix} \end{aligned} $$

となります。

 これら2つの平均

$$ \begin{aligned} \mathbf{h} &= \frac{1}{2} ( \mathbf{h}_1 + \mathbf{h}_2 ) \\ &= \begin{pmatrix} \frac{1}{2} (w_{\mathrm{you},1} + w_{\mathrm{goodbye},1}) & \frac{1}{2} (w_{\mathrm{you},2} + w_{\mathrm{goodbye},2}) & \frac{1}{2} (w_{\mathrm{you},3} + w_{\mathrm{goodbye},3}) \end{pmatrix} \end{aligned} $$

を中間層の入力とします。

 この計算結果から見ると、中間層のニューロンの値$\mathbf{h}$は解釈しづらいものです。そこで各単語に対応した値になるように、つまり要素(行)数が単語の種類数となるように再度変換したものを、CBOWモデルの出力とします。

 出力層の重みを

$$ \mathbf{W}_{\mathrm{out}} = \begin{pmatrix} w_{1,\mathrm{you}} & w_{1,\mathrm{say}} & w_{1,\mathrm{goodbye}} & w_{1,\mathrm{and}} & w_{1,\mathrm{I}} & w_{1,\mathrm{hello}} & w_{1,\mathrm{period}} \\ w_{2,\mathrm{you}} & w_{2,\mathrm{say}} & w_{2,\mathrm{goodbye}} & w_{2,\mathrm{and}} & w_{2,\mathrm{I}} & w_{2,\mathrm{hello}} & w_{2,\mathrm{period}} \\ w_{3,\mathrm{you}} & w_{3,\mathrm{say}} & w_{3,\mathrm{goodbye}} & w_{3,\mathrm{and}} & w_{3,\mathrm{I}} & w_{3,\mathrm{hello}} & w_{3\mathrm{period}} \\ \end{pmatrix} $$

とします。入力層の重みと区別するために、添字を$\mathrm{out}$としておきます。行数が中間層のニューロン数、列数が単語の種類数になります。

 出力層も全結合層とすると、最終的な出力は

$$ \begin{aligned} \mathbf{s} &= \mathbf{h} \mathbf{W}_{\mathrm{out}} \\ &= \begin{pmatrix} s_{\mathrm{you}} & s_{\mathrm{say}} & s_{\mathrm{goodbye}} & s_{\mathrm{and}} & s_{\mathrm{I}} & s_{\mathrm{hello}} & s_{\mathrm{period}} \end{pmatrix} \end{aligned} $$

になります。
 「you」に関する要素(1番目の要素)の計算は

$$ \begin{aligned} s_{\mathrm{you}} &= \frac{1}{2} (w_{\mathrm{you},1} + w_{\mathrm{goodbye},1}) w_{1,\mathrm{you}} + \frac{1}{2} (w_{\mathrm{you},2} + w_{\mathrm{goodbye},2}) w_{2,\mathrm{you}} + \frac{1}{2} (w_{\mathrm{you},3} + w_{\mathrm{goodbye},3}) w_{3,\mathrm{you}} \\ &= \frac{1}{2} \sum_{i=1}^3 (w_{\mathrm{you},i} + w_{\mathrm{goodbye},i}) w_{i,\mathrm{you}} \end{aligned} $$

コンテキストに対応する入力層の重みの平均と「you」に関する出力の重みの積になります。
 他の要素(単語)についても同様に計算できるので、最終的な出力は

$$ \begin{aligned} \mathbf{s} &= \begin{pmatrix} s_{\mathrm{you}} & s_{\mathrm{say}} & s_{\mathrm{goodbye}} & s_{\mathrm{and}} & s_{\mathrm{I}} & s_{\mathrm{hello}} & s_{\mathrm{period}} \end{pmatrix} \\ &= \begin{pmatrix} \frac{1}{2} \sum_{i=1}^3 (w_{\mathrm{you},i} + w_{\mathrm{goodbye},i}) w_{i,\mathrm{you}} & \frac{1}{2} \sum_{i=1}^3 (w_{\mathrm{you},i} + w_{\mathrm{goodbye},i}) w_{i,\mathrm{say}} & \cdots & \frac{1}{2} \sum_{i=1}^3 (w_{\mathrm{you},i} + w_{\mathrm{goodbye},i}) w_{i,\mathrm{hello}} & \frac{1}{2} \sum_{i=1}^3 (w_{\mathrm{you},i} + w_{\mathrm{goodbye},i}) w_{i,\mathrm{period}} \end{pmatrix} \end{aligned} $$

となります。
 またこの値をスコアと呼びます。スコアが一番高い要素に対応する単語を、ターゲットの単語であるとして採用します。そのため、スコアを求める処理を推論処理と言います。

・処理の確認

 推論処理をNumPyを利用して行ってみましょう。

# NumPyをインポート
import numpy as np
# コンテキストデータを指定
c0 = np.array([[1, 0, 0, 0, 0, 0, 0]]) # you
c1 = np.array([[0, 0, 1, 0, 0, 0, 0]]) # goodbye

# 重みの初期値をランダムに生成
W_in = np.random.randn(7, 3)  # 入力層
W_out = np.random.randn(3, 7) # 出力層

# レイヤを生成
in_layer0 = MatMul(W_in)  # 入力層
in_layer1 = MatMul(W_in)  # 入力層
out_layer = MatMul(W_out) # 出力層

# 入力層の順伝播を計算
h0 = in_layer0.forward(c0) # you
h1 = in_layer1.forward(c1) # goodbye
h = 0.5 * (h0 + h1)
print(h0)
print(h1)
print(h)

# 出力層の順伝播(スコア)を計算
s = out_layer.forward(h)
print(np.round(s, 3))
[[-0.83839278  1.36845899  0.04013497]]
[[-0.05727094 -0.08164524  0.56260335]]
[[-0.44783186  0.64340688  0.30136916]]
[[ 0.08   0.485  0.181 -0.59   0.593  0.404  0.094]]

 以上でスコアが求まりました。

 次項では、スコアを正規化して、損失を求めます。

3.2.2 CBOWモデルの学習

 Softmax関数によってスコアを確率として扱えるように変換します。この処理を正規化と呼びます。また正規化した値と教師ラベルを用いて損失を求めます。CBOWモデルでは、交差エントロピー誤差を損失とします。

 Softmax関数については「ソフトマックス関数のオーバーフロー対策」を、交差エントロピー誤差については「損失関数」を参照してください。

 1.3.1項で実装したsoftmax()を使って処理します。

# スコアを確認
print(np.round(s, 3))

# Softmax関数による正規化
y = softmax(s)
print(np.round(y, 3))
print(np.sum(y))
[[ 0.08   0.485  0.181 -0.59   0.593  0.404  0.094]]
[[0.122 0.183 0.135 0.063 0.204 0.169 0.124]]
1.0

 各要素(単語)で、スコアの大小関係を保ったまま、総和が1となるような0から1の値に正規化されます。

 損失も、1.3.1項で実装したcross_entropy_error()を使って計算します。

# 教師ラベルを設定
t = np.array([0, 1, 0, 0, 0, 0, 0, 0, 0, 0])

# 損失を計算
loss = cross_entropy_error(y, t)
print(loss)
20.61569560179386

 この例では学習を行っていないので、損失の値は高くなります。

 またSoftmax with Lossレイヤとして実装したクラスSoftmaxWithLossの順伝播メソッド.forward()でも求められます。

# lossレイヤのインスタンスを作成
loss_layer = SoftmaxWithLoss()

# 損失を計算
loss = loss_layer.forward(s, t)
print(loss)
20.61569560179386

 当然同じ値になります。

 この節では、CBOWモデルの順伝播の処理を確認しました。次節では、CBOWモデルの入力データとなるコンテキストとターゲットを作成する関数を実装します。

参考文献

  • 斎藤康毅『ゼロから作るDeep Learning 2――自然言語処理編』オライリー・ジャパン,2018年.

おわりに

 3章1つ目の記事です!話には聞いていたword2vecにようやく取り組み始めました。
 1・2章が大丈夫なら3章も大丈夫そうですね。

 2020年9月14日は、モーニング娘。結成23周年の記念日です!!!おめでとうございます。

 私はオタ歴3年目ですが、これからもお慕い申し上げます。

【次節の内容】

https://www.anarchive-beta.com/entry/2020/09/15/190000www.anarchive-beta.com