在Keras中如何计算多个类别的总损失?

假设我有一个具有以下参数的网络:

  1. 用于语义分割的全卷积网络
  2. 损失函数 = 加权二元交叉熵(但可以是任何损失函数,这不重要)
  3. 5个类别 – 输入是图像,真实标签是二元掩码
  4. 批次大小 = 16

我知道损失是按以下方式计算的:二元交叉熵被应用于图像中每个像素的每个类别。因此,本质上,每个像素将有5个损失值

在这一步之后会发生什么?

当我训练我的网络时,它在每个epoch只打印一个损失值。需要进行许多层次的损失累积才能产生一个单一的值,而这种过程在文档/代码中完全不清楚。

  1. 首先合并什么 – (1)类的损失值(例如,每个像素合并5个值(每个类别一个)),然后是图像中的所有像素,还是(2)图像中每个类别的所有像素,然后再合并所有类的损失?
  2. 这些不同的像素组合是如何发生的 – 它们在哪里被求和/在哪里被平均?
  3. Keras的binary_crossentropyaxis=-1上进行平均。那么这是每个类别的所有像素的平均值,还是所有类别的平均值,或者两者都是?

换句话说:不同类别的损失是如何组合起来产生一个图像的单一损失值的?

这在文档中完全没有解释,对于在Keras上进行多类别预测的人来说会非常有帮助,无论网络类型如何。这里是keras代码的开始链接,其中首次传入损失函数。

我能找到的最接近解释的东西是

loss: 字符串(目标函数的名称)或目标函数。参见losses。如果模型有多个输出,可以通过传递字典或损失列表在每个输出上使用不同的损失。然后,模型将最小化的损失值将是所有个体损失的总和

来自keras。那么这是否意味着图像中每个类别的损失只是简单地相加?

示例代码在这里供某人尝试。这里是一个从Kaggle借来的基本实现,并修改为多标签预测:

# 构建U-Net模型num_classes = 5IMG_DIM = 256IMG_CHAN = 3weights = {0: 1, 1: 1, 2: 1, 3: 1, 4: 1000} #选择一个极端值只是为了检查任何反应inputs = Input((IMG_DIM, IMG_DIM, IMG_CHAN))s = Lambda(lambda x: x / 255) (inputs)c1 = Conv2D(8, (3, 3), activation='relu', padding='same') (s)c1 = Conv2D(8, (3, 3), activation='relu', padding='same') (c1)p1 = MaxPooling2D((2, 2)) (c1)c2 = Conv2D(16, (3, 3), activation='relu', padding='same') (p1)c2 = Conv2D(16, (3, 3), activation='relu', padding='same') (c2)p2 = MaxPooling2D((2, 2)) (c2)c3 = Conv2D(32, (3, 3), activation='relu', padding='same') (p2)c3 = Conv2D(32, (3, 3), activation='relu', padding='same') (c3)p3 = MaxPooling2D((2, 2)) (c3)c4 = Conv2D(64, (3, 3), activation='relu', padding='same') (p3)c4 = Conv2D(64, (3, 3), activation='relu', padding='same') (c4)p4 = MaxPooling2D(pool_size=(2, 2)) (c4)c5 = Conv2D(128, (3, 3), activation='relu', padding='same') (p4)c5 = Conv2D(128, (3, 3), activation='relu', padding='same') (c5)u6 = Conv2DTranspose(64, (2, 2), strides=(2, 2), padding='same') (c5)u6 = concatenate([u6, c4])c6 = Conv2D(64, (3, 3), activation='relu', padding='same') (u6)c6 = Conv2D(64, (3, 3), activation='relu', padding='same') (c6)u7 = Conv2DTranspose(32, (2, 2), strides=(2, 2), padding='same') (c6)u7 = concatenate([u7, c3])c7 = Conv2D(32, (3, 3), activation='relu', padding='same') (u7)c7 = Conv2D(32, (3, 3), activation='relu', padding='same') (c7)u8 = Conv2DTranspose(16, (2, 2), strides=(2, 2), padding='same') (c7)u8 = concatenate([u8, c2])c8 = Conv2D(16, (3, 3), activation='relu', padding='same') (u8)c8 = Conv2D(16, (3, 3), activation='relu', padding='same') (c8)u9 = Conv2DTranspose(8, (2, 2), strides=(2, 2), padding='same') (c8)u9 = concatenate([u9, c1], axis=3)c9 = Conv2D(8, (3, 3), activation='relu', padding='same') (u9)c9 = Conv2D(8, (3, 3), activation='relu', padding='same') (c9)outputs = Conv2D(num_classes, (1, 1), activation='sigmoid') (c9)model = Model(inputs=[inputs], outputs=[outputs])model.compile(optimizer='adam', loss=weighted_loss(weights), metrics=[mean_iou])def weighted_loss(weightsList):    def lossFunc(true, pred):        axis = -1 #如果通道在最后         #axis=  1 #如果通道在最前                classSelectors = K.argmax(true, axis=axis)         classSelectors = [K.equal(tf.cast(i, tf.int64), tf.cast(classSelectors, tf.int64)) for i in range(len(weightsList))]        classSelectors = [K.cast(x, K.floatx()) for x in classSelectors]        weights = [sel * w for sel,w in zip(classSelectors, weightsList)]         weightMultiplier = weights[0]        for i in range(1, len(weights)):            weightMultiplier = weightMultiplier + weights[i]        loss = BCE_loss(true, pred) - (1+dice_coef(true, pred))        loss = loss * weightMultiplier        return loss    return lossFuncmodel.summary()

