Np-Urのデータ分析教室

オーブンソースデータなどWeb上から入手できるデータを用いて、RとPython両方使って分析した結果を書いていきます

word2vec(Skip-Gram Model)の仕組みを恐らく日本一簡潔にまとめてみたつもり

久しぶりの記事更新です。
今回はかねてより書いてみたかったword2vecについて。

word2vecはとても面白い考え方なのですが、個人的には仕組みがちょっと捉えづらく、理解するのに結構時間がかかりました。

そこで今回は、過去の自分を救えるように、word2vecをできるだけ簡潔に、そして直観的に理解できるように解説していきます。

なお、word2vecについては以下書籍でよくまとまっているので、よろしければ是非!

Pythonと実データで遊んで学ぶ データ分析講座

Pythonと実データで遊んで学ぶ データ分析講座

※追記※
スマホのAMPだと、行列や数式がうまく表示されない可能性がありますので、こちらのリンクかPCから購読頂けますと幸いです。

word2vecを使うと何ができるの?

そもそもword2vecを使うと何が嬉しいのか?について、まずは説明します。

word2vecは、大量のテキストデータを解析し、各単語の意味をベクトル表現化する手法です。単語をベクトル化することで、

  • 単語同士の意味の近さを計算
  • 単語同士の意味を足したり引いたり

ということが可能になります。

例えば、word2vecにより『松本人志』、『浜田雅功』、『ボケ』、『ツッコミ』という言葉を以下のようにベクトル化できたりします。

  • 松本人志: (0.4, 0.1, 0. 9, 0.4)
  • 浜田雅功: (0.5, 0.2, 0. 3, 0.4)
  • ボケ: (0.1, 0.0, 0. 8, 0.2)
  • ツッコミ: (0.2, 0.1, 0. 2, 0.3)

このベクトル表現から、

  • 『松本人志』、『浜田雅功』の距離を計算すると、結構近いところにありそうだ。なので二つの意味は近いのでは?
  • 『松本人志』 - 『ボケ』 + 『ツッコミ』 ≒ 『浜田雅功』 になる。

といった考察・計算ができます。

単語ベクトルを作るために必要なフェイクタスク

単語ベクトルがあると色々楽しい分析ができることが分かったところで、実際のword2vecの仕組みを説明していきましょう。word2vecには大きく分けて
  • CBOW
  • Skip-Gram Model

という2つの手法があります。

Skip-Gram Modelの方が分かりやすいので、今回はこちらを使ったword2vecを解説していきます。

単語ベクトルを直接求めることは大変なので、word2vecでは、ある偽のタスクを解くことを考え、その過程で間接的に計算していきます。

この辺りがword2vecの捉えづらいポイントかなと思います。
「間接的に求める、ってなに?」と思われるかもしれませんが、その疑問を心の片隅に置いたまま、最後までお付き合いいただければ。

意味が近い単語と周辺語の関係性

意味が近い(≒単語ベクトルの距離が近い)言葉は周辺語も似ているはずです。

「私は東急田園都市線の三軒茶屋駅付近に住んでいます。」
「神奈川の溝の口には、東急田園都市線とJR南武線が走っています。」

このような文があったとしましょう。

  • 三軒茶屋
  • 溝の口

という2つの言葉はどちらも「東急田園都市線」という単語から近いところに配置されています。
ということは、この二つの単語は比較的近い属性なのではないか?と予想できます。

他にも例えば、「Python」や「R」という単語の近くには「データ」という単語がよく現れます。逆に言えば、「データ」という単語が周辺によく現れる「Python」と「R」は似ている意味なのでは?と考えられそうですよね。

「各単語はその周辺語と何らかの関係性がある」という考えを元に、「ある単語を与えた時にその周辺語を予測する」こんなタスクも解けそうですよね。

これが前述したある偽のタスクの正体です。
ということで、次節でこの問題を実際に解くようにニューラルネットを構成してみましょう。

入力層・隠れ層・出力層のみのニューラルネットワーク

word2vecのSkip-Gram Modelでは、入力された単語を元に周辺語を出力する、入力層・隠れ層・出力層のみのシンプルなニューラルネットを考えます。

下図のようなイメージです。

f:id:Np-Ur:20180115072550p:plain

とてもシンプルな構造ですね。
ある単語を入力データに、そしてその周辺語を教師データにして、重みを学習していきます。

