Все переменные, которые вы хотите оптимизировать с помощью optimizer.step()
, должны иметь градиент.
В вашем случае это будет y
предсказано сетью, поэтому вы не должны detach
его (изграфик).
Обычно вы не меняете свой targets
, поэтому для них не нужны градиенты. Вам не нужно их detach
, тензоры по умолчанию не требуют градиента и не будут иметь обратного распространения.
Loss
будет иметь градиент, если его ингредиенты (по крайней мере, один) имеют градиент.
В целом, вам редко нужно заботиться об этом вручную.
Кстати. не используйте numpy
с PyTorch, редко бывает так. Вы можете выполнять большинство операций над массивом numpy
с тензором PyTorch.
BTW2. В pytorch
больше нет такой вещи, как Variable
, только тензоры, которым требуется градиент, и те, которые не требуют.
Недифференцируемость
1.1 Проблемы с существующим кодом
Действительно, вы используете функции, которые нельзя дифференцировать (а именно >=
и ==
). Это доставит вам неприятности только в случае ваших выходов, поскольку они требуют градиента (хотя вы можете использовать ==
и >=
для targets
).
Ниже я добавил вашу функцию потерь и обрисовал в общих чертахПроблемы в нем в комментариях:
# Gradient can't propagate if you detach and work in another framework
# Most Python constructs should be fine, detaching will ruin it though.
def msfe(outputs, targets):
# outputs=outputs.detach().numpy() # Do not detach, no need to do that
# targets=targets.detach().numpy() # No need for numpy either
pred_class = outputs >= 0.5 # This one is non-differentiable
# n_0 = sum(targets==0) # Do not use sum, there is pytorch function for that
# n_1 = sum(targets==1)
n_0 = torch.sum(targets == 0) # Those are not differentiable, but...
n_1 = torch.sum(targets == 1) # It does not matter as those are targets
# FPE = sum((targets==0)[[bool(p) for p in (pred_class==1)]])/n_0 # Do not use Python bools
# FNE = sum((targets==1)[[bool(p) for p in (pred_class==0)]])/n_1 # Stay within PyTorch
# Those two below are non-differentiable due to == sign as well
FPE = torch.sum((targets == 0.0) * (pred_class == 1.0)).float() / n_0
FNE = torch.sum((targets == 1.0) * (pred_class == 0.0)).float() / n_1
# This is obviously fine
loss = FPE ** 2 + FNE ** 2
# Loss should be a tensor already, don't do things like that
# Gradient will not be propagated, you will have a new tensor
# Always returning gradient of `1` and that's all
# loss = torch.tensor(loss, dtype=torch.float64, requires_grad=True)
return loss
1.2 Возможное решение
Итак, нужно избавиться от 3 недифференцируемых частей. В принципе, вы можете попытаться приблизить его к непрерывным выходам из вашей сети (при условии, что вы используете sigmoid
в качестве активации). Вот мое мнение:
def msfe_approximation(outputs, targets):
n_0 = torch.sum(targets == 0) # Gradient does not flow through it, it's okay
n_1 = torch.sum(targets == 1) # Same as above
FPE = torch.sum((targets == 0) * outputs).float() / n_0
FNE = torch.sum((targets == 1) * (1 - outputs)).float() / n_1
return FPE ** 2 + FNE ** 2
Обратите внимание, что для минимизации FPE
outputs
будет пытаться быть zero
в индексах, где targets
равны нулю. Аналогично для FNE
, если цели 1
, сеть также попытается вывести 1
.
Обратите внимание на сходство этой идеи с BCELoss (Binary CrossEntropy).
И, наконец, пример, на котором вы можете запустить это, просто для проверки работоспособности:
if __name__ == "__main__":
model = torch.nn.Sequential(
torch.nn.Linear(30, 100),
torch.nn.ReLU(),
torch.nn.Linear(100, 200),
torch.nn.ReLU(),
torch.nn.Linear(200, 1),
torch.nn.Sigmoid(),
)
optimizer = torch.optim.Adam(model.parameters())
targets = torch.randint(high=2, size=(64, 1)) # random targets
inputs = torch.rand(64, 30) # random data
for _ in range(1000):
optimizer.zero_grad()
outputs = model(inputs)
loss = msfe_approximation(outputs, targets)
print(loss)
loss.backward()
optimizer.step()
print(((model(inputs) >= 0.5) == targets).float().mean())