Entendendo os LSTMs do Keras

311

Estou tentando reconciliar meu entendimento dos LSTMs e apontado aqui neste post por Christopher Olah implementado no Keras. Estou seguindo o blog escrito por Jason Brownlee para o tutorial de Keras. O que mais me deixa confuso é,

  1. A remodelagem das séries de dados em [samples, time steps, features]e,
  2. Os LSTMs com estado

Vamos nos concentrar nas duas perguntas acima com referência ao código colado abaixo:

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

Nota: create_dataset pega uma sequência de comprimento N e retorna uma N-look_backmatriz da qual cada elemento é uma look_backsequência de comprimento.

O que são etapas e recursos de tempo?

Como pode ser visto, o TrainX é uma matriz 3D com Time_steps e Feature sendo as duas últimas dimensões, respectivamente (3 e 1 neste código em particular). Com relação à imagem abaixo, isso significa que estamos considerando o many to onecaso, em que o número de caixas cor de rosa é 3? Ou significa literalmente que o comprimento da corrente é 3 (ou seja, são consideradas apenas 3 caixas verdes).insira a descrição da imagem aqui

O argumento dos recursos se torna relevante quando consideramos séries multivariadas? por exemplo, modelar duas ações financeiras simultaneamente?

LSTMs com estado

Os LSTMs com estado indicam que salvamos os valores da memória da célula entre execuções de lotes? Se for esse o caso, batch_sizeé um deles, e a memória é redefinida entre as execuções de treinamento. Suponho que isso esteja relacionado ao fato de que os dados de treinamento não são embaralhados, mas não sei como.

Alguma ideia? Referência da imagem: http://karpathy.github.io/2015/05/21/rnn-effectiveness/

Editar 1:

Um pouco confuso sobre o comentário de @ van sobre as caixas vermelhas e verdes serem iguais. Portanto, apenas para confirmar, as seguintes chamadas de API correspondem aos diagramas não desenrolados? Observando especialmente o segundo diagrama ( batch_sizefoi escolhido arbitrariamente): insira a descrição da imagem aqui insira a descrição da imagem aqui

Edição 2:

Para as pessoas que fizeram o curso de aprendizado profundo da Udacity e ainda confundiram com o argumento time_step, consulte a seguinte discussão: https://discussions.udacity.com/t/rnn-lstm-use-implementation/163169

Atualizar:

Acontece que model.add(TimeDistributed(Dense(vocab_len)))era o que eu estava procurando. Aqui está um exemplo: https://github.com/sachinruk/ShakespeareBot

Update2:

Resumi a maior parte do meu entendimento dos LSTMs aqui: https://www.youtube.com/watch?v=ywinX5wgdEU

sachinruk
fonte
7
A primeira foto deve ser (batch_size, 5, 1); a segunda foto deve ser (batch_size, 4, 3) (se não houver as seguintes seqüências). E por que a saída ainda é "X"? Deveria ser "Y"?
Van
1
Aqui, presumo que X_1, X_2 ... X_6 é um número único. E três números (X_1, X_2, X_3) formam um vetor de forma (3). Um número (X_1) cria um vetor de forma (1,).
Van
2
@ Van, sua suposição está correta. Isso é interessante, então, basicamente, o modelo não aprende padrões além do número de etapas de tempo. Portanto, se eu tiver uma série temporal de 1000 e puder visualizar visualmente um padrão a cada 100 dias, devo definir o parâmetro time_steps pelo menos 100. Essa é uma observação correta?
Sachinruk 4/08/16
3
Sim. E se você pode coletar três recursos relevantes por dia, pode definir o tamanho do recurso como 3, como fez na segunda foto. Sob essa circunstância, a forma de entrada será (batch_size, 100, 3).
Van
1
e para responder sua primeira pergunta foi porque eu estava fazendo uma única série temporal. Por exemplo, preços de ações, então X e Y são da mesma série.
Sachinruk

Respostas:

173

