추천 시스템 평가지표 3. RR
Intro
“정답을 얼마나 빨리 맞췄는가”
이전 포스팅들에서는 Precision 과 Recall 을 통해, 추천시스템이 얼마나 정확하고, 얼마나 빠짐없이 정답을 잘 재현했는지를 살펴봤습니다. 이 지표들은 추천리스트의 아이템 개수와 정답 아이템의 개수, 그리고 그 교집합을 통해 “얼마나 많이” 맞추거나 재현했는지를 살펴보는 지표들입니다.
하지만 다음과 같은 상황에서는, 이 두 지표가 힘을 잃습니다.
- 검색 결과에서는 상위의 결과들의 클릭률이 압도적으로 높다.
- Q&A(질문과 답변) 서비스에서는 가장 위에 노출된 답변 하나가 Q&A의 신뢰도를 좌우한다.
즉, 얼마나 많이 맞췄는지도 중요하지만, 정답을 얼마나 빠르게 맞췄는지가 더 중요한 경우들이 존재합니다. 그리고 이걸 평가할 수 있는 지표가 바로 RR입니다.
개념
RR은 추천 리스트에서 가장 먼저 등장한 정답의 순위만을 평가하는 지표입니다. 정답이 추천 리스트에서 앞쪽에 등장할수록 높은 점수를 받으며, 처음 등장하는 정답의 순위가 뒤로 갈수록 점수는 급격히 낮아집니다.
- Reciprocal Rank
- “정답” 데이터가 “추천 리스트” 에서 몇 번째에 등장했는지
- 즉, 사용자가 선호하는 아이템이 얼마나 빨리 추천 리스트에 처음 등장했는지에 대한 지표이다.
- 정답이 처음 등장한 순번의 역수로 계산
Reciprocal : 역수
Rank : 순위
특징
- 여러 개의 정답이 있더라도 두 번째 정답부터는 점수에 전혀 반영하지 않는 극단적인 랭킹 지표
사용하는 경우
- 사용자가 단 하나의 정답만 찾으면 탐색을 바로 끝내는 서비스에 적합
- 검색에서 “오늘 날씨”를 검색하거나, 지식인에서 정답 답변을 찾는 경우
- 혹은 “유튜브에서 특정 가수 이름”을 검색하는 경우
- “내가 찾는 바로 그것”이 상단에 바로 나와야 하는 서비스에서 메인 지표로 활용
수식
RR은 정답이 처음 등장한 순번의 역수로 계산됩니다. 수식은 아래와 같습니다.
\[RR = \frac{1}{rank}\]- $rank$ : 가장 먼저 등장한 relevant item의 순위
- 정답이 추천 리스트에 없다면 점수는 0
- RR은 오직 첫 번째 정답만을 기준으로 삼는다는 점에서 다른 평가 지표들과 매우 다른 성격을 가집니다.
코드
- 상위 5개(K=5)의 추천 결과에 대한 RR
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
predict_list = ["A", "B", "C", "L", "Y", "U", "F", "Z"]
ground_truth = {"C":1.0, "K":1.0, "B":1.0, "Z":1.0}
def calc_rr(predict_list:list[str|int],
truth_items:list[str|int],
k:int=None) -> float:
if (k is None) or (k > len(predict_list)):
k = len(predict_list)
predict_list_k = predict_list[:k]
if len(predict_list_k) == 0:
return 0.0
for i, item in enumerate(predict_list_k):
if item in truth_items:
return 1/(i+1)
return 0.0
calc_rr(predict_list, list(ground_truth.keys()), 5)
# >> 0.5
한계점
RR은 오직 “첫 번째 정답”만 봅니다. 따라서 정답이 여러 개인 경우(예: 맛집 추천 10개를 다 잘 맞췄는지)에는 부적합할 수 있습니다. 이를 위해서 다음 포스팅에서 MAP에 대해 알아보도록 하겠습니다.
MRR
개념
MRR 은 Mean Reciprocal Rank의 약자로, 여러 케이스에 대한 RR 평가 결과를 평균낸 값입니다.
수식
\[MRR = \frac{1}{n}\sum_{i=1}^{n} RR_i,\quad RR_i = \begin{cases} \frac{1}{rank_i} & \text{if relevant item exists} \\ 0 & \text{otherwise} \end{cases}\]- $n$ : number of cases
- $rank_{i}$ : 가장 먼저 등장한 relevant item의 순위
코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
cases = [
{
"predict_list" : ["A", "B", "C", "L", "Y", "U", "F", "Z"],
"ground_truth" : {"C":1.0, "K":1.0, "B":1.0, "Z":1.0}
},
{
"predict_list" : ["N", "X", "Y", "B", "M"],
"ground_truth" : {"E":1.0, "B":1.0}
},
]
def calc_rr(predict_list:list[str|int],
truth_items:list[str|int],
k:int=None) -> float:
if (k is None) or (k > len(predict_list)):
k = len(predict_list)
predict_list_k = predict_list[:k]
if len(predict_list_k) == 0:
return 0.0
for i, item in enumerate(predict_list_k):
if item in truth_items:
return 1/(i+1)
return 0.0
def calc_mrr(cases, k:int=None) -> float:
mrr = sum(calc_rr(case["predict_list"], list(case["ground_truth"].keys()), k) for case in cases) / len(cases)
return mrr
calc_mrr(cases)
# >> 0.375
- predict_list : 추천 결과
- ground_truth : 실제 사용자가 선호한 아이템(정답 아이템)과 관련도 점수
- 첫 번째 케이스 : 추천 결과 중 “B” 아이템이 가장 빨리 등장한 정답이므로 RR 은 0.5 (1/2)
- 두 번째 케이스 : 추천 결과 중 “B” 아이템이 가장 빨리 등장한 정답이므로 RR 은 0.25 (1/4)
- 두 케이스의 평균 = (3/4)/2 = 0.375
Comments