PyTorch Tutorial : テキスト : NLP のための深層学習 : 単語埋め込み : 語彙セマンティクスのエンコード

PyTorch Tutorial : テキスト : NLP のための深層学習 : 単語埋め込み: 語彙セマンティクスのエンコード (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
更新日時 : 09/28/2018 (1.0.0.dev20180918)
作成日時 : 05/05/2018 (0.4.0)

* 1.0 dev でドキュメント構成が変更されました。幾つかチュートリアルが追加されて改訂もされていますので、必要に応じて再翻訳しています。
* 本ページは、PyTorch Tutorials の Deep Learning for NLP with Pytorch – Word Embeddings: Encoding Lexical Semantics を動作確認・翻訳した上で適宜、補足説明したものです:

* サンプルコードの動作確認はしておりますが、適宜、追加改変している場合もあります。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

本文

単語埋め込み (= Word embeddings) は実数の密ベクトルで、貴方の語彙の単語毎に 1 です。NLP では、貴方の特徴が単語である場合が殆どです!しかしコンピュータでどのように単語を表現するのでしょう?そのアスキーキャラクタ表現をストアできるでしょうが、それは単語が何であるかを伝えるだけで、それが何を意味するかについてはそれは殆ど何も言っていません (その接辞から音声の一部を、あるいはその大文字 (= capitalization) から特性を導出することができるかもしれません、しかし多くはありません)。更に、どのような意味でこれらの表現を結合できるのでしょう?私達はしばしばニューラルネットワークから密出力を望みます、そこでは入力は $|V|$ 次元で、ここで $V$ は語彙です、しかししばしば出力は少数の次元です (もし少数のラベルを予測するだけであれば、例えば)。どのように大規模な次元空間からより小さい次元空間に到達するのでしょう? 

アスキー表現の代わりに、one-hot エンコーディングを使用するのはどうでしょう?つまり、単語を次によって表します :

\[
\overbrace{\left[ 0, 0, \dots, 1, \dots, 0, 0 \right]}^\text{|V| elements}
\]

ここで 1 は $w$ に一意な位置にあります。任意の他の単語はある他の位置で 1 を持ち、他の総ては 0 です。

この表現については非常に大きな欠点があります、単にそれが巨大である他にも。それは基本的には総ての単語を互いに無関係に独立した存在として扱います。私達が本当に望むものは単語間の類似性の何某かの知見です。何故でしょう?例を見てみましょう。

私達は言語モデルを構築していると仮定します。私達の訓練データのセンテンスを見たことを仮定します :

  • The mathematician ran to the store.
  • The physicist ran to the store.
  • The mathematician solved the open problem.

今、訓練データで前に決して見ていない新しいセンテンスを得ると仮定します :

  • The physicist solved the open problem.

私達の言語モデルはこのセンテンス上で上手くやるかもしれませんが、次の2つの事実を使用して :

  • 私達は mathematician と physicist がセンテンスで同じ役割りを果たすことを見ました。それらはある程度セマンティック関係を持ちます。
  • physicist を見ているとき、この新しい見ていないセンテンスで同じ役割りで mathematician を見ています。

そしてそれから新しい見ていないセンテンスで physicist が実際に良い適合であると推論するほうが遥かに良いのではないでしょうか?これが類似性の知見で意味するところです : 類似する正書法的な表現を単純に持つことではありません。見たものと見ていないものの間のドットを結びつけることにより、言語のデータの希薄性 (= sparsity) と戦うことはテクニックです。このサンプルはもちろん基礎的な言語的仮定に依拠しています : それは類似のコンテキストに現れる単語は互いに意味論的に関係することです。これは分布仮説 (= distributional hypothesis) と呼ばれます。

 

密な単語埋め込みを得る

この問題をどのように解くことができるでしょう?つまり、単語のセマンティックな類似性をどのように実際にエンコードできるでしょう?多分何某かのセマンティック属性を考え出します。例えば、mathematicians と physicists の両者は run できることを見ますので、多分これらの単語に “is able to run” セマンティック属性のために高いスコアを与えます。他の属性を考えてください、そしてそれらの属性上で幾つかの一般的な単語にスコアをつけることを想像してください。

各属性が次元である場合、各単語にベクトルを与えられるでしょう、このようにです :

\[
q_\text{mathematician} = \left[ \overbrace{2.3}^\text{can run},
\overbrace{9.4}^\text{likes coffee}, \overbrace{-5.5}^\text{majored in Physics}, \dots \right]
\]

\[
q_\text{physicist} = \left[ \overbrace{2.5}^\text{can run},
\overbrace{9.1}^\text{likes coffee}, \overbrace{6.4}^\text{majored in Physics}, \dots \right]
\]

それからこれらの単語間の類似性の尺度を次により得ることができます :

\[
\text{Similarity}(\text{physicist}, \text{mathematician}) = q_\text{physicist} \cdot q_\text{mathematician}
\]

長さにより正規化することがより一般的ですけれども :

\[
\text{Similarity}(\text{physicist}, \text{mathematician}) = \frac{q_\text{physicist} \cdot q_\text{mathematician}}
{\| q_\text{\physicist} \| \| q_\text{mathematician} \|} = \cos (\phi)
\]

ここで $\phi$ は2つのベクトルの角度です。その方法では、極めて類似している単語 (同じ方向の埋め込みポイントを持つ単語) は類似性 1 を持つでしょう。極めて似ていない単語は類似性 -1 を持つはずです。

このセクションの冒頭からのスパース one-hot ベクトルを定義したこれらの新しいベクトルの特別なケースとして考えることができます、そこでは各単語は基本的に類似性 0 を持ち、そして私達はある一意なセマンティック属性を与えました。これらの新しいベクトルは密で、つまりそれらの項目は (典型的には) 非ゼロです。しかしこれらの新しいベクトルは大きな苦痛です : 類似性を決定することに関連するかもしれない異なるセマンティック属性の数千を考えるかもしれず、そして異なる属性の値をいったいどのように設定しますか?深層学習の中核となる考えは、ニューラルネットワークは特徴の表現をプログラマにそれらを自身で設計することを要求するよりも寧ろ学習することです。そこで単に単語埋め込みを私達のモデルのパラメータとして、そして訓練の間に更新されるように何故しないのでしょう?これは正確に私達が行なおうとすることです。ネットワークが原理的に学習できる何某かの隠れたセマンティック属性を持つでしょう。単語埋め込みはおそらく解釈可能ではないことに注意してください。つまり、上の私達の手製のベクトルによって mathematicians と physicists が両者がコーヒーを好きであるという点で類似していますが、もしニューラルネットワークに埋め込みを学習させて mathematicians と physicisits が 2 番目の次元で巨大な値を持つことを見る場合、それが何を意味するか明瞭ではありません。それらはある隠れたセマンティック属性で類似していても、これはおそらく私達にとって解釈を持ちません。

要約すれば、単語埋め込みは単語の *セマンティクス* の表現で、手元のタスクに関係あるかもしれないセマンティック情報を効率的にエンコードします。他のものも埋め込むことができます : 品詞タギング、解析木、何でもです!特徴埋め込みのアイデアはこの分野の中核です。

 

PyTorch の単語埋め込み

考案されたサンプルと課題に進む前に PyTorch と一般の深層学習プログラミングで埋め込みをどのように使用するかについての2, 3 の簡単なノートです。one-hot ベクトルを作成するときに各単語に一意なインデックスをどのように定義したかに類似して、埋め込みを使用するときにも各単語のためにインデックスを定義する必要があります。これらは検索テーブルへのキーとなります。つまり、インデックス $i$ が割り当てられた単語は行列の $i$ 番目の行にストアされたその埋め込みを持つように、埋め込みは $|V| \times D$ 行列にストアされます、ここで $D$ は埋め込みの次元です。私のコードの総てにおいて、単語からインデックスへのマッピングは word_to_ix という名前の辞書です。

埋め込みを可能にするモジュールは torch.nn.Embedding で、これは2つの引数を取ります : 語彙サイズと、埋め込みの次元です。

このテーブルにインデックスするためには、torch.LongTensor を使用しなければなりません (インデックスは整数であり、浮動小数点ではないため)。

# Author: Robert Guthrie

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

torch.manual_seed(1)
word_to_ix = {"hello": 0, "world": 1}
embeds = nn.Embedding(2, 5)  # 2 words in vocab, 5 dimensional embeddings
lookup_tensor = torch.tensor([word_to_ix["hello"]], dtype=torch.long)
hello_embed = embeds(lookup_tensor)
print(hello_embed)
tensor([[ 0.6614,  0.2669,  0.0617,  0.6213, -0.4519]])

 

サンプル: N-Gram 言語モデリング

n-gram 言語モデルでは、単語のシークエンス $w$ が与えられたとき、次を計算することを望むことを思い出してください :

\[
P(w_i | w_{i-1}, w_{i-2}, \dots, w_{i-n+1} )
\]

ここで $w_i$ はシークエンスの i 番目の単語です。

このサンプルでは、ある訓練サンプル上で損失関数を計算して backpropagation でパラメータを更新します。

CONTEXT_SIZE = 2
EMBEDDING_DIM = 10
# Shakespeare Sonnet 2 を使用します。
test_sentence = """When forty winters shall besiege thy brow,
And dig deep trenches in thy beauty's field,
Thy youth's proud livery so gazed on now,
Will be a totter'd weed of small worth held:
Then being asked, where all thy beauty lies,
Where all the treasure of thy lusty days;
To say, within thine own deep sunken eyes,
Were an all-eating shame, and thriftless praise.
How much more praise deserv'd thy beauty's use,
If thou couldst answer 'This fair child of mine
Shall sum my count, and make my old excuse,'
Proving his beauty by succession thine!
This were to be new made when thou art old,
And see thy blood warm when thou feel'st it cold.""".split()
# 入力をトークン化するべきですが、今はそれを無視します。
# タプルのリストを構築します。各タプルは ([ word_i-2, word_i-1 ], target word) です。
trigrams = [([test_sentence[i], test_sentence[i + 1]], test_sentence[i + 2])
            for i in range(len(test_sentence) - 2)]
# 最初の 3 つをプリントします、単にそれらがどのようなものか見れるようにです。
print(trigrams[:3])

vocab = set(test_sentence)
word_to_ix = {word: i for i, word in enumerate(vocab)}


class NGramLanguageModeler(nn.Module):

    def __init__(self, vocab_size, embedding_dim, context_size):
        super(NGramLanguageModeler, self).__init__()
        self.embeddings = nn.Embedding(vocab_size, embedding_dim)
        self.linear1 = nn.Linear(context_size * embedding_dim, 128)
        self.linear2 = nn.Linear(128, vocab_size)

    def forward(self, inputs):
        embeds = self.embeddings(inputs).view((1, -1))
        out = F.relu(self.linear1(embeds))
        out = self.linear2(out)
        log_probs = F.log_softmax(out, dim=1)
        return log_probs


losses = []
loss_function = nn.NLLLoss()
model = NGramLanguageModeler(len(vocab), EMBEDDING_DIM, CONTEXT_SIZE)
optimizer = optim.SGD(model.parameters(), lr=0.001)

for epoch in range(10):
    total_loss = torch.Tensor([0])
    for context, target in trigrams:

        # Step 1. Prepare the inputs to be passed to the model (i.e, turn the words
        # into integer indices and wrap them in variables)
        context_idxs = torch.tensor([word_to_ix[w] for w in context], dtype=torch.long)

        # Step 2. Recall that torch *accumulates* gradients. Before passing in a
        # new instance, you need to zero out the gradients from the old
        # instance
        model.zero_grad()

        # Step 3. Run the forward pass, getting log probabilities over next
        # words
        log_probs = model(context_idxs)

        # Step 4. Compute your loss function. (Again, Torch wants the target
        # word wrapped in a variable)
        loss = loss_function(log_probs, torch.tensor([word_to_ix[target]], dtype=torch.long))

        # Step 5. Do the backward pass and update the gradient
        loss.backward()
        optimizer.step()

        # Get the Python number from a 1-element Tensor by calling tensor.item()
        total_loss += loss.item()
    losses.append(total_loss)
print(losses)  # The loss decreased every iteration over the training data!

Out:

[(['When', 'forty'], 'winters'), (['forty', 'winters'], 'shall'), (['winters', 'shall'], 'besiege')]
[tensor([ 524.4060]), tensor([ 521.6964]), tensor([ 519.0042]), tensor([ 516.3311]), tensor([ 513.6758]), tensor([ 511.0352]), tensor([ 508.4101]), tensor([ 505.8000]), tensor([ 503.2033]), tensor([ 500.6176])]

 
 

課題: 単語埋め込みを計算する: Continuous Bag-of-Words

Continuous Bag-of-Words model (CBOW) は NLP 深層学習で頻繁に使用されます。それはターゲット単語の前の少数の単語と後の少数の単語が与えられたときに単語を予測しようとするモデルです。これは言語モデリングとは異なります、何故ならば CBOW はシーケンシャルではなくて確率的でなければならないということはありません。典型的には、CBOW は単語埋め込みを迅速に訓練するために使用されて、そしてこれらの埋め込みはあるより複雑なモデルの埋め込みを初期化するために使用されます。通常は、これは事前訓練された埋め込みとして参照されます。それは殆どいつもパフォーマンスを 2, 3 パーセント手助けします。

CBOW モデルは以下のようなものです。ターゲット単語 $w_i$ と各サイド上に N コンテキスト・ウィンドウ, $w_{i-1}, \dots, w_{i-N}$ と $w_{i+1}, \dots, w_{i+N}$ が与えられたとき、総てのコンテキスト単語をまとめて $C$ として参照し、CBOW はつぎを最小化しようとします :

\[
-\log p(w_i | C) = -\log \text{Softmax}(A(\sum_{w \in C} q_w) + b)
\]

ここで $q_w$ は単語 $w$ に対する埋め込みです。

下のクラスを埋めることにより PyTorch でこのモデルを実装します。幾つかのティップスです :

  • どのパラメータを定義する必要があるかについて考えます。
  • 各演算子が何の shape を想定するかを知っていることを確実にしてください。reshape が必要であれば .view を使用してください。
CONTEXT_SIZE = 2  # 2 words to the left, 2 to the right
raw_text = """We are about to study the idea of a computational process.
Computational processes are abstract beings that inhabit computers.
As they evolve, processes manipulate other abstract things called data.
The evolution of a process is directed by a pattern of rules
called a program. People create programs to direct processes. In effect,
we conjure the spirits of the computer with our spells.""".split()

# By deriving a set from `raw_text`, we deduplicate the array
vocab = set(raw_text)
vocab_size = len(vocab)

word_to_ix = {word: i for i, word in enumerate(vocab)}
data = []
for i in range(2, len(raw_text) - 2):
    context = [raw_text[i - 2], raw_text[i - 1],
               raw_text[i + 1], raw_text[i + 2]]
    target = raw_text[i]
    data.append((context, target))
print(data[:5])


class CBOW(nn.Module):

    def __init__(self):
        pass

    def forward(self, inputs):
        pass

# create your model and train.  here are some functions to help you make
# the data ready for use by your module


def make_context_vector(context, word_to_ix):
    idxs = [word_to_ix[w] for w in context]
    return torch.tensor(idxs, dtype=torch.long)


make_context_vector(data[0][0], word_to_ix)  # example

Out:

[(['We', 'are', 'to', 'study'], 'about'), (['are', 'about', 'study', 'the'], 'to'), (['about', 'to', 'the', 'idea'], 'study'), (['to', 'study', 'idea', 'of'], 'the'), (['study', 'the', 'of', 'a'], 'idea')]

 

 

以上