PyTorch : Tutorial 初級 : NLP のための深層学習 : シーケンスモデルと Long-Short Term Memory ネットワーク (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 05/11/2018 (0.4.0)
* 本ページは、PyTorch Tutorials の Deep Learning for NLP with Pytorch – Sequence Models and Long-Short Term Memory Networks を動作確認・翻訳した上で適宜、補足説明したものです:
* サンプルコードの動作確認はしておりますが、適宜、追加改変している場合もあります。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
この時点で、様々な feed-forward ネットワークを見てきました。つまり、ネットワークにより維持される状態はまったくありません。これは私達が望む挙動ではないかもしれません。シーケンスモデルは NLP の中核を成します : それらはそこでは貴方の入力間の時間を通してある種の依存があるモデルです。シーケンスモデルの古典的な例は品詞タギングのための隠れマルコフモデルです。もう一つの例は条件付き確率場 (= CRF, conditional random field) です。
リカレント・ニューラルネットワークはある種の状態を維持するネットワークです。例えば、その出力は次の入力の一部として使用できるでしょう、その結果、情報はネットワークがシークエンスを渡すときに伝播します。LSTM の場合には、シークエンスの各要素のために、対応する隠れ状態 $h_t$ があります、これは原理的にはシークエンスの前の任意のポイントからの情報を含むことができます。言語モデルで単語を予測したり、品詞タグ付け、そして無数の他のことに隠れ状態を使用できます。
Pytorch の LSTM
サンプルに進む前に、2, 3 のことを書き留めます。PyTorch の LSTM はその入力の総てに 3D tensor であることを想定します。これらの tensor の軸のセマンティクスは重要です。最初の軸はシークエンス自身で、2 番目はミニバッチのインスタンスをインデックスし、そして 3 番目は入力の要素をインデックスします。ミニバッチ処理は議論していませんので、それは単に無視して 2 番目の軸上では 1 次元だけを常に持つことを仮定しましょう。センテンス “The cow jumped” に渡るシーケンスモデルを実行することを望む場合、私達の入力は次のように見えるはずです :
\[
\begin{split}\begin{bmatrix}
\overbrace{q_\text{The}}^\text{row vector} \\
q_\text{cow} \\
q_\text{jumped}
\end{bmatrix}\end{split}
\]
追加のサイズ 1 の 2 番目の次元があることを覚えておくことを除いてです。
加えて、シークエンスを一つずつ調べることができて、その場合には最初の軸もまたサイズ 1 を持ちます。
素早い (= quick) サンプルを見てみましょう。
# 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)
lstm = nn.LSTM(3, 3) # 入力 dim が 3 で、出力 dim が 3 です。 inputs = [torch.randn(1, 3) for _ in range(5)] # 長さ 5 のシークエンスを作成します。 # 隠れ状態を初期化します。 hidden = (torch.randn(1, 1, 3), torch.randn(1, 1, 3)) for i in inputs: # シークエンスを一度に 1 要素 step through します。 # 各ステップ後、hidden は隠れ状態を含みます。 out, hidden = lstm(i.view(1, 1, -1), hidden) # 代わりに、シークエンス全体を総て一度に行なうことができます。 # LSTM から返される最初の値はシークエンスを通した隠れ状態の総てです。 # 2 番目は単に最も最近の隠れ状態です。 # (下の "out" の最後のスライスを "hidden" と比べてください、それらは同じです) # この理由は : # "out" はシークエンスの総ての隠れ状態へのアクセスを与えます。 # "hidden" はシークエンスを続けて逆伝播を続けることを可能にします、 # 後でそれを lstm への引数として渡すことにより。 # Add the extra 2nd dimension inputs = torch.cat(inputs).view(len(inputs), 1, -1) hidden = (torch.randn(1, 1, 3), torch.randn(1, 1, 3)) # clean out hidden state out, hidden = lstm(inputs, hidden) print(out) print(hidden)
Out :
tensor([[[-0.0187, 0.1713, -0.2944]], [[-0.3521, 0.1026, -0.2971]], [[-0.3191, 0.0781, -0.1957]], [[-0.1634, 0.0941, -0.1637]], [[-0.3368, 0.0959, -0.0538]]]) (tensor([[[-0.3368, 0.0959, -0.0538]]]), tensor([[[-0.9825, 0.4715, -0.0633]]]))
サンプル: 品詞タギングのための LSTM
このセクションでは、品詞タグを得るために LSTM を使用します。ビタビ (= Viterbi) (アルゴリズム) や Forward-Backward あるいはそのようなものは使用しませんが、読者への (挑戦的な) 課題として何が起きているのかを見た後でビタビがどのように使用されるかを考えてください。
モデルは次のようなものです : 入力センテンスを $w_1, \dots, w_M$ とします、ここで $w_i \in V$、私達の語彙です。また、$T$ をタグセット、そして $y_i$ を単語 $w_i$ のタグとします。単語 $w_i$ のタグの予測を $\hat{y}_i$ で表します。
これは構造予測、モデル、ここで出力はシーケンス $\hat{y}_1, \dots, \hat{y}_M$、ここで $\hat{y}_i \in T$ です。
予測を行なうために、センテンスに渡り LSTM を通過させます。タイムスタンプ $i$ における隠れ状態を $h_i$ として表します。また、各タグに一意なインデックスを割り当てます (単語埋め込みセクションで word_to_ix をどのように持ったかのように)。すると $\hat{y}_i$ のための予測ルールは
\[
\hat{y}_i = \text{argmax}_j \ (\log \text{Softmax}(Ah_i + b))_j
\]
つまり、隠れ状態のアフィン写像の log softmax を取り、そして予測タグはこのベクトルで最大値を持つタグです。これは $A$ のターゲット空間の次元は $|T|$ であることを直ちに包含していることに注意してください。
データを準備します :
def prepare_sequence(seq, to_ix): idxs = [to_ix[w] for w in seq] return torch.tensor(idxs, dtype=torch.long) training_data = [ ("The dog ate the apple".split(), ["DET", "NN", "V", "DET", "NN"]), ("Everybody read that book".split(), ["NN", "V", "DET", "NN"]) ] word_to_ix = {} for sent, tags in training_data: for word in sent: if word not in word_to_ix: word_to_ix[word] = len(word_to_ix) print(word_to_ix) tag_to_ix = {"DET": 0, "NN": 1, "V": 2} # These will usually be more like 32 or 64 dimensional. # We will keep them small, so we can see how the weights change as we train. EMBEDDING_DIM = 6 HIDDEN_DIM = 6
Out :
{'The': 0, 'dog': 1, 'ate': 2, 'the': 3, 'apple': 4, 'Everybody': 5, 'read': 6, 'that': 7, 'book': 8}
モデルを作成します :
class LSTMTagger(nn.Module): def __init__(self, embedding_dim, hidden_dim, vocab_size, tagset_size): super(LSTMTagger, self).__init__() self.hidden_dim = hidden_dim self.word_embeddings = nn.Embedding(vocab_size, embedding_dim) # LSTM は引数として word embeddings を取り、そして # 次元 hidden_dim を持つ隠れ状態を出力します。 self.lstm = nn.LSTM(embedding_dim, hidden_dim) # 隠れ状態空間からタグ空間へマップする線形層 self.hidden2tag = nn.Linear(hidden_dim, tagset_size) self.hidden = self.init_hidden() def init_hidden(self): # 何かをなす前には、どのような隠れ状態も持ちません。 # Refer to the Pytorch documentation to see exactly # why they have this dimensionality. # The axes semantics are (num_layers, minibatch_size, hidden_dim) return (torch.zeros(1, 1, self.hidden_dim), torch.zeros(1, 1, self.hidden_dim)) def forward(self, sentence): embeds = self.word_embeddings(sentence) lstm_out, self.hidden = self.lstm( embeds.view(len(sentence), 1, -1), self.hidden) tag_space = self.hidden2tag(lstm_out.view(len(sentence), -1)) tag_scores = F.log_softmax(tag_space, dim=1) return tag_scores
モデルを訓練します :
model = LSTMTagger(EMBEDDING_DIM, HIDDEN_DIM, len(word_to_ix), len(tag_to_ix)) loss_function = nn.NLLLoss() optimizer = optim.SGD(model.parameters(), lr=0.1) # 訓練前にスコアが何であるかを見ます。 # 出力の要素 i,j は単語 i のためのタグ j のためのスコアであることに注意してください。 # ここでは訓練は必要ないので、コードは torch.no_grad() でラップされます。 with torch.no_grad(): inputs = prepare_sequence(training_data[0][0], word_to_ix) tag_scores = model(inputs) print(tag_scores) for epoch in range(300): # again, normally you would NOT do 300 epochs, it is toy data for sentence, tags in training_data: # Step 1. PyTorch は勾配を累積することを忘れないでください。 # 各インスタンスの前にそれらをクリアする必要があります。 model.zero_grad() # また、LSTM の隠れ状態をクリアする必要があります、 # 最後のインスタンス上の履歴からそれをデタッチします。 model.hidden = model.init_hidden() # Step 2. 入力をネットワークに対して準備します、つまり、 # それらを単語インデックスの tensor に変えます。 sentence_in = prepare_sequence(sentence, word_to_ix) targets = prepare_sequence(tags, tag_to_ix) # Step 3. forward パスを実行します。 tag_scores = model(sentence_in) # Step 4. 損失、勾配を計算し、そして optimizer.step() を呼び出してパラメータを更新します。 loss = loss_function(tag_scores, targets) loss.backward() optimizer.step() # See what the scores are after training with torch.no_grad(): inputs = prepare_sequence(training_data[0][0], word_to_ix) tag_scores = model(inputs) # The sentence is "the dog ate the apple". i,j corresponds to score for tag j # for word i. The predicted tag is the maximum scoring tag. # Here, we can see the predicted sequence below is 0 1 2 0 1 # since 0 is index of the maximum value of row 1, # 1 is the index of maximum value of row 2, etc. # Which is DET NOUN VERB DET NOUN, the correct sequence! print(tag_scores)
Out:
tensor([[-1.1389, -1.2024, -0.9693], [-1.1065, -1.2200, -0.9834], [-1.1286, -1.2093, -0.9726], [-1.1190, -1.1960, -0.9916], [-1.0137, -1.2642, -1.0366]]) tensor([[-0.0858, -2.9355, -3.5374], [-5.2313, -0.0234, -4.0314], [-3.9098, -4.1279, -0.0368], [-0.0187, -4.7809, -4.5960], [-5.8170, -0.0183, -4.1879]])
課題: LSTM 品詞 tagger を文字レベル特徴で増強する
上のサンプルでは、各単語は埋め込みを持ち、それは私達のシーケンスモデルへの入力として提供されました。単語の文字に由来する表現で単語埋め込みを増強しましょう。これは本質的に役立つことを期待します、何故ならば接辞 (= affixes) のような文字レベルの情報は品詞の大きなベアリング (軸受け) を持つからです。例えば、接辞 -ly を持つ単語は英語では殆どいつも副詞としてタグ付けされます。
これを行なうために、$c_w$ を単語 $w$ の文字レベル表現としましょう。$x_w$ は前のように単語埋め込みとします。すると私達のシーケンスモデルへの入力は $x_w$ と $c_w$ の結合です。従って $x_w$ が次元 5 を持ち、そして $c_w$ が次元 3 を持つ場合、LSTM は次元 8 の入力を受け取るべきです。
文字レベル表現を得るために、単語の文字に渡り LSTM を遂行します、そして $c_w$ をこの LSTM の最後の隠れ状態とします。ヒントです :
- 貴方のモデルには2つの LSTM があることになります。元の一つは POS タグ・スコアを出力して、そして新しい一つは各単語の文字レベル表現を出力します。
- 文字に渡るシーケンスモデルを遂行するためには、文字を埋め込まなければならないでしょう。文字埋め込みは文字 LSTM への入力となるでしょう。
以上