交叉熵损失函数公式_keras中两种交叉熵损失函数的探讨

交叉熵损失函数公式_keras中两种交叉熵损失函数的探讨_第1张图片

交叉熵loss function, 多么熟悉的名字! 做过机器学习中分类任务的炼丹师应该随口就能说出这两种loss函数: categorical cross entropy 和 binary cross entropy,以下简称CE和BCE. 关于这两个函数, 想必大家听得最多的俗语或忠告就是:"CE用于多分类, BCE适用于二分类, 千万别用混了." 对于BCE前边的那个binary, 我们多半也能猜到它的适用场景, 但背后的原因是什么呢? 这个约定俗成的忠告真的对吗? 在本文中, 我们将在Neural Network的模型背景下,就这个问题进行稍微深入的探讨, 尝试给大家一个清晰合理的解释. (限于笔者对TF和Keras框架更为熟悉, 本文中出现的代码和framework将以这两者为主)


一、基本概念和公式

首先,我们先从公式入手:

CE:

。。。。。。(1)
其中, x表示输入样本, C为待分类的类别总数, 这里我们以手写数字识别任务(MNIST-based)为例, 其输入出的类别数为10, 对应的C=10.
为第i个类别对应的真实标签,
为对应的模型输出值.

BCE:

.。。。。。。(2)
其中
, 即每个类别输出节点都对应一个BCE值.

看到这里, 大家会发现两者的shape并不相同,对于单个样本而言,CE是一个数值,而BCE是一个向量,其维度与输出类别的个数相同,即为C。但在Keras中,最终使用的是均值,即:

那如果是batch的情况呢?Keras中的做法是对batch中所有样本的loss求均值:

对应的代码片段可在keras/engine/training_utils/weighted 函数中找到:

交叉熵损失函数公式_keras中两种交叉熵损失函数的探讨_第2张图片

在tensorflow中则只提供原始的BCE(sigmoid_cross_entropy_with_logits)和CE(softmax_cross_entropy_with_logits_v2),这也给开发人员提供了更大的灵活性。

另外,再补充一点,keras使用tensorflow作为backend时,默认情况下CE的实现调用的是自己内部实现的计算方法,而没有像之前想象的那样调用的tensorflow对应的函数(keras/backend/tensorflow_backend.py):

def binary_crossentropy(target, output, from_logits=False):
    """Binary crossentropy between an output tensor and a target tensor.

    # Arguments
        target: A tensor with the same shape as `output`.
        output: A tensor.
        from_logits: Whether `output` is expected to be a logits tensor.
            By default, we consider that `output`
            encodes a probability distribution.

    # Returns
        A tensor.
    """
    # Note: tf.nn.sigmoid_cross_entropy_with_logits
    # expects logits, Keras expects probabilities.
    if not from_logits:
        # transform back to logits
        _epsilon = _to_tensor(epsilon(), output.dtype.base_dtype)
        output = tf.clip_by_value(output, _epsilon, 1 - _epsilon)
        output = tf.log(output / (1 - output))

    return tf.nn.sigmoid_cross_entropy_with_logits(labels=target,
                                                   logits=output)


def categorical_crossentropy(target, output, from_logits=False, axis=-1):
    """Categorical crossentropy between an output tensor and a target tensor.

    # Arguments
        target: A tensor of the same shape as `output`.
        output: A tensor resulting from a softmax
            (unless `from_logits` is True, in which
            case `output` is expected to be the logits).
        from_logits: Boolean, whether `output` is the
            result of a softmax, or is a tensor of logits.
        axis: Int specifying the channels axis. `axis=-1`
            corresponds to data format `channels_last`,
            and `axis=1` corresponds to data format
            `channels_first`.

    # Returns
        Output tensor.

    # Raises
        ValueError: if `axis` is neither -1 nor one of
            the axes of `output`.
    """
    output_dimensions = list(range(len(output.get_shape())))
    if axis != -1 and axis not in output_dimensions:
        raise ValueError(
            '{}{}{}'.format(
                'Unexpected channels axis {}. '.format(axis),
                'Expected to be -1 or one of the axes of `output`, ',
                'which has {} dimensions.'.format(len(output.get_shape()))))
    # Note: tf.nn.softmax_cross_entropy_with_logits
    # expects logits, Keras expects probabilities.
    if not from_logits:
        # scale preds so that the class probas of each sample sum to 1
        output /= tf.reduce_sum(output, axis, True)
        # manual computation of crossentropy
        _epsilon = _to_tensor(epsilon(), output.dtype.base_dtype)
        output = tf.clip_by_value(output, _epsilon, 1. - _epsilon)
        return - tf.reduce_sum(target * tf.log(output), axis)
    else:
        return tf.nn.softmax_cross_entropy_with_logits(labels=target,
                                                       logits=output)

