ML/평가(Evaluation)

오차행렬(Confusion Matrix), 정밀도(Precision), 재현율(Recall)

야뤼송 2024. 3. 11. 15:14
반응형

 

1. 오차행렬(Confusion Matrix)이란?

 

오차 행렬은 이진 분류의 예측 오류가 얼마인지와 더불어 어떠한 유형의 예측 오류가 발생하고 있는지를 함께 나타내는 지표이다.

 

 

오차 행렬은 크게 4가지로 나누어 볼수 있는데 간단히 설명하면 다음과 같다.

  • TN(True Negative) : 예측을 Negative로 하였고 그 결과도 Negative인 경우
  • TP(True Positive) : 예측을 Positive로 하였고 그 결과도 Positive인 경우
  • FN(True Negative) : 예측을 Negative로 하였으나 결과는 Positive인 경우
  • FP(True Positive) : 예측을 Positive로 하였으나 결과는 Negative인 경우

 실습

앞서 정확도 포스팅(정확도(Accuracy)란?)에서 MNIST 데이터셋을 이용하여 오차행렬을 구해보자.

 

MNIST 데이터셋을 이용하여 7인 값을 1로 변환,나머지는 0으로 변환하였다.

총 450개의 데이터 중 0인 데이터는 405개, 1인 데이터는 45개이며 예측을 실행하는 함수, MyFackeClassifier는 앞서 했던 것과 동일하게 모두 0으로 반환하여 모든 예측결과가 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
from sklearn.metrics import confusion_matrix

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

y = (digits.target == 7).astype(int)
X_train, X_test, y_train, y_test = train_test_split( digits.data, y, random_state=11)

fakeclf = MyFakeClassifier()
fakeclf.fit(X_train , y_train)
fakepred = fakeclf.predict(X_test)

 

사이킷런에서 제공하는 confusion_matrix를 이용하여  오차행렬을 구해보면 다음과 같다.

  • TN : 405, TP : 0, FN : 45, FP : 0    

오차행렬을 풀어서 설명하면 다음과 같다.

  • TN(True Negative) 결과를 살펴보면 모든 예측을 false로 하였고 실제 데이터 중 fasle인 데이터는 405개이므로 예측도 Negative이고 그 결과도 Negative인 TN은 405이다.
  • TP(True Postive) 결과를 살펴보면 모든 예측을 False로 하였기 때문에 예측이 True는 존재할 수가 없다. 그래서 TP는 0이 된다.
  • FN(False Negative) 결과를 살펴보면 모든 예측을 False로 하였고 실제 데이터 중 true인 데이터는 45개이므로 예측은 Negative이고 그 결과가 Positvie인 FN는 45이다.
  • FP(True Positive) 결과는 모든 예측이 False이므로 예측이 Postive는 나올 수 없다. 그러므로 FP는 0이 된다.
#MNIST 데이터셋의 예측 결과인 fakepred와 실제 결과인 y_test의 Confusion Matrix출력
confusion_matrix(y_test , fakepred)

▶ Out

array([[405,   0],
       [ 45,   0]])

 

 

앞의 포스팅에서 이야기한 정확도 지표의 문제점을 오차행렬을 통해서도 확인할 수 있다.

정확도는 전체 데이터를 예측 결과와 실제 값이 동일한 건수로 나눈 결과이다. 이를 오차 행렬로 표현하면 다음과 같다.

정확도는 90%로 이나 TP, FP가 모두 0이므로 Positive로 예측 자체를 수행하지 않았음을 확인할 수 있고 이는 예측모델에 문제가 있으며 정확도 지표의 문제점을 확인할 수 있다.

 

2. 정밀도(Precision)와 재현율(Recall)

 

정밀도는 예측을 Positive로 한 대상 중에 예측과 실제 값이 Positive로 일치한 데이터의 비율이다. 이를 식으로 표현하면 아래와 같다.


정밀도를 날씨로 예를들면 날씨 예측 모델이 맑다고 예측했는데 실제 날씨가 맑았는지를 보는 지표이다.

 

