3장_평가

14 minute read

본 포스팅은 [파이썬 머신러닝 완벽 가이드 _ 권철민 저] 도서를 기반으로 하고 있으며, 본인이 직접 요약, 정리한 내용입니다.

1 : Accuracy(정확도)

정확도는 실제 데이터에서 예측 데이터가 얼마나 같은지를 판단하는 지표이다.

$ 정확도(Accuracy)$ = ${예측 결과가 동일한 데이터 건수} \over {전체 예측 데이터 건수} $

그러나 이진 분류의 경우, 데이터의 구성에 따라 ML 모델의 성능을 왜곡할 수 있기 때문에 정확도 수치 하나만 가지고 성능을 평가하지 않는다.

아래의 예시를 보자

import numpy as np
from sklearn.base import BaseEstimator

# 사이킷 런은 BaseEstimator를 상속 받으면 Customized 형태의 Estimator를 개발자가
# 생성할 수 있다. 

# 다음의 MyDummyClassfier 함수는 단순히 'sex' feature 만으로 survive(target) 값을
# 분류하는 Classifier이다. 이때, 학습을 수행하지 않고, 바로 예측만 수행한다.
class MyDummyClassifier(BaseEstimator):
    # fit() 메서드는 아무것도 학습하지 않음.
    
    def fit(self, X, y = None):
        pass
    def predict(self, X):
        pred = np.zeros( (X.shape[0], 1))
        for i in range( X.shape[0]) :
            if X['Sex'].iloc[i] == 1:
                pred[i] = 0
            else :
                pred[i] = 1
                
        return pred
import pandas as pd
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

# 앞에서 설정한 Data Preprocessing 함수 호출
def transform_features(df):
    df = fillna(df)
    df = drop_features(df)
    df = format_features(df)
    return df
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# 원본 데이터를 재로딩, 데이터 가공, 학습데이터/테스트 데이터 분할. 
titanic_df = pd.read_csv('C:/Users/Oh Won Jin/Python/PerfectGuide/3장/train.csv')
y_titanic_df = titanic_df['Survived']
X_titanic_df= titanic_df.drop('Survived', axis=1)
# transform_features 함수 - 각 column에 레이블 인코딩을 적용
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.2, random_state=0)

# 위에서 생성한 Dummy Classifier를 이용하여 학습/예측/평가 수행. 
myclf = MyDummyClassifier()
# 물론 fit에서 학습은 수행되지 않는다.
myclf.fit(X_train ,y_train)

mypredictions = myclf.predict(X_test)
print('Dummy Classifier의 정확도는: {0:.4f}'.format(accuracy_score(y_test , mypredictions)))
Dummy Classifier의 정확도는: 0.7877

이렇듯 따로 학습을 하지 않고도, 0.7877의 꽤 높은 ‘정확도’를 나타내기에 이를 평가 지표로 사용할 때는 매우 신중해야 한다.

특히 정확도는 불균형한 레이블 값 분포에서 ML 모델의 성능을 판단할 경우, 적합한 평가 지표가 아니다.

다음의 예시는 MNIST 세트를 변환하여 불균형한 데이터 세트로 만든 뒤에 정확도 지표 적용 시 어떤 문제가 발생할 수 있는지 살펴보겠다. 이때, 레이블 값이 7인 것만 True, 나머지 값은 모두 False로 변환해 이진 분류 문제로 바꿔보겠다.

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()

print(digits.data)
print("### digits.data.shape:", digits.data.shape)
print(digits.target)
print("### digits.target.shape:", digits.target.shape)
[[ 0.  0.  5. ...  0.  0.  0.]
 [ 0.  0.  0. ... 10.  0.  0.]
 [ 0.  0.  0. ... 16.  9.  0.]
 ...
 [ 0.  0.  1. ...  6.  0.  0.]
 [ 0.  0.  2. ... 12.  0.  0.]
 [ 0.  0. 10. ... 12.  1.  0.]]
