PyTorch 1.0 Tutorials : テキスト : NLP 深層学習 (2) PyTorch で深層学習

PyTorch 1.0 Tutorials : テキスト : NLP 深層学習 (2) PyTorch で深層学習 (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 12/11/2018 (1.0.0.dev20181207)

* 本ページは、PyTorch 1.0 Tutorials : Text : Deep Learning for NLP with Pytorch : DEEP LEARNING WITH PYTORCH を
翻訳した上で適宜、補足説明したものです:

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

 

テキスト : NLP 深層学習 (2) PyTorch で深層学習

深層学習ビルディングブロック: アフィン写像、非線形性と目的(関数)

深層学習は線形性を非線形性と賢明な方法で組み合わせたものから成ります。非線形性の導入はパワフルなモデルを可能にします。このセクションでは、これらの中心的なコンポーネントと遊び、目的関数を作り上げ、そしてモデルがどのように訓練されるかを見ます。

 

アフィン写像

深層学習の中心的な workhorse (馬車馬) の一つはアフィン写像です、これは関数 $f(x)$ でここで行列 $A$ とベクトル $x$, $b$ について :
\[
f(x) = Ax + b
\]

ここで学習されるパラメータは $A$ と $b$ です。しばしば、$b$ はバイアス項として参照されます。
(訳注: アフィン写像は平行移動を伴う線型写像。)

PyTorch と他の深層学習フレームワークの殆どは伝統的な線形代数とは少し違うやり方をします。それは列の代わりに入力の行をマップします。つまり、下の出力の i 番目の行は $A$ のもとで入力の i 番目の行のマッピング、足すバイアス項です。下のサンプルを見てください。

# 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)
lin = nn.Linear(5, 3)  # maps from R^5 to R^3, parameters A, b
# data is 2x5.  A maps from 5 to 3... can we map "data" under A?
data = torch.randn(2, 5)
print(lin(data))  # yes
tensor([[ 0.1755, -0.3268, -0.5069],
        [-0.6602,  0.2260,  0.1089]], grad_fn=)

 

非線形性

最初に、以下の事実に注意してください、これは何故私達が非線形性をそもそも必要とするのかを説明するでしょう。2つのアフィン写像 $f(x)=Ax+b$ と $g(x)=Cx+d$ を持つとします。$f(g(x))$ は何でしょう?
\[
f(g(x)) = A(Cx + d) + b = ACx + (Ad + b)
\]

$AC$ は行列で $Ad+b$ はベクトルですので、アフィン写像の合成はアフィン写像を与えることを見ます。

このことから、もし貴方のニューラルネットワークにアフィン合成の長いチェインであることを望んだ場合、これは貴方のモデルに単に単一のアフィン写像を行なう以外の新しいパワーを追加しないことが見て取れます。

もしアフィン層の間に非線形性を導入すれば、これはもはや当てはまりません、そして遥かによりパワフルなモデルを構築することができます。

少数の中心的な非線形性があり、$\tanh(x), \sigma(x), \text{ReLU}(x)$ は最も一般的です。貴方はおそらく疑問に思うでしょう : 「何故これらの関数?私はたくさんの他の非線形性を考えられます。」この理由はそれらが計算するのが容易な勾配を持つためで、勾配の計算は学習に対して本質的です。例えば :
\[
\frac{d\sigma}{dx} = \sigma(x)(1 – \sigma(x))
\]

Quick Note: $\sigma(x)$ がデフォルト非線形であるような、AI クラスへのイントロで幾つかのニューラルネットワークを学習したかもしれませんが、典型的には人々は実際にはそれを避けます。これは引数の絶対値が増大するにつれて勾配が非常に速く消失するからです。小さい勾配は学習することが困難であることを意味します。多くの人々は tanh や ReLU をデフォルトにします。

# In pytorch, most non-linearities are in torch.functional (we have it imported as F)
# Note that non-linearites typically don't have parameters like affine maps do.
# That is, they don't have weights that are updated during training.
data = torch.randn(2, 2)
print(data)
print(F.relu(data))
tensor([[-0.5404, -2.2102],
        [ 2.1130, -0.0040]])
tensor([[0.0000, 0.0000],
        [2.1130, 0.0000]])

 

Softmax と確率

関数 $\text{Softmax}(x)$ はまた単なる非線形ですが、それは通常はネットワークで遂行される最後の演算であるという点で特別です。これは何故ならばそれは実数のベクトルを取って確率分布を返すからです。その定義は次のようなものです。$x$ を実数のベクトルとします (正、負、何でも、制約はありません)。そして $\text{Softmax}(x)$ の i 番目の要素は
\[
\frac{\exp(x_i)}{\sum_j \exp(x_j)}
\]

出力が確率分布であることは明瞭です : 各要素は非負で総ての要素に渡る総計は 1 です。

それを入力に element-wise なべき乗演算子を単に適用して総てを非負にしてから正規化定数で除算すると考えることもできます。

# Softmax is also in torch.nn.functional
data = torch.randn(5)
print(data)
print(F.softmax(data, dim=0))
print(F.softmax(data, dim=0).sum())  # Sums to 1 because it is a distribution!
print(F.log_softmax(data, dim=0))  # theres also log_softmax
tensor([ 1.3800, -1.3505,  0.3455,  0.5046,  1.8213])
tensor([0.2948, 0.0192, 0.1048, 0.1228, 0.4584])
tensor(1.)
tensor([-1.2214, -3.9519, -2.2560, -2.0969, -0.7801])

 

目的関数

目的関数はそれを最小化するために貴方のネットワークが訓練されている関数です (その場合それはしばしば損失関数やコスト関数と呼ばれます)。これは最初に訓練インスタンスを選択して、それを貴方のニューラルネットワークを通して実行して、そしてそれから出力の損失を計算することにより進みます。それからモデルのパラメータは損失関数の導関数を取ることにより更新されます。直感的には、貴方のモデルがその答えについて完全に確信していて、そしてその答えが間違っていれば、損失は高くなるでしょう。それがその答えについて非常に確信していて、その答えが正しい場合には、損失は低くなるでしょう。

貴方の訓練サンプル上で損失関数を最小化する背後にある考えは貴方のネットワークが望ましく上手く一般化されて貴方の dev セット、テストセット、または稼働中に見ていないサンプル上で小さい損失も持つであろうことです。サンプル損失関数は負の対数尤度損失で、これはマルチクラス分類のための非常に一般的な目的 (関数) です。教師ありマルチクラス分類については、これは正しい出力の負の対数確率を最小化する (あるいは、同値に、正しい出力の対数確率を最大化する) ためにネットワークを訓練することを意味します。

 

最適化と訓練

私達がインスタンスのために損失関数を計算できるとしてそれが何なのでしょう?それで私達は何を行なうのでしょう?それを計算するために使用されたものに関する勾配をどのように計算するかを Tensor は知っていることを先に見ました。そうですね、私達の損失は Tensor ですので、それを計算するために使用されたパラメータ総てに関する勾配を計算することができます!それから標準的な勾配更新を遂行できます。$\theta$ をパラメータ、$L(\theta)$ を損失関数、そして $\eta$ を正の学習率とします。すると :
\[
\theta^{(t+1)} = \theta^{(t)} – \eta \nabla_\theta L(\theta)
\]

この単なる vanilla 勾配更新以上の何かを行なう試みとしてアルゴリズムの膨大なコレクションと活発な研究があります。訓練時に起きることを基に学習率を多様化するための多くの試みです。これらのアルゴリズムが具体的に何をしているのかについては (貴方が本当に興味があるのでなければ) 心配する必要はありません。Torch は torch.optim パッケージで多くを提供します、そしてそれらは総て完全に透過です。最も単純な勾配更新を使用することはより複雑なアルゴリズムと同じです。異なる更新アルゴリズムと (異なる初期学習率のような) 更新アルゴリズムのための異なるパラメータを試すことは貴方のネットワークのパフォーマンスを最適化する点で重要です。しばしば、vanilla SGD を Adam や RMSProp のような optimizer と単に置き換えることはパフォーマンスを顕著にブーストします。

 

PyTorch でネットワーク・コンポーネントを作成する

私達の焦点 NLP に進む前に、アフィン写像と非線形性を使用して、PyTorch でネットワークを構築する注釈つきのサンプルを遂行します。私達はまた PyTorch の組み込み負の対数尤度を使用して損失関数をどのように計算するかを見ます、そして backpropagation でパラメータを更新します。

総てのネットワーク・コンポーネントは nn.Module から継承して forward() メソッドを override するべきです。ボイラープレートに関する限り、そんなところです。nn.Module からの継承は貴方のコンポーネントに機能を提供します。例えば、それはそれの訓練可能なパラメータを追跡させます、.to(device) メソッドで CPU と GPU 間でそれを swap できます、そこでは device は CPU デバイス torch.device(“cpu”) か CUDA デバイス torch.device(“cuda:0”) です。

ネットワークの注釈つきサンプルを書きましょう、これはスパース bag-of-words 表現を取って2つのラベル: 「英語」と「スペイン語」に渡る確率分布を出力します。このモデルは単なるロジスティック回帰です。

 

サンプル: ロジスティック回帰 Bag-of-Words 分類器

私達のモデルはスパース BoW 表現をラベルに渡る対数確率にマップするでしょう。語彙の各単語にインデックスを割り当てます。例えば、私達の全体の語彙が2つの単語 “hello” と “world”、それぞれインデックス 0 と 1 を持つとします。センテンス “hello hello hello hello” に対する BoW ベクトルは
\[
\left[ 4, 0 \right]
\]

“hello world world hello” に対しては、それは
\[
\left[ 2, 2 \right]
\]

etc. 一般に、それは
\[
\left[ \text{Count}(\text{hello}), \text{Count}(\text{world}) \right]
\]

この BOW ベクトルを x で表します。私達のネットワークの出力は :
\[
\log \text{Softmax}(Ax + b)
\]

つまり、アフィン写像を通して入力を渡してそしてそれから対数 softmax を行ないます。

data = [("me gusta comer en la cafeteria".split(), "SPANISH"),
        ("Give it to me".split(), "ENGLISH"),
        ("No creo que sea una buena idea".split(), "SPANISH"),
        ("No it is not a good idea to get lost at sea".split(), "ENGLISH")]

test_data = [("Yo creo que si".split(), "SPANISH"),
             ("it is lost on me".split(), "ENGLISH")]

# word_to_ix maps each word in the vocab to a unique integer, which will be its
# index into the Bag of words vector
word_to_ix = {}
for sent, _ in data + test_data:
    for word in sent:
        if word not in word_to_ix:
            word_to_ix[word] = len(word_to_ix)
print(word_to_ix)

VOCAB_SIZE = len(word_to_ix)
NUM_LABELS = 2


class BoWClassifier(nn.Module):  # inheriting from nn.Module!

    def __init__(self, num_labels, vocab_size):
        # calls the init function of nn.Module.  Dont get confused by syntax,
        # just always do it in an nn.Module
        super(BoWClassifier, self).__init__()

        # Define the parameters that you will need.  In this case, we need A and b,
        # the parameters of the affine mapping.
        # Torch defines nn.Linear(), which provides the affine map.
        # Make sure you understand why the input dimension is vocab_size
        # and the output is num_labels!
        self.linear = nn.Linear(vocab_size, num_labels)

        # NOTE! The non-linearity log softmax does not have parameters! So we don't need
        # to worry about that here

    def forward(self, bow_vec):
        # Pass the input through the linear layer,
        # then pass that through log_softmax.
        # Many non-linearities and other functions are in torch.nn.functional
        return F.log_softmax(self.linear(bow_vec), dim=1)


def make_bow_vector(sentence, word_to_ix):
    vec = torch.zeros(len(word_to_ix))
    for word in sentence:
        vec[word_to_ix[word]] += 1
    return vec.view(1, -1)


def make_target(label, label_to_ix):
    return torch.LongTensor([label_to_ix[label]])


model = BoWClassifier(NUM_LABELS, VOCAB_SIZE)

# the model knows its parameters.  The first output below is A, the second is b.
# Whenever you assign a component to a class variable in the __init__ function
# of a module, which was done with the line
# self.linear = nn.Linear(...)
# Then through some Python magic from the PyTorch devs, your module
# (in this case, BoWClassifier) will store knowledge of the nn.Linear's parameters
for param in model.parameters():
    print(param)

# To run the model, pass in a BoW vector
# Here we don't need to train, so the code is wrapped in torch.no_grad()
with torch.no_grad():
    sample = data[0]
    bow_vector = make_bow_vector(sample[0], word_to_ix)
    log_probs = model(bow_vector)
    print(log_probs)
{'me': 0, 'gusta': 1, 'comer': 2, 'en': 3, 'la': 4, 'cafeteria': 5, 'Give': 6, 'it': 7, 'to': 8, 'No': 9, 'creo': 10, 'que': 11, 'sea': 12, 'una': 13, 'buena': 14, 'idea': 15, 'is': 16, 'not': 17, 'a': 18, 'good': 19, 'get': 20, 'lost': 21, 'at': 22, 'Yo': 23, 'si': 24, 'on': 25}
Parameter containing:
tensor([[ 0.1194,  0.0609, -0.1268,  0.1274,  0.1191,  0.1739, -0.1099, -0.0323,
         -0.0038,  0.0286, -0.1488, -0.1392,  0.1067, -0.0460,  0.0958,  0.0112,
          0.0644,  0.0431,  0.0713,  0.0972, -0.1816,  0.0987, -0.1379, -0.1480,
          0.0119, -0.0334],
        [ 0.1152, -0.1136, -0.1743,  0.1427, -0.0291,  0.1103,  0.0630, -0.1471,
          0.0394,  0.0471, -0.1313, -0.0931,  0.0669,  0.0351, -0.0834, -0.0594,
          0.1796, -0.0363,  0.1106,  0.0849, -0.1268, -0.1668,  0.1882,  0.0102,
          0.1344,  0.0406]], requires_grad=True)
Parameter containing:
tensor([0.0631, 0.1465], requires_grad=True)
tensor([[-0.5378, -0.8771]])

 
上の値のどちらが英語の対数確率に相当して、どちらがスペイン語でしょう?それを定義していませんでしたが、物事を訓練することを望むのであれば必要です。

label_to_ix = {"SPANISH": 0, "ENGLISH": 1}

さて訓練しましょう!これを行なうために、対数確率を得るためにインスタンスを渡して、損失関数を計算して、損失関数の勾配を計算して、そしてそれから勾配ステップでパラメータを更新します。損失関数は nn パッケージで Torch により提供されます。nn.NLLLoss() が私達が望む負の対数尤度損失です。それはまた torch.optim で最適化関数も定義します。ここでは、SGD を単に使用します。

NLLLoss への入力は対数確率のベクトル、そしてターゲット・ラベルであることに注意してください。それは私達のために対数確率を計算しません。それがネットワークの最終層が log softmax である理由です。損失関数 nn.CrossEntropyLoss() は NLLLoss() と同じですが、それが貴方のために log softmax することを除いて。

# Run on test data before we train, just to see a before-and-after
with torch.no_grad():
    for instance, label in test_data:
        bow_vec = make_bow_vector(instance, word_to_ix)
        log_probs = model(bow_vec)
        print(log_probs)

# Print the matrix column corresponding to "creo"
print(next(model.parameters())[:, word_to_ix["creo"]])

loss_function = nn.NLLLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)

