Am einfachsten ist das Auffüllen und Maskieren .
Es gibt drei allgemeine Möglichkeiten, Sequenzen mit variabler Länge zu verarbeiten:
- Polsterung und Maskierung (die für (3) verwendet werden kann),
- Chargengröße = 1 und
- Chargengröße> 1 mit Proben gleicher Länge in jeder Charge.
Polstern und Maskieren
Bei diesem Ansatz füllen wir die kürzeren Sequenzen mit einem speziellen Wert auf, der später maskiert (übersprungen) werden soll. Angenommen, jeder Zeitstempel hat die Dimension 2 und -10ist dann der spezielle Wert
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)
]
wird konvertiert zu
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)
]
Auf diese Weise hätten alle Sequenzen die gleiche Länge. Dann verwenden wir eine MaskingEbene, die diese speziellen Zeitstempel überspringt, als ob sie nicht existieren. Ein vollständiges Beispiel finden Sie am Ende.
Für die Fälle (2) und (3) müssen Sie die seq_lenvon LSTM auf Nonez
model.add(LSTM(units, input_shape=(None, dimension)))
Auf diese Weise akzeptiert LSTM Chargen mit unterschiedlichen Längen. Die Proben in jeder Charge müssen jedoch gleich lang sein. Dann müssen Sie einen einzuspeisen benutzerdefinierten Batch - Generator zu model.fit_generator(statt model.fit).
Ich habe am Ende ein vollständiges Beispiel für den einfachen Fall (2) (Chargengröße = 1) angegeben. Basierend auf diesem Beispiel und dem Link sollten Sie in der Lage sein, einen Generator für Fall (3) zu erstellen (Stapelgröße> 1). Insbesondere geben wir entweder (a) batch_sizeSequenzen mit derselben Länge zurück oder (b) wählen Sequenzen mit fast derselben Länge aus und füllen die kürzeren wie in Fall (1) auf und verwenden eine MaskingEbene vor der LSTM-Ebene, um das aufgefüllte zu ignorieren Zeitstempel, z
model.add(Masking(mask_value=special_value, input_shape=(None, dimension)))
model.add(LSTM(lstm_units))
wobei die erste Dimension von input_shapein Maskingwieder Nonedarin besteht, Chargen mit unterschiedlichen Längen zuzulassen.
Hier ist der Code für die Fälle (1) und (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)
Zusätzliche Hinweise
- Beachten Sie, dass, wenn wir ohne Maskierung auffüllen, der aufgefüllte Wert als tatsächlicher Wert betrachtet wird und somit zu Datenrauschen wird. Eine gepolsterte Temperatursequenz entspricht
[20, 21, 22, -10, -10]beispielsweise einem Sensorbericht mit zwei verrauschten (falschen) Messungen am Ende. Das Modell kann lernen, dieses Rauschen vollständig oder zumindest teilweise zu ignorieren, aber es ist sinnvoll, die Daten zuerst zu bereinigen, dh eine Maske zu verwenden.