by museonghwang

confusion matrix 및 정밀도(Precision)와 재현율(Recall)

|

오차 행렬(confusion matrix, 혼동행렬)

이진 분류에서 성능 지표로 잘 활용되는 오차행렬(confusion matrix, 혼동행렬)학습된 분류 모델이 예측을 수행하면서 얼마나 헷갈리고(confused) 있는지를 함께 보여주는 지표 입니다. 즉, 이진 분류의 예측 오류가 얼마인지와 더불어 어떠한 유형의 예측 오류가 발생하고 있는지를 함께 나타내는 지표 입니다.

오차 행렬은 다음과 같이 4분면 행렬에서 실제 레이블 클래스 값과 예측 레이블 클래스 값이 어떠한 유형을 가지고 매핑 되는지를 나타내며, 예측 클래스와 실제 클래스의 값 유형에 따라 결정되는 TN, FP, FN, TP 형태로 오차 행렬의 4분면을 채울 수 있습니다. TN, FP, FN, TP 값을 다양하게 결합해 분류 모델 예측 성능의 오류가 어떠한 모습으로 발생하는지 알 수 있습니다.

image


TN, FP, FN, TP예측 클래스와 실제 클래스의 Positive 결정값(값 1)과 Negative 결정 값(값 0)의 결합에 따라 결정 됩니다. 앞 문자 True/False예측값과 실제값이 ‘같은가/틀린가’를 의미 하고, 뒤 문자 Negative/Positive예측 결과 값이 부정(0)/긍정(1)을 의미 합니다.

  • TN : 예측을 Negative(0) 으로 예측, 정답 -> 실제 값은 Negative(0)
  • FP : 예측을 Positive(1) 으로 예측, 오답 -> 실제 값은 Negative(0)
  • FN : 예측을 Negative(0) 으로 예측, 오답 -> 실제 값은 Positive(1)
  • TP : 예측을 Positive(1) 으로 예측, 정답 -> 실제 값은 Positive(1)


다음 그림은 TN, FP, FN, TP 구분을 재미있게 표현한 그림입니다.

image


사이킷런은 오차 행렬을 구하기 위해 confusion_matrix() API 를 제공합니다.

MNIST 데이터의 10%만 True, 나머지 90%는 False인 불균형한 데이터 세트MyFakeClassifier 를 생성하고, MyFakeClassifier 의 예측 성능 지표를 confusion_matrix() 를 이용해 오차 행렬로 표현하겠습니다.

from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.base import BaseEstimator
from sklearn.metrics import accuracy_score
import numpy as np
import pandas as pd

class MyFakeClassifier(BaseEstimator):
    def fit(self, X, y):
        pass
    
    # 입력값으로 들어오는 X 데이터 셋의 크기만큼 모두 0값으로 만들어서 반환
    def predict(self, X):
        return np.zeros((len(X), 1), dtype=bool)

# 사이킷런의 내장 데이터 셋인 load_digits()를 이용하여 MNIST 데이터 로딩
digits = load_digits()

# digits번호가 7번이면 True이고 이를 astype(int)로 1로 변환, 7번이 아니면 False이고 0으로 변환. 
y = (digits.target == 7).astype(int)
X_train, X_test, y_train, y_test = train_test_split(digits.data, y, random_state=11)

# 불균형한 레이블 데이터 분포도 확인. 
print('레이블 테스트 세트 크기 :', y_test.shape)
print('테스트 세트 레이블 0 과 1의 분포도')
print(pd.Series(y_test).value_counts())

# Dummy Classifier로 학습/예측/정확도 평가
fakeclf = MyFakeClassifier()
fakeclf.fit(X_train, y_train)
fakepred = fakeclf.predict(X_test)
print('\n모든 예측을 0으로 하여도 정확도는 : {:.3f}'.format(accuracy_score(y_test, fakepred)))
[output]
레이블 테스트 세트 크기 : (450,)
테스트 세트 레이블 0 과 1의 분포도
0    405
1     45
dtype: int64

모든 예측을 0으로 하여도 정확도는 : 0.900


from sklearn.metrics import confusion_matrix

