Como alimentar LSTM com diferentes tamanhos de matriz de entrada?

18

Se eu gosto de escrever uma LSTMrede e alimentá-la com diferentes tamanhos de matriz de entrada, como isso é possível?

Por exemplo, quero receber mensagens de voz ou de texto em um idioma diferente e traduzi-las. Portanto, a primeira entrada talvez seja "olá", mas a segunda é "como você está?" Como posso projetar um LSTMque possa lidar com diferentes tamanhos de matriz de entrada?

Estou usando a Kerasimplementação de LSTM.

user145959
fonte

Respostas:

24

A maneira mais fácil é usar Preenchimento e Mascaramento .

Existem três maneiras gerais de lidar com seqüências de tamanho variável:

  1. Preenchimento e máscara (que pode ser usado para (3)),
  2. Tamanho do lote = 1 e
  3. Tamanho do lote> 1, com amostras de comprimento igual em cada lote.

Preenchimento e máscara

Nesta abordagem, preenchemos as seqüências mais curtas com um valor especial a ser mascarado (ignorado) posteriormente. Por exemplo, suponha que cada carimbo de data / hora tenha a dimensão 2 e -10seja o valor especial;

X = [

  [[1,    1.1],
   [0.9, 0.95]],  # sequence 1 (2 timestamps)

  [[2,    2.2],
   [1.9, 1.95],
   [1.8, 1.85]],  # sequence 2 (3 timestamps)

]

será convertido para

X2 = [

  [[1,    1.1],
   [0.9, 0.95],
   [-10, -10]], # padded sequence 1 (3 timestamps)

  [[2,    2.2],
   [1.9, 1.95],
   [1.8, 1.85]], # sequence 2 (3 timestamps)
]

Dessa forma, todas as seqüências teriam o mesmo comprimento. Em seguida, usamos uma Maskingcamada que ignora esses carimbos de data / hora especiais como se eles não existissem. Um exemplo completo é dado no final.

Para os casos (2) e (3), é necessário definir o seq_lenLSTM como None, por exemplo,

model.add(LSTM(units, input_shape=(None, dimension)))

dessa forma, o LSTM aceita lotes com diferentes comprimentos; embora as amostras dentro de cada lote devam ter o mesmo comprimento. Em seguida, você precisa alimentar um gerador de lotes personalizado para model.fit_generator(em vez de model.fit).

Forneci um exemplo completo para o caso simples (2) (tamanho do lote = 1) no final. Com base neste exemplo e no link, você poderá criar um gerador para o caso (3) (tamanho do lote> 1). Especificamente, (a) retornamos batch_sizeseqüências com o mesmo comprimento, ou (b) selecionamos sequências com quase o mesmo comprimento e colocamos as mais curtas da mesma forma que no caso (1), e usamos uma Maskingcamada antes da camada LSTM para ignorar as registros de data e hora, por exemplo

model.add(Masking(mask_value=special_value, input_shape=(None, dimension)))
model.add(LSTM(lstm_units))

onde a primeira dimensão de input_shapein Maskingé novamente Nonepara permitir lotes com comprimentos diferentes.

Aqui está o código para os casos (1) e (2):

from keras import Sequential
from keras.utils import Sequence
from keras.layers import LSTM, Dense, Masking
import numpy as np


class MyBatchGenerator(Sequence):
    'Generates data for Keras'
    def __init__(self, X, y, batch_size=1, shuffle=True):
        'Initialization'
        self.X = X
        self.y = y
        self.batch_size = batch_size
        self.shuffle = shuffle
        self.on_epoch_end()

    def __len__(self):
        'Denotes the number of batches per epoch'
        return int(np.floor(len(self.y)/self.batch_size))

    def __getitem__(self, index):
        return self.__data_generation(index)

    def on_epoch_end(self):
        'Shuffles indexes after each epoch'
        self.indexes = np.arange(len(self.y))
        if self.shuffle == True:
            np.random.shuffle(self.indexes)

    def __data_generation(self, index):
        Xb = np.empty((self.batch_size, *X[index].shape))
        yb = np.empty((self.batch_size, *y[index].shape))
        # naively use the same sample over and over again
        for s in range(0, self.batch_size):
            Xb[s] = X[index]
            yb[s] = y[index]
        return Xb, yb


# Parameters
N = 1000
halfN = int(N/2)
dimension = 2
lstm_units = 3

