LSTM 自编码器问题

roc*_*ves 11 python neural-network autoencoder lstm pytorch

域名注册地址:

自编码器欠拟合时间序列重建,仅预测平均值。

问题设置:

这是我对序列到序列自动编码器的尝试的总结。该图片取自本文: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
Run Code Online (Sandbox Code Playgroud)

解码器类:

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
Run Code Online (Sandbox Code Playgroud)

将两者结合起来:

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)        
Run Code Online (Sandbox Code Playgroud)

这是我的训练功能:

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
Run Code Online (Sandbox Code Playgroud)

数据:

从新闻中抓取的大型事件数据集 (ICEWS)。存在描述每个事件的各种类别。我最初对这些变量进行单热编码,将数据扩展到 274 维。但是,为了调试模型,我将其缩减为一个 14 个时间步长且仅包含 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)
Run Code Online (Sandbox Code Playgroud)

这是自定义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.")
Run Code Online (Sandbox Code Playgroud)

问题:

模型只学习平均值,无论我制作模型有多复杂,或者现在训练它多长时间。

预测/重建: 在此处输入图片说明

实际的:

在此处输入图片说明

我的研究:

此问题与此问题中讨论的问题相同:LSTM 自编码器始终返回输入序列的平均值

这种情况下的问题最终是目标函数在计算损失之前对目标时间序列进行平均。这是由于一些广播错误造成的,因为作者没有正确大小的目标函数输入。

就我而言,我不认为这是问题所在。我已经检查并仔细检查了我所有的尺寸/尺寸是否对齐。我很茫然。

我尝试过的其他事情

  1. 我已经尝试了从 7 个时间步长到 100 个时间步长的不同序列长度。
  2. 我在时间序列中尝试了不同数量的变量。我已经尝试使用单变量一直到数据包含的所有 274 个变量。
  3. 我试过reductionnn.MSELoss模块上使用各种参数。该论文要求sum,但我已经尝试了summean。没有不同。
  4. 该论文要求以相反的顺序重建序列(见上图)。我已经flipud在原始输入上使用 尝试了这种方法(在训练之后但在计算损失之前)。这没什么区别。
  5. 我尝试通过在编码器中添加额外的 LSTM 层来使模型更加复杂。
  6. 我试过玩潜在空间。我已经尝试了从 50% 的输入特征数量到 150%。
  7. 我尝试过拟合单个序列(在上面的数据部分中提供)。

题:

是什么导致我的模型预测平均值,我该如何解决?

Szy*_*zke 7

好的,经过一些调试我想我知道原因了。

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()
Run Code Online (Sandbox Code Playgroud)

它能做什么:

  • get_data要么处理您提供的数据,如果subtract=False或(如果subtract=True)它从当前时间步长中减去前一个时间步长的值
  • 其余代码优化模型直到1e-4达到损失(因此我们可以比较模型的容量和增加如何帮助以及当我们使用时间步长而不是时间步长时会发生什么)

我们只会改变HIDDEN_SIZESUBTRACT参数!

无减法,小模型

  • HIDDEN_SIZE=5
  • SUBTRACT=False

在这种情况下,我们得到一条直线。模型无法拟合和掌握数据中呈现的现象(因此您提到了扁平线)。

在此处输入图片说明

达到 1000 次迭代限制

减去,小模型

  • HIDDEN_SIZE=5
  • SUBTRACT=True

目标现在远非平坦线,但由于容量太小,模型无法适应。

在此处输入图片说明

达到 1000 次迭代限制

没有减法,更大的模型

  • HIDDEN_SIZE=100
  • SUBTRACT=False

它变得更好了,我们的目标在942几步之后就被击中了。没有更多的扁平线,模型容量似乎很好(对于这个单一的例子!)

在此处输入图片说明

减去,更大的模型

  • HIDDEN_SIZE=100
  • SUBTRACT=True

虽然图看起来不那么漂亮,但我们只经过215迭代就得到了期望的损失。

在此处输入图片说明

最后

  • 通常使用时间步长的差异而不是时间步长(或其他一些转换,请参阅此处了解更多信息)。在其他情况下,神经网络将尝试简单地...复制上一步的输出(因为这是最容易做的事情)。以这种方式会找到一些最小值,而摆脱它需要更多的容量。
  • 当您使用时间步长之间的差异时,无法从前一个时间步长“推断”趋势;神经网络必须学习函数实际上是如何变化的
  • 使用更大的模型(对于整个数据集,您应该尝试像300我认为的那样),但您可以简单地调整该模型。
  • 不要使用flipud. 使用双向 LSTM,这样您就可以从 LSTM 的前向和后向传递中获取信息(不要与反向传播混淆!)。这也应该提高你的分数

问题

好的,问题 1:您是说对于时间序列中的变量 x,我应该训练模型学习 x[i] - x[i-1] 而不是 x[i] 的值?我的解释正确吗?

对,就是这样。差异消除了神经网络过多地基于过去的时间步长进行预测的冲动(通过简单地获取最后一个值并可能稍微改变它)

问题 2:您说我对零瓶颈的计算不正确。但是,例如,假设我使用一个简单的密集网络作为自动编码器。获得正确的瓶颈确实取决于数据。但是如果你让瓶颈与输入的大小相同,你就会得到恒等函数。

是的,假设不涉及非线性,这会使事情变得更难(请参阅此处了解类似情况)。在 LSTM 存在非线性的情况下,这是一点。

另一个是我们正在累积timesteps到单个编码器状态。所以本质上,我们必须将timesteps身份积累到单个隐藏和细胞状态中,这是极不可能的。

最后一点,根据序列的长度,LSTM 很容易忘记一些最不相关的信息(这是它们的设计目的,不仅是记住所有内容),因此更不可能。

num_features * num_timesteps 不是与输入大小相同的瓶颈,因此它不应该促进模型学习身份吗?

确实如此,但它假设您num_timesteps对每个数据点都有,这种情况很少见,可能在这里。关于身份以及为什么很难对上面回答的网络进行非线性处理。

最后一点,关于恒等函数;如果它们真的很容易学习,ResNets 架构就不太可能成功。网络可以收敛到身份并在没有身份的情况下对输出进行“小修正”,但事实并非如此。

我对以下声明很好奇:“始终使用时间步长的差异而不是时间步长”通过将所有功能更紧密地结合在一起似乎有一些规范化效果,但我不明白为什么这是关键?拥有一个更大的模型似乎是解决方案,而减法只是有帮助。

事实上,这里的关键是增加模型容量。减法技巧实际上取决于数据。让我们想象一个极端的情况:

  • 我们有100时间步长,单一功能
  • 初始时间步长值为 10000
  • 其他时间步长值1最多变化

神经网络会做什么(这里最简单的是什么)?它可能会将这个1或更小的变化作为噪音丢弃,并只预测1000所有这些变化(特别是如果一些正则化到位),因为偏离1/1000并不多。

如果我们减去呢?整个神经网络损失在[0, 1]每个时间步的边缘而不是[0, 1001],因此错误更严重。

是的,从某种意义上说,它与规范化有关。

  • 非常好的答案,我对这个说法很好奇:“总是使用时间步长的差异而不是时间步长”它似乎通过将所有功能更紧密地结合在一起而具有一定的标准化效果,但我不明白为什么这是关键?拥有一个更大的模型似乎是解决方案,而缩小模型只是有帮助。谢谢 (2认同)