재현율은 실제 값이 Positive인 대상 중에 예측과 실제 값이 Positive로 일치한 데이터의 비율이다. 이를 식으로 표현하면 아래와 같다.

재현율을 날씨로 예를 들면 실제 날씨가 맑은 날중에서 모델이 맑다고 예측한 비율을 보는 지표이다.

 

즉, 정밀도와 재현율을 바라보고자 하는 관점이 다르다.  정밀도는 모델의 입장에서, 재현율은 실제 Positive 데이터 입장에서 결과가 Positive라고 맞춘 경우를 바라보는 것이다.

 

3. 정밀도와 재현율의 트레이드오프(Trade-Off)

 

정밀도와 재현율의 트레이드오프란 정밀도와 재현율은 상호보완적인 평가 지표이기에 어느 한쪽을 강제로 높이게 되면 다른 하나의 수치는 떨어지게 되는 것을 말한다.

 

정밀도와 재현율이 사용될 때 업무에 따라 상대적 중요도가 다를 수 있다.

먼저, 재현율이 상대적으로 더 중요한 경우는 실제 Positive인 데이터에 대해 예측을 Negative로 잘못 판단하게 되어 경우이다. 예를 들면 '암진단', '금융사기판별' 같은 경우가 있다.

 

정밀도가 상대적으로 더 중요한 경우는 실제 Negative인 데이터에  대해 예측을 Positive로 잘못 판단하게 되는 경우이다. 예를 들면

'스팸메일분류' 같은 경우이다.

 

분류하려는 위의 예시와 같이 업무의 특성상 정밀도 또는 재현율이 특별히 강조돼야 할 경우 분류의 결정 임계값(Threshold)을 조정해 정밀도 또는 재현율의 수치를 높일 수 있다.

 

분류 결정 임계값에 따른 Positive 예측 확률 변화를 살펴보자

 

분류 결정 임계값이 낮아질 수록 Positive로 예측할 확률이 높아지고 재현율도 증가하게 된다. 

사이킷런 Estimator 객체의 predict_proba() 메소드는 분류 결정 예측 확률을 반환하고 이를 이용하면 임의로 분류 결정 임계값을 조정하면서 예측 확률을 변경할 수 있다.

또한, precision_recall_curve() 함수를 통해 임계값에 따른 정밀도, 재현율의 변화값을 확인할 수 있다.

 

 실습

정밀도와 재현율의 트레이드오프와 임계값을 실습을 통해 확인해보자.

 

먼저 실습을 위해 이전 포스팅에서 다뤘던 타이타닉 생존예측(사이킷런을 이용한 타이타닉 생존자 예측)를 통해 오차행렬, 정밀도, 정확도, 재현율을 구해보자.

 

먼저 데이터 전처리를 위한 메소드를 생성한다.(자사한 내용은 위의 포스팅 참고)

 

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

 

 

다음은 오차행렬, 정확도, 정밀도, 재현율을 구하는 메소드를 생성해준다. 

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
import warnings 
warnings.filterwarnings('ignore')

# 원본 데이터를 재로딩, 데이터 가공, 학습데이터/테스트 데이터 분할. 
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)

 

이제 위에서 생성한 get_clf_eval() 함수를 호출하여 오차행렬, 정확도, 정밀도, 재현율을 구해보자

get_clf_eval(y_test , pred)

▶ Out

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

 

 

predict_proba() 메소드를 호출하면 2차원 배열로 결과값이 리턴된다.

결과를 살펴보면 이진분류에서 앞에꺼는 0이 될 확률이고 뒤에꺼는 1이될 확률이다.

샘플로 추출한 3개의 데이터를 살펴보자.

  • [0.44935226 0.55064774] : 0이될 확률은 0.44, 1이될 확률은 0.55
  • [0.86335512 0.13664488] : 0이될 확률은 0.86, 1이될 확률은 0.13
  • [0.86429644 0.13570356] : 0이될 확률은 0.86, 1이될 확률은 0.13
pred_proba = lr_clf.predict_proba(X_test)
pred  = lr_clf.predict(X_test)
print('pred_proba()결과 Shape : {0}'.format(pred_proba.shape))
print('pred_proba array에서 앞 3개만 샘플로 추출 \n:', pred_proba[:3])