实际的BCE-DICE损失函数可以在这里找到。

提问的动机:基于上述代码,网络在20个epoch后的总验证损失约为1%;然而,前4个类别的平均交并比得分都超过95%,但最后一个类别只有23%。显然表明第5个类别表现得非常差。然而,这种准确率的损失在损失中完全没有反映出来。因此,这意味着样本的个体损失是以一种完全抵消我们看到的第5个类别的巨大损失的方式组合起来的。而且,当样本损失在批次上组合时,它仍然非常低。我不确定如何调和这些信息。


回答:

虽然我已经在相关回答中提到过部分答案,但让我们逐步检查源代码,以更详细地找到具体答案。

首先,让我们前向传播(!):有一个调用weighted_loss函数,它接受y_truey_predsample_weightmask作为输入:

weighted_loss = weighted_losses[i]# ...output_loss = weighted_loss(y_true, y_pred, sample_weight, mask)

weighted_loss实际上是一个列表的元素,其中包含所有传递给fit方法的(增强)损失函数:

weighted_losses = [    weighted_masked_objective(fn) for fn in loss_functions]

我提到的“增强”这个词在这里很重要。因为,正如你所看到的,实际的损失函数被另一个名为weighted_masked_objective的函数包装,它的定义如下:

def weighted_masked_objective(fn):    """为目标函数添加掩码和样本加权支持。    它将一个目标函数fn(y_true, y_pred)    转换为一个样本加权、成本掩码的目标函数    fn(y_true, y_pred, weights, mask)。    # 参数        fn: 要包装的目标函数,            具有签名fn(y_true, y_pred)。    # 返回        具有签名fn(y_true, y_pred, weights, mask)的函数。    """    if fn is None:        return None    def weighted(y_true, y_pred, weights, mask=None):        """包装函数。        # 参数            y_true: fny_true参数。            y_pred: fny_pred参数。            weights: 权重张量。            mask: 掩码张量。        # 返回            标量张量。        """        # score_array的维度>=2        score_array = fn(y_true, y_pred)        if mask is not None:            # 将掩码转换为floatX以避免Theano中的float64上升            mask = K.cast(mask, K.floatx())            # 掩码应与score_array具有相同的形状            score_array *= mask            # 批次的损失应与未掩码样本的数量成比例            score_array /= K.mean(mask)        # 应用样本加权        if weights is not None:            # 将score_array减少到与权重数组相同的维度            ndim = K.ndim(score_array)            weight_ndim = K.ndim(weights)            score_array = K.mean(score_array,                                 axis=list(range(weight_ndim, ndim)))            score_array *= weights            score_array /= K.mean(K.cast(K.not_equal(weights, 0), K.floatx()))        return K.mean(score_array)return weighted

因此,有一个嵌套函数weighted,它实际上在score_array = fn(y_true, y_pred)这一行调用了真正的损失函数fn。现在,为了具体起见,在OP提供的示例中,fn(即损失函数)是binary_crossentropy。因此我们需要查看Keras中binary_crossentropy()的定义:

def binary_crossentropy(y_true, y_pred):    return K.mean(K.binary_crossentropy(y_true, y_pred), axis=-1)

它反过来调用后端函数K.binary_crossentropy()。在使用Tensorflow作为后端的情况下,K.binary_crossentropy()的定义如下:

def binary_crossentropy(target, output, from_logits=False):    """输出张量和目标张量之间的二元交叉熵。    # 参数        target: 与output形状相同的张量。        output: 一个张量。        from_logits: 是否期望output是一个logits张量。            默认情况下,我们认为output            编码了一个概率分布。    # 返回        一个张量。    """    # 注意:tf.nn.sigmoid_cross_entropy_with_logits    # 期望logits,Keras期望概率。    if not from_logits:        # 转换回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)

tf.nn.sigmoid_cross_entropy_with_logits返回:

一个与logits形状相同的张量,包含逐元素的逻辑损失。

现在,让我们反向传播(!):考虑到上述说明,K.binray_crossentropy的输出形状将与y_pred(或y_true)相同。正如OP提到的,y_true的形状为(batch_size, img_dim, img_dim, num_classes)。因此,K.mean(..., axis=-1)应用于形状为(batch_size, img_dim, img_dim, num_classes)的张量,结果输出张量的形状为(batch_size, img_dim, img_dim)。所以所有类别的损失值在图像中的每个像素上被平均。因此,weighted函数中score_array的形状将是(batch_size, img_dim, img_dim)。还有一个步骤:weighted函数中的返回语句再次取平均值,即return K.mean(score_array)。那么它是如何计算平均值的呢?如果你查看mean后端函数的定义,你会发现axis参数默认是None

def mean(x, axis=None, keepdims=False):    """张量的平均值,沿着指定的轴计算。    # 参数        x: 一个张量或变量。        axis: 一个整数列表。计算平均值的轴。        keepdims: 一个布尔值,是否保留维度。            如果keepdimsFalse,张量的秩            会因axis中的每个条目减少1。如果keepdimsTrue,            减少的维度将被保留,长度为1。    # 返回        一个包含x元素平均值的张量。    """    if x.dtype.base_dtype == tf.bool:        x = tf.cast(x, floatx())return tf.reduce_mean(x, axis, keepdims)

它调用了tf.reduce_mean(),当axis=None时,它在输入张量的所有轴上取平均,并返回一个单一的值。因此,形状为(batch_size, img_dim, img_dim)的整个张量的平均值被计算,这意味着在批次中所有标签及其所有像素上取平均,并作为一个单一的标量值返回,该值代表损失值。然后,Keras将此损失值报告回来,并用于优化。


附加:如果我们的模型有多个输出层,因此使用了多个损失函数会怎样?

记住我在这个答案中提到的第一段代码:

weighted_loss = weighted_losses[i]# ...output_loss = weighted_loss(y_true, y_pred, sample_weight, mask)

如你所见,这里有一个i变量用于索引数组。你可能已经猜对了:它实际上是循环的一部分,该循环使用其指定的损失函数为每个输出层计算损失值,然后取所有这些损失值的(加权)总和来计算总损失

# 计算总损失。total_loss = Nonewith K.name_scope('loss'):    for i in range(len(self.outputs)):        if i in skip_target_indices:            continue        y_true = self.targets[i]        y_pred = self.outputs[i]        weighted_loss = weighted_losses[i]        sample_weight = sample_weights[i]        mask = masks[i]        loss_weight = loss_weights_list[i]        with K.name_scope(self.output_names[i] + '_loss'):            output_loss = weighted_loss(y_true, y_pred,                                        sample_weight, mask)        if len(self.outputs) > 1:            self.metrics_tensors.append(output_loss)            self.metrics_names.append(self.output_names[i] + '_loss')        if total_loss is None:            total_loss = loss_weight * output_loss        else:            total_loss += loss_weight * output_loss    if total_loss is None:        if not self.losses:            raise ValueError('模型无法编译,因为它没有要优化的损失。')        else:            total_loss = 0.    # 添加正则化惩罚    # 和其他层特定的损失。    for loss_tensor in self.losses:        total_loss += loss_tensor  

Related Posts

使用LSTM在Python中预测未来值

这段代码可以预测指定股票的当前日期之前的值,但不能预测…

如何在gensim的word2vec模型中查找双词组的相似性

我有一个word2vec模型,假设我使用的是googl…

dask_xgboost.predict 可以工作但无法显示 – 数据必须是一维的

我试图使用 XGBoost 创建模型。 看起来我成功地…

ML Tuning – Cross Validation in Spark

我在https://spark.apache.org/…

如何在React JS中使用fetch从REST API获取预测

我正在开发一个应用程序,其中Flask REST AP…

如何分析ML.NET中多类分类预测得分数组?

我在ML.NET中创建了一个多类分类项目。该项目可以对…

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注