web-dev-qa-db-ja.com

LSTMRNNバックプロパゲーション

誰かがLSTMRNNのバックプロパゲーションについて明確に説明できますか?これは私が使用している型構造です。私の質問は、バックプロパゲーションとは何かということではありません。ニューラルネットワークの重みを調整するために使用される仮説と出力の誤差を計算する逆順の方法であることを理解しています。私の質問は、LSTMバックプロパゲーションが通常のニューラルネットワークとどのように異なるかです。

enter image description here

各ゲートの初期エラーを見つける方法がわかりません。各ゲートに最初のエラー(仮説から出力を引いて計算)を使用しますか?または、計算によって各ゲートの誤差を調整しますか?セルの状態がLSTMのバックプロパゲーションでどのように役割を果たすかは、まったくわかりません。私はLSTMの良い情報源を徹底的に探しましたが、まだ見つけていません。

10
Jjoseph

それは良い質問です。詳細については、提案された投稿を確認する必要がありますが、ここでの完全な例も役立ちます。

RNNバックプロパゲーション

最初に通常のRNNについて話し(LSTMダイアグラムは特に混乱するため)、その逆伝播を理解することは理にかなっていると思います。

バックプロパゲーションに関しては、重要なアイデアはネットワーク展開です。これは、RNNの再帰をフィードフォワードシーケンスに変換する方法です(上の写真)。抽象RNNは永遠です(任意に大きくすることができます)が、メモリが制限されているため、特定の実装はそれぞれ制限されていることに注意してください。その結果、展開されたネットワークは実際には長いフィードフォワードネットワークであり、複雑さはほとんどありません。異なるレイヤーの重みは共有されます。

古典的な例を見てみましょう Andrej Karpathyによるchar-rnn 。ここで、各RNNセルは、次の式によって2つの出力_h[t]_(次のセルに供給される状態)と_y[t]_(このステップの出力)を生成します。ここで、WxhWhhWhyは共有パラメーターです。

rnn-cell-formula

コードでは、3つの行列と2つのバイアスベクトルです。

_# model parameters
Wxh = np.random.randn(hidden_size, vocab_size)*0.01 # input to hidden
Whh = np.random.randn(hidden_size, hidden_size)*0.01 # hidden to hidden
Why = np.random.randn(vocab_size, hidden_size)*0.01 # hidden to output
bh = np.zeros((hidden_size, 1)) # hidden bias
by = np.zeros((vocab_size, 1)) # output bias
_

フォワードパスは非常に簡単です。この例では、ソフトマックス損失とクロスエントロピー損失を使用しています。各反復は同じ_W*_配列と_h*_配列を使用しますが、出力と非表示の状態が異なることに注意してください。

_# forward pass
for t in xrange(len(inputs)):
  xs[t] = np.zeros((vocab_size,1)) # encode in 1-of-k representation
  xs[t][inputs[t]] = 1
  hs[t] = np.tanh(np.dot(Wxh, xs[t]) + np.dot(Whh, hs[t-1]) + bh) # hidden state
  ys[t] = np.dot(Why, hs[t]) + by # unnormalized log probabilities for next chars
  ps[t] = np.exp(ys[t]) / np.sum(np.exp(ys[t])) # probabilities for next chars
  loss += -np.log(ps[t][targets[t],0]) # softmax (cross-entropy loss)
_

これで、逆方向パスはフィードフォワードネットワークの場合とまったく同じように実行されますが、_W*_および_h*_配列の勾配は、すべてのセルの勾配を累積します。

_for t in reversed(xrange(len(inputs))):
  dy = np.copy(ps[t])
  dy[targets[t]] -= 1
  dWhy += np.dot(dy, hs[t].T)
  dby += dy
  dh = np.dot(Why.T, dy) + dhnext # backprop into h
  dhraw = (1 - hs[t] * hs[t]) * dh # backprop through tanh nonlinearity
  dbh += dhraw
  dWxh += np.dot(dhraw, xs[t].T)
  dWhh += np.dot(dhraw, hs[t-1].T)
  dhnext = np.dot(Whh.T, dhraw)
_

上記の両方のパスは、展開されたRNNのサイズに対応するサイズlen(inputs)のチャンクで実行されます。入力のより長い依存関係をキャプチャするために大きくしたい場合がありますが、各セルごとにすべての出力とグラデーションを保存することで料金を支払います。

LSTMの違い

