各位朋友大家好,欢迎来到 月来客栈。由于推文不支持后续修订, 所以本文将同步推送至网站 www.ylkz.life,欢迎关注,谢谢大家!

1 引言

各位朋友大家好,欢迎来到月来客栈。在 前面的一篇文章[1]中笔者介绍了在单标签分类问题中模型损失的度量方法,即交叉熵损失函数。同时也介绍了 多分类任务中常见的评价指标 及其实现方法[2]。在接下来的这篇文章中,笔者将会详细介绍在多标签分类任务中两种常见的损失评估方法,以及在多标签分类场景中的模型评价指标。

2 方法一

将原始输出层的softmax操作替换为simoid操作,然后通过计算输出层与标签之间的sigmoid交叉熵来作为误差的衡量标准,具体计算公式如下:

loss(y,\hat{y})=-\frac{1}{C} \sum_{i=1}^m\left[y^{(i)}\cdot\log\left(\frac{1}{1+\exp(-\hat{y}^{(i)})}\right)+\left(1-y^{(i)}\right)\cdot\log\left(\frac{\exp(-\hat{y}^{(i)})}{1+\exp(-\hat{y}^{(i)})}\right)\right]\;\;\;\;\;(1)

其中C 表示类别数量, y^{(i)}和 \hat{y}^{(i)} 均为一个向量,分别用来表示真实标签和未经任何激活函数处理的网络输出值。

从式 (1)可以发现,这种误差损失衡量方式其实就是在逻辑回归中用来衡量预测概率与真实标签之间误差的方法。

2.1 numpy 实现:

根据式(3) 的计算公式,可以通过如下Python代码来完成损失值的计算:

def sigmoid(z):
    return 1 / (1 + np.exp(-z))

def compute_loss_v1(y_true, y_pred):
    t_loss = y_true * np.log(sigmoid(y_pred)) + \
             (1 - y_true) * np.log(1 - sigmoid(y_pred))  # [batch_size,num_class]
    loss = t_loss.mean(axis=-1)  # 得到每个样本的损失值, 这里可以是
    return -loss.mean()  # 返回整体样本的损失均值(或其他)

if __name__ == '__main__':
    y_true = np.array([[1, 1, 0, 0], [0, 1, 0, 1]])
    y_pred = np.array([[0.2, 0.5, 0, 0], [0.1, 0.5, 0, 0.8]])
    print(compute_loss_v1(y_true, y_pred)) # 0.5926

当然,在 TensorFlow 1.xPytorch 中也分别对这两种方法进行了实现。

2.2 TensorFlow 实现

Tensorflow 1.x 中,可以通过 tf.nn 模块下的 sigmoid_cross_entropy_with_logits 方法进行调用:

def sigmoid_cross_entropy_with_logits(labels, logits):
    loss = tf.nn.sigmoid_cross_entropy_with_logits(labels=labels, logits=logits)
    loss = tf.reduce_mean(loss, axis=-1)
    return tf.reduce_mean(loss)

if __name__ == '__main__':
    y_true = tf.constant([[1, 1, 0, 0], [0, 1, 0, 1]],dtype=tf.float16)
    y_pred = tf.constant([[0.2, 0.5, 0, 0], [0.1, 0.5, 0, 0.8]],dtype=tf.float16)
    with tf.Session() as sess:
        loss = sess.run(sigmoid_cross_entropy_with_logits(y_true,y_pred))
        print(loss) # 0.5926

当然,在模型训练完成后,可以通过如下代码来得到预测的标签结果和相应的概率值:

def prediction(logits, K):
      y_pred = np.argsort(-logits, axis=-1)[:,:K]
    print("预测标签:",y_pred)
    p = np.vstack([logits[r,c] for r,c in enumerate(y_pred)])
    print("预测概率:",p)

prediction(y_pred,2)
#####
预测标签:
[[1 0]
 [3 1]]
预测概率:
[[0.5 0.2]
 [0.8 0.5]]

2.3 Pytorch 实现

Pytorch 中,可以通过 torch.nn 模块中的 MultiLabelSoftMarginLoss 类来完成损失的计算:

if __name__ == '__main__':
    y_true = torch.tensor([[1, 1, 0, 0], [0, 1, 0, 1]],dtype=torch.int16)
    y_pred = torch.tensor([[0.2, 0.5, 0, 0], [0.1, 0.5, 0, 0.8]],dtype=torch.float32)
    loss = nn.MultiLabelSoftMarginLoss(reduction='mean')
    print(loss(y_pred, y_true)) #0.5926

