A maneira mais fácil é usar Preenchimento e Mascaramento .
Existem três maneiras gerais de lidar com seqüências de tamanho variável:
- Preenchimento e máscara (que pode ser usado para (3)),
- Tamanho do lote = 1 e
- 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 -10
seja 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 Masking
camada 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_len
LSTM 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_size
seqüê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 Masking
camada 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_shape
in Masking
é novamente None
para 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
- 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.