Tensorflow里的交叉熵损失函数及互相转换

2021年4月4日

CategoricalCrossentropy 交叉熵

在数学上,交叉熵(cross entropy)的定义如下

\[
H \left (\vec{y},\vec{\hat{y}} \right )=-\sum_{i=1}^{\left \| y \right \|} y_i \log \hat{y_i}
\]

交叉熵的输入是两个向量。另外,向量每个元素之和必须为1,即[latex]\mathrm{sum}(\vec{y})=1[/latex],[latex]\mathrm{sum}(\vec{\hat{y}})=1[/latex]。

这种最原始的交叉熵在Tensorflow里被称为CategoricalCrossentropy

试计算

\[
\begin{align*}
& H([0,1,0],[0.05,0.95,0])+H([0,0,1],[0.1,0.8,0.1])\\
=& -(0-0.0513) – (-2.3026)\\
= &1.177
\end{align*}
\]

y_true = [[0, 1, 0], 		[0, 0, 1]]
y_pred = [[0.05, 0.95, 0], 	[0.1, 0.8, 0.1]]
print(tf.keras.losses.CategoricalCrossentropy()(y_true, y_pred))
tf.Tensor(1.1769392, shape=(), dtype=float32)

BinaryCrossentropy

当输入向量的长度退化为2,则数学上交叉熵的公式可简化为

\[
H \left (\vec{y},\vec{\hat{y}} \right )=- y_1 \log \hat{y_1} – (1- y_1) \log (1- \hat{y_1})
\]

这种交叉熵在Tensorflow里被称为BinaryCrossentropy。

试计算

\[
\begin{align*}
& H([0,1],[0.05,0.95])+H([0,1],[0.8,0.2])\\
=& -(0-0.0513) – (-1.6094)\\
= &0.8304
\end{align*}
\]

y_true = [[0, 1], 		[0, 1]]
y_pred = [[0.05, 0.95], 	[0.8, 0.2]]
print(tf.keras.losses.CategoricalCrossentropy()(y_true, y_pred))
tf.Tensor(0.8303656, shape=(), dtype=float32)
print(tf.keras.losses.BinaryCrossentropy()(y_true, y_pred))
tf.Tensor(0.8303653, shape=(), dtype=float32)

binary_crossentropy和categorical_crossentropy

binary_crossentropy跟BinaryCrossentropy非常相似,但有以下区别:

  • binary_crossentropy是一个函数,BinaryCrossentropy是一个类。
  • binary_crossentropy不对返回值求平均,BinaryCrossentropy对返回值求平均。
y_true = [[0, 1], 		[0, 1]]
y_pred = [[0.05, 0.95], 	[0.8, 0.2]]
print(tf.keras.losses.BinaryCrossentropy()(y_true, y_pred))tf.Tensor(0.8303653, shape=(), dtype=float32)
print(tf.keras.losses.binary_crossentropy(y_true,y_pred))tf.Tensor([0.05129318 1.6094375 ], shape=(2,), dtype=float32)

在神经网络学习中,我们需要的是loss之和,所以一般用binary_crossentropy。

categorical_crossentropy与CategoricalCrossentropy的区别类似。

如果输入值并不是概率分布?

交叉熵要求输入向量之和为1。[latex]\hat{y}[/latex]预测值由模型算出,有时候和并不为1。如果贸然传入CategoricalCrossentropy,代码并不会报错,但模型无法正确学习。

y_true = [[0, 1]]
y_pred = [[5.0, 7.0]]
print(tf.keras.losses.CategoricalCrossentropy()(y_true, y_pred))
tf.Tensor(0.5389965, shape=(), dtype=float32)

这就要求开发人员准确理解模型和数值范围。解决方法有以下三种。

设置from_logits=True

print(tf.keras.losses.CategoricalCrossentropy(from_logits=True)(y_true, y_pred))
tf.Tensor(0.12692805, shape=(), dtype=float32)

from_logits=True的作用是在CategoricalCrossentropy函数内部对y_pred计算softmax或sigmoid(见#如果预测值为长度为1的向量?)。

手动添加softmax计算

y_true = [[0, 1]]
y_pred = [[5.0, 7.0]]
print(tf.keras.losses.CategoricalCrossentropy()(y_true, tf.nn.softmax(y_pred)))
tf.Tensor(0.126928, shape=(), dtype=float32)

由softmax的定义可知,对任意长度为1的向量求softmax,结果总为1。如果碰到这个情况,参见#如果预测值为长度为1的向量?

在模型最后层添加activation='softmax'

model = tf.keras.Sequential()
model.add(tf.keras.layers.Flatten())
...
model.add(tf.keras.layers.Dense(..., activation='softmax'))

model.compile(optimizer='adam', loss='categorical_crossentropy', ...)

总是进行softmax?

即使模型前面已经计算为概率分布(softmax)了,那么能否始终设置from_logits=True?

softmax的二次方并不是幂等的。

该图画出了softmax([1,2,3])迭代10次的结果,发现结果稳定在[0.33,0.33,0.33];对[1.1,1.2,1.3]同样计算10次softmax,结果也是稳定在[0.33,0.33,0.33]。这说明,如果对模型少做或多做softmax,都会令损失值失准。

如果预测值为长度为1的向量?

如果模型的输出为长度为1的向量,即最后一层为Dense(1),则无法使用softmax。解决方法有以下三种。

  • 在最后一层设置Dense(1,activation='sigmoid')
  • 在loss函数设置from_logits=True
  • 改用tf.nn.sigmoid_cross_entropy_with_logits。
y_true = [[0.], [1.]]
y_pred = [[0.2], [0.7]]
print(tf.keras.losses.binary_crossentropy(y_true, y_pred, from_logits=True))
tf.Tensor([0.79813886 0.40318602], shape=(2,), dtype=float32)
print(tf.nn.sigmoid_cross_entropy_with_logits(y_true, y_pred))
tf.Tensor([[0.79813886] [0.40318602]], shape=(2, 1), dtype=float32)

SparseCategoricalCrossentropy

这里sparse的意思与Tensorflow word embeddings教程里不同,word embeddings教程说“A one-hot encoded vector is sparse”,但是如果真实标签采用了sparse的one-hot encoding却不能使用SparseCategoricalCrossentropy,而要用categorical cross enptropy。

事实上SparseCategoricalCrossentropy的文档说了,仅当y_true采用整数编码,y_pred采用类概率分布时,才能用SparseCategoricalCrossentropy。

y_true = [1, 2]
y_pred = [[0.05, 0.95, 0], [0.1, 0.8, 0.1]]
print(tf.keras.losses.SparseCategoricalCrossentropy()(y_true, y_pred).numpy())
1.1769392
y_true = [[0, 1, 0], [0, 0, 1]]
print(tf.keras.losses.CategoricalCrossentropy()(y_true, y_pred).numpy())
1.1769392

最佳实践

如果模型输出类似概率分布,就不要在最后一层设置激活函数,而是在loss function里设置from_logits=True,因为这个开关可以自动判断模型输出的长度为1还是2,并且可以减少一个神经元。