by museonghwang

분류의 성능 평가 지표(Evaluation Metric)와 정확도(Accuracy)

|

머신러닝은 데이터 가공/변환, 모델 학습/예측, 그리고 평가(Evaluation)의 프로세스로 구성되며, 머신러닝 모델은 여러 가지 방법으로 예측 성능을 평가할 수 있습니다. 성능 평가 지표(Evaluation Metric) 는 일반적으로 모델이 분류냐 회귀냐에 따라 여러 종류로 나뉩니다. 회귀 의 경우 대부분 실제값과 예측값의 오차 평균값에 기반하며, 분류 의 평가방법도 일반적으로 실제 결과 데이터와 예측 결과 데이터가 얼마나 정확하고 오류가 적게 발생하는가에 기반하지만, 단순히 정확도만 가지고 판단했다가는 잘못된 평가 결과에 빠질 수 있습니다.

분류의 대표적인 성능 평가 지표를 살펴보면 다음과 같습니다.

  • 정확도(Accuracy)
  • 오차행렬(Confusion Matrix)
  • 정밀도(Precision)
  • 재현율(Recall)
  • F1 스코어
  • ROC AUC


분류는 결정 클래스 값 종류의 유형에 따라 긍정/부정과 같은 2개의 결과값만을 가지는 이진 분류 와 여러 개의 결정 클래스 값을 가지는 멀티 분류 로 나뉠 수 있습니다. 분류에 사용되는 성능 평가 지표에 대해 자세히 알아보겠습니다.


정확도(Accuracy)

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

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


정확도는 직관적으로 모델 예측 성능을 나타내는 평가 지표 입니다. 하지만 이진 분류의 경우 데이터의 구성에 따라 ML 모델의 성능을 왜곡할 수 있기 때문에 정확도 수치 하나만 가지고 성능을 평가하지 않습니다. 정확도 지표가 어떻게 ML 모델의 성능을 왜곡하는지 예제로 살펴보겠습니다.


타이타닉 데이터 세트를 기준으로 탑승객이 남자인 경우보다 여자인 경우에 생존 확률이 높았기 때문에, 별다른 알고리즘의 적용 없이 무조건 성별이 여자인 경우 생존으로, 남자인 경우 사망으로 예측해도 예측 정확도의 결과가 약 80%가 나올 수 있습니다. 단지 성별 조건 하나만을 가지고 결정하는 별거 아닌 알고리즘도 높은 정확도를 나타내는 상황이 발생하는 것 입니다.

다음 예제에서는 사이킷런의 BaseEstimator 클래스를 상속받아 아무런 학습을 하지 않고, 성별에 따라 생존자를 예측하는 단순한 Classifier 를 생성합니다. 사이킷런은 BaseEstimator 를 상속받으면 Customized 형태의 Estimator 를 개발자가 생성할 수 있습니다. 생성할 MyDummyClassifier 클래스는 학습을 수행하는 fit() 메서드는 아무것도 수행하지 않으며 예측을 수행하는 predict() 메서드는 단순히 Sex 피처가 1이면 0, 그렇지 않으면 1로 예측하는 매우 단순한 Classifier 입니다.

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로 예측함. 
    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


이제 생성된 MyDummyClassifier 를 이용해 타이타닉 생존자 예측을 수행해 보겠습니다. 데이터를 가공하고 나서 이 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
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)))
[output]
Dummy Classifier의 정확도는: 0.7877


이렇게 단순한 알고리즘으로 예측을 하더라도 데이터의 구성에 따라 정확도 결과는 약 78.77%로 꽤 높은 수치가 나올 수 있기에 정확도를 평가 지표로 사용할 때는 매우 신중해야 합니다. 특히 정확도는 불균형한(imbalanced) 레이블 값 분포에서 ML 모델의 성능을 판단할 경우, 적합한 평가 지표가 아닙니다. 예를 들어 100개의 데이터가 있고 이 중에 90개의 데이터 레이블이 0, 단 10개의 데이터 레이블이 1이라고 한다면 무조건 0으로 예측 결과를 반환하는 ML 모델의 경우라도 정확도가 90%가 됩니다.


MNIST 데이터 세트(0부터 9까지의 숫자 이미지 픽셀 정보)를 변환해 불균형한 데이터 세트로 만든 뒤에 정확도 지표 적용 시 어떤 문제가 발생할 수 있는지 살펴보겠습니다. 사이킷런은 load_digits() API를 통해 MNIST 데이터 세트를 제공합니다. MNIST 데이터 세트는 레이블 값이 0부터 9까지 있는 멀티 레이블 분류 를 위한 것입니다. 이것을 레이블 값이 7 인 것만 True, 나머지 값 은 모두 False 로 변환해 이진 분류 문제 로 살짝 바꿔 보겠습니다.

즉, 전체 데이터의 10%만 True, 나머지 90%는 False인 불균형한 데이터 세트로 변형하는 것입니다.

image


이렇게 불균형한 데이터 세트에 모든 데이터를 False, 즉 0으로 예측하는 classifier를 이용해 정확도를 측정하면 약 90%에 가까운 예측 정확도를 나타냅니다. 아무것도 하지 않고 무조건 특정한 결과로 찍어도(?) 데이터 분포도가 균일하지 않은 경우 높은 수치가 나타날 수 있는 것이 정확도 평가 지표의 맹점입니다. 예제 코드로 확인하기위해, 불균형한 데이터 세트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('\n모든 예측을 0으로 하여도 정확도는 : {:.3f}'.format(accuracy_score(y_test, fakepred)))
[output]
레이블 테스트 세트 크기 : (450,)
테스트 세트 레이블 0 과 1의 분포도
0    405
1     45
dtype: int64

모든 예측을 0으로 하여도 정확도는 : 0.900


단순히 predict() 의 결과를 np.zeros() 로 모두 0 값으로 반환함에도 불구하고 450개의 테스트 데이터 세트에 수행한 예측 정확도는 90%입니다. 단지 모든 것을 0으로만 예측해도 MyFakeClassifier 의 정확도가 90%로 유수의 ML 알고리즘과 어깨를 겨룰 수 있다는 것은 말도 안 되는 결과입니다.

이처럼 정확도 평가 지표는 불균형한 레이블 데이터 세트에서는 성능 수치로 사용돼서는 안 됩니다. 이러한 한계점을 극복하기 위해 여러 가지 분류 지표와 함께 적용하여 ML 모델 성능을 평가해야 합니다.