# Usually you want to pass over the training data several times.
# 100 is much bigger than on a real data set, but real datasets have more than
# two instances.  Usually, somewhere between 5 and 30 epochs is reasonable.
for epoch in range(100):
    for instance, label in data:
        # Step 1. Remember that PyTorch accumulates gradients.
        # We need to clear them out before each instance
        model.zero_grad()

        # Step 2. Make our BOW vector and also we must wrap the target in a
        # Tensor as an integer. For example, if the target is SPANISH, then
        # we wrap the integer 0. The loss function then knows that the 0th
        # element of the log probabilities is the log probability
        # corresponding to SPANISH
        bow_vec = make_bow_vector(instance, word_to_ix)
        target = make_target(label, label_to_ix)

        # Step 3. Run our forward pass.
        log_probs = model(bow_vec)

        # Step 4. Compute the loss, gradients, and update the parameters by
        # calling optimizer.step()
        loss = loss_function(log_probs, target)
        loss.backward()
        optimizer.step()

with torch.no_grad():
    for instance, label in test_data:
        bow_vec = make_bow_vector(instance, word_to_ix)
        log_probs = model(bow_vec)
        print(log_probs)

# Index corresponding to Spanish goes up, English goes down!
print(next(model.parameters())[:, word_to_ix["creo"]])
tensor([[-0.9297, -0.5020]])
tensor([[-0.6388, -0.7506]])
tensor([-0.1488, -0.1313], grad_fn=)
tensor([[-0.2093, -1.6669]])
tensor([[-2.5330, -0.0828]])
tensor([ 0.2803, -0.5605], grad_fn=)

私達は正しい答えを得ました!テストデータについて最初のサンプルではスペイン語に対する対数確率が遥かに高く、2番目では英語に対する対数確率が遥かに高いことが見て取れます、それがあるべきように。

さてどのように PyTorch コンポーネントを作成し、それを通してあるデータを渡して勾配更新を行なうかを見ました。深層 NLP が提供するものにより深く掘り進む準備ができました。

 
以上