同样,在模型训练完成后也可以通过上面的 prediction 函数来完成推理预测。需要注意的是,在 TensorFlow 1.xsigmoid_cross_entropy_with_logits 方法返回的是所有样本损失的均值;而在 Pytorch 中, MultiLabelSoftMarginLoss 默认返回的是所有样本损失的均值,但是可以通过指定参数 reductionmeansum 来指定返回的类型。

3 方法二

在衡量多标签分类结果损失的方法中,除了上面介绍的方法一之外还有一种常用的损失函数。这种损失函数其实就是我们在单标签分类中用到的交叉熵损失函数的拓展版,单标签可以看作是其中的一种特例情况。其具体计算公式如下所示:

loss(y,\hat{y})=-\frac{1}{m}\sum_{i=1}^m\sum_{j=1}^qy^{(i)}_j\log{\hat{y}^{(i)}_j}\;\;\;\;\;\;\;\;\;\;(2)

其中 y^{(i)}_j 表示第 i个样本第j 个类别的真实值, \hat{y}^{(i)}_j表示第i个样本第 j个类别的输出经过softmax处理后的结果。

例如对于如下样本来说:

y_true = np.array([[1, 1, 0, 0], [0, 1, 0, 1.]])
y_pred = np.array([[0.2, 0.5, 0.1, 0], [0.1, 0.5, 0, 0.8]])

输出值经过softmax处理后的结果为:

[[0.24549354 0.33138161 0.22213174 0.20099311]
 [0.18482871 0.27573204 0.16723993 0.37219932]]

那么,根据公式(2) 可知,对于上述2个样本来说其损失值为:

3.1 numpy 实现:

根据式(3) 的计算公式,可以通过如下Python代码来完成损失值的计算:

def softmax(x):
    s = np.exp(x)
    return s / np.sum(s, axis=-1, keepdims=True)

def compute_loss_v2(logits, y):
    logits = softmax(logits)
    print(logits)
    c = -(y * np.log(logits)).sum(axis=-1)  # 计算每一个样本的在各个标签上的损失和
    return np.mean(c)  # 计算所有样本损失的平均值
y_true = np.array([[1, 1, 0, 0], [0, 1, 0, 1.]])
y_pred = np.array([[0.2, 0.5, 0.1, 0], [0.1, 0.5, 0, 0.8]])
print(compute_loss_v2(y_pred, y_true))# 2.392

3.2 TensorFlow 实现

Tensorflow 1.x 中,可以通过 tf.nn 模块下的 softmax_cross_entropy_with_logits_v2 方法进行调用:

def softmax_cross_entropy_with_logits(labels, logits):
    loss = tf.nn.softmax_cross_entropy_with_logits_v2(labels=labels, logits=logits)
    return tf.reduce_mean(loss)
y_true = tf.constant([[1, 1, 0, 0], [0, 1, 0, 1.]], dtype=tf.float16)
y_pred = tf.constant([[0.2, 0.5, 0.1, 0], [0.1, 0.5, 0, 0.8]], dtype=tf.float16)
with tf.Session() as sess:
    loss = sess.run(softmax_cross_entropy_with_logits(y_true, y_pred))
    print(loss)# 2.395

3.3 Pytorch 实现

Pytorch 中,笔者目前还没找到可以调用的相应模型,但是可以通过自己来编码实现:

def cross_entropy(logits, y):
    s = torch.exp(logits)
    logits = s / torch.sum(s, dim=1, keepdim=True)
    c = -(y * torch.log(logits)).sum(dim=-1)
    return torch.mean(c)

y_true = torch.tensor([[1, 1, 0, 0], [0, 1, 0, 1.]])
y_pred = torch.tensor([[0.2, 0.5, 0.1, 0], [0.1, 0.5, 0, 0.8]])
loss = cross_entropy(y_pred,y_true)
print(loss)# 2.392

需要注意的是,由于各个框架在计算时保留小数的策略不同,所以最后的结果在小数位后面会出现略微的差异。

4 评估指标

4.1 不考虑部分正确的评估方法