### digits.data.shape: (1797, 64)
[0 1 2 ... 8 9 8]
### digits.target.shape: (1797,)
# 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('모든 예측을 0으로 하여도 정확도는:{:.3f}'.format(accuracy_score(y_test , fakepred)))
레이블 테스트 세트 크기 : (450,)
테스트 세트 레이블 0 과 1의 분포도
0    405
1     45
dtype: int64
모든 예측을 0으로 하여도 정확도는:0.900

즉, 위에서 보듯이 모든 예측을 0으로 하여도 정확도가 0.9로 매우 높은 값을 가짐을 알 수 있다.

2 : 오차 행렬(Confusion Matrix)

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

  • TN은 예측값을 Negative 값 0으로 예측했고 실제 값 역시 Negative 값 0
  • FP는 예측값을 Positive 값 1로 예측했는데 실제 값은 Negative 값 0
  • FN은 예측값을 Negative 값 0으로 예측했는데 실제 값은 Positive 값 1
  • TP는 예측값을 Positive 값 1로 예측했는데 실제 값 역시 Positive 값 1

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

이를 통해 MyFakeClassifier의 예측 성능 지표를 오차 행렬로 표현해보자.

from sklearn.metrics import confusion_matrix
confusion_matrix(y_test, fakepred)
array([[405,   0],
       [ 45,   0]], dtype=int64)

위 행렬에서 columns은 예측 클래스 [Negative, Positive]를 의미하고, rows는 실제 클래스 [Negative, Positive]를 의미한다.

  • Tips

    일반적으로 이러한 불균형한 레이블 클래스를 가지는 이진 분류 모델에서는 많은 데이터 중에서 중점적으로 찾아야 하는 매우 적은 수의 결괏값에 Positive를 설정해 1 값을 부여하고, 그렇지 않은 경우는 Negative로 0 값을 부여하는 경우가 많다.

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

10000건의 데이터 세트에서 9900건이 Negative이고 100건이 Positive라면 Negative로 예측하는 경향이 더 강해져 TN은 매우 커지고, TP는 매우 작아지게 된다. 또한, Negative로 예측할 때 정확도가 높기 때문에 FN이 매우 작고, Positive로 예측하는 경우가 작기 때문에 FP 역시 매우 작아진다.

결과적으로 정확도 지표는 비대칭한 데이터 세트에서 Positive에 대한 예측 정확도를 판단하지 못한 채 Negative에 대한 예측 정확도만으로도 분류의 정확도가 매우 높게 나타나는 수치적인 판단 오류를 일으키게 되는 것이다.

다음으로 불균형한 데이터 세트에서 정확도보다 더 선호되는 평가 지표인 정밀도(Precision)와 재현율(Recall)에 대해 알아보자.

3 : 정밀도(Precision) 과 재현율(Recall)

  • 정밀도 = TP / (TP + NP)
  • 재현율 = TP / (TP + FN)

Tip

정밀도 : 모델이 예측을 얼마나 더 ‘정밀’하게 했는가!

재현율 : 모델이 실제 값을 얼마나 많이 ‘재현’했는가!

재현율이 중요 지표인 경우 : 실제 Positive 양성 데이터를 Negative로 잘못 판단하게 되면 업무상 큰 영향이 발생하는 경우이다. ex) 암 양성, 금융 거래 사기

정밀도가 중요 지표인 경우 : 실제 Negative 음성 데이터를 Positive로 잘못 판단하게 되면 업무상 큰 영향이 발생하는 경우이다. ex) 스팸 메일

사이킷 런은 정밀도 계산을 위해 precision_score()을, 재현율 계산을 위해 recall_score()을 API로 제공한다.

MyFakeClassifier의 예측 결과로 정밀도와 재현율 측정

from sklearn.metrics import accuracy_score, precision_score, recall_score

print(precision_score(y_test, fakepred))
print(recall_score(y_test,fakepred))
0.0
0.0


C:\Users\Oh Won Jin\Anaconda3\lib\site-packages\sklearn\metrics\classification.py:1437: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 due to no predicted samples.
  'precision', 'predicted', average, warn_for)

