PyTorch : Tutorial 初級 : NLP のための深層学習 : PyTorch で深層学習

PyTorch : Tutorial 初級 : NLP のための深層学習 : PyTorch で深層学習 (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
更新日時 : 09/08/2018
作成日時 : 05/02/2018 (0.4.0)

* 本ページは、PyTorch Tutorials の Deep Learning for NLP with Pytorch – Deep Learning with PyTorch
を動作確認・翻訳した上で適宜、補足説明したものです:

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

 

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

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

 

アフィン写像

深層学習の中心的な 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]])

 

非線形性

最初に、以下の事実に注意してください、これは何故私達が非線形性をそもそも必要とするのかを説明するでしょう。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 なべき乗演算子を単に適用して総てを非負にしてから正規化定数で除算すると考えることもできます。

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 は語彙の各単語を一意の整数にマップします、
# これは Bag of words ベクトルへのそのインデックスとなります。
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):  # nn.Module から継承します!

    def __init__(self, num_labels, vocab_size):
        # nn.Module の init 関数を呼び出します。
        # Dont get confused by syntax, just always do it in an nn.Module
        super(BoWClassifier, self).__init__()

        # 貴方が必要なパラメータを定義します。
        # この場合、A と b、アフィン写像のパラメータが必要です。
        # Torch は nn.Linear() を定義します、これはアフィン写像を提供します。
        # 何故入力次元が vocab_size  で出力が num_labels なのか理解を確実にしてください!
        self.linear = nn.Linear(vocab_size, num_labels)

        # NOTE! 非線形 log softmax はパラメータを持ちません!
        # 従ってここではそれについて心配する必要はありません。

    def forward(self, bow_vec):
        # 線形層を通して入力を渡します、
        # それから log_softmax を通してそれを渡します。
        # 多くの非線形性と他の関数は 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)

# model はそのパラメータを知っています。下の最初の出力が A で、2番目が b です。
# module の __init__ 関数でクラス変数にコンポーネントを割り当てるときはいつでも、
# それは self.linear = nn.Linear(...) の行で成されました。
# それから PyTorch devs からのある Python マジックを通して、
# 貴方の module (この場合、BoWClassifier) は nn.Linear のパラメータの知識をストアするでしょう。
for param in model.parameters():
    print(param)

# model を実行するためには、BoW ベクトルを渡します。
# ここでは訓練する必要がありませんので、コードは 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]])
Parameter containing:
tensor([ 0.0631,  0.1465])
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 することを除いて。

# 訓練の前にテストデータ上で実行します、単に前後を見るためです。
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)

# "creo" に対応する行列カラムをプリントします。
print(next(model.parameters())[:, word_to_ix["creo"]])

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

# 通常は訓練データに渡り何回か渡すことを望むでしょう。
# 100 は現実のデータセット上では非常に大きいですが、
# 現実のデータセットは 2 インスタンス以上を持ちます。
# 通常は、5 と 30 epochs の間のどこかが合理的です。
for epoch in range(100):
    for instance, label in data:
        # Step 1. PyTorch は勾配を累積することを覚えていてください。
        # 各インスタンスの前にそれらをクリアする必要があります。
        model.zero_grad()

        # Step 2. BOW ベクトルを作成します、
        # そしてまたターゲットを Tensor に整数としてラップしなければなりません。
        # 例えば、ターゲットがスペイン語であれば、整数 0 をラップします。
        # それで損失関数は対数確率の 0th 要素がスペイン語に対応する対数確率であることを知ります。
        bow_vec = make_bow_vector(instance, word_to_ix)
        target = make_target(label, label_to_ix)

        # Step 3. forward パスを実行します。
        log_probs = model(bow_vec)

        # Step 4. 損失、勾配を計算して、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])
tensor([[-0.2093, -1.6669]])
tensor([[-2.5330, -0.0828]])
tensor([ 0.2803, -0.5605])

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

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

 

以上