Probleme mit dem LSTM-Autoencoder
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
:
- Erhalten des anfänglichen verborgenen Zustands des Decoders
hs[N]
: Verwenden Sie einfach den endgültigen verborgenen Zustand des Encoders. - Rekonstruieren Sie das letzte Element in der Sequenz :
x[N]= w.dot(hs[N]) + b
. - Gleiches Muster für andere Elemente:
x[i]= w.dot(hs[i]) + b
- Verwenden Sie
x[i]
undhs[i]
als EingabenLSTMCell
, umx[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 Dataset
Klasse:
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
- Ich habe dies mit verschiedenen Sequenzlängen von 7 Zeitschritten bis 100 Zeitschritten versucht.
- 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.
- Ich habe es mit verschiedenen
reduction
Parametern amnn.MSELoss
Modul versucht . Die Zeitung fordertsum
, aber ich habe beides versuchtsum
undmean
. Kein Unterschied. - Das Papier fordert die Rekonstruktion der Sequenz in umgekehrter Reihenfolge (siehe Grafik oben). Ich habe diese Methode unter Verwendung der
flipud
ursprünglichen Eingabe ausprobiert (nach dem Training, aber vor der Berechnung des Verlusts). Das macht keinen Unterschied. - Ich habe versucht, das Modell komplexer zu gestalten, indem ich dem Encoder eine zusätzliche LSTM-Ebene hinzugefügt habe.
- Ich habe versucht, mit dem latenten Raum zu spielen. Ich habe versucht, von 50% der eingegebenen Anzahl von Funktionen auf 150%.
- 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
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_features
Anzahl 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_data
funktioniert entweder mit den von Ihnen angegebenen Daten, wennsubtract=False
oder (wennsubtract=True
) der Wert des vorherigen Zeitschritts vom aktuellen Zeitschritt subtrahiert wird- Der Rest des Codes optimiert das Modell, bis der
1e-4
Verlust 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_SIZE
und SUBTRACT
Parameter!
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 942
Schritten 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 215
Iterationen 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
300
ich 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 timesteps
in einem einzigen Encoderzustand ansammeln . Im Wesentlichen müssten wir also timesteps
Identitä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_timesteps
fü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 ResNet
wä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
100
Zeitschritte, einzelne Funktion - Der anfängliche Zeitschrittwert ist
10000
- Andere Zeitschrittwerte variieren
1
höchstens um
Was würde das neuronale Netzwerk tun (was ist hier am einfachsten)? Es würde wahrscheinlich diese 1
oder eine kleinere Änderung als Rauschen verwerfen und nur 1000
für alle vorhersagen (insbesondere, wenn eine gewisse Regularisierung vorhanden ist), da es 1/1000
nicht 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.