위 결과에서 확인할 수 있듯이, 정밀도와 재현율 에서의 TP가 0이므로, 모두 0의 값을 가진다.

다음은 평가를 간편하게 적용하기 위해서 confusion matrix,accuracy, precision, recall 등의 평가를 한꺼번에 호출하는 get_clf_eval() 함수를 만들도록 하겠다.

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))
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('C:/Users/Oh Won Jin/Python/PerfectGuide/3장/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()

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


C:\Users\Oh Won Jin\Anaconda3\lib\site-packages\sklearn\linear_model\logistic.py:432: FutureWarning: Default solver will be changed to 'lbfgs' in 0.22. Specify a solver to silence this warning.
  FutureWarning)

위 결과에서 정밀도에 비해 재현율이 낮게 나왔음을 확인할 수 있다.

정밀도를 높히려면, TP를 높히고, FP 를 낮춰야한다.

재현율을 높히려면, TP를 높히고, FN 을 낮춰야 한다.

Precision/Recall Trade-off

사이킷런의 분류 알고리즘은 예측 데이터가 특정 레이블에 속하는지를 계산하기 위해 먼저 개별 레이블 별로 예측 확률을 구한다. 그리고 예측 확률이 큰 레이블 값으로 예측하게 된다. 가령 이진 분류 모델에서 특정 데이터가 0이 될 확률이 10%, 1이 될 확률이 90%로 예측됐다면 최종 예측은 더 큰 확률을 가진, 즉 90%확률을 가진 1로 예측한다.

  • 일반적으로 이진 분류에서는 이 임곗값 을 0.5, 즉 50%로 정하고 이 기준 값보다 확률이 크면 Positive, 작으면 Negative로 결정한다.

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

개별 클래스의 예측 확률을 ndarray( m x n : m은 입력 값의 레코드 수, n은 클래스 값 유형) 형태로 반환한다.

predict_proba( ) 메소드 확인

predict_proba = lr_clf.predict_proba(X_test)
pred = lr_clf.predict(X_test)
print(predict_proba[:3])
print(pred[:3].reshape(-1,1))
[[0.44935228 0.55064772]
 [0.86335513 0.13664487]
 [0.86429645 0.13570355]]
[[1]
 [0]
 [0]]

위 결과에서처럼 사이킷 런의 predict()는 predict_proba() 메서드가 반환하는 확률 값을 가진 ndarray에서 정해진 임곗값을 만족하는 ndarray의 칼럼 위치를 최종 예측 클래스로 결정하는 것이다. 이를 구현하기 위해 사이킷 런의 Binarizer 클래스를 이용한다.

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))
[[0. 0. 1.]
 [1. 0. 0.]
 [0. 0. 1.]]

분류 결정 임계값 0.5 기반에서 Binarizer를 이용하여 예측값 변환

from sklearn.preprocessing import Binarizer

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

#predict_proba() 반환 값의 두 번째 컬럼, 즉 Positive 클래스 컬럼 하나만 추출하여 
#Binarizer을 적용

pred_proba_1 = predict_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)
오차 행렬
[[108  10]
 [ 14  47]]
정확도: 0.8659, 정밀도: 0.8246, 재현율: 0.7705

위 결과에서 임곗값을 0.4로 낮추면 어떻게 될까?

custom_threshold = 0.4

pred_proba_1 = predict_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)
오차 행렬
[[97 21]
 [11 50]]
정확도: 0.8212, 정밀도: 0.7042, 재현율: 0.8197

여러개의 분류 결정 임곗값을 변경하면서 Binarizer를 이용하여 예측값 변환

# 테스트를 수행할 모든 임곗값을 리스트 객체로 저장. 
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('임곗값:',custom_threshold)
        get_clf_eval(y_test , custom_predict)

get_eval_by_threshold(y_test ,predict_proba[:,1].reshape(-1,1), thresholds )
임곗값: 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

precision_recall_curve( ) 를 이용하여 임곗값에 따른 정밀도-재현율 값 추출

