머신러닝

[3장] 파이썬 머신러닝 완벽가이드_평가_1

zsun 2023. 5. 5. 00:45

[ 머신러닝 프로세스 ]

1. 데이터 가공/변환
2. 모델 학습/예측

3. 평가

  • 성능 평가 지표(Evaluation Metric)는 모델이 회귀인지 분류인지에 따라 여러 종류로 나뉨
  • 회귀의 경우 대부분 실제값과 예측값의 오차 평균값에 기반
  • 분류의 성능 평가 지표
    - 정확도(Accuracy)
    - 오차행렬(Confusion Matrix)
    - 정밀도 (Precision)
    - 재현율 (Recall)
    - F1 스코어
    - ROC AUC

  • 분류는 결정 클래스 값 종류의 유형에 따라 긍정/부정과 같은 2개의 결과만 갖는 이진 분류와 
    여러개의 결정 클래스 값을 갖는 멀티 분류로 나뉨

01. 정확도(Accuracy)

  • 정확도 : 실제 데이터에서 예측 데이터가 얼마나 같은지 판단하는 지표
    = 예측 결과가 동일한 데이터 건수 / 전체 예측 데이터 건수

  • 정확도는 직관적으로 모델 예측 성능을 나타내는 평가 지표이지만
    이진 분류의 경우 정확도만으로 성능 평가하면 안됨 (ML 모델의 성능을 왜곡할 수 있기 때문)

 

타이타닉 데이터의 정확도 구하기

import numpy as np
from sklearn.base import BaseEstimator

class MyDummyClassifier(BaseEstimator):
    # fit( ) 메소드는 아무것도 학습하지 않음. 
    def fit(self, X , y=None):
        pass
    
    # predict( ) 메소드는 단순히 Sex feature가 1 이면 0 , 그렇지 않으면 1 로 예측함. 
    # self는 객체화된 자신의 객체를 가르키는 변수이지만 파이썬 문법에서는 메소드의 인자로 넣어줘야 함
    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

# BaseEstimator 클래스를 상속받아 아무런 학습을 하지 않고, 
# 성별에 따라 생존자를 예측하는 단순한 Classifier를 생성함
# MyDummyClassifier 클래스는 학습을 수행하는 fit() 메서드는 아무것도 수행하지 않으며
# 예측을 수행하는 predict() 메서드는 Sex피처가 1이면 0, 그렇지 않으면 1로 예측하는 단순한 Classifier

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
# 생성된 MyDummyClassifier를 이용해 타이타닉 생존자 예측 수행

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

수행 결과, 정확도는 0.7877

 

하지만,

정확도는 불균형한(imbalanced) 레이블 값 분포에서 ML 모델의 성능을 판단할 경우, 적합한 평가 지표 X
예를 들어 100개의 데이터(90개는 0, 10개는 1)를 무조건 0으로 예측 결과를 반환하는 모델의 경우 정확도가 90% 임

 

 

 

평가의 지표로 정확도 사용 시 발생할 수 있는 문제점 (MNIST 데이터셋 활용)

 

* MNIST 데이터셋

- 0부터 9까지의 숫자 이미지의 픽셀 정보를 가지고 있음
- 이를 기반으로 숫자 Digit을 예측하는데 사용
- 사이킷런은 load_digits()를 API를 통해 MNIST 데이터셋 제공

< 진행 조건 >
원래 MNIST 데이터셋은 레이블 값이 0부터 9까지 있는 멀티 레이블 분류를 위한 것이지만
이것을 레이블이 7인것만 True, 나머지는 False로 변환해 이진 분류 문제로 변경
즉, 전체 데이터의 10%만 True, 나머지는 False

# 불균형한 데이터셋과 Dummy Classifier 생성

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)
# 불균형한 데이터로 생성한 y_test의 데이터 분포도를 확인하고 MyFakeClassifier를 이용해 예측과 평가
# 불균형한 레이블 데이터 분포도 확인. 
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으로 하여도 정확도는 0.900

--> 이러한 문제를 해결하기 위해 오차행렬 사용

 

 

 

02. 오차행렬

- 오차행렬

: 이진 분류에서 성능 지표로 잘 활용되는 오차행렬(confusion matrix)은 학습된 분류 모델이
  예측을 수행하면서 얼마나 헷갈리고 있는지 보여주는 지표
이진 분류의 예측 오류가 얼마인지와 함께 어떠한 유형의 예측 오류가 발생하고 있는지 함께 보여줌

 

 

 

- 사이킷런은 오차 행렬을 구하기 위해 confusion_matrix() API를 제공함
from sklearn.metrics import confusion_matrix

 

 

 

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

 

  • 정밀도와 재현율은 Positive 데이터셋의 예측 성능에 좀 더 초점을 맞춘 평가 지표

 

정밀도(Precision)

