推荐系统排序指标的使用及其代码实现
这篇文章介绍了推荐系统中常用的排序指标,包括HitRate、MRR、NDCG、Precision、Recall和F-score。每个指标都有相应的公式表示和计算方法,并通过代码展示了如何在测试数据集上计算这些指标的数值。这些指标能够评估推荐系统的性能,从不同角度全面评估推荐结果的准确性和覆盖度。
目录
- 1前情提要
- [2 HitRate](#2 HitRate "#2-HitRate")
- [3 MRR](#3 MRR "#3-MRR")
- [4 NDCG](#4 NDCG "#4-NDCG")
- [5 Precision](#5 Precision "#5-Precision")
- [6 Recall](#6 Recall "#6-Recall")
- [7 F-score](#7 F-score "#7-F-score")
1前情提要
1.1测试数据集
python
# 生成一个虚假的test_df,其中包含user_id, item_id, label,pre_col五列
import pandas as pd
import numpy as np
test_df = pd.DataFrame()
# user_id定义3个用户,每个用户对应十个item_id
test_df['user_id'] = [1]*10 + [2]*10 + [3]*10
test_df['item_id'] = np.random.randint(1, 100, 30)
# pre_col随机生成
test_df['pre_col'] = np.random.rand(30)
# 为每个用户的每个item_id生成一个ranking
test_df['ranking'] = test_df.groupby('user_id')['pre_col'].rank(ascending=False, method='first')
# 按照user_id和ranking排序
test_df = test_df.sort_values(by=['user_id', 'ranking'])
# 生成label列
test_df['label'] = [1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0]
# 去除ranking列
test_df = test_df.drop(columns=['ranking'])
python
# 默认的col_name
col_name = {
'user_col':'user_id',
'item_col':'item_id',
'label_col':'label',
'pre_col':'pre_col',
}
使用虚拟生成的测试数据集,其中user_id表示用户的id,item_id表示物品的id,label为真实标签,pre_col为预测推荐的概率,具体的表现形式展示如下。
1.2文章说明
在图片展示中橙色表示物品被真实浏览或者点击过,灰色表示物品没有被浏览或者点击过,图片中的物品已经按照预测的概率从大到小进行排序。代码部分的test_df需要输入测试数据集,其中需要包括对应的列,列名通过col_name参数进行传递,K则是限定的线。
1.3相关问题
-
为什么这些指标都是计算每个用户的指标后在除以用户数?
这是为了得到综合的系统性能评估,考虑到不同用户群体的需求和行为差异,同时也不能保证每个用户返回的推荐列表的长度都是一样的。
-
为什么要设置K,而不是使用返回的全部推荐数据进行评估?
因为推荐系统可以会一次性返回成百上千的数据,但是用户真正浏览的数据并没有那么多,因此只需要返回预测程度最大的那么K个即可,而前K个数据的评估指标才可以更好的反应出推荐系统的质量。
2 HitRate
HitRate表示前K个项目中公至少有一个相关项目的命中率,可以直观的反应出推荐系统的命中率,可以很好的比较不同的模型,但是他只考虑了是否命中物品,而没有考虑命中的具体次数和位置,可能忽略了用户对不同推荐物品的偏好程度。
2.1公式表示
HitRate=测试集中用户数量测试集中命中用户的数量
2.2举例说明
当K为4的时候,只有用户2没有命中,另外两个用户都存在命中的物品,因此两个计算结果如下。
HitRate=32
有人可能会问,为什么用户的推荐列表中有一个真实喜欢的数据就算命中,而不是根据物品是否命中来进行计算的,这是因为基于物品进行计算的指标叫做6 Recall。
2.3代码展示
python
def hitrate_user(self, test_df: pd.DataFrame, col_name: dict, k: int = 20):
"""基于命中用户的评估指标
Args:
test_df (pd.DataFrame): 测试集
col_name (dict): 列名,包含user_col, pre_col, label_col
k (int, optional): 排序数量. Defaults to 20.
Returns:
hitrate (int): 命中用户数量比例
"""
test_df['ranking'] = test_df.groupby(col_name['user_id'])['pre_col'].rank(ascending=False, method='first')
test_gd_df = test_df[test_df['ranking'] <= k].reset_index(drop=True)
test_gd_df = test_gd_df[test_gd_df['label'] == 1]
return test_gd_df[col_name['user_id']].nunique() / test_df[col_name['user_id']].nunique()
3 MRR
衡量系统在给定查询中对相关结果的排序质量,更重视排名靠前的正确答案。MRR的结果越大越好,即假如每次预测都是第一个预测成功,那么MRR的结果就是1。
MRR适合只有一个正确答案的场景,当然这也就意味着MRR过于强调排名第一的物品是否与用户实际兴趣匹配,忽略了其他命中位置的影响。
3.1公式表示
MRR=N1i=1∑Nranki1
N表示总共的查询数
ranki表示第 i个查询结果中正确答案的排名(越小越好)
3.2举例说明
上面有三个用户,第一个的RR为1,第二个没有匹配成功因此为0,第三个用户在第三个才匹配到,因此RR为 1/3,最终的计算结果如下。
MRR=31×(1+0+31)=94
3.3代码展示
python
def mrr(self, test_df: pd.DataFrame, col_name: dict, k: int = 20):
"""Mean Reciprocal Rank
Args:
test_df (pd.DataFrame): 测试集
col_name (dict): 列名,包含user_col, pre_col, label_col
k (int, optional): 排序数量. Defaults to 20.
Returns:
mrr (int): 平均倒数排名
"""
test_df['ranking'] = test_df.groupby(col_name['user_col'])[col_name['pre_col']].rank(ascending=False, method='first')
test_gd_df = test_df[(test_df['ranking'] <= k) & (test_df[col_name['label_col']] == 1)].reset_index(drop=True)
test_gd_df = test_gd_df.sort_values(by=[col_name['user_col'], 'ranking'], ascending=[True, True])
test_gd_df = test_gd_df.drop_duplicates(subset=[col_name['user_col']], keep='first').reset_index(drop=True)
test_gd_df['mrr'] = 1 / test_gd_df['ranking']
return test_gd_df['mrr'].sum() / test_df[col_name['user_col']].nunique()
4 NDCG
贴现累积收益 (DCG) 是衡量信息检索质量的指标。它通常被规范化,以便在查询之间具有可比性,从而给出规范化 DCG(nDCG 或 NDCG)。NDCG 通常用于衡量搜索引擎算法和相关应用程序的有效性。DCG 使用搜索引擎结果集中文档的分级相关性量表,将结果的有用性或收益相加,并按其在结果列表中的位置进行折扣。 NDCG 是 DCG 归一化,当从最高到最低增益排序时,由结果集的最大可能 DCG 归一化,从而针对不同查询的不同数量的相关结果进行调整。
4.1公式表示
CGp=i=1∑preli
DCG1=i=1∑Nlog2(i+1)reli
DCG2=i=1∑plog2(i+1)2rel i−1
IDCG=i=1∑Nlog2(i+1)rel(i)(理想情况下的DCG)
NDCG@K=IDCG@KDCG@K
上面的公式中 reli表示第 i个结果的相关性得分, N表示总共的结果数,需要注意这里 i是从1开始计算的,如果在代码中从0开始计算记得修改分母部分,同时上面还展示了两种不同的DCG的计算公式,后者比起前者来说更加的注重相关系数,主要用于工业界,下文中的计算则主要使用前者,需要注意如果DCG使用后者进行计算,IDCG也要随之发生变化。
要计算NDCG首先要计算DCG和IDCG,DCG的全称是Discounted Cumulative Gain,IDCG的全称是Ideal Discounted Cumulative Gain,顾名思义,IDCG就是理想状态下的DCG,下面会重点介绍一下什么是相关性得分?什么是Cumulative Gain?以及为什么在计算了DCG之后还要计算NDCG?
4.2举例说明
4.2.1 CG
首先先来明确一下什么是相关性得分,在推荐系统中,你可以根据是否点击或者浏览来判断该物品是否推荐成功,如果二元相关的状况,那么相关性得分就是0和1,0就表示用户和物品没有发生交互,1则表示用户和物品发生了交互。当然还可以有其他情况,例如用户浏览之后相关性可以记作1,如果在浏览之后点击了收藏则可以记作2,在收藏之后进行了购买操作则可以记作3,可以通过不断细化颗粒度来更好的确定相关性,从而进一步的优化推荐系统。不过后文中的内容均以二元相关的情况来举例。
如果通过上面的情况来看,直接计算CG可以得到用户1和用户3的Cumulative Gain都是3,但是实际上,用户1推荐的情况应该更符合逻辑,因为推荐的物品在真实情况下被优先浏览,所以和MRR一样,需要引入排名的概念,即Discounted部分。
4.2.2 DCG
按照上面的公式进行计算,可以得到对应用户的DCG得分,计算结果如下。
DCGu1=1+log2(3)1+log2(4)1=2.13
DCGu2=0
DCGu3=log2(4)1+log2(6)1+log2(7)1=1.24
DCG=N∑i=1NDCGui=32.13+1.24=1.12
从上面的计算结果来看,DCG可以很好的解决CG计算过程中不注重排名的问题,但是直接使用DCG进行计算可能会导致不同推荐列表长度之间的比较困难,因为较长的推荐列表往往会有更高的DCG值。这可能使得排名算法在评估时偏向于较长的推荐列表,而忽略了推荐内容的质量和相关性。因此,除了DCG,还需要使用其他指标或者将DCG进行归一化处理,如使用NDCG(normalized Discounted Cumulative Gain)来解决这个问题。
4.2.3 IDCG
通过将DCG除以IDCG能够得到IDCG,可以考虑到特定推荐列表的理想累积增益,从而确保在不同推荐列表长度和内容质量之间进行公平比较和归一化处理,下面来介绍一下什么是IDCG。
IDCG(Ideal Discounted Cumulative Gain)是指在一个理想情况下,根据相关性对推荐结果进行排序后得到的累积增益值。它表示在最佳排序情况下,用户能够获得的最大累积增益,用于计算推荐系统的性能指标。
依旧是上面的这个例子,但是和上面的情况有所不同,可以发现所有橙色的块都移动到顶部了,因为当K按照6进行截断之后,用户1和用户3的理想推荐情况应该是上面这种情况,当然这里考虑的是二元的情况,如果多元的相关性那么就需要按照相关性的大小进行排序,下面计算一下IDCG。
IDCGu1=1+log2(3)1+log2(4)1=2.13
IDCGu2=0
IDCGu3=1+log2(3)1+log2(4)1=2.13
IDCG=N∑i=1NIDCGui=32.13+2.13=1.42
4.2.4 NDCG
上面计算了当K为6的时候的DCG和IDCG,就可以根据下面的公式计算出NDCG了。
NDCG@6=IDCG@6DCG@6=1.421.12=0.79
4.3代码展示
python
def ndcg(self, test_df: pd.DataFrame, col_name: dict, k: int = 20):
test_df['ranking'] = test_df.groupby(col_name['user_col'])[col_name['pre_col']].rank(ascending=False, method='first')
test_gd_df = test_df[(test_df['ranking'] <= k) & (test_df[col_name['label_col']] == 1)].reset_index(drop=True)
ndcg_values = [] # 保存每个用户的dcg和idcg
for user_id, user_group in test_gd_df.groupby(col_name['user_col']):
dcg = sum(user_group[col_name['label_col']] / np.log2(user_group['ranking'] + 1))
labels_sorted = user_group[col_name['label_col']].sort_values(ascending=False)
idcg = sum(labels_sorted / np.log2(range(2, len(labels_sorted) + 2)))
ndcg_values.append((user_id, dcg, idcg))
ndcg_values = np.array(ndcg_values)
ndcg_values = ndcg_values[ndcg_values[:, 2] != 0] # 防止出现0除
return np.mean(ndcg_values[:, 1] / ndcg_values[:, 2])
4.4资料
1.https://en.wikipedia.org/wiki/Discounted_cumulative_gain#cite_note-4
5 Precision
5.1公式表示
Precision=TP+FPTP
TP(True Positive)表示被正确预测为正例的样本数量。
FP(False Positive)表示被错误预测为正例的样本数量。
5.2举例说明
Precisionu1=63
Precisionu1=60
Precisionu3=63
Precision=N∑i=1NPrecisionui=30.5+0.5=0.3333
通过上面的计算可以知道,Precision指标的计算并不会考虑位置信息,即用户1和用户3的指标都是一样的,因此如果需要考虑排名信息,可以参考使用3 MRR或者4 NDCG这种加入排名数据的指标进行计算。
同时注意到K对于指标的计算有很大的影响,例如当K为3的时候,用户1的Precision为1,即全部正确,而且如果返回的推荐数据只有10条,但是设置了K大于10,例如上图中的例子设置为12,那么就相当于自动生成了2条数据,这会进一步降低Precision的值,因此K的设置是非常重要的,要根据具体的情况进行设置。
综上所诉,如果你只是查看模型运行结果的准确性而不考虑排名信息,那么Precision指标是合适;如果有众多的候选推荐物品,而你只是选择少量的物品进行推荐,那么Precision指标是合适的。
5.3代码展示
python
def precision(self, test_df: pd.DataFrame, col_name: dict, k: int = 20):
"""Precision@k
Args:
test_df (pd.DataFrame): 测试集
col_name (dict): 列名,包含user_col, pre_col, label_col
k (int, optional): 排序数量. Defaults to 20.
Returns:
precision (int): 平均准确率
"""
test_df['ranking'] = test_df.groupby(col_name['user_col'])[col_name['pre_col']].rank(ascending=False, method='first')
test_gd_df = test_df[test_df['ranking'] <= k].reset_index(drop=True)
return test_gd_df[col_name['label_col']].sum() / test_gd_df.shape[0]
6 Recall
预测正确的相关结果占所有相关结果的比例,取值范围 0,1,越大越好。
6.1公式表示
Recall@k=TP@k+FN@kTP@k
TP表示在前k个预测中被正确预测为正类别的样本数。
FN表示在前k个预测中实际为正类别但被错误地预测为负类别的样本数。
6.2举例说明
Recall@4u1=43
Recall@4u1=20
Recall@4u3=41
Recall@4=N∑i=1NRecall@4ui=30.75+0.25=0.3333
以用户1为例,当K等于4的时候,前四个推荐中就有三个项目被真实推荐,而推荐的10个项目中有四个被被真实推荐,应该reacll就是 43。
综上所诉,和Precision一样,Recall也能够全面评估系统检索到的相关物品占所有相关物品的比例,提供了系统发现相关物品的能力,但是不关注相关物品的排名顺序,可能忽视了用户对排名靠前物品的偏好,同时只考虑检索到的相关物品数量,无法反映推荐物品的准确性和质量。
6.3代码展示
python
def recall(self, test_df: pd.DataFrame, col_name: dict, k: int = 20):
"""Recall@k
Args:
test_df (pd.DataFrame): 测试集
col_name (dict): 列名,包含user_col, pre_col, label_col
k (int, optional): 排序数量. Defaults to 20.
Returns:
recall (int): 平均召回率
"""
recalls = []
for _, user_group in test_df.groupby(col_name['user_col']):
user_group['ranking'] = user_group[col_name['pre_col']].rank(ascending=False, method='first')
user_relevant_items = user_group[user_group['ranking'] <= k]
recall = user_relevant_items[col_name['label_col']].sum() / user_group[col_name['label_col']].sum()
recalls.append(recall)
return sum(recalls) / len(recalls)
7 F-score
F-score综合衡量了 precision 和 recall两个指标,帮助找到平衡的precision和recall值,在二元分类问题中很有用。F-score用户用于评估分类模型的综合性能,特别是在数据不平衡的情况下更具有说服力。结果在 0,1,数值越大意味着整体性能越好。
7.1公式表示
Fβ=(β2⋅precision)+recall(1+β2)⋅precision⋅recall
其中:
precision=TP+FPTP
recall=TP+FNTP
其中,TP表示真正例(True Positives),FP表示假正例(False Positives),FN表示假负例(False Negatives)。系数 β 用于调节对 precision 和 recall 的相对重视程度。
7.2举例说明
这里就不进行图示的展示了,按照上面两个指标进行计算即可。常用的 β为1,而当 β为1的时候,这样计算出来的F-score被称为"F1-score"。
7.3代码展示
python
def Fscore(self, test_df: pd.DataFrame, col_name: dict, k: int = 20, beta: int = 1):
"""F-score@k
Args:
test_df (pd.DataFrame): 测试集
col_name (dict): 列名,包含user_col, pre_col, label_col
k (int, optional): 排序数量. Defaults to 20.
beta (int, optional): beta值. Defaults to 1.
Returns:
fscore (int): 平均F-score
"""
fscores = []
for _, user_group in test_df.groupby(col_name['user_col']):
user_group['ranking'] = user_group[col_name['pre_col']].rank(ascending=False, method='first')
user_relevant_items = user_group[user_group['ranking'] <= k]
precision = user_relevant_items[col_name['label_col']].sum() / k
recall = user_relevant_items[col_name['label_col']].sum() / user_group[col_name['label_col']].sum()
if precision + recall == 0:
fscores.append(0)
else:
fscores.append((1 + beta**2) * precision * recall / (beta**2 * precision + recall))
return sum(fscores) / len(fscores)
原文链接:www.wolai.com/wyx-hhhh/oi...
个人博客:wyxhhhh.cn/
欢迎与我进行交流,如果要联系我可以在个人博客中查看我的信息,如果有任何问题也可以直接私信或者评论。