Masalah LSTM Autoencoder

Dec 09 2020

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:

  1. Dapatkan status tersembunyi awal Decoder hs[N]: Cukup gunakan status tersembunyi akhir pembuat enkode.
  2. Merekonstruksi elemen terakhir dalam urutan: x[N]= w.dot(hs[N]) + b.
  3. Pola yang sama untuk elemen lainnya: x[i]= w.dot(hs[i]) + b
  4. menggunakan x[i]dan hs[i]sebagai masukan LSTMCelluntuk mendapatkan x[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 Datasetkelas 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

  1. Saya telah mencoba ini dengan panjang urutan yang bervariasi dari 7 langkah waktu hingga 100 langkah waktu.
  2. Saya sudah mencoba dengan berbagai variabel dalam deret waktu. Saya sudah mencoba dengan univariat hingga semua 274 variabel yang dikandung datanya.
  3. Saya sudah mencoba dengan berbagai reductionparameter pada nn.MSELossmodul. Makalah itu memanggil sum, tapi saya sudah mencoba keduanya sumdan mean. Tidak ada perbedaan.
  4. Makalah tersebut meminta untuk merekonstruksi urutan dalam urutan terbalik (lihat grafik di atas). Saya telah mencoba metode ini menggunakan flipudinput asli (setelah pelatihan tetapi sebelum menghitung kerugian). Tidak ada bedanya.
  5. Saya mencoba membuat model lebih kompleks dengan menambahkan lapisan LSTM ekstra di encoder.
  6. Saya sudah mencoba bermain dengan ruang laten. Saya sudah mencoba dari 50% dari jumlah input fitur hingga 150%.
  7. 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

7 SzymonMaszke Dec 16 2020 at 05:04

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_featuresNomor 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_databekerja pada data yang Anda berikan jika subtract=Falseatau (jika subtract=True) mengurangi nilai langkah sebelumnya dari langkah waktu saat ini
  • Sisa kode mengoptimalkan model sampai 1e-4kerugian 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_SIZEdan SUBTRACTparameter!

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 942langkah. 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 215iterasi.

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 300saya 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 timestepske dalam status encoder tunggal. Jadi pada dasarnya kita harus mengakumulasi timestepsidentitas 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_timestepsuntuk 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, ResNetarsitektur 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 100timesteps, fitur tunggal
  • Nilai langkah waktu awal adalah 10000
  • Nilai langkah waktu lainnya 1paling banyak bervariasi

Apa yang akan dilakukan oleh jaringan saraf (apa yang paling mudah di sini)? Mungkin, ini akan membuang 1perubahan ini atau yang lebih kecil sebagai noise dan hanya memprediksi 1000untuk 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.