Funções de Perda em PyTorch

Introdução

As funções de perda são fundamentais na treinamento de modelos de ML, e, na maioria dos projetos de aprendizagem de máquina, não há como guiar o modelo para fazer previsões corretas sem uma função de perda. Em termos simples, uma função de perda é uma função matemática ou expressão usada para medir quão bem um modelo está sendo feito em algum conjunto de dados. Sabendo quão bem um modelo está sendo feito the um determinado conjunto de dados dá ao desenvolvedor insights para tomar muitas decisões durante o treinamento, como usar um novo modelo mais poderoso ou até mesmo mudar a própria função de perda para um tipo diferente. Falando de tipos de funções de perda, muitas destas funções de perda foram desenvolvidas ao longo dos anos, cada uma adequada para uso em uma tarefa de treinamento particular.

Pré-requisitos

Este artigo requer entendimento de redes neurais. Ao nível superior, as redes neurais são compostas de nós interligados (“”neurônios””) organizados em camadas. Eles aprendem e fazem previsões através de um processo chamado “treinamento”, que ajusta os pesos e bias das conexões entre os neurônios. O entendimento de redes neurais inclui o conhecimento de suas diferentes camadas (camada de entrada, camadas ocultas, camada de saída), funções de ativação, algoritmos de otimização (variantes do descida de gradiente), funções de perda, etc.

Ainda assim, a familiaridade com a sintaxe do Python e com a biblioteca PyTorch é essencial para entender os exemplos de código apresentados nesta publicação.

Neste artigo, vamos explorar diferentes funções de perda que fazem parte do módulo PyTorch nn. Vamos então mergulhar mais a fundo em como a PyTorch expõe essas funções de perda aos usuários como parte de sua API de módulo nn, construindo uma personalizada.

Com um entendimento de nível superior de que é uma função de perda, vamos explorar alguns detalhes técnicos sobre como as funções de perda funcionam.

O que são funções de perda?

Como dissemos anteriormente, as funções de perda nos diz quanto bem um modelo está num determinado conjunto de dados. Tecnicamente, elas fazem isso medindo quão próximo o valor predito é do valor real. Quando nosso modelo está fazendo previsões muito próximas dos valores reais tanto no conjunto de treinamento quanto no de teste, isso significa que temos um modelo bastante robusto.

Embora as funções de perda nos dêem informações críticas sobre o desempenho do nosso modelo, essa não é a função primária das funções de perda, já que existem técnicas mais robustas para avaliar os nossos modelos, como a acurácia e as pontuações F. O importante das funções de perda é realizado principalmente durante o treinamento, onde nudgamos os pesos do nosso modelo na direção que minimiza a perda. Fazendo isso, aumentamos a probabilidade do nosso modelo fazer previsões corretas, algo que provavelmente não seria possível sem uma função de perda.

Diferentes funções de perda servem para diferentes problemas, cada uma elaborada com cuidado por pesquisadores para garantir um fluxo de gradiente estavel durante o treinamento.

Às vezes, as expressões matemáticas das funções de perda podem ser um pouco assustadoras, e isso levou alguns desenvolvedores a tratá-las como caixas pretas. Vamos revelar algumas das funções de perda mais usadas do PyTorch mais tarde, mas antes disso, vamos ver como usamos funções de perda no mundo do PyTorch.

Funções de Perda no PyTorch

O PyTorch vem com muitas funções de perda canônicas com padrões de design simples que permitem que os desenvolvedores iteriem rapidamente sobre essas diferentes funções de perda durante o treinamento. Todas as funções de perda do PyTorch estão empacotadas no módulo nn, a classe base de todas as redes neurais do PyTorch. Isso torna a adição de uma função de perda em seu projeto tão simples quanto adicionar uma linha de código. Vamos ver como adicionar uma função de perda de erro quadrático médio no PyTorch.

import torch.nn as nn
MSE_loss_fn = nn.MSELoss()

A função retornada acima pode ser usada para calcular quanto uma previsão está longe do valor real usando o formato abaixo.

#predicted_value é a previsão de nossa rede neural
#target é o valor real em nossa base de dados
#loss_value é a perda entre o valor previso e o valor real
Loss_value = MSE_loss_fn(predicted_value, target)

Agora que temos uma ideia de como usar funções de perda em PyTorch, vamos mergulhar fundo nas operações de trás das cenas de várias das funções de perda que PyTorch oferece.

Quais funções de perda estão disponíveis em PyTorch?

