为什么对于相同问题,二元交叉熵和分类交叉熵会表现出不同的性能?

215

我正在尝试使用卷积神经网络按主题对文本进行分类。当我使用二元交叉熵时,准确率约为80%;而使用分类交叉熵时,准确率约为50%。

我不明白这是为什么。这是一个多类问题,难道我不得不使用分类交叉熵,并且使用二元交叉熵的结果是没有意义的吗?

model.add(embedding_layer)
model.add(Dropout(0.25))
# convolution layers
model.add(Conv1D(nb_filter=32,
                    filter_length=4,
                    border_mode='valid',
                    activation='relu'))
model.add(MaxPooling1D(pool_length=2))
# dense layers
model.add(Flatten())
model.add(Dense(256))
model.add(Dropout(0.25))
model.add(Activation('relu'))
# output layer
model.add(Dense(len(class_id_index)))
model.add(Activation('softmax'))

然后我使用 categorical_crossentropy 作为损失函数编译它,就像这样:

model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

或者
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])

直观地讲,我可以理解为什么要使用分类交叉熵,但我不明白为什么使用二元交叉熵会得到好的结果,而使用分类交叉熵会得到较差的结果。


24
如果这是多分类问题,你需要使用categorical_crossentropy。同时,标签需要转换为分类格式。请参考to_categorical进行转换。另外,请查看categorical和binary交叉熵的定义 - Autonomous
2
我认为他只是将其与向量中的第一个数字进行比较,而忽略了其余部分。 - Thomas Pinetz
我观察到了一个类似的情况,如果我使用二元交叉熵,我会得到更好的结果(在损失方面也是如此),非常有趣。 - fermat4214
@ParagS.Chandakkar。对于二元分类,表示将为0、1;对于分类分类,表示将为[[0, 0],[0, 1]]。这也高度取决于您如何设计最终的softmax层。Dense(1, activation='softmax')应允许0,1。 Dense(2, activation='softmax')需要[[0,0],[0,1]]。 - Nilav Baran Ghosh
7
表示两个分类的分类问题,其表示将是[[1,0],[0,1]],而不是您提到的[[0,0],[0,1]]。对于二元分类,使用Dense(1, activation='softmax') 是错误的。请记住,softmax 输出是一个概率分布,其总和为一。如果您想要仅具有一个输出神经元并进行二元分类,请使用sigmoid和二元交叉熵。 - Autonomous
显示剩余5条评论
12个回答

265
这种表现差异的原因在于分类和二元交叉熵之间,正如用户xtof54在他在下面的回答中所报道的那样:
“使用二元交叉熵且标签多于两个时,使用Keras方法evaluate计算的准确度是错误的。”
我想进一步阐述、展示实际的根本问题、解释并提供解决方案。
这种行为不是一个bug。其背后的原因是Keras实际上如何“猜测”要使用哪种准确性度量,这取决于您选择的损失函数,当您在模型编译中包含简单的metrics=['accuracy']时。换句话说,虽然您的第一个编译选项是指定了正确的准确性度量,但是当您更改损失函数时,Keras会自动更改准确性度量,而这可能导致评估结果的不准确。
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

是有效的,你的第二个。

model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])

不会产生您期望的结果,但原因并非使用二元交叉熵(至少在原理上是绝对有效的损失函数)。

为什么呢?如果您查看度量源代码,Keras 不定义单个准确性指标,而是定义了几个不同的指标,其中包括binary_accuracycategorical_accuracy在幕后发生的事情是,由于您选择了二元交叉熵作为损失函数,并且没有指定特定的准确性指标,Keras(错误地……)推断出您对binary_accuracy感兴趣,并返回这个值——而实际上您对categorical_accuracy感兴趣。

让我们使用Keras中的MNIST CNN示例进行验证,以下是修改内容:

model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])  # WRONG way

model.fit(x_train, y_train,
          batch_size=batch_size,
          epochs=2,  # only 2 epochs, for demonstration purposes
          verbose=1,
          validation_data=(x_test, y_test))

# Keras reported accuracy:
score = model.evaluate(x_test, y_test, verbose=0) 
score[1]
# 0.9975801164627075

# Actual accuracy calculated manually:
import numpy as np
y_pred = model.predict(x_test)
acc = sum([np.argmax(y_test[i])==np.argmax(y_pred[i]) for i in range(10000)])/10000
acc
# 0.98780000000000001

score[1]==acc
# False    

