개발/ML&DL

[추천시스템] 성능 평가 지표(pyspark) - Precision, Recall, Map, NDCG

wonpick 2023. 3. 11. 12:53

추천시스템에서 랭킹하는 방식으로 좋은 추천(랭킹)을 했는지에 대해 정량평가 할 수 있는 4가지 지표를 정리해보고자 한다. 

* 이하 코드는 함께 인턴했던 뢀뢀씨❤️와 함께 작성했습니다! 뢀뢀씨 보고 있다면 댓글 달아주세요

 

1. Precision/ Recall

https://becominghuman.ai/whats-recall-and-precision-4a801b1ac0da

Precision

  • K 개 추천했을때, 추천결과가 hit한 precision 평균을 의미
  • 순서가 중요하지 않다.
  • 모델이 10개를 추천했을때 사용자가 몇개를 봤냐

 

사실 pyspark에서는 프리시즌을 계산하는 모듈을 제공하는데 생각했던 수식과의 차이 때문에 직접 udf를 제작하게되었다. 

pyspark 모듈과 udf의 차이는 관련도(relevance)를 매기는 기준이 다르다. 
pyspark
 : label
udf : label[:k]
예시) 
prediction : [a, b, c, d, e]
label : [h, a, i, j, d]
pyspark의 관련도 : [O, X, X, O, X]
udf의 관련도
k=1 : [X]
k=2 : [O, X]
k=3 : [O, X, X]
k=4 : [O, X, X, X]
k=5: [O, X, X, O, X]

 

def calPrecision(prediction, label, k):
    p = min(len(prediction), k)
    return len(set(prediction[:p]).intersection(set(label[:p]))) / p

calPrecisionUDF = F.udf(calPrecision, FloatType())

Recall

  • K 개 추천했을때, 추천되어야했을 relevant한 item이 몇개 추천되었나?
  • 사용자가 100개를 클릭했을 때 모델이 몇개를 추천했냐
  • 순서가 중요하지 않다.

 

📌precision과 recall만 놓고 봤을 경우 개인화와 글로벌한 추천 했을 시 중요하게 생각하는 지표의 차이가 존재한다. 

  • 개인화의 경우에는 우리가 개인에 맞춰서 모델링을한것이기 때문에 얼마나 정확하게 정답을 맞췄는지가 중요해서 프리시즌이 중요
  • 평균(통계량)의 경우에는 coverage를 단순 채우기 위해서 사용한것으로, 개인정보가 없을때 추천을 안해줄 수는 없기때문에 제공하는 것이라서 리콜이 중요

2. MAP(Mean Average Precision)

def calAP(prediction, labels, k):
    if len(prediction)>k:
        prediction = prediction[:k]

    score = 0.0
    num_hits = 0.0

    for i,p in enumerate(prediction):
        if p in labels[:i+1] and p not in prediction[:i]:
            num_hits += 1.0
            score += num_hits / (i+1.0)

    if not labels:
        return 0.0

    if num_hits == 0:
        return 0.0
    else:
        return score / num_hits

calAPUDF = F.udf(calAP, FloatType())

 

3. NDCG (Normalized Discounted Cumulative Gain)

검색 분야에서 처음 등장하여 사용하던 지표이나, 추천시스템 영역에서도 많이 이용되고 있다. 

NDCG는 추천 순서에 특히 가중치를 둬서 스코어링을 하게 되기 때문에 ndcg 지표를 제일 중요하게 생각하여 평가를 진행하고 있다. 

 

  • relevance 바이너리로 했을 때? 선호 지표로 했을 때?
  • relevance scores는 주어진 대로 사용되며, IDCG를 계산할 때는 relevance scores를 내림차순으로 정렬하여 사용한다.
  • 앞 순서를 잘맞추고 뒤로 갈 수록 못 맞춘경우 k가 커질수록 ndcg값이 작아질 수 있다.

https://arize.com/blog-course/ndcg/

 

import numpy as np
import pyspark.sql.functions as F
from pyspark.sql.types import FloatType

#label score를 기준으로 relevance 만듦
def calNDCG(prd_pred, prd_label, rel_true, k=5):
    p = min(len(prd_label), k)
    prd_idx = {prd_label[i]: i for i in range(p)}
    
    rel_pred = [rel_true[prd_idx.get(prd_no, -1)] if prd_no in prd_idx else 0 for prd_no in prd_pred]
    
    p = min(len(rel_true), min(len(rel_pred), p))
    discount = 1 / (np.log2(np.arange(p) + 2))

    # relevance순으로 소팅, IDCG를 계산할 때는 relevance scores를 내림차순으로 정렬
    rel_true_sorted = sorted(rel_true[:p], reverse=True)
    
    idcg = np.sum(np.array(rel_true_sorted) * discount)
    dcg = np.sum(np.array(rel_pred[:p]) * discount)

    ndcg = dcg / idcg if idcg > 0 else 0
    return float(ndcg)