# 앞절의 예측 결과인 fakepred와 실제 결과인 y_test의 Confusion Matrix출력
confusion_matrix(y_test, fakepred)
[output]
array([[405,   0],
       [ 45,   0]])


이진 분류의 TN, FP, FN, TP 는 상단 도표와 동일한 위치를 가지며, array에서 가져올 수 있습니다. 즉, TNarray[0,0] 로 405, FParray[0,1] 로 0, FNarray[1,0] 로 45, TParray[1,1] 로 0에 해당합니다.


MyFakeClassifieload_digits()target==7 인지 아닌지에 따라 클래스 값을 True/False 이진 분류 로 변경한 데이터 세트를 사용해 무조건 Negative 로 예측하는 Classifier 였고, 테스트 데이터 세트의 클래스 값 분포는 0이 405건, 1이 45건입니다.

따라서 TN 은 전체 450건 데이터 중 무조건 Negative 0으로 예측해서 True가 된 결과 405건, FP 는 Positive 1로 예측한 건수가 없으므로 0건, FN 은 Positive 1인 건수 45건을 Negative로 예측해서 False가 된 결과 45건, TP 는 Positive 1로 예측한 건수가 없으므로 0건입니다.

image


TN, FP, FN, TP 값은 Classifier 성능의 여러 면모를 판단할 수 있는 기반 정보를 제공 합니다. 이 값을 조합해 Classifier의 성능을 측정할 수 있는 주요 지표인 정확도(Accuracy), 정밀도(Precision), 재현율(Recall) 값을 알 수 있습니다.

정확도예측값과 실제 값이 얼마나 동일한가에 대한 비율만으로 결정 됩니다. 즉, 오차 행렬에서 True에 해당하는 값인 TN과 TP에 좌우 됩니다. 정확도는 오차 행렬상에서 다음과 같이 재정의될 수 있습니다.


\[정확도 = \frac{예측\ 결과와\ 실제\ 값이\ 동일한\ 건수}{전체\ 데이터\ 수}=\frac{FN + TP}{TN + FP + FN + TP}\]


일반적으로 불균형한 레이블 클래스를 가지는 이진 분류 모델 에서는 많은 데이터 중에서 중점적으로 찾아야 하는 매우 적은 수의 결과값에 Positive 를 설정해 1 값을 부여하고, 그렇지 않은 경우는 Negative 로 0 값을 부여하는 경우가 많습니다. 예를 들어 암 검진 예측 모델에서는 암이 양성일 경우 Positive 양성으로 1, 암이 음성일 경우 Negative 음성으로 값이 할당되는 경우가 일반적입니다.


불균형한 이진 분류 데이터 세트에서는 Positive 데이터 건수가 매우 작기 때문에 데이터에 기반한 ML 알고리즘은 Positive보다는 Negative로 예측 정확도가 높아지는 경향이 발생 합니다.

10,000건의 데이터 세트에서 9,900건이 Negative이고 100건이 Positive라면 Negative로 예측하는 경향이 더 강해져TN은 매우 커지고, TP는 매우 작아지게 됩니다. 또한 Negative로 예측할 때 정확도가 높기 때문에 FN(Negative로 예측할 때 틀린 데이터 수)이 매우 작고, Positive로 예측하는 경우가 작기 때문FP 역시 매우 작아집니다.


결과적으로 정확도 지표비대칭한 데이터 세트에서 Positive에 대한 예측 정확도를 판단하지 못한 채 Negative에 대한 예측 정확도만으로도 분류의 정확도가 매우 높게 나타나는 수치적인 판단 오류 를 일으키게 됩니다. 불균형한 데이터 세트에서 정확도만으로는 모델 신뢰도가 떨어질 수 있는 사례를 확인했습니다.


정밀도와 재현율

정밀도재현율Positive 데이터 세트의 예측 성능에 좀 더 초점을 맞춘 평가 지표 입니다. 정밀도와 재현율은 다음과 같은 공식으로 계산됩니다.


\[정밀도 = \frac{TP}{FP + TP},\ 재현율 = \frac{TP}{FN + TP}\]


