絵で理解するWord2vecの仕組み

皆さん、Word2vec の仕組みはご存知ですか？

Word2vec は gensim や TensorFlow で簡単に試せるので使ったことのある方は多いと思います。しかし、仕組みまで理解している方はそう多くないのではないでしょうか。そもそも[本家の論文](https://arxiv.org/abs/1301.3781)でも内部の詳細については詳しく解説しておらず、[解説論文](https://arxiv.org/abs/1411.2738)が書かれているくらいです。

本記事では Word2vec のモデルの一つである **Skip-Gram** について絵を用いて説明し、概要を理解することを目指します。まずは Skip-Gram がどのようなモデルなのかについて説明します。

※ 対象読者はニューラルネットワークの基礎を理解しているものとします。

# どのようなモデルなのか？
Skip-Gram はニューラルネットワークのモデルの一つです。Skip-Gram は２層のニューラルネットワークであり隠れ層は一つだけです。隣接する層のユニットは全結合しています。Skip-Gram のアーキテクチャは以下の図のようになっています。

<img width="400" alt="model.png" src="https://qiita-image-store.s3.amazonaws.com/0/77079/633894e8-6e1e-c274-a33a-83d0b46e99a8.png">

このニューラルネットワークは**<font color="red">あるタスク</font>**を実行するために学習されますが、実際には学習したタスクに対してニューラルネットワークを使うことはありません。実際の目的は隠れ層の重みを学習することにあります。この隠れ層の重みのことを**単語ベクトル**と呼び、私たちが真に必要とするものなのです。



# あるタスクとは？
では Skip-Gram が行うタスクについて説明します。繰り返しますが、このタスクの結果を使うことはありません。あくまでも重みの学習が目的です。

ではあらためて Skip-Gram で行うタスクについて説明します。Skip-gramでは、ある単語を入力した時、その周辺にどのような単語が現れやすいか予測することをモデル化します。以下の例文を使って考えてみましょう。

```
I want to eat an apple everyday.
```

ここで、ある単語が "eat" だったとします。この単語に注目すると、周辺語には食べ物の名前である "apple" や "orange" が現れそうです。つまり、ある単語の周辺語としてどういった単語が出現しやすいかという確率を考えることができます。例えば、訓練済みのネットワークに "eat" という単語を与えるとします。その時、"apple" や "orange" のような関連性の高い単語は周辺に現れる確率が高く、"tank" や "network" のような関連の低い単語は低い確率を取るようにします。この確率をボキャブラリ内のすべての単語に対して計算します。

ここで周辺語として何単語まで考えるのかというのを**ウィンドウサイズ $C$** として与えます。ウィンドウサイズと周辺語の関係は以下の図のようにするとわかりやすいですね。
<img width="712" alt="window_size.png" src="https://qiita-image-store.s3.amazonaws.com/0/77079/a86bed6d-df90-5274-e905-5041ef465b10.png">


学習は教師あり学習で行います。具体的に何を与えるかというと、入力としてある単語を、出力としてその周辺語を与えます。これらの単語は訓練データ内に現れる単語です。これらの単語を与えて、ネットワークに、ある単語に対するその周辺語の確率を学習させます。以下の図は、周辺語の数を1つとした時の入出力のイメージです。
<img width="840" alt="in_out.png" src="https://qiita-image-store.s3.amazonaws.com/0/77079/5b8b8ff5-6540-db62-9f4f-719e3e2c7ab2.png">



例えば、ネットワークはおそらく (eat, apple) のようなサンプルに対しては (eat, network) より高い確率になるように学習します。学習が終わった時には、"eat" という単語を入力として与えると、"apple" や "orange" は "network" よりはるかに高い確率を出力します。以下がそのイメージです。
<img width="829" alt="learned_model.png" src="https://qiita-image-store.s3.amazonaws.com/0/77079/2e376fd0-03f9-83e1-63ca-9cc7ae925cec.png">

さて、以降ではモデルの詳細について入力層、隠れ層、出力層ごとに見ていきましょう。説明する際は、話を簡単にするために周辺語の数を1つと仮定して説明していきます。

# 入力層
さて、では入力層の詳細について見ていきましょう。

単語のような可変長の文字列をニューラルネットワークに与えることはできません。そのため、固定長形式で単語を表す必要があります。そのためにまず、学習データから**ボキャブラリ**を構築します。ボキャブラリというのは重複のない単語集合のことです。例を挙げてみてみましょう。以下のような学習データがあったとします。

```
I want to eat apple. I like apple.
```

この時、この学習データからボキャブラリを構築すると以下のようになります。重複のない単語集合になっていることがわかるでしょうか？

```
{apple eat I like to want .}
```

この場合ボキャブラリ数は **7** でした。

ボキャブラリを構築したら、"apple" のような入力語を **one-hotベクトル** として表します。このベクトルはボキャブラリ数と同じサイズの要素を持ちます。上記ボキャブラリの場合は 7 要素を持ちます。この各要素がボキャブラリ内のすべての単語に対応します。そして "apple" に対応する場所に1を、その他の場所に0を入れます。

たとえば上記のボキャブラリで 単語 "apple" を入力する場合、入力層には以下のベクトルが入力されます。

```math
\left[\begin{array}{l}
1 \cdots apple\\
0 \cdots eat\\
0 \cdots I\\
0 \cdots like\\
0 \cdots to\\
0 \cdots want\\
0 \cdots .\\
\end{array}\right]
```



# 隠れ層
さて、300次元の単語ベクトルを学習することにしましょう。また、ボキャブラリ数は10000としましょう。そうすると隠れ層は 10000 x 300 の重み行列で表されます。10000はボキャブラリ内のすべての単語を表し、300は隠れ層のニューロン数を表します。重み行列は以下のように表されます。
<img width="410" alt="lookup_table.png" src="https://qiita-image-store.s3.amazonaws.com/0/77079/da458afb-21ca-94c4-d9d2-5b7adb7966b4.png">



**この重み行列の各行が実は単語ベクトルになっています。**そのため、最終目標は隠れ層の重み行列を学習することなのです。ニューラルネットワークの学習が終わったら出力層は必要ありません。

さて、では入力層と隠れ層の間でどのような計算が行われるかみてみましょう。

もし 1 x 10000 のone-hotベクトルを 10000 x 300 の行列にかけると、one-hotベクトルの1に対応する箇所の行列の行を効率的に抽出することができます。以下がその例です。先のボキャブラリでいうと "eat" の単語ベクトルを抽出していることになります。

<img width="1002" alt="input-hidden.png" src="https://qiita-image-store.s3.amazonaws.com/0/77079/d4c5640c-1de0-371e-9dec-d57f70f698cb.png">

ニューラルネットワーク的にどうなっているかというと以下のような計算をしています。赤線で表される重みが抽出されるわけですね。
<img width="783" alt="input_hidden_nn.png" src="https://qiita-image-store.s3.amazonaws.com/0/77079/c9597999-eafc-4651-0a57-803d48094067.png">



ここまでの話をまとめると、このモデルの隠れ層は実際には入力語の単語ベクトルのルックアップテーブルとして機能することを意味しています。Skip-Gramでは隠れ層に活性化関数を設定しないので、隠れ層の出力は単なる入力語の単語ベクトルになります。

# 出力層
隠れ層から出力された入力語に対応する 1 x 300 の単語ベクトルは、隠れ層〜出力層間の重みがかけられた後、出力層に入力されます。実はこの隠れ層〜出力層間の重みも単語ベクトルを表しています。重み行列は以下のように表されます。入力層〜隠れ層間と違って重み行列の列が単語ベクトルになっています。
<img width="593" alt="weight_matrix.png" src="https://qiita-image-store.s3.amazonaws.com/0/77079/aa494fb1-8ff8-5c09-eb7a-b2ff7894168d.png">


つまり、出力層に入力されるのは結局単語の重みベクトル同士の内積ということになります。図示すると以下のようになります。
<img width="827" alt="hidden_output_nn.png" src="https://qiita-image-store.s3.amazonaws.com/0/77079/d7450515-40d0-0fb3-d560-f9f8de4f7066.png">


そして、出力層に入力された値に対してソフトマックス関数をかませます。これにより、出力層からの出力を確率値に変換しています。出力層の各ニューロンはボキャブラリ内の各単語に対応しますが、それは0から1の値を取り、合計すると1になります。

確率を出力するまでの計算を図示すると以下のような感じになります。
<img width="845" alt="summary.png" src="https://qiita-image-store.s3.amazonaws.com/0/77079/4dd648db-539f-1bf2-0371-e7b798004c7c.png">

まとめると、Skip-Gram では単語の重みベクトル同士の内積を計算していると見なせます。それが出力層のユニットに入力されます。ここで出力層への入力値にソフトマックス関数を使うのは確率値に変換するためです。結局、学習で行われているのは、ある単語とその単語に対して実際に現れる周辺語の内積が大きくなるように重みを調整していくということでしょう。



# おわりに
本記事では Skip-Gram の概要を理解するために絵を用いて説明してみました。
やっていること自体は難しくないと感じてもらえたと思います。
次は実装するために丁寧に数式展開をしてみましょう。

つづく


# 参考文献

* [word2vec Parameter Learning Explained](https://arxiv.org/abs/1411.2738)
* [Efficient Estimation of Word Representations in Vector Space](https://arxiv.org/abs/1301.3781)
* [Distributed Representations of Words and Phrases and their Compositionality](https://arxiv.org/abs/1310.4546)
* [Word2Vec Tutorial - The Skip-Gram Model](http://mccormickml.com/2016/04/19/word2vec-tutorial-the-skip-gram-model/)