Probleme mit dem LSTM-Autoencoder

Dec 09 2020

TLDR:

Autoencoder passt die Rekonstruktion von Zeitreihen an und sagt nur den Durchschnittswert voraus.

Frageneinstellung:

Hier ist eine Zusammenfassung meines Versuchs eines Sequenz-zu-Sequenz-Autoencoders. Dieses Bild wurde von diesem Papier genommen:https://arxiv.org/pdf/1607.00148.pdf

Encoder: Standard-LSTM-Schicht. Die Eingabesequenz wird im endgültigen verborgenen Zustand codiert.

Decoder: LSTM Cell (glaube ich!). Rekonstruieren Sie die Sequenz Element für Element, beginnend mit dem letzten Element x[N].

Der Decoder-Algorithmus ist für eine Sequenz von Längen wie folgt N:

  1. Erhalten des anfänglichen verborgenen Zustands des Decoders hs[N]: Verwenden Sie einfach den endgültigen verborgenen Zustand des Encoders.
  2. Rekonstruieren Sie das letzte Element in der Sequenz : x[N]= w.dot(hs[N]) + b.
  3. Gleiches Muster für andere Elemente: x[i]= w.dot(hs[i]) + b
  4. Verwenden Sie x[i]und hs[i]als Eingaben LSTMCell, um x[i-1]und zu erhaltenhs[i-1]

Mindestarbeitsbeispiel:

Hier ist meine Implementierung, beginnend mit dem Encoder:

class SeqEncoderLSTM(nn.Module):
    def __init__(self, n_features, latent_size):
        super(SeqEncoderLSTM, self).__init__()
        
        self.lstm = nn.LSTM(
            n_features, 
            latent_size, 
            batch_first=True)
        
    def forward(self, x):
        _, hs = self.lstm(x)
        return hs

Decoderklasse:

class SeqDecoderLSTM(nn.Module):
    def __init__(self, emb_size, n_features):
        super(SeqDecoderLSTM, self).__init__()
        
        self.cell = nn.LSTMCell(n_features, emb_size)
        self.dense = nn.Linear(emb_size, n_features)
        
    def forward(self, hs_0, seq_len):
        
        x = torch.tensor([])
        
        # Final hidden and cell state from encoder
        hs_i, cs_i = hs_0
        
        # reconstruct first element with encoder output
        x_i = self.dense(hs_i)
        x = torch.cat([x, x_i])
        
        # reconstruct remaining elements
        for i in range(1, seq_len):
            hs_i, cs_i = self.cell(x_i, (hs_i, cs_i))
            x_i = self.dense(hs_i)
            x = torch.cat([x, x_i])
        return x

Die beiden zusammenbringen:

class LSTMEncoderDecoder(nn.Module):
    def __init__(self, n_features, emb_size):
        super(LSTMEncoderDecoder, self).__init__()
        self.n_features = n_features
        self.hidden_size = emb_size

        self.encoder = SeqEncoderLSTM(n_features, emb_size)
        self.decoder = SeqDecoderLSTM(emb_size, n_features)
    
    def forward(self, x):
        seq_len = x.shape[1]
        hs = self.encoder(x)
        hs = tuple([h.squeeze(0) for h in hs])
        out = self.decoder(hs, seq_len)
        return out.unsqueeze(0)        

Und hier ist meine Trainingsfunktion:

def train_encoder(model, epochs, trainload, testload=None, criterion=nn.MSELoss(), optimizer=optim.Adam, lr=1e-6,  reverse=False):

    device = 'cuda' if torch.cuda.is_available() else 'cpu'
    print(f'Training model on {device}')
    model = model.to(device)
    opt = optimizer(model.parameters(), lr)

    train_loss = []
    valid_loss = []

    for e in tqdm(range(epochs)):
        running_tl = 0
        running_vl = 0
        for x in trainload:
            x = x.to(device).float()
            opt.zero_grad()
            x_hat = model(x)
            if reverse:
                x = torch.flip(x, [1])
            loss = criterion(x_hat, x)
            loss.backward()
            opt.step()
            running_tl += loss.item()

        if testload is not None:
            model.eval()
            with torch.no_grad():
                for x in testload:
                    x = x.to(device).float()
                    loss = criterion(model(x), x)
                    running_vl += loss.item()
                valid_loss.append(running_vl / len(testload))
            model.train()
            
        train_loss.append(running_tl / len(trainload))
    
    return train_loss, valid_loss

