3.13 多标签分类#

3.5.5节内容中,我们介绍了在单标签分类问题中模型损失的度量方法,即交叉熵损失函数。但是在实际应用中我们还会遇到多标签分类(Multi-Label Class)的情况,即对于每个样本来说都可能存在不止一个正确标签的情况。例如在文本分类这一场景中,同一条文本可能会涉及到“体育”、“娱乐”等多个类别标签。在接下来的这篇文章中,我们将会详细介绍在多标签分类任务中两种常见的损失评估方法,以及在多标签分类场景中的模型评价指标。

3.13.1 Sigmoid损失#

在多标签分类场景中,第1种损失衡量方式就是将原始输出层的Softmax操作替换为Sigmoid操作,然后通过计算输出层与标签之间的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]\tag{3-122} $$

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

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

在PyTorch中,可以通过torch.nn模块中的MultiLabelSoftMarginLoss类来完成损失的计算,示例代码如下所示:

1 def Sigmoid_loss(y_true, y_pred):
2     loss = nn.MultiLabelSoftMarginLoss(reduction='mean')
3     print(loss(y_pred, y_true))  # 0.5927
4 
5 if __name__ == '__main__':
6     y_true = torch.tensor([[1, 1, 0, 0], [0, 1, 0, 1]], dtype=torch.int16)
7     y_pred = torch.tensor([[0.2, 0.5, 0, 0], [0.1, 0.5, 0, 0.8]], dtype=torch.float32)
8     Sigmoid_loss(y_true, y_pred)

在上述代码中,第6~7行是构造了两个样本的预测结果和真实标签,且每个样本均有两个类别。同时,需要注意的是MultiLabelSoftMarginLoss默认返回的是所有样本损失的均值,我们可以通过指定参数reductionmeansum来指定返回的类型。

对于上述计算过程,我们还可以通过如下代码来进行完成:

1 l = -(y_true * torch.log(1 / (1 + torch.exp(-y_pred))) 
2       + (1 - y_true) * torch.log(torch.exp(-y_pred) / (1 + torch.exp(-y_pred)))).mean()

在完成模型的训练过程后,我们可以通过如下方式来得到模型的预测结果:

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

在上述代码中,第1行中K表示多标签的数量。运行结束以后,我们便可以得到如下所示结果:

1 预测标签 tensor([[1, 0], [3, 1]])
2 预测概率 [[0.5 0.2] [0.8 0.5]]

在上述输出结果中,第1~2行便是每个样本对应每个类别的标签,且是以概率值递减进行排序

3.13.2 交叉熵损失#

在衡量多标签分类损失的方法中,除了Sigmoid损失以外还有一种常用的损失函数。这种损失函数本质上是我们在单标签分类中用到的交叉熵损失函数的扩展版,单标签可以看作是其中的一种特例情况。其具体计算公式为:

$$ loss(y,\hat{y})=-\frac{1}{m}\sum_{i=1}^m\sum_{j=1}^qy^{(i)}_j\log{\hat{y}^{(i)}_j}\tag{3-123} $$

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

例如对于如下样本来说:

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

经过$\text{Softmax}$处理后的结果为:

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

此时,根据式(3-123)可知,对于上述2个样本来说其损失值为:

$$ loss= -\frac{1}{2}\left(1\cdot \log{(0.24549)}+1\cdot \log{(0.33138)}+1\cdot \log{(0.27573)} +1\cdot \log{(0.37219)}\right)\approx 2.392 \tag{3-124} $$

由于PyTorch中并没有直接提供对应的实现,所以我们需要自己动手实现,示例代码如下所示:

1 def cross_entropy(logits, y):
2     s = torch.exp(logits)
3     logits = s / torch.sum(s, dim=1, keepdim=True)
4     c = -(y * torch.log(logits)).sum(dim=-1)
5     return torch.mean(c)
6 
7 if __name__ == '__main__':
8     loss = cross_entropy(y_pred,y_true)
9     print(loss)# 2.392

在介绍完两种不同的损失度量方法后,我们再来看如何对多标签分类任务中模型的预测结果进行评估。根据多标签分类任务的性质来看,评估指标整体上可以分为两类:不考虑部分正确的评估指标和考虑部分正确的评估指标。下面我们开始分别进行介绍。

