我正在尝试使用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_encoder
和init_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>)
调整和正确批处理这个网络的工作留给你,希望你现在能找到一些乐趣,并且你明白了这个想法。:)
附注。我重复了整个输入序列的形状,因为这是一种更通用的方法,应该能在不修改的情况下处理批次和更多维度的数据。