Muitas destas funções de perda que vem com PyTorch são amplamente categorizadas em 3 grupos – perda de regressão, perda de classificação e perda de classificação.

Perda de regressão são principalmente preocupadas com valores contínuos que podem assumir qualquer valor entre dois limites. Um exemplo disto seria as previsões dos preços de casas de uma comunidade.

Funções de perda de classificação lidam com valores discretos, como a tarefa de classificar um objeto como caixa, caneta ou garrafa.

Perda de classificação prevê as distâncias relativas entre valores. Um exemplo disto seria a verificação facial, onde queremos saber quais imagens de faces pertencem a uma determinada face e consegue fazer isso classificando quais faces sim e não sim pertencem ao original dono da face através do grau de aproximação relativa à face alvo.

Função de perda L1 / Erro Absoluto Médio

A função de perda L1 calcula a média aritmética do erro absoluto entre cada valor no tensor predito e o valor alvo. Primeiro, ela calcula a diferença absoluta entre cada valor no tensor predito e o valor alvo, e computa a soma de todos os valores retornados de cada cálculo de diferença absoluta. Finalmente, ela calcula a média desta soma de valores para obter o erro absoluto médio (MAE). A função de perda L1 é muito resistente ao tratamento de ruído.

import torch.nn as nn

#size_average e reduce estão obsoletos

#reduction especifica o método de redução a ser aplicado ao saída. Valores possíveis são 'mean' (padrão) onde computamos a média da saída, 'sum' onde a saída é somada e 'none' que aplica nenhuma redução à saída

Loss_fn = nn.L1Loss(size_average=None, reduce=None, reduction='mean')

input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss_fn(input, target)
print(output) #tensor(0.7772, grad_fn=<L1LossBackward>)

O valor único retornado é a perda calculada entre dois tensores com dimensão 3×5.

Erro Quadrático Médio

A erro quadrático médio partilha algumas semelhanças notáveis com a MAE. Em vez de calcular a diferença absoluta entre os valores no tensor de predição e no alvo, como é o caso com o erro médio absoluto, ele calcula a diferença quadrada entre os valores no tensor de predição e no tensor de alvo. Fazendo isso, as diferenças relativamente grandes são penalizadas mais, enquanto as diferenças relativamente pequenas são penalizadas menos. O MSE é considerado menos resistente em lidar com valores atípicos e ruído do que a MAE, no entanto.

import torch.nn as nn

loss = nn.MSELoss(size_average=None, reduce=None, reduction='mean')
#As explicações dos parâmetros da função de perda L1 se aplicam aqui.

input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss(input, target)
print(output) #tensor(0.9823, grad_fn=<MseLossBackward>)

Perda de Cross-Entropy

A perda de cross-entropy é usada em problemas de classificação envolvendo um número de classes discretas. Ela mede a diferença entre duas distribuições de probabilidade para um conjunto determinado de variáveis aleatórias. Normalmente, quando usamos a perda de cross-entropy, a saída de nossa rede é uma camada softmax, que garante que a saída da rede neural é um valor de probabilidade (valor entre 0-1).

A camada softmax consiste em duas partes – o expoente da predição para uma classe particular.

yi é a saída da rede neural para uma classe particular. A saída desta função é um número próximo de zero, mas nunca zero, se yi for grande e negativo, e mais próximo de 1 se yi for positivo e muito grande.

import numpy as np

np.exp(34) #583461742527454.9
np.exp(-34) #1.713908431542013e-15

A segunda parte é um valor de normalização e é usado para garantir que a saída da camada softmax seja sempre um valor de probabilidade.

Este é obtido pela soma de todos os expoentes de cada valor de classe. A equação final da softmax se parece com isto:

]

No módulo nn do PyTorch, a perda de cross-entropia combina a log-softmax e a perda de negativo log-likelihood (NLL) em uma única função de perda.

Note como a função de gradiente na saída imprimida é uma perda NLL. Isso na verdade revela que a perda de cross-entropia combina a perda NLL por trás de uma camada log-softmax.

Perda de Negativo Log-Likelihood (NLL)

A função de perda NLL funciona muito semelhante à função de perda de cross-entropia. A perda de cross-entropia combina uma camada log-softmax e a perda NLL para obter o valor da perda de cross-entropia. Isso significa que a perda NLL pode ser usada para obter o valor de perda de cross-entropia colocando na camada de saída do neural network uma camada log-softmax em vez de uma camada softmax normal.