Daten:

Großer Datensatz von Ereignissen aus den Nachrichten (ICEWS). Es gibt verschiedene Kategorien, die jedes Ereignis beschreiben. Ich habe diese Variablen zunächst einmal im laufenden Betrieb codiert und die Daten auf 274 Dimensionen erweitert. Um das Modell zu debuggen, habe ich es jedoch auf eine einzelne Sequenz reduziert, die 14 Zeitschritte lang ist und nur 5 Variablen enthält. Hier ist die Sequenz, die ich zu überpassen versuche:

tensor([[0.5122, 0.0360, 0.7027, 0.0721, 0.1892],
        [0.5177, 0.0833, 0.6574, 0.1204, 0.1389],
        [0.4643, 0.0364, 0.6242, 0.1576, 0.1818],
        [0.4375, 0.0133, 0.5733, 0.1867, 0.2267],
        [0.4838, 0.0625, 0.6042, 0.1771, 0.1562],
        [0.4804, 0.0175, 0.6798, 0.1053, 0.1974],
        [0.5030, 0.0445, 0.6712, 0.1438, 0.1404],
        [0.4987, 0.0490, 0.6699, 0.1536, 0.1275],
        [0.4898, 0.0388, 0.6704, 0.1330, 0.1579],
        [0.4711, 0.0390, 0.5877, 0.1532, 0.2201],
        [0.4627, 0.0484, 0.5269, 0.1882, 0.2366],
        [0.5043, 0.0807, 0.6646, 0.1429, 0.1118],
        [0.4852, 0.0606, 0.6364, 0.1515, 0.1515],
        [0.5279, 0.0629, 0.6886, 0.1514, 0.0971]], dtype=torch.float64)

Und hier ist die benutzerdefinierte DatasetKlasse:

class TimeseriesDataSet(Dataset):
    def __init__(self, data, window, n_features, overlap=0):
        super().__init__()
        if isinstance(data, (np.ndarray)):
            data = torch.tensor(data)
        elif isinstance(data, (pd.Series, pd.DataFrame)):
            data = torch.tensor(data.copy().to_numpy())
        else: 
            raise TypeError(f"Data should be ndarray, series or dataframe. Found {type(data)}.")
        
        self.n_features = n_features
        self.seqs = torch.split(data, window)
        
    def __len__(self):
        return len(self.seqs)
    
    def __getitem__(self, idx):
        try:    
            return self.seqs[idx].view(-1, self.n_features)
        except TypeError:
            raise TypeError("Dataset only accepts integer index/slices, not lists/arrays.")

Problem:

Das Modell lernt nur den Durchschnitt, egal wie komplex ich das Modell mache oder wie lange ich es trainiere.

Vorausgesagt / Rekonstruktion:

Tatsächlich:

Meine Forschung:

Dieses Problem ist identisch mit dem in dieser Frage beschriebenen: Der LSTM-Autoencoder gibt immer den Durchschnitt der Eingabesequenz zurück

Das Problem in diesem Fall bestand darin, dass die Zielfunktion die Zielzeitreihen vor der Berechnung des Verlusts mittelte. Dies war auf einige Sendefehler zurückzuführen, da der Autor nicht die richtigen Eingaben für die Zielfunktion hatte.

In meinem Fall sehe ich dies nicht als Problem. Ich habe überprüft und doppelt überprüft, ob alle meine Abmessungen / Größen übereinstimmen. Ich bin ratlos.

