안 쓰던 블로그

평가(Evaluation)_1 본문

머신러닝/머신러닝

평가(Evaluation)_1

proqk 2021. 1. 24. 17:25
반응형

분류 성능 평가 지표

1. 정확도(Accuracy)

2. 오차행렬(Confusion Matrix)

3. 정밀도(Precision)

4. 재현율(Recall)

5. F1 스코어: 정밀도와 재현율가 얼마나 균형 잡혀 있는가?

6. ROC AUC: 이진분류에서 많이 활용하는 성적 지표

 

1~4: 현재글

5~6: foxtrotin.tistory.com/440

실습: foxtrotin.tistory.com/441

 


1. 정확도

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

-직관적으로 모델 예측 선응을 나타내는 평가 지표

-이진 분류의 경우 데이터 구성에 따라 ML 모델의 성능을 왜곡할 수 있어서 이 수치 하나만 가지고 성능 평가 하지 않는다(타이타닉 예제에서 여성의 생존률이 높았기 때문에 특별한 알고리즘 없이 여성을 생존, 남성을 사망으로 분류해도 정확도는 높을 수 있다. 하나의 조건만 가지고 결정하는 알고리즘도 높은 정확도가 나올 수 있는 상황 발생)

-특히 불균형한 레이블 값 분포에서 실제와 다른 결과를 보일 수 있다


이진 분류의 경우 데이터 구성에 따라 ML 모델의 성능을 왜곡할 수 있어서 이 수치 하나만 가지고 성능 평가 하지 않는다. 어떻게 정확도가 왜곡이 되는지 더미분류기로 확인해 본다

 

사이킷런의 BaseEstimator클래스를 활용하여 단순히 성별에 따라 생존자를 예측하는 분류기를 생성하기

BaseEstimator: 커스터마이즈된 Estimator 생성 가능

import numpy as np
from sklearn.base import BaseEstimator

#분류기의 가장 기본이 되는 메소드 두 개: fit, predit
class MyDummyClassifier(BaseEstimator):
    # fit( ) 메소드는 아무것도 학습하지 않음. ->원래라면 학습에 대한 알고리즘들이 들어있음
    def fit(self, X , y=None):
        pass
    
    # predict( ) 메소드는 단순히 Sex feature가 1 이면 0 , 그렇지 않으면 1 로 예측함. 
    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

단순히 성별에 따라 생존자를 예측하는 classifier

 

preprocessing 함수들을 호출하여 데이터를 쪼갠다

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

 

위에서 만든 classifier를 가지고 학습 예측 평가를 수행한다

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

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

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

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

성별이 남자인지 아닌지만 따져서 생존사망만 넣었는데도 무려 정확도가 78.77%가 나왔다. 때문에 정확도를 평가할 때는 매우 신중해야 한다


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

가령, 100개의 데이터 중 90개의 레이블이 0, 10개의 레이블이 1인 경우, 무조건 0을 반환하는 모델을 만들면 정확도가 90%가 나온다

아래 코드로 이 내용을 실험해보고 실제로 그렇게 나오는지 확인해 본다

 

MNIST 데이터 세트를 변환하여 불균형한 데이터 세트를 만든 뒤 정확도 지표 적용해 보기

MNIST 데이터세트: 0부터 9까지의 숫자 이미지의 픽셀 정보를 가지고 있으며, 이를 기반으로 숫자 Digit을 예측하는데 사용한다

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)

기존의 데이터

 

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

digits.target==7을 하면 아래처럼 array가 생긴다

이 값의 False는 0으로, True는 1로 바꾼다(astype(int))

 

# 불균형한 레이블 데이터 분포도 확인. 
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)))

전체 데이터는 0이 405개, 1이 45개가 나온다

아까 예측을 단순히 0 한 가지 값만 가지고 했는데 0이 405개가 나왔으므로 정확도는 90%맞췄다고 뜬다

