Comprendre les LSTM Keras

311

J'essaie de concilier ma compréhension des LSTM et souligné ici dans ce post par Christopher Olah mis en œuvre à Keras. Je suis le blog écrit par Jason Brownlee pour le tutoriel Keras. Ce qui me trouble le plus, c'est

  1. Le remodelage de la série de données en [samples, time steps, features]et,
  2. Les LSTM avec état

Concentrons-nous sur les deux questions ci-dessus en référence au code collé ci-dessous:

# reshape into X=t and Y=t+1
look_back = 3
trainX, trainY = create_dataset(train, look_back)
testX, testY = create_dataset(test, look_back)

# reshape input to be [samples, time steps, features]
trainX = numpy.reshape(trainX, (trainX.shape[0], look_back, 1))
testX = numpy.reshape(testX, (testX.shape[0], look_back, 1))
########################
# The IMPORTANT BIT
##########################
# create and fit the LSTM network
batch_size = 1
model = Sequential()
model.add(LSTM(4, batch_input_shape=(batch_size, look_back, 1), stateful=True))
model.add(Dense(1))
model.compile(loss='mean_squared_error', optimizer='adam')
for i in range(100):
    model.fit(trainX, trainY, nb_epoch=1, batch_size=batch_size, verbose=2, shuffle=False)
    model.reset_states()

Remarque: create_dataset prend une séquence de longueur N et retourne un N-look_backtableau dont chaque élément est une look_backséquence de longueur.

Qu'est-ce que les pas de temps et les fonctionnalités?

Comme on peut le voir TrainX est un tableau 3-D avec Time_steps et Feature étant respectivement les deux dernières dimensions (3 et 1 dans ce code particulier). En ce qui concerne l'image ci-dessous, cela signifie-t-il que nous envisageons le many to onecas où le nombre de cases roses est de 3? Ou cela signifie-t-il littéralement que la longueur de la chaîne est de 3 (c'est-à-dire seulement 3 cases vertes considérées).entrez la description de l'image ici

L'argument des caractéristiques devient-il pertinent lorsque nous considérons des séries multivariées? Par exemple, modéliser simultanément deux valeurs financières?

LSTM avec état

Les LSTM avec état signifient-ils que nous sauvegardons les valeurs de la mémoire cellulaire entre les cycles de lots? Si tel est le cas, en batch_sizeest un, et la mémoire est réinitialisée entre les cycles d'entraînement donc quel était le point de dire qu'elle était avec état. Je suppose que cela est lié au fait que les données de formation ne sont pas mélangées, mais je ne sais pas comment.

Des pensées? Référence image: http://karpathy.github.io/2015/05/21/rnn-effectiveness/

Modifier 1:

Un peu confus au sujet du commentaire de @ van à propos de l'égalité des cases rouges et vertes. Donc, juste pour confirmer, les appels d'API suivants correspondent-ils aux diagrammes déroulés? Notant particulièrement le deuxième diagramme (a batch_sizeété choisi arbitrairement.): entrez la description de l'image ici entrez la description de l'image ici

Modifier 2:

Pour les personnes qui ont suivi le cours d'apprentissage en profondeur d'Udacity et qui sont toujours confus à propos de l'argument time_step, consultez la discussion suivante: https://discussions.udacity.com/t/rnn-lstm-use-implementation/163169

Mettre à jour:

Il s'est avéré que model.add(TimeDistributed(Dense(vocab_len)))c'était ce que je cherchais. Voici un exemple: https://github.com/sachinruk/ShakespeareBot

Update2:

J'ai résumé la plupart de ma compréhension des LSTM ici: https://www.youtube.com/watch?v=ywinX5wgdEU