Primeiro de tudo, você escolhe ótimos tutoriais ( 1 , 2 ) para começar.

O que significa Etapa do tempo : Time-steps==3em X.shape (Descrição da forma dos dados) significa que existem três caixas cor de rosa. Como em Keras, cada etapa requer uma entrada, portanto, o número de caixas verdes deve normalmente ser igual ao número de caixas vermelhas. A menos que você hackear a estrutura.

muitos para muitos vs. muitos para um : no keras, existe um return_sequencesparâmetro ao inicializar LSTMou GRUou SimpleRNN. Quando return_sequencesé False(por padrão), é muitos para um, como mostrado na figura. Sua forma de retorno é (batch_size, hidden_unit_length), que representa o último estado. Quando return_sequencesé True, então é muitos para muitos . Sua forma de retorno é(batch_size, time_step, hidden_unit_length)

O argumento dos recursos se torna relevante : Argumento do recurso significa "Qual é o tamanho da sua caixa vermelha" ou qual é a dimensão de entrada em cada etapa. Se você deseja prever, digamos, 8 tipos de informações de mercado, poderá gerar seus dados com feature==8.

Com estado : você pode procurar o código fonte . Ao inicializar o estado, se stateful==True, então, o estado do último treinamento será usado como estado inicial, caso contrário, ele gerará um novo estado. Ainda não liguei stateful. No entanto, eu discordo de que batch_sizesó pode ser 1 quando stateful==True.

Atualmente, você gera seus dados com dados coletados. Como as informações de estoque estão chegando como fluxo, em vez de esperar um dia para coletar todas as seqüências, você deseja gerar dados de entrada on-line enquanto treina / prevê com a rede. Se você tiver 400 ações compartilhando a mesma rede, poderá configurar batch_size==400.

furgão
fonte
Um pouco confuso sobre por que as caixas vermelhas e verdes precisam ser as mesmas. Você poderia olhar a edição que fiz (principalmente as novas fotos) e comentar?
Sachinruk
1
De fato. Verifique o documento: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 Se eu tiver uma série temporal multivariada, ainda devo usar lookback = 1?
Innm 31/07
1
Por que a dimensionalidade LSTM do espaço de saída (32) difere do número de neurônios (células LSTM)?
Fixo
1
Além de stateful=True: O tamanho do lote pode ser o que você quiser, mas você deve cumpri-lo. Se você construir o seu modelo com um tamanho de lote de 5, então todos fit(), predict()e métodos relacionados irá requerer um lote de 5. Note, porém, que este estado não será salvo com model.save(), o que pode parecer indesejável. No entanto, você pode adicionar manualmente o estado ao arquivo hdf5, se necessário. Mas, efetivamente, isso permite alterar o tamanho do lote, salvando e recarregando um modelo.
Jlh
192

Como complemento à resposta aceita, esta resposta mostra os comportamentos de keras e como obter cada imagem.

Comportamento geral de Keras

O processamento interno do keras padrão é sempre de muitos para muitos, como na figura a seguir (onde usei features=2pressão e temperatura, apenas como exemplo):

Muitos para muitos

Nesta imagem, aumentei o número de etapas para 5, para evitar confusão com as outras dimensões.

Para este exemplo:

  • Temos N tanques de óleo
  • Passamos 5 horas tomando medidas por hora (intervalos de tempo)
  • Medimos dois recursos:
    • Pressão P
    • Temperatura T

Nossa matriz de entrada deve ter o formato de (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]],
        ]

Entradas para janelas deslizantes

Freqüentemente, as camadas LSTM devem processar as seqüências inteiras. Dividir janelas pode não ser a melhor ideia. A camada possui estados internos sobre como uma sequência está evoluindo à medida que avança. O Windows elimina a possibilidade de aprender seqüências longas, limitando todas as sequências ao tamanho da janela.

Nas janelas, cada janela faz parte de uma longa sequência original, mas por Keras elas serão vistas como uma sequência independente:

        [     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]],
  ....
        ]