m = nn.LogSoftmax(dim=1)
loss = nn.NLLLoss()
# entrada tem tamanho N x C = 3 x 5
input = torch.randn(3, 5, requires_grad=True)
# cada elemento no alvo deve ter 0 <= valor < C
target = torch.tensor([1, 0, 4])
output = loss(m(input), target)
output.backward()
# exemplo de perda 2D (usado, por exemplo, com entradas de imagem)
N, C = 5, 4
loss = nn.NLLLoss()
# entrada tem tamanho N x C x altura x largura
data = torch.randn(N, 16, 10, 10)
conv = nn.Conv2d(16, C, (3, 3))
m = nn.LogSoftmax(dim=1)
# cada elemento no alvo deve ter 0 <= valor < C
target = torch.empty(N, 8, 8, dtype=torch.long).random_(0, C)
output = loss(m(conv(data)), target)
print(output) # tensor(1.4892, grad_fn=<NllLoss2DBackward>)

# crédito NLLLoss — Documentação PyTorch 1.9.0

Perda de Cross-Entropia Binaria

A perda de cross-entropia binaria é uma classe especial de perdas de cross-entropia usada para o problema especial de classificar pontos de dados em apenas duas classes. As etiquetas para este tipo de problema são normalmente binárias, portanto, o objetivo é empurrar o modelo a prever um número próximo de zero para uma etiqueta zero e um número próximo de um para uma etiqueta um. Normalmente, quando usando a perda BCE para classificação binaria, a saída da rede neural é uma camada de sigmóide para garantir que a saída seja um valor próximo de zero ou um valor próximo de um.

import torch.nn as nn

m = nn.Sigmoid()
loss = nn.BCELoss()
input = torch.randn(3, requires_grad=True)
target = torch.empty(3).random_(2)
output = loss(m(input), target)
print(output) # tensor(0.4198, grad_fn=<BinaryCrossEntropyBackward>)

Perda de Cross-Entropia Binária com Logits

Como mencionamos na seção anterior, uma perda de cross-entropia binária é normalmente output com uma camada de sigmóide para garantir que o output esteja entre 0 e 1. Uma perda de cross-entropia binária com logits combina estas duas camadas em apenas uma camada. De acordo com a documentação de PyTorch, esta é uma versão mais numéricamente estável, pois ela usa a técnica de log-sum exp.

import torch
import torch.nn as nn

target = torch.ones([10, 64], dtype=torch.float32)  # 64 classes, tamanho do lote = 10
output = torch.full([10, 64], 1.5)  # Uma previsão (logit)
pos_weight = torch.ones([64])  # Todos os pesos são iguais a 1
criterion = torch.nn.BCEWithLogitsLoss(pos_weight=pos_weight)
loss = criterion(output, target)  # -log(sigmoid(1.5))
print(loss) #tensor(0.2014)

Perda Smooth L1

A função de perda L1 suave combina os benefícios da perda MSE e da perda MAE através de um valor heurístico beta. Este critério foi introduzido no artigo Fast R-CNN. Quando a diferença absoluta entre o valor verdadeiro e o valor predito é inferior a beta, o critério usa uma diferença quadrada, muito como a perda MSE. O gráfico da perda MSE é uma curva contínua, o que significa que o gradiente em cada valor de perda varia e pode ser derivado em todo o lado. Além disso, conforme o valor de perda diminui, o gradiente diminui, o que é conveniente durante o descida de gradiente. No entanto, para valores de perda muito altos, o gradiente explode, portanto o critério para passar para a MAE, para a qual o gradiente é quase constante para cada valor de perda, quando a diferença absoluta torna-se maior do que beta e a explosão potencial de gradiente é eliminada.

import torch.nn as nn

loss = nn.SmoothL1Loss()
input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss(input, target)

print(output) #tensor(0.7838, grad_fn=<SmoothL1LossBackward>)

Perda de Embedimento Hinge

A perda de embedimento hinge é usada principalmente em tarefas de aprendizado semi-supervisionado para medir a similaridade entre dois entradas. Ela é usada quando existe um tensor de entrada e um tensor de rótulo contendo valores de 1 ou -1. Ela é principalmente usada em problemas envolvendo embeddings não-lineares e aprendizado semi-supervisionado.

import torch
import torch.nn as nn

input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)

hinge_loss = nn.HingeEmbeddingLoss()
output = hinge_loss(input, target)
output.backward()