为了解决这个问题,即在使用二元交叉熵作为损失函数的情况下(至少在原则上没有问题),仍然能够获得所需的分类准确性,您应该在模型编译中明确要求categorical_accuracy,如下所示:
from keras.metrics import categorical_accuracy
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=[categorical_accuracy])

在MNIST示例中,经过训练、评分和预测测试集(如上所示),现在这两个度量标准是相同的,正如它们应该是的:
# Keras reported accuracy:
score = model.evaluate(x_test, y_test, verbose=0) 
score[1]
# 0.98580000000000001

# Actual accuracy calculated manually:
y_pred = model.predict(x_test)
acc = sum([np.argmax(y_test[i])==np.argmax(y_pred[i]) for i in range(10000)])/10000
acc
# 0.98580000000000001

score[1]==acc
# True    

系统设置:
Python version 3.5.3
Tensorflow version 1.2.1
Keras version 2.0.4

更新:在我发帖后,我发现这个问题已经在这个答案中被识别出来了。


86

这完全取决于您要处理的分类问题的类型。有三个主要类别:

  • 二元分类(两个目标类),
  • 多类分类(超过两个排他性目标),
  • 多标签分类(超过两个非排他性目标),其中多个目标类可以同时处于激活状态。

在第一种情况下,应该使用二元交叉熵,并将目标编码为one-hot向量。

在第二种情况下,应该使用分类交叉熵,并将目标编码为one-hot向量。

在最后一种情况下,应该使用二元交叉熵,并将目标编码为one-hot向量。每个输出神经元(或单元)被视为单独的二元随机变量,整个输出向量的损失是单个二元变量损失的乘积。因此,每个单独输出单元的二元交叉熵是乘积。

二元交叉熵定义如下:

enter image description here

分类交叉熵定义如下:

enter image description here

其中c是跑遍类别数C的索引。


1
你确定二进制和分类交叉熵的定义与这个答案中的公式一致吗? - nbro
我选择这条路,因为我觉得这些问题没有足够的细节来解决其中的三个问题。是的,你说得对,我忘了提到所有这些情况的激活函数,但正如你指出的那样,在二进制情况下,应该使用sigmoid函数。但这是因为两个输出单元上的softmax函数在数学上等同于sigmoid的输出。 - Whynote
我在二元分类中使用分类交叉熵有什么需要注意的吗?毕竟,二元分类只是多类分类的特殊情况,所以这应该也可以工作,对吧? - bers
是的,你说得对。二元交叉熵的公式实际上是分类交叉熵的一种特殊情况,因为如果只有两个类别,而且概率总和为1,那么一个类别的概率恰好等于另一个类别的概率的补数。当你有多个输出神经元时,就会使用分类交叉熵,所以(1-y)被“嵌入”到其他神经元的y(x)中。但在二元分类中,人们通常只使用一个输出神经元,在这种情况下,你需要在损失函数中明确地包含(1-y)项。这样讲清楚了吗?你可以找到更详细的文章来了解这个问题。 - Whynote
讲解得很清楚。然而,在多标签分类的情况下,最终损失是每个单一二进制CE损失的总和(或平均值)...而不是它们的乘积。 - Allohvk
显示剩余5条评论

45

我遇到了一个“倒置”的问题——使用具有2个类别的 categorical_crossentropy 能够得到好的结果,但使用 binary_crossentropy 得到的效果却很差。看起来问题出在错误的激活函数上。正确的设置如下:

  • 对于 binary_crossentropy:使用 sigmoid 激活函数,标量目标
  • 对于 categorical_crossentropy:使用 softmax 激活函数,独热编码目标

5
你确定使用二元交叉熵的标量目标吗?看起来你应该使用“多热”编码的目标(例如[0 1 0 0 1 1])。 - Dmitry
6
好的。请查看https://keras.io/losses/#usage-of-loss-functions,它说: “当使用categorical_crossentropy损失时,您的目标应该采用分类格式(例如,如果您有10个类别,则每个样本的目标应该是一个10维向量,除了在相应于样本类别的索引处为1之外,其他位置都是零)。” - Alex Svetkin
1
但我们正在谈论二进制交叉熵 - 而不是分类交叉熵。 - Dmitry
@Whynote 不一定。请参见https://dev59.com/e1YO5IYBdhLWcg3wIuTp#47238223。 - nbro
@nbro 对不起,我指的是二进制(01),而不是在这个答案中提到的任何标量。 - Whynote
显示剩余2条评论

31

这是一个非常有趣的案例。实际上在您的设置中,以下声明是正确的:

