前言
指标在机器学习或深度学习领域扮演着相当重要的角色。我们从根据问题选择指标开始,以了解特定模型的基线分数。 在本博客中,我们研究了多标签分类的最佳和最常用的指标,以及它们的不同之处。
接下来,让我们深入了解什么是多标签分类,以防万一您需要它。 如果我们有关于狗的特征的数据,我们可以预测它属于哪个品种和宠物类别。
在物体检测的情况下,多标签分类为我们提供了图像中所有对象的列表,如下图所示。我们可以看到,分类器检测到图像中的 3 个对象。 如果训练对象的总数为 4 个,则可以将其表示成如下列表[1 0 1 1]
(对应的对象为[狗、人、自行车、卡车]
)。这种分类被称为多标签分类。
多类别(Multiclass)分类和多标签(Multilabel)分类的区别:
- 多类别分类: 超过两个类别的分类任务。多类别分类假设每个样本属于且仅属于一个标签,例如一个水果可以是苹果或者是桔子,但是不能同时属于两者。
- 多标签分类: 给每个样本分配一个或多个标签。例如一个新闻可以既属于体育类,也属于文娱类。
多标签分类评估指标
适用于多标签分类的最常见的指标如下:
- Precision at k
- Avg precision at k
- Mean avg precision at k
- Sampled F1 Score
- Log Loss
让我们来看看这些指标的详细信息。
Precision at k (P@K)
给定实际类别和预测类别的列表,Precision@K定义为仅考虑前k个元素正确预测的数量除以每个预测类别的前k个元素。值的范围在 0 到 1 之间。
代码示例如下:
def patk(actual, pred, k): #we return 0 if k is 0 because # we can't divide the no of common values by 0 if k == 0: return 0 #taking only the top k predictions in a class k_pred = pred[:k] # taking the set of the actual values actual_set = set(actual) print(list(actual_set)) # taking the set of the predicted values pred_set = set(k_pred) print(list(pred_set)) # 求预测值与真实值得交集 common_values = actual_set.intersection(pred_set) print(common_values) return len(common_values)/len(pred[:k]) # defining the values of the actual and the predicted class y_true = [1 ,2, 0] y_pred = [1, 1, 0] if __name__ == "__main__": print(patk(y_true, y_pred,3)) 复制代码
运行结果如下:
K:3,真实值:{0, 1, 2}, 预测值:{0, 1}, 交集:{0, 1}, P@k:0.6666666666666666 0.6666666666666666 复制代码
Average Precision at K (AP@K)
它被定义为 k = 1 到 k 时, 所有Precision@K的平均值。 为了更清楚,让我们看一下代码。 值的范围在 0 到 1 之间。
import numpy as np def apatk(acutal, pred, k): #creating a list for storing the values of precision for each k precision_ = [] for i in range(1, k+1): #calculating the precision at different values of k # and appending them to the list precision_.append(patk(acutal, pred, i)) #return 0 if there are no values in the list if len(precision_) == 0: return 0 #returning the average of all the precision values return np.mean(precision_) #defining the values of the actual and the predicted class y_true = [[1,2,0,1], [0,4], [3], [1,2]] y_pred = [[1,1,0,1], [1,4], [2], [1,3]] if __name__ == "__main__": for i in range(len(y_true)): for j in range(1, 4): print( f""" y_true = {y_true[i]} y_pred = {y_pred[i]} AP@{j} = {apatk(y_true[i], y_pred[i], k=j)} """ ) print("-----------") 复制代码
运行结果如下:
K:1,真实值:{0, 1, 2}, 预测值:{1}, 交集:{1}, P@k:1.0 y_true = [1, 2, 0, 1] y_pred = [1, 1, 0, 1] AP@1 = 1.0 K:1,真实值:{0, 1, 2}, 预测值:{1}, 交集:{1}, P@k:1.0 K:2,真实值:{0, 1, 2}, 预测值:{1}, 交集:{1}, P@k:0.5 y_true = [1, 2, 0, 1] y_pred = [1, 1, 0, 1] AP@2 = 0.75 K:1,真实值:{0, 1, 2}, 预测值:{1}, 交集:{1}, P@k:1.0 K:2,真实值:{0, 1, 2}, 预测值:{1}, 交集:{1}, P@k:0.5 K:3,真实值:{0, 1, 2}, 预测值:{0, 1}, 交集:{0, 1}, P@k:0.6666666666666666 y_true = [1, 2, 0, 1] y_pred = [1, 1, 0, 1] AP@3 = 0.7222222222222222 ----------- K:1,真实值:{0, 4}, 预测值:{1}, 交集:set(), P@k:0.0 y_true = [0, 4] y_pred = [1, 4] AP@1 = 0.0 K:1,真实值:{0, 4}, 预测值:{1}, 交集:set(), P@k:0.0 K:2,真实值:{0, 4}, 预测值:{1, 4}, 交集:{4}, P@k:0.5 y_true = [0, 4] y_pred = [1, 4] AP@2 = 0.25 K:1,真实值:{0, 4}, 预测值:{1}, 交集:set(), P@k:0.0 K:2,真实值:{0, 4}, 预测值:{1, 4}, 交集:{4}, P@k:0.5 K:3,真实值:{0, 4}, 预测值:{1, 4}, 交集:{4}, P@k:0.5 y_true = [0, 4] y_pred = [1, 4] AP@3 = 0.3333333333333333 ----------- K:1,真实值:{3}, 预测值:{2}, 交集:set(), P@k:0.0 y_true = [3] y_pred = [2] AP@1 = 0.0 K:1,真实值:{3}, 预测值:{2}, 交集:set(), P@k:0.0 K:2,真实值:{3}, 预测值:{2}, 交集:set(), P@k:0.0 y_true = [3] y_pred = [2] AP@2 = 0.0 K:1,真实值:{3}, 预测值:{2}, 交集:set(), P@k:0.0 K:2,真实值:{3}, 预测值:{2}, 交集:set(), P@k:0.0 K:3,真实值:{3}, 预测值:{2}, 交集:set(), P@k:0.0 y_true = [3] y_pred = [2] AP@3 = 0.0 ----------- K:1,真实值:{1, 2}, 预测值:{1}, 交集:{1}, P@k:1.0 y_true = [1, 2] y_pred = [1, 3] AP@1 = 1.0 K:1,真实值:{1, 2}, 预测值:{1}, 交集:{1}, P@k:1.0 K:2,真实值:{1, 2}, 预测值:{1, 3}, 交集:{1}, P@k:0.5 y_true = [1, 2] y_pred = [1, 3] AP@2 = 0.75 K:1,真实值:{1, 2}, 预测值:{1}, 交集:{1}, P@k:1.0 K:2,真实值:{1, 2}, 预测值:{1, 3}, 交集:{1}, P@k:0.5 K:3,真实值:{1, 2}, 预测值:{1, 3}, 交集:{1}, P@k:0.5 y_true = [1, 2] y_pred = [1, 3] AP@3 = 0.6666666666666666 ----------- 复制代码
Mean avg precision at k (MAP@K)
整个训练数据上 AP@k 的所有值的平均值称为 MAP@k。这有助于我们准确表示整个预测数据的准确性。值的范围在 0 到 1 之间。
AP@k衡量的是学出来的模型在每个类别上的好坏,MAP@k衡量的是学出的模型在所有类别上的好坏 代码示例如下:
import numpy as np def mapk(acutal, pred, k): #creating a list for storing the Average Precision Values average_precision = [] #interating through the whole data and calculating the apk for each for i in range(len(acutal)): ap = apatk(acutal[i], pred[i], k) print(f"AP@k: {ap}") average_precision.append(ap) #returning the mean of all the data return np.mean(average_precision) #defining the values of the actual and the predicted class y_true = [[1,2,0,1], [0,4], [3], [1,2]] y_pred = [[1,1,0,1], [1,4], [2], [1,3]] if __name__ == "__main__": print(mapk(y_true, y_pred,3)) 复制代码
运行结果如下:
K:1,真实值:{0, 1, 2}, 预测值:{1}, 交集:{1}, P@k:1.0 K:2,真实值:{0, 1, 2}, 预测值:{1}, 交集:{1}, P@k:0.5 K:3,真实值:{0, 1, 2}, 预测值:{0, 1}, 交集:{0, 1}, P@k:0.6666666666666666 AP@k: 0.7222222222222222 K:1,真实值:{0, 4}, 预测值:{1}, 交集:set(), P@k:0.0 K:2,真实值:{0, 4}, 预测值:{1, 4}, 交集:{4}, P@k:0.5 K:3,真实值:{0, 4}, 预测值:{1, 4}, 交集:{4}, P@k:0.5 AP@k: 0.3333333333333333 K:1,真实值:{3}, 预测值:{2}, 交集:set(), P@k:0.0 K:2,真实值:{3}, 预测值:{2}, 交集:set(), P@k:0.0 K:3,真实值:{3}, 预测值:{2}, 交集:set(), P@k:0.0 AP@k: 0.0 K:1,真实值:{1, 2}, 预测值:{1}, 交集:{1}, P@k:1.0 K:2,真实值:{1, 2}, 预测值:{1, 3}, 交集:{1}, P@k:0.5 K:3,真实值:{1, 2}, 预测值:{1, 3}, 交集:{1}, P@k:0.5 AP@k: 0.6666666666666666 0.4305555555555556 复制代码
这里分数很差,因为预测集有很多错误。
Sampled F1 Score
该指标先计算数据中每个实例的 F1 分数,然后计算 F1 分数的平均值。
我们将在代码中使用 sklearn 的实现。计算 F1 分数的文档请参考这里。 值的范围在 0 到 1 之间。
我们首先将数据转换为0-1
格式,然后对其计算F1分数。
代码示例如下:
from sklearn.metrics import f1_score from sklearn.preprocessing import MultiLabelBinarizer def f1_sampled(actual, pred): # converting the multi-label classification to a binary output mlb = MultiLabelBinarizer() actual = mlb.fit_transform(actual) pred = mlb.fit_transform(pred) print(f"多标签二值化后的标签值:{mlb.classes_}") print(f"真实值:\n{actual} \n预测值:\n{pred}") # fitting the data for calculating the f1 score f1 = f1_score(actual, pred, average = "samples") return f1 # defining the values of the actual and the predicted class # 总共有五个类别 y_true = [[1,2,0,1], [0,4], [3], [1,2]] y_pred = [[1,1,0,1], [1,4], [2], [1,3]] if __name__ == "__main__": print(f1_sampled(y_true, y_pred)) 复制代码
运行结果如下:
多标签二值化后的标签值:[0 1 2 3 4] 真实值: [[1 1 1 0 0] [1 0 0 0 1] [0 0 0 1 0] [0 1 1 0 0]] 预测值: [[1 1 0 0 0] [0 1 0 0 1] [0 0 1 0 0] [0 1 0 1 0]] 0.45 复制代码
我们知道 F1 分数介于 0 和 1 之间,这里我们得到了 0.45 的分数。 这是因为预测集不好。 如果我们有更好的预测集,该值将更接近 1。
Log Loss
Log Loss,中文名为对数损失,又名逻辑损失或交叉熵损失。
这种误差损失衡量方式其实就是在逻辑回归中用来衡量预测概率与真实标签之间误差的方法。
其公式为:
loss(x,y)=−1C∗∑iy[i]∗log(1(1+exp(−x[i])))+(1−y[i])∗log(exp(−x[i])(1+exp(−x[i])))loss(x, y) = - \frac{1}{C} * \sum_i y[i] * \log( \frac{1}{(1 + \exp(-x[i]))}) + (1-y[i]) * \log\left(\frac{\exp(-x[i])}{(1 + \exp(-x[i]))}\right)loss(x,y)=−C1∗i∑y[i]∗log((1+exp(−x[i]))1)+(1−y[i])∗log((1+exp(−x[i]))exp(−x[i]))
其中 i∈{0, ⋯ , x.nElement()−1},y[i]∈{0, 1}。i \in \left\{0, \; \cdots , \; \text{x.nElement}() - 1\right\}, y[i] \in \left\{0, \; 1\right\}。i∈{0,⋯,x.nElement()−1},y[i]∈{0,1}。
公式中x相当于预测值,y相当于真实值。
首先,你需要将标签进行二值化处理,然后对每一列使用对数损失。最后,您可以取每列中对数损失的平均值,这也被称为平均列对数损失。当然,您还可以通过其他方式实现。
代码示例如下:
import numpy as np # y_test = np.random.randint(0,2,(3,4)) # y_pred = np.random.random((3,4)) y_test = 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(f"{y_test}\n{y_pred}") def sigmoid(z): return 1 / (1 + np.exp(-z)) def log_loss(y_test,y_pred): y_test = y_test.astype(np.float16) y_pred = y_pred.astype(np.float16) N,M = y_test.shape print(f"列-M:{M}, 行-N:{N}") a=[] for m in range(M): loss=0 for i in range(N): loss -= (y_test[i,m]*np.log(sigmoid(y_pred[i,m])))+((1.0-y_test[i,m])*np.log(1.0-sigmoid(y_pred[i,m]))) loss = loss/N a.append(round(loss,8)) print(f"每一列的损失如下:{a}") return np.mean(a) a = log_loss(y_test,y_pred) print(a) 复制代码
运行结果如下:
[[1 1 0 0] [0 1 0 1]] [[0.2 0.5 0. 0. ] [0.1 0.5 0. 0.8]] 列-M:4, 行-N:2 每一列的损失如下:[0.67131506, 0.47402386, 0.69314718, 0.53217012] 0.592664055 复制代码
上面的代码是对每一列求对数损失,我们也可以针对每一个样本(行)求对数损失,结果一致。具体代码如下:
import numpy as np 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) # 得到每个样本的损失值 print(f"每个样本的损失如下:{loss}") 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 复制代码
运行结果如下:
每个样本的损失如下:[-0.61462755 -0.57068037] 0.5926539631803737 复制代码
对应的PyTorch实现代码如下:
import torch import torch.nn as nn 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 复制代码
运行结果如下:
tensor(0.5927) 复制代码
总结
针对多标签分类场景的问题,我们通常使用 MAP@K 、 Sampled F1 Score或Log Loss来为您的模型设置评估指标。
参考文档
- Metrics for Multi-Label Classification
- 多标签分类中的损失函数与评价指标
- Pytorch MultiLabelSoftMarginLoss
- sklearn multiclass-and-multilabel-classification
- Approaching (Almost) Any Machine Learning Problem