ปัญหา LSTM Autoencoder

Dec 09 2020

TLDR:

ตัวเข้ารหัสอัตโนมัติรองรับการสร้างชุดเวลาใหม่และเพียงแค่ทำนายค่าเฉลี่ย

การตั้งคำถาม:

นี่คือสรุปความพยายามของฉันในการเข้ารหัสอัตโนมัติแบบลำดับต่อลำดับ ภาพนี้นำมาจากกระดาษนี้:https://arxiv.org/pdf/1607.00148.pdf

ตัวเข้ารหัส:เลเยอร์ LSTM มาตรฐาน ลำดับอินพุตถูกเข้ารหัสในสถานะสุดท้ายที่ซ่อนอยู่

ตัวถอดรหัส: LSTM Cell (ฉันคิดว่า!) x[N]สร้างองค์ประกอบหนึ่งลำดับในช่วงเวลาที่เริ่มต้นด้วยองค์ประกอบที่ผ่านมา

ขั้นตอนวิธีการถอดรหัสมีดังต่อไปนี้สำหรับลำดับความยาวN:

  1. รับสถานะการซ่อนเริ่มต้นของตัวถอดรหัสhs[N]: เพียงใช้สถานะสุดท้ายที่ซ่อนอยู่ของตัวเข้ารหัส
  2. x[N]= w.dot(hs[N]) + bสร้างองค์ประกอบสุดท้ายในลำดับ:
  3. รูปแบบเดียวกันสำหรับองค์ประกอบอื่น ๆ : x[i]= w.dot(hs[i]) + b
  4. ใช้x[i]และhs[i]เป็นอินพุตLSTMCellเพื่อรับx[i-1]และhs[i-1]

ตัวอย่างการทำงานขั้นต่ำ:

นี่คือการใช้งานของฉันโดยเริ่มจากตัวเข้ารหัส:

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

คลาสตัวถอดรหัส:

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

นำทั้งสองมารวมกัน:

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)        

และนี่คือฟังก์ชั่นการฝึกของฉัน:

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

ข้อมูล:

ชุดข้อมูลขนาดใหญ่ของเหตุการณ์ที่ดึงมาจากข่าว (ICEWS) มีหมวดหมู่ต่างๆที่อธิบายแต่ละเหตุการณ์ ตอนแรกฉันเข้ารหัสตัวแปรเหล่านี้แบบร้อนแรงโดยขยายข้อมูลเป็น 274 มิติ อย่างไรก็ตามในการดีบักโมเดลฉันได้ตัดมันลงเป็นลำดับเดียวที่มีความยาว 14 timesteps และมีเพียง 5 ตัวแปรเท่านั้น นี่คือลำดับที่ฉันพยายามจะใส่มากเกินไป:

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)

และนี่คือDatasetคลาสที่กำหนดเอง:

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.")

ปัญหา:

โมเดลเรียนรู้ค่าเฉลี่ยเท่านั้นไม่ว่าฉันจะสร้างโมเดลที่ซับซ้อนแค่ไหนหรือตอนนี้ฉันฝึกมานานแค่ไหน

คาดการณ์ / สร้างใหม่:

ตามจริง:

งานวิจัยของฉัน:

ปัญหานี้เหมือนกับปัญหาที่กล่าวถึงในคำถามนี้: ตัวเข้ารหัสอัตโนมัติ LSTM จะส่งกลับค่าเฉลี่ยของลำดับอินพุตเสมอ

ปัญหาในกรณีนั้นจบลงด้วยการที่ฟังก์ชันวัตถุประสงค์คือการหาค่าเฉลี่ยของไทม์ซีรีส์เป้าหมายก่อนที่จะคำนวณการสูญเสีย นี่เป็นเพราะข้อผิดพลาดในการออกอากาศเนื่องจากผู้เขียนไม่มีอินพุตขนาดที่เหมาะสมสำหรับฟังก์ชันวัตถุประสงค์

ในกรณีของฉันฉันไม่เห็นว่านี่เป็นปัญหา ฉันได้ตรวจสอบและตรวจสอบอีกครั้งว่ามิติ / ขนาดทั้งหมดของฉันสอดคล้องกัน ฉันกำลังสูญเสีย

