Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

word2vec

前章では、「カウントベースの手法」によって単語分散表現を得ました。具体的には、単語の共起行列を作り、その行列に対してSVDを適用することで、密なベクトル(単語分散表現)を獲得したのです。

しかし、カウントベースの手法にはいくつかの問題点があります。

  • 大規模なコーパスを扱う場合、巨大な共起行列に対してSVDを計算することが難しい。

  • コーパスの全体から一回の学習で単語分散表現を獲得していますので、新しい単語が追加される場合、再度最初から学習を行う必要があり、単語分散表現更新の効率が低い。

「カウントベースの手法」に代わる強力な手法として「推論ベース」の手法が挙げられます。特に、Mikolov et al. (2013)によって提案されたword2vecの有用性が多くの自然言語処理タスクにおいて示されてきたのです。

本章では、word2vecの仕組みについて説明し、それを実装することで理解を深めます。

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

推論ベースの手法は、ミニバッチで学習する形で、ニューラルネットワークを用いて、重みを繰り返し更新することで単語分散表現を獲得します。

1.1推論ベース手法の設計

推論ベース手法では、you 【?】 goodbye and I say hello .のような、周囲の単語が与えられたときに、【?】にどのような単語が出現するのかを推測する推論問題を繰り返し解くことで、単語の出現パターンを学習します。

つまり、コンテキスト情報を入力として受け取り、各単語の出現する確率を出力する「モデル」を作成することが目標です。ここで、正しい推測ができるように、コーパスを使って、ニューラルネットワークモデルの学習を行います。そして、その学習の結果として、単語の分散表現を得られます。

1.2one-hot表現

ニューラルネットワークで単語を処理するには、それを「固定長のベクトル」に変換する必要があります。

そのための方法の一つは、単語をone-hot表現へと変換することです。one-hot表現とは、ベクトルの要素の中で一つだけが1で、残りは全て0であるようなベクトルのことです。

単語をone-hot表現に変換するには、語彙数分の要素を持つベクトルを用意して、単語IDの該当する箇所を1に、残りは全て0に設定します。

1.3CBOW(continuous bag-of-words)モデル

CBOWモデルは、コンテキストからターゲットを推測することを目的としたニューラルネットワークです。このCBOWモデルで、できるだけ正確な推測ができるように訓練することで、単語の分散表現を取得することができます。

ここで、例として、コンテキスト["you","goodbye"]からターゲット"say"を予測するタスクを考えます。

1.3.1入力層から中間層(エンコード)

one-hotエンコーディングで、単語を固定長のベクトルに変換することができます。

単語をベクトルで表すことができれば、そのベクトルはニューラルネットワークを構成する「レイヤ」によって処理することができるようになりました。

コンテキストをc\mathbf{c}、重みをW\mathbf{W}とし、それぞれ次の形状とします。

c=(cyoucsaycgoodbyecandcIchellocperiod), W=(wyou,1wyou,2wyou,3wsay,1wsay,2wsay,3wgoodbye,1wgoodbye,2wgoodbye,3wand,1wand,2wand,3wI,1wI,2wI,3whello,1whello,2whello,3wperiod,1wperiod,2wperiod,3)\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}

コンテキストの要素数(列数)と重みの行数が、単語の種類数に対応します。

コンテキスト(単語)はone-hot表現として扱うため、例えば「you」の場合は

cyou=(1000000)\mathbf{c}_{\mathrm{you}} = \begin{pmatrix} 1 & 0 & 0 & 0 & 0 & 0 & 0 \end{pmatrix}

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

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

h=cyouW=(h1h2h3)\begin{aligned} \mathbf{h} &= \mathbf{c}_{\mathrm{you}} \mathbf{W} \\ &= \begin{pmatrix} h_1 & h_2 & h_3 \end{pmatrix} \end{aligned}

h1h_1の計算を詳しく見ると、次のようになります。

h1=cyouwyou,1+csaywsay,1+cgoodbyewgoodbye,1+candwand,1+cIwI,1+chellowhello,1+cperiodwperiod,1=1wyou,1+0wsay,1+0wgoodbye,1+0wand,1+0wI,1+0whello,1+0wperiod,1=wyou,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}

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

残りの2つの要素も同様に計算できるので、重み付き和

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

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

コンテキストの形状:(1, 7)
重み
[[ 0.06783164 -0.80640223 -0.77462623]
 [-0.23186237  0.17269022 -0.41683942]
 [ 0.29064795  0.09633413  2.16713912]
 [ 0.49322236 -1.09178806 -0.00444333]
 [ 1.48199498 -0.64959851  0.39150032]
 [ 1.69079835 -0.53560302  0.74973898]
 [ 0.87951791 -0.72085047  1.64067938]]