정밀도예측을 Positive로 한 대상 중예측과 실제 값이 Positive로 일치한 데이터의 비율 을 뜻합니다. Positive 예측 성능을 더욱 정밀하게 측정하기 위한 평가 지표로 양성 예측도 라고도 불립니다.

정밀도가 상대적으로 더 중요한 지표인 경우실제 Negative 음성인 데이터 예측을 Positive 양성으로 잘못 판단하게 되면 업무상 큰 영향이 발생하는 경우 입니다.

  • 스팸메일 여부를 판단하는 모델
    • 실제 Positive인 스팸 메일을 Negative인 일반 메일로 분류 -> 사용자가 불편함을 느끼는 정도.
    • 실제 Negative인 일반 메일을 Positive인 스팸 메일로 잘못 분류 -> 메일을 아예 받지 못하게 돼 업무에 차질이 생김.


재현율실제 값이 Positive인 대상 중예측과 실제 값이 Positive로 일치한 데이터의 비율 을 뜻합니다. 민감도(Sensitivity) 또는 TPR(True Positive Rate) 라고도 불립니다.

재현율이 상대적으로 더 중요한 지표인 경우실제 Positive 양성인 데이터 예측을 Negative로 잘못 판단하게 되면 업무상 큰 영향이 발생하는 경우 입니다.

  • 암 판단 모델
    • 실제 Negative인 건강한 환자를 암 환자인 Positive로 예측 -> 다시 한번 재검사를 하는 수준의 비용이 소모.
    • 실제 Positive인 암(양성) 환자를 Negative 음성으로 잘못 예측 -> Die.
  • 보험 사기 및 금융 사기 적발 모델
    • 정상 금융거래인 Negative를 금융사기인 Positive로 잘못 예측 -> 금융 사기인지 재확인 절차.
    • 실제 금융거래 사기인 Positive를 Negative로 잘못 예측 -> 회사에 미치는 손해가 클 것.


다시 한번 재현율과 정밀도의 공식을 살펴보겠습니다.

\[정밀도 = \frac{TP}{FP + TP},\ 재현율 = \frac{TP}{FN + TP}\]
  • 공통점
    • 재현율과 정밀도 모두 TP를 높이는 데 초점을 맞춤.
  • 차이점
    • 재현율 : FN(실제Positive, 예측 Negative)를 낮추는 데 초점을 맞춤.
    • 정밀도 : FP(실제Positive, 예측 Positive)를 낮추는 데 초점을 맞춤.


위와 같은 특성 때문에 재현율과 정밀도는 서로 보완적인 지표로 분류의 성능을 평가하는 데 적용 됩니다. 가장 좋은 성능평가는 재현율과 정밀도 모두 높은 수치를 얻는 것이며, 반면에 둘 중 어느 한 평가 지표에 편향되는 경우는 바람직하지 않습니다.

이번에는 오차 행렬 및 정밀도, 재현율을 모두 구해서 예측 성능을 평가해 보겠습니다. 사이킷런은 정밀도 계산을 위해 precision_score(), 재현율 계산을 위해 recall_score()를 API로 제공합니다.

from sklearn.metrics import accuracy_score, precision_score , recall_score , confusion_matrix

def get_clf_eval(y_test , pred):
    confusion = confusion_matrix(y_test, pred)
    accuracy = accuracy_score(y_test, pred)
    precision = precision_score(y_test, pred)
    recall = recall_score(y_test, pred)
    print('오차 행렬')
    print(confusion)
    print('정확도: {0:.4f}, 정밀도: {1:.4f}, 재현율: {2:.4f}'.format(accuracy, precision, recall))


이제 로지스틱 회귀 기반으로 타이타닉 생존자를 예측하고 confusion matrix, accuracy, precision, recall 평가를 수행합니다.

import numpy as np
import pandas as pd

from sklearn.model_selection import train_test_split 
from sklearn.linear_model import LogisticRegression

# 원본 데이터를 재로딩, 데이터 가공, 학습데이터/테스트 데이터 분할. 
titanic_df = pd.read_csv('./titanic/train.csv')
y_titanic_df = titanic_df['Survived']
X_titanic_df= titanic_df.drop('Survived', axis=1)
X_titanic_df = transform_features(X_titanic_df)
X_train, X_test, y_train, y_test = train_test_split(
    X_titanic_df,
    y_titanic_df,
    test_size=0.20,
    random_state=11
)