이처럼 정확도 평가지표는 불균형한 레이블 데이터 셋에서는 성능지표로 사용되서는 안 된다

->이를 극복하기 위해 정확도는 여러 지표와 함께 적용되어야 한다


2. 오차행렬(혼동 행렬, Confusion Matrix)

분류 문제에서 예측 오류가 얼마인지, 어떤 유형의 오류가 발생하고 있는지를 함께 나타내는 지표

(이 글에서도 한 번 다뤘다 foxtrotin.tistory.com/429 )

TN: 실제 Negative인데 예측도 Negative(지표로 쓰지 않음)

FN: 실제 Negative인데 예측은 Positive(실제 값이 있는데도 예측을 안 한 경우)

FP: 실제를 Positive로 했는데 예측을 Negative(틀린 경우-잘못 예측, IOU가 너무 낮음, 실제와 위치가 틀림)

TP: 실제를 Positive로 하고 예측도 Positive(예측과 실제가 맞은 경우)

 

위의 7에 대한 예제로 다시 생각하면 다음과 같다

TN: 7이 아닌데 7이 아니라고 예측

FN: 7이 아닌데 7이라고 예측

FP: 7인데 7이 아니라고 예측

TP: 7인데 7이라고 예측


사이킷런에서는 오차행렬을 구하기 위해 confusion_matrix()를 제공한다

from sklearn.metrics import confusion_matrix

# 앞절의 예측 결과인 fakepred와 실제 결과인 y_test의 Confusion Matrix출력
confusion_matrix(y_test , fakepred)

반환값은 ndarray형태로 TP, FP, FN, TN은 위의 표와 동일하게 배치된다

이 값을 보면, 총 450 결과값 중 TN: 405개, TP: 0개, FN: 45개, FP: 0개이다P는 틀리지도, 맞지도 않았으므로 아예 전체 Negative로 예측했다는 의미로 볼 수 있다

 

정확도 = 예측 결과와 실제 값이 동일한 건수/전체 데이터 수 = (TN+TP)/(TN+FP+FN+TP)


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

정밀도: 예측을 Positive로 한 대상 중에 예측과 실제 값이 Positive로 일치한 데이터 비율

재현율: 실제 값이 Positive인 대상 중 예측과 실제 값이 Positive로 일치한 데이터의 비율

 

정밀도 = TP / (FP + TP)

    예측을 다 하긴 함(FP, TP) 예측에는 맞기도, 틀리기도 했고, 그 중에서 맞은 것

    정밀도가 좋으려면 FP가 작아야 한다

재현율 = TP / (FN + TP)

    맞은 것과 아예 잡지 못 한 것 전체 중에서 맞는 것

    재현율이 좋으려면 FN이 작아야 한다

 

 

업무에 따른 재현율과 정밀도의 상대적 중요도와 맹점

재현율이 상대적으로 더 중요한 지표인 경우는 실제 Positive 양성인 데이터 예측을 Negative로 잘못 판단하게 되면 업무상 큰 영향이 발생하는 경우 : 암 진단, 금융사기 판별(틀리면 문제가 큰 분야)

정밀도가 상대적으로 더 중요한 지표인 경우는 실제 Negative 음성인 데이터 예측을 Positive 양성으 로 잘못 판단하게 되면 업무상 큰 영향이 발생하는 경우: 스팸 메일(스팸이 아닌 메일을 스팸이라고 하면 문제)

 

그렇다고 해서 정밀도를 100%, 재현율을 100%로 만들면 또 문제가 생긴다

정밀도를 100%으로 만드는 법: 확실한 기준이 되는 경우만 Positive로 예측, 나머지는 모두 Negative로 예측한다(누가봐도 암인 너무 정확한 사람만 암이라고 진단)

재현율을 100%으로 만드는 법: 모든 환자를 Positive로 예측한다(전체 환자를 암이라고 해 버림)

 

 

분류 결정 임계값에 따른 정밀도-재현율 변화

