PyTorch: Multi-class 分割损失值 != 0 当使用目标图像作为预测时

PyTorch: Multi-class segmentation loss value != 0 when using target image as the prediction

我正在使用 PyTorch 执行语义分割。数据集中共有 103 个不同的 类,目标是 RGB 图像,只有 Red 通道包含标签。我使用 nn.CrossEntropyLoss 作为我的损失函数。为了理智,我想检查使用 nn.CrossEntropyLoss 对于这个问题是否正确以及它是否具有预期的行为

我从我的数据集中选择一个随机掩码并使用此自定义转换创建它的分类版本

class ToCategorical:
    def __init__(self, n_classes: int) -> None:
        self.n_classes = n_classes

    def __call__(self, sample: torch.Tensor):
        mask = sample.permute(1, 2, 0)
        categories = torch.unique(mask).tolist()[1:]  # get all categories other than 0
        # build a tensor with `n_classes` channels
        one_hot_image = torch.zeros(self.n_classes, *mask.shape[:-1])
        for category in categories:
            # get spacial locs where the categ is present
            rows, cols, _ = torch.where(mask == category)
            # in same spacial loc but in `categ` channel fill 1
            one_hot_image[category, rows, cols] = 1  
        return one_hot_image

然后我将此图像作为输出(预测)发送,并使用地面实况掩码作为损失函数的目标。

import torch.nn as nn

mask = T.PILToTensor()(Image.open("path_to_image").convert("RGB"))
categorical_mask = ToCategorical(103)(mask).unsqueeze(0)
mask = mask[0].unsqueeze(0)  # get only the red channel, add fake batch_dim

loss_fn = nn.CrossEntropyLoss()

target = mask
output = categorical_mask

print(output.shape, target.shape)
print(loss_fn(output, target.to(torch.long)))

我预计损失为零,但令我惊讶的是,输出如下

torch.Size([1, 103, 600, 800]) torch.Size([1, 600, 800])
tensor(4.2836)

我用数据集中的其他样本进行了验证,我也获得了其他掩码的相似值。难道我做错了什么?当输出与目标相同时,我预计损失 = 0。

PS。我也知道 nn.CrossEntropyLoss 与使用 log_softmax 后跟 nn.NLLLoss() 相同,但即使我也通过使用 nllloss

获得了相同的值

供参考

使用的数据集:UECFoodPixComplete

我想解决这个问题:

I expect the loss to be = 0 when the output is the same as the target.

如果预测与目标匹配,预测对应于密集目标张量中包含的one-hot-encoding个标签,但损失本身不应该等于零。实际上,它永远不会等于零,因为 nn.CrossEntropyLoss 函数根据 定义.

始终为正

让我们举一个最小的例子,其中 #C classes 和目标 y_pred 以及由 prefect[ 组成的预测 y_pred =102=] 预测:

快速提醒:

  • softmax 应用于 logits (q_i) 作为 p_i = log(exp(q_i)/sum_j(exp(q_j)):

    >>> p = F.softmax(y_pred, 1)
    

    同样,如果您使用 log-softmax,定义为 logp_i = log(p_i):

    >>> logp = F.log_softmax(y_pred, 1)
    
  • 然后是 负似然 函数在输入 x 和目标 y 之间计算:-y*x。与 softmax 相关联,它分别归结为 -y*p-y*logp。在任何情况下,无论您是否应用日志,只有对应于真实 classes 的预测将保留,因为其他的是 zeroed-out.


也就是说,在 y_pred 上应用 NLLLoss 确实会像您在问题中预期的那样得到 0。然而,这里我们将它应用于概率分布或 log-probability: plogp 分别!

在我们的特定情况下,p_i = 1 代表真正的 class 和 p_i = 0 代表所有其他 classes (其中有 #C - 1 个)。这意味着与真实 class 关联的 logit 的 softmax 将等于 exp(1)/sum_i(p_i)。自从 sum_i(p_i) = (#C-1)*exp(0) + exp(1)。因此我们有:

softmax(p) = e / (#C - 1 + e)

log-softmax类似:

log-softmax(p) = log(e / (#C-1 + e)) = 1 - log(#C - 1 + e)

如果我们继续应用负似然函数,我们只会得到 cross-entropy(y_pred, y_true) = (nllloss o log-softmax)(y_pred, y_true)。这导致:

loss = - (1 - log(#C - 1 + e)) = log(#C - 1 + e) - 1

这实际上对应于 nn.CrossEntropyLoss 函数的最小值。


关于您 #C = 103 的具体情况,您的代码可能存在问题...因为平均损失应等于 log(102 + e) - 1 大约 3.65.

>>> y_true = torch.randint(0,103,(1,1,2,5))
>>> y_pred = torch.zeros(1,103,2,5).scatter(1, y_true, value=1)

您可以使用提供的方法之一亲自查看:

  • 内置函数nn.functional.cross_entropy:

    >>> F.cross_entropy(y_pred, y_true[:,0])
    tensor(3.6513)
    
  • 人工计算数量:

    >>> logp = F.log_softmax(y_pred, 1)
    >>> -logp.gather(1, y_true).mean()
    tensor(3.6513)
    
  • 分析结果:

    >>> log(102 + e) - 1
    3.6513