其中BCE对应binary_crossentropy, CE对应categorical_crossentropy,两者都有一个默认参数from_logits,用以区分输入的output是否为logits(即为未通过激活函数的原始输出,这与TF的原生接口一致),但这个参数默认情况下都是false,所以通常情况下我们只需要关心 if not from_logits: 这个分支下的代码块即可。可以看到在binary_crossentropy中,就是简单的对output进行了一个还原,即将通过了激活函数的输出重新还原成logits,然后再调用TF的sigmoid_cross_entropy_with_logits函数。

这个过程的推导如下:

变形,移项得:

两边取对数得:

其中
对应传入参数output,
即为原始的logits。这就对应了函数中的代码:
output = tf.log(output / (1 - output))

而在categorical_crossentropy中,由于难以对softmax的输出进行还原,故重使用了自定义代码计算CE,而没有调用TF接口,其计算方法与上文定义的公式(1)一致。


二、应用场景分析

那么接下来,我们就来回答本文开头提到的问题,着重讨论以下三种场景:二分类、单标签多分类,多标签多分类(multi-label)

2.1 二分类的情况

首先在二分类的场景下,我们只有一个输出节点,其输出值

。那么按照约定俗成的观点,应该使用sigmoid+BCE作为最后的输出层配置。

由于只有一个分类输出,上式中的

可以忽略。那如果使用CE会怎样呢?由于输出类别数C=1,所以CE中的求和可以忽略:

可以看到,在这种情况下,两者都具有相同的部分,BCE仅在样本标签

时多了一个反向的损失函数因子。

交叉熵损失函数公式_keras中两种交叉熵损失函数的探讨_第3张图片

因此,只有对错分的正样本,CE损失函数的值才大于0,此时的网络权值才会得到调整,最终的结果是正样本预测精度会很高,但负样本基本上相当于随机猜测。而对应BCE而言,对错分的正负样本都会产生梯度,网络权值都会进行调整。所以,从直觉上来看,BCE的收敛会更快,学习出的weight也会更合理。

事实上,在keras的train模块中对categorical_crossentropy的使用进行了强制限制,如果输出标签维度为1,只能使用binary_crossentropy,否则程序会报错。为了验证上述推论,我们使用keras自带的imdb二分类例子进行试验,由于输出维度为1的情况下不能直接使用categorical_crossentropy,我们修改例子的代码,通过在自定义loss函数中直接调用backend.categorical_crossentropy函数的方法实验。

运行200个step后,binary_crossentropy已经明显收敛:

20000/25000 [==>......] - ETA: 1:53 - loss: 0.5104 - acc: 0.7282

而categorical_crossentropy却收敛缓慢:

20000/25000 [==>......] - ETA: 1:58 - loss: 5.9557e-08 - acc: 0.5005

可以看到,CE损失函数工作得很差,这与我们的推论相符。所以keras明确禁止了在这种情况下categorical_crossentropy的使用,现在看来也是很合理的。

2.2 单标签多分类的情况

按约定俗成的观点,应该使用softmax+CE的方案,我们同样先把对应的两种损失函数写出来,

同样的,由于只有一个类别的真实标签

,对CE来说,可以将前边的求和符号去掉:

看到这里,大家可能会有个疑问,在CE中我们同样只计算了某个类别标签为1时的loss,而没有计算它为0时的loss,会不会也像二分类的场景那样,导致模型收敛缓慢?但在这里答案是否定的,原因就在于前边的softmax函数具有“排它”性质,某一个输出增大,必然导致其它类别的输出减小,因为其进行了归一化操作,使得每个类别的预测输出概率加和必须为1。但好奇的读者可能又要问了,那使用BCE应该也可以吧?没错!理论上确实是可以,下面我们使用keras自带的mnist多分类例子进行实验:

运行100个step后,binary_crossentropy的结果如下:

100/600 [=>.........................] - ETA: 19:58 - loss: 0.0945 - categorical_accuracy: 0.8137

categorical_crossentropy的结果如下:

100/600 [=>.........................] - ETA: 18:36 - loss: 0.6024 - acc: 0.8107

可以看到,两者并没有太大差距,binary_crossentropy效果反而略好于categorical_crossentropy。注意这里的acc为训练集上的精度,训练步数也仅有100个step,读者如有兴趣,可以深入分析。但这里至少说明了一点,在单标签多分类的情况下BCE同样是适用的。

2.3 多标签多分类

multi-label由于假设每个标签的输出是相互独立的,因此常用配置是sigmoid+BCE, 其中每个类别输出对应一个sigmoid。如果读者仔细看了前面两个小节,相信不用分析,也可以自行得出结果,即这种场景下使用CE将难以收敛,原因跟2.1中的分析类似---我们只计算了某个类别标签为1时的loss及梯度,而忽略了为0时的loss,而每个输出又相互独立,不像softmax函数那样有归一化的限制。所以multi-label是一定不能使用CE作为loss函数的。


