by museonghwang

ROC 곡선과 AUC

|

ROC 곡선과 AUC

ROC 곡선과 이에 기반한 AUC 스코어는 이진 분류의 예측 성능 측정에서 중요하게 사용되는 지표 입니다. ROC 곡선(Receiver Operation Characteristic Curve) 은 우리말로 수신자 판단 곡선 으로 불립니다. ROC 곡선은 FPR(False Positive Rate)이 변할 때 TPR(True Positive Rate)이 어떻게 변하는지를 나타내는 곡선, 즉 FPR의 변화에 따른 TPR의 변화를 곡선 형태 로 나타납니다.

TPR(True Positive Rate)민감도(재현율) 를 나타내고, 민감도에 대응하는 지표로 TNR(True Negative Rate)특이성(Specificity) 을 나타냅니다.

  • 민감도(TPR) : 실제값 Positive(양성)가 정확히 예측돼야 하는 수준 을 나타냄(질병이 있는 사람은 질병이 있는 것으로 양성 판정).
  • 특이성(TNR) : 실제값 Negative(음성)가 정확히 예측돼야 하는 수준 을 나타냄(질병이 없는 건강한 사람은 질병이 없는 것으로 음성 판정).


\[TPR(True\ Positive\ Rate) = \frac{TP}{FN + TP}\] \[TNR(True\ Negative\ Rate) = \frac{TN}{FP + TN}\] \[FPR(False\ Positive\ Rate) = \frac{FP}{FP + TN} = 1 - TNR = 1 - 특이성\]


image


다음은 ROC 곡선의 예입니다. 가운데 직선은 ROC 곡선의 최저 값(AUC는 0.5)으로, 동전을 무작위로 던져 앞/뒤를 맞추는 랜덤 수준의 이진 분류의 ROC 직선입니다. ROC 곡선이 가운데 직선에 가까울수록 성능이 떨어지는 것이며, 멀어질수록 성능이 뛰어난 것 입니다.

ROC 곡선FPR을 0부터 1까지 분류 결정 임계값(Positive 예측값을 결정하는 확률의 기준)을 변경하면서 TPR의 변화값을 구합니다.

  • FPR을 0으로 만드는 방법 : 분류 결정 임계값을 1로 지정
    • 임계값을 1로 지정하면 Postive 예측 기준이 매우 높기 때문에 분류기(Classifier)가 임계값보다 높은 확률을 가진 데이터를 Positive로 예측할 수 없기 때문에 FPR은 0이 됩니다.
    • 즉, 아예 Positive로 예측하지 않기 때문에 FP 값이 0이 되므로 자연스럽게 FPR은 0이 됩니다.
  • FPR을 1으로 만드는 방법 : 분류 결정 임계값을 0으로 지정
    • 임계값을 0으로 지정하면 분류기의 Positive 확률 기준이 너무 낮아서 다 Positive로 예측합니다.
    • 즉, 아예 Negative 예측이 없기 때문에 TN이 0이 되고 FPR 값은 1이 됩니다.


정리하면 분류 결정 임계값을 1부터 0까지 변화시키면서 FPR을 구하고 이 FPR 값의 변화에 따른 TPR 값을 구하는 것ROC 곡선 입니다.

사이킷런은 ROC 곡선을 구하기 위해 roc_curve() API 를 제공합니다.

  • roc_curve() API
    • y_true : 실제 클래스 값 array
    • y_score : Positive 칼럼의 예측 확률 배열
    • 반환값 : FPR, TPR, 임계값


roc_curve() API를 이용해 타이타닉 생존자 예측 모델의 FPR, TPR, 임계값 을 구해 보겠습니다.

from sklearn.preprocessing import LabelEncoder

# Null 처리 함수
def fillna(df):
    df['Age'].fillna(df['Age'].mean(), inplace=True)
    df['Cabin'].fillna('N', inplace=True)
    df['Embarked'].fillna('N', inplace=True)
    df['Fare'].fillna(0, inplace=True)
    return df

# 머신러닝 알고리즘에 불필요한 피처 제거
def drop_features(df):
    df.drop(['PassengerId', 'Name', 'Ticket'], axis=1, inplace=True)
    return df

# 레이블 인코딩 수행.
def format_features(df):
    df['Cabin'] = df['Cabin'].str[:1]
    features = ['Cabin', 'Sex', 'Embarked']
    for feature in features:
        le = LabelEncoder()
        le = le.fit(df[feature])
        df[feature] = le.transform(df[feature])
    return df

# 앞에서 설정한 데이터 전처리 함수 호출
def transform_features(df):
    df = fillna(df)
    df = drop_features(df)
    df = format_features(df)
    return df
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
)
from sklearn.metrics import roc_curve

lr_clf = LogisticRegression(solver='liblinear')
lr_clf.fit(X_train, y_train)

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

fprs, tprs, thresholds = roc_curve(y_test, pred_proba_class1)

# 반환된 임곗값 배열에서 샘플로 데이터를 추출하되, 임곗값을 5 Step으로 추출. 
# thresholds[0]은 max(예측확률)+1로 임의 설정됨. 이를 제외하기 위해 np.arange는 1부터 시작
thr_index = np.arange(1, thresholds.shape[0], 5)
print('샘플 추출을 위한 임곗값 배열의 index:', thr_index)
print('샘플 index로 추출한 임곗값: ', np.round(thresholds[thr_index], 2))