重み付き和
[[ 0.06783164 -0.80640223 -0.77462623]]
重み付き和の形状:(1, 3)

コンテキストに複数の単語がある場合、入力層も複数になります。このとき、中間層にあるニューロンは、各入力層の全結合による変換後の値が平均されたものになります。

中間層のニューロンの数を入力層よりも減らすことによって、中間層には、単語を予測するために必要な情報が"コンパクト"に収められて、結果としては密なベクトル表現が得られます。このとき、この中間層の情報は、人間には理解できない「ブラックボックス」のような状態になります。この作業は、「エンコード」と言います。

1.3.2中間層から出力層(デコード)

中間層の情報から目的の結果を得る作業は、「デコード」と言います。ここでは、中間層のニューロンの値h\mathbf{h}を各単語に対応した値になるように、つまり要素(行)数が単語の種類数となるように再度変換したものを、CBOWモデルの出力とします。

出力層の重みを

Wout=(w1,youw1,sayw1,goodbyew1,andw1,Iw1,hellow1,periodw2,youw2,sayw2,goodbyew2,andw2,Iw2,hellow2,periodw3,youw3,sayw3,goodbyew3,andw3,Iw3,hellow3,period)\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}

とします。行数が中間層のニューロン数、列数が単語の種類数になります。

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

s=hWout=(syoussaysgoodbyesandsIshellosperiod)\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」に関する要素の計算は、

syou=12(wyou,1+wgoodbye,1)w1,you+12(wyou,2+wgoodbye,2)w2,you+12(wyou,3+wgoodbye,3)w3,you=12∑i=13(wyou,i+wgoodbye,i)wi,you\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」に関する出力の重みの積になります。

他の要素(単語)についても同様に計算できるので、最終的な出力は

s=(syoussaysgoodbyesandsIshellosperiod)=(12∑i=13(wyou,i+wgoodbye,i)wi,you12∑i=13(wyou,i+wgoodbye,i)wi,say⋯12∑i=13(wyou,i+wgoodbye,i)wi,hello12∑i=13(wyou,i+wgoodbye,i)wi,period)\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}

となります。

ここで、出力層の各ニューロンの値は各単語に対応し、「スコア」と呼ばれます。

「スコア」の値が高ければ高いほど、それに対応する単語の出現確率も高くなり、ターゲットの単語であるとして採用します。そのため、スコアを求める処理を推論処理と言います。

(tensor([[ 0.6297, -0.8924, 0.7035]]), tensor([[-0.9046, -0.7538, 0.7135]]), tensor([[-0.1375, -0.8231, 0.7085]]), tensor([[-0.1280, 0.1380, -1.1100, -0.5470, -1.1300, 0.1390, -0.5510]]))
Fig1
課題
ヒント

正解は「say」として、Softmax関数によってスコアsを確率として扱えるように変換し、そして、正規化した値と教師ラベルを用いて損失を求めなさい。

1.3.3word2vecの重みと分散表現

与えられたコンテキストに対して単語を予測するときに、「良い重み」のネットワークがあれば、「確率」を表すニューロンにおいて、正解に対応するニューロンが高くなっていることが期待できます。そして、大規模コーパスを使って得られる単語の分散表現は、単語の意味や文法のルールにおいて、人間の直感と合致するケースが多く見られます。

word2vecモデルの学習で行うことが、正しい予測ができるように重みを調整することです。つまり、「コンテキストから、出現する単語」を予測するという偽タスクをニューラルネットで解いてきましたが、目的はニューラルネットの重みを求めることになります。

もっと具体的に言えば、word2vecで使用されるネットワークには二つの重みがあります。それは、入力層の重みWin\mathbf{W_{in}}と、出力層の重みWout\mathbf{W_{out}}です。それでは、どちらの重みを使えば良いでしょうか?

  1. 入力側の重みを利用する

  2. 出力側の重みを利用する

  3. 二つの重みの両方を利用する

word2vecモデルに関しては、多くの研究や応用例で、入力層の重みを単語のベクトル表現として使用されており、良好なパフォーマンスを示しています。

2word2vecモデルの実装

2.1学習データの準備

2.1.1コンテキストとターゲット

word2vecモデルのためのニューラルネットワークでは、「コンテキスト」を入力した時に、「ターゲット」が出現する確率を高くなるように学習を行います。

そのため、コーパスから「コンテキスト」と「ターゲット」が対応するデータを作成する必要があります。