sachinruk
la source
7
La première photo doit être (batch_size, 5, 1); la deuxième photo doit être (batch_size, 4, 3) (s'il n'y a pas de séquences suivantes). Et pourquoi la sortie est toujours "X"? Doit-il être "Y"?
Van
1
Ici, je suppose que X_1, X_2 ... X_6 est un nombre unique. Et trois nombres (X_1, X_2, X_3) font un vecteur de forme (3,). Un nombre (X_1) fait un vecteur de forme (1,).
Van
2
@ Van, votre hypothèse est correcte. C'est intéressant, donc fondamentalement, le modèle n'apprend pas les modèles au-delà du nombre de pas de temps. Donc, si j'ai une série temporelle de 1000 et que je peux voir visuellement un modèle tous les 100 jours, je devrais faire le paramètre time_steps au moins 100. Est-ce une observation correcte?
sachinruk
3
Oui. Et si vous pouvez collecter 3 fonctionnalités pertinentes par jour, vous pouvez définir la taille des fonctionnalités sur 3 comme vous l'avez fait dans la deuxième photo. Dans ce cas, la forme d'entrée sera (batch_size, 100, 3).
Van
1
et pour répondre à votre première question, c'était parce que je prenais une seule série chronologique. Par exemple, le cours des actions, X et Y sont donc de la même série.
sachinruk

Réponses:

173

Tout d'abord, vous choisissez d'excellents tutoriels ( 1 , 2 ) pour commencer.

Time-steps==3Signification du pas de temps : dans X.shape (Description de la forme des données) signifie qu'il y a trois cases roses. Étant donné que dans Keras, chaque étape nécessite une entrée, le nombre de cases vertes doit donc généralement être égal au nombre de cases rouges. Sauf si vous piratez la structure.

plusieurs à plusieurs vs plusieurs à un : en keras, il y a un return_sequencesparamètre lors de votre initialisation LSTMou GRUou SimpleRNN. Quand return_sequencesest False(par défaut), alors il est plusieurs pour un comme indiqué dans l'image. Sa forme de retour est (batch_size, hidden_unit_length), qui représente le dernier état. Quand return_sequencesc'est True, alors c'est plusieurs à plusieurs . Sa forme de retour est(batch_size, time_step, hidden_unit_length)

L'argument des fonctionnalités devient-il pertinent ? L'argument des fonctionnalités signifie "Quelle est la taille de votre boîte rouge" ou quelle est la dimension d'entrée à chaque étape. Si vous souhaitez prévoir, par exemple, 8 types d'informations sur le marché, vous pouvez générer vos données avec feature==8.

Avec état : vous pouvez rechercher le code source . Lors de l'initialisation de l'état, si stateful==True, alors l'état de la dernière formation sera utilisé comme état initial, sinon il générera un nouvel état. Je ne me suis pas statefulencore allumé. Cependant, je ne suis pas d'accord avec le fait que le batch_sizene peut être que 1 lorsque stateful==True.

Actuellement, vous générez vos données avec les données collectées. Imaginez que vos informations boursières arrivent sous forme de flux, plutôt que d'attendre une journée pour collecter toutes les informations séquentielles, vous souhaitez générer des données d'entrée en ligne tout en vous entraînant / prédisant avec le réseau. Si vous avez 400 actions partageant un même réseau, vous pouvez définir batch_size==400.

Van
la source
Légèrement confus quant à la raison pour laquelle les cases rouges et vertes doivent être les mêmes. Pourriez-vous regarder le montage que j'ai fait (les nouvelles photos principalement) et commenter?
sachinruk
1
En effet. Consultez le document:stateful: Boolean (default False). If True, the last state for each sample at index i in a batch will be used as initial state for the sample of index i in the following batch.
Van
1
@Van Si j'ai une série temporelle multivariée, dois-je quand même l'utiliser lookback = 1?
au
1
Pourquoi la dimensionnalité LSTM de l'espace de sortie (32) diffère-t-elle du nombre de neurones (cellules LSTM)?
Sticky
1
Ajout à stateful=True: La taille du lot peut être tout ce que vous aimez, mais vous devez vous y tenir. Si vous construisez votre modèle avec une taille de lot de 5, puis tous fit(), predict()et les méthodes connexes aurez besoin d' un lot de 5. Notez toutefois que cet état ne sera pas sauvé avec model.save(), ce qui pourrait sembler indésirable. Cependant, vous pouvez ajouter manuellement l'état au fichier hdf5, si vous en avez besoin. Mais en fait, cela vous permet de modifier la taille du lot en enregistrant et en rechargeant simplement un modèle.
jlh
192