lr_clf = LogisticRegression(solver='liblinear')
lr_clf.fit(X_train, y_train)
pred = lr_clf.predict(X_test)
get_clf_eval(y_test, pred)
[output]
오차 행렬
[[108  10]
 [ 14  47]]
정확도: 0.8659, 정밀도: 0.8246, 재현율: 0.7705


정밀도/재현율 트레이드오프(Trade-off)

분류 결정 임계값(Threshold) 을 조정해 정밀도 또는 재현율의 수치를 높일 수 있습니다. 하지만 정밀도와 재현율은 상호 보완적인 평가 지표이기 때문에 정밀도/재현율은 트레이드오프(Trade-off) 를 가집니다.

사이킷런의 분류 알고리즘은 예측 데이터가 특정 레이블(Label, 결정 클래스 값)에 속하는지를 계산하기 위해 먼저 개별 레이블별로 결정 확률을 구합니다. 그리고 예측 확률이 큰 레이블값으로 예측 하게 됩니다. 가령 이진 분류 모델에서 특정 데이터가 0이 될 확률이 10%, 1이 될 확률이 90%로 예측됐다면 최종 예측은 더 큰 확률을 가진, 즉 90% 확률을 가진 1로 예측합니다. 일반적으로 이진 분류에서는 이 임곗값을 0.5, 즉 50%로 정하고 이 기준 값보다 확률이 크면 Positive, 작으면 Negative로 결정합니다.


사이킷런은 개별 데이터별로 예측 확률을 반환하는 메서드인 predict_proba() 를 제공합니다.

  • predict_proba()
    • 반환 결과가 예측 결과 클래스값이 아닌 예측 확률 결과.
    • 학습이 완료된 사이킷런 Classifier 객체에서 호출 가능.
    • 이진 분류에서 첫 번째 칼럼은 0 Negative의 확률, 두 번째 칼럼은 1 Positive의 확률.


앞 예제의 타이타닉 생존자 데이터를 학습한 LogisiticRegression 객체에서 predict_proba() 메서드와 predict() 메서드의 결과를 비교해 보겠습니다.

pred_proba = lr_clf.predict_proba(X_test)
pred = lr_clf.predict(X_test)
print('pred_proba()결과 Shape : {0}'.format(pred_proba.shape))
print('\npred_proba array에서 앞 3개만 샘플로 추출 \n:', pred_proba[:3])

# 예측 확률 array와 예측 결과값 array를 concatenate 하여 예측 확률과 결과값을 한눈에 확인
pred_proba_result = np.concatenate([pred_proba, pred.reshape(-1, 1)], axis=1)
print('\n두개의 class 중에서 더 큰 확률을 클래스 값으로 예측 \n', pred_proba_result[:3])
[output]
pred_proba()결과 Shape : (179, 2)

pred_proba array에서 앞 3개만 샘플로 추출 
: [[0.44935227 0.55064773]
 [0.86335512 0.13664488]
 [0.86429645 0.13570355]]

두개의 class 중에서 더 큰 확률을 클래스 값으로 예측 
 [[0.44935227 0.55064773 1.        ]
 [0.86335512 0.13664488 0.        ]
 [0.86429645 0.13570355 0.        ]]


반환 결과인 ndarray는 0과 1에 대한 확률을 나타내므로 첫 번째 칼럼 값과 두 번째 칼럼 값을 더하면 1이 됩니다. 두 개의 칼럼 중에서 더 큰 확률 값으로 predict() 메서드가 최종 예측하고 있습니다.

predict() 메서드는 predict_proba() 메서드에 기반해 생성된 API로, predict_proba() 호출 결과로 반환된 배열에서 분류 결정 임계값보다 큰 값이 들어 있는 칼럼의 위치를 받아 최종적으로 예측 클래스를 결정하는 API 입니다. 이는 사이킷런이 어떻게 정밀도/재현율 트레이드오프를 구현했는지를 이해할 수 있으며, 사이킷런은 분류 결정 임계값을 조절해 정밀도와 재현율의 성능 수치를 상호 보완적으로 조정할 수 있습니다.


