web-dev-qa-db-fra.com

LSTM RNN Backpropagation

Quelqu'un pourrait-il donner une explication claire de la rétropropagation pour les RNN LSTM? C'est la structure de type avec laquelle je travaille. Ma question ne concerne pas ce qui est de la propagation en retour, si je comprends bien, il s’agit d’une méthode inverse pour calculer l’erreur de l’hypothèse et la sortie utilisée pour ajuster les poids des réseaux de neurones. Ma question est de savoir en quoi la rétro-propagation LSTM est différente des réseaux de neurones ordinaires.

 enter image description here

Je ne sais pas comment trouver l'erreur initiale de chaque porte. Utilisez-vous la première erreur (calculée par hypothèse moins la sortie) pour chaque porte? Ou ajustez-vous l'erreur pour chaque porte par un calcul? Je ne suis pas sûr de savoir comment l'état de la cellule joue un rôle dans le backprop des LSTM si c'est le cas. J'ai minutieusement cherché une bonne source de LSTM, mais je n’en ai pas encore trouvé.

8
Jjoseph

C'est une bonne question. Vous devriez certainement jeter un coup d'œil aux messages suggérés pour plus de détails, mais un exemple complet ici serait également utile.

RNN Backpropagaion

Je pense qu'il est logique de parler d'abord d'un RNN ordinaire (parce que le diagramme LSTM est particulièrement déroutant) et de comprendre sa contre-propagation.

En ce qui concerne la rétroprojection, l’idée principale est network unrolling , ce qui permet de transformer la récursivité dans RNN en une séquence de feed-forward (comme sur l’illustration ci-dessus). Notez que RNN abstrait est éternel (peut être arbitrairement grand), mais chaque implémentation particulière est limitée car la mémoire est limitée. En conséquence, le réseau déroulé est vraiment un long réseau à feed-forward, avec peu de complications, par ex. les poids dans les différentes couches sont partagés.

Jetons un coup d'oeil à un exemple classique, char-rnn d'Andrej Karpathy . Ici, chaque cellule RNN produit deux sorties h[t] (l'état inséré dans la cellule suivante) et y[t] (la sortie de cette étape) par les formules suivantes, où Wxh, Whh et Why sont les paramètres partagés:

 rnn-cell-formula

Dans le code, il s'agit simplement de trois matrices et de deux vecteurs de biais:

# 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

Le passage en avant est assez simple, cet exemple utilise softmax et la perte d'entropie croisée. Notez que chaque itération utilise les mêmes tableaux W* et h*, mais la sortie et l'état masqué sont différents:

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

À présent, la passe arrière est effectuée exactement comme s'il s'agissait d'un réseau à feed-forward, mais les tableaux de gradients W* et h* accumulent les gradients dans toutes les cellules:

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)

Les deux passages ci-dessus sont effectués en morceaux de taille len(inputs), ce qui correspond à la taille du RNN déroulé. Vous voudrez peut-être l'agrandir pour capturer des dépendances plus longues dans l'entrée, mais vous payez pour cela en stockant toutes les sorties et tous les gradients pour chaque cellule.

Ce qui est différent dans les LSTM

L'image et les formules LSTM semblent intimidantes, mais une fois que vous avez codé plain Vanilla RNN, la mise en œuvre de LSTM est quasiment identique. Par exemple, voici la passe en arrière:

# 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

Résumé

Revenons maintenant à vos questions.

Ma question est de savoir en quoi la rétroprojection LSTM est différente des réseaux de neurones ordinaires

Les sont des poids partagés dans différentes couches et peu de variables supplémentaires (états) auxquelles vous devez faire attention. Autre que cela, pas de différence du tout.

Utilisez-vous la première erreur (calculée par hypothèse moins la sortie) pour chaque porte? Ou ajustez-vous l'erreur pour chaque porte par un calcul?

Tout d'abord, la fonction de perte n'est pas nécessairement L2. Dans l'exemple ci-dessus, il s'agit d'une perte d'entropie croisée. Le signal d'erreur initial obtient donc son gradient:

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

Notez que c'est le même signal d'erreur que dans un réseau neuronal à feed-forward ordinaire. Si vous utilisez la perte L2, le signal est égal à la vérité du sol moins la sortie réelle.

Dans le cas de LSTM, c'est un peu plus compliqué: d_next_h = d_h_next_t + d_h[:,t,:], où d_h est le gradient en amont de la fonction de perte, ce qui signifie que le signal d'erreur de chaque cellule est accumulé. Mais encore une fois, si vous déroulez LSTM, vous verrez une correspondance directe avec le câblage du réseau.

7
Maxim

Je pense que vos questions ne pourraient pas être répondu dans une réponse courte. Nico's simple LSTM a un lien vers un excellent article de Lipton et al., Veuillez lire ceci. De plus, son exemple de code python simple aide à répondre à la plupart de vos questions. Si vous comprenez la dernière phrase de Nico ds = self.state.o * top_diff_h + top_diff_s en détail, donnez-moi un retour. Pour l’instant, j’ai un dernier problème avec son "Assembler toutes ces dérivations s et h ensemble".

1
Thor