LSTMの図と式は威圧的に見えますが、プレーンなVanilla RNNをコーディングすると、LSTMの実装はほとんど同じになります。たとえば、これがバックワードパスです。

_# Loop over all cells, like before
d_h_next_t = np.zeros((N, H))
d_c_next_t = np.zeros((N, H))
for t in reversed(xrange(T)):
  d_x_t, d_h_prev_t, d_c_prev_t, d_Wx_t, d_Wh_t, d_b_t = lstm_step_backward(d_h_next_t + d_h[:,t,:], d_c_next_t, cache[t])
  d_c_next_t = d_c_prev_t
  d_h_next_t = d_h_prev_t

  d_x[:,t,:] = d_x_t
  d_h0 = d_h_prev_t
  d_Wx += d_Wx_t
  d_Wh += d_Wh_t
  d_b += d_b_t

# The step in each cell
# Captures all LSTM complexity in few formulas.
def lstm_step_backward(d_next_h, d_next_c, cache):
  """
  Backward pass for a single timestep of an LSTM.

  Inputs:
  - dnext_h: Gradients of next hidden state, of shape (N, H)
  - dnext_c: Gradients of next cell state, of shape (N, H)
  - cache: Values from the forward pass

  Returns a Tuple of:
  - dx: Gradient of input data, of shape (N, D)
  - dprev_h: Gradient of previous hidden state, of shape (N, H)
  - dprev_c: Gradient of previous cell state, of shape (N, H)
  - dWx: Gradient of input-to-hidden weights, of shape (D, 4H)
  - dWh: Gradient of hidden-to-hidden weights, of shape (H, 4H)
  - db: Gradient of biases, of shape (4H,)
  """
  x, prev_h, prev_c, Wx, Wh, a, i, f, o, g, next_c, z, next_h = cache

  d_z = o * d_next_h
  d_o = z * d_next_h
  d_next_c += (1 - z * z) * d_z

  d_f = d_next_c * prev_c
  d_prev_c = d_next_c * f
  d_i = d_next_c * g
  d_g = d_next_c * i

  d_a_g = (1 - g * g) * d_g
  d_a_o = o * (1 - o) * d_o
  d_a_f = f * (1 - f) * d_f
  d_a_i = i * (1 - i) * d_i
  d_a = np.concatenate((d_a_i, d_a_f, d_a_o, d_a_g), axis=1)

  d_prev_h = d_a.dot(Wh.T)
  d_Wh = prev_h.T.dot(d_a)

  d_x = d_a.dot(Wx.T)
  d_Wx = x.T.dot(d_a)

  d_b = np.sum(d_a, axis=0)

  return d_x, d_prev_h, d_prev_c, d_Wx, d_Wh, d_b
_

概要

さて、あなたの質問に戻りましょう。

私の質問は、LSTMバックプロパゲーションが通常のニューラルネットワークとどのように異なるかです。

これらは、さまざまなレイヤーで共有される重みであり、注意が必要な追加の変数(状態)がいくつかあります。これ以外は全く違いはありません。

各ゲートに最初のエラー(仮説から出力を引いて計算)を使用しますか?または、計算によって各ゲートの誤差を調整しますか?

まず、損失関数は必ずしもL2ではありません。上記の例では、クロスエントロピー損失であるため、初期エラー信号はその勾配を取得します。

_# remember that ps is the probability distribution from the forward pass
dy = np.copy(ps[t])  
dy[targets[t]] -= 1
_

これは通常のフィードフォワードニューラルネットワークと同じエラー信号であることに注意してください。 L2損失を使用する場合、信号は実際にグラウンドトゥルースから実際の出力を引いたものに等しくなります。

LSTMの場合、少し複雑になります。_d_next_h = d_h_next_t + d_h[:,t,:]_、ここで_d_h_は、損失関数の上流勾配です。これは、各セルのエラー信号が累積されることを意味します。しかし、もう一度、LSTMを展開すると、ネットワーク配線との直接の対応がわかります。

11
Maxim

短い回答ではお答えできなかったと思います。ニコの 単純なLSTM には、リプトンらのすばらしい論文へのリンクがあります。これを読んでください。また、彼の単純なpythonコードサンプルは、ほとんどの質問に答えるのに役立ちます。ニコの最後の文ds = self.state.o * top_diff_h + top_diff_sを詳しく理解している場合は、フィードバックをください。現時点では、彼の「このすべてとhの派生を一緒にする」という最後の問題があります。

1
Thor