# Data
np.random.seed(123)  # to generate the same numbers
# create sequence lengths between 1 to 10
seq_lens = np.random.randint(1, 10, halfN)
X_zero = np.array([np.random.normal(0, 1, size=(seq_len, dimension)) for seq_len in seq_lens])
y_zero = np.zeros((halfN, 1))
X_one = np.array([np.random.normal(1, 1, size=(seq_len, dimension)) for seq_len in seq_lens])
y_one = np.ones((halfN, 1))
p = np.random.permutation(N)  # to shuffle zero and one classes
X = np.concatenate((X_zero, X_one))[p]
y = np.concatenate((y_zero, y_one))[p]

# Batch = 1
model = Sequential()
model.add(LSTM(lstm_units, input_shape=(None, dimension)))
model.add(Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='rmsprop', metrics=['accuracy'])
print(model.summary())
model.fit_generator(MyBatchGenerator(X, y, batch_size=1), epochs=2)

# Padding and Masking
special_value = -10.0
max_seq_len = max(seq_lens)
Xpad = np.full((N, max_seq_len, dimension), fill_value=special_value)
for s, x in enumerate(X):
    seq_len = x.shape[0]
    Xpad[s, 0:seq_len, :] = x
model2 = Sequential()
model2.add(Masking(mask_value=special_value, input_shape=(max_seq_len, dimension)))
model2.add(LSTM(lstm_units))
model2.add(Dense(1, activation='sigmoid'))
model2.compile(loss='binary_crossentropy', optimizer='rmsprop', metrics=['accuracy'])
print(model2.summary())
model2.fit(Xpad, y, epochs=50, batch_size=32)

Notas extras

  1. Observe que, se pressionarmos sem mascarar, o valor acolchoado será considerado como valor real, tornando-se ruído nos dados. Por exemplo, uma sequência de temperatura acolchoada [20, 21, 22, -10, -10]será igual a um relatório de sensor com duas medições ruidosas (erradas) no final. O modelo pode aprender a ignorar esse ruído completamente ou pelo menos parcialmente, mas é razoável limpar os dados primeiro, ou seja, use uma máscara.
Esmailiano
fonte
Muito obrigado esmailiano por seu exemplo completo. Apenas uma pergunta: qual é a diferença entre usar preenchimento + mascaramento e usar apenas preenchimento (como o que a outra resposta sugeriu)? Veremos um efeito considerável no resultado final?
user145959
@ user145959 meu prazer! Eu adicionei uma nota no final.
Esmailian 7/04/19
Uau, uma ótima resposta! É chamado de balde, certo?
Aditya
11
@Aditya Thanks Aditya! Penso que o bucketing é o particionamento de uma sequência grande em partes menores, mas as seqüências em cada lote não são necessariamente partes da mesma sequência (maior), elas podem ser pontos de dados independentes.
Esmailian 08/04/19
11
@ flow2k Não importa, os pads são completamente ignorados. Dê uma olhada nesta pergunta .
Esmailian 19/08/19
3

Usamos camadas LSTM com vários tamanhos de entrada. Mas você precisa processá-los antes que eles sejam alimentados pelo LSTM.

Preenchendo as seqüências:

Você precisa do bloco com as seqüências de comprimento variável para um comprimento fixo. Para esse pré-processamento, você precisa determinar o tamanho máximo das seqüências no seu conjunto de dados.

Os valores são preenchidos principalmente pelo valor 0. Você pode fazer isso no Keras com:

y = keras.preprocessing.sequence.pad_sequences( x , maxlen=10 )
  • Se a sequência for menor que o comprimento máximo, os zeros serão anexados até que ele tenha um comprimento igual ao comprimento máximo.

  • Se a sequência for maior que o comprimento máximo, a sequência será cortada para o comprimento máximo.

Shubham Panchal
fonte
3
Estofar tudo em um comprimento fixo é desperdício de espaço.
Aditya
Concordo com o @Aditya, e também incorre em custos de computação. Mas não é que o preenchimento simplista ainda seja amplamente utilizado? Keras ainda tem uma função apenas para isso. Talvez isso ocorra porque outras soluções mais eficientes e desafiadoras não fornecem ganho significativo de desempenho do modelo? Se alguém tem experiência ou fez comparações, por favor pesar.
flow2k