1.多标签和多分类区分
怎么定义多标签问题?——多标签和多分类问题,神经网络结构其实可以一摸一样,最后的输出层就是N个节点,每个节点表示一个类别。在estimator.head中有两个子类分别是MultiClassHead和MultiLebelHead,分别对应多分类和多标签问题,这俩最大的区别其实是默认的损失函数不同。多分类用softmax_cross_entropy,多标签用sigmoid_cross_entropy,也就是输出层分别对应softmax和sigmoid。softmax相当于对多个类目进行归一化,最后概率和是1;sigmoid则没有这一处理,所以你会看到多个输出节点概率值可能同时大于0.5——定性解释,多分类问题要求类别之间互斥非此即彼,多标签问题可以允许类目共存。
2.MultiLabelHead.loss代码走读
MuiltLabelHead是tf.estimator.Head的子类,用于多标签问题,默认损失函数为sigmoid_cross_entropy(也可以自定义loss或者train_op,初始化的时候传入)。具体的调用关系如下(version=tensorflow 1.15,其他版本可能稍有不同):
关键函数:
_unweighted_loss_and_weights:返回loss和weights。
def _unweighted_loss_and_weights(self, logits, processed_labels, features):
"""Computes loss spec."""
# 如果自定义了loss_fn就用loss_fn,注意expected_loss_dim这里希望这个loss_fn返回值最后一维是1(这里后面会说到)
if self._loss_fn:
unweighted_loss = base_head.call_loss_fn(
loss_fn=self._loss_fn,
labels=processed_labels,
logits=logits,
features=features,
expected_loss_dim=1)
# 默认用sigmoid_cross_entropy,并在classes维度上求平均,这样unweighted_loss最后一维就是1
else:
unweighted_loss = losses.sigmoid_cross_entropy(
multi_class_labels=processed_labels,
logits=logits,
reduction=losses.Reduction.NONE)
# Averages loss over classes.
unweighted_loss = math_ops.reduce_mean(
unweighted_loss, axis=-1, keepdims=True)
# 从features里面获取_weight_column中定义的权重,_weight_column是特征名
weights = base_head.get_weights_and_check_match_logits(
features=features,
weight_column=self._weight_column,
logits=logits)
return unweighted_loss, weights
- loss默认就是sigmoid_cross_entropy,且默认loss reduction是None,这样loss的shape就和logits一样,但返回前有一个loss=reduce_mean(loss, axis=-1),loss在类目维度进行求均值,如果原来的shape是[batch_size, logits]会变为[batch_size,1];
- weights是model_fn传入的features中读到的,样本粒度的权重,默认是1。一般用于样本类目不均衡时候调整权重,有点类似于focal_loss,这个权重值可以落训练数据的时候当作一列特征读入,也可以在input_fn中根据特定label训练时生成。该权重对应MultiLabelHead初始化的weight_column参数。
- 这里的loss和weights分别返回,此时还没有加权。
losses_utils.compute_weighted_loss:使用weights对loss加权,返回training_loss。
def compute_weighted_loss(losses,
sample_weight=None,
reduction=ReductionV2.SUM_OVER_BATCH_SIZE,
name=None):
ReductionV2.validate(reduction)
# If this function is called directly, then we just default 'AUTO' to
# 'SUM_OVER_BATCH_SIZE'. Eg. Canned estimator use cases.
if reduction == ReductionV2.AUTO:
reduction = ReductionV2.SUM_OVER_BATCH_SIZE
if sample_weight is None:
sample_weight = 1.0
with K.name_scope(name or 'weighted_loss'):
# Save the `reduction` argument for loss normalization when distributing
# to multiple replicas. Used only for estimator + v1 optimizer flow.
ops.get_default_graph()._last_loss_reduction = reduction # pylint: disable=protected-access
losses = ops.convert_to_tensor(losses)
input_dtype = losses.dtype
weighted_losses = tf_losses_utils.scale_losses_by_sample_weight(
losses, sample_weight)
# Apply reduction function to the individual weighted losses.
loss = reduce_weighted_loss(weighted_losses, reduction)
# Convert the result back to the input type.
loss = math_ops.cast(loss, input_dtype)
return loss
- weigths可以是一个标量,也可以跟loss的形状一样,比如loss形状是[batch_size,1],那么weighs可以rank=0或者形状跟loss一样。如果形状跟loss一样,相当于在纵向样本粒度上进行加权,每一个样本都有一个自己的权重,当然前面说了默认值都是1.
3 能否对每一个类目进行加权/掩码?
我们在实际应用中,有时候不仅需要样本粒度的权重,也希望每一个类目都有权重,MultiHeadLabel默认的损失函数没有支持,需要我们自定义loss_fn,在_unweighted_loss_and_weights中首先会检查是否有自定义的loss_fn,如果没有才会用默认的loss.
from tensorflow.python.ops.losses import losses
def build_loss_fn(weights_per_logits):
def loss_fn(labels, logits):
loss = losses.sigmoid_cross_entropy(
multi_class_labels=labels,
logits=logits,
weights=weights_per_logits,
reduction=losses.Reduction.SUM_OVER_NONZERO_WEIGHTS)
return loss
return loss_fn
这里定义一个闭包函数,返回loss_fn,具体到loss_fn,我们传入weight_per_logits,该参数是每一个类别的权重,形状和logits一致,在losses.sigmoid_cross_entropy内部调用weighted_losses = math_ops.multiply(losses, weights)实现加权。注意这里我们用了reduction=losses.Reduction.SUM_OVER_NONZERO_WEIGHTS来对loss进行处理,主要原因是我们的weights其实是0/1组成的掩码,每条样本只有一个类目是1,其他地方都是0不贡献损失,所以最后计算loss将非0的元素求平均更合理,当然你也可以换一个其他的reduction。
定义好了loss_fn,可以初始化MultiLabelHead类的时候作为参数传入:
loss_fn = build_loss_fn(weights_per_logits)
head = MyMultiLabelHead( n_classes= K, loss_fn=loss_fn)
不过这里还有个坑,因为在原始MultiLabelHead的_unweighted_loss_and_weights函数中,希望返回的loss是一个最后一维是1的tensor,比如shape=[batch_size, 1]的tensor。但我们刚刚定义的loss_fn的reduction用了SUM_OVER_NONZERO_WEIGHTS,已经直接把loss整成一个标量,最后一维不是1了。这里我们要么改我们的loss_fn,和默认loss处理方式一样reduction用None再在classed维度求平均后返回,如果没有特殊要求这种方案应该是比较不错的。而我就是想用SUM_OVER_NONZERO_WEIGHTS,所以最终采用了另一种方法,直接继承MultiLabelHead,重写_unweighted_loss_and_weights函数,把else后面的默认流程改成我想要的了,构建子类还有一个好处能改几乎所有的重要地方,比如直接在__init__函数中加入weights_per_logits成员变量、重写preditions加自己想要的输出(比如label),等等。