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 -10
ist 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 Masking
Ebene, 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_len
von LSTM auf None
z
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_size
Sequenzen 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 Masking
Ebene 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_shape
in Masking
wieder None
darin 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.