사이킷런의 Binarizer 클래스 를 이용하여 정밀도/재현율 트레이드오프 방식을 이해해 보겠습니다.

다음 예제는 threshold 변수를 특정 값으로 설정하고 Binarizer 클래스 를 객체로 생성합니다. 생성된 Binarizer 객체fit_transform() 메서드를 이용하여 넘파이 ndarray 를 입력하면 입력된 ndarray 의 값을 지정된 threshold 보다 같거나 작으면 0값으로, 크면 1값으로 변환해 반환합니다.

from sklearn.preprocessing import Binarizer

X = [[ 1, -1,  2],
     [ 2,  0,  0],
     [ 0,  1.1, 1.2]]

# threshold 기준값보다 같거나 작으면 0을, 크면 1을 반환
binarizer = Binarizer(threshold=1.1)                     
print(binarizer.fit_transform(X))
[output]
[[0. 0. 1.]
 [1. 0. 0.]
 [0. 0. 1.]]


입력된 X 데이터 세트에서 Binarizerthreshold 값이 1.1보다 같거나 작으면 0, 크면 1로 변환됨을 알 수 있습니다. 각 클래스별 예측 확률값에 분류 결정 임계값(threshold)을 지정한 Binarizer 클래스 를 통해 최종 예측값을 구하는 predict() 를 만들겠습니다.

from sklearn.preprocessing import Binarizer

#Binarizer의 threshold 설정값. 분류 결정 임곗값임.  
custom_threshold = 0.5

# predict_proba() 반환값의 두번째 컬럼, 즉 Positive 클래스 컬럼 하나만 추출하여 Binarizer를 적용
pred_proba_1 = pred_proba[:,1].reshape(-1, 1)

binarizer = Binarizer(threshold=custom_threshold).fit(pred_proba_1) 
custom_predict = binarizer.transform(pred_proba_1)

get_clf_eval(y_test, custom_predict)
[output]
오차 행렬
[[108  10]
 [ 14  47]]
정확도: 0.8659, 정밀도: 0.8246, 재현율: 0.7705


해당 코드로 계산된 평가 지표는 앞 예제의 타이타닉 데이터로 학습된 로지스틱 회귀 Classifier 객체에서 호출된 predict() 로 계산된 지표 값과 정확히 같습니다. predict()predict_proba() 에 기반함을 알 수 있습니다.


만일 이 분류 결정 임계값을 0.4로 낮춰보겠습니다.

# Binarizer의 threshold 설정값을 0.4로 설정. 즉 분류 결정 임곗값을 0.5에서 0.4로 낮춤  
custom_threshold = 0.4
pred_proba_1 = pred_proba[:,1].reshape(-1, 1)
binarizer = Binarizer(threshold=custom_threshold).fit(pred_proba_1) 
custom_predict = binarizer.transform(pred_proba_1)

get_clf_eval(y_test, custom_predict)
[output]
오차 행렬
[[97 21]
 [11 50]]
정확도: 0.8212, 정밀도: 0.7042, 재현율: 0.8197


임계값을 낮추니 재현율 값이 올라가고 정밀도가 떨어졌습니다.

이유가 뭘까요? 분류 결정 임계값Positive 예측값을 결정하는 확률의 기준 이 됩니다. 확률이 0.5가 아닌 0.4부터 Positive로 예측을 더 너그럽게 하기 때문에 임계값 값을 낮출수록 True 값이 많아지게 됩니다.

image


Positive 예측값이 많아지면 상대적으로 재현율 값이 높아 집니다. 양성 예측을 많이 하다 보니 실제 양성을 음성으로 예측하는 횟수가 상대적으로 줄어들기 때문 입니다.

image


임계값이 0.5에서 0.4로 낮아지면서 TP 가 47에서 50으로 늘었고 FN 이 14에서 11로 줄었습니다. 그에 따라 재현율이 0.770에서 0.820으로 좋아졌습니다. 하지만 FP 는 10에서 21로 늘면서 정밀도가 0.825에서 0.704로 많이 나빠졌습니다. 그리고 정확도도 0.866에서 0.821로 나빠졌습니다.