- 예측을 Positive로 한 대상 중에 예측과 실제 값이 Positive로 일치한 비율
- TP / (FP+TP)
- Positive 예측 성능을 더욱 정밀하게 측정하기 위한 평가 지표로 양성 예측도라고도 불림

 

재현율(Recall)

- TP / (FN+TP)
- 실제 값이 positive 인 대상 중에 예측과 실제 값이 Positive로 일치한 데이터의 비율

- 민감도(Sensitivity) 라고도 함

 

 

  • 재현율이 중요한 경우
    : 실제 Positive인 데이터 예측을 Negative로 잘못 판단하면 업무상 큰 영향있는 경우

  • 정밀도가 중요한 경우
    : 실제 Negative인 데이터 예측을 Positive로 잘못 판단하면 업무상 큰 영향있는 경우

  • 가장 좋은 성능 평가는 재현율과 정밀도 모두 높은 수치를 얻는 것

 

 

타이타닉 예제를 정밀도 재현율로 예측 성능 평가


- 정밀도 계산 : precision_score()
- 재현율 계산 : recall_score()

# get_clf_eval() 함수 생성
: confusion matrix, accuracy, precision, recall 등의 평가를 한번에 호출

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

 

# 로지스틱 회귀 기반으로 타이타닉 생존자 예측
: LogisticRegression 객체의 생성 인자로 입력되는 solver='liblinear'는
로지스틱 회귀의 최적화 알고리즘 유형을 지정하는 것


(* solver의 기본값은 lbfgs이며 데이터셋이 상대적으로 크고 다중 분류인 경우 적합,
작은 데이터셋의 이진 분류인 경우는 liblinear가 성능이 좋은 경향이 있음)

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

 

 

정밀도 / 재현율 트레이드오프

 

  • 정밀도와 재현율은 상호보완적인 평가지표로, 한쪽을 높이면 한쪽의 수치는 떨어지기 쉬움 : 트레이드오프
  • 사이킷런의 분류 알고리즘은 예측 데이터가 특정 레이블(결정클래스값)에 속하는지를 계산하기 위해
    먼저 개별 레이블별로 결정 확률을 구함 -> 예측 확률이 큰 레이블 값으로 예측하게 됨

  • predict_proba() : 사이킷런은 개별 데이터별로 예측 확률을 반환하는 메서드
    - predict_proba() 메서드는 학습이 완료된 사이킷런 classifier 객체에서 호출이 가능하며
    - 테스트 피처 데이터셋을 파라미터로 입력해주면 테스트 피처 레코드의 개별 클래스 예측 확률 반환
    - predict() 메서드와 유사하지만 반환 결과가 예측 결과 클래스 값이 아닌 예측 확률 결과
    - 개별 클래스의 예측 확률을 ndarray m X n (m:입력 레코드 수 x n:클래스 값 유형) 형태로 반환
    --> 입력 테스트 데이터셋의 표본 개수가 100개이고 예측 클래스 값 유형이 2개(이진 분류)라면
           반환값 : 100*2 ndarray임
  • 각 열은 개별 클래스의 예측 확률,
    이진 분류에서 첫번째 컬럼은 0 Negative 의 확률, 두번째 확률은 1 Positive 확률

# 예시

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

# predict()는 predict_proba() 메서드가 반환하는 확률값을 가진 ndarray에서 정해진 임곗값의
   컬럼 위치를 최종 예측 클래스로 결정

 

 

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

 

여러개의 분류 결정 임곗값을 변경하면서 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
오차 행렬
[[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
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))

# 샘플 10개를 보면 임곗값이 증가할몌수록 정밀도 값은 동시에 높아지나 재현율 값은 낮아짐
반환된 분류 결정 임곗값 배열의 Shape: (147,)
반환된 precisions 배열의 Shape: (148,)
반환된 recalls 배열의 Shape: (148,)
thresholds 5 sample: [0.11573102 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]
샘플 추출을 위한 임계값 배열의 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]

# precision_recall_curve() API는 정밀도와 재현율의 임곗값에 따른 값 변화를 곡선으로 시각화

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

# 임곗값이 낮을수록 많은 수의 양성 예측으로 인해 재현율 값이 극도로 높아지고 정밀도 값이 극도로 낮아짐
# 이 case에서는 임곗값 = 0.45 지점에서 재현율과 정밀도가 비슷해짐

 

 

  •  정밀도가 100%가 되는 방법
    : 확실한 기준이 되는 경우에만 positive로 얘측하고 나머지는 모두 negative로 예측

  • 재현율이 100%가 되는 방법
    : 모든 환자를 Positive로 예측

  • 정밀도 또는 재현율 중 하나에 상대적인 중요도를 부여해 각 얘측 상황에 맞는 분류 알고리즘을 튜닝할 수는 있지만,
    정밀도/재현율 중 하나만 강조하는 상황이 돼서는 안됨