Wie füttere ich LSTM mit verschiedenen Eingangsarraygrößen?


18

LSTMWie ist es möglich , ein Netzwerk zu schreiben und es mit unterschiedlichen Eingangsarraygrößen zu versorgen?

Zum Beispiel möchte ich Sprach- oder Textnachrichten in einer anderen Sprache erhalten und übersetzen. Die erste Eingabe ist vielleicht "Hallo", aber die zweite ist "Wie geht es dir?". Wie kann ich ein Design entwerfen LSTM, das unterschiedliche Eingangsarraygrößen verarbeiten kann?

Ich benutze die KerasImplementierung von LSTM.

Antworten:


24

Am einfachsten ist das Auffüllen und Maskieren .

Es gibt drei allgemeine Möglichkeiten, Sequenzen mit variabler Länge zu verarbeiten:

  1. Polsterung und Maskierung (die für (3) verwendet werden kann),
  2. Chargengröße = 1 und
  3. 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

  1. 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.

Vielen Dank Esmailian für Ihr vollständiges Beispiel. Nur eine Frage: Was ist der Unterschied zwischen Padding + Masking und nur Padding (wie in der anderen Antwort vorgeschlagen)? Werden wir einen erheblichen Einfluss auf das Endergebnis haben?
user145959

@ user145959 mein Vergnügen! Ich habe am Ende eine Notiz hinzugefügt.
Esmailian

Wow eine tolle Antwort! Es heißt Eimer, oder?
Aditya

1
@ Aditya Danke Aditya! Ich denke, Bucketing ist die Aufteilung einer großen Sequenz in kleinere Blöcke, aber Sequenzen in jedem Stapel sind nicht unbedingt Blöcke derselben (größeren) Sequenz, sie können unabhängige Datenpunkte sein.
Esmailian

1
@ flow2k Es spielt keine Rolle, Pads werden komplett ignoriert. Schauen Sie sich diese Frage an .
Esmailian

3

Wir verwenden LSTM-Ebenen mit mehreren Eingabegrößen. Sie müssen sie jedoch verarbeiten, bevor sie dem LSTM zugeführt werden.

Auffüllen der Sequenzen:

Sie benötigen das Pad, um die Sequenzen unterschiedlicher Länge auf eine feste Länge zu bringen. Für diese Vorverarbeitung müssen Sie die maximale Länge der Sequenzen in Ihrem Dataset bestimmen.

Die Werte werden meistens mit dem Wert 0 aufgefüllt. Sie können dies in Keras tun mit:

y = keras.preprocessing.sequence.pad_sequences( x , maxlen=10 )
  • Wenn die Sequenz kürzer als die maximale Länge ist, werden Nullen angehängt, bis die Länge der maximalen Länge entspricht.

  • Wenn die Sequenz länger als die maximale Länge ist, wird die Sequenz auf die maximale Länge gekürzt.


3
Alles auf eine feste Länge zu polstern ist Platzverschwendung.
Aditya

Ich stimme @Aditya zu und es entstehen auch Rechenkosten. Aber ist es nicht so, dass eine vereinfachte Polsterung immer noch weit verbreitet ist? Keras hat sogar eine Funktion nur dafür. Vielleicht liegt dies daran, dass andere, effizientere und herausforderndere Lösungen keinen signifikanten Gewinn für die Modellleistung bieten? Wenn jemand Erfahrung hat oder Vergleiche angestellt hat, wiegen Sie sich bitte ein.
flow2k
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.