Observe que, nesse caso, você possui inicialmente apenas uma sequência, mas a divide em várias seqüências para criar janelas.

O conceito de "o que é uma sequência" é abstrato. As partes importantes são:

  • você pode ter lotes com muitas sequências individuais
  • o que faz as sequências serem sequências é que elas evoluem em etapas (geralmente etapas de tempo)

Atingir cada caso com "camadas únicas"

Atingir o padrão de muitos para muitos:

StandardManyToMany

Você pode conseguir muitos para muitos com uma camada LSTM simples, usando return_sequences=True:

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

#output_shape -> (batch_size, steps, units)

Conseguindo muitos para um:

Usando exatamente a mesma camada, o keras executará exatamente o mesmo pré-processamento interno, mas quando você usar return_sequences=False(ou simplesmente ignorar esse argumento), o keras descartará automaticamente as etapas anteriores à anterior:

ManyToOne

outputs = LSTM(units)(inputs)

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

Conseguir um para muitos

Agora, isso não é suportado apenas pelas camadas keras LSTM. Você precisará criar sua própria estratégia para multiplicar as etapas. Existem duas boas abordagens:

  • Crie uma entrada constante de várias etapas repetindo um tensor
  • Use a stateful=Truepara obter recorrentemente a saída de uma etapa e servi-la como a entrada da próxima etapa (necessidades output_features == input_features)

Um para muitos com vetor de repetição

Para ajustar ao comportamento padrão do keras, precisamos de entradas em etapas; portanto, simplesmente repetimos as entradas pelo comprimento que queremos:

OneToManyRepeat

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

#output_shape -> (batch_size, steps, units)

Compreendendo stateful = True

Agora vem um dos possíveis usos de stateful=True(além de evitar o carregamento de dados que não cabem na memória do computador de uma só vez)

Stateful nos permite inserir "partes" das seqüências em etapas. A diferença é:

  • Em stateful=False, o segundo lote contém novas seqüências inteiras, independentes do primeiro lote
  • Em stateful=True, o segundo lote continua o primeiro lote, estendendo as mesmas seqüências.

É como dividir as seqüências nas janelas também, com essas duas principais diferenças:

  • essas janelas não se sobrepõem !!
  • stateful=True verá essas janelas conectadas como uma única sequência longa

Em stateful=True, cada novo lote será interpretado como continuando o lote anterior (até você ligar model.reset_states()).

  • A sequência 1 no lote 2 continuará a sequência 1 no lote 1.
  • A sequência 2 no lote 2 continuará a sequência 2 no lote 1.
  • A sequência n no lote 2 continuará a sequência n no lote 1.

Exemplo de entradas, o lote 1 contém as etapas 1 e 2, o lote 2 contém as etapas 3 a 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]],
        ]                                  ]

Observe o alinhamento dos tanques no lote 1 e no lote 2! É por isso que precisamos shuffle=False(a menos que estejamos usando apenas uma sequência, é claro).

Você pode ter qualquer número de lotes, indefinidamente. (Para ter comprimentos variáveis ​​em cada lote, use input_shape=(None,features).

Um para muitos com stateful = True

Para o nosso caso aqui, usaremos apenas 1 etapa por lote, porque queremos obter uma etapa de saída e torná-la uma entrada.

Observe que o comportamento na imagem não é "causado por" stateful=True. Forçaremos esse comportamento em um loop manual abaixo. Neste exemplo, stateful=Trueé o que "nos permite" parar a sequência, manipular o que queremos e continuar de onde paramos.

OneToManyStateful

Honestamente, a abordagem de repetição é provavelmente a melhor escolha para este caso. Mas, como estamos analisando stateful=True, este é um bom exemplo. A melhor maneira de usar isso é o próximo caso "muitos para muitos".

Camada:

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) 

Agora, vamos precisar de um loop manual para previsões:

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

Muitos para muitos com stateful = True