▶ Out

pred_proba()결과 Shape : (179, 2)
pred_proba array에서 앞 3개만 샘플로 추출 
: [[0.44935226 0.55064774]
 [0.86335512 0.13664488]
 [0.86429644 0.13570356]]

 

 

예측 확률 Array(pred_proba)와 예측 결과 Array(pred)를 numpy의 concatenate() 함수를 이용하여 결과를 한눈에 살펴보자.

pred의 경우 1차원이므로 reshape()를 이용하여 2차원으로 바꿔준다.

예측결과와 예측확률 데이터를 보면 0이 될 확률과 1이 될 확률 중 더 큰 값으로 결과를 예측하는 것을 알 수 있다.

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

▶ Out

두개의 class 중에서 더 큰 확률을 클래스 값으로 예측 
 [[0.44935226 0.55064774 1.        ]
 [0.86335512 0.13664488 0.        ]
 [0.86429644 0.13570356 0.        ]]

 

이제 사이킷런에서 제공하는 Binarizer를 이용하여 분류 임계값 0.5를 변경해본다.

Binarizer의 간단하게 설명하면 설정한 기준값 초과시 1로, 같거나 작으면 0으로 변환해준다.

아래는 간단한 예시이다. 기준값을 1.1로 설정하고 기준 값을 넘으면 1로  작거나 같으면 0으로 변환해주는 것을 알 수 있다.

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

▶ Out

[[0. 0. 1.]
 [1. 0. 0.]
 [0. 0. 1.]]

 

 

이제 Binarizer를 이용하여 임계값을 변경하여 예측값 변화를 확인해보자.

임계값(Threshold)를 0.5로 하고 predict_proba()의 결과 값 중에서 Positive 값, 즉 뒤에 값만 적용을 해본다.

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)

▶ Out

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

 

이제 임계값을 0.5 -> 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)

▶ Out

오차 행렬
[[97 21]
 [11 50]]
정확도: 0.8212, 정밀도: 0.7042, 재현율: 0.8197

 

 

precision_recall_curve()  함수를 이용하여 임계값에 따른 정밀도, 재현율의 값을 추출할 수 있다.

먼저 predict_proba()함수를 호출하여 Positive 값만  추출해 준다. 이제 실제값이 담겨있는 y_test와  positive 값만 추출한 값을 precision_recall_curve() 파라미터로 입력해주면  임계값, 정밀도, 재현율 값을 반환해준다.

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

▶ Out

반환된 분류 결정 임곗값 배열의 Shape: (147,)
반환된 precisions 배열의 Shape: (148,)
반환된 recalls 배열의 Shape: (148,)
thresholds 5 sample: [0.11573102 0.11636722 0.11819212 0.12102774 0.12349479]
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]

 

임계값의 변화에 따른 재현율, 정밀도를 살펴보자.

먼저 임계값 배열 로우가 총 147개를 가지므로 샘플로 10개의 데이터를 추출하기 위해  15개 단위로 index 값을 추출한다.

이렇게 추출한 index에 해당하는 값을 추출하고 소수점 2자리에서 반올림 처리를 해준다.

이렇게 추출한 10개의 임계값을 이용해 정밀도와 재현율의 값을 살펴보면 임계값이 커짐에 따라 정밀도는 커지고 재현율은 작아지는 것을 다시한번 확인할 수 있다.

#반환된 임계값 배열 로우가 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], 3))
print('샘플 임계값별 재현율: ', np.round(recalls[thr_index], 3))

▶ Out

샘플 추출을 위한 임계값 배열의 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.379 0.424 0.455 0.519 0.618 0.676 0.797 0.93  0.964 1.   ]
샘플 임계값별 재현율:  [1.    0.967 0.902 0.902 0.902 0.82  0.77  0.656 0.443 0.213]

 

 

이제 마지막으로 임계값에 따라 재현율과 정밀도 변화를 그래프로 나타낼 수 있다.

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

 

▶ Out

 

반응형