(1) 绝对匹配率( Exact Match Ratio

所谓绝对匹配率指的就是,对于每一个样本来说,只有预测值与真实值完全相同的情况下才算预测正确,也就是说只要有一个类别的预测结果有差异都算没有预测正确。因此,其准确率计算公式为:

MR=\frac{1}{m}\sum_{i=1}^mI(y^{(i)}==\hat{y}^{(i)})\;\;\;\;\;\;\;\;\;\;(4)

其中 n表示样本总数;I(\cdot) 为指示函数(indicator function),当 y_i完全等同于\hat{y}_i 时取 1,否则为 0。可以看出,MR值越大,表示分类的准确率越高。

例如现有如下真实值和预测值:

y_true = np.array([[0, 1, 0, 1],
                       [0, 1, 1, 0],
                       [1, 0, 1, 1]])

y_pred = np.array([[0, 1, 1, 0],
                       [0, 1, 1, 0],
                       [0, 1, 0, 1]])

那么其对应的MR就应该是 $0.333$,因为只有第2个样本才算预测正确。在 sklearn 中,可以直接通过 sklearn.metrics 模块中的 accuracy_scor 方法来完成计算[3],如下所示:

from sklearn.metrics import accuracy_score
print(accuracy_score(y_true,y_pred)) # 0.33333333

(2)0-1损失

除了绝对匹配率之外,还有另外一种与之计算过程恰好相反的评估标准,即0-1损失( Zero-One Loss)。绝对准确率计算的是完全预测正确的样本占总样本数的比例,而0-1损失计算的是完全预测错误的样本占总样本的比例。因此对于上面的预测和真实结果,其0-1损失就应该为0.667。计算公式如下:

L_{0-1}=\frac{1}{m}\sum_{i=1}^mI(y^{(i)}\neq\hat{y}^{(i)})\;\;\;\;\;\;\;\;\;\;(5)

sklearn 中,可以通过 sklearn.metrics 模块中的 zero_one_loss 方法来完成计算[3],如下所示:

4.2 考虑部分正确的评估方法

从上面的两种评估指标可以看出,不管是绝对匹配率还是0-1损失,两者在计算结果的时候都没有考虑到部分正确的情况,而这对于模型的评估来说显然是不准确的。例如,假设正确标签为 [1,0,0,1],模型预测的标签为 [1,0,1,0]。可以看到,尽管模型没有预测对全部的标签,但是预测对了一部分。因此,一种可取的做法就是将部分预测正确的结果也考虑进去[4]。为了实现这一想法,文献[5]中提出了在多标签分类场景下的准确率(Accuracy)、精确率(Precision)、召回率(Recall)和 F_1值( F_1-Measure)计算方法。

(1)准确率

对于准确率来说,其计算公式为:

\text{Accuracy} = \frac{1}{m} \sum_{i=1}^{m} \frac{\lvert y^{(i)} \cap \hat{y}^{(i)}\rvert}{\lvert y^{(i)} \cup \hat{y}^{(i)}\rvert}\;\;\;\;\;\;\;\;\;\;(6)

从公式(6) 可以看出,准确率其实计算的是所有样本的平均准确率。而对于每个样本来说,准确率就是预测正确的标签数在整个预测为正确或真实为正确标签数中的占比。例如对于某个样本来说,其真实标签为 [0, 1, 0, 1],预测标签为 [0, 1, 1, 0]。那么该样本对应的准确率就应该为:

acc = \frac{1}{1+1+1}=\frac{1}{3}\;\;\;\;\;\;\;\;\;\;(7)

因此,对于如下真实结果和预测结果来说:

y_true = np.array([[0, 1, 0, 1], [0, 1, 1, 0], [1, 0, 1, 1]])

y_pred = np.array([[0, 1, 1, 0], [0, 1, 1, 0], [0, 1, 0, 1]])

其准确率为:

\text{Accuracy}=\frac{1}{3}\times(\frac{1}{3}+\frac{2}{2}+\frac{1}{4})\approx0.5278\;\;\;\;\;\;\;\;\;\;(8)

对应的实现代码为[6]:

def Accuracy(y_true, y_pred): count = 0 for i in range(y_true.shape[0]): p = sum(np.logical_and(y_true[i], y_pred[i])) q = sum(np.logical_or(y_true[i], y_pred[i])) count += p / q return count / y_true.shape[0] print(Accuracy(y_true, y_pred)) # 0.52777

(2)精确率

对于精确率来说,其计算公式为:

\text{Precision} = \frac{1}{m} \sum_{i=1}^{m} \frac{\lvert y^{(i)} \cap \hat{y}^{(i)}\rvert}{\lvert \hat{y}^{(i)}\rvert}\;\;\;\;\;\;\;\;\;\;(9)

从公式(9) 可以看出,精确率其实计算的是所有样本的平均精确率。而对于每个样本来说,精确率就是预测正确的标签数在整个预测为正确的标签数中的占比。例如对于某个样本来说,其真实标签为 [0, 1, 0, 1],预测标签为 [0, 1, 1, 0]。那么该样本对应的精确率就应该为:

\text{pre} = \frac{1}{1+1}=\frac{1}{2}\;\;\;\;\;\;\;\;\;\;(10)

因此,对于上面的真实结果和预测结果来说,其精确率为:

\text{Precision} = \frac{1}{3}\times(\frac{1}{2}+\frac{2}{2}+\frac{1}{2})\approx0.6666\;\;\;\;\;\;\;\;\;\;(11)

对应的实现代码为:

def Precision(y_true, y_pred):
    count = 0
    for i in range(y_true.shape[0]):
        if sum(y_pred[i]) == 0:
            continue
        count += sum(np.logical_and(y_true[i], y_pred[i])) / sum(y_pred[i])
    return count / y_true.shape[0]
print(Precision(y_true, y_pred))# 0.6666

(3)召回率

对于召回率来说,其计算公式为:

\text{Recall} = \frac{1}{m} \sum_{i=1}^{m} \frac{\lvert y^{(i)} \cap \hat{y}^{(i)}\rvert}{\lvert y^{(i)}\rvert} \;\;\;\;\;\;\;\;\;\;(12)

从公式 (12)可以看出,召回率其实计算的是所有样本的平均精确率。而对于每个样本来说,召回率就是预测正确的标签数在整个正确的标签数中的占比。

因此,对于如下真实结果和预测结果来说:

y_true = np.array([[0, 1, 0, 1],
                       [0, 1, 1, 0],
                       [1, 0, 1, 1]])

y_pred = np.array([[0, 1, 1, 0],
                       [0, 1, 1, 0],
                       [0, 1, 0, 1]])

其召回率为:

\text{Recall}=\frac{1}{3}\times(\frac{1}{2}+\frac{2}{2}+\frac{1}{3})\approx0.6111\;\;\;\;\;\;\;\;\;\;(13)

对应的实现代码为:

def Recall(y_true, y_pred):
    count = 0
    for i in range(y_true.shape[0]):
        if sum(y_true[i]) == 0:
            continue
        count += sum(np.logical_and(y_true[i], y_pred[i])) / sum(y_true[i])
    return count / y_true.shape[0]
print(Recall(y_true, y_pred))# 0.6111

(4) F_1值

对于 F_1值来说,其计算公式为:

F_{1} = \frac{1}{m} \sum_{i=1}^{m} \frac{2 \lvert y^{(i)} \cap \hat{y}^{(i)}\rvert}{\lvert y^{(i)}\rvert + \lvert \hat{y}^{(i)}\rvert} \;\;\;\;\;\;\;\;\;\;(14)

从公式(14) 可以看出, F_1计算的也是所有样本的平均精确率。因此,对于上面的真实结果和预测结果来说,其 F_1值为:

F_1=\frac{2}{3}(\frac{1}{4}+\frac{2}{4}+\frac{1}{5})\approx0.6333\;\;\;\;\;\;\;\;\;\;(15)

对应的实现代码为:

def F1Measure(y_true, y_pred):
    count = 0
    for i in range(y_true.shape[0]):
        if (sum(y_true[i]) == 0) and (sum(y_pred[i]) == 0):
            continue
        p = sum(np.logical_and(y_true[i], y_pred[i]))
        q = sum(y_true[i]) + sum(y_pred[i])
        count += (2 * p) / q
    return count / y_true.shape[0]
print(F1Measure(y_true, y_pred))# 0.6333

在上述4项指标中,都是值越大,对应模型的分类效果越好。同时,从公式 (6)(9)(12)(14)可以看出,多标签场景下的各项指标尽管在计算步骤上与单标签场景有所区别,但是两者在计算各个指标时所秉承的思想却是类似的。

当然,对于后面3个指标的计算,还可以直接通过 sklearn 来完成,代码如下:

from sklearn.metrics import precision_score, recall_score, f1_score
print(precision_score(y_true=y_true, y_pred=y_pred, average='samples'))# 0.6666
print(recall_score(y_true=y_true, y_pred=y_pred, average='samples'))# 0.6111
print(f1_score(y_true,y_pred,average='samples'))# 0.6333

(5)Hamming Loss

除了前面已经介绍的6中评估方法外,下面再介绍另外一种更加直观的衡量方法Hamming Loss[3],它的计算公式为:

\text{Hamming Loss} = \frac{1}{m q} \sum_{i=1}^{m}\sum_{j=1}^{q} I\left( y^{(i)}_{j} \neq \hat{y}^{(i)}_{j} \right) \;\;\;\;\;\;\;\;\;\;(16)

其中 y^{(i)}_j表示第 i个样本的第 j个标签, q表示一种有多少个类别。

从公式(16) 可以看出,Hamming Loss衡量的是所有样本中,预测错的标签数在整个标签标签数中的占比。所以对于Hamming Loss损失来说,其值越小表示模型的表现结果越好。因此,对于如下真实结果和预测结果来说:

y_true = np.array([[0, 1, 0, 1],
                       [0, 1, 1, 0],
                       [1, 0, 1, 1]])

y_pred = np.array([[0, 1, 1, 0],