binary_crossentropy = len(class_id_index) * categorical_crossentropy

这意味着你的损失相当于一个常数乘法因子,损失等价。在训练过程中观察到的奇怪行为可能是以下现象的示例:

  1. 在开始阶段,最频繁的类别占据了损失 - 所以网络学习为每个示例预测大部分该类别。
  2. 在学习了最频繁的模式后,它开始区分不太频繁的类别。但是当使用adam时,学习率比训练开始时小得多(这是这个优化器的性质)。它使得训练变慢,并且防止您的网络离开不良局部最小值的可能性更小。

这就是为什么这个常数因子可能有助于binary_crossentropy的情况。经过多个epoch之后,学习率的值比categorical_crossentropy情况更大。当我注意到这种行为时,我通常会重新启动训练(和学习阶段),或者使用以下模式调整类权重:

class_weight = 1 / class_frequency

这使得来自不太频繁的类别的损失在训练初期和优化过程的后半部分平衡了主导类别的损失影响。

编辑:

实际上 - 我检查了一下,即使在数学情况下:

binary_crossentropy = len(class_id_index) * categorical_crossentropy

在某些情况下,应该保持输出结果的总和为1,但在keras中并不是这样,因为keras会自动将所有输出规范化为总和等于1。这就是多类分类训练中出现异常行为的实际原因。


2
这是一个非常合理的解释。但我不确定这是否真的是主要原因。因为我还观察到在几个我的学生工作中,当应用二进制-X-ent而不是cat-X-ent(这是一个错误)时出现了这种奇怪的行为。即使只训练2个epochs也是如此!使用类权重和反向类先验并没有帮助。也许严格调整学习率会有所帮助,但默认值似乎更有利于bin-X-ent。我认为这个问题值得更多的研究... - xtof54
2
等一下,不好意思,我没看懂你的更新:softmax 总是使输出总和为1,所以我们不需要关心这个?只要每个样本只有一个正确的黄金类别,那么这会如何影响训练呢? - xtof54

24

在评论了@Marcin的答案之后,我更加仔细地检查了我的一个学生代码,发现即使在只有两个周期之后,也存在相同奇怪的行为!(所以,@Marcin的解释在我的情况下不太可能)。

我发现答案其实非常简单:当使用binary_crossentropy超过2个标签时,使用Keras方法evaluate计算出来的准确率是完全错误的。您可以通过重新计算准确率(首先调用Keras方法“predict”,然后计算predict返回的正确答案数)来进行检查:您会得到真正的准确率,这比Keras“evaluate”得到的要低得多。


2
我在第一次迭代时也看到了类似的行为。 - dolbi

11

一个简单的例子,涉及多类别设置,以说明。

假设您有4个分类(onehot编码),以下是只有一个预测结果:

true_label = [0,1,0,0] predicted_label = [0,0,1,0]

当使用categorical_crossentropy时,精度为0,它只关心您是否正确获取相关的类别。

然而,当使用binary_crossentropy时,会为所有类别计算准确率,对于这个预测,准确率将为50%。最终结果将是两种情况下各自准确率的平均值。

建议在多类别(类别相互排斥)问题中使用categorical_crossentropy,但在多标签问题中使用binary_crossentropy。


9
由于这是一个多类问题,您需要使用categorical_crossentropy,而binary cross entropy会产生虚假结果,很可能只评估前两个类别。
对于多类问题,50%的准确率可能是相当不错的,具体取决于类别数量。如果您有n个分类,则通过输出随机分类,您可以获得100/n的最低性能。

4
desernaut 的精妙侦探工作已经令人满意地回答了主要问题。然而,在某些情况下,二元交叉熵(BCE)可能会产生不同于分类交叉熵(CCE)的结果,并且可能是首选。虽然上述选择损失的经验法则对于99%的情况都适用,但我想为这个讨论增加一些新的维度。
原帖使用 softmax 激活函数,这将生成一个概率分布作为预测值。这是一个多类问题。首选的损失函数是分类交叉熵。本质上,这归结为 -ln(p) ,其中'p'是样本中唯一正类的预测概率。这意味着在计算 CE 时,负面预测没有发挥作用。这是有意而为之的。
在极少数情况下,可能需要考虑负面预测。这可以通过将上述示例视为一系列二进制预测来完成。因此,如果期望值为[1 0 0 0 0],而预测值为[0.1 0.5 0.1 0.1 0.2],则可以进一步分解为:
expected = [1,0], [0,1], [0,1], [0,1], [0,1]
predicted = [0.1, 0.9], [.5, .5], [.1, .9], [.1, .9], [.2, .8]