이번에는 임계값을 0.4에서부터 0.6까지 0.05씩 증가시키며 평가 지표를 조사하겠습니다.

# 테스트를 수행할 모든 임곗값을 리스트 객체로 저장. 
thresholds = [0.4, 0.45, 0.50, 0.55, 0.60]

def get_eval_by_threshold(y_test, pred_proba_c1, thresholds):
    # thresholds list객체내의 값을 차례로 iteration하면서 Evaluation 수행.
    for custom_threshold in thresholds:
        binarizer = Binarizer(threshold=custom_threshold).fit(pred_proba_c1) 
        custom_predict = binarizer.transform(pred_proba_c1)
        print('\n임곗값:', custom_threshold)
        get_clf_eval(y_test, custom_predict)

get_eval_by_threshold(y_test, pred_proba[:,1].reshape(-1, 1), thresholds)
[output]
임곗값: 0.4
오차 행렬
[[97 21]
 [11 50]]
정확도: 0.8212, 정밀도: 0.7042, 재현율: 0.8197

임곗값: 0.45
오차 행렬
[[105  13]
 [ 13  48]]
정확도: 0.8547, 정밀도: 0.7869, 재현율: 0.7869

임곗값: 0.5
오차 행렬
[[108  10]
 [ 14  47]]
정확도: 0.8659, 정밀도: 0.8246, 재현율: 0.7705

임곗값: 0.55
오차 행렬
[[111   7]
 [ 16  45]]
정확도: 0.8715, 정밀도: 0.8654, 재현율: 0.7377

임곗값: 0.6
오차 행렬
[[113   5]
 [ 17  44]]
정확도: 0.8771, 정밀도: 0.8980, 재현율: 0.7213


재현율을 향상시키면서 다른 수치를 어느 정도 감소하는 희생을 해야 한다면 임계값 0.45가 가장 적당해 보입니다. 지금까지 임계값 변화에 따른 평가 지표 값을 알아보는 코드를 작성했는데, 사이킷런은 이와 유사한 precision_recall_curve() API 를 제공합니다.

  • precision_recall_curve()
    • y_true : 실제 클래스값 배열
    • probas_pred : Positive 칼럼의 예측 확률 배열


precision_recall_curve() 를 이용해 타이타닉 예측 모델의 임계값별 정밀도와 재현율을 구해 보겠습니다. 일반적으로 0.11 ~ 0.95 정도의 임계값을 담은 넘파이 ndarray이 임계값에 해당하는 정밀도 및 재현율 값을 담은 넘파이 ndarray 를 반환합니다.

from sklearn.metrics import precision_recall_curve

# 레이블 값이 1일때의 예측 확률을 추출 
pred_proba_class1 = lr_clf.predict_proba(X_test)[:, 1] 

# 실제값 데이터 셋과 레이블 값이 1일 때의 예측 확률을 precision_recall_curve 인자로 입력 
precisions, recalls, thresholds = precision_recall_curve(y_test, pred_proba_class1)
print('반환된 분류 결정 임곗값 배열의 Shape:', thresholds.shape)
print('반환된 precisions 배열의 Shape:', precisions.shape)
print('반환된 recalls 배열의 Shape:', recalls.shape)

print("\nthresholds 5 sample:", thresholds[:5])
print("precisions 5 sample:", precisions[:5])
print("recalls 5 sample:", recalls[:5])

# 반환된 임계값 배열 로우가 147건이므로 샘플로 10건만 추출하되, 임곗값을 15 Step으로 추출. 
thr_index = np.arange(0, thresholds.shape[0], 15)
print('\n샘플 추출을 위한 임계값 배열의 index 10개:', thr_index)
print('샘플용 10개의 임곗값: ', np.round(thresholds[thr_index], 2))

# 15 step 단위로 추출된 임계값에 따른 정밀도와 재현율 값 
print('\n샘플 임계값별 정밀도: ', np.round(precisions[thr_index], 3))
print('샘플 임계값별 재현율: ', np.round(recalls[thr_index], 3))
[output]
반환된 분류 결정 임곗값 배열의 Shape: (165,)
반환된 precisions 배열의 Shape: (166,)
반환된 recalls 배열의 Shape: (166,)

