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

我正在尝试使用PyTorch构建一个非常简单的LSTM自编码器。我总是用相同的数据进行训练:

x = torch.Tensor([[0.0], [0.1], [0.2], [0.3], [0.4]])

我按照这个链接构建了我的模型:

inputs = Input(shape=(timesteps, input_dim))encoded = LSTM(latent_dim)(inputs)decoded = RepeatVector(timesteps)(encoded)decoded = LSTM(input_dim, return_sequences=True)(decoded)sequence_autoencoder = Model(inputs, decoded)encoder = Model(inputs, encoded)

我的代码运行没有错误,但y_pred收敛到:

tensor([[[0.2]],        [[0.2]],        [[0.2]],        [[0.2]],        [[0.2]]], grad_fn=<StackBackward>)

这是我的代码:


回答:

1. 初始化隐藏状态

在你的源代码中,你使用init_hidden_encoderinit_hidden_decoder函数在每次前向传递时将两个循环单元的隐藏状态置零。

在PyTorch中,你不需要这样做,如果没有初始隐藏状态传递给RNN单元(无论是LSTM、GRU还是PyTorch当前默认提供的RNN),它会隐式地以零作为初始状态输入。

所以,为了获得与你的初始解决方案相同的代码(这简化了后续部分),我将删除不需要的部分,留下如下所示的模型:

class LSTM(nn.Module):    def __init__(self, input_dim, latent_dim, num_layers):        super(LSTM, self).__init__()        self.input_dim = input_dim        self.latent_dim = latent_dim        self.num_layers = num_layers        self.encoder = nn.LSTM(self.input_dim, self.latent_dim, self.num_layers)        self.decoder = nn.LSTM(self.latent_dim, self.input_dim, self.num_layers)    def forward(self, input):        # 编码        _, (last_hidden, _) = self.encoder(input)        encoded = last_hidden.repeat(5, 1, 1)        # 解码        y, _ = self.decoder(encoded)        return torch.squeeze(y)

添加torch.squeeze

我们不需要任何多余的维度(如[5,1,1]中的1)。实际上,这是你结果等于0.2的线索

此外,我将输入重塑移出网络(在我看来,网络应被输入已准备好处理的数据),以严格分离这两项任务(输入准备和模型本身)。

这种方法为我们提供了以下设置代码和训练循环:

model = LSTM(input_dim=1, latent_dim=20, num_layers=1)loss_function = nn.MSELoss()optimizer = optim.Adam(model.parameters(), lr=0.0001)y = torch.Tensor([[0.0], [0.1], [0.2], [0.3], [0.4]])# 序列 x 批次 x 维度x = y.view(len(y), 1, -1)while True:    y_pred = model(x)    optimizer.zero_grad()    loss = loss_function(y_pred, y)    loss.backward()    optimizer.step()    print(y_pred)

整个网络与你的相同(目前),除了它更简洁和可读性更强。

2. 我们想要的,描述网络变化

正如你提供的Keras代码所示,我们想要做的(实际上你已经正确地做了)是从编码器获取最后的隐藏状态(它编码了我们的整个序列),并从这个状态解码序列以获得原始序列。

顺便提一下,这种方法被称为序列到序列或简称seq2seq(常用于像语言翻译这样的任务)。嗯,也许是这种方法的一种变体,但我仍然会将它归类为这种方法。

PyTorch为我们提供了RNN家族的最后隐藏状态作为一个单独的返回变量。我建议不要使用你的encoded[-1]。这样做的原因是双向和多层方法。假设你想对双向输出求和,这将意味着类似这样的代码

# 批次大小和隐藏大小应该被推断出来,使代码更加复杂    encoded[-1].view(batch_size, 2, hidden_size).sum(dim=1)

这就是为什么使用_, (last_hidden, _) = self.encoder(input)这行代码的原因。

3. 为什么输出收敛到0.2?

实际上,这是你最后部分的一个错误。

你的预测和目标的输出形状:

# 你的输出torch.Size([5, 1, 1])# 你的目标torch.Size([5, 1])

如果提供了这些形状,MSELoss默认使用参数size_average=True。是的,它会平均你的目标和你的输出,这实质上是为你的张量(开始时大约是2.5)的平均值计算损失,以及你的目标的平均值,即0.2

所以网络正确地收敛了,但你的目标是错误的。

3.1 第一种且错误的解决方案

MSELoss提供参数reduction=”sum”,尽管这只是暂时的,而且是偶然有效的。网络首先会试图让所有的输出等于总和(0 + 0.1 + 0.2 + 0.3 + 0.4 = 1.0),起初是半随机的输出,过了一段时间后,它会收敛到你想要的结果,但不是因为你想要的原因!

这里最简单的选择是恒等函数,即使是求和(因为你的输入数据非常简单)。

3.2 第二种且正确的解决方案

只需为损失函数传递适当的形状,例如batch x outputs,在你的情况下,最终部分将如下所示:

model = LSTM(input_dim=1, latent_dim=20, num_layers=1)loss_function = nn.MSELoss()optimizer = optim.Adam(model.parameters())y = torch.Tensor([0.0, 0.1, 0.2, 0.3, 0.4])x = y.view(len(y), 1, -1)while True:    y_pred = model(x)    optimizer.zero_grad()    loss = loss_function(y_pred, y)    loss.backward()    optimizer.step()    print(y_pred)

你的目标是一维的(因为批次大小为1),你的输出也是(在压缩不必要的维度后)。

我将Adam的参数改为默认值,因为这样收敛得更快。

4. 最终工作代码

为了简洁,这里是代码和结果:

这是大约60k步后的结果(实际上在大约20k步后就已经卡住了,你可能想要改进你的优化方法,并尝试调整隐藏大小以获得更好的结果):

step=59682                       tensor([0.0260, 0.0886, 0.1976, 0.3079, 0.3962], grad_fn=<SqueezeBackward0>)

此外,L1Loss(也称为平均绝对误差)在这种情况下可能会获得更好的结果:

step=10645                        tensor([0.0405, 0.1049, 0.1986, 0.3098, 0.4027], grad_fn=<SqueezeBackward0>)

调整和正确批处理这个网络的工作留给你,希望你现在能找到一些乐趣,并且你明白了这个想法。:)

附注。我重复了整个输入序列的形状,因为这是一种更通用的方法,应该能在不修改的情况下处理批次和更多维度的数据。

Related Posts

如何从数据集中移除EXIF数据?

我在尝试从数据集中的图像中移除EXIF数据(这些数据将…

用于Python中的“智能点”游戏的遗传算法不工作

过去几天我一直在尝试实现所谓的“智能点”游戏。我第一次…

哪个R平方得分更有帮助?

data.drop(‘Movie Title’, ax…

使用线性回归预测GRE分数对录取率的影响

我正在学习线性回归,并尝试在Jupyter笔记本中用P…

使用mlrMBO贝叶斯优化进行SVM超参数调优时出现错误

我试图针对一个分类任务优化SVM,这个方法在许多其他模…

Keras模型的二元交叉熵准确率未发生变化

我在网上看到了很多关于这个问题的提问,但没有找到明确的…

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注