본 글에서는 Top-K 추천 랭킹 품질을 평가하기 위해 실무적으로 사용되는 변형 정의도 함께 소개합니다.
이는 IR 관련의 표준 AP의 정의와 다를 수 있습니다. 이 점 유의하시기 바랍니다.
추천 시스템 평가지표 4. AP
Intro
“정답이 여러 개라면, 정답을 얼마나 전반적으로 앞쪽에서 잘 맞췄는가”
이전 포스팅에서는 RR(Reciprocal Rank)을 통해 “정답이 얼마나 빨리 등장하는가”를 살펴봤습니다. RR 만으로는 추천 시스템을 평가하기에 충분하지 않은 경우가 있습니다. 바로 아래와 같은 상황들입니다.
- 하나의 추천 리스트에 정답이 여러 개 존재한다.
- 첫 번째 정답 이후의 추천 품질도 중요하다.
- 사용자가 여러 번 클릭하거나, 여러 아이템을 탐색한다.
이러한 상황에서는 정답을 얼마나 빨리 맞추었는지뿐만 아니라, 정답들이 전반적으로 얼마나 앞쪽에 잘 배치되었는지까지 함께 평가할 수 있는 지표가 필요합니다. 이 경우에 바로 AP를 사용하면 됩니다.
개념
AP(Average Precision) 는 추천 결과에 여러 개의 정답이 있을 때, 전반적으로 앞쪽에 정답을 잘 위치시키는가를 평가하는 지표입니다.
이름에서도 알 수 있듯, Precision 의 평균(Average)로 이루어집니다. 추천 리스트에 여러 개의 정답 아이템이 있을 때, 각 정답이 등장하는 시점의 Precision을 평균낸 지표입니다. 조금 더 직관적으로 말하면 다음과 같습니다.
- 정답이 등장할 때마다 “그 시점까지의 Precision”을 계산
- 모든 Precision 들을 평균
- 결과적으로 여러 정답이 앞쪽에 배치될수록 점수가 높아짐
특징
- 정답이 여러 개일 때, 이 정답들이 상단에 뭉쳐 있을수록 높은 점수를 도출한다.
- Precision과 Recall의 장점을 모두 합친 고급 지표.
사용하는 경우
- 정답 아이템이 여러 개이고, 추천 품질의 전반적인 만족도를 높여야 할 때 사용
- 사용자가 리스트를 훑어보며 여러 개의 아이템에 관심을 가질 수 있는 서비스(검색 결과/추천 리스트 등)의 지표로 활용
수식
추천리스트에서 정답이 등장할 때의 precision을 합산하여, 전체 정답 개수로 나눠 평균을 냅니다. 먼저, 표준적인 AP는 아래와 같이 계산합니다.
\[AP = \frac{1}{|Relevant|}\sum_{i=1}^{n}{Precision@i \cdot rel(i)}\]- $Precision@i$ : 추천 목록 상위 i개에 대한 precision
- $rel(i)$ : i번째 추천 아이템이 정답이면 1, 아니면 0
|Relevant|: 전체 정답 개수.- $n$ : 전체 추천 목록의 길이.
하지만 사용자가 선호한 정답의 개수가 100개, 추천 목록의 개수가 3개라고 가정해 생각해봅시다. 이 경우엔 추천 아이템이 모두 정답이더라도 AP 점수는 매우 낮을 수밖에 없습니다. 따라서 추천 목록의 개수를 K개로 한정했다면, 아래와 같이 계산할 수도 있습니다.
\[AP@K = \frac{1}{min(|Relevant|, K)}\sum_{k=1}^{K}{Precision@k \cdot rel(k)}\]- $Precision@k$ : 추천 목록에서 k번째에서의 precision
- $rel(k)$ : k번째 추천 아이템이 정답이면 1, 아니면 0
|Relevant|: 전체 정답 개수.- $K$ : 평가에 사용한 추천 목록의 길이 cutoff.
AP@K 에 대한 정의는 표준적인 AP의 정의와 다를 수 있으니, 꼭 사용 전 확인하시기 바랍니다.
Precision 인데 왜 정답 개수로 나누나?
위 표준 AP 수식을 살펴보면, 분모항이 |Relevant|, 즉 “정답의 개수” 입니다. 앞선 포스팅에서 살펴본 지표 중, 정답의 개수로 나누는 지표가 있습니다. 바로 Recall입니다. MAP는 이름은 Precision이 들어가는데, 왜 Recall 처럼 정답의 개수로 나눌까요?
여기서 헷갈리지 말아야 할 것은, 표준 AP에서는 모든 정답(|R|)을 찾을 때까지 이 과정을 반복할 것이기 때문에, 결국 전체 정답 개수만큼의 부분항이 나오게 되고, 이를 평균 내기 위해 전체 정답 개수로 나누는 것입니다.
쉽게 말해, Recall을 평가하기 위한 개념으로 접근한 게 아니라, 정답을 맞춘 지점에서의 Precision 총합을 평균내기 위해서이고, 결론적으로는계산된 모든 Precision 부분항(=정답을 맞춘 경우)의 개수가 정답의 개수와 일치할 것이기 때문 입니다. 지표의 목적은 Recall에 대한 평가가 아니라, Precision에 대한 평가입니다.
예시
본 예시에서는 표준적인 AP의 정의가 아닌, 실무에서 사용되는 AP@K의 정의를 사용했습니다.
이 점 유의하시기 바랍니다.
예시 1
- 추천 결과와 정답 예시
1
2
3
4
5
# 추천 결과
[ A, B, C, D, E ]
# 정답 아이템
[ B, D, Z ]
- AP 계산 준비 표
| k | item | rel(k) | P@k | rel(k)*P@k |
|---|---|---|---|---|
| 1 | A | 0 | 0/1 = 0 | 0 |
| 2 | B | 1 | 1/2 = 0.5 | 0.5 |
| 3 | C | 0 | 1/3 ≈ 0.333… | 0 |
| 4 | D | 1 | 2/4 = 0.5 | 0.5 |
| 5 | E | 0 | 2/5 = 0.4 | 0 |
- AP 계산
예시 2
- 추천 리스트 안의 아이템은 같지만, 정답이 뒷쪽에 배치된 경우
1
2
3
4
5
# 추천 결과
[ A, C, E, B, D ]
# 정답 아이템
[ B, D , Z]
- AP 계산 준비 표
| k | item | rel(k) | P@k | rel(k)*P@k |
|---|---|---|---|---|
| 1 | A | 0 | 0/1 = 0 | 0 |
| 2 | C | 0 | 0/2 = 0 | 0 |
| 3 | E | 0 | 0/3 = 0 | 0 |
| 4 | B | 1 | 1/4 = 0.25 | 0.25 |
| 5 | D | 1 | 2/5 = 0.4 | 0.4 |
- AP 계산
코드
본 코드에서는 표준적인 AP의 정의가 아닌, 실무에서 사용되는 AP@K의 정의를 사용했습니다.
이 점 유의하시기 바랍니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
predict_list = ["A", "B", "C", "D", "E"]
ground_truth = {"B":1.0, "D":1.0, "Z" :1.0}
def calc_ap(predict_list:list[str|int],
truth_items:list[str|int],
k:int=None) -> float:
if len(truth_items) == 0:
return 0.0
if (k is None) or (k > len(predict_list)):
k = len(predict_list)
predict_list_k = predict_list[:k]
truth_items_set = set(truth_items)
sum_precision = 0
hit = 0
for i, item_yn in enumerate(predict_list_k):
if item_yn in truth_items_set:
hit += 1
sum_precision += hit/(i + 1)
return sum_precision/min(len(truth_items_set), k)
calc_ap(predict_list, list(ground_truth.keys()), 5)
# >> 0.3333333333333333
MAP
개념
MAP는 Mean Average Precision의 약자로, 여러 케이스에 대한 AP 평가 결과를 평균낸 값입니다.
수식
\[\begin{cases} \frac{1}{|Relevant|}\sum_{i=1}^{n}{Precision@i \cdot rel(i)}, & 표준적인 AP\\ \frac{1}{min(|Relevant|, K)}\sum_{k=1}^{K}{Precision@k \cdot rel(k)}, & K개로 \, Cutoff \, AP@K \end{cases}\\ MAP@K = \frac{1}{n}\sum_{i=1}^{n}{AP@K(i)}\]- $Precision@k$ : 추천 목록 상위 k개에 대한 precision
- $rel(k)$ : k번째 추천 아이템이 정답이면 1, 아니면 0
|Relevant|: 전체 정답 개수.- $K$ : 평가에 사용한 추천 목록의 길이 cutoff.
코드
본 코드에서는 표준적인 AP의 정의가 아닌, 실무에서 사용되는 AP@K의 정의를 사용했습니다.
이 점 유의하시기 바랍니다.
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
31
32
33
34
cases = [
{
"predict_list" : ["A", "B", "C", "D", "E"],
"ground_truth" : {"B":1.0, "D":1.0, "Z":1.0}
},
{
"predict_list" : ["A", "C", "E", "B", "D"],
"ground_truth" : {"B":1.0, "D":1.0, "Z":1.0}
},
]
def calc_ap(predict_list:list[str|int],
truth_items:list[str|int],
k:int=None) -> float:
if len(truth_items) == 0:
return 0.0
if (k is None) or (k > len(predict_list)):
k = len(predict_list)
predict_list_k = predict_list[:k]
truth_items_set = set(truth_items)
sum_precision = 0
hit = 0
for i, item_yn in enumerate(predict_list_k):
if item_yn in truth_items_set:
hit += 1
sum_precision += hit/(i + 1)
return sum_precision/min(len(truth_items_set), k)
def calc_map(cases, k:int=None) -> float:
mean_ap = sum(calc_ap(case["predict_list"], list(case["ground_truth"].keys()), k) for case in cases) / len(cases)
return mean_ap
calc_map(cases, 5)
# >> 0.275
- predict_list : 추천 결과
- ground_truth : 실제 사용자가 선호한 아이템(정답 아이템)과 관련도 점수
- 첫 번째 케이스 : 추천 결과 중 2개 아이템이 정답, AP = (B(1/2) + D(2/4))/3 = 0.33333…
- 두 번째 케이스 : 추천 결과 중 2개 아이템이 정답, AP = (B(1/4) + D(2/5))/3 = 0.21666…
- 두 케이스의 평균 = (0.33333…+0.21666…)/2 = 0.275
Comments