{'you': 0, 'say': 1, 'goodbye': 2, 'and': 3, 'i': 4, 'hello': 5, '.': 6}
{0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}
[0, 1, 2, 3, 4, 1, 5, 6]

テキストの単語を単語IDに変換したcorpusからターゲットを抽出します。

ターゲットはコンテキストの中央の単語なので、corpusの始めと終わりのウインドウサイズ分の単語は含めません。

[1, 2, 3, 4, 1, 5]

ターゲットの単語に対して、for文で前後ウィンドウサイズの範囲の単語を順番に抽出しcsに格納します。

つまりウィンドウサイズを1とすると、corpusにおけるターゲットのインデックスidxに対して、1つ前(idx - window_size)から1つ後(idx + window_size)までの範囲の単語を順番にcsに格納します。ただしターゲット自体の単語はコンテキストに含めません。

[0]
[0, 2]
[[0, 2]]
[[0 2]
 [1 3]
 [2 4]
 [3 1]
 [4 5]
 [1 6]]
[1 2 3 4 1 5]

2.2PyTorchでCBOWモデルの実装

2.2.1Embeddingレイヤ

先ほど、理解しやすいone-hot表現でコンテキストを変換する方法を説明しましたが、大規模なコーパスで学習する際、one-hot表現の次元数も大きくなって、非効率な学習の原因になります。

ただ、one-hot表現による計算は、単に行列の特定の行を抜き出すことだけですから、同じ機能を持つレイヤで入れ替えることは可能です。このような、重みパラメータから「単語IDに該当する行(ベクトル)」を抜き出すためのレイヤは「Embeddingレイヤ」と言います。

PyTorchで提供されるモジュールnn.Embeddingを使うと、簡単にEmbeddingレイヤを実装することができます。

例えば、語彙に6つの単語があり、各埋め込みベクトルの次元数を3に設定した場合、nn.Embeddingの定義は以下のようになります。

もしインデックス2のトークンの埋め込みを取得したい場合、次のようにします:

tensor([[[-0.1405, -0.8412, -0.3166], [-0.2717, -0.2344, 0.5441]]], grad_fn=<EmbeddingBackward0>)

埋め込みベクトルの和を取って、入力層から中間層までにエンコードの機能を実装できます。

tensor([[-0.4121, -1.0756, 0.2275]], grad_fn=<SumBackward1>)
tensor([[-1.6742, -2.3260, -2.1915, -1.1772, -1.9379, -1.8908]], grad_fn=<LogSoftmaxBackward0>)

2.2.2ミニバッチ化データセットの作成

word2vecも含めて、深層学習によって学習を行う際には、ミニバッチ化して学習させることが一般的です。

PyTorchで提供されているDatasetとDataLoaderという機能を用いてミニバッチ化を簡単に実現できます。

2.2.2.1DataSet

Datasetは、元々のデータを全て持っていて、ある番号を指定されると、その番号の入出力のペアをただ一つ返します。クラスを使って実装します。

Datasetを実装する際には、クラスのメンバ関数として__len__()と__getitem__()を必ず作ります。

  • __len__()は、len()を使ったときに呼ばれる関数です。

  • __getitem__()は、array[i]のようにインデックスを使って要素を参照するときに呼ばれる関数です。

全データ数: 6
4番目のデータ: (tensor([3, 1]), tensor(4))
4~5番目のデータ: (tensor([[3, 1],
        [4, 5]]), tensor([4, 1]))
2.2.2.2DataLoader

torch.utils.dataモジュールには、データのシャッフルとミニバッチの整形に役立つDataLoaderというクラスが用意されています。

[tensor([[1, 3],
        [0, 2]]), tensor([2, 1])]
[tensor([[2, 4],
        [3, 1]]), tensor([3, 4])]
[tensor([[1, 6],
        [4, 5]]), tensor([5, 1])]

2.2.3CBOWモデルの構築

  • self.embeddings = nn.Embedding(vocab_size, embedding_size): 語彙の各単語に対してembedding_size次元のベクトルを割り当てる埋め込み層を作成します。

  • self.linear1 = nn.Linear(embedding_size, vocab_size): 埋め込みベクトルを受け取り、語彙のサイズに対応する出力を生成します。

  • embeds = self.embeddings(inputs):入力された単語のインデックスに基づいて、埋め込み層から対応するベクトルを取得します。

Epoch 0, Total loss: 6.139737367630005
Epoch 10, Total loss: 4.7057578563690186
Epoch 20, Total loss: 3.722508132457733
Epoch 30, Total loss: 3.0618834495544434
Epoch 40, Total loss: 2.5887580513954163
Epoch 50, Total loss: 2.251922070980072
Epoch 60, Total loss: 2.0073779821395874
Epoch 70, Total loss: 1.8148369193077087
Epoch 80, Total loss: 1.6640116572380066
Epoch 90, Total loss: 1.5436910688877106