Andere Dinge, die ich versucht habe

  1. Ich habe dies mit verschiedenen Sequenzlängen von 7 Zeitschritten bis 100 Zeitschritten versucht.
  2. Ich habe es mit einer unterschiedlichen Anzahl von Variablen in der Zeitreihe versucht. Ich habe es mit univariate bis zu allen 274 Variablen versucht, die die Daten enthalten.
  3. Ich habe es mit verschiedenen reductionParametern am nn.MSELossModul versucht . Die Zeitung fordert sum, aber ich habe beides versucht sumund mean. Kein Unterschied.
  4. Das Papier fordert die Rekonstruktion der Sequenz in umgekehrter Reihenfolge (siehe Grafik oben). Ich habe diese Methode unter Verwendung der flipudursprünglichen Eingabe ausprobiert (nach dem Training, aber vor der Berechnung des Verlusts). Das macht keinen Unterschied.
  5. Ich habe versucht, das Modell komplexer zu gestalten, indem ich dem Encoder eine zusätzliche LSTM-Ebene hinzugefügt habe.
  6. Ich habe versucht, mit dem latenten Raum zu spielen. Ich habe versucht, von 50% der eingegebenen Anzahl von Funktionen auf 150%.
  7. Ich habe versucht, eine einzelne Sequenz zu überpassen ( siehe Abschnitt Daten oben).

Frage:

Was veranlasst mein Modell, den Durchschnitt vorherzusagen und wie behebe ich ihn?

Antworten

7 SzymonMaszke Dec 16 2020 at 05:04

Okay, nach einigem Debuggen denke ich, dass ich die Gründe kenne.

TLDR

  • Sie versuchen, den nächsten Zeitschrittwert anstelle der Differenz zwischen dem aktuellen und dem vorherigen Zeitschritt vorherzusagen
  • Ihre hidden_featuresAnzahl ist zu klein, sodass das Modell nicht einmal eine einzelne Probe aufnehmen kann

Analyse

Code verwendet

Beginnen wir mit dem Code (Modell ist das gleiche):

import seaborn as sns
import matplotlib.pyplot as plt

def get_data(subtract: bool = False):
    # (1, 14, 5)
    input_tensor = torch.tensor(
        [
            [0.5122, 0.0360, 0.7027, 0.0721, 0.1892],
            [0.5177, 0.0833, 0.6574, 0.1204, 0.1389],
            [0.4643, 0.0364, 0.6242, 0.1576, 0.1818],
            [0.4375, 0.0133, 0.5733, 0.1867, 0.2267],
            [0.4838, 0.0625, 0.6042, 0.1771, 0.1562],
            [0.4804, 0.0175, 0.6798, 0.1053, 0.1974],
            [0.5030, 0.0445, 0.6712, 0.1438, 0.1404],
            [0.4987, 0.0490, 0.6699, 0.1536, 0.1275],
            [0.4898, 0.0388, 0.6704, 0.1330, 0.1579],
            [0.4711, 0.0390, 0.5877, 0.1532, 0.2201],
            [0.4627, 0.0484, 0.5269, 0.1882, 0.2366],
            [0.5043, 0.0807, 0.6646, 0.1429, 0.1118],
            [0.4852, 0.0606, 0.6364, 0.1515, 0.1515],
            [0.5279, 0.0629, 0.6886, 0.1514, 0.0971],
        ]
    ).unsqueeze(0)

    if subtract:
        initial_values = input_tensor[:, 0, :]
        input_tensor -= torch.roll(input_tensor, 1, 1)
        input_tensor[:, 0, :] = initial_values
    return input_tensor


