wake-up-neo.com

LSTM RNN Backpropagation

Könnte jemand eine klare Erklärung für die Backpropagation von LSTM-RNNs geben? Dies ist die Typstruktur, mit der ich arbeite. Meine Frage bezieht sich nicht auf die Rückausbreitung. Ich verstehe, dass es sich um eine Methode in umgekehrter Reihenfolge handelt, mit der der Fehler der Hypothese und der Ausgabe berechnet wird, die zum Anpassen der Gewichte neuronaler Netze verwendet werden. Meine Frage ist, wie sich die LSTM-Backpropagation von regulären neuronalen Netzen unterscheidet.

 enter image description here

Ich bin mir nicht sicher, wie ich den anfänglichen Fehler der einzelnen Tore finden soll. Verwenden Sie für jedes Gate den ersten Fehler (berechnet nach Hypothese minus Ausgabe)? Oder passen Sie den Fehler für jedes Tor durch eine Berechnung an? Ich bin mir nicht sicher, wie der Zellzustand im Backprop von LSTMs eine Rolle spielt, wenn überhaupt. Ich habe gründlich nach einer guten Quelle für LSTMs gesucht, aber noch keine gefunden.

8
Jjoseph

Das ist eine gute Frage. Sie sollten sich auf jeden Fall die vorgeschlagenen Beiträge ansehen, um Details zu erfahren. Ein vollständiges Beispiel wäre auch hier hilfreich.

RNN Backpropagaion

Ich denke, es ist sinnvoll, zuerst über ein gewöhnliches RNN zu sprechen (da das LSTM-Diagramm besonders verwirrend ist) und dessen Backpropagation zu verstehen.

Wenn es um die Rückübertragung geht, lautet die Schlüsselidee Network Unrolling . Auf diese Weise können Sie die Rekursion in RNN in eine Feed-Forward-Sequenz umwandeln (wie im obigen Bild). Es ist zu beachten, dass das abstrakte RNN ewig ist (beliebig groß sein kann), aber jede bestimmte Implementierung ist begrenzt, da der Speicher begrenzt ist. Infolgedessen ist das abgewickelte Netzwerk wirklich ein langes Feed-Forward-Netzwerk mit wenigen Komplikationen, z. Die Gewichte in verschiedenen Schichten werden geteilt.

Schauen wir uns ein klassisches Beispiel an: char-rnn von Andrej Karpathy . Hier erzeugt jede RNN-Zelle zwei Ausgaben h[t] (Den Zustand, der in die nächste Zelle eingespeist wird) und y[t] (Die Ausgabe in diesem Schritt) durch die folgenden Formeln, wobei Wxh , Whh und Why sind die gemeinsamen Parameter:

rnn-cell-formula

Im Code sind es einfach drei Matrizen und zwei Bias-Vektoren:

# 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

Der Vorwärtsdurchlauf ist ziemlich unkompliziert. In diesem Beispiel werden Softmax- und Cross-Entropy-Verluste verwendet. Beachten Sie, dass für jede Iteration die gleichen Arrays W* Und h* Verwendet werden, der Ausgabe- und der verborgene Status jedoch unterschiedlich sind:

# 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)

Der Rückwärtsdurchlauf wird nun genau so ausgeführt, als wäre es ein Feed-Forward-Netzwerk, aber der Gradient der Arrays W* Und h* Akkumuliert die Gradienten in allen Zellen:

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)

Beide oben genannten Durchläufe werden in Stücken der Größe len(inputs) durchgeführt, die der Größe des entrollten RNN entspricht. Möglicherweise möchten Sie die Erfassung längerer Abhängigkeiten in der Eingabe vergrößern, aber Sie zahlen dafür, indem Sie alle Ausgaben und Verläufe für jede Zelle speichern.

Was ist anders in LSTMs

LSTM-Bilder und -Formeln sehen einschüchternd aus, aber wenn Sie einmal Vanilla RNN codiert haben, ist die Implementierung von LSTM nahezu identisch. Zum Beispiel ist hier der Rückwärtsdurchlauf:

# 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

Zusammenfassung

Nun zurück zu Ihren Fragen.

Meine Frage ist, wie sich die LSTM-Backpropagation von regulären neuronalen Netzen unterscheidet

Dabei handelt es sich um gemeinsame Gewichte in verschiedenen Ebenen und einige zusätzliche Variablen (Status), auf die Sie achten müssen. Davon abgesehen überhaupt kein Unterschied.

Verwenden Sie für jedes Gatter den ersten Fehler (berechnet nach Hypothese minus Ausgabe)? Oder passen Sie den Fehler für jedes Tor durch eine Berechnung an?

Erstens ist die Verlustfunktion nicht notwendigerweise L2. Im obigen Beispiel handelt es sich um einen Kreuzentropieverlust, sodass das anfängliche Fehlersignal seinen Gradienten erhält:

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

Es ist zu beachten, dass es sich um dasselbe Fehlersignal wie in einem normalen neuronalen Vorwärtskopplungsnetz handelt. Wenn Sie L2-Verlust verwenden, entspricht das Signal tatsächlich der Masse-Wahrheit minus der tatsächlichen Ausgabe.

Im Falle von LSTM ist es etwas komplizierter: d_next_h = d_h_next_t + d_h[:,t,:], Wobei d_h Der Upstream-Gradient der Verlustfunktion ist, was bedeutet, dass das Fehlersignal jeder Zelle akkumuliert wird. Wenn Sie LSTM jedoch wieder ausrollen, wird eine direkte Korrespondenz mit der Netzwerkverkabelung angezeigt.

8
Maxim

Ich denke, Ihre Fragen konnten nicht in einer kurzen Antwort beantwortet werden. Nicos einfaches LSTM hat einen Link zu einem großartigen Artikel von Lipton et al., Bitte lesen Sie diesen. Auch sein einfaches Python-Codebeispiel hilft bei der Beantwortung der meisten Ihrer Fragen. Wenn du Nicos letzten Satz im Detail verstehst, gib mir bitte eine Rückmeldung. Ds = self.state.o * top_diff_h + top_diff_s Im Moment habe ich ein letztes Problem mit seiner "Zusammenstellung all dieser s und h Ableitungen ".

1
Thor