En complément de la réponse acceptée, cette réponse montre les comportements des kéros et comment réaliser chaque image.

Comportement général de Keras

Le traitement interne des keras standard est toujours un à plusieurs comme dans l'image suivante (où j'ai utilisé features=2, pression et température, juste à titre d'exemple):

Plusieurs à plusieurs

Dans cette image, j'ai augmenté le nombre d'étapes à 5, pour éviter toute confusion avec les autres dimensions.

Pour cet exemple:

  • Nous avons N réservoirs d'huile
  • Nous avons passé 5 heures à prendre des mesures toutes les heures (pas de temps)
  • Nous avons mesuré deux caractéristiques:
    • Pression P
    • Température T

Notre tableau d'entrée devrait alors avoir la forme suivante (N,5,2):

        [     Step1      Step2      Step3      Step4      Step5
Tank A:    [[Pa1,Ta1], [Pa2,Ta2], [Pa3,Ta3], [Pa4,Ta4], [Pa5,Ta5]],
Tank B:    [[Pb1,Tb1], [Pb2,Tb2], [Pb3,Tb3], [Pb4,Tb4], [Pb5,Tb5]],
  ....
Tank N:    [[Pn1,Tn1], [Pn2,Tn2], [Pn3,Tn3], [Pn4,Tn4], [Pn5,Tn5]],
        ]

Entrées pour fenêtres coulissantes

Souvent, les couches LSTM sont censées traiter l'intégralité des séquences. La division des fenêtres n'est peut-être pas la meilleure idée. La couche a des états internes sur l'évolution d'une séquence à mesure qu'elle avance. Windows élimine la possibilité d'apprendre de longues séquences, limitant toutes les séquences à la taille de la fenêtre.

Dans les fenêtres, chaque fenêtre fait partie d'une longue séquence d'origine, mais selon Keras, elles seront vues chacune comme une séquence indépendante:

        [     Step1    Step2    Step3    Step4    Step5
Window  A:  [[P1,T1], [P2,T2], [P3,T3], [P4,T4], [P5,T5]],
Window  B:  [[P2,T2], [P3,T3], [P4,T4], [P5,T5], [P6,T6]],
Window  C:  [[P3,T3], [P4,T4], [P5,T5], [P6,T6], [P7,T7]],
  ....
        ]

Notez que dans ce cas, vous n'avez initialement qu'une seule séquence, mais vous la divisez en plusieurs séquences pour créer des fenêtres.

Le concept de "qu'est-ce qu'une séquence" est abstrait. Les parties importantes sont:

  • vous pouvez avoir des lots avec de nombreuses séquences individuelles
  • ce qui fait que les séquences sont des séquences, c'est qu'elles évoluent par étapes (généralement des pas de temps)

Atteindre chaque cas avec des "couches simples"

Atteindre la norme plusieurs à plusieurs:

StandardManyToMany

Vous pouvez réaliser plusieurs à plusieurs avec une simple couche LSTM, en utilisant return_sequences=True:

outputs = LSTM(units, return_sequences=True)(inputs)

#output_shape -> (batch_size, steps, units)

Atteindre plusieurs à un:

En utilisant exactement la même couche, les keras effectueront exactement le même prétraitement interne, mais lorsque vous utilisez return_sequences=False(ou ignorez simplement cet argument), les keras ignoreront automatiquement les étapes précédentes à la dernière:

ManyToOne

outputs = LSTM(units)(inputs)

#output_shape -> (batch_size, units) --> steps were discarded, only the last was returned

Atteindre un à plusieurs

Maintenant, cela n'est pas pris en charge par les couches keras LSTM seules. Vous devrez créer votre propre stratégie pour multiplier les étapes. Il existe deux bonnes approches:

  • Créer une entrée à plusieurs étapes constante en répétant un tenseur
  • Utilisez a stateful=Truepour prendre de façon récurrente la sortie d'une étape et la servir d'entrée de l'étape suivante (besoins output_features == input_features)

