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.

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.

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.


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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.