假设我有一个具有以下参数的网络:
- 用于语义分割的全卷积网络
- 损失函数 = 加权二元交叉熵(但可以是任何损失函数,这不重要)
- 5个类别 – 输入是图像,真实标签是二元掩码
- 批次大小 = 16
我知道损失是按以下方式计算的:二元交叉熵被应用于图像中每个像素的每个类别。因此,本质上,每个像素将有5个损失值
在这一步之后会发生什么?
当我训练我的网络时,它在每个epoch只打印一个损失值。需要进行许多层次的损失累积才能产生一个单一的值,而这种过程在文档/代码中完全不清楚。
- 首先合并什么 – (1)类的损失值(例如,每个像素合并5个值(每个类别一个)),然后是图像中的所有像素,还是(2)图像中每个类别的所有像素,然后再合并所有类的损失?
- 这些不同的像素组合是如何发生的 – 它们在哪里被求和/在哪里被平均?
- Keras的binary_crossentropy在
axis=-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_true
、y_pred
、sample_weight
和mask
作为输入:
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: fn
的y_true
参数。 y_pred: fn
的y_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: 一个布尔值,是否保留维度。 如果keepdims
为False
,张量的秩 会因axis
中的每个条目减少1。如果keepdims
为True
, 减少的维度将被保留,长度为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