# 5 step 단위로 추출된 임계값에 따른 FPR, TPR 값
print('\n샘플 임곗값별 FPR: ', np.round(fprs[thr_index], 3))
print('샘플 임곗값별 TPR: ', np.round(tprs[thr_index], 3))
[output]
샘플 추출을 위한 임곗값 배열의 index: [ 1  6 11 16 21 26 31 36 41 46]
샘플 index로 추출한 임곗값:  [0.94 0.73 0.62 0.52 0.44 0.28 0.15 0.14 0.13 0.12]

샘플 임곗값별 FPR:  [0.    0.008 0.025 0.076 0.127 0.254 0.576 0.61  0.746 0.847]
샘플 임곗값별 TPR:  [0.016 0.492 0.705 0.738 0.803 0.885 0.902 0.951 0.967 1.   ]


roc_curve() 의 결과를 살펴보면 임계값이 1에 가까운 값에서 점점 작아지면서 FPR 이 점점 커집니다. 그리고 FPR 이 조금씩 커질 때 TPR 은 가파르게 커짐을 알 수 있습니다. FPR 의 변화에 따른 TPR 의 변화를 ROC 곡선 으로 시각화해 보겠습니다.

import matplotlib.pyplot as plt
%matplotlib inline

def roc_curve_plot(y_test, pred_proba_c1):
    # 임곗값에 따른 FPR, TPR 값을 반환 받음. 
    fprs , tprs , thresholds = roc_curve(y_test ,pred_proba_c1)

    plt.figure(figsize=(14, 8))
    # ROC Curve를 plot 곡선으로 그림. 
    plt.plot(fprs, tprs, label='ROC')
    # 가운데 대각선 직선을 그림. 
    plt.plot([0, 1], [0, 1], 'k--', label='Random')
    
    # FPR X 축의 Scale을 0.1 단위로 변경, X,Y 축명 설정등   
    start, end = plt.xlim()
    plt.xticks(np.round(np.arange(start, end, 0.1), 2))
    plt.xlim(0,1); plt.ylim(0,1)
    plt.xlabel('FPR( 1 - Specificity )'); plt.ylabel('TPR( Recall )')
    plt.legend()
    plt.show()
    
roc_curve_plot(y_test, lr_clf.predict_proba(X_test)[:, 1])

image


일반적으로 ROC 곡선 자체는 FPRTPR의 변화 값을 보는 데 이용하며, 분류의 성능 지표로 사용되는 것은 ROC 곡선 면적에 기반한 AUC 값으로 결정합니다. AUC(Area Under Curve) 값은 ROC 곡선 밑의 면적을 구한 것으로서 일반적으로 1에 가까울수록 좋은 수치 입니다.

AUC 수치가 커지려면 FPR이 작은 상태에서 얼마나 큰 TPR을 얻을 수 있느냐가 관건 입니다. 가운데 직선에서 멀어지고 왼쪽상단 모서리 쪽으로 가파르게 곡선이 이동할수록 직사각형에 가까운 곡선이 되어 면적이 1에 가까워지는 좋은 ROC AUC 성능 수치를 얻게 됩니다. 가운데 대각선 직선은 랜덤 수준의(동전 던지기 수준) 이진 분류 AUC 값으로 0.5입니다. 따라서 보통의 분류는 0.5 이상의 AUC 값을 가집니다.

from sklearn.metrics import roc_auc_score

pred_proba = lr_clf.predict_proba(X_test)[:, 1]
roc_score = roc_auc_score(y_test, pred_proba)
print('ROC AUC 값: {0:.4f}'.format(roc_score))
[output]
ROC AUC 값: 0.8987


마지막으로 get_clf_eval() 함수에 roc_auc_score() 를 이용해 ROC AUC값을 측정하는 로직을 추가하는데, ROC AUC는 예측 확률값을 기반으로 계산되므로 이를 get_clf_eval() 함수의 인자로 받을 수 있도록 get_clf_eval(y_test, pred=None, pred_proba=None) 로 함수형을 변경해 줍니다. 이제 get_clf_eval() 함수는 정확도, 정밀도, 재현율, F1 스코어, ROC AUC 값까지 출력할 수 있습니다.

from sklearn.metrics import accuracy_score
from sklearn.metrics import precision_score, recall_score, confusion_matrix
from sklearn.metrics import f1_score
from sklearn.metrics import roc_curve, roc_auc_score

def get_clf_eval(y_test, pred=None, pred_proba=None):
    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_score(y_test, pred)
    # ROC-AUC 추가 
    roc_auc = roc_auc_score(y_test, pred_proba)
    
    print('오차 행렬')
    print(confusion)
    # ROC-AUC print 추가
    print('정확도: {0:.4f}, 정밀도: {1:.4f}, 재현율: {2:.4f}, F1: {3:.4f}, AUC:{4:.4f}'.format(accuracy, precision, recall, f1, roc_auc))
from sklearn.preprocessing import Binarizer

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, pred_proba_c1)

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, AUC:0.8987

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

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

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

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