Как правильно обновить веса в PyTorch?
Я пытаюсь реализовать градиентный спуск с PyTorch в соответствии с этой схемой, но не могу понять, как правильно обновлять веса. Это просто игрушечный пример с 2 линейными слоями с 2 узлами в скрытом слое и одним выходом.
Скорость обучения = 0,05; целевой выход = 1
https://hmkcode.github.io/ai/backpropagation-step-by-step/
Мой код выглядит следующим образом:
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
class MyNet(nn.Module):
def __init__(self):
super(MyNet, self).__init__()
self.linear1 = nn.Linear(2, 2, bias=None)
self.linear1.weight = torch.nn.Parameter(torch.tensor([[0.11, 0.21], [0.12, 0.08]]))
self.linear2 = nn.Linear(2, 1, bias=None)
self.linear2.weight = torch.nn.Parameter(torch.tensor([[0.14, 0.15]]))
def forward(self, inputs):
out = self.linear1(inputs)
out = self.linear2(out)
return out
losses = []
loss_function = nn.L1Loss()
model = MyNet()
optimizer = optim.SGD(model.parameters(), lr=0.05)
input = torch.tensor([2.0,3.0])
print('weights before backpropagation = ', list(model.parameters()))
for epoch in range(1):
result = model(input )
loss = loss_function(result , torch.tensor([1.00],dtype=torch.float))
print('result = ', result)
print("loss = ", loss)
model.zero_grad()
loss.backward()
print('gradients =', [x.grad.data for x in model.parameters()] )
optimizer.step()
print('weights after backpropagation = ', list(model.parameters()))
Результат следующий:
weights before backpropagation = [Parameter containing:
tensor([[0.1100, 0.2100],
[0.1200, 0.0800]], requires_grad=True), Parameter containing:
tensor([[0.1400, 0.1500]], requires_grad=True)]
result = tensor([0.1910], grad_fn=<SqueezeBackward3>)
loss = tensor(0.8090, grad_fn=<L1LossBackward>)
gradients = [tensor([[-0.2800, -0.4200], [-0.3000, -0.4500]]),
tensor([[-0.8500, -0.4800]])]
weights after backpropagation = [Parameter containing:
tensor([[0.1240, 0.2310],
[0.1350, 0.1025]], requires_grad=True), Parameter containing:
tensor([[0.1825, 0.1740]], requires_grad=True)]
Значенияпрямого прохода:
2x0.11 + 3*0.21=0.85 ->
2x0.12 + 3*0.08=0.48 -> 0.85x0.14 + 0.48*0.15=0.191 -> loss =0.191-1 = -0.809
Обратный проход: давайте вычислим w5 и w6 (веса выходных узлов)
w = w - (prediction-target)x(gradient)x(output of previous node)x(learning rate)
w5= 0.14 -(0.191-1)*1*0.85*0.05= 0.14 + 0.034= 0.174
w6= 0.15 -(0.191-1)*1*0.48*0.05= 0.15 + 0.019= 0.169
В моем примере Torch не умножает потери на производную, поэтому мы получаем неправильные веса после обновления. Для выходного узла мы получили новые веса w5,w6 [0,1825, 0,1740], когда он должен быть [0,174, 0,169]
Двигаясь назад, чтобы обновить первый вес выходного узла (w5), нам нужно вычислить: (цель прогноза)x(градиент)x(выход предыдущего узла)x(скорость обучения)=-0,809*1*0,85*0,05=-0,034. Обновленный вес w5 = 0,14-(-0,034)=0,174. Но вместо этого пыторч вычислил новый вес = 0,1825. Забыл умножить на (прогноз-цель)=-0,809. Для выходного узла мы получили градиенты -0,8500 и -0,4800. Но нам все еще нужно умножить их на потери 0,809 и скорость обучения 0,05, прежде чем мы сможем обновить веса.
Как правильно это сделать? Должны ли мы передать "убыток" в качестве аргумента функции backward() следующим образом: loss.backward(loss) .
Это, кажется, исправить это. Но я не смог найти никакого примера по этому поводу в документации.
1 ответ
Вы должны использовать .zero_grad()
с оптимизатором, так optimizer.zero_grad()
не потеря или модель, как предлагается в комментариях (хотя модель в порядке, но она не ясна или не читаема IMO).
За исключением того, что ваши параметры обновляются нормально, поэтому ошибка не на стороне PyTorch.
На основании значений градиента вы указали:
gradients = [tensor([[-0.2800, -0.4200], [-0.3000, -0.4500]]),
tensor([[-0.8500, -0.4800]])]
Давайте умножим их все на вашу скорость обучения (0,05):
gradients_times_lr = [tensor([[-0.014, -0.021], [-0.015, -0.0225]]),
tensor([[-0.0425, -0.024]])]
Наконец, давайте применим обычный SGD (theta -= градиент * lr), чтобы получить точно такие же результаты, как в PyTorch:
parameters = [tensor([[0.1240, 0.2310], [0.1350, 0.1025]]),
tensor([[0.1825, 0.1740]])]
То, что вы сделали, взяли градиенты, рассчитанные PyTorch, и умножили их на выходные данные предыдущего узла, и это не так!,
Что ты наделал:
w5= 0.14 -(0.191-1)*1*0.85*0.05= 0.14 + 0.034= 0.174
Что нужно было сделать (используя результаты PyTorch):
w5 = 0.14 - (-0.85*0.05) = 0.1825
Нет умножения предыдущего узла, это делается за кулисами (вот что .backprop()
has - вычисляет правильные градиенты для всех узлов), нет необходимости умножать их на предыдущие.
Если вы хотите рассчитать их вручную, вы должны начать с убытка (с дельтой, равной единице) и с обратным ходом до упора (не используйте здесь скорость обучения, это другая история!).
После того, как все они рассчитаны, вы можете умножить каждый вес на скорость обучения оптимизаторов (или любую другую формулу по этому вопросу, например, Momentum), и после этого у вас будет правильное обновление.
Как рассчитать бэкпроп
Скорость обучения не является частью обратного распространения, оставьте ее в покое, пока вы не вычислите все градиенты (она смешивает отдельные алгоритмы вместе, процедуры оптимизации и обратное распространение).
1. Производная от общей ошибки по выводу
Ну, я не знаю, почему вы используете Mean Absolute Error (в то время как в учебнике это Mean Squared Error), и поэтому оба эти результата различаются. Но пойдем с твоим выбором.
Производная от | y_true - y_pred | wrt to y_pred равно 1, так что это НЕ то же самое, что потеря. Перейдите на MSE, чтобы получить равные результаты (здесь производная будет (1/2 * y_pred - y_true), но мы обычно умножаем MSE на два, чтобы убрать первое умножение).
В случае MSE вы умножили бы на величину потерь, но она полностью зависит от функции потерь (было немного прискорбно, что учебник, который вы использовали, не указывал на это).
2. Производная от общей ошибки по w5
Вы, вероятно, могли бы пойти отсюда, но... Производная полной ошибки по отношению к w5 - это выходной результат h1 (0,85 в данном случае). Мы умножаем его на производную от общей ошибки по выводу (это 1!) И получаем 0,85, как это сделано в PyTorch. Та же идея относится и к w6.
Я настоятельно советую вам не путать скорость обучения с backprop, вы усложняете свою жизнь (и это нелегко с backprop IMO, довольно нелогичным), и это две разные вещи (не могу не подчеркнуть, что этого достаточно).
Этот источник хороший, более пошаговый, с немного более сложной сетевой идеей (включая активации), так что вы можете лучше понять, если вы пройдете через все это.
Кроме того, если вы действительно заинтересованы (и, похоже, знаете), чтобы узнать больше об этом, рассчитайте поправки на вес для других оптимизаторов (скажем, нестеров), чтобы вы знали, почему мы должны разделять эти идеи.