3.13.3 不考虑部分正确的评估指标#

1. 绝对匹配率

所谓绝对匹配率(Exact Match Ratio)是指,对于每一个样本来说除非每个标签的预测结果均正确,否则认为该样本的预测结果为错误。也就是说只有预测值与真实值完全相同的情况下才算预测正确,因此其计算公式为

$$ \text{MR}=\frac{1}{m}\sum_{i=1}^mI(y^{(i)}==\hat{y}^{(i)})\tag{3-125} $$

其中 $m$ 表示样本总数;$I(\cdot)$为指示函数(Indicator Function),当$y^{(i)}$完全等同于$\hat{y}^{(i)}$时取$1$,否则为$0$。

从式(3-125)可以看出,MR值越大,表示分类的准确率越高。

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

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

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

1 from sklearn.metrics import accuracy_score
2 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)})\tag{3-126} $$

此时,我们可以通过sklearn.metrics模块中的zero_one_loss方法来完成计算 [1],示例代码如下所示:

1 from sklearn.metrics import zero_one_loss
2 print(zero_one_loss(y_true,y_pred))# 0.66666

3.13.4 考虑部分正确的评估指标#

从上面的两种评估指标可以看出,不管是绝对匹配率还是0-1损失,两者在计算结果时都没有考虑部分正确的情况,而这对于模型的评估来说显然是不够准确的。例如,假设某个样本的正确标签为[1, 0, 0, 1],模型的预测标签为[1, 0, 1, 0]。可以看到,尽管模型没有把该样本的所有标签都预测正确,但是同样也预测正确了一部分。因此,一种可取的做法就是将部分预测正确的结果也考虑进去 [2]。

为了实现这一想法,文献 [3]中提出了在多标签分类场景下的准确率(Accuracy)、精确率(Precision)、召回率(Recall)和$F_1$值($F_1$-Measure)计算方法,整体思想类似于3.9节中的内容,下面我们逐一进行介绍。

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}\tag{3-127} $$

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

$$ \text{Acc} = \frac{1}{1+1+1}=\frac{1}{3}\tag{3-128} $$

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

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

其准确率为:

$$ \text{Accuracy}=\frac{1}{3}\times(\frac{1}{3}+\frac{2}{2}+\frac{0}{4})\approx0.4444\tag{3-129} $$

对于式(3-127)所示的计算过程来说,其对应的实现代码为 [4]:

1 def Accuracy(y_true, y_pred):
2     count = 0
3     for i in range(y_true.shape[0]):
4         p = sum(np.logical_and(y_true[i], y_pred[i]))
5         q = sum(np.logical_or(y_true[i], y_pred[i]))
6         count += p / q
7     return count / y_true.shape[0]
8 print(Accuracy(y_true, y_pred)) # 0.4444

2. 精确率

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

$$ \text{Precision} = \frac{1}{m} \sum_{i=1}^{m} \frac{\lvert y^{(i)} \cap \hat{y}^{(i)}\rvert}{\lvert \hat{y}^{(i)}\rvert}\tag{3-130} $$

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

$$ \text{Pre} = \frac{1}{1+1}=\frac{1}{2}\tag{3-131} $$

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

$$ \text{Precision} = \frac{1}{3}\times(\frac{1}{2}+\frac{2}{2}+\frac{0}{2})\approx0.5\tag{3-132} $$

对于式(3-130)所示的计算过程来说,其对应的实现代码为:

1 def Precision(y_true, y_pred):
2     count = 0
3     for i in range(y_true.shape[0]):
4         if sum(y_pred[i]) == 0:
5             continue
6         count += sum(np.logical_and(y_true[i], y_pred[i])) / sum(y_pred[i])
7     return count / y_true.shape[0]
8 print(Precision(y_true, y_pred))# 0.5

3. 召回率

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

$$ \text{Recall} = \frac{1}{m} \sum_{i=1}^{m} \frac{\lvert y^{(i)} \cap \hat{y}^{(i)}\rvert}{\lvert y^{(i)}\rvert} \tag{3-133} $$

56