现在我们要计算5个不同的交叉熵 - 分别为上述5个期望/预测组合,然后将它们相加。接下来:
CE = -[ ln(.1) + ln(0.5) + ln(0.9) + ln(0.9) + ln(0.8)]

CE指标的量表不同,但仍然是衡量预期值与预测值之间差异的一种度量。唯一的区别在于,在这种方案中,负值与正值一起受到惩罚/奖励。如果您的问题是使用输出概率(包括+和-)而不是使用max()来预测仅为1的正标签,则可以考虑使用此版本的CE。
那么,对于期望值为[1 0 0 0 1]的多标签情况呢?传统方法是针对每个输出神经元使用一个Sigmoid函数,而不是整体使用Softmax函数。这样可以确保输出概率彼此独立。因此我们得到如下结果:
expected = [1 0 0 0 1]
predicted is = [0.1 0.5 0.1 0.1 0.9]

根据定义,CE用于测量两个概率分布之间的差异。但上述两个列表不是概率分布。概率分布应该总是加起来等于1。因此,传统的解决方案是使用与之前相同的损失方法 - 将期望和预测值分解为5个单独的概率分布,继续计算5个交叉熵并将它们相加。然后:

CE = -[ ln(.1) + ln(0.5) + ln(0.9) + ln(0.9) + ln(0.9)] = 3.3

挑战在于类别数量可能非常多,例如1000个,但每个样本中只有几个类别存在。因此,预期结果应该是:[1,0,0,0,0,0,1,0,0,0.....990个零]。而预测结果可能是:[.8, .1, .1, .1, .1, .1, .8, .1, .1, .1.....990个0.1]。
在这种情况下,CE =
- [ ln(.8) + ln(.8) for the 2 +ve classes and 998 * ln(0.9) for the 998 -ve classes]

= 0.44 (for the +ve classes) +  105 (for the negative classes)

您可以看到,当计算损失时,负类开始产生麻烦。正样本(可能是我们关心的全部内容)的声音被淹没了。怎么办?我们不能使用分类CE(只考虑正样本的版本来进行计算),因为我们被迫将概率分布分成多个二进制概率分布,否则它就不会是一个概率分布。一旦我们把它分成多个二进制概率分布,我们别无选择,只能使用二进制CE,这当然会给负类带来权重。
一个选择是通过乘以一个倍数来淹没负类的声音。因此,我们将所有负损失乘以一个值 gamma,其中 gamma < 1。例如,在上述情况下,gamma 可以是 0.0001。现在损失变为:
= 0.44 (for the +ve classes) +  0.105 (for the negative classes)

烦扰值已经降低了。两年前,Facebook在一篇论文中做了这件事,并且还把负损失乘以p的x次方(其中“p”是输出为正数的概率,“x”是一个>1的常数)。这进一步惩罚了负损失,尤其是模型非常自信(其中1-p接近1)的情况下。这种将惩罚负分类损失与更严厉地惩罚易分类的案例相结合的效果非常好,这些案例占大多数负面案例,Facebook称之为焦点损失。
因此,在回答OP关于二元CE在他的情况下是否有意义的问题时,答案是-这取决于情况。在99%的情况下,传统的经验法则都有效,但也可能有一些情况需要弯曲或甚至是打破规则以适应手头的问题。
要获得更深入的介绍,可以参考:https://towardsdatascience.com/cross-entropy-classification-losses-no-math-few-stories-lots-of-intuition-d56f8c7f06b0

4

在使用 categorical_crossentropy 损失函数时,您正在传递一个形状为 (x 维度,y 维度) 的目标数组。然而,categorical_crossentropy 期望的目标是形状为 (样本数,类别数) 的二元矩阵(由 1 和 0 组成)。如果您的目标是整数类别,则可以通过以下方式将其转换为期望的格式:

from keras.utils import to_categorical
y_binary = to_categorical(y_int)

或者,您可以使用损失函数sparse_categorical_crossentropy,这个函数期望整数目标值。

model.compile(loss='sparse_categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

3

看一下这个方程,您会发现二元交叉熵不仅惩罚label=1且predicted=0的情况,还惩罚label=0且predicted=1的情况。

然而,分类交叉熵只惩罚那些label=1但predicted=1的情况。这就是为什么我们假设只有一个标签为正例的原因。


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