Un à plusieurs avec vecteur de répétition

Afin de s'adapter au comportement standard des keras, nous avons besoin d'entrées par étapes, donc, nous répétons simplement les entrées pour la longueur que nous voulons:

OneToManyRepeat

outputs = RepeatVector(steps)(inputs) #where inputs is (batch,features)
outputs = LSTM(units,return_sequences=True)(outputs)

#output_shape -> (batch_size, steps, units)

Comprendre stateful = True

Vient maintenant l'un des usages possibles de stateful=True(en plus d'éviter de charger des données qui ne peuvent pas contenir la mémoire de votre ordinateur à la fois)

Stateful nous permet de saisir des "parties" des séquences par étapes. La différence est:

  • Dans stateful=False, le deuxième lot contient de nouvelles séquences entières, indépendantes du premier lot
  • Dans stateful=True, le deuxième lot continue le premier lot, étendant les mêmes séquences.

C'est comme diviser les séquences dans les fenêtres aussi, avec ces deux différences principales:

  • ces fenêtres ne se superposent pas !!
  • stateful=True verra ces fenêtres connectées comme une seule longue séquence

Dans stateful=True, chaque nouveau lot sera interprété comme poursuivant le lot précédent (jusqu'à ce que vous appeliez model.reset_states()).

  • La séquence 1 du lot 2 continuera la séquence 1 du lot 1.
  • La séquence 2 du lot 2 continuera la séquence 2 du lot 1.
  • La séquence n du lot 2 continuera la séquence n du lot 1.

Exemple d'entrées, le lot 1 contient les étapes 1 et 2, le lot 2 contient les étapes 3 à 5:

                   BATCH 1                           BATCH 2
        [     Step1      Step2        |    [    Step3      Step4      Step5
Tank A:    [[Pa1,Ta1], [Pa2,Ta2],     |       [Pa3,Ta3], [Pa4,Ta4], [Pa5,Ta5]],
Tank B:    [[Pb1,Tb1], [Pb2,Tb2],     |       [Pb3,Tb3], [Pb4,Tb4], [Pb5,Tb5]],
  ....                                |
Tank N:    [[Pn1,Tn1], [Pn2,Tn2],     |       [Pn3,Tn3], [Pn4,Tn4], [Pn5,Tn5]],
        ]                                  ]

Remarquez l'alignement des réservoirs des lots 1 et 2! C'est pourquoi nous avons besoin shuffle=False(à moins que nous n'utilisions qu'une seule séquence, bien sûr).

Vous pouvez avoir un nombre illimité de lots, indéfiniment. (Pour avoir des longueurs variables dans chaque lot, utilisez input_shape=(None,features).

Un à plusieurs avec stateful = True

Pour notre cas ici, nous n'utiliserons qu'une seule étape par lot, car nous voulons obtenir une étape de sortie et en faire une entrée.

Veuillez noter que le comportement dans l'image n'est pas "provoqué par" stateful=True. Nous forcerons ce comportement dans une boucle manuelle ci-dessous. Dans cet exemple, stateful=Truec'est ce qui nous «permet» d'arrêter la séquence, de manipuler ce que nous voulons et de continuer d'où nous nous sommes arrêtés.

OneToManyStateful

Honnêtement, l'approche répétée est probablement un meilleur choix pour ce cas. Mais puisque nous examinons stateful=True, c'est un bon exemple. La meilleure façon de l'utiliser est le prochain cas "plusieurs à plusieurs".

Couche:

outputs = LSTM(units=features, 
               stateful=True, 
               return_sequences=True, #just to keep a nice output shape even with length 1
               input_shape=(None,features))(inputs) 
    #units = features because we want to use the outputs as inputs
    #None because we want variable length

#output_shape -> (batch_size, steps, units) 

Maintenant, nous allons avoir besoin d'une boucle manuelle pour les prédictions:

input_data = someDataWithShape((batch, 1, features))

#important, we're starting new sequences, not continuing old ones:
model.reset_states()

output_sequence = []
last_step = input_data
for i in steps_to_predict:

    new_step = model.predict(last_step)
    output_sequence.append(new_step)
    last_step = new_step

 #end of the sequences
 model.reset_states()

Plusieurs à plusieurs avec stateful = True

Maintenant, ici, nous obtenons une très belle application: étant donné une séquence d'entrée, essayez de prédire ses futures étapes inconnues.

Nous utilisons la même méthode que dans le "un à plusieurs" ci-dessus, à la différence que:

  • nous utiliserons la séquence elle-même pour être les données cibles, une longueur d'avance
  • nous connaissons une partie de la séquence (nous rejetons donc cette partie des résultats).

ManyToManyStateful

Couche (comme ci-dessus):

outputs = LSTM(units=features, 
               stateful=True, 
               return_sequences=True, 
               input_shape=(None,features))(inputs) 
    #units = features because we want to use the outputs as inputs
    #None because we want variable length

#output_shape -> (batch_size, steps, units) 

Formation:

Nous allons former notre modèle pour prédire la prochaine étape des séquences:

totalSequences = someSequencesShaped((batch, steps, features))
    #batch size is usually 1 in these cases (often you have only one Tank in the example)

X = totalSequences[:,:-1] #the entire known sequence, except the last step
Y = totalSequences[:,1:] #one step ahead of X

#loop for resetting states at the start/end of the sequences:
for epoch in range(epochs):
    model.reset_states()
    model.train_on_batch(X,Y)

Prédire:

La première étape de notre prévision consiste à "ajuster les états". C'est pourquoi nous allons à nouveau prédire la séquence entière, même si nous en connaissons déjà cette partie:

model.reset_states() #starting a new sequence
predicted = model.predict(totalSequences)
firstNewStep = predicted[:,-1:] #the last step of the predictions is the first future step

Nous passons maintenant à la boucle comme dans le cas un à plusieurs. Mais ne réinitialisez pas les états ici! . Nous voulons que le modèle sache à quelle étape de la séquence il se trouve (et il sait qu'il est à la première nouvelle étape en raison de la prédiction que nous venons de faire ci-dessus)

output_sequence = [firstNewStep]
last_step = firstNewStep
for i in steps_to_predict:

    new_step = model.predict(last_step)
    output_sequence.append(new_step)
    last_step = new_step

 #end of the sequences
 model.reset_states()

Cette approche a été utilisée dans ces réponses et dossier:

Réalisation de configurations complexes

Dans tous les exemples ci-dessus, j'ai montré le comportement de "une couche".

Vous pouvez, bien sûr, empiler plusieurs couches les unes sur les autres, sans nécessairement toutes suivre le même modèle, et créer vos propres modèles.

Un exemple intéressant qui est apparu est le "autoencoder" qui a un "plusieurs à un codeur" suivi d'un décodeur "un à plusieurs":

Encodeur:

inputs = Input((steps,features))

#a few many to many layers:
outputs = LSTM(hidden1,return_sequences=True)(inputs)
outputs = LSTM(hidden2,return_sequences=True)(outputs)    

#many to one layer:
outputs = LSTM(hidden3)(outputs)

encoder = Model(inputs,outputs)

Décodeur:

Utilisation de la méthode "répétition";

inputs = Input((hidden3,))

#repeat to make one to many:
outputs = RepeatVector(steps)(inputs)

#a few many to many layers:
outputs = LSTM(hidden4,return_sequences=True)(outputs)

#last layer
outputs = LSTM(features,return_sequences=True)(outputs)

decoder = Model(inputs,outputs)

Encodeur automatique:

inputs = Input((steps,features))
outputs = encoder(inputs)
outputs = decoder(outputs)

autoencoder = Model(inputs,outputs)

Entraînez-vous avec fit(X,X)

Explications supplémentaires

Si vous voulez des détails sur la façon dont les étapes sont calculées dans les LSTM, ou des détails sur les stateful=Truecas ci-dessus, vous pouvez en savoir plus dans cette réponse: Doutes concernant `Comprendre les LSTM Keras`

Daniel Möller
la source
1
Utilisation très intéressante de stateful avec utilisation de sorties comme entrées. Juste comme note supplémentaire, une autre façon de faire serait d'utiliser l'API Keras fonctionnelle (comme vous l'avez fait ici, bien que je pense que vous auriez pu utiliser celle séquentielle), et simplement réutiliser la même cellule LSTM pour chaque pas de temps , tout en passant à la fois l'état résultant et la sortie de la cellule à elle-même. Ie my_cell = LSTM(num_output_features_per_timestep, return_state=True), suivi d'une boucle dea, _, c = my_cell(output_of_previous_time_step, initial_states=[a, c])
Jacob R
1
Les cellules et la longueur sont des valeurs complètement indépendantes. Aucune des images ne représente le nombre de "cellules". Ils sont tous pour la "longueur".
Daniel Möller
1
@ DanielMöller Je sais qu'il est un peu en retard, mais votre réponse attire vraiment mon attention. Un de vos arguments a tout bouleversé dans ma compréhension de ce qu'est le lot pour LSTM. Vous fournissez un exemple avec N réservoirs, cinq étapes et deux fonctionnalités. Je pensais que, si le lot est par exemple deux, cela signifie que deux échantillons (réservoirs avec 5 fonctions 2 étapes) seront introduits dans le réseau et après cela, les poids seront adaptés. Mais si je comprends bien, vous déclarez que le lot 2 signifie que les pas de temps des échantillons seront divisés en 2 et que la première moitié de tous les échantillons sera envoyée à LSTM-> mise à jour du poids et ensuite.
viceriel du
1
Oui. Sur un état = Vrai, lot 1 = groupe d'échantillons, mise à jour. Lot 2 = étapes supplémentaires pour le même groupe d'échantillons, mise à jour.
Daniel Möller
2
J'aimerais pouvoir voter 100 fois. Réponse super utile.
adamconkey
4

Lorsque vous avez return_sequences dans votre dernière couche de RNN, vous ne pouvez pas utiliser une simple couche dense à la place, utilisez TimeDistributed.

Voici un exemple de code qui pourrait aider les autres.

words = keras.layers.Input (batch_shape = (None, self.maxSequenceLength), name = "input")

    # Build a matrix of size vocabularySize x EmbeddingDimension 
    # where each row corresponds to a "word embedding" vector.
    # This layer will convert replace each word-id with a word-vector of size Embedding Dimension.
    embeddings = keras.layers.embeddings.Embedding(self.vocabularySize, self.EmbeddingDimension,
        name = "embeddings")(words)
    # Pass the word-vectors to the LSTM layer.
    # We are setting the hidden-state size to 512.
    # The output will be batchSize x maxSequenceLength x hiddenStateSize
    hiddenStates = keras.layers.GRU(512, return_sequences = True, 
                                        input_shape=(self.maxSequenceLength,
                                        self.EmbeddingDimension),
                                        name = "rnn")(embeddings)
    hiddenStates2 = keras.layers.GRU(128, return_sequences = True, 
                                        input_shape=(self.maxSequenceLength, self.EmbeddingDimension),
                                        name = "rnn2")(hiddenStates)

    denseOutput = TimeDistributed(keras.layers.Dense(self.vocabularySize), 
        name = "linear")(hiddenStates2)
    predictions = TimeDistributed(keras.layers.Activation("softmax"), 
        name = "softmax")(denseOutput)  

    # Build the computational graph by specifying the input, and output of the network.
    model = keras.models.Model(input = words, output = predictions)
    # model.compile(loss='kullback_leibler_divergence', \
    model.compile(loss='sparse_categorical_crossentropy', \
        optimizer = keras.optimizers.Adam(lr=0.009, \
            beta_1=0.9,\
            beta_2=0.999, \
            epsilon=None, \
            decay=0.01, \
            amsgrad=False))
Sanjay Krishna
la source