在语义分割中,如何创建一个自定义损失函数以忽略特定类别的假阴性?

请查看下面的编辑部分,初始帖子现在几乎没有意义,但问题仍然存在。


我正在开发一个神经网络来对图像进行语义分割。我已经尝试了各种损失函数(分类交叉熵(CCE)、加权CCE、焦点损失、Tversky损失、Jaccard损失、焦点Tversky损失等),这些函数试图处理类别分布严重不平衡的情况,但都没有达到预期效果。我的导师提到尝试创建一个自定义损失函数,该函数忽略特定类别的假阴性(但仍然惩罚假阳性)。

我有一个6类问题,我的网络设置为处理/使用独热编码的真实数据。因此,我的损失函数将接受两个张量,y_true, y_pred,形状为(batch, row, col, class)(当前为(8, 128, 128, 6))。为了能够利用我已经探索过的损失函数,我希望更改y_pred,以便将特定类别的预测值(第0类)始终设置为正确。即在y_true == class 0的地方设置y_pred == class 0,否则不做任何更改。

由于TensorFlow张量是不可变的,我花了太多时间试图创建这个损失函数。我的第一次尝试(通过我对numpy的经验引导)

def weighted_categorical_crossentropy_ignore(weights):    weights = K.variable(weights)    def loss(y_true, y_pred):        y_pred[tf.where(y_true == [1, 0, 0, 0, 0, 0])] = [1, 0, 0, 0, 0, 0]        # 缩放预测值,使每个样本的类别概率之和为1        y_pred /= K.sum(y_pred, axis=-1, keepdims=True)        # 裁剪以防止NaN和Inf        y_pred = K.clip(y_pred, K.epsilon(), 1 - K.epsilon())        loss = y_true * K.log(y_pred) * weights        loss = -K.sum(loss, -1)        return loss    return loss

显然,我无法更改y_pred,所以这个尝试失败了。我最终创建了一些尝试通过迭代[batch, row, col]并进行比较来“构建”张量的怪物。虽然这些尝试在技术上没有失败,但它们从未真正开始训练。我假设计算损失需要几分钟的时间。


在经历了更多失败的尝试后,我开始尝试在纯numpy中进行必要的计算,以创建一个SSCCE。但我意识到我基本上只能实例化“简单”的张量(即oneszeros)并只执行“简单”的操作,如逐元素乘法、加法和重塑。因此,我得到了这个SSCCE

import numpy as npfrom tensorflow.keras.utils import to_categorical# 随机生成“图像”true_flat = np.argmax(np.random.rand(1, 2, 2, 4), axis=3).astype('int')true = to_categorical(true_flat, num_classes=4).astype('int')pred_flat = np.argmax(np.random.rand(1, 2, 2, 4), axis=3).astype('int')pred = to_categorical(pred_flat, num_classes=4).astype('int')print('True:\n', true_flat)print('Pred:\n', pred_flat)# 创建一个表示所有“类0”图像的掩码class_zero_label = np.array([1, 0, 0, 0])czl_all = class_zero_label * np.ones(true.shape).astype('int')# 掩码真实值和预测值以定位类0像素czl_true_locs = czl_all * trueczl_pred_locs = czl_all * pred# 减去以创建“加法”矩阵a  = (czl_true_locs - czl_pred_locs) * czl_true_locsprint('a:\n', a)# 执行此操作m = ((a + 1) - (a * 2))print('m - ', m.shape, ':\n', m)# 从'm'中提取前面的条目并“扩展”其值#x = (m[:, :, :, 0].flatten() * np.ones(pred.shape).astype('int')).T.reshape(pred.shape)m_front = m[:, :, :, 0]print('m_front - ', m_front.shape, ':\n', m_front)#m_flat = m_front.flatten()m_flat = m_front.reshape(m_front.shape[0], m_front.shape[1]*m_front.shape[2])print('m_flat - ', m_flat.shape, ':\n', m_flat)m_expand = m_flat * np.ones(pred.shape).astype('int')print('m_expand - ', m_expand.shape, ':\n', m_expand)m_trans = m_expand.Tm_fixT = m_trans.reshape(pred.shape)print('m_fixT - ', m_fixT.shape, ':\n', m_fixT)m = m_fixTprint('m:\n', m.shape)# 执行描述的数学运算pred = (pred * m) + aprint('Pred:\n', np.argmax(pred, axis=3))

