LSTM 自动编码器问题

Posted

技术标签:

【中文标题】LSTM 自动编码器问题【英文标题】:LSTM Autoencoder problems 【发布时间】:2021-03-20 03:54:48 【问题描述】:

TLDR:

自动编码器不适合时间序列重建,仅预测平均值。

问题设置:

这是我对序列到序列自动编码器的尝试的总结。这张图片取自这篇论文:https://arxiv.org/pdf/1607.00148.pdf

编码器:标准 LSTM 层。输入序列在最终隐藏状态下编码。

解码器: LSTM 单元(我认为!)。从最后一个元素 x[N] 开始,一次重建一个元素。

对于长度为N的序列的解码算法如下:

    获取解码器初始隐藏状态hs[N]:使用编码器最终隐藏状态即可。 重构序列中的最后一个元素:x[N]= w.dot(hs[N]) + b。 其他元素的模式相同:x[i]= w.dot(hs[i]) + b 使用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 个时间步长且仅包含 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 autoencoder always returns the average of the input sequence

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

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

我尝试过的其他事情

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

问题:

是什么导致我的模型预测平均值以及如何修复它?

【问题讨论】:

评论不用于扩展讨论;这个对话是moved to chat。 【参考方案1】:

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

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_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 迭代后就达到了预期的损失。

终于

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

问题

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

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

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

是的,假设 不涉及非线性,这会使事情变得更难(类似情况请参阅 here)。在 LSTM 的情况下,存在非线性,仅此而已。

另一个是我们将timesteps 累积到单个编码器状态。因此,基本上我们必须将timesteps 身份累积到单个隐藏和单元状态中,这是极不可能的。

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

num_features * num_timesteps 是否不是与 输入,因此它不应该促进模型学习 身份?

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

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

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

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

我们有 100 时间步长,单一特征 初始时间步长值为10000 其他时间步长值最多相差1

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

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

是的,在某种意义上它与规范化有关。想想看吧。

【讨论】:

好的,问题1:你是说对于时间序列中的变量x,我应该训练模型学习x[i] - x[i-1]而不是x[i]的值?我的解释正确吗? 问题 2:您说我对零瓶颈的计算不正确。但是,例如,假设我使用一个简单的密集网络作为自动编码器。获得正确的瓶颈确实取决于数据。但是,如果您使瓶颈与输入的大小相同,您将获得恒等函数。 num_features * num_timesteps 不是和输入一样大小的瓶颈,因此它不应该促进模型学习身份吗? 很好的答案,我很好奇这样的说法:“总是使用时间步长的差异而不是时间步长”它似乎通过将所有功能靠得更近而产生了一些规范化效果,但我不明白为什么这是关键?拥有一个更大的模型似乎是解决方案,而减法只是有帮助。谢谢 @SzymonMaszke 感谢您的澄清,但它之所以有效,是因为您的平均值远大于您的标准偏差,因此它大致相当于从每个实例中减去平均值。但它不一定会推广到其他问题,所以说“总是使用差异”可能会产生误导。 "不要使用 Flipud。使用双向 LSTM,这样你可以从 LSTM 的前向和后向传递中获取信息(不要与反向传播混淆!)。这也应该提高你的分数",只是回来告诉你我终于理解了双向 LSTM,并将在未来研究它们的应用。再次感谢。

以上是关于LSTM 自动编码器问题的主要内容,如果未能解决你的问题,请参考以下文章

从 LSTM 自动编码器馈送分类器数据

LSTM 自动编码器总是返回输入序列的平均值

Keras LSTM 自动编码器时间序列重建

损失函数的爆炸式增长,LSTM 自动编码器

带有嵌入层的 Keras LSTM 自动编码器

在 LSTM 自动编码器中需要帮助 - 异常检测