print('input: ', input)
print('target: ', target)
print('output: ', output)

#entrada:  tensor([[ 1.4668e+00,  2.9302e-01, -3.5806e-01,  1.8045e-01,  #1.1793e+00],
#       [-6.9471e-05,  9.4336e-01,  8.8339e-01, -1.1010e+00,  #1.5904e+00],
#       [-4.7971e-02, -2.7016e-01,  1.5292e+00, -6.0295e-01,  #2.3883e+00]],
#       requires_grad=True)
#alvo:  tensor([[-0.2386, -1.2860, -0.7707,  1.2827, -0.8612],
#        [ 0.6747,  0.1610,  0.5223, -0.8986,  0.8069],
#        [ 1.0354,  0.0253,  1.0896, -1.0791, -0.0834]])
#saída:  tensor(1.2103, grad_fn=<MeanBackward0>)

Perda de Classificação por Margem

A perda de classificação por margem é uma das perdas de classificação cujo objetivo principal, ao contrário de outras funções de perda, é medir a distância relativa entre um conjunto de entradas em um conjunto de dados. A função de perda de classificação por margem recebe dois inputs e uma etiqueta que contém apenas 1 ou -1. Se a etiqueta for 1, então se assume que o primeiro input deve ter um melhor ranking do que o segundo input e, se a etiqueta for -1, se assume que o segundo input deve ter um melhor ranking do que o primeiro input. Esta relação é mostrada pela equação e pelo código abaixo.

import torch.nn as nn

loss = nn.MarginRankingLoss()
input1 = torch.randn(3, requires_grad=True)
input2 = torch.randn(3, requires_grad=True)
target = torch.randn(3).sign()
output = loss(input1, input2, target)
print('input1: ', input1)
print('input2: ', input2)
print('output: ', output)

#input1:  tensor([-1.1109,  0.1187,  0.9441], requires_grad=True)
#input2:  tensor([ 0.9284, -0.3707, -0.7504], requires_grad=True)
#output:  tensor(0.5648, grad_fn=<MeanBackward0>)

Perda de Margem de Tripla

Este critério mede a similaridade entre pontos de dados usando triplas de amostras de dados de treinamento. As triplas envolvidas são uma amostra de ancora, uma amostra positiva e uma amostra negativa. O objetivo é 1) obter a distância entre a amostra positiva e a ancora o mínimo possível e 2) obter a distância entre a ancora e a amostra negativa ser maior que a soma da margem e da distância entre a amostra positiva e a ancora. Normalmente, a amostra positiva pertence à mesma classe que a ancora, mas a amostra negativa não. Portanto, usando esta função de perda, nós visamos usar a perda de margem de tripla para prever um valor alto de similaridade entre a ancora e a amostra positiva e um valor baixo de similaridade entre a ancora e a amostra negativa.

import torch.nn as nn

triplet_loss = nn.TripletMarginLoss(margin=1.0, p=2)
anchor = torch.randn(100, 128, requires_grad=True)
positive = torch.randn(100, 128, requires_grad=True)
negative = torch.randn(100, 128, requires_grad=True)
output = triplet_loss(anchor, positive, negative)
print(output)  #tensor(1.1151, grad_fn=<MeanBackward0>)

Perda de Embedding por Cosseno

A perda de embedução de cosseno mede a perda dada entradas x1, x2, e uma matriz de rótulo y contendo valores 1 ou -1. Ela é usada para medir o grau de semelhança ou dissimilaridade entre duas entradas.

O critério mede a semelhança calculando a distância cossenóide entre os dois pontos de dados no espaço. A distância cossenóide correlaciona com o ângulo entre os dois pontos, o que significa que quanto menor o ângulo, mais próximos as entradas estão e, portanto, mais semelhantes elas são.

import torch.nn as nn

loss = nn.CosineEmbeddingLoss()
input1 = torch.randn(3, 6, requires_grad=True)
input2 = torch.randn(3, 6, requires_grad=True)
target = torch.randn(3).sign()
output = loss(input1, input2, target)
print('input1: ', input1)
print('input2: ', input2)
print('output: ', output)