这个SSCCE非常糟糕且复杂。本质上,我的目标是创建两个矩阵,即“加法”和“乘法”矩阵。乘法矩阵旨在“清零”预测值中所有真实值等于类0的像素。不管像素值如何(即一个独热编码向量),将其清零为[0, 0, 0, 0, 0, 0]。然后,加法矩阵旨在将向量[1, 0, 0, 0, 0, 0]添加到每个被清零的位置。最终,这将实现将每个真正类0像素的预测值设置为正确的目标。

问题是这个SSCCE无法完全转换为TensorFlow操作。第一个问题是生成乘法矩阵时,当batch_size > 1时,它没有正确定义。我认为没关系,只是为了看看它是否有效,我会打破y_truey_pred张量并迭代它们。这导致了我当前的损失函数实例

def weighted_categorical_crossentropy_ignore(weights):    weights = K.variable(weights)    def loss(y_true, y_pred):        y_true_un = tf.unstack(y_true)        y_pred_un = tf.unstack(y_pred)        y_pred_new = []        for i in range(0, y_true.shape[0]):            yt = y_true_un[i]            yp = y_pred_un[i]            # Pred:            # [[[0 3] * [[[1 0] + [[[0 1] = [[[0 0]            #  [3 1]]]   [[1 1]]]  [[0 0]]]  [[3 1]]]            # 如果我们将预测值乘以一个张量,该张量仅清零错误的类0标记            # 然后将类0添加到这些清零的位置            # 我们可以抵消错误分类类0像素的影响,但仍然惩罚            # 其他类别的错误预测类0标签。            # 创建一个表示所有“类0”图像的掩码            class_zero_label = K.variable([1.0, 0.0, 0.0, 0.0, 0.0, 0.0])            czl_all = class_zero_label * K.ones(yt.shape)            # 掩码真实值和预测值以定位类0像素            czl_true = czl_all * yt            czl_pred = czl_all * yp            # 减去以创建“加法矩阵”            a = czl_true - czl_pred            # 执行此操作            m = ((a + 1) - (a * 2.))            # 执行此操作            x = K.flatten(m[:, :, 0])            x = x * K.ones(yp.shape)            x = K.transpose(x)            x = K.reshape(x, yp.shape)            # 完成了。            ypnew = (yp * x) + a            y_pred_new.append(ypnew)        y_pred_new = tf.concat(y_pred_new, 0)        # 继续计算加权分类交叉熵        # -------------------------------------------------------        # 缩放预测值,使每个样本的类别概率之和为1        y_pred_new /= K.sum(y_pred_new, axis=-1, keepdims=True)        # 裁剪以防止NaN和Inf        y_pred_new = K.clip(y_pred_new, K.epsilon(), 1 - K.epsilon())        loss = y_true * K.log(y_pred_new) * weights        loss = -K.sum(loss, -1)        return loss    return loss

当前损失函数的问题在于numpytensorflow在执行操作时行为上的明显差异

x = K.flatten(m[:, :, 0])x = x * K.ones(yp.shape)

这意味着要表示的行为

m_flat = m_front.flatten()m_expand = m_flat * np.ones(pred.shape).astype('int')

来自SSCCE。


所以在这一点上,我感觉自己已经深入到了穴居人编码中,无法摆脱它。我必须想象有一个类似于我最初尝试的简单方法来执行描述的行为。

所以,我猜我的直接问题是如何在自定义TensorFlow损失函数中实现

y_pred[tf.where(y_true == [1, 0, 0, 0, 0, 0])] = [1, 0, 0, 0, 0, 0]


编辑: 在进一步摸索之后,我终于确定了如何在y_truey_pred张量上调用.numpy()来利用numpy操作(显然,在程序开始时设置tf.compat.v1.enable_eager_execution“不起作用”。我必须在Model().compile(...)中传递run_eagerly=True)。

这使我能够实现基本上是第一次尝试的方案

def weighted_categorical_crossentropy_ignore(weights):    weights = K.variable(weights)    def loss(y_true, y_pred):        yp = y_pred.numpy()        yt = y_true.numpy()        yp[np.nonzero(np.all(yt == [1, 0, 0, 0, 0, 0], axis=3))] = [1, 0, 0, 0, 0, 0]         # 继续计算加权分类交叉熵        # -------------------------------------------------------        # 缩放预测值,使每个样本的类别概率之和为1        yp /= K.sum(yp, axis=-1, keepdims=True)        # 裁剪以防止NaN和Inf        yp = K.clip(yp, K.epsilon(), 1 - K.epsilon())        loss = y_true * K.log(yp) * weights        loss = -K.sum(loss, -1)        return loss    return loss

