Keras的CategoricalCrossEntropy到底是做什么的?

7

我正在将一个keras模型移植到torch中,但在softmax层后复制keras/tensorflow的'categorical_crossentropy'的确切行为时遇到了问题。我已经有一些解决此问题的方法,所以我只想了解tensorflow计算分类交叉熵时确切地是如何进行计算的。

作为玩具问题,我设置了标签和预测向量。

>>> import tensorflow as tf
>>> from tensorflow.keras import backend as K
>>> import numpy as np


>>> true = np.array([[0.0, 1.0], [1.0, 0.0]])
>>> pred = np.array([[0.0, 1.0], [0.0, 1.0]])

使用以下方法计算分类交叉熵:

>>> loss = tf.keras.losses.CategoricalCrossentropy()
>>> print(loss(pred, true).eval(session=K.get_session()))
8.05904769897461

这与分析结果不同。

>>> loss_analytical = -1*K.sum(true*K.log(pred))/pred.shape[0]
>>> print(loss_analytical.eval(session=K.get_session()))
nan

我研究了keras/tf的交叉熵源代码(参见Tensorflow Github源代码中的Softmax Cross Entropy实现),并在https://github.com/tensorflow/tensorflow/blob/c903b4607821a03c36c17b0befa2535c7dd0e066/tensorflow/compiler/tf2xla/kernels/softmax_op.cc的第116行找到了C函数。在那个函数中,有一个注释:

// sum(-labels *
// ((logits - max_logits) - log(sum(exp(logits - max_logits)))))
// along classes
// (The subtraction broadcasts along the batch dimension.)

实施这一点后,我尝试了:

>>> max_logits = K.max(pred, axis=0)
>>> max_logits = max_logits
>>> xent = K.sum(-true * ((pred - max_logits) - K.log(K.sum(K.exp(pred - max_logits)))))/pred.shape[0]

>>> print(xent.eval(session=K.get_session()))
1.3862943611198906

我也尝试打印xent.eval(session=K.get_session())的跟踪,但跟踪内容有大约95000行。 因此,问题是:计算'categorical_crossentropy'时Keras/TensorFlow究竟在做什么?它不返回nan是有道理的,否则会导致训练问题,但是这个8是从哪里来的呢?
2个回答

9

这里有一些我在您的代码中注意到的问题。

首先,您的预测结果显示了两个数据实例,[0.0, 1.0][0.0, 1.0]

pred = np.array([[0.0, 1.0], [0.0, 1.0]])

它们应该表示概率,但softmax之后的值通常不是完全为0.0和1.0。建议改为使用0.01和0.99。

其次,调用CateogoricalCrossEntropy()的参数应该是true, pred,而不是pred,true。具体请参考此处

因此,这就是我得到的结果:

import tensorflow as tf
from tensorflow.keras import backend as K
import numpy as np

true = np.array([[0.0, 1.0], [1.0, 0.0]])
pred = np.array([[0.01, 0.99], [0.01, 0.99]])

loss = tf.keras.losses.CategoricalCrossentropy()
print(loss(true, pred).numpy())
# 2.307610273361206

为了完整起见,让我们尝试使用 pred, true 所做的事情:

print(loss(pred, true).numpy())
# 8.05904769897461

这就是神秘的8.05来自哪里。

我的答案2.307610273361206正确吗?让我们手动计算损失。按照StackOverflow帖子中的解释,我们可以计算两个数据实例的损失,然后计算它们的平均值。

loss1 = -(0.0 * np.log(0.01) + 1.0 * np.log(0.99))
print(loss1) # 0.01005033585350145

loss2 = -(1.0 * np.log(0.01) + 0.0 * np.log(0.99))
print(loss2) # 4.605170185988091

# Total loss is the average of the per-instance losses.
loss = (loss1 + loss2) / 2
print(loss) # 2.307610260920796

所以看起来CategoricalCrossEntropy()产生了正确的答案。


1
谢谢你。我同意大多数 softmax 输出并不完全为零,但是在我转换成 torch 的代码中,训练通常会导致 loss 为“nan”,这可能是因为其中一个预测值为零。看起来 keras 中应用了剪辑(通过 epsilon,就像 @xdurch0 的回答一样)。 - ahagen

6
问题在于你的预测使用了硬的0和1。这会导致计算中出现nan,因为log(0)未定义(或无限)。
但没有记录的是Keras交叉熵自动通过剪切将值裁剪到范围[eps, 1-eps]内,以防止这种情况发生。这意味着,在你的例子中,Keras给出了不同的结果,因为它直接用其他值替换了预测值。
如果你使用软值替换你的预测,你应该能够复现结果。这是有意义的,因为你的网络通常会通过softmax激活返回这样的值;硬的0/1只会在数字下溢的情况下发生。
如果你想亲自检查这一点,裁剪是在这里发生的。这个函数最终被CategoricalCrossentropy函数调用。epsilon在其他地方定义,但似乎是0.0000001——尝试使用pred = np.clip(pred, 0.0000001, 1-0.0000001)进行手动计算,你应该能看到结果为8.059047875479163

1
太好了!我知道Keras如何进行nan保护存在差异,而你找到了它。我能够使用Keras和torch.clamp进行复现。有趣的是,这使用了分析计算(sum(-true * log(pred))/batch_size),而不是Keras的softmax_op.cc中描述的log(sum(exp))。 - ahagen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接