之前在排序项目中用到了nDCG,其离线指标对模型训练及上线具有一定的参考意义,在此做一个总结。因为计算是在集群的hive表进行的,所以加上了spark上的计算代码。
有以下例子:
| 位置(iii) | 预测值排序 | 真实的相关性分(relirel_ireli) | 折损值(log2(i+1)log_2(i+1)log2(i+1)) | 折损后的相关性(reli/log2(i+1)rel_i/log_2(i+1)reli/log2(i+1)) |
|---|---|---|---|---|
| 1 | D1 | 3 | 1 | 3 |
| 2 | D2 | 2 | 1.585 | 1.262 |
| 3 | D3 | 3 | 2 | 1.5 |
| 4 | D4 | 0 | 2.322 | 0 |
| 5 | D5 | 1 | 2.585 | 0.387 |
| 6 | D6 | 2 | 2.807 | 0.712 |
CG(Cumulative Gain)是结果列表中所有 items 的相关性得分的总和。CGk\text{CG}_kCGk是前 kkk 个 items 的相关性得分的总和。
CGk=∑i=1kreli\text{CG}_k=\sum_{i=1}^{k}rel_i CGk=i=1∑kreli
比如上面表格中的 CG1=3\text{CG}_1=3CG1=3, CG2=5\text{CG}_2=5CG2=5, CG3=8\text{CG}_3=8CG3=8…
缺点:CG 忽略了位次的重要性,比如CG3=8\text{CG}_3=8CG3=8的序列有{(3,3,2), (3,2,3), (2,3,3)},但最优的序列是将最大值都排在前面的序列,如(3,3,2)。
DCG(Discounted CG),折损累计收益。因为CG的缺点是不能区分位次,所以将位次作为折损,位次越靠后,折损越大,所以DCG的计算为:
DCGk=∑i=1krelilog2(i+1)\text{DCG}_k=\sum_{i=1}^{k}{\frac {rel_i} {log_2(i+1)}} DCGk=i=1∑klog2(i+1)reli
通常在排序中,nDCG(normalized DCG)是使用最多的。即对DCG进行归一化,归一化就是理想的排序结果,即相关性最大的排在前面,其DCG成为IDCG(Ideal DCG)。
nDCGk=DCGIDCG\text{nDCG}_k={\frac {DCG} {IDCG}} nDCGk=IDCGDCG
def get_dcg(y_true, y_scores, k):'''@pred_arr: 预测顺序的相关度,ndarray,shape=(n,1)@gt_arr: 实际顺序相关度,ndarray,shape=(n,1)@k: 要计算的最大位置,int'''arr = [x[1] for x in sorted(zip(y_scores, y_true), reverse=True)[:k]]weights = np.power(np.log2(range(2,len(arr)+2)), -1)dcg = np.sum(arr*weights)return dcgdef get_ndcg(y_true, y_scores, k):dcg = get_dcg(y_true, y_scores, k)idcg = get_dcg(sorted(y_true), np.arange(len(y_true)), k)return dcg/idcg
使用方式可参考我的博文sklearn.metrics模块重要API的原理与应用总结 的“DGC和nDCG”一节。
随机生成不同数量的样本,查看两个函数的运行时长。
m = 100 # 样本量
y_true = np.random.randint(0,1000000,m).reshape(1,-1)
y_scores = np.random.randint(0,1000000,m).reshape(1,-1)k = 10 # 前k个最高排名的ndcg
ndcg1 = get_ndcg(y_true[0], y_scores[0], k=k)
ndcg2 = ndcg_score(y_true, y_scores, k=k)# 在jupyter中执行
"""
%%timeit
get_ndcg(y_true[0], y_scores[0], k=k)%%timeit
ndcg_score(y_true, y_scores, k=k)
"""
| 样本量(m) | 自己的代码 | sklearn |
|---|---|---|
| 10000 | 22.7 ms | 5.61 ms |
| 5000 | 10.5 ms | 3.06 ms |
| 3000 | 5.99 ms | 1.93 ms |
| 2000 | 3.79 ms | 1.44 ms |
| 1000 | 1.81 ms | 0.95 ms |
| 500 | 896 µs | 711 µs |
| 400 | 717 µs | 639 µs |
| 350 | 631 µs | 618 µs |
| 300 | 547 µs | 588 µs |
| 250 | 462 µs | 558 µs |
| 100 | 212 µs | 458 µs |
可以看到,当样本数量较大时,使用sklearn提供的函数速度较。值得注意的是,自己写的代码还没有优化,与复杂度与数据量大致成线性增长,可以使用前k大个数算法进行优化,进一步缩短时长。
两种代码之间的误差主要来自于对相同y_scores的值不同的排序位次导致的,比如:
y_scores = [0.9, 0.5, 0.6, 0.9, 0.9]
y_true = [7, 4, 1, 0, 0]
根据y_scores,y_true的排序方式及相应的ndcg为:
| 相关性排序 | Value |
|---|---|
| [7,0,0,1,4] | 0.896 |
| [0,7,0,1,4] | 0.638 |
| [0,0,7,1,4] | 0.547 |
如果y_score保留小数点后6位的话,两个方法的误差将在10−610^{-6}10−6数量级。