尽管通过调用y_pred.numpy()(或其后的使用)似乎我已经“破坏”了网络中的路径/流。基于尝试.fit时的错误

ValueError: No gradients provided for any variable: ['conv3d/kernel:0', <....>

我假设我需要以某种方式将张量“重新封装”回GPU内存?我尝试了

yp = tf.convert_to_tensor(yp)

但无济于事;同样的错误。所以我猜同样的问题仍然存在,但出于不同的动机..


编辑2: 从这个SO答案来看,似乎我不能实际使用numpy()来封装y_truey_pred以使用普通的numpy操作。这必然会“破坏”网络路径,因此无法计算梯度。

结果我意识到使用run_eagerly=True,我可以将y_true/y_pred设为tf.Variable并执行赋值。所以在纯TensorFlow中,我再次尝试重新创建相同的代码

def weighted_categorical_crossentropy_ignore(weights):    weights = K.variable(weights)    def loss(y_true, y_pred):        # yp = y_pred.numpy().copy()        # yt = y_true.numpy().copy()        # yp[np.nonzero(np.all(yt == [1, 0, 0, 0, 0, 0], axis=3))] = [1, 0, 0, 0, 0, 0]        yp = K.variable(y_pred)        yt = K.variable(y_true)        #np.all        x = K.all(yt == [1, 0, 0, 0, 0, 0], axis=3)        #np.nonzero        ne = tf.not_equal(x, tf.constant(False))        y = tf.where(ne)        # 执行所需的操作        yp[y] = [1, 0, 0, 0, 0, 0]        # 继续计算加权分类交叉熵        # -------------------------------------------------------        # 缩放预测值,使每个样本的类别概率之和为1        #yp /= K.sum(yp, axis=-1, keepdims=True) # 不能对tf.var使用\=,必须使用var = var /        yp = yp / K.sum(yp, axis=-1, keepdims=True)        # 裁剪以防止NaN和Inf        yp = K.clip(yp, K.epsilon(), 1 - K.epsilon())        loss = y_true * K.log(yp) * weights        loss = -K.sum(loss, -1)        return loss    return loss

但遗憾的是,这显然与调用.numpy()时产生了相同的问题;无法计算梯度。所以我似乎又回到了起点。


编辑3: 使用gobrewers14在下方答案中提出的解决方案,但基于我对问题的了解进行了修改,我产生了这个损失函数

def weighted_categorical_crossentropy_ignore(weights):    weights = K.variable(weights)    def loss(y_true, y_pred):        print('y_true.shape: ', y_true.shape)        print('y_pred.shape: ', y_pred.shape)        # 生成修改后的y_pred,其中所有真正类0像素都是正确的        y_true_class0_indicies = tf.where(tf.math.equal(y_true, [1., 0., 0., 0., 0., 0.]))        y_pred_updates = tf.repeat([            [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]],            repeats=y_true_class0_indicies.shape[0],            axis=0)        yp = tf.tensor_scatter_nd_update(y_pred, y_true_class0_indicies, y_pred_updates)        # 继续计算加权分类交叉熵        # -------------------------------------------------------        # 缩放预测值,使每个样本的类别概率之和为1        yp /= K.sum(yp, axis=-1, keepdims=True)        # 裁剪以防止NaN和Inf        yp = K.clip(yp, K.epsilon(), 1 - K.epsilon())        loss = y_true * K.log(yp) * weights        loss = -K.sum(loss, -1)        return loss    return loss

考虑到原始答案假设y_true的形状为[8, 128, 128](即“平面”类别表示,而不是独热编码表示[8, 128, 128, 6]),我首先打印y_truey_pred输入张量的形状以进行健全性检查

y_true.shape:  (8, 128, 128, 6)y_pred.shape:  (8, 128, 128, 6)

为了进一步的健全性检查,网络的输出形状,由model.summary的尾部提供是

conv2d_18 (Conv2D)              (None, 128, 128, 6)  1542        dropout_5[0][0]                  __________________________________________________________________________________________________activation_9 (Activation)       (None, 128, 128, 6)  0           conv2d_18[0][0]                  ==================================================================================================Total params: 535,551,494Trainable params: 535,529,478Non-trainable params: 22,016__________________________________________________________________________________________________

然后我遵循提出的解决方案中的“模式”,并将原始的tf.math.equal(y_true, 0)替换为tf.math.equal(y_true, [1., 0., 0., 0., 0., 0.])以处理独热编码的情况。从我对提出的解决方案的理解(在检查了大约10分钟后),我假设这应该有效。然而,在尝试训练模型时,抛出了以下异常

InvalidArgumentError: Inner dimensions of output shape must match inner dimensions of updates shape. Output: [8,128,128,6] updates: [684584,6] [Op:TensorScatterUpdate]

因此,似乎(如我所命名的)y_pred_updates的生成产生了一个“折叠”的张量,具有“太多”的元素。我理解使用tf.repeat的动机,但其具体使用似乎不正确。我假设它应该根据我对tf.tensor_scatter_nd_update的理解产生形状为(8, 128, 128, 6)的张量。我假设这很可能是基于在调用tf.repeat时选择的repeatsaxis


回答:

如果我正确理解了您的问题,您正在寻找类似这样的东西:

import tensorflow as tf# 真实标签的批次y_true = tf.constant([5, 0, 1, 3, 4, 0, 2, 0], dtype=tf.int64)# 类别概率的批次y_pred = tf.constant(  [    [0.34670502, 0.04551039, 0.14020428, 0.14341979, 0.21430719, 0.10985339],    [0.25681055, 0.14013883, 0.19890164, 0.11124421, 0.14526634, 0.14763844],    [0.09199252, 0.21889475, 0.1170236 , 0.1929019 , 0.20311192, 0.17607528],    [0.3246354 , 0.23257554, 0.15549366, 0.17282239, 0.00000001, 0.11447308],    [0.16502093, 0.13163856, 0.14371352, 0.19880624, 0.23360236, 0.12721846],    [0.27362782, 0.21408406, 0.10917682, 0.13135742, 0.10814326, 0.16361059],    [0.20697299, 0.23721898, 0.06455399, 0.11071447, 0.18990229, 0.19063729],    [0.10320242, 0.22173141, 0.2547973 , 0.2314068 , 0.07063974, 0.11822232]  ], dtype=tf.float32)# 在批次中查找真实标签为类0的索引indices = tf.where(tf.math.equal(y_true, 0))# 创建一个包含您想要在`y_pred`中替换的更新数量的张量updates = tf.repeat(    [[1.0, 0.0, 0.0, 0.0, 0.0, 0.0]],    repeats=indices.shape[0],    axis=0)# 在指定的索引处将更新插入到`y_pred`中modified_y_pred = tf.tensor_scatter_nd_update(y_pred, indices, updates)print(modified_y_pred)# tf.Tensor(#   [[0.34670502, 0.04551039, 0.14020428, 0.14341979, 0.21430719, 0.10985339],#    [1.00000000, 0.00000000, 0.00000000, 0.00000000, 0.00000000, 0.00000000],#    [0.09199252, 0.21889475, 0.1170236 , 0.1929019 , 0.20311192, 0.17607528],#    [0.3246354 , 0.23257554, 0.15549366, 0.17282239, 0.00000001, 0.11447308],#    [0.16502093, 0.13163856, 0.14371352, 0.19880624, 0.23360236, 0.12721846],#    [1.00000000, 0.00000000, 0.00000000, 0.00000000, 0.00000000, 0.00000000],#    [0.20697299, 0.23721898, 0.06455399, 0.11071447, 0.18990229, 0.19063729],#    [1.00000000, 0.00000000, 0.00000000, 0.00000000, 0.00000000, 0.00000000]], #    shape=(8, 6), dtype=tf.float32)

这个最终的张量modified_y_pred可以用于求导。

编辑:

使用掩码可能更容易实现这一点。

示例:

# 这些未归一化为1,但您明白这一点probs = tf.random.normal([2, 4, 4, 6])# 每像素的原始标签labels = tf.random.uniform(    shape=[2, 4, 4],    minval=0,    maxval=6,    dtype=tf.int64)# 您的标签已经是独热编码labels = tf.one_hot(labels, 6)# 布尔掩码,其中类别为`0`# 将其转换回整数标签以便使用`tf.math.equal`。匹配`[1, 0, 0, 0, 0, 0]`可能有问题;# 匹配整数更明确。mask = tf.math.equal(tf.math.argmax(labels, -1), 0)[..., None]# 翻转掩码以清零标签为零的像素跨通道probs *= tf.cast(tf.math.logical_not(mask), tf.float32)# 将掩码乘以独热编码标签,并添加回已掩码的概率。probs += labels * tf.cast(mask, tf.float32)

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中创建了一个多类分类项目。该项目可以对…

发表回复

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