ここで、「どこまでを周辺語とするか?」ということも重要です。周辺5単語(前後合わせて10単語)というのが一般的なようですが、適宜調整しつつ分析を進めましょう。

例えば、(日本語よりも英語の方が分かりやすいので英語で説明します。)
「I usually go to the company near Sangenjaya station on the Denentoshi line.」
この文から、入力語と周辺語のセットを取り出してみましょう。

  • I が入力語の場合:(I, usually), (I, go), (I, to), (I, the), (I, company)の5個
  • usually が入力語の場合:(usually, I), (usually, go), (usually, to), (usually, the), (usually, company), (usually, near)の6個
  • go が入力語の場合:(go, I), (go, usually), (go, to), (go, the), (go, company), (go, neaar), (go, Sangenjaya )の7個

・・・

  • company が入力語の場合:(company , I), (company , usually), (company , go), (company , to), (company , the), (company , near), (company ,Sangenjaya ), (company , station ), (company , on), (company , the)の10個

・・・

という風にそれぞれセットでデータを取り出すことができます。

Iが入力されると、usuallyやgoという単語が予測結果として出力されるようにニューラルネットを学習していくイメージです。
1文だけでこのように多くの入力語と周辺語のセットを取得できることを考えると、大量のテキストデータを読み込むととんでもない量のデータが得られそうですね。

入力データと教師データと重み行列

簡単のため、「I usually go to the company near Sangenjaya station on the Denentoshi line.」のみしか学習するテキストが無いとしましょう。単語数を数えると12ですね。

もちろん実際に学習させる際は、大量のテキストを読み込み、何万~何百万という単位の単語を用います。

I や usually, go という単語をそのまま文字列の状態で計算することはできないので、何らかの方法で数値化する必要があります。

