首先,我们先从公式入手:
CE:
其中, x表示输入样本, C为待分类的类别总数, 这里我们以手写数字识别任务(MNIST-based)为例, 其输入出的类别数为10, 对应的C=10.为第i个类别对应的真实标签,为对应的模型输出值.
BCE:
其中, 即每个类别输出节点都对应一个BCE值.
看到这里, 大家会发现两者的shape并不相同,对于单个样本而言,CE是一个数值,而BCE是一个向量,其维度与输出类别的个数相同,即为C。但在Keras中,最终使用的是均值,即:
那如果是batch的情况呢?Keras中的做法是对batch中所有样本的loss求均值:
对应的代码片段可在keras/engine/training_utils/weighted 函数中找到:
在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)
首先在二分类的场景下,我们只有一个输出节点,其输出值
由于只有一个分类输出,上式中的
可以看到,在这种情况下,两者都具有相同的部分,BCE仅在样本标签
因此,只有对错分的正样本,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的使用,现在看来也是很合理的。
按约定俗成的观点,应该使用softmax+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同样是适用的。
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.net2.
Keras binary_crossentropy vs categorical_crossentropy performance?stackoverflow.com3.
keras-team/kerasgithub.comimport 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