if __name__ == "__main__":
    torch.manual_seed(0)

    HIDDEN_SIZE = 10
    SUBTRACT = False

    input_tensor = get_data(SUBTRACT)
    model = LSTMEncoderDecoder(input_tensor.shape[-1], HIDDEN_SIZE)
    optimizer = torch.optim.Adam(model.parameters())
    criterion = torch.nn.MSELoss()
    for i in range(1000):
        outputs = model(input_tensor)
        loss = criterion(outputs, input_tensor)
        loss.backward()
        optimizer.step()
        optimizer.zero_grad()
        print(f"{i}: {loss}")
        if loss < 1e-4:
            break

    # Plotting
    sns.lineplot(data=outputs.detach().numpy().squeeze())
    sns.lineplot(data=input_tensor.detach().numpy().squeeze())
    plt.show()

Was es macht:

  • get_datafunktioniert entweder mit den von Ihnen angegebenen Daten, wenn subtract=Falseoder (wenn subtract=True) der Wert des vorherigen Zeitschritts vom aktuellen Zeitschritt subtrahiert wird
  • Der Rest des Codes optimiert das Modell, bis der 1e-4Verlust erreicht ist (damit wir vergleichen können, wie die Kapazität und die Erhöhung des Modells helfen und was passiert, wenn wir die Differenz von Zeitschritten anstelle von Zeitschritten verwenden).

Wir werden nur variieren HIDDEN_SIZEund SUBTRACTParameter!

KEIN SUBTRAKT, KLEINES MODELL

  • HIDDEN_SIZE=5
  • SUBTRACT=False

In diesem Fall erhalten wir eine gerade Linie. Das Modell ist nicht in der Lage, die in den Daten dargestellten Phänomene anzupassen und zu erfassen (daher die von Ihnen erwähnten flachen Linien).

1000 Iterationslimit erreicht

SUBTRAKT, KLEINES MODELL

  • HIDDEN_SIZE=5
  • SUBTRACT=True

Ziele sind jetzt weit entfernt von flachen Linien , aber das Modell kann aufgrund zu geringer Kapazität nicht passen.

1000 Iterationslimit erreicht

KEIN SUBTRAKT, GRÖSSERES MODELL

  • HIDDEN_SIZE=100
  • SUBTRACT=False

Es wurde viel besser und unser Ziel wurde nach 942Schritten getroffen. Keine flachen Linien mehr, die Modellkapazität scheint in Ordnung zu sein (für dieses einzelne Beispiel!)

SUBTRAKT, GRÖSSERES MODELL

  • HIDDEN_SIZE=100
  • SUBTRACT=True

Obwohl das Diagramm nicht so hübsch aussieht, haben wir den gewünschten Verlust bereits nach 215Iterationen erreicht.

Schließlich

  • Verwenden Sie normalerweise unterschiedliche Zeitschritte anstelle von Zeitschritten (oder eine andere Transformation, siehe hier für weitere Informationen dazu). In anderen Fällen versucht das neuronale Netzwerk einfach, die Ausgabe des vorherigen Schritts zu kopieren (da dies am einfachsten ist). Einige Minima werden auf diese Weise gefunden und das Verlassen erfordert mehr Kapazität.
  • Wenn Sie den Unterschied zwischen Zeitschritten verwenden, gibt es keine Möglichkeit, den Trend aus dem vorherigen Zeitschritt zu "extrapolieren". Das neuronale Netzwerk muss lernen, wie sich die Funktion tatsächlich ändert
  • Verwenden Sie ein größeres Modell (für den gesamten Datensatz sollten Sie etwas ausprobieren, wie 300ich denke), aber Sie können dieses Modell einfach optimieren.
  • Nicht benutzen flipud. Verwenden Sie bidirektionale LSTMs. Auf diese Weise können Sie Informationen aus dem Vorwärts- und Rückwärtsdurchlauf von LSTM abrufen (nicht zu verwechseln mit Backprop!). Dies sollte auch Ihre Punktzahl steigern

Fragen

Okay, Frage 1: Sie sagen, dass ich für die Variable x in der Zeitreihe das Modell trainieren sollte, um x [i] - x [i-1] anstatt den Wert von x [i] zu lernen? Dolmetsche ich richtig?