Agora, aqui, temos uma aplicação muito boa: dada uma sequência de entrada, tente prever suas futuras etapas desconhecidas.

Estamos usando o mesmo método do "um para muitos" acima, com a diferença de que:

  • usaremos a própria sequência como dados de destino, um passo à frente
  • conhecemos parte da sequência (então descartamos essa parte dos resultados).

ManyToManyStateful

Camada (o mesmo que acima):

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) 

Treinamento:

Vamos treinar nosso modelo para prever o próximo passo das seqüências:

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)

Prever:

O primeiro estágio de nossa previsão envolve "ajustar os estados". É por isso que vamos prever a sequência inteira novamente, mesmo que já conheçamos essa parte:

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

Agora vamos ao loop, como no caso de um para muitos. Mas não redefina os estados aqui! . Queremos que o modelo saiba em qual etapa da sequência ele está (e sabe que está no primeiro novo passo por causa da previsão que acabamos de fazer acima)

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

Esta abordagem foi usada nestas respostas e arquivo:

Atingindo configurações complexas

Em todos os exemplos acima, mostrei o comportamento de "uma camada".

Obviamente, é possível empilhar muitas camadas umas sobre as outras, nem todas seguindo o mesmo padrão, e criar seus próprios modelos.

Um exemplo interessante que vem aparecendo é o "autoencoder" que possui um "muitos para um codificador" seguido por um "um para muitos" decodificador:

Codificador:

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)

Decodificador:

Usando o método "repeat";

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)

Autoencoder:

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

autoencoder = Model(inputs,outputs)

Treinar com fit(X,X)

Explicações adicionais

Se você quiser detalhes sobre como as etapas são calculadas nos LSTMs ou detalhes sobre os stateful=Truecasos acima, poderá ler mais nesta resposta: Dúvidas sobre `Noções básicas sobre LSTMs do Keras`

Daniel Möller
fonte
1
Uso muito interessante de stateful com o uso de saídas como entradas. Apenas como uma observação adicional, outra maneira de fazer isso seria usar a API Keras funcional (como você fez aqui, embora eu acredite que você possa ter usado a sequencial) e simplesmente reutilizar a mesma célula LSTM para cada etapa , enquanto passa o estado resultante e a saída da célula para si mesma. Ou seja my_cell = LSTM(num_output_features_per_timestep, return_state=True), seguido por um loop dea, _, c = my_cell(output_of_previous_time_step, initial_states=[a, c])
Jacob R
1
Células e comprimento são valores completamente independentes. Nenhuma das imagens representa o número de "células". Eles são todos para "comprimento".
Daniel Möller
1
@ DanielMöller Eu sei que é um pouco tarde, mas sua resposta realmente me chama a atenção. Um dos seus pontos quebrou tudo sobre a minha compreensão do que é o lote para LSTM. Você fornece exemplo com N tanques, cinco etapas e dois recursos. Eu acreditava que, se o lote for, por exemplo, dois, isso significa que duas amostras (tanques com 5 etapas 2 apresentam) serão alimentadas na rede e depois serão adaptados os pesos. Mas se eu entendi direito, você afirma que o lote 2 significa que os timestados das amostras serão divididos em 2 e a primeira metade de todas as amostras será alimentada com LSTM-> atualização de peso e depois com a segunda.
Viceriel # 12/18
1
Sim. Em um stateful = True, lote 1 = grupo de amostras, atualize. Em seguida, lote 2 = mais etapas para o mesmo grupo de amostras, atualize.
Daniel Möller
2
Eu gostaria de poder votar isso 100 vezes. Resposta super útil.
precisa saber é o seguinte
4

Quando você tem sequências de retorno em sua última camada de RNN, não pode usar uma camada Densa simples, e sim TimeDistributed.

Aqui está um exemplo de código que pode ajudar outras pessoas.

words = keras.layers.Input (batch_shape = (Nenhum, self.maxSequenceLength), nome = "entrada")

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