#input1:  tensor([[ 1.2969e-01,  1.9397e+00, -1.7762e+00, -1.2793e-01, #-4.7004e-01,
#         -1.1736e+00],
#        [-3.7807e-02,  4.6385e-03, -9.5373e-01,  8.4614e-01, -1.1113e+00,
#          4.0305e-01],
#        [-1.7561e-01,  8.8705e-01, -5.9533e-02,  1.3153e-03, -6.0306e-01,
#          7.9162e-01]], requires_grad=True)
#input2:  tensor([[-0.6177, -0.0625, -0.7188,  0.0824,  0.3192,  1.0410],
#        [-0.5767,  0.0298, -0.0826,  0.5866,  1.1008,  1.6463],
#        [-0.9608, -0.6449,  1.4022,  1.2211,  0.8248, -1.9933]],
#       requires_grad=True)
#output:  tensor(0.0033, grad_fn=<MeanBackward0>)

Perda de Divergência de Kullback-Leibler

Dada duas distribuições, P e Q, a perda de divergência de Kullback-Leibler (KL) mede quanto informação é perdida quando P (suposto ser a distribuição verdadeira) é substituído por Q. Medindo quanto informação é perdida quando usamos Q para aproximar P, conseguimos obter a similaridade entre P e Q e, portanto, guiar o nosso algoritmo para produzir uma distribuição muito próxima da distribuição verdadeira, P. A perda de informação quando Q é usada para aproximar P não é a mesma quando P é usada para aproximar Q, portanto, a divergência KL não é simétrica.

import torch.nn as nn

loss = nn.KLDivLoss(size_average=None, reduce=None, reduction='mean', log_target=False)
input1 = torch.randn(3, 6, requires_grad=True)
input2 = torch.randn(3, 6, requires_grad=True)
output = loss(input1, input2)

print('output: ', output) #tensor(-0.0284, grad_fn=<KlDivBackward>)

Criando Uma Função de Perda Personalizada

O PyTorch nos fornece duas formas populares para construir nossa própria função de perda ajustada ao nosso problema; elas são, respectivamente, usando uma implementação de classe e usando uma implementação de função. Vamos ver como podemos implementar ambos os métodos começando pela implementação de função.

Este é sem dúvida a maneira mais simples de escrever sua própria função de perda personalizada. É tão fácil quanto criar uma função, passando para ela os entradas necessários e outros parâmetros, realizando alguma operação usando o core API de PyTorch ou o API Functional e retornando um valor. Vamos ver uma demonstração com a função de perda de erro quadrático médio personalizada.

def custom_mean_square_error(y_predictions, target):
  square_difference = torch.square(y_predictions - target)
  loss_value = torch.mean(square_difference)
  return loss_value

No código acima, definimos uma função de perda personalizada para calcular o erro quadrático médio dado um tensor de predição e um tensor de alvo

y_predictions = torch.randn(3, 5, requires_grad=True);
target = torch.randn(3, 5)
pytorch_loss = nn.MSELoss();
p_loss = pytorch_loss(y_predictions, target)
loss = custom_mean_square_error(y_predictions, target)
print('custom loss: ', loss)
print('pytorch loss: ', p_loss)

#custom loss:  tensor(2.3134, grad_fn=<MeanBackward0>)
#pytorch loss:  tensor(2.3134, grad_fn=<MseLossBackward>)

Nós podemos calcular a perda usando nossa função de perda personalizada e a função de perda MSE de PyTorch para observar que obtivemos os mesmos resultados.

Custom Loss with Python Classes

Este método é provavelmente a maneira padrão e recomendada de definir perdas personalizadas em PyTorch. A função de perda é criada como um nó na gráfico de rede neural por subclasse do módulo nn. Isso significa que nossa função de perda personalizada é exatamente o mesmo que uma camada de PyTorch, da mesma forma que uma camada de convolução é. Vamos ver uma demonstração de como isso funciona com uma perda MSE personalizada.

class Custom_MSE(nn.Module):
  def __init__(self):
    super(Custom_MSE, self).__init__();

  def forward(self, predictions, target):
    square_difference = torch.square(predictions - target)
    loss_value = torch.mean(square_difference)
    return loss_value
  
  def __call__(self, predictions, target):
     square_difference = torch.square(y_predictions - target)
     loss_value = torch.mean(square_difference)
     return loss_value

Pensamentos finais

Discutimos muito sobre as funções de perda disponíveis em PyTorch e também mergulhámos no funcionamento interno de quase todas essas funções de perda. Escolher a função de perda certa para um problema particular pode ser uma tarefa abrupta. Espero que este tutorial, juntamente com a documentação oficial do PyTorch, sirva de guia quando tentando entender qual função de perda se adapta bem ao seu problema.

Source:
https://www.digitalocean.com/community/tutorials/pytorch-loss-functions