calNDCGUDF = F.udf(calNDCG, FloatType())

 

((결론)) 

prediction : [a, b, c, d, e]
label : [h, a, i, j, d]
relevance : [1,3,0,1,2]

 

추천결과는 모두 5개로 고정한다. 

  precision recall
map ndcg (0-1)
k=1 [X]  0 [X]  0 추천결과 hit했을 때
precision 평균 (추천5개)
0.0
k=2 [O, X] 1/2 [O, X] 1/5 0.82
k=3 [O, X, X] 1/3 [O, X, X] 1/5 유저1) 1,3,5 hit
(1+2/3+3/5)/3=(2.26)/3
0.75 0.82
k=4 [O, X, X, X] 1/4 [O, X, X, X] 1/5 유저2) 2,3 hit
(1/2+2/3)/2=
(1.16)/2
0.58 0.72
k=5 [O, X, X, O, X] 2/5 [O, X, X, O, X] 2/5 MAP 0.665 0.74

 

((그래프)) 

test_list = [0.021, 0.036, 0.045, 0.052, 0.055]
k_list=[1,2,3,4,5]
plt.plot(k_list, test_list, marker='.')
plt.xlabel('k')
plt.xlim([0.5, 5.5]) 
plt.ylabel('Average nDCG@k')
plt.grid()
plt.show()

실무를 통해 정량평가를 진행하면서 얻었던 엄청난 2가지 깨달음을 공유해보고 싶다. 

1. relevant하다는 것도 어떤 서비스의 UI/UX에 들어가있는지에 따라 다르다. 
2. 우리의 UI/UX에 따라 들어온 피드백 label 데이터의 성향을 고려하여 label 데이터 제작이 필요하다. 

이 두가지를 고려하여 label 데이터를 제작하고 평가해야된다는 것이다. 두가지의 경우에 대해서 사과로 예시를 들어보겠다. 

1번 예시) (사과를 사려고 하는 사람/ 과일을 사려고 하는 사람) 두명의 유저에게 추천을 했을 때 
사과를 사는 사람에게는 사과만을 보여줘야하는 반면, 과일을 사려는 사람에게는 사과/포도/감 등등의 것들을 보여줘도 무관하다.
다만 이때 서비스가 나가는 영역의 명이 어떻게 되어있는지, 어떤 목표로 추천을 하고 있는지를 고려하여 진행을 해야한다는 것이다. 

과일을 사려는 사람에게 사과를 추천하면서 다른 과일들을 추천하는 것이 대체재가 될 수도 있지만, 서비스 명이 "함께 구매하면 좋은 상품"이 되었을 시 대체재가 아니게 되기 때문이다. 

2번 예시) (사과를 사려고 하는 사람/ apple 제품을 구매하려는 사람) 두명의 유저에게 추천을 했을 때
사과를 사고 싶어서 apple이라고 검색했는데, apple 제품의 인기가 많아 과일보다는 애플 상품이 상위에 뜨는 경우 
학습데이터를 구축할때도 사과보다는 애플 제품의 피드백이 더 많아 이후 추천에도 그대로 영향을 미치게 된다. 

일반적인 기업들은 기존에 있던 로그를 학습 데이터를 구축하고 그 이후 시점의 n일 동안의 데이터를 레이블로 만들어서 학습을 하게 되는데, 이 레이블은 기업이 의도한대로 유저행동을 유도하기 때문에 데이터를 100% 믿고 label 데이터를 제작하고 평가하면 안된다는 말이다.

즉, 우리가 노출을 시키지 않아서 피드백이 안온걸수도 있는데, 항상 모델 개발하고 구축할때 데이터를 쭉 크롤링해서 가져다 쓰는게 아니고 그 시점에서 우리 서비스의 ui/ux가 어떻게 되어있는가, side effect에 의해서 발생하는 학습데이터의 특성을 고려해서 임의로 데이터를 집어 넣던지, 아니면 ui를 바꿔서 피드백을 받은 뒤의 데이터로 학습을 하던지 하는 방법을 고려해서 평가해야된다는 것이다.