분류 결정 임계값이 낮을 수록 더 많은 예측 bounding box(bbox)를 만들어 낸다(난사)->정밀도는 낮아지고 재현율은 높아짐

분류 결정 임계값이 높을 수록 예측 bbox를 만드는데 매우 신중하게 된다->정밀도는 높아지고 재현율은 낮아짐

 

정밀도 재현율 트레이드 오프: 분류 결정 임계값(Threshold)를 조정하면 정밀도, 재현율의 수치를 높힐 수 있다. 그런데 정밀도와 재현율은 상호 보완적인 평가 지표라서 어느 한 쪽이 높아지면 다른 쪽은 떨어지기 쉽다

 

정밀도 재현율 곡선: Recall 값의 변화에 따른(분류 결정 임계값을 조정하면서 얻어진) Precision 값을 나타낸 곡선. Precision 값의 평균을 AP라고 하며, 일반적으로 정밀도 재현율 곡선의 면적 값으로 계산한다

 

분류 결정 임계

값보다 크면 무조건 1 아니면 0으로 분류된다

->분류 결정 임계값이 낮아질 수록 Positive로 예측할 확률이 높아짐. 재현율 증가(FN값이 작아짐-Negative였는데 틀린 경우의 비율인데 Negative로 예측하는 횟수조차 적어지기 때문)


임곗값의 변경에 따른 정밀도-재현율 변화 곡선 확인하기

사이킷런에서는 정밀도 계산을 위해 precision_score(), 재현율 계산을 위해 recall_score()를 제공한다

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

precision_recall_curvs()함수를 통해 임계값에 따른 정밀도, 재현율의 변화값을 제공한다

 

predict_proba() 메소드 확인

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

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

반환 결과인 ndarray는 0과 1에 대한 확률을 나타낸다

첫 번째 컬럼이 0이 될 확률, 두 번째 컬럼은 1이 될 확률로 두 값의 합은 1이 된다

그리고 두 확률 중 큰 값의 레이블 값으로 predict()메소드가 최종 예측을 한다

 

정밀도/재현율 트레이드오프를 살펴보기 위해 로직을 구현하기

사이킷런의 Binarizer클래스: fit_transform()을 이용하여 정해진 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))

기준값 1.1기준으로 큰 값은 1, 아니면 0이 반환되었다

 

위에서 생성한 predict_proba()의 결과 값에 Binarizer클래스를 적용하여 최종 예측 값을 구하고, 최종 예측 값을 평가하기

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

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)

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

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

임계값이 0.5->0.4로 조정되니까 재현율은 높아지고 정밀도는 낮아졌다

즉, 0.4부터 Positive로 예측을 하게 되었으므로 전체 Positive 수 대비 Positive로 예측된 값의 수가 많아졌다는 의미이다

 

-여러 개의 분류 결정 임곗값을 변경하면서 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 ,pred_proba[:,1].reshape(-1,1), thresholds )

임계값을 0.4에서부터 0.6까지 0.05씩 증가시키며 평가지표를 조사한다

해당 데이터에서 정밀도와 재현율의 중요도를 따져서 원하는 수치로 결정한다


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

precision_recall_curve(실제 클래스 값, 예측 확률 값): 임계값 변화에 따른 평가 지표 값을 반환하는 API

반환값: 정밀도-임계값별 정밀도 값을 배열로 반환, 재현율-임계값별 재현율 값을 배열로 반환

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

#반환된 임계값 배열 로우가 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))

임계값이 증가할 수록 정밀도는 높아지고 재현율은 낮아진다

 

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

그래프로 그리면 다음과 같이 나온다

 

 

반응형

'머신러닝 > 머신러닝' 카테고리의 다른 글

[평가] kaggle - Pima 인디언 당뇨병 예측  (0) 2021.01.25
평가(Evaluation)_2  (0) 2021.01.24
Linear Regression 선형 회귀  (0) 2021.01.21
RPN  (0) 2021.01.20
Faster RCNN  (0) 2021.01.20
Comments