1.Инициализация скрытых состояний
В вашем исходном коде вы используете функции init_hidden_encoder
и init_hidden_decoder
для обнуления скрытых состояний обоих повторяющихся блоков при каждом прямом проходе.
В PyTorch вы используете't должен сделать это, если начальное скрытое состояние не передается в RNN-ячейку (будь то LSTM, GRU или RNN из тех, которые в настоящее время доступны по умолчанию в PyTorch), оно неявно заполняется нулями.
Итак, чтобы получить тот же код, что и в вашем исходном решении (что упрощает последующие части), я удалю ненужные части, что оставляет нам модель, показанную ниже:
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):
# Encode
_, (last_hidden, _) = self.encoder(input)
encoded = last_hidden.repeat(5, 1, 1)
# Decode
y, _ = self.decoder(encoded)
return torch.squeeze(y)
Добавление torch.squeeze
Нам не нужны лишние размеры (например, 1 в [5,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]])
# Sequence x batch x dimension
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]
.Причиной этого будет двунаправленный и многослойный подход.Скажем, вы хотите суммировать двунаправленный вывод, это будет означать код вдоль этих строк
# batch_size and hidden_size should be inferred which clusters the code further
encoded[-1].view(batch_size, 2, hidden_size).sum(dim=1)
И именно поэтому была использована строка _, (last_hidden, _) = self.encoder(input)
.
3.Почему выходные данные сходятся к 0,2?
На самом деле, это была ошибка на вашей стороне и только в последней части.
Выведите формы ваших прогнозов и целей:
# Your output
torch.Size([5, 1, 1])
# Your target
torch.Size([5, 1])
Если предоставлены эти фигуры, MSELoss по умолчанию использует аргумент size_average=True
.И да, он усредняет ваши цели и ваш результат, который, по сути, рассчитывает потери для среднего значения вашего тензора (около 2,5 в начале) и среднего значения вашей цели , равного 0,2 .
Таким образом, сеть сходится правильно, но ваши цели неверны.
3.1 Первое и неправильное решение
Предоставьте MSELoss с аргументомсокращение = "сумма", хотя это действительно временно и работает случайно.Сначала сеть попытается получить все выходы равными сумме (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), как и ваш вывод (после сжатия)ненужные размеры).
Я изменил параметры Адама на значения по умолчанию, так как он быстрее сходится таким образом.
4.Окончательный рабочий код
Для краткости, вот код и результаты:
import torch
import torch.nn as nn
import torch.optim as optim
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):
# Encode
_, (last_hidden, _) = self.encoder(input)
# It is way more general that way
encoded = last_hidden.repeat(input.shape)
# Decode
y, _ = self.decoder(encoded)
return torch.squeeze(y)
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)
А вот результаты после ~ 60k шагов (на самом деле они застряли после ~ 20k шагов, вы можете захотетьулучшить оптимизацию и поиграть со скрытым размером для лучших результатов):
step=59682
tensor([0.0260, 0.0886, 0.1976, 0.3079, 0.3962], grad_fn=<SqueezeBackward0>)
Кроме того, L1Loss (он же Mean Absolute Error ) может получить лучшие результаты в этом случае:
step=10645
tensor([0.0405, 0.1049, 0.1986, 0.3098, 0.4027], grad_fn=<SqueezeBackward0>)
Осталась настройка и правильное пакетирование этой сетидля вас, надеюсь, вы повеселитесь сейчас, и вы поняли идею.:)
PS.Я повторяю всю форму входной последовательности, так как это более общий подход и должен работать с партиями и большим количеством измерений из коробки.