사이킷 런은 이와 유사한 precison_recall_curve() API를 제공한다. precision_recall_curve() API의 입력 파라미터와 반환 값은 다음과 같다.

  • 입력 파라미터

    y_true : 실제 클래스 값 배열 (배열 크기 = [데이터 건수])

probas_pred : Positive 칼럼의 예측 확률 배열 (배열 크기 = [데이터 건수])

  • 반환 값

    정밀도 : 임곗값 별 정밀도 값을 배열로 반환

재현율 : 임곗값 별 재현율 값을 배열로 반환

임곗값

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("thresholds 5 sample:", thresholds[:5])
print("precisions 5 sample:", precisions[:5])
print("recalls 5 sample:", recalls[:5])

반환된 분류 결정 임곗값 배열의 Shape: (147,)
반환된 precisions 배열의 Shape: (148,)
반환된 recalls 배열의 Shape: (148,)
thresholds 5 sample: [0.11573101 0.11636721 0.11819211 0.12102773 0.12349478]
precisions 5 sample: [0.37888199 0.375      0.37735849 0.37974684 0.38216561]
recalls 5 sample: [1.         0.98360656 0.98360656 0.98360656 0.98360656]

위 반환된 배열에서 확인할 수 있듯이, 반환되는 임곗값이 너무 작은 값 단위로 많이 구성되어 있다. 따라서 임곗값을 15단계로 추출하여 좀 더 큰 값의 임곗값과 그때의 정밀도와 재현율 값을 같이 살펴보겠다.

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

# 15 step 단위로 추출된 임계값에 따른 정밀도와 재현율 값 
print('샘플 임계값별 정밀도: ', np.round(precisions[thr_index], 2))
print('샘플 임계값별 재현율: ', np.round(recalls[thr_index], 2))
샘플 추출을 위한 임계값 배열의 index 10개: [  0  15  30  45  60  75  90 105 120 135]
샘플용 10개의 임곗값:  [0.12 0.13 0.15 0.17 0.26 0.38 0.49 0.63 0.76 0.9 ]
샘플 임계값별 정밀도:  [0.38 0.42 0.45 0.52 0.62 0.68 0.8  0.93 0.96 1.  ]
샘플 임계값별 재현율:  [1.   0.97 0.9  0.9  0.9  0.82 0.77 0.66 0.44 0.21]

위 결과에서 알 수 있듯이 임곗값이 높아질 수록 정밀도 또한 높아진다

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=(8,6))
    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] )

output_52_0

4 : F1 Score

F1 Score는 정밀도와 재현율을 결합한 지표이다.

  • F1 Score는 정밀도와 재현율이 어느 한 쪽으로 치우치지 않는 수치를 나타낼 때 상대적으로 높은 값을 가진다.

  • F1 Score의 공식

$F1 = $ $2 \over { {1 \over recall} + {1 \over precision }} $ $ = 2*{ {precision * recall} \over {precision + recall } }$

만일 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를 제공한다.

from sklearn.metrics import f1_score 
f1 = f1_score(y_test , pred)
print('F1 스코어: {0:.4f}'.format(f1))
F1 스코어: 0.7966
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)

임곗값: 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

위 결과에서 F1 값은 임곗값이 0.6일 때 가장 크지만, 재현율이 급격히 감소함을 주지하자.

5 : ROC Curve와 AUC

  • ROC 곡선과 이에 기반한 AUC 스코어는 이진 분류의 예측 성능 측정에서 중요하게 사용되는 지표이다.

  • ROC 곡선은 FPR이 변할 때 TPR(재현율)이 어떻게 변하는지를 나타내는 곡선이다.

  • 민감도(= 재현율, TPR)는 실제값 Positive가 정확히 예측돼야 하는 수준을 나타낸다.

  • __특이성(TNR)__은 실제값 Negative가 정확히 예측돼야 하는 수준을 나타낸다.

  • FPR = FP / (FP + TN ) = 1 - TNR이다.

    즉, Negative라고 예측한 수 중에서 (실제로는 Positive이지만)잘못 예측했을 비율이다!