สิ่งอื่น ๆ ที่ฉันพยายาม

  1. ฉันได้ลองแล้วโดยมีความยาวของลำดับที่แตกต่างกันตั้งแต่ 7 timesteps ไปจนถึง 100 time steps
  2. ฉันได้ลองใช้ตัวแปรหลายตัวในอนุกรมเวลาแล้ว ฉันได้ลองใช้ตัวแปรทั้งหมด 274 ตัวแปรที่ข้อมูลมีอยู่
  3. ฉันได้ลองใช้reductionพารามิเตอร์ต่างๆในnn.MSELossโมดูลแล้ว กระดาษเรียกร้องsumแต่ฉันได้ลองทั้งสองอย่างsumและmean. ไม่แตกต่าง.
  4. กระดาษเรียกร้องให้สร้างลำดับใหม่ตามลำดับย้อนกลับ (ดูภาพด้านบน) ฉันได้ลองใช้วิธีนี้โดยใช้flipudอินพุตต้นฉบับ (หลังจากการฝึกอบรม แต่ก่อนที่จะคำนวณการสูญเสีย) สิ่งนี้ไม่ทำให้เกิดความแตกต่าง
  5. ฉันพยายามทำให้โมเดลซับซ้อนขึ้นโดยการเพิ่มเลเยอร์ LSTM พิเศษในตัวเข้ารหัส
  6. ฉันได้ลองเล่นกับช่องว่างแฝงแล้ว ฉันได้ลองจาก 50% ของจำนวนคุณสมบัติที่ป้อนเป็น 150%
  7. ฉันได้ลองใส่ลำดับเดียวมากเกินไปแล้ว (มีให้ในส่วนข้อมูลด้านบน)

คำถาม:

อะไรทำให้แบบจำลองของฉันทำนายค่าเฉลี่ยและฉันจะแก้ไขได้อย่างไร

คำตอบ

7 SzymonMaszke Dec 16 2020 at 05:04

เอาล่ะหลังจากการดีบักฉันคิดว่าฉันรู้เหตุผลแล้ว

TLDR

  • คุณพยายามคาดคะเนค่าการประทับเวลาถัดไปแทนความแตกต่างระหว่างการประทับเวลาปัจจุบันกับค่าก่อนหน้า
  • hidden_featuresจำนวนของคุณน้อยเกินไปทำให้แบบจำลองไม่สามารถใส่ได้แม้แต่ตัวอย่างเดียว

การวิเคราะห์

รหัสที่ใช้

เริ่มต้นด้วยรหัส (รุ่นเดียวกัน):

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()

มันทำอะไร:

  • get_dataทำงานกับข้อมูลที่คุณระบุถ้าsubtract=Falseหรือ (ถ้าsubtract=True) มันลบค่าของการประทับเวลาก่อนหน้าออกจากการประทับเวลาปัจจุบัน
  • โค้ดที่เหลือจะปรับโมเดลให้เหมาะสมจนกว่าจะ1e-4ถึงจุดสูญเสีย (ดังนั้นเราสามารถเปรียบเทียบได้ว่าความจุของโมเดลและการเพิ่มขึ้นช่วยได้อย่างไรและจะเกิดอะไรขึ้นเมื่อเราใช้ความแตกต่างของการประทับเวลาแทนการประทับเวลา)

เราจะเปลี่ยนแปลงHIDDEN_SIZEและSUBTRACTพารามิเตอร์เท่านั้น!

ไม่มีสารเสพติดรุ่นเล็ก

  • HIDDEN_SIZE=5
  • SUBTRACT=False

ในกรณีนี้เราจะได้เส้นตรง โมเดลไม่สามารถปรับให้พอดีและเข้าใจปรากฏการณ์ที่นำเสนอในข้อมูลได้ (ด้วยเหตุนี้เส้นแบนที่คุณกล่าวถึง)

ถึงขีด จำกัด การทำซ้ำ 1,000 ครั้ง

SUBTRACT รุ่นเล็ก

  • HIDDEN_SIZE=5
  • SUBTRACT=True

ขณะนี้เป้าหมายอยู่ไกลจากเส้นแบนแต่โมเดลไม่สามารถใส่ได้เนื่องจากความจุน้อยเกินไป

ถึงขีด จำกัด การทำซ้ำ 1,000 ครั้ง

ไม่มีสารเสพติดรุ่นที่ใหญ่กว่า

  • HIDDEN_SIZE=100
  • SUBTRACT=False

มันดีขึ้นมากและเป้าหมายของเราก็โดนโจมตีตาม942ขั้นตอน ไม่มีเส้นแบนอีกต่อไปความจุของรุ่นดูเหมือนจะค่อนข้างดี (สำหรับตัวอย่างเดียวนี้!)

SUBTRACT รุ่นใหญ่

  • HIDDEN_SIZE=100
  • SUBTRACT=True

แม้ว่ากราฟจะดูไม่สวย แต่เราก็ต้องสูญเสียที่ต้องการหลังจาก215การทำซ้ำเท่านั้น

สุดท้าย

  • มักจะใช้ความแตกต่างของ timesteps แทน timesteps (หรือบางส่วนการเปลี่ยนแปลงอื่นดูได้ที่นี่สำหรับข้อมูลเพิ่มเติมเกี่ยวกับที่) ในกรณีอื่นโครงข่ายประสาทเทียมจะพยายาม ... คัดลอกผลลัพธ์จากขั้นตอนก่อนหน้า (ซึ่งเป็นสิ่งที่ง่ายที่สุดที่จะทำ) minima บางส่วนจะพบในลักษณะนี้และการออกไปจากที่นั่นจะต้องใช้ความจุมากขึ้น
  • เมื่อคุณใช้ความแตกต่างระหว่างการประทับเวลาไม่มีทางที่จะ "คาดการณ์" แนวโน้มจากระยะเวลาก่อนหน้านี้ได้ โครงข่ายประสาทเทียมต้องเรียนรู้ว่าฟังก์ชันแตกต่างกันอย่างไร
  • ใช้โมเดลที่ใหญ่กว่า (สำหรับชุดข้อมูลทั้งหมดคุณควรลองอย่างที่300ฉันคิด) แต่คุณสามารถปรับแต่งได้
  • อย่าใช้flipud. ใช้ LSTM แบบสองทิศทางด้วยวิธีนี้คุณจะได้รับข้อมูลจากการส่งผ่านไปข้างหน้าและข้างหลังของ LSTM (เพื่อไม่ให้สับสนกับ backprop!) นอกจากนี้ยังควรเพิ่มคะแนนของคุณ

คำถาม

โอเคคำถามที่ 1: คุณกำลังบอกว่าสำหรับตัวแปร x ในอนุกรมเวลาฉันควรฝึกโมเดลให้เรียนรู้ x [i] - x [i-1] แทนค่า x [i]? ฉันตีความถูกต้องหรือไม่?

ใช่แน่นอน ความแตกต่างจะลบแรงกระตุ้นของเครือข่ายประสาทเทียมให้เป็นฐานของการคาดการณ์ในช่วงเวลาที่ผ่านมามากเกินไป (โดยเพียงแค่รับค่าสุดท้ายและอาจเปลี่ยนแปลงเล็กน้อย)

คำถาม 2: คุณบอกว่าการคำนวณของฉันสำหรับคอขวดเป็นศูนย์ไม่ถูกต้อง แต่ตัวอย่างเช่นสมมติว่าฉันใช้เครือข่ายหนาแน่นธรรมดาเป็นตัวเข้ารหัสอัตโนมัติ การได้รับคอขวดที่ถูกต้องขึ้นอยู่กับข้อมูล แต่ถ้าคุณทำให้คอขวดมีขนาดเท่ากับอินพุตคุณจะได้รับฟังก์ชันเอกลักษณ์

ใช่สมมติว่าไม่มีความไม่เป็นเชิงเส้นที่เกี่ยวข้องซึ่งจะทำให้สิ่งนี้ยากขึ้น (ดูที่นี่สำหรับกรณีที่คล้ายกัน) ในกรณีของ LSTMs มี non-linearites นั่นคือจุดหนึ่ง

อีกประการหนึ่งคือเรากำลังสะสมtimestepsในสถานะตัวเข้ารหัสเดียว โดยพื้นฐานแล้วเราจะต้องสะสมtimestepsข้อมูลประจำตัวให้เป็นสถานะที่ซ่อนอยู่และเซลล์เดียวซึ่งไม่น่าเป็นไปได้สูง

ประเด็นสุดท้ายขึ้นอยู่กับความยาวของลำดับ LSTM มีแนวโน้มที่จะลืมข้อมูลที่เกี่ยวข้องน้อยที่สุด (นั่นคือสิ่งที่พวกเขาออกแบบมาให้ทำไม่ใช่แค่จำทุกอย่าง) จึงไม่น่าจะเกิดขึ้นได้อีก

num_features * num_timesteps ไม่ใช่คอขวดที่มีขนาดเท่ากับอินพุตหรือไม่ดังนั้นจึงไม่ควรอำนวยความสะดวกให้โมเดลเรียนรู้เอกลักษณ์

เป็น แต่สมมติว่าคุณมีnum_timestepsจุดข้อมูลแต่ละจุดซึ่งไม่ค่อยเป็นเช่นนั้นอาจอยู่ที่นี่ เกี่ยวกับข้อมูลประจำตัวและเหตุใดจึงยากที่จะทำกับความไม่เป็นเชิงเส้นสำหรับเครือข่ายตามคำตอบข้างต้น

ประเด็นสุดท้ายเกี่ยวกับฟังก์ชันเอกลักษณ์ ถ้าพวกเขาเรียนรู้ได้ง่ายจริง ๆResNetสถาปัตยกรรมก็ไม่น่าจะประสบความสำเร็จ เครือข่ายสามารถรวมตัวกันเป็นตัวตนและทำการ "แก้ไขเล็ก ๆ " ไปยังเอาต์พุตโดยไม่ใช้เครือข่ายซึ่งไม่เป็นเช่นนั้น

ฉันอยากรู้เกี่ยวกับข้อความนี้: "ใช้ความแตกต่างของเวลาแทนการประทับเวลาเสมอ" ดูเหมือนว่าจะมีเอฟเฟกต์การทำให้เป็นมาตรฐานโดยการนำคุณสมบัติทั้งหมดเข้ามาใกล้กัน แต่ฉันไม่เข้าใจว่าเหตุใดจึงเป็นกุญแจสำคัญ การมีแบบจำลองที่ใหญ่ขึ้นดูเหมือนจะเป็นวิธีแก้ปัญหาและสารทดแทนก็ช่วยได้

ที่สำคัญคือการเพิ่มความจุของโมเดล เคล็ดลับการลบขึ้นอยู่กับข้อมูลจริงๆ ลองจินตนาการถึงสถานการณ์ที่รุนแรง:

  • เรามีการ100ประทับเวลาคุณลักษณะเดียว
  • ค่าการประทับเวลาเริ่มต้นคือ 10000
  • ค่าการประทับเวลาอื่น ๆ จะแตกต่างกันไปโดย1มาก

โครงข่ายประสาทเทียมจะทำอะไร (ง่ายที่สุดที่นี่) คืออะไร? มันอาจจะทิ้ง1การเปลี่ยนแปลงนี้หรือเล็กลงเป็นสัญญาณรบกวนและเพียงแค่คาดการณ์1000สำหรับสิ่งเหล่านี้ทั้งหมด (โดยเฉพาะอย่างยิ่งถ้ามีการกำหนดมาตรฐานบางอย่างไว้) เนื่องจากการปิดโดย1/1000ไม่มากนัก

จะเกิดอะไรขึ้นถ้าเราลบ? การสูญเสียโครงข่ายประสาทเทียมทั้งหมดอยู่ใน[0, 1]ระยะขอบของการประทับเวลาแต่ละครั้งแทนที่จะเป็น[0, 1001]เช่นนั้นจึงรุนแรงกว่าที่จะผิด

และใช่มันเชื่อมต่อกับการทำให้เป็นมาตรฐานในแง่หนึ่งมาลองคิดดู