Treinando uma RNN com exemplos de diferentes comprimentos em Keras

61

Estou tentando começar a aprender sobre RNNs e estou usando o Keras. Entendo a premissa básica das camadas RNN e LSTM de baunilha, mas estou tendo problemas para entender um certo ponto técnico do treinamento.

Na documentação do keras , diz que a entrada para uma camada RNN deve ter forma (batch_size, timesteps, input_dim). Isso sugere que todos os exemplos de treinamento têm um comprimento de sequência fixo, a saber timesteps.

Mas isso não é especialmente típico, é? Talvez eu queira que o RNN opere em sentenças de comprimentos variados. Quando treiná-lo em algum corpus, alimentarei lotes de frases, com comprimentos diferentes.

Suponho que a coisa mais óbvia a fazer seria encontrar o comprimento máximo de qualquer sequência no conjunto de treinamento e zerá-lo. Mas então isso significa que não posso fazer previsões no tempo de teste com tamanho de entrada maior que isso?

Esta é uma pergunta sobre a implementação específica de Keras, suponho, mas também estou perguntando o que as pessoas normalmente fazem quando enfrentam esse tipo de problema em geral.

Tac-Tics
fonte
@kbrose está correto. No entanto, tenho uma preocupação. No exemplo, você tem um gerador muito especial de rendimentos infinitos. Mais importante, ele foi projetado para produzir lotes de tamanho 1000. Na prática, isso é muito difícil de satisfazer, se não impossível. Você precisa reorganizar suas entradas para que aquelas com o mesmo comprimento sejam organizadas juntas e é necessário definir cuidadosamente as posições de divisão de lotes. Além disso, você não tem chance de embaralhar os lotes. Portanto, minha opinião é: nunca use entrada de comprimento variável no Keras, a menos que você saiba exatamente o que está fazendo. Use o preenchimento e defina a Maskingcamada como ignor
Bs He

Respostas:

57

Isso sugere que todos os exemplos de treinamento têm um comprimento de sequência fixo, a saber timesteps.

Isso não está correto, pois essa dimensão pode ser None, ou seja, comprimento variável. Dentro de um único lote , você deve ter o mesmo número de timesteps (normalmente é onde você encontra preenchimento de 0 e máscara). Mas entre lotes não existe essa restrição. Durante a inferência, você pode ter qualquer comprimento.

Código de exemplo que cria lotes aleatórios de dados de treinamento.

from keras.models import Sequential
from keras.layers import LSTM, Dense, TimeDistributed
from keras.utils import to_categorical
import numpy as np

model = Sequential()

model.add(LSTM(32, return_sequences=True, input_shape=(None, 5)))
model.add(LSTM(8, return_sequences=True))
model.add(TimeDistributed(Dense(2, activation='sigmoid')))

print(model.summary(90))

model.compile(loss='categorical_crossentropy',
              optimizer='adam')

def train_generator():
    while True:
        sequence_length = np.random.randint(10, 100)
        x_train = np.random.random((1000, sequence_length, 5))
        # y_train will depend on past 5 timesteps of x
        y_train = x_train[:, :, 0]
        for i in range(1, 5):
            y_train[:, i:] += x_train[:, :-i, i]
        y_train = to_categorical(y_train > 2.5)
        yield x_train, y_train

model.fit_generator(train_generator(), steps_per_epoch=30, epochs=10, verbose=1)

E é isso que imprime. Observe que as formas de saída estão (None, None, x)indicando tamanho de lote variável e tamanho de timestep variável.

__________________________________________________________________________________________
Layer (type)                            Output Shape                        Param #
==========================================================================================
lstm_1 (LSTM)                           (None, None, 32)                    4864
__________________________________________________________________________________________
lstm_2 (LSTM)                           (None, None, 8)                     1312
__________________________________________________________________________________________
time_distributed_1 (TimeDistributed)    (None, None, 2)                     18
==========================================================================================
Total params: 6,194
Trainable params: 6,194
Non-trainable params: 0
__________________________________________________________________________________________
Epoch 1/10
30/30 [==============================] - 6s 201ms/step - loss: 0.6913
Epoch 2/10
30/30 [==============================] - 4s 137ms/step - loss: 0.6738
...
Epoch 9/10
30/30 [==============================] - 4s 136ms/step - loss: 0.1643
Epoch 10/10
30/30 [==============================] - 4s 142ms/step - loss: 0.1441
kbrose
fonte
Obrigado por isso. No entanto, se 0 preenchermos as seqüências, isso afetará os estados ocultos e a célula de memória, porque continuaremos passando x_t como 0s; quando, na verdade, não deve passar nada. No normal fit(), podemos passar o sequence_lenthparâmetro para especificar o comprimento da sequência para excluí-lo. Parece que a abordagem do gerador não permite ignorar 0 seqüências?
GRS
11
@GRS Seu gerador pode retornar três tuplas (inputs, targets, sample_weights)e você pode definir sample_weightsseus pads 0 como 0. No entanto, não tenho certeza se isso funcionaria perfeitamente para RNNs bidirecionais.
Kbrose #
Isso foi útil, mas eu gostaria que também incluísse um exemplo de uso model.predict_generatorcom um conjunto de testes. Quando tento prever com um gerador, recebo um erro em relação à concatenação (o conjunto de testes também possui seqüências de comprimento variável). Minha solução foi usar o padrão de model.predictmaneira hacky. Talvez isso seja mais adequado para uma nova pergunta?
mickey
@mickey que soa como uma pergunta diferente. Esta pergunta é sobre treinamento, não predição.
kbrose 29/01
Se a pergunta nos comentários foi realmente feita como uma nova pergunta, você pode vincular a ela?
Itamar Mushkin 27/10
7

@kbrose parece ter uma solução melhor

Suponho que a coisa mais óbvia a fazer seria encontrar o comprimento máximo de qualquer sequência no conjunto de treinamento e zerá-lo.

Geralmente é uma boa solução. Talvez tente o comprimento máximo da sequência + 100. Use o que for melhor para o seu aplicativo.

Mas então isso significa que não posso fazer previsões no momento do teste com tamanho de entrada maior que isso?

Não necessariamente. A razão pela qual um comprimento fixo é usado no keras é porque ele melhora muito o desempenho ao criar tensores de formas fixas. Mas isso é apenas para treinamento. Após o treinamento, você aprenderá os pesos certos para sua tarefa.

Vamos supor que, após o treinamento por horas, você perceba que o comprimento máximo do seu modelo não era grande / pequeno o suficiente e agora precisa alterar as etapas de tempo, basta extrair os pesos aprendidos do modelo antigo, criar um novo modelo com as novas etapas de tempo e injetar os pesos aprendidos nele.

Você provavelmente pode fazer isso usando algo como:

new_model.set_weights(old_model.get_weights())

Eu ainda não tentei. Experimente e publique seus resultados aqui para benefício de todos. Aqui estão alguns links: um dois

aneesh joshi
fonte
11
Você pode realmente ter entradas de comprimento variável, sem necessidade de introduzir hacks como max length + 100. Veja minha resposta, por exemplo, código.
Kbrose
11
Transferir os pesos para um modelo com mais timesteps realmente funciona perfeitamente bem! Aumentei o timesteps Bidirectional(LSTM)()e as RepeatVector()camadas, e as previsões são perfeitamente viáveis.
komodovaran_ 25/03
@ kbrose Isso não é um truque, é como você normalmente faz. O uso de um lote de tamanho de um é muito lento e o keras habilita camadas de máscara, para que a máscara não afete a perda.
Ferus