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

27

我正在学习tensorflow(2.3)中的keras API。在tensorflow网站的指南中,我发现了一个自定义损失函数的例子:

    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,)的数组。 但是上面的函数为整个批次给出单个值。

也许以上示例是错误的?有人能帮助我解决这个问题吗?


p.s. 为什么我认为损失函数应该返回一个数组而不是一个单一的值?

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

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

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


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

7个回答

10
实际上,据我所知,损失函数的返回值形状并不重要,即它可以是标量张量或每个样本一个或多个值的张量。重要的是如何将其缩减为标量值,以便在优化过程中使用或向用户显示。为此,您可以查看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)。

对于那些怀疑自定义损失函数可以返回标量值的人:您可以运行以下代码段,您会发现模型可以正常训练和收敛。

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)

1
是的,你说得对。Loss.__call__() 方法调用 compute_weighted_loss 函数来将每个示例的损失减少为训练批次的标量损失。除非我们定义 Loss 的子类并重写 __call__() 方法,否则我们无法更改此行为。但是当我们提供自定义损失函数时,它应该返回一个损失数组,以便 compute_weighted_loss 可以计算平均值。 - Gödel
那不正确。这与子类化“Loss”或定义自定义损失函数无关。你可以自己尝试:实现一个虚拟模型并定义一个自定义损失函数,该函数返回标量值作为损失;你会发现模型将能够训练和收敛。 - today
1
@Gödel 我刚刚在我的回答中添加了一个使用标量返回值的损失函数的模型的最小示例。你可以自己尝试一下,看看它是否能够正确地训练和收敛。 - today
此外,如果您想计算每个样本损失的加权平均值作为训练批次的损失值怎么办?您无法将权重提供给自定义损失函数。您可以检查“sample_weight”最终在Loss.__call __()方法中使用,而不是在自定义损失函数中使用。 - Gödel
没错。目前你可以利用Loss类的这种行为(即它不检查自定义损失函数返回值的形状)。但是在将来,如果Loss.__call__()方法进行了检查,这可能会导致问题。但现在,让我们就按照这种方式定义我们的自定义损失函数吧~ - Gödel
显示剩余3条评论

8

我在Github上开了一个问题。确认需要自定义损失函数以每个样本返回一个损失值。需要更新示例以反映这一点。


我认为 TF 的开发人员不正确。没有明确或逻辑上对损失函数返回每个样本的损失值的要求(尽管这是一个非常合理的做法)。正如文档也确认的那样,损失函数也可以返回标量值,模型将能够顺利训练。 - today
这是因为标量被传递到了 compute_weighted_loss 函数中。这并不会引起问题。但是计算训练批次的损失值的方法是错误的。 - Gödel

6
我认为@Gödel发布的问题是完全合理和正确的。自定义损失函数应该针对每个样本返回一个损失值。同时,@today提供的解释也是正确的。最终,一切都取决于所使用的减少(reduction)类型。
因此,如果使用类API创建损失函数,则自定义类会自动继承减少参数。它的默认值为"sum_over_batch_size"(即在给定批次中所有损失值的平均值)。其他选项是"sum",它计算总和而不是平均值,最后一个选项是"none",它将返回一组损失值。
Keras文档还指出,当使用model.fit()时,这些减少(reduction)的差异是无关紧要的,因为减少(reduction)由TF/Keras自动处理。
最后,还提到当创建自定义损失函数时,应返回一组损失值(单个样本损失值)。它们的减少(reduction)由框架处理。
链接:

3

tf.math.reduce_mean函数对批次进行平均并返回标量。这就是为什么它是一个标量的原因。


这就是我写的代码,为什么它返回一个标量,因为正在进行平均值计算。而且它应该只返回一个标量,因为反向传播需要单个值而不是数组。 - Abhishek Verma
但是根据源代码显示,损失函数实际上应该返回一个批次中每个样本的损失数组。例如,源代码中的mean_squared_error函数将返回一个数组,而不是标量。LossFunctionWrappercall()方法也为每个样本返回损失值。Loss对象的__call__()方法将使用call()方法或损失函数获取每个样本的损失值,然后平均这些损失以获得整个批次的损失。 - Gödel
需要计算平均损失。 - Abhishek Verma
当您向Model.compile()方法提供损失函数时,此损失函数将转换为Loss对象。 Loss.__call __()方法使用Loss.call()方法来获取每个样本的损失数组,然后获取批次的平均损失。问题是,Loss.call()方法使用损失函数,因此我认为您提供给Model.compile()方法的损失函数应该返回每个样本的损失数组,而不是平均损失。 - Gödel
看一下批处理的反向传播算法,你一定会知道算法需要什么。 - Abhishek Verma
显示剩余5条评论

3

Tensorflow网站上提供的损失函数是完全正确的。

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

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

关于创建损失容器:

在使用并行计算时,创建容器是一种更简单可行的方法,可以跟踪计算批次而不是整个训练集的损失索引。


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

1
TensorFlow文档中遗漏了这一点,但Keras文档中明确说明并进行了澄清。它说:
注意,这是tf.keras.losses.mean_squared_error等损失函数和tf.keras.losses.MeanSquaredError默认损失类实例之间的重要区别:函数版本不执行减少操作,但默认情况下类实例会执行。
它还指出:
默认情况下,损失函数每个输入样本返回一个标量损失值。

0

由于存在多个通道,可以增加维数...然而,每个通道只能具有标量损失值。


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