thresholds 5 sample: [0.01974988 0.06956414 0.08402808 0.08474207 0.08920161]
precisions 5 sample: [0.34078212 0.34269663 0.34463277 0.34659091 0.34857143]
recalls 5 sample: [1. 1. 1. 1. 1.]

샘플 추출을 위한 임계값 배열의 index 10개: [  0  15  30  45  60  75  90 105 120 135 150]
샘플용 10개의 임곗값:  [0.02 0.11 0.13 0.14 0.16 0.24 0.32 0.45 0.62 0.73 0.87]

샘플 임계값별 정밀도:  [0.341 0.372 0.401 0.44  0.505 0.598 0.688 0.774 0.915 0.968 0.938]
샘플 임계값별 재현율:  [1.    1.    0.967 0.902 0.902 0.902 0.869 0.787 0.705 0.492 0.246]


추출된 임계값 샘플 10개에 해당하는 정밀도 값과 재현율 값을 살펴보면 임계값이 증가할수록 정밀도값은 동시에 높아지나 재현율 값은 낮아짐을 알 수 있습니다. 정밀도와 재현율 곡선을 시각화해 보겠습니다.

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
%matplotlib inline

def precision_recall_curve_plot(y_test, pred_proba_c1):
    # threshold ndarray와 이 threshold에 따른 정밀도, 재현율 ndarray 추출. 
    precisions, recalls, thresholds = precision_recall_curve(y_test, pred_proba_c1)
    
    # X축을 threshold값으로, Y축은 정밀도, 재현율 값으로 각각 Plot 수행. 정밀도는 점선으로 표시
    plt.figure(figsize=(14, 8))
    
    threshold_boundary = thresholds.shape[0]
    plt.plot(thresholds, precisions[0:threshold_boundary], linestyle='--', label='precision')
    plt.plot(thresholds, recalls[0:threshold_boundary],label='recall')
    
    # threshold 값 X 축의 Scale을 0.1 단위로 변경
    start, end = plt.xlim()
    plt.xticks(np.round(np.arange(start, end, 0.1), 2))
    
    # x축, y축 label과 legend, 그리고 grid 설정
    plt.xlabel('Threshold value'); plt.ylabel('Precision and Recall value')
    plt.legend(); plt.grid()
    plt.show()
    
precision_recall_curve_plot( y_test, lr_clf.predict_proba(X_test)[:, 1] )

image


임계값이 낮을수록 많은 수의 양성 예측으로 인해 재현율 값이 극도로 높아지고 정밀도 값이 극도로 낮아지며, 임계값이 증가할수록 재현율 값이 낮아지고 정밀도 값이 높아지는 반대의 양상 이 됩니다.


정밀도와 재현율의 맹점

앞에서도 봤듯이 Positive 예측의 임계값을 변경함에 따라 정밀도와 재현율의 수치가 변경됩니다. 임곗값의 변경은 업무 환경에 맞게 두 개의 수치를 상호 보완할 수 있는 수준에서 적용돼야 합니다. 그렇지 않고 단순히 하나의 성능 지표 수치를 높이기 위한 수단으로 사용돼서는 안 됩니다. 다음은 정밀도 또는 재현율 평가 지표 수치 중 하나를 극단적으로 높이는 방법이지만 숫자놀음에 불과한 방법입니다.

정밀도가 100%가 되는 방법

확실한 기준이 되는 경우만 Positive로 예측하고 나머지는 모두 Negative로 예측합니다. 예를 들어 환자가 80세 이상이고 비만이며 이전에 암 진단을 받았고 암 세포의 크기가 상위 0.1% 이상이면 무조건 Positive, 다른 경우는 Negative 로 예측하는 겁니다.

$정밀도 = TP / (TP + FP)$ 입니다. 전체 환자 1000명 중 확실한 Positive 징후만 가진 환자는 단 1명이라고 하면 이 한 명만 Positive 로 예측하고 나머지는 모두 Negative 로 예측하더라도 FP 는 0, TP 는 1이 되므로 정밀도는 1/(1+0)으로 100% 가 됩니다.

