Keras 中的自定义损失函数应该返回批次的单个损失值还是训练批次中每个样本的一系列损失?

Posted

技术标签:

【中文标题】Keras 中的自定义损失函数应该返回批次的单个损失值还是训练批次中每个样本的一系列损失?【英文标题】:Should the custom loss function in Keras return a single loss value for the batch or an arrary of losses for every sample in the training batch? 【发布时间】:2020-12-03 00:58:25 【问题描述】:

我正在学习 tensorflow(2.3) 中的 keras API。在 tensorflow 网站上的这个 guide 中,我找到了一个自定义损失函数的示例:

    def custom_mean_squared_error(y_true, y_pred):
        return tf.math.reduce_mean(tf.square(y_true - y_pred))

此自定义损失函数中的reduce_mean 函数将返回一个标量。

这样定义损失函数是否正确?据我所知,y_truey_pred 形状的第一个维度是批量大小。我认为损失函数应该为批次中的每个样本返回损失值。所以损失函数应该给出一个形状为(batch_size,)的数组。但是上面的函数为整个批次给出了一个单一的值。

也许上面的例子是错误的?谁能帮我解决这个问题?


附言为什么我认为损失函数应该返回一个数组而不是单个值?

我阅读了Model类的源代码。当您向Model.compile() 方法提供损失函数(请注意它是函数,而不是损失)时,该损失函数用于构造LossesContainer 对象,存储在Model.compiled_loss。这个传递给LossesContainer类的构造函数的损失函数再次用于构造一个LossFunctionWrapper对象,该对象存储在LossesContainer._losses中。

根据LossFunctionWrapper类的源码,通过LossFunctionWrapper.__call__()方法(继承自Loss类)计算一个训练batch的整体损失值,即返回单个损失值整个批次。 但是LossFunctionWrapper.__call__() 首先调用LossFunctionWrapper.call() 方法来获取训练批次中每个样本的损失数组。然后这些损失最终被平均以获得整个批次的单个损失值。在LossFunctionWrapper.call() 方法中调用了提供给Model.compile() 方法的损失函数。

这就是为什么我认为自定义损失函数应该返回一系列损失,而不是单个标量值。此外,如果我们为Model.compile() 方法编写一个自定义Loss 类,我们自定义Loss 类的call() 方法也应该返回一个数组,而不是一个信号值。


我在 github 上开了一个issue。已确认需要自定义损失函数才能为每个样本返回一个损失值。该示例需要更新以反映这一点。

【问题讨论】:

【参考方案1】:

tf.math.reduce_mean 获取批次的平均值并将其返回。这就是为什么它是一个标量。

【讨论】:

我知道它是一个标量。但我认为损失函数应该为批次中的每个样本返回一系列损失,而不是整个批次的标量。 这就是我写的为什么它返回一个标量,因为正在取平均值。它应该只返回一个标量,因为对于反向传播,您需要一个值而不是数组。 但是根据source code,损失函数实际上应该为批次中的每个样本返回一个损失数组。例如,源代码中的mean_squared_error 函数将返回一个数组,而不是一个标量。 LossFunctionWrappercall() 方法也返回每个样本的损失值。 Loss 对象的 __call__() 方法将使用 call() 方法或损失函数来获取每个样本的损失值,然后对这些损失进行平均以获得整个批次的损失。 reduce_sum 在这里被使用。最初的 cmets 表明了这一点。 那么,在给定y_truey_pred 的情况下,LOSS 函数应该返回什么?【参考方案2】:

Tensorflow网站上给出的损失函数是绝对正确的。

def custom_mean_squared_error(y_true, y_pred):
    return tf.math.reduce_mean(tf.square(y_true - y_pred))

在机器学习中,我们使用的loss是单个训练样例的loss之和,所以它应该是一个标量值。 (因为对于所有示例,我们使用的是单个网络,因此我们需要单个损失值来更新参数。)

关于制作容器以防损失:

当使用并行计算时,制作容器是一种更简单且可行的方法来跟踪计算的损失指数,因为我们使用批量训练而不是整个训练集。

【讨论】:

而在这个post中,作者还说“损失函数应该总是返回一个长度为batch_size的向量,因为你必须为每个数据点返回一个损失”。 在losses模块的源代码中,MeanAbsoluteError类使用mean_squared_error函数构造了一个LossFunctionWrapper类。可以检查mean_squared_error函数返回的K.mean(math_ops.squared_difference(y_pred, y_true), axis=-1)是一个数组,而不是单个值。 我知道在训练模型时,我们需要整个批次的单个损失值。但是根据源码,我们自定义的损失函数并不负责获取那个单一的损失值。计算所有单个训练样本的平均损失的是LossFunctionWrapper.__call__() 方法。 LossFunctionWrapper.__call__() 方法调用LossFunctionWrapper.call() 方法来获取单个样本的损失。在LossFunctionWrapper.call() 方法中调用了我们的自定义损失函数。你读过我上面提到的源代码吗?【参考方案3】:

我在 github 上开了一个issue。已确认需要自定义损失函数才能为每个样本返回一个损失值。该示例需要更新以反映这一点。

【讨论】:

我不认为 TF 开发人员就在那里。损失函数没有明确或合乎逻辑的要求来返回每个样本的损失(尽管这是一件非常合理的事情)。正如文档也证实了这一点,损失函数也可以返回一个标量值,并且模型将被毫无问题地训练。 这是因为标量传递给compute_weighted_loss函数。它不会引起问题。但是计算训练批次损失值的方法是错误的。【参考方案4】:

实际上,据我所知,损失函数的返回值的形状并不重要,即它可以是标量张量,也可以是每个样本一个或多个值的张量。重要的是它应该如何减少到一个标量值,以便它可以用于优化过程或显示给用户。为此,您可以检查 Reduction documentation 中的缩减类型。

此外,以下是 compile 方法 documentation 关于 loss 参数的说明,部分解决了这一点:

loss:字符串(目标函数的名称)、目标函数或tf.keras.losses.Loss 实例。见tf.keras.losses。目标函数是任何带有签名loss = fn(y_true,y_pred) 的可调用函数,其中y_true = 形状=[batch_size, d0, .. dN] 的地面真值,除了稀疏损失函数,例如形状=[batch_size, d0, .. dN-1] 的稀疏分类交叉熵。 y_pred = 形状预测值 = [batch_size, d0, .. dN]。它返回一个加权损失浮点张量。如果使用自定义 Loss 实例并将缩减设置为 NONE,则返回值的形状为 [batch_size, d0, .. dN-1] 即。每个样本或每个时间步的损失值;否则,它是一个标量。如果模型有多个输出,您可以通过传递字典或损失列表对每个输出使用不同的损失。模型将最小化的损失值将是所有单个损失的总和。

此外,值得注意的是,TF/Keras 中的大多数内置损失函数通常在最后一个维度(即axis=-1)上进行缩减。


对于那些怀疑返回标量值的自定义损失函数是否有效的人:您可以运行以下 sn-p,您将看到模型将正确训练和收敛。

import tensorflow as tf
import numpy as np

def custom_loss(y_true, y_pred):
    return tf.reduce_sum(tf.square(y_true - y_pred))

inp = tf.keras.layers.Input(shape=(3,))
out = tf.keras.layers.Dense(3)(inp)

model = tf.keras.Model(inp, out)
model.compile(loss=custom_loss, optimizer=tf.keras.optimizers.Adam(lr=0.1))

x = np.random.rand(1000, 3)
y = x * 10 + 2.5
model.fit(x, y, epochs=20)

【讨论】:

是的,你是对的。 Loss.__call__() 方法调用 compute_weighted_loss 函数将每个示例的损失减少为训练批次的标量损失。除非我们定义Loss 的子类并重写__call__() 方法,否则我们无法更改此行为。但是当我们提供自定义损失函数时,它应该返回一个损失数组compute_weighted_loss 来计算平均值。 对于内置的损失函数,如果y_truey_pred的形状是(batch_size, output_dimension),那么这些损失函数只是返回一个形状为(batch_size,)的张量,即一个每个样本的损失。如果y_truey_pred有两个以上的维度,那么输出中可能会有时间步长,就像RNN/LSTM层一样。 这不正确。这与子类化Loss 或定义自定义损失函数无关。您可以自己尝试:实现一个虚拟模型并定义一个自定义损失函数,该函数返回一个标量值作为损失;您会看到模型会正确训练和收敛。 @Gödel 我刚刚添加了一个模型的最小示例,该模型在我的答案末尾使用带有标量返回值的损失函数。您可以自己尝试一下,看看它是否正确训练和收敛。 我知道即使您的自定义风暴损失函数返回标量,您也可以训练模型。这只是意味着代码不检查损失函数的返回值的形状。但从逻辑上讲,训练批次的损失值应该是批次中每个样本损失的平均值。【参考方案5】:

由于有多个通道,可以增加维度……但是,每个通道应该只有一个损失的标量值。

【讨论】:

【参考方案6】:

我认为@Gödel 发布的问题完全合法且正确。自定义损失函数应返回每个样本的损失值。而且,@today 提供的解释也是正确的。最后,这一切都取决于所使用的reduction类型。

因此,如果使用类 API 创建损失函数,那么自定义类中会自动继承缩减参数。使用它的默认值“sum_over_batch_size”(这只是给定批次中所有损失值的平均值)。其他选项是“sum”,它计算总和而不是平均,最后一个选项是“none”,其中返回一组损失值。

Keras 文档中还提到,当使用 model.fit() 时,这些缩减的差异是无关紧要的,因为缩减是由 TF/Keras 自动处理的。

最后,还提到当创建自定义损失函数时,应该返回一组损失(单个样本损失)。它们的减少由框架处理。

链接:

https://keras.io/api/losses/ Checkout CategoricalCrossentropy 类: https://keras.io/api/losses/probabilistic_losses/#categoricalcrossentropy-class

【讨论】:

以上是关于Keras 中的自定义损失函数应该返回批次的单个损失值还是训练批次中每个样本的一系列损失?的主要内容,如果未能解决你的问题,请参考以下文章

Keras:为啥损失函数必须为每个批次项目返回一个标量,而不仅仅是一个标量?

Keras 中带有附加变量输入的自定义损失/目标函数

Keras 中的自定义损失函数(IoU 损失函数)和梯度误差?

Keras 在自定义损失函数中访问单个值

Keras 中的自定义损失函数 - 遍历 TensorFlow

R Keras 中的自定义损失函数