from_logits=True 和 from_logits=False 得到 UNet 的 tf.losses.CategoricalCrossentropy 不同的训练结果

from_logits=True and from_logits=False get different training result for tf.losses.CategoricalCrossentropy for UNet

我正在使用 unet 进行图像语义分割工作,如果我像这样为最后一层设置 Softmax Activation

...
conv9 = Conv2D(n_classes, (3,3), padding = 'same')(conv9)
conv10 = (Activation('softmax'))(conv9)
model = Model(inputs, conv10)
return model
...

然后使用 loss = tf.keras.losses.CategoricalCrossentropy(from_logits=False) 即使只有一张训练图像,训练也不会收敛

但是如果我不这样设置最后一层的Softmax Activation

...
conv9 = Conv2D(n_classes, (3,3), padding = 'same')(conv9)
model = Model(inputs, conv9)
return model
...

然后使用 loss = tf.keras.losses.CategoricalCrossentropy(from_logits=True) 对于一张训练图像,训练将收敛

我的 groundtruth 数据集是这样生成的:

X = []
Y = []
im = cv2.imread(impath)
X.append(im)
seg_labels = np.zeros((height, width, n_classes))
for spath in segpaths:
    mask = cv2.imread(spath, 0)
    seg_labels[:, :, c] += mask
Y.append(seg_labels.reshape(width*height, n_classes))

为什么?我的用法有问题吗?

这是我的git实验代码:https://github.com/honeytidy/unet 您可以签出并 运行(可以 运行 上 cpu)。您可以更改激活层和 CategoricalCrossentropy 的 from_logits,看看我说的。

我猜问题出在softmax激活函数上。查看 doc 我发现 sotmax 默认应用于最后一个轴。你能看看 model.summary() 并检查这是否是你想要的吗?

将 "softmax" 激活推入交叉熵损失层显着简化了损失计算并使其在数值上更加稳定。
在您的示例中,数字问题可能足以使训练过程对 from_logits=False 选项无效。

你可以在this post中找到交叉熵损失("info gain"损失的特例)的推导。此推导说明了将 softmax 与交叉熵损失相结合时避免的数值问题。

为了 softmax 正常工作,您必须确保:

  • 您正在使用 'channels_last' 作为 Keras 默认通道配置。

    • 这意味着模型中的形状将像 (None, height, width, channels)
    • 这似乎是你的情况,因为你将 n_classes 放在最后一个轴上。但这也很奇怪,因为您使用的是 Conv2D 并且输出 Y 应该是 (1, height, width, n_classes) 而不是您使用的那种奇怪的形状。
  • 您的 Y 只有零和一(不是图像通常出现的 0 和 255)

    • 检查 Y.max() == 1Y.min() == 0
    • 您可能需要 Y = Y / 255.
  • 只有一个 class 是正确的(您的数据没有超过一个 path/channel 值 = 1)。

    • 检查 (Y.sum(axis=-1) == 1).all()True

from_logits = True 表示模型获得的损失值未归一化,主要用于我们的模型中没有任何 softmax 函数的情况。例如https://www.tensorflow.org/tutorials/generative/dcgan 在这个模型中,他们没有使用 softmax 激活函数,换句话说,我们可以说它有助于数值稳定性。

默认情况下,Tensorflow 中针对 class化问题实现的所有损失函数都使用 from_logits=False。请记住,在 class化问题的情况下,在预测结束时,通常需要根据概率产生输出。

看看下图,网络的最后一层(就在softmax函数之前)

所以序列是神经网络⇒最后一层输出⇒Softmax或Sigmoid函数⇒每个class.

的概率

例如在 multi-class class化问题的情况下,输出可以是 y1, y2, ....... yn 想要以某种概率产生每个输出. (见输出层)。现在,这个输出层将在 cross-entropy 损失函数中与真实标签进行比较。

让我们举一个例子,我们的网络为 classification 任务生成输出。假设您的神经网络正在产生输出,然后您使用 softmax 函数将该输出转换为概率,并使用 cross-entropy 损失函数

计算损失
# output produced by the last layer of NN
nn_output_before_softmax = [3.2, 1.3, 0.2, 0.8]

# converting output of last layer of NN into probabilities by applying softmax
nn_output_after_softmax = tf.nn.softmax(nn_output_before_softmax)

# output converted into softmax after appling softmax
print(nn_output_after_softmax.numpy())
[0.77514964 0.11593805 0.03859243 0.07031998]

y_true = [1.0, 0.0, 0.0, 0.0]

现在有两种情况:

  1. 一个是明确使用 softmax(或 sigmoid)函数

  2. 一个是没有单独使用softmax函数,想包含在损失函数的计算中

1) 一个是显式使用 softmax(或 sigmoid)函数

当明确使用 softmax(或 sigmoid)函数时,对于 classification 任务,TensorFlow 损失函数中有一个默认选项,即 from_logits=False。所以这里 TensorFlow 假设你将输入到损失函数的任何输入都是概率,所以不需要应用 softmax 函数。

# By default from_logits=False
loss_taking_prob = tf.keras.losses.CategoricalCrossentropy(from_logits=False) 

loss_1 = loss_taking_prob(y_true, nn_output_after_softmax)
print(loss_1)
tf.Tensor(0.25469932, shape=(), dtype=float32)

2)一种是没有单独使用softmax函数,想把它包含在损失函数的计算中。这意味着您提供给损失函数的任何输入都不会缩放(意味着输入只是从 -inf 到 +inf 的数字,而不是概率)。这里你让 TensorFlow 为你执行 softmax 操作。

loss_taking_logits = tf.keras.losses.CategoricalCrossentropy(from_logits=True)

loss_2 = loss_taking_logits(y_true, nn_output_before_softmax)
print(loss_2)
tf.Tensor(0.2546992, shape=(), dtype=float32)

请记住,当 from_logits=False 应该为 True 时,您使用它会导致采用概率的 softmax 并生成不正确的模型