ROC 곡선이 가운데 직선에 가까울 수록 성능이 떨어지는 것이며, 멀어질 수록 성능이 뛰어난 것이다.

즉, FPR이 작고( Negative를 잘 못 재현한 것이 작고) TPR이 높은 ( Positive를 잘 재현한 것이 높은) 곡선이 좋은 것이다.

ROC 곡선은 FPR을 0부터 1까지 변경하면서 TPR의 변화를 구한다. 그렇다면 어떻게 FPR을 0부터 1까지 변경할 수 있을까?

바로 앞에서 배운 분류 결정 임곗값을 변경하면서 FPR의 변화를 이룬다.

임곗값이 0일 때 모델이 모든 예측 값을 Positive로 판단하므로 TN이 0이 되어 FPR은 1이 된다.

임곗값이 1일 때 모델이 모든 예측 값을 Negative로 판단하므로 FP가 0이 되어 FPR은 0이 된다.

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

입력 파라미터는 실제 클래스 값과 에측 확률 값이다

반환 값은 fpr, tpr, 임곗값이다.

from sklearn.metrics import roc_curve

# 레이블 값이 1일 때 예측 확률을 추출
pred_proba_class1 = lr_clf.predict_proba(X_test)[:,1]
fprs, tprs, thresholds = roc_curve(y_test, pred_proba_class1)
# 반환된 임곗값 배열 로우가 47건이므로 샘플로 10건만 추출하되, 임곗값을 5 Step으로 추출. 
thr_index = np.arange(0, thresholds.shape[0], 5)
print('샘플 추출을 위한 임곗값 배열의 index 10개:', thr_index)
print('샘플용 10개의 임곗값: ', np.round(thresholds[thr_index], 2))

# 5 step 단위로 추출된 임계값에 따른 FPR, TPR 값
print('샘플 임곗값별 FPR: ', np.round(fprs[thr_index], 3))
print('샘플 임곗값별 TPR: ', np.round(tprs[thr_index], 3))

샘플 추출을 위한 임곗값 배열의 index 10개: [ 0  5 10 15 20 25 30 35 40 45]
샘플용 10개의 임곗값:  [1.94 0.87 0.63 0.55 0.44 0.32 0.15 0.14 0.13 0.12]
샘플 임곗값별 FPR:  [0.    0.008 0.025 0.059 0.127 0.203 0.559 0.602 0.695 0.847]
샘플 임곗값별 TPR:  [0.    0.246 0.672 0.738 0.787 0.885 0.902 0.951 0.967 0.984]
def roc_curve_plot(y_test , pred_proba_c1):
    # 임곗값에 따른 FPR, TPR 값을 반환 받음. 
    fprs , tprs , thresholds = roc_curve(y_test ,pred_proba_c1)

    # 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 - Sensitivity )'); plt.ylabel('TPR( Recall )')
    plt.legend()
    plt.show()
    
roc_curve_plot(y_test, lr_clf.predict_proba(X_test)[:, 1] )

output_64_0

  • 일반적으로 ROC 곡선 자체는 FPR과 TPR의 변화 값을 보는데 이용하며 분류의 성능 지표로 사용되는 것은 ROC 곡선 면적에 기반한 AUC 값으로 결정한다.

  • AUC(Area Under Curve) 값은 ROC 곡선 밑의 면적을 구한 것으로서 일반적으로 1에 가까울 수록 좋은 수치이다. AUC 수치가 커지려면 FPR이 작은 상태에서 얼마나 큰 TPR을 얻을 수 있느냐가 관건이다.

  • 가운데 대각선 직선은 랜덤 수준의 이진 분류 AUC 값으로 0.5이다. 따라서 보통의 분류는 0.5 이상의 값을 가진다.

사이킷 런에서는 roc_auc_score 클래스를 제공한다.

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))
ROC AUC 값 : 0.8987

다음의 코드를 통하여 get_clf_eval() 함수는 정확도, 정밀도, 재현율, F1스코어, ROC AUC 값까지 출력할 수 있게 된다.

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


Tags:

Categories:

Updated: