Masalah LSTM Autoencoder
TLDR:
Autoencoder mendukung rekonstruksi rangkaian waktu dan hanya memprediksi nilai rata-rata.
Pengaturan Pertanyaan:
Berikut adalah ringkasan dari upaya saya di autoencoder urutan-ke-urutan. Gambar ini diambil dari makalah ini:https://arxiv.org/pdf/1607.00148.pdf

Encoder: Lapisan LSTM standar. Urutan masukan dikodekan dalam keadaan tersembunyi terakhir.
Decoder: LSTM Cell (saya pikir!). Rekonstruksi urutan satu elemen pada satu waktu, dimulai dengan elemen terakhir x[N]
.
Algoritma decoder adalah sebagai berikut untuk urutan panjangnya N
:
- Dapatkan status tersembunyi awal Decoder
hs[N]
: Cukup gunakan status tersembunyi akhir pembuat enkode. - Merekonstruksi elemen terakhir dalam urutan:
x[N]= w.dot(hs[N]) + b
. - Pola yang sama untuk elemen lainnya:
x[i]= w.dot(hs[i]) + b
- menggunakan
x[i]
danhs[i]
sebagai masukanLSTMCell
untuk mendapatkanx[i-1]
danhs[i-1]
Contoh Kerja Minimum:
Inilah penerapan saya, dimulai dengan pembuat enkode:
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
Kelas decoder:
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
Menyatukan keduanya:
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)
Dan inilah fungsi pelatihan saya:
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
Data:
Kumpulan besar peristiwa yang diambil dari berita (ICEWS). Berbagai kategori ada yang menggambarkan setiap peristiwa. Saya awalnya one-hot encoded variabel-variabel ini, memperluas data ke 274 dimensi. Namun, untuk men-debug model, saya telah memotongnya menjadi satu urutan yang panjangnya 14 langkah waktu dan hanya berisi 5 variabel. Inilah urutan yang saya coba untuk overfit:
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)
Dan inilah Dataset
kelas khusus :
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.")
Masalah:
Model hanya mempelajari rata-rata, tidak peduli betapa rumitnya saya membuat model atau lama saya melatihnya.
Prediksi / Rekonstruksi:

Sebenarnya:

Penelitian saya:
Masalah ini identik dengan yang dibahas dalam pertanyaan ini: Autoencoder LSTM selalu mengembalikan rata-rata urutan input
Masalah dalam kasus itu akhirnya adalah bahwa fungsi tujuan rata-rata dari rentang waktu target sebelum menghitung kerugian. Hal ini disebabkan oleh beberapa kesalahan penyiaran karena penulis tidak memiliki ukuran masukan yang tepat untuk fungsi tujuan.
Dalam kasus saya, saya tidak melihat ini menjadi masalahnya. Saya telah memeriksa dan memeriksa ulang bahwa semua dimensi / ukuran saya sejajar. Saya bingung
Hal-Hal Lain yang Saya Coba
- Saya telah mencoba ini dengan panjang urutan yang bervariasi dari 7 langkah waktu hingga 100 langkah waktu.
- Saya sudah mencoba dengan berbagai variabel dalam deret waktu. Saya sudah mencoba dengan univariat hingga semua 274 variabel yang dikandung datanya.
- Saya sudah mencoba dengan berbagai
reduction
parameter padann.MSELoss
modul. Makalah itu memanggilsum
, tapi saya sudah mencoba keduanyasum
danmean
. Tidak ada perbedaan. - Makalah tersebut meminta untuk merekonstruksi urutan dalam urutan terbalik (lihat grafik di atas). Saya telah mencoba metode ini menggunakan
flipud
input asli (setelah pelatihan tetapi sebelum menghitung kerugian). Tidak ada bedanya. - Saya mencoba membuat model lebih kompleks dengan menambahkan lapisan LSTM ekstra di encoder.
- Saya sudah mencoba bermain dengan ruang laten. Saya sudah mencoba dari 50% dari jumlah input fitur hingga 150%.
- Saya sudah mencoba melakukan overfitting pada satu urutan (disediakan di bagian Data di atas).
Pertanyaan:
Apa yang menyebabkan model saya memprediksi rata-rata dan bagaimana cara memperbaikinya?
Jawaban
Oke, setelah beberapa debugging saya rasa saya tahu alasannya.
TLDR
- Anda mencoba memprediksi nilai langkah waktu berikutnya, bukan perbedaan antara langkah waktu saat ini dan yang sebelumnya
hidden_features
Nomor Anda terlalu kecil sehingga model tidak dapat memuat satu sampel pun
Analisis
Kode digunakan
Mari kita mulai dengan kode (modelnya sama):
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()
Apa fungsinya:
get_data
bekerja pada data yang Anda berikan jikasubtract=False
atau (jikasubtract=True
) mengurangi nilai langkah sebelumnya dari langkah waktu saat ini- Sisa kode mengoptimalkan model sampai
1e-4
kerugian tercapai (sehingga kita dapat membandingkan bagaimana kapasitas model dan peningkatannya membantu dan apa yang terjadi ketika kita menggunakan perbedaan langkah waktu dan bukan langkah waktu)
Kami hanya akan memvariasikan HIDDEN_SIZE
dan SUBTRACT
parameter!
TANPA SUBTRAK, MODEL KECIL
HIDDEN_SIZE=5
SUBTRACT=False
Dalam hal ini kita mendapatkan garis lurus. Model tidak dapat menyesuaikan dan memahami fenomena yang disajikan dalam data (karenanya garis datar yang Anda sebutkan).

1000 batas iterasi tercapai
SUBTRAK, MODEL KECIL
HIDDEN_SIZE=5
SUBTRACT=True
Target sekarang jauh dari garis datar , tetapi model tidak dapat disesuaikan karena kapasitas yang terlalu kecil.