モデルの入力層の重みが単語分散表現であり、単語 ×\times 埋め込み次元数の形の行列になります。

torch.Size([7, 10])
Word: you
Vector: [-0.9910462  -0.26103663  0.37548813 -0.8958604   0.4883364  -0.10967051
  0.83219934  1.2695521   0.4894465  -0.93762267]

Word: say
Vector: [ 0.16747946 -0.39847788  1.0496353  -1.5360904  -1.5014437   0.4942124
  0.34624904 -0.34384438  0.5449382  -0.3768147 ]

Word: goodbye
Vector: [-1.9509926   1.5140524   0.74627566  1.6006314  -0.6697169  -0.16869175
 -0.16643491 -0.13721499  0.98251516  0.464983  ]

Word: and
Vector: [ 2.20343     0.95126253 -0.97306097  0.8715391  -0.16568483  0.20985529
 -2.0275564   0.462676   -0.8142235   0.53176856]

Word: i
Vector: [ 0.4384659  -1.1122588   0.39310274 -1.0731715  -0.6900966  -2.290304
 -1.7379956  -0.3933478   0.6740521   0.5169324 ]

Word: hello
Vector: [-0.73360014  0.9347052  -0.11820035  0.7009177   0.47436512  1.431984
 -0.46498355 -0.5125341   0.70763314 -1.6589094 ]

Word: .
Vector: [-0.74944794  0.8782758  -0.31801623 -0.8320363   1.4819576   1.5980204
 -1.5042956  -0.9542531  -1.1500387   1.3903668 ]

課題
テキスト

与えられたテキストを用いて、単語分散表現を学習しなさい。

  • window_sizeを2に設定します

  • batch_sizeを10に設定します

3補足

3.1学習データの作成

Fig1 Fig2

3.2Negative Sampling

今まで紹介した学習の仕組みでは、正例(正しい答え)について学習を行いました。ここで、「良い重み」があれば、ターゲット単語についてSigmoidレイヤの出力は1に近づくことになります。

 Fig1  Fig1

それだけでなく、本当に行いたいのは、コンテキストが与えられるときに、間違った単語を予測してしまう確率も低いことが望まれます。ただ、全ての誤った単語(負例)を対象として、学習を行うことは非効率であるので、そこで、負例をいくつかピックアップします。これが「Negative Sampling」という手法の意味です。

 Fig1 

Negative Samplingでは、正例をターゲットとした場合の損失を求めると同時に、負例をいくつかサンプリングし、その負例に対しても同様に損失を求めます。そして、両者を足し合わせ、最終的な損失とします。このプロセスは、モデルが正しい単語を予測するだけでなく、不適切な単語を予測しない能力を同時に学習することを目的としています。

 Fig1 

それでは、負例をどのようにサンプリングすべきですか?単純にランダムサンプリングの場合、高頻度の単語はサンプリングされやすく、低頻度の単語はサンプリングされにくく、珍しい単語や文脈に適切に対応できない原因になります。

この問題点を克服するために、word2vecで提案されるNegative Samplingでは、元となる確率分布に対して以下のように改良しました

P′(wi)=P(wi)0.75∑j=1nP(wj)0.75P'(w_i)= \frac{P(w_i)^{0.75}}{\sum_{j=1}^{n} P(w_j)^{0.75}}

0.75乗して調整することで、確率の低い単語に対してその確率を少しだけ高くすることができます。これにより、モデルはより現実的な言語パターンを学習し、実際の使用状況においてより正確な予測を行うことができます。

3.3Skip-gram

これまで見てきたCBOWモデルのほかに、word2vecを学習する方法として、Skip-gramと呼ばれる言語モデルが提案されています。

Skip-gramは、CBOWで扱うコンテキストとターゲットを逆転させて、ターゲットから、周囲の複数ある単語(コンテキスト)を推測します。

実際、単語の分散表現の精度の点において、多くの場合、Skip-gramモデルの方が良い結果が得られています。特に、コーパスが大規模になるにつれて、低頻度の単語や類推問題の性能の点において、Skip-gramモデルの方が優れている傾向にあります。

 Fig1 
References
  1. Mikolov, T., Yih, W., & Zweig, G. (2013). Linguistic Regularities in Continuous Space Word Representations. In L. Vanderwende, H. Daumé III, & K. Kirchhoff (Eds.), Proceedings of the 2013 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies (pp. 746–751). Association for Computational Linguistics. https://aclanthology.org/N13-1090