三、总结

对于本文开始提到的俗语:“CE用于多分类, BCE适用于二分类”其实大部分都是正确的,唯一有待商榷的部分在于多分类(单标签)其实也可以使用BCE,而对于multi-label的多分类,则不能使用CE。

四、题外话

在运行keras的代码时,发现一个有趣的现象,当使用binary_crossentropy和categorical_crossentropy时,其日志中输出的acc有较大差异,后经多方查阅,发现原因是keras对两者使用了不同的metrics计算acc,对于binary_crossentropy使用的是binary_accuracy,对于后者使用的是categorical_accuracy,读者有兴趣可以参考官方源码

如果不想使用默认值可以在调用compile手动设置metrics参数:

model.compile(optimizer=optimizers.Adam(lr=args.lr),
               loss=['binary_crossentropy'],
               metrics=["categorical_accuracy"]
                  )

五、参考链接

1.

Tensorflow四种交叉熵函数计算公式:tf.nn.cross_entropy - 清舞 点滴 - CSDN博客​blog.csdn.net
交叉熵损失函数公式_keras中两种交叉熵损失函数的探讨_第4张图片

2.

Keras binary_crossentropy vs categorical_crossentropy performance?​stackoverflow.com
交叉熵损失函数公式_keras中两种交叉熵损失函数的探讨_第5张图片

3.

keras-team/keras​github.com
交叉熵损失函数公式_keras中两种交叉熵损失函数的探讨_第6张图片

六、tensorflow代码示例

import tensorflow as tf
import numpy as np

def sigmoid(x):
    return 1.0/(1+np.exp(-x))

y = np.array([[1,0,0],[0,1,0],[0,0,1],[1,1,0],[0,1,0]])
logits = np.array([[12,3,2],[3,10,1],[1,2,5],[4,6.5,1.2],[3,6,1]])
y_pred = sigmoid(logits)

BCE1 = -y*np.log(y_pred)-(1-y)*np.log(1-y_pred)
print(BCE1)
[[6.14419348e-06 3.04858735e+00 2.12692801e+00]
 [3.04858735e+00 4.53988992e-05 1.31326169e+00]
 [1.31326169e+00 2.12692801e+00 6.71534849e-03]
 [1.81499279e-02 1.50231016e-03 1.46328247e+00]
 [3.04858735e+00 2.47568514e-03 1.31326169e+00]]

sess =tf.Session()
y = np.array(y).astype(np.float64)
BCE2 = sess.run(tf.nn.sigmoid_cross_entropy_with_logits(labels=y,logits=logits))

print(BCE2)
[[6.14419348e-06 3.04858735e+00 2.12692801e+00]
 [3.04858735e+00 4.53988992e-05 1.31326169e+00]
 [1.31326169e+00 2.12692801e+00 6.71534849e-03]
 [1.81499279e-02 1.50231016e-03 1.46328247e+00]
 [3.04858735e+00 2.47568514e-03 1.31326169e+00]]

print(y_pred)
[[0.99999386 0.95257413 0.88079708]
 [0.95257413 0.9999546  0.73105858]
 [0.73105858 0.88079708 0.99330715]
 [0.98201379 0.99849882 0.76852478]
 [0.95257413 0.99752738 0.73105858]]

def softmax(x):
    sum_raw = np.sum(np.exp(x),axis=-1)
    x1 = np.ones(np.shape(x))
    for i in range(np.shape(x)[0]):
        x1[i] = np.exp(x[i])/sum_raw[i]
    return x1

y_pred =softmax(logits)
CE1 = -np.sum(y*np.log(y_pred),-1)
print(CE1)
[1.68795487e-04 1.03475622e-03 6.58839038e-02 2.66698414e+00
 5.49852354e-02]
CE2 = sess.run(tf.nn.softmax_cross_entropy_with_logits_v2(labels=y,logits=logits))

print(CE2)
[1.68795487e-04 1.03475622e-03 6.58839038e-02 2.66698414e+00
 5.49852354e-02]

print(y_pred)
[[9.99831219e-01 1.23388975e-04 4.53922671e-05]
 [9.10938878e-04 9.98965779e-01 1.23282171e-04]
 [1.71478255e-02 4.66126226e-02 9.36239552e-01]
 [7.55098575e-02 9.19898383e-01 4.59175917e-03]
 [4.71234165e-02 9.46499123e-01 6.37746092e-03]]
final_BCE = np.mean(BCE1)

print(final_BCE)
1.2554386947664657
final_CE = np.mean(CE1)

print(final_CE)
0.5578113653559053

你可能感兴趣的:(交叉熵损失函数公式)