1000 batas iterasi tercapai
TANPA SUBTRAK, MODEL YANG LEBIH BESAR
HIDDEN_SIZE=100
SUBTRACT=False
Itu menjadi jauh lebih baik dan target kami tercapai setelah beberapa 942
langkah. Tidak ada lagi garis datar, kapasitas model tampaknya cukup baik (untuk contoh tunggal ini!)

SUBTRAK, MODEL YANG LEBIH BESAR
HIDDEN_SIZE=100
SUBTRACT=True
Meskipun grafiknya tidak terlihat cantik, kami mendapatkan kerugian yang diinginkan hanya setelah 215
iterasi.

Akhirnya
- Biasanya menggunakan perbedaan langkah waktu daripada langkah waktu (atau beberapa transformasi lainnya, lihat di sini untuk info lebih lanjut tentang itu). Dalam kasus lain, jaringan saraf akan mencoba untuk ... menyalin keluaran dari langkah sebelumnya (karena itu hal termudah untuk dilakukan). Beberapa minimum akan ditemukan dengan cara ini dan untuk keluar darinya akan membutuhkan lebih banyak kapasitas.
- Saat Anda menggunakan perbedaan antara langkah waktu, tidak ada cara untuk "mengekstrapolasi" tren dari langkah waktu sebelumnya; jaringan saraf harus mempelajari bagaimana fungsinya sebenarnya bervariasi
- Gunakan model yang lebih besar (untuk seluruh dataset Anda harus mencoba sesuatu seperti yang
300
saya kira), tetapi Anda dapat menyesuaikan yang satu itu. - Jangan gunakan
flipud
. Gunakan LSTM dua arah, dengan cara ini Anda bisa mendapatkan info dari forward dan backward pass LSTM (jangan bingung dengan backprop!). Ini juga akan meningkatkan skor Anda
Pertanyaan
Oke, pertanyaan 1: Anda mengatakan bahwa untuk variabel x dalam deret waktu, saya harus melatih model untuk mempelajari x [i] - x [i-1] daripada nilai x [i]? Apakah saya menafsirkan dengan benar?
Ya persis. Perbedaan menghilangkan dorongan jaringan saraf untuk mendasarkan prediksinya pada langkah waktu yang lalu (hanya dengan mendapatkan nilai terakhir dan mungkin mengubahnya sedikit)
Pertanyaan 2: Anda mengatakan bahwa perhitungan saya untuk zero bottleneck salah. Namun, misalnya, saya menggunakan jaringan padat sederhana sebagai pembuat enkode otomatis. Mendapatkan bottleneck yang tepat memang bergantung pada datanya. Tetapi jika Anda membuat ukuran bottleneck sama dengan input, Anda mendapatkan fungsi identitas.
Ya, dengan asumsi bahwa tidak ada non-linearitas yang terlibat yang membuat masalah menjadi lebih sulit (lihat di sini untuk kasus serupa). Dalam kasus LSTM ada non-linearit, itu satu poin.
Satu lagi adalah bahwa kami terakumulasi timesteps
ke dalam status encoder tunggal. Jadi pada dasarnya kita harus mengakumulasi timesteps
identitas menjadi satu keadaan tersembunyi dan sel yang sangat tidak mungkin.
Satu poin terakhir, tergantung pada panjang urutan, LSTM cenderung melupakan beberapa informasi yang paling tidak relevan (itulah yang dirancang untuk dilakukan, tidak hanya untuk mengingat semuanya), bahkan lebih tidak mungkin.
Apakah num_features * num_timesteps bukan leher botol dengan ukuran yang sama dengan input, dan oleh karena itu, bukankah seharusnya itu memfasilitasi model untuk mempelajari identitas?
Memang, tetapi mengasumsikan Anda memiliki num_timesteps
untuk setiap titik data, yang jarang terjadi, mungkin ada di sini. Tentang identitas dan mengapa sulit untuk dilakukan dengan non-linearitas untuk jaringan, telah dijawab di atas.
Satu poin terakhir, tentang fungsi identitas; jika mereka benar-benar mudah dipelajari, ResNet
arsitektur mungkin tidak akan berhasil. Jaringan dapat menyatu dengan identitas dan membuat "perbaikan kecil" pada output tanpanya, yang tidak akan terjadi.
Saya ingin tahu tentang pernyataan: "selalu gunakan perbedaan langkah waktu daripada langkah waktu" Tampaknya ada beberapa efek normalisasi dengan mendekatkan semua fitur tetapi saya tidak mengerti mengapa ini kuncinya? Memiliki model yang lebih besar tampaknya menjadi solusi dan pengurangan hanya membantu.
Kuncinya di sini adalah, memang, meningkatkan kapasitas model. Trik pengurangan tergantung datanya kok. Bayangkan situasi ekstrim:
- Kami memiliki
100
timesteps, fitur tunggal - Nilai langkah waktu awal adalah
10000
- Nilai langkah waktu lainnya
1
paling banyak bervariasi
Apa yang akan dilakukan oleh jaringan saraf (apa yang paling mudah di sini)? Mungkin, ini akan membuang 1
perubahan ini atau yang lebih kecil sebagai noise dan hanya memprediksi 1000
untuk semuanya (terutama jika beberapa regularisasi ada), karena tidak terlalu aktif 1/1000
.
Bagaimana jika kita mengurangi? Kehilangan seluruh jaringan neural berada dalam [0, 1]
margin untuk setiap langkah waktu, bukan [0, 1001]
, oleh karena itu kesalahan yang lebih parah.
Dan ya, itu terhubung ke normalisasi dalam beberapa hal kalau dipikir-pikir.