Ja genau. Der Unterschied beseitigt den Drang des neuronalen Netzwerks, seine Vorhersagen zu sehr auf den vergangenen Zeitschritt zu stützen (indem einfach der letzte Wert ermittelt und möglicherweise ein wenig geändert wird).

Frage 2: Sie sagten, meine Berechnungen für einen Engpass von Null seien falsch. Nehmen wir zum Beispiel an, ich verwende ein einfaches dichtes Netzwerk als Auto-Encoder. Der richtige Engpass hängt in der Tat von den Daten ab. Wenn Sie den Engpass jedoch auf die gleiche Größe wie die Eingabe einstellen, erhalten Sie die Identitätsfunktion.

Ja, vorausgesetzt, es handelt sich nicht um eine Nichtlinearität, die die Sache schwieriger macht (siehe hier für einen ähnlichen Fall). Bei LSTMs gibt es Nichtlinearitäten, das ist ein Punkt.

Ein weiterer Grund ist, dass wir uns timestepsin einem einzigen Encoderzustand ansammeln . Im Wesentlichen müssten wir also timestepsIdentitäten in einem einzigen verborgenen und Zellzustand akkumulieren , was höchst unwahrscheinlich ist.

Ein letzter Punkt, abhängig von der Länge der Sequenz, neigen LSTMs dazu, einige der am wenigsten relevanten Informationen zu vergessen (dafür wurden sie entwickelt, um sich nicht nur an alles zu erinnern), was noch unwahrscheinlicher ist.

Ist num_features * num_timesteps kein Flaschenhals mit der gleichen Größe wie die Eingabe und sollte es dem Modell daher nicht helfen, die Identität zu lernen?

Es ist, aber es wird davon ausgegangen, dass Sie num_timestepsfür jeden Datenpunkt, was selten der Fall ist, hier sein könnten. Über die Identität und warum es schwierig ist, mit Nichtlinearitäten für das Netzwerk umzugehen, wurde oben geantwortet.

Ein letzter Punkt über Identitätsfunktionen; Wenn sie tatsächlich leicht zu erlernen ResNetwären, wäre es unwahrscheinlich , dass die Architekturen erfolgreich sind. Das Netzwerk könnte zur Identität konvergieren und ohne sie "kleine Korrekturen" an der Ausgabe vornehmen, was nicht der Fall ist.

Ich bin neugierig auf die Aussage: "Verwenden Sie immer Zeitunterschiede anstelle von Zeitschritten." Es scheint einen normalisierenden Effekt zu haben, wenn alle Funktionen näher zusammengebracht werden, aber ich verstehe nicht, warum dies der Schlüssel ist. Ein größeres Modell zu haben schien die Lösung zu sein und der Subtrakt hilft nur.

Der Schlüssel hier war in der Tat die Erhöhung der Modellkapazität. Der Subtraktionstrick hängt wirklich von den Daten ab. Stellen wir uns eine extreme Situation vor:

  • Wir haben 100Zeitschritte, einzelne Funktion
  • Der anfängliche Zeitschrittwert ist 10000
  • Andere Zeitschrittwerte variieren 1höchstens um

Was würde das neuronale Netzwerk tun (was ist hier am einfachsten)? Es würde wahrscheinlich diese 1oder eine kleinere Änderung als Rauschen verwerfen und nur 1000für alle vorhersagen (insbesondere, wenn eine gewisse Regularisierung vorhanden ist), da es 1/1000nicht viel ist, wenn man davon abweicht.

Was ist, wenn wir subtrahieren? Der gesamte Verlust des neuronalen Netzwerks liegt [0, 1]für jeden Zeitschritt am Rand [0, 1001], daher ist es schwerwiegender, falsch zu liegen.

Und ja, es ist in gewissem Sinne mit Normalisierung verbunden, um darüber nachzudenken.