TensorFlowのチュートリアル(Vector Representations of Words)
https://www.tensorflow.org/versions/master/tutorials/word2vec/index.html
の翻訳です。
翻訳の誤りなどあればご指摘お待ちしております。
このチュートリアルでは、Mikolov et al. の word2vec モデルを見ていきます。このモデルは、「単語埋め込み」と呼ばれる単語のベクトル表現の学習に使用されます。
##ハイライト
このチュートリアルは、 TensorFlow で word2vec モデルを構築する、興味深く、実質的な部分を強調することを目的としています。
- 単語をベクトル表現する動機を説明することから始めます
- モデルの背後にある直感と、モデルが(良い物差しとして数学を派手に使うことにより)訓練される方法を見ていきます
- また、 TensorFlow によるモデルの単純な実装を示します
- 最後に、より良くスケールする素朴なバージョンを作る方法を示します
チュートリアルの後半でコードを扱いますが、直接始めることをを好む場合は、 tensorflow/examples/tutorials/word2vec/word2vec_basic.py の最小限の実装を見て下さい。この基本的な例は、若干のデータをダウンロードして、そのデータにより少し訓練し、結果を視覚化するために必要なコードを含みます。基本的なバージョンを読み、実行することに慣れたら、 tensorflow/models/embedding/word2vec.py に移ることができます。これはより重たい実装であり、テキスト・モデルに効率的にデータを移動するためにスレッドを使用する方法、訓練中にチェックポイントを作る方法など、より高度ないくつかの TensorFlow の原理を示します。
しかし、まずは、単語の埋め込みを訓練する理由を見ていきましょう。埋め込みのプロの方は、このセクションをスキップし、詳細に入って構いません。
##動機:なぜ単語の埋め込みを学習するのか?
画像または音声処理システムは、画像データの個々の生のピクセル強度のベクトルとして、あるいは例えばオーディオデータのパワースペクトル密度係数のベクトルとして符号化された、リッチで、高次元のデータセットで動作します。物体認識や音声認識などのタスクにおいて、(人は生データからこれらのタスクを実行することが可能なため)首尾よくタスクを実行するために必要なすべての情報がデータに符号化されていることが分かります。しかしながら、自然言語処理システムでは、伝統的に単語を個別のアトミックな記号として扱い、たとえば「猫」を「Id537」で、「犬」を「Id143」で表します。これらの符号化は恣意的であり、個々の記号の間に存在するかもしれない関係に関して、有益な情報をシステムに提供しません。つまり、モデルが「猫」について学習した内容を、「犬」についてのデータ処理に活用することはほとんどできません(たとえば、どちらも動物、4本足、ペット、…など)。さらに、言葉を一意で離散的なIDで表現することは、疎なデータにつながり、大抵、統計モデルをうまく訓練するためにはより多くのデータを必要とすることを意味します。ベクトル表現を使用すると、これらの障害のいくつかを克服することができます。
ベクトル空間モデル(VSM)は、意味論的に類似した単語が近くの点にマップされる(『点は互いに近くに埋め込まれる』)、連続したベクトル空間における(埋め込まれた)単語を表現します。 NLPにおいて、VSMは長く豊かな歴史を持っていますが、すべてのメソッドは、分布仮説(Distributional Hypothesis)にさまざまな方法で依存します。分布仮説とは、同じ文脈に現れる単語は意味論的意味を共有する、というものです。この原理を活用するアプローチは、2つのカテゴリーに分けることができます:カウントベースの方法(例えば、潜在的意味解析)、および予測的方法(例えば、ニューラル確率的言語モデル)です。
この区別はBaroni et al.によってより詳細に述べられていますが、一言でいえば:カウントベースの方法とは、単語が大規模テキスト・コーパスにおけるその隣接語と共起する頻度の統計を算出し、これらのカウント統計を、各単語の小さく密なベクトルにマップする方法で、予測モデルは、学習済みの小さく密な埋め込みベクトル(モデルのパラメータと見なされる)を用いて、直接隣接語から単語を予測することを試みます。
Word2vec は生のテキストから単語の埋め込みを学習するための、特に計算効率の高い予測モデルです。それには、2つの種類があります、連続単語集合モデル(CBOW)とスキップグラム・モデルです。アルゴリズム的にこれらのモデルは似ていますが、CBOWは元の文脈単語(『猫が座る』)からターゲット単語(例えば、『マット』)を予測し、スキップグラムは逆に、ターゲット単語から元の文脈単語を予測します。この反転は、任意選択のように思えるかもしれませんが、統計学的には、CBOWは(文脈全体を一つの観測値として扱うことにより)多くの分布情報を平滑化するという効果があります。ほとんどの場合、CBOWは、より小さなデータセットで有用であることが判明しました。一方、スキップグラムは各文脈・ターゲット単語のペアを新たな観測値として扱い、これはより大きなデータセットの場合により良い傾向にあります。以後、このチュートリアルではスキップグラム・モデルに焦点を当てます。
##ノイズ対照訓練によるスケールアップ
ニューラル確率的言語モデルは、伝統的に、すでに現れた複数の単語 $h$ (「history」の h )から、次の単語 $w_t$ (「target」の t )の確率をソフトマックス関数に関して最尤(ML)法を使用して訓練します。
P(w_t | h) = \text{softmax}(\text{score}(w_t, h)) \\
= \frac{\exp\{\text{score}(w_t, h)\}}
{\sum_\text{Word w' in Vocab} \exp \{ \text{score}(w', h) \} }.
ここで、 $\text{score}(w_t,h)$ は単語 $w_t$ と文脈 $h$ の相性を(一般的には内積により)計算します。訓練セットの対数尤度を最大化することで、このモデルを訓練します。すなわち、以下を最大化します。
J_\text{ML} = \log P(w_t | h) \\
= \text{score}(w_t, h) -
\log \left( \sum_\text{Word w' in Vocab} \exp \{ \text{score}(w', h) \} \right)
これは、言語モデリングのために適切に正規化された確率モデルを生成します。しかし、各学習ステップにおいて、現在の文脈 $h$ 内の他のすべての $V$ 個の単語 $w'$ のスコアを用いて各確率を計算し、正規化する必要があるため、これは非常にコストがかかります。
一方、 word2vec による特徴学習には、完全な確率モデルは必要としません。 CBOWとスキップグラム・モデルは代わりに、同じ文脈における $k$ 個の仮想の(ノイズ)単語 $\tilde w$ から、実際の対象単語 $w_t$ を識別するために、二値分類法(ロジスティック回帰)を使用して訓練されます。CBOWモデルについてこれを説明します。スキップグラムの場合、方向が単純に反転されます。
数学的には、(それぞれの例について)目的は、以下を最大化することです
J_\text{NEG} = \log Q_\theta(D=1 |w_t, h) +
k \mathop{\mathbb{E}}_{\tilde w \sim P_\text{noise}}
\left[ \log Q_\theta(D = 0 |\tilde w, h) \right]
ここで、 $Q_\theta(D=1 | w, h)$ は、モデルの下でデータセット $D$ の中の文脈 $h$ において単語 $w$ が出現する確率を、学習した埋め込みベクトル $\theta$ に関して計算したものです。実際にはノイズ分布から $k$ 対照単語を振り出すことにより期待値を近似します(すなわちモンテカルロ平均を計算)。
モデルが真の単語に高い確率を、偽の単語に低い確率を割り当てるとき、この目的関数は最大になります。技術的には、これは負例サンプリングと呼ばれ、この損失関数を使用する、良い数学的な動機があります:この方法が提案する更新は極限においてソフトマックス関数の更新に漸近します。しかし、損失関数の計算は、選択した $k$ 個のノイズ単語の数に比例し、語彙の全単語 $V$ には比例しないため、計算上特に魅力的です。これは、訓練をはるかに高速にします。実際には、非常によく似たノイズ対照評価(NCE)損失を使用します。 TensorFlow は便利なヘルパー関数 tf.nn.nce_loss() を持っています。
では、これが実際にどのように動作するか実感してみましょう!
##スキップグラム・モデル
例として、以下のデータセットを考えます。
the quick brown fox jumped over the lazy dog
まず、単語とそれらが現れる文脈のデータセットを形成します。意味のあるどのような方法でも「文脈」を定義することができます。実際、構文上の文脈(すなわち、現在ターゲットとする単語の構文上の依存性、Levy et al.を参照)、ターゲットの左側にある複数の単語、ターゲットの右側にある複数の単語などが検討されてきました。ここでは単純な定義を採用し、「文脈」をターゲット語の左右にある複数の単語の窓と定義しましょう。窓サイズ1を使用すると、以下の (文脈, ターゲット) ペアのデータセットを得ます。
([the, brown], quick), ([quick, fox], brown), ([brown, jumped], fox), ...
スキップグラムは文脈とターゲットを反転させ、ターゲット語から各文脈語の予測を試みることを思い出してください。そのためタスクは、「quick」から「the」や「brown」を、「brown」から「quick」や「fox」を、予測する、という風になります。従ってデータセットは以下の (入力, 出力) ペアになります。
(quick, the), (quick, brown), (brown, quick), (brown, fox), ...
目的関数はデータセット全体にわたって定義されていますが、一般的には、一度に1つのサンプル(または batch_size 個のサンプルの「ミニバッチ」、ここで一般に 16 <= batch_size <= 512)を使用して確率的勾配降下(SGD)でこれを最適化します。それでは、このプロセスの1ステップを見てみましょう。
訓練ステップ $t$ で上記の最初の訓練ケースを観察すると想像しましょう、ここでの目標は「quick」から「the」を予測することです。いくつかのノイズ分布、通常はユニグラム分布、 $P(w)$ から振り出すことにより、num_noise 個のノイズ(対照)サンプルを選択します。簡単にするため num_noise=1 とし、ノイズ・サンプルとして「sheep」を選択しましょう。次に、この観測されたサンプルとノイズ・サンプルのペアの損失を計算しましょう、すなわち、ステップ $t$ における目的関数は、以下になります。
J^{(t)}_\text{NEG} = \log Q_\theta(D=1 | \text{the, quick}) +
\log(Q_\theta(D=0 | \text{sheep, quick}))
目標は、この目的関数を改善(この場合、最大化)するために、埋め込みパラメータ $\theta$ を更新することです。埋め込みパラメータ $\theta$ に対する損失の勾配、すなわち $\frac{\partial}{\partial \theta} J_\text{NEG}$ を導出することにより、これを行います(幸運にも TensorFlow ではこれを行うための簡単なヘルパー関数が提供されています)。それから、勾配の方向に小さなステップを取ることで埋め込みの更新を行います。このプロセスを全訓練セットにわたって繰り返すと、モデルが実際の単語をノイズ単語から識別することに成功するまで、各単語について埋め込みベクトルを「移動する」効果があります。
例えばt-SNE次元削減法などを使用して2次元に射影することにより、学習されたベクトルを可視化することができます。これらの可視化を精査すると、ベクトルは、単語および単語相互の関係に関する、一般的かつ実際に非常に有用な、意味情報を取り込むことが明らかになりました。誘導されたベクトル空間内の特定の方向は、例えば、次の図に示すように、男女のジェンダーや、国と首都の単語の関係など、特定の意味関係に特化していることを最初に発見したとき、非常に興味深かったです(また、Mikolov et al., 2013参照)。
これは、品詞タグ付けや固有表現認識など、多くの標準的なNLP予測タスクにおける特徴量として、これらのベクトルが有用である理由を説明しています(Collobert et al., 2011 (pdf)によるオリジナルの研究や Turian et al., 2010 によるフォローアップを参照)。
しかし、ここでは、単にきれいな絵を描くためにそれらを使用しましょう!
##グラフの構築
これが、埋め込みについてのすべてです。では、埋め込み行列を定義してみましょう。これは、最初、大きなランダム行列です。一様乱数で値を初期化します。
embeddings = tf.Variable(
tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))
ノイズ対照評価の損失をロジスティック回帰モデルについて定義します。このため、語彙内の各単語についての重みとバイアスを定義する必要があります(「入力の埋め込み」に対比して、「出力重み」と呼ばれます)。では、それを定義しましょう。
nce_weights = tf.Variable(
tf.truncated_normal([vocabulary_size, embedding_size],
stddev=1.0 / math.sqrt(embedding_size)))
nce_biases = tf.Variable(tf.zeros([vocabulary_size]))
パラメータを定義したため、スキップグラム・モデルのグラフを定義することができます。簡単にするために、語彙の各単語が整数として表現され、テキスト・コーパスがすでに整数化されているとしましょう(詳細についてはtensorflow/https://www.tensorflow.org/versions/master/tutorials/word2vec/index.html/tutorials/word2vec/word2vec_basic.py を参照)。スキップグラム・モデルは、2つの入力を取ります。1つはソース文脈単語を表す整数で満たされたバッチで、もう1つはターゲット単語のためのものです。後でデータを送り込むことができるように、これらの入力のプレースホルダ―・ノードを作成しましょう。
# Placeholders for inputs
train_inputs = tf.placeholder(tf.int32, shape=[batch_size])
train_labels = tf.placeholder(tf.int32, shape=[batch_size, 1])
ここで必要なことは、バッチ内の各ソース単語について、ベクトルをルックアップすることです。 TensorFlow には、これを簡単に行うための便利なヘルパーがあります。
embed = tf.nn.embedding_lookup(embeddings, train_inputs)
OK、各単語の埋め込みが得られたので、ノイズ対照訓練の目的関数を使用してターゲット単語の予測を試みましょう。
# Compute the NCE loss, using a sample of the negative labels each time.
loss = tf.reduce_mean(
tf.nn.nce_loss(nce_weights, nce_biases, embed, train_labels,
num_sampled, vocabulary_size))
損失のノードが得られたので、勾配を計算しパラメータを更新するなどのノードを追加する必要があります。このために確率的勾配降下を使用しますが、 TensorFlow にはまた、これを簡単に行うための便利なヘルパーがあります。
# We use the SGD optimizer.
optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0).minimize(loss)
##モデルの訓練
モデルの訓練は簡単で、 feed_dict を使用してプレースホルダーにデータをプッシュし、ループ内でこの新しいデータとともに session.run を呼び出すだけです。
for inputs, labels in generate_batch(...):
feed_dict = {training_inputs: inputs, training_labels: labels}
_, cur_loss = session.run([optimizer, loss], feed_dict=feed_dict)
完全なコード例は tensorflow/g3doc/tutorials/word2vec/word2vec_basic.py を参照してください。
##学習済み埋め込みの可視化
訓練後、t-SNEを使用して学習済み埋め込みを可視化することができます。
出来上がり!予想されたように、類似した単語は、最終的に互いに近くで集団(クラスター)を形成します。TensorFlow の高度な特徴をもっと示す、より重量級の word2vec の実装については、 tensorflow/models/embedding/word2vec.py の実装を参照してください。
##埋め込みの評価:アナロジー推論
埋め込みは、NLPのさまざまな予測タスクにおいて有用です。本格的な品詞モデルまたは固有表現モデルの訓練の不足、埋め込みを評価する1つの簡単な方法は、直接、「king に対する queen は father に対する何?」のような、構文的あるいは意味的関係を予測するためにそれらを使用することです。これはアナロジー推論と呼ばれ、そのタスクはMikolovらによって導入され、データセットは、ここからダウンロードできます:
https://word2vec.googlecode.com/svn/trunk/questions-words.txt
これを評価する方法については、 tensorflow/models/embedding/word2vec.py の build_eval_graph() と eval() 関数を参照してください。
ハイパーパラメータの選択はこのタスクの精度に強い影響を与えます。このタスクにおいてステート・オブ・ジ・アートな性能を達成するためには、非常に大規模なデータセットで訓練し、ハイパーパラメータを慎重にチューニングし、このチュートリアルの範囲外であるデータのサブサンプリングのようなトリックを使う必要があります。
##実装の最適化
この単純な実装は TensorFlow の柔軟性をショーケース化しています。例えば、訓練目的の変更は簡単で、 tf.nn.nce_loss() の呼び出しを、 tf.nn.sampled_softmax_loss() のような既製の代替に交換するだけです。損失関数について新しいアイデアがある場合、 TensorFlow において手動で新たな目的の式を記述し、オプティマイザにその導関数を計算させることができます。機械学習のモデル開発における探索段階において、いくつかのさまざまなアイデアを試し、迅速に反復するためには、この柔軟性は非常に貴重です。
満足なモデル構造が得られたら、より効率的に実行するために(およびより少ない時間でより多くのデータをカバーするために)、実装を最適化する価値があるでしょう。例えば、 TensorFlow バックエンドにはほとんど作業を必要としない、データ項目を読んで供給するために Python を使用しているため、このチュートリアルの素朴なコードはスピードが損なわれます。モデルにおいて入力データに深刻なボトルネックが見つかった場合は、新規データ・フォーマットで説明したように、問題のためにカスタム・データ・リーダーを実装することもできます。スキップグラム・モデルの場合、例として tensorflow/models/embedding/word2vec.py で実際にすでにこれを行っています。
モデルがもはやI/O律速ではなく、まだそれ以上の性能が必要な場合、新規操作の追加で説明したように、独自の TensorFlow 操作を書くことによって、さらに性能を得ることができます。再び、スキップグラムの場合の例として、 tensorflow/models/embedding/word2vec_optimized.py を提供しています。各段階でのパフォーマンスの向上を測定するために、互いに対してベンチマークしてみてください。
##結論
このチュートリアルでは、 word2vec モデル、単語の埋め込みを学習するための計算上効率的なモデルをカバーしました。埋め込みが有用である理由を動機付け、効率的なトレーニング手法を議論し、 TensorFlow でこれらすべてを実装する方法を示しました。全体として、初期の実験のために必要な柔軟性、およびその後のオーダーメイドの最適化された実装に必要な制御を、 TensorFlow がどのように提供しているかについて、ショーケース化できたことを願っています。