재현율이 100%가 되는 방법

모든 환자를 Positive로 예측하면 됩니다. $재현율 = TP / (TP + FN)$ 이므로 전체 환자 1000명을 전부 Positive 로 예측하는 겁니다. 이 중 실제 양성인 사람이 30명 정도라도 TN 이 수치에 포함되지 않고 FN 은 아예 0이므로 재현율은 30/(30+0)으로 100% 가 됩니다.


이처럼 정밀도와 재현율 성능 수치도 어느 한쪽만 참조하면 극단적인 수치 조작이 가능합니다. 따라서 정밀도 또는 재현율 중 하나만 스코어가 좋고 다른 하나는 스코어가 나쁜 분류는 성능이 좋지 않은 분류로 간주할 수 있습니다. 정밀도와 재현율의 수치가 적절하게 조합돼 분류의 종합적인 성능 평가에 사용될 수 있는 평가 지표가 필요합니다.


F1 스코어

F1 스코어(Score)정밀도와 재현율을 결합한 지표 입니다. F1 스코어는 정밀도와 재현율이 어느 한쪽으로 치우치지 않는 수치를 나타낼 때 상대적으로 높은 값 을 가집니다. F1 스코어의 공식은 다음과 같습니다.

image


만일 A 예측 모델의 경우 정밀도가 0.9, 재현율이 0.1로 극단적인 차이가 나고, B 예측 모델은 정밀도가 0.5, 재현율이 0.5로 정밀도와 재현율이 큰 차이가 없다면 A 예측 모델의 F1 스코어 는 0.18이고, B예측 모델의 F1 스코어 는 0.5로 B 모델이 A모델에 비해 매우 우수한 F1 스코어를 가지게 됩니다.

사이킷런은 F1 스코어를 구하기 위해 f1_score() API 를 제공합니다. 이를 이용해 학습/예측한 로지스틱 회귀 기반 타이타닉 생존자 모델의 F1 스코어를 구해 보겠습니다.

from sklearn.metrics import f1_score

f1 = f1_score(y_test, pred)
print('F1 스코어: {0:.4f}'.format(f1))
[output]
F1 스코어: 0.7966


이번에는 타이타닉 생존자 예측에서 임계값을 변화시키면서 F1 스코어를 포함한 평가 지표를 구해 보겠습니다.

def get_clf_eval(y_test, pred):
    confusion = confusion_matrix(y_test, pred)
    accuracy = accuracy_score(y_test, pred)
    precision = precision_score(y_test, pred)
    recall = recall_score(y_test, pred)
    
    # F1 스코어 추가
    f1 = f1_score(y_test,pred)
    print('오차 행렬')
    print(confusion)
    # f1 score print 추가
    print('정확도: {0:.4f}, 정밀도: {1:.4f}, 재현율: {2:.4f}, F1:{3:.4f}'.format(accuracy, precision, recall, f1))

thresholds = [0.4 , 0.45 , 0.50 , 0.55 , 0.60]
pred_proba = lr_clf.predict_proba(X_test)
get_eval_by_threshold(y_test, pred_proba[:,1].reshape(-1, 1), thresholds)
[output]
임곗값: 0.4
오차 행렬
[[97 21]
 [11 50]]
정확도: 0.8212, 정밀도: 0.7042, 재현율: 0.8197, F1:0.7576

임곗값: 0.45
오차 행렬
[[105  13]
 [ 13  48]]
정확도: 0.8547, 정밀도: 0.7869, 재현율: 0.7869, F1:0.7869

임곗값: 0.5
오차 행렬
[[108  10]
 [ 14  47]]
정확도: 0.8659, 정밀도: 0.8246, 재현율: 0.7705, F1:0.7966

임곗값: 0.55
오차 행렬
[[111   7]
 [ 16  45]]
정확도: 0.8715, 정밀도: 0.8654, 재현율: 0.7377, F1:0.7965

임곗값: 0.6
오차 행렬
[[113   5]
 [ 17  44]]
정확도: 0.8771, 정밀도: 0.8980, 재현율: 0.7213, F1:0.8000