请查看下面的编辑部分,初始帖子现在几乎没有意义,但问题仍然存在。
我正在开发一个神经网络来对图像进行语义分割。我已经尝试了各种损失函数(分类交叉熵(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。但我意识到我基本上只能实例化“简单”的张量(即ones
、zeros
)并只执行“简单”的操作,如逐元素乘法、加法和重塑。因此,我得到了这个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_true
和y_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
当前损失函数的问题在于numpy
和tensorflow
在执行操作时行为上的明显差异
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_true
、y_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_true
、y_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_true
和y_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
时选择的repeats
和axis
。
回答:
如果我正确理解了您的问题,您正在寻找类似这样的东西:
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)