全部で12単語なので、

  • I: (1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
  • usually: (0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
  • go: (0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0)
  • to: (0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0)

…

  • line: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)

という風に変換すると良いでしょう。このような、一つの要素だけ「1」で残りは「0」のベクトルを、one-hotベクトルと言います。

なので入力語と周辺語のセットが、(I, usually)だとすると、下図のようなベクトルをニューラルネットに学習させるということになります。
f:id:Np-Ur:20180115091718p:plain
同様に(I, go)だと以下のようになります。
f:id:Np-Ur:20180115091904p:plain

入力層の次元が今回12次元なので、同じく出力層も12次元です。

実際にword2vecを用いる際は、入力層を数万次元のベクトル、入力層から隠れ層への重み行列を、数万 × 数百の次元に設定することが多いようです。
ここでは簡単のため、入力層から隠れ層への重み行列ですが、  12 × 3 としましょう。すると、隠れ層から出力層への重み行列は  3 × 12 となります。

まとめると、

  • 入力ベクトル( 1 × 12 )に、入力層から隠れ層への重み行列( 12 × 3 )をかけ、得られた 1 × 3 のデータを隠れ層に入れる。
  • 隠れ層の値に、隠れ層から出力層への重み行列( 3 × 12 )をかけ、得られた 1 × 12 のデータを出力層に入れる。

という流れです。また、これは分類問題なので出力層にはソフトマックス関数をかまして、確率化してあげる必要があります。
分類問題を解くための、とてもシンプルなニューラルネットですね。

フェイクタスクの結果と単語ベクトルの関係性

さて、テキストから入力語と周辺語のセットを作り、そのデータを元に「入力された単語から周辺語を予測する学習」を行うことで、
  • 入力層から隠れ層への重み行列: W
  • 隠れ層から出力層への重み行列: W'

が計算されました。

例えば、以下のような入力層から隠れ層への重み行列が、学習の結果作れたとします。

 W = \left(
    \begin{array}{ccc}
      0.3 & 0.2 & 0.6 \\
      0.5 & 0.1 & 0.8 \\
      0.7 & 0.2 & 0.4 \\
      0.1 & 0.3 & 0.5 \\
      0.2 & 0.4 & 0.1 \\
      0.6 & 0.1 & 0.4 \\
      0.3 & 0.7 & 0.9 \\
      0.1 & 0.4 & 0.4 \\
      0.3 & 0.6 & 0.8 \\
      0.5 & 0.9 & 0.4 \\
      0.8 & 0.2 & 0.7 \\
      0.5 & 0.1 & 0.3 \\
    \end{array}
  \right)

前述したとおり、入力はone-hotベクトルです。

例えば「usually」(第2成分のみ1で残りは0)という単語が入力された際、隠れ層の値は



(0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
 \left(
    \begin{array}{ccc}
      0.3 & 0.2 & 0.6 \\
      0.5 & 0.1 & 0.8 \\
      0.7 & 0.2 & 0.4 \\
      0.1 & 0.3 & 0.5 \\
      0.2 & 0.4 & 0.1 \\
      0.6 & 0.1 & 0.4 \\
      0.3 & 0.7 & 0.9 \\
      0.1 & 0.4 & 0.4 \\
      0.3 & 0.6 & 0.8 \\
      0.5 & 0.9 & 0.4 \\
      0.8 & 0.2 & 0.7 \\
      0.5 & 0.1 & 0.3 \\
    \end{array}
  \right)\\
=  \left(
    \begin{array}{ccc}
      0.5 & 0.1 & 0.8 \\
    \end{array}
  \right)\\
となり、重み行列の第2行目がそのまま入ります。

同様に「company」(第6成分のみ1で残りは0)という単語が入力された際、隠れ層の値は



(0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0)
 \left(
    \begin{array}{ccc}
      0.3 & 0.2 & 0.6 \\
      0.5 & 0.1 & 0.8 \\
      0.7 & 0.2 & 0.4 \\
      0.1 & 0.3 & 0.5 \\
      0.2 & 0.4 & 0.1 \\
      0.6 & 0.1 & 0.4 \\
      0.3 & 0.7 & 0.9 \\
      0.1 & 0.4 & 0.4 \\
      0.3 & 0.6 & 0.8 \\
      0.5 & 0.9 & 0.4 \\
      0.8 & 0.2 & 0.7 \\
      0.5 & 0.1 & 0.3 \\
    \end{array}
  \right)\\
=  \left(
    \begin{array}{ccc}
      0.6 & 0.1 & 0.4 \\
    \end{array}
  \right)\\
となり、重み行列の第6行目がそのまま入ります。

これらの値が隠れ層に入り、またそこから W′をかけてあげることで出力層の値が決まります。



この辺りが一番大切なので、もう一度まとめます。

「usually」(第2成分のみ1で残りは0)が入力層に入ったとすると、重み行列 Wの第2行がそのまま隠れ層に入る。
そして「company」(第6成分のみ1で残りは0)が入力層に入ったとすると、重み行列 Wの第6行がそのまま隠れ層に入る。

こうして計算された隠れ層の値を元に、出力層が計算される……
ということは、

  • 「usually」という単語が持つ特性は、重み行列 Wの第2行目の値である
  • 「company」という単語が持つ特性は、重み行列 Wの第6行目の値である

と結論づけても全く強引じゃないですよね?
なぜなら、結局は各単語と対応する行の重みしかその後の計算に使われていないので。

同様に、

  • 「I」という単語が持つ特性は、重み行列 Wの第1行目の値である
  • 「usually」という単語が持つ特性は、重み行列 Wの第2行目の値である
  • 「go」という単語が持つ特性は、重み行列 Wの第3行目の値である

…

  • 「line」という単語が持つ特性は、重み行列 Wの第12行目の値である

ということで、入力層から隠れ層への重み行列を元に単語の特性をベクトル化することができました!

これまで「入力語から周辺語」を予測するという偽タスクをニューラルネットで解いてきましたが、目的はこの入力層から隠れ層への重み行列だったんですね!

重み行列の各行のベクトルが、そのまま単語の特徴を表すベクトルになるわけです。

全体の流れをおさらい

つまり、
  • 同じような意味の単語からは、同じような周辺語が予測されるはず
  • ある単語の周りに出現する単語を予測する学習を、ニューラルネットワークで行う
  • 学習の結果、入力層から隠れ層への重みが計算される
  • 入力がont-hotベクトルなので、入力層から隠れ層への重み行列の各行を、そのまま単語ベクトルと表現してよいだろう

という流れです。

長い説明でしたが、最後まで読んでいただいた方々ありがとうございます。
そしてお疲れ様でした!

まとめ

今回はword2vecについて仕組みをまとめました。
ものすごく分かりやすく書けたのではないかと自画自賛しています笑。

せっかく理論面を学んだので、次回からはPythonとRを使って実際に分析してみましょう。
そちらも面白い分析ができそうです。

お楽しみに!!

追記

予定通り、PythonとRで実践記事を書きました。是非読んでみてください。
Python実践記事
www.randpy.tokyo
R実践記事
www.randpy.tokyo