【问题标题】:What exactly is Keras's CategoricalCrossEntropy doing?Keras 的 CategoricalCrossEntropy 到底在做什么?
【发布时间】:2021-03-15 18:55:02
【问题描述】:

我正在将 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 的交叉熵的源代码(参见 Softmax Cross Entropy implementation in Tensorflow Github Source Code),并在 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/tf 到底在做什么?它不返回nan 是有道理的,这会导致训练问题,但是 8 是从哪里来的呢?

【问题讨论】:

    标签: python tensorflow machine-learning keras


    【解决方案1】:

    以下是我在您的代码中注意到的一些内容。

    首先,您的预测显示两个数据实例,[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。

    其次,arguments to the CateogoricalCrossEntropy() call 应该是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 正确吗?让我们手动计算损失。按照this StackOverflow post的解释,我们可以计算两个数据实例的损失,然后计算它们的平均值。

    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() 给出了正确的答案。

    【讨论】:

    • 谢谢。我同意大多数 softmax 输出并非完全为零,但我的 Torch 移植代码中的训练通常以丢失nan 而告终,我认为这是因为其中一个预测为零而发生的。看起来在 keras 中应用了一个剪辑(通过 epsilon,如@xdurch0 的回答)。
    【解决方案2】:

    问题在于您在预测中使用了硬 0 和 1。这会导致您的计算中出现nan,因为log(0) 是未定义的(或无限的)。

    没有真正记录的是,Keras 交叉熵通过将值裁剪到[eps, 1-eps] 范围内来自动“保护”这一点。这意味着,在您的示例中,Keras 为您提供了不同的结果,因为它完全用其他值替换了预测。

    如果您用软值替换您的预测,您应该能够重现结果。无论如何,这是有道理的,因为您的网络通常会通过 softmax 激活返回这些值;硬 0/1 仅发生在数值下溢的情况下。

    如果您想自己检查,剪裁发生在here。此函数最终由CategoricalCrossentropy 函数调用。 epsilon 是在别处定义的,但它似乎是 0.0000001 - 尝试使用 pred = np.clip(pred, 0.0000001, 1-0.0000001) 进行手动计算,您应该会看到结果 8.059047875479163

    【讨论】:

    • 太棒了!我知道差异在于 Keras 的 nan 守卫方式,而你发现了它。我能够使用 Keras 和 torch.clamp 进行复制。有趣的是,这使用了解析计算 (sum(-true * log(pred))/batch_size),而不是 Keras 的 softmax_op.cc 中描述的 log(sum(exp)。
    猜你喜欢
    • 2011-10-30
    • 2010-12-30
    • 1970-01-01
    • 1970-01-01
    • 2014-03-20
    • 2019-06-14
    • 2023-03-15
    • 1970-01-01
    相关资源
    最近更新 更多