머신러닝 모델의 평가 (2. 다중 분류)
제목이 다중 분류이긴 한데 약간 모호할 수 있으므로 Multiclass Classification임을 밝힙니다.
이전 글 https://rython.tistory.com/7
이전 글에서는 이진 분류의 평가 지표들에 대해 알아보았습니다. Titanic 생존자 예측과 같은 경우에는 target이 0과 1, 즉 이진 분류로 해결할 수 있는 문제인데요, 분류 문제를 보면 항상 2가지로 나누는 문제만 있는 것은 아닙니다. 예를 들면 Iris 품종 예측과 같은 경우가 있죠. Setosa, Versicolor, Virginica 3가지로 분류하게 되는데 이 경우에도 이전 글에서 정의한 여러 평가 지표들을 사용할 수 있어야 합니다. 많은 지표들이 Confusion matrix를 통해 정의되므로 우선 Confusion matrix를 그려보도록 하겠습니다. 편의상 class의 이름은 A, B, C 세 개로 하겠습니다.
아마 Confusion matrix는 위 그림처럼 생겼을 것입니다. 빈 칸에는 각 조건에 맞는 data의 수가 적힐 것이고요. 이제 이진 분류와 같이 각 평가 지표들을 계산해보려 한다면 한 가지 문제가 있게 됩니다. 바로 TP, FN, FP, TN을 정의할 수 없다는 것입니다. 뭐.. Accuracy 계산은 대충 느낌이 오지만 다른 값들도 계산하려면 필요하겠죠. Multiclass Classification에서는 각각의 class별로 TP, FN, FP, TN값을 따로 정의하게 되고 그 값들을 이용하여 계산하는 방법에 따라 micro average와 macro average 지표 두 가지가 나타나게 됩니다.
우선 각 class별로 TP, FN, FP, TN을 정의해보도록 하겠습니다. TP, FN, FP, TN은 이진 분류에서 정의되던 값들이니 multiclass classification을 이진 분류로 생각해보아야 합니다. 그 방법으로는 하나의 class를 positive(1)로, 나머지 class를 전부 negative(0)으로 생각하게 됩니다. 아래 그림을 본다면 쉽게 이해가 가실 겁니다.
위 그림에서 각 TP, FN, FP, TN값들은 각 색에 맞는 칸들에 적힌 숫자들의 합입니다. 여기까지 이해를 하셨다면 Multiclass Classification에서는 각 class별로 TP, FN, FP, TN값들이 따로 존재한다는 것을 알 수 있습니다.
Micro Average
Micro Average는 각각의 TP, FN, FP, TN값들을 모두 합친 total TP, total FN, total FP, total TN값들을 이용해 계산합니다. 위의 Confusion matrix에서는 헷갈림을 막기 위해 class의 이름을 A, B, C로 하였지만 수식에서는 class의 이름을 $1,2,3,\cdots ,c$로 하겠습니다. $c$는 class의 총 개수입니다. 수식으로 나타내지 않아도 아시겠지만 수식으로 한 번 나타내보겠습니다.
$$\text{TP}_{\text{total}}=\sum^{c}_{i=1}\text{TP}_{i}\\\text{FN}_{\text{total}}=\sum^{c}_{i=1}\text{FN}_{i}\\\text{FP}_{\text{total}}=\sum^{c}_{i=1}\text{FP}_{i}\\\text{TN}_{\text{total}}=\sum^{c}_{i=1}\text{TN}_{i}$$
이런 식으로 정의를 한 후 Accuracy, Sensitivity, Specificity, False Alarm rate, Precision, F score들을 total 값들을 이용해서 계산해주게 됩니다.
눈치채신 분들도 있겠지만 total 값들을 구하다보면 몇 개의 값들이 서로 연관되게 됩니다. 그 예로 $\text{FN}_{\text{total}}=\text{FP}_{\text{total}}$가 됩니다. 이런 식으로 같아지다 보니 Micro average를 통해 계산한 평가 지표들간에도 같은 값들이 생기는데 $\text{Accuracy}=\text{Precision}=\text{Sensitivity}=\text{F1 score}$가 성립하게 됩니다. 위 Confusion matrix에서 조금만 생각해보면 알 수 있습니다.
Macro Average
Micro Average는 각각을 합친 후 계산하는 것으로 생각해본다면 Macro Average는 계산 후 합치는 것으로 생각해볼 수 있습니다. 즉 각각의 class에 따라 TP, FN, FP, TN값들을 이용해서 평가 지표를 계산한 후 그 값들의 평균을 사용합니다. 예시로 Sensitivity의 식을 나타내본다면
$$\text{Sensitivity}=\frac{1}{c}\sum^{c}_{i=1}\text{Sensitivity}_{i}=\frac{1}{c}\sum^{c}_{i=1}\frac{\text{TP}_{i}}{\text{TP}_{i}+\text{FN}_{i}}$$
로 나타낼 수 있습니다.
Weighted Average
Macro Average의 확장으로 Weighted Average도 있는데요 이는 그냥 평균을 계산하는 것이 아니라 각 class에 해당하는 data의 개수에 가중치를 주어 평균을 구한 것입니다. 전체 data 중 class $i$에 속하는 data 개수의 비율을 $p(i)$로 생각한다면 Sensitivity의 식은
$$\text{Sensitivity}=\sum^{c}_{i=1}p(i)\text{Sensitivity}_{i}=\sum^{c}_{i=1}p(i)\frac{\text{TP}_{i}}{\text{TP}_{i}+\text{FN}_{i}}$$
가 됩니다.
마지막으로 이진 분류 글에서 다룬 내용에는 ROC curve와 AUC라는 값이 있었습니다. ROC curve도 찾아보면 Micro average ROC, Macro average ROC 등이 나오기는 하는데 위에서 Sensitivity와 False Alarm rate를 정의했더라도 ROC curve를 그리는 것은 문제가 있습니다. 일반적으로 그 class에 속할 확률을 통해 ROC curve를 그리게 되는데 multiclass의 경우 각 data에 따라 $c$개의 수가 존재한다는 것이죠. 이진 분류에서 1개의 수가 있었지만 확률의 합은 1이 되어야 하므로 원래는 2개의 수이지만 1개만 나타난 것입니다. 이런 식으로 수가 여러 개가 된다면 threshold보다 큰지 작은지를 판별하는 것이 모호해집니다. 뭐 $c(c-1)$차원에서 ROC curve(hyper곡면?)를 그린다고 하지만 여기서는 우리가 알던 ROC curve처럼 2차원에서 average ROC curve를 그리는 방법을 알아보겠습니다. 또한 단순히 ROC curve의 아래 면적으로 정의되던 AUC의 다른 의미를 알아보고 multiclass에서 average AUC를 구하는 4가지 방법에 대해 알아볼 것입니다.
ROC curve와 AUC의 의미
우선 ROC curve를 한 번 다른 방식으로 해석해봅시다. ROC curve를 그리기 위해 threshold가 움직이는 그 값에 대해 각 data를 정렬해볼 수 있습니다. 위 그림에서는 맨 아래 적힌 수열이 되겠습니다. 오른쪽으로 갈 수록 0이 많아지는 것으로 보아 그 값은 아마 모델이 예측한 0일 확률이 될 것 같네요. threshold가 움직이면 각각의 숫자 사이를 지나게 될 것입니다. 그러다가 하나의 숫자를 통과하게 된다면 Confusion matrix의 값이 바뀌며 ROC curve 상에서는 점이 움직이며 자취를 남기게 되겠죠. 만약 1을 통과하게 되었다면 Sensitivity를 바꾸게 될 것이고 0을 통과하게 된다면 False Alarm rate를 바꾸게 됩니다. 아래의 예시에서 1과 0의 개수는 각각 10개이므로 $\frac{1}{10}$만큼 그 값이 바뀔 것입니다(Sensitivity와 False Alarm rate의 분모는 각각 1과 0의 총 개수인 것을 생각해봅시다).
이제 ROC curve에서 어떻게 나타나는지를 생각해봅시다. threshold가 가장 왼쪽에 있을 때 ROC curve에서는 가장 왼쪽 아래 점이 될 것입니다. threshold가 오른쪽으로 가며 1을 통과하게 되면 ROC curve에서 위로 $\frac{1}{10}$ 이동하게 됩니다. 만약 0을 통과하게 된다면 ROC curve에서는 오른쪽으로 $\frac{1}{10}$만큼 이동할 것이고요. 그러면서 threshold가 가장 오른쪽에 도달한다면 ROC curve에서는 가장 오른쪽 위에 도달합니다. 그러면서 빨간색 자취를 남기게 되고 이를 ROC curve라고 합니다.(설명에서 0과 1의 개수를 10개로 정했기 때문에 $\frac{1}{10}$입니다.)
threshold가 숫자를 통과하며 선분을 남기기 때문에 그림에서 작은 정사각형의 한 변에 해당하는 선분들은 각각 숫자에 대응시킬 수 있을 것입니다. 숫자의 배열이 바뀌더라도 각각의 1에 대해서 그 선분 뿐만아니라 그 가로줄에 있는 모든 줄들은 같은 수를 나타낼 것입니다. 0에 대해서는 세로 방향으로 그럴 것이고요. 그렇게 되면 각 행들에는 0을 대응시킬 수 있고 각 열에는 1을 대응시킬 수 있게 됩니다.
AUC 값은 위 그림에서 붉게 색칠한 부분의 넓이가 될 텐데요 총 넓이가 1이므로 전체 칸 중에서 무작위로 하나의 칸을 골랐을 때 붉은 칸을 고를 확률로 보아도 될 것 같습니다. 하나의 칸을 고른다는 것은 행과 열을 결정하게 되므로 즉 0과 1에서 각각 무작위로 하나를 고르는 것으로 볼 수 있습니다. 그리고 칸이 붉다는 것은 고른 0이 고른 1보다 수열에서 오른쪽에 등장한다는 것을 나타냅니다. 수열에서 오른쪽에 등장한다는 것은 처음에 우리가 모델이 예측한 0일 확률로 정렬하였으므로 0일 확률이 더 크다는 것으로 해석해볼 수 있습니다.
정리를 하면 AUC의 값은 임의로 0과 1에서 각각 하나씩 뽑았을 때 모델이 예측한 0일 확률에 대해 0이 1보다 클 확률을 의미합니다. 이진 분류에서는 0일 확률이 0이 1보다 크다는 것과 1일 확률이 1이 0보다 크다는 같은 의미가 되겠지만 다중 분류에서는 다를 수 있습니다.
위 설명은 논문을 읽다가 AUC에 저런 의미가 있다고 나와 제가 이해하기 위해 만들어낸 설명이라 틀린 부분이 있을 수 있습니다. 논문에서는 Fawcett, T., 2006. An introduction to ROC analysis. Pattern Recogn. Lett. 27 (8), 861– 874. 여기에서 그 내용이 나온다고 하지만 보려면 돈을 내라고 해서 못 봤습니다.
위에서부터 정의한 여러 값들을 다시 정리하고 추가적으로 정의해보겠습니다.
-각각의 class는 $1$부터 $c$의 값으로 나타냅니다. $c$는 총 class의 개수입니다.
-각각의 data는 $1$부터 $m$의 값으로 나타냅니다. $m$은 총 data의 개수입니다.
-$f(i, j)$를 data $i$가 class $j$에 속하는가? 에 대한 논리값(0 또는 1)입니다.
-$m_{j}=\sum^{m}_{i=1}f(i,j)$ 즉 class $j$에 속하는 data의 수 입니다.
-$p(j)=\frac{m_{j}}{m}$ 즉 class $j$에 속하는 data의 비율입니다.
-문자가 겹치긴 하지만 $p(i,j)$는 모델이 예측한 data $i$가 class $j$에 속할 확률로 구간 $[0,1]$에 포함됩니다.
-$C(i,j)$는 모델이 예측한 data $i$의 class 가 $j$인가? 에 대한 논리값(0 또는 1)입니다.
이 정의들은 제가 마음대로 정한 것은 아니고 논문 An experimental comparison of performance measures for classification의 표기를 사용하였습니다. 이 논문도 보려면 돈을 내라고 하는데 RDocumentation이라는 R에 관련한 사이트에서 링크가 있어서 읽을 수 있었습니다. 나중에 알게되었는데 scikit-learn에도 링크가 있더군요.
https://www.math.ucdavis.edu/~saito/data/roc/ferri-class-perf-metrics.pdf
이 논문에서 두 class $j$와 $k$에 대해 AUC를 계산하는 식이 나와있는데 한 번 살펴보며 위의 내용을 확인해봅시다.
$$\text{AUC}(j,k)=\frac{\sum^{m}_{i=1}f(i,j)\sum^{m}_{t=1}f(t,k)I(p(i,j),p(t,j))}{m_{j}m_{k}}$$
앞의 $\sum^{m}_{i=1}f(i,j)\sum^{m}_{t=1}f(t,k)$ 부분은 data $i$와 $t$가 각각 class $j$와 $k$에 속하면 1 아닌 경우 0이 되도록 한 부분이며 따라서 $j$에서 추출한 $i$와 $k$에서 추출한 $t$에 대해서만 계산하도록 한 것으로 보입니다.
논문에서 $I(a,b)$는 $a>b$인 경우에 1, $a<b$인 경우는 0, $a=b$인 경우 0.5를 반환하는 함수로 정의되어있는데요, 간단히 앞의 값이 뒷 값보다 큰가? 에 대한 논리값 정도로 생각해볼 수 있을 것 같습니다(같은 경우는 0.5).
즉 $\text{AUC}(j,k)$의 값은 $j$에서 뽑은 $i$와 $k$에서 뽑은 $t$에 대해 모델이 $j$에서 왔을 것으로 추정한 확률이 $i$가 $t$보다 클 확률으로 해석해볼 수 있을 것 같습니다. 위에서 우리가 생각해본 AUC의 의미와 통하네요.
Average ROC
Average ROC를 그리는 방법이 잘 나와있는 곳이 없어서 https://scikit-learn.org/stable/auto_examples/model_selection/plot_roc.html를 참고하였습니다. 참고로 scikit-learn의 ROC 함수는 multiclass classification을 지원해주지 않습니다.
Average ROC도 micro와 macro 두 가지 종류가 있습니다. 우선 micro부터 알아봅시다.
# Compute micro-average ROC curve and ROC area
fpr["micro"], tpr["micro"], _ = roc_curve(y_test.ravel(), y_score.ravel())
roc_auc["micro"] = auc(fpr["micro"], tpr["micro"])
micro를 계산하는 부분은 이 부분입니다. y_test와 y_score를 ravel 메소드를 호출한 후 일반적인 AUC와 같은 방식으로 계산하네요. y_test와 y_score는 ndarray이므로 ravel 함수를 찾아보면 1차원으로 만들어주는 메소드라고 합니다. 즉 class를 상관하지 않고 그 class일 확률과 실제 class인가를 통해 ROC curve를 그리게 됩니다.
다음은 macro입니다.
# First aggregate all false positive rates
all_fpr = np.unique(np.concatenate([fpr[i] for i in range(n_classes)]))
# Then interpolate all ROC curves at this points
mean_tpr = np.zeros_like(all_fpr)
for i in range(n_classes):
mean_tpr += interp(all_fpr, fpr[i], tpr[i])
# Finally average it and compute AUC
mean_tpr /= n_classes
fpr["macro"] = all_fpr
tpr["macro"] = mean_tpr
roc_auc["macro"] = auc(fpr["macro"], tpr["macro"])
macro를 계산하는 부분은 이 부분입니다. 7번째 줄을 보면 interp라는 함수가 나오는데 제일 윗 부분에서 scipy의 interp 함수를 import 하게 됩니다. interp 함수는 여러 점들이 주어졌을 때 각각을 선분으로 이어 사이의 값을 보간해주는 함수입니다. 따라서 macro average는 각각의 class에 대한 이진 분류로 ROC curve를 그린 것을 y축 방향으로 평균낸 ROC curve인 것으로 생각할 수 있습니다.
Average AUC
위의 링크에서 아래쪽을 보면 AUC를 4가지 방법으로 구하는 코드가 나옵니다. scikit-learn의 roc_auc_score는 Multiclass classification을 지원해줍니다. roc는 지원해주지 않는데 roc_auc_score는 지원해주는 신기한 현상
이 또한 scikit-learn의 설명 scikit-learn.org/stable/modules/model_evaluation.html을 참고하였습니다.
roc_auc_score는 Multiclass classification에 대해 multiclass 파라미터를 'ovo'와 'ovr' 두 가지를 지원하고 average로 'macro'와 'weighted' 두 가지를 지원합니다. 즉, 4가지 방법이 있습니다.
ovo는 one-vs-one으로 각각의 모든 두 class별로 비교한다는 의미이고 ovr은 one-vs-rest로 각각의 class와 나머지 전체를 비교하여 구한 값입니다. 글에는 ovo가 먼저 설명되어있지만 ovo에 대해 할 이야기가 많으므로 ovr부터 다뤄보겠습니다.
one-vs-rest
이는 각각의 class에 따라 이진 분류로 생각하여 AUC 값을 구한 후 평균 낸 것입니다. weighted의 경우 각 class에 해당하는 data의 수에 따라 가중치를 주어 평균 낸 것입니다. 식으로 나타내보면
$$\text{AUC}_{\text{ovr},\text{macro}}=\frac{1}{c}\sum^{c}_{i=1}\text{AUC}(i,\text{rest}_{i})$$
$$\text{AUC}_{\text{ovr},\text{weighted}}=\sum^{c}_{i=1}p(i)\text{AUC}(i,\text{rest}_{i})$$
로 표현할 수 있습니다. $\text{rest}_{i}$는 $i$를 제외한 다른 모든 class를 하나로 생각한 것입니다.
one-vs-one
우선 scikit-learn의 설명을 따라 작성해보겠습니다. 우선 macro의 경우입니다.
$$\text{AUC}_{\text{ovo},\text{macro}}=\frac{2}{c(c-1)}\sum^{c}_{j=1}\sum^{c}_{k>j}(\text{AUC}(j,k)+\text{AUC}(k,j))$$
식을 조금만 정리해본다면
$$\text{AUC}_{\text{ovo},\text{macro}}=\frac{2}{c(c-1)}\sum^{c}_{j=1}\sum^{c}_{k\neq j}(\text{AUC}(j,k))$$
로 나타내어 보겠습니다. $\sum$을 통해 더해진 AUC의 개수는 $c(c-1)$개가 되는데 앞에는 $\frac{2}{c(c-1)}$이 곱해지게 됩니다. 그렇다면 이 값은 0에서 2 사이의 값을 가지게됩니다.
그 다음으로 weighted를 알아봅시다.
$$\text{AUC}_{\text{ovo},\text{weighted}}=\frac{2}{c(c-1)}\sum^{c}_{j=1}\sum^{c}_{k>j}p(j\cup k)(\text{AUC}(j,k)+\text{AUC}(k,j))$$
이 것도 식을 조금 정리해보겠습니다.
$$\text{AUC}_{\text{ovo},\text{weighted}}=\frac{4}{c}\frac{1}{c-1}\sum^{c}_{j=1}\sum^{c}_{k\neq j}\frac{p(j)+p(k)}{2}\text{AUC}(j,k)$$
최댓값이 $\frac{4}{c}$가 될 것 같네요...
문제가 있는 것 같아 인용한 논문을 찾아가보기로 하였습니다. 위에서 여러 값들의 표기를 참고한 논문입니다.
https://www.math.ucdavis.edu/~saito/data/roc/ferri-class-perf-metrics.pdf
논문의 내용은 여러 평가 지표들을 실제 적용해보고 지표들을 분석해보는 내용입니다.
$\text{AUC}_{\text{ovo},\text{macro}}$의 정의로 보이는 AU1U라는 값과 $\text{AUC}_{\text{ovo},\text{weighted}}$의 정의로 보이는 AU1P라는 값이 있었습니다. 논문에서 각각의 정의를 살펴보면
$$\text{AU1U}=\frac{1}{c(c-1)}\sum^{c}_{j=1}\sum^{c}_{k\neq j}\text{AUC}(j,k)$$
$$\text{AU1P}=\frac{1}{c(c-1)}\sum^{c}_{j=1}\sum^{c}_{k\neq j}p(j)\text{AUC}(j,k)$$
이렇게 정의되는 것을 알 수 있습니다. 첫 번째 AU1U는 scikit-learn의 값과 2배 차이가 나고 0에서 1 사이의 값이 될 것을 유추해볼 수 있습니다. AU1P의 경우 scikit-learn의 값과 2배 차이가 나며 논문에서 $p(j)$를 사용하였지만 scikit-learn에서는 $p(j\cup k)$가 사용되었네요. 논문의 값도 0과 $\frac{1}{c}$ 사이의 값으로 생각되는데 이 또한 이상하긴 하지만 scikit-learn의 설명과 다른 것을 확인할 수 있었습니다.
그렇다면 실제로 scikit-learn은 어떻게 계산할까요? 실제 코드를 통해 알아보았습니다. 참고로 제 scikit-learn의 버전은 0.23.1입니다.
sklearn 다운받은 위치를 가서 metric이라는 폴더에 _ranking.py와 _base.py에 roc_auc_score함수와 이 함수가 호출하여 계산이 이루어지는 함수들의 코드를 볼 수 있었습니다. 코드가 너무 긴 관계로 주석을 제거한 코드를 보면 아래와 같습니다.
roc_auc_score 함수를 호출하면 여러 예외를 판단한 후에 ovo의 경우 이 부분에서 처리하게 됩니다. 즉 _average_multiclass_ovo_score를 호출하게 되고 첫 번째 인자로 _binary_roc_auc_score라는 함수가 전달됩니다.
위 코드에서 _binary_roc_auc_score는 우리가 아는 이진 분류에서의 AUC를 계산해주는 함수로 보입니다. 이 함수는 _average_multiclass_ovo_score의 binary_metric 인자에 들어가게 됩니다. 실질적인 계산이 이루어지는 것으로 보이는 반복문을 살펴보겠습니다. 반복문에 나오는 combinations는 itertools 패키지의 함수로 위에서 두 개로 이루어진 조합을 반환합니다. 즉 $\sum^{c}_{j=1}\sum^{c}_{k>j}$ 처럼 생각할 수 있습니다. a_mask와 b_mask는 각각 실제 label이 a인 data들과 b인 data들을 알기 위해 0과 1로 나타낸 것으로 보이고요 ab_mask는 label이 a 또는 b인 data를 나타내는 것 같습니다. a_true와 b_true는 이를 통해 실제 label이 a 또는 b인 data만을 추출하였고 이 값은 binary_metric을 통해 계산되어 a_true_score와 b_true_score가 됩니다. binary_metric이 _binary_roc_auc_score이었으므로 AUC값이 계산되어 들어갔을 것입니다. 그런데 실제 나중에 평균을 구하게 되는 pair_scores에 a_true_score와 b_true_score가 더해진 후 2로 나누어져 들어가게 됩니다. 평균을 구하는 과정은 모두 더하고 총 개수인 $\frac{c(c-1)}{2}$로 나눈다고 생각해보면 scikit-learn의 설명과 달리 실제 논문과 같은 식으로 계산하는 것으로 보입니다.
두 번째 weighted의 경우를 살펴봅시다. weighted의 경우 prevalence라는 가중치를 통해 평균을 구하게 됩니다. 가중치는 scikit-learn의 설명대로 $p(j\cup k)$를 사용하고 있습니다. 하지만 np.average라는 함수는 각각의 가중치 합을 1로 환산하여 그 값을 통해 평균을 계산하게 됩니다. 즉 return 되는 값은 무조건 0과 1사이의 값이 된다는 것이죠. $p(j\cup k)$의 합은 1이 될리가 없으므로 계산하는 값은 scikit-learn의 설명과 논문의 값 둘 다 아닌 값이 되는 것으로 보입니다. 식으로 계산해본다면 $\frac{1}{2(c-1)}\sum^{c}_{j=1}\sum^{c}_{k\neq j}p(j\cup k)\text{AUC}(j,k)$가 될 것 같네요.
제가 어떤 부분을 간과하여 틀린 이야기를 했을지는 모르지만 논문과 scikit-learn의 값이 다르다는 점은 확실한 것 같습니다. 틀린 부분이 있다면 지적해주세요.
글이 엄청나게 길어졌네요... 참고로 위에 confusion matrix와 ROC curve 설명 그림은 ppt 표를 통해 그렸습니다. 대각선을 그어도 칸이 안 나누어지기 때문에 그 칸을 2×3으로 나눠서 대각선을 각각 그렸습니다.
Written by Xylene