본문 바로가기
대학공부/기계학습

실습 5차시: 성능 평가, cross validation

by 진진리 2023. 10. 20.
728x90

1. Accuracy(정확도)

Confusion matrix:

  • True Positive(TP): 참양성
  • False Positive(FP): 위양성
  • True Negative(TN): 참음성
  • False Negative(FN): 위음성

분류 모델을 만든 후 다음 단계는 모델의 예측 능력을 평가해야합니다.

모델의 성능을 평가하는 가장 간단한 방법은 모델의 정확도를 계산하는 것입니다.

(True Positives + True Negatives) / (True Positives + True Negatives + False Positives + False Negatives)

labels_A = [0, 0, 0, 0, 0, 0, 0, 1, 1, 1]
guesses_A =[0, 0, 0, 0, 0, 0, 0, 1, 0, 0]

labels_B = [0, 0, 0, 0, 0, 0, 0, 1, 1, 1]
guesses_B =[1, 1, 1, 1, 1, 1, 1, 0, 1, 1]
# 위의 예제로 TP, FP, TN, FN을 직접 계산해보는 함수입니다
def cal_metrics(labels, guesses):
  true_positives = 0
  true_negatives = 0
  false_positives = 0
  false_negatives = 0
  for i in range(len(guesses)):
    #True Positives
    if labels[i] == 1 and guesses[i] == 1:
      true_positives += 1
    #True Negatives
    if labels[i] == 0 and guesses[i] == 0:
      true_negatives += 1
    #False Positives
    if labels[i] == 0 and guesses[i] == 1:
      false_positives += 1
    #False Negatives
    if labels[i] == 1 and guesses[i] == 0:
      false_negatives += 1
  return true_positives, true_negatives, false_positives, false_negatives
# A에 대하여 accuracy를 계산해봅니다.
TP_A, TN_A, FP_A, FN_A = cal_metrics(labels_A, guesses_A)

accuracy_A = (TP_A + TN_A) / len(guesses_A)
print(accuracy_A)

0.8

# B에 대하여 accuracy를 계산해봅니다.
TP_B, TN_B, FP_B, FN_B = cal_metrics(labels_B, guesses_B)

accuracy_B = (TP_B + TN_B) / len(guesses_B)
print(accuracy_B)

0.2

 

2. Precision(정밀도)

양성으로 식별된 사례 중 실제로 양성이었던 사례의 비율

  • Precision은 양성으로 예측된 것 (TP + FP) 중 얼마나 많은 샘플이 진짜 양성 (TP) 인지 측정합니다.
  • 직관적으로 Precision은 negative sample을 positive sample로 예측하지 않는 분류기의 성능을 나타냅니다.
  • Precision은 거짓 양성(FP)의 수를 줄이는 것이 목표일 때 성능 지표로 사용합니다.
  • 위양성이 생성되지 않는 모델의 정밀도는 1.0입니다.
  • 예시) 모델의 정밀도는 0.5일때, 어떠한 종양이 악성일 가능성이 있다고 예측하면 예측 정확도가 50% 입니다.

True Positives / (True Positives + False Positives)

# precision_A을 계산해봅니다.
precision_A = TP_A / (TP_A + FP_A)

print(precision_A)

1.0

# precision_B을 계산해봅니다.
precision_B = TP_B / (TP_B + FP_B)

print(precision_B)

0.2222222222222

 

3. Recall(재현율)

실제 양성 중 정확히 양성이라고 식별된 사례의 비율

  • Recall은 전체 양성 샘플 (TP + FN) 중에서 얼마나 많은 샘플이 양성 클래스(TP)로 분류되는지를 측정합니다.
  • 직관적으로, Recall은 분류기가 positive sample들을 올바르게 찾는 성능을 평가합니다.
  • Recall은 모든 양성 샘플을 식별해야 할 때 성능 지표로 사용됩니다. 즉, 거짓 음성(FN)을 피하는 것이 중요할 때 입니다.
  • 위음성을 생성하지 않는 모델의 재현율은 1.0입니다.
  • 예시) 모델의 재현율은 0.11일때, 모든 악성 종양의 11% 를 올바르게 식별합니다..

True Positives / (True Positives + False Negatives)

 

Precision과 Recall은 서로 상충하는 metric입니다. 하나가 내려가면 다른 하나가 올라갑니다.

# Recall_A을 계산해봅니다.
recall_A = TP_A / (TP_A + FN_A)
print(recall_A)

0.3333333333333333

# Recall_B을 계산해봅니다.
recall_B = TP_B / (TP_B + FN_B)
print(recall_B)

0.6666666666666666

 

Side Note:

  • Precision(정밀도): 양성으로 식별된 사례 중 실제로 양성이었던 사례의 비율
Precision=TP/(TP+FP)
  • Recall(재현율): 실제 양성 중 정확히 양성이라고 식별된 사례의 비율
Recall=TP/(TP+FN)
  • Sensitivity(민감도) = recall: 실제로 양성인 사람이 검사에서 양성으로 판정될 확률
Sensitivity=TP/(TP+FN)
  • Specificity(특이도): 실제로 음성인 사람이 검사에서 음성으로 판정될 확률
Specificity=TN/(FP+TN)

 

4. F1 Score

모델의 성능을 완전히 평가하려면 정밀도와 재현율을 모두 검사해야 합니다. F1 score을 통해 Precision과 Recall 모두를 고려하는 하나의 metric을 제시할 수 있습니다. F1 score은 Precision과 Recall의 조화평균(harmonic mean)입니다. 조화평균은 n개의 양수에 대하여 그 역수들을 산술평균한 것의 역수를 말합니다.

2∗(Precision∗Recall) / (Precision+Recall)

 

F1 score는 Precision과 Recall을 하나의 metric으로 결합합니다. 산술 평균보다 조화 평균을 사용하여 F1 score을 계산하는 이유는 Precision이나 Recall이 0일 때 F1 score이 낮아지기를 원하기 때문입니다.

  • 예를 들어, Recall = 1이고 Precision = 0.01인 모델이 있다고 가정합니다. Precision이 너무 낮기 때문에 이 모델에 문제가 있을 가능성이 높습니다. 따라서 F1 score은 Precision과 Recall을 같이 고려하므로 불균형한 이진 분류 데이터셋에서는 정확도보다 더 나은 지표가 될 수 있습니다.

산술평균(arithmetic mean)을 사용한 F1 score은 다음과 같습니다.

(1+0.01)/2=0.505

너무 높습니다. 따라서 조화 평균을 사용한 F1 score은 다음과 같습니다.

2∗(1∗0.01)/(1+0.01)=0.019

산술 평균을 사용한 F1 score은 분류기의 성능을 더욱 정확하게 파악하는데 도움이 됩니다.

# F1 score을 계산해봅니다.
f1_A = 2 * (precision_A * recall_A) / (precision_A + recall_A)
print(f1_A)

0.5

# F1 score을 계산해봅니다.
f1_B = 2 * (precision_B * recall_B) / (precision_B + recall_B)
print(f1_B)

0.3333333333333333

 

5. Scikit-Learn

Scikit_learn 라이브러리에는 위의 metric들을 간단하게 계산해주는 함수들이 있습니다.

from sklearn.metrics import accuracy_score, recall_score, precision_score, f1_score

labels = [1, 0, 0, 1, 1, 1, 0, 1, 1, 1]
guesses = [0, 1, 1, 1, 1, 0, 1, 0, 1, 0]
# 정확도, recall, precision, F1 score을 계산해봅니다.
print(accuracy_score(labels, guesses))
print(recall_score(labels, guesses))
print(precision_score(labels, guesses))
print(f1_score(labels, guesses))

0.3

0.42857142857142855

0.5

0.4615384615384615


6. Binary Classification and Model Evaluation

6-1. Dataset

이번 실습은 California Housing Dataset를 사용합니다. 이 데이터는 미국의 1990년 인구 조사 데이터를 기반으로 캘리포니아 특정 지역의 주택에 관한 것입니다.

  1. longitude: 집이 서쪽으로 얼마나 멀리 떨어져 있는지 나타내는 척도이며, 값이 더 높은 것은 서쪽으로 더 멀리 떨어져 있습니다.
  2. latitude: 집이 북쪽으로 얼마나 멀리 떨어져 있는지 나타내는 척도이며, 더 높은 값이 더 북쪽입니다.
  3. housing_median_age: 블록 내 주택의 연식입니다. 숫자가 낮으면 새 건물입니다.
  4. total_rooms: 블록 내의 방 개수입니다.
  5. total_bedrooms: 한 블록 내의 총 침실 개수입니다.
  6. population: 한 블록 내의 총 인구 수입니다.
  7. households: 한 블록에 대한 총 가구 수입니다.
  8. median_income: 주택 블록 내 가구의 중위소득입니다.
  9. medium_house_value: 블록 내 가구의 평균 주택 값(미국 달러로 측정)입니다.
train_df.describe()

(생략)

 

6-2. Z-score Normalization

여러 feature이 존재하는 데이터로 모델을 훈련시키는 경우 각 피쳐의 값은 거의 동일한 범위를 가져야합니다. 예를 들어 한 feature의 범위가 500 ~ 100,000이고 다른 feature의 범위가 2 ~ 12인 경우 모델을 훈련하기가 어렵거나 불가능합니다.

이번 실습에서 다룰 정규화 방법은 각 raw value(레이블 포함)을 Z-score으로 변환하여 정규화하는 방식입니다. Z-score는 특정 raw value에 대한 평균으로부터의 표준 편차의 개수입니다. 예를 들어, 다음과 같은 특성을 가진 feature이 있다고 가정합니다.

  • 평균은 60 입니다.
  • 표준편차는 10 입니다.

75인 raw value는 Z-score이 +1.5이 될 것입니다:

 Z-score = (75 - 60) / 10 = +1.5

38인 raw value는 Z-score이 -2.2가 될 것입니다:

  Z-score = (38 - 60) / 10 = -2.2

Z-score 정규화 공식은 다음과 같습니다.

(value−μ) / σ

여기서 μ는 평균값이고 σ는 표준 편차입니다. raw value가 feature의 모든 값의 평균과 정확히 같으면 0으로 정규화됩니다. 평균 아래에 있으면 음수가 되고 평균 위에 있으면 양수가 됩니다. Z-score의 크기는 표준 편차에 의해 결정됩니다. 정규화되지 않은 데이터에 큰 표준 편차가 있으면 정규화 값이 0에 가깝습니다.

 

train_df.columns

Index (['longitude', 'latitude', 'housing_median_age', 'total_rooms', 'total_bedrooms', 'population', 'households', 'median_income', 'median_house_value'], dtype='object')

import seaborn as sns

sns.scatterplot(x=train_df.total_rooms, y=train_df.housing_median_age)

<Axes: xlabel='total_rooms', ylabel='housing_median_age'>

# training set에 대하여 Z-Score 정규화를 합니다.
train_df_mean = train_df.mean()
train_df_std = train_df.std()
train_df_norm = (train_df - train_df_mean) / train_df_std
# 정규화된 데이터를 확인합니다.
train_df_norm.head()
# Z-score 전후 데이터의 범위를 비교해봅니다.
import seaborn as sns
import matplotlib.pyplot as plt

sns.set(rc={'figure.figsize':(14,5)})

fig, ax = plt.subplots(1,2)
sns.scatterplot(x=train_df.total_rooms, y=train_df.housing_median_age, ax=ax[0]).set(title='Before Normalization')
sns.scatterplot(x=train_df_norm.total_rooms, y=train_df_norm.housing_median_age, ax=ax[1]).set(title='Z-score Normalization')
fig.show()

# 그래프 사이즈 재설정
sns.set(rc={'figure.figsize':(11.7,8.27)})
# test set에 대해서도 동일하게 Z-Score 정규화를 합니다.
test_df_mean = test_df.mean()
test_df_std = test_df.std()
test_df_norm = (test_df - test_df_mean) / test_df_std

 

6-3. Create a binary label

분류 문제에서 모든 예제의 레이블은 0 또는 1이어야 합니다. 하지만 California Housing Dataset의 label인 median_house_value는 0과 1이 아닌 80,100 또는 85,700과 같은 float 값을 포함하고 있는 반면, 정규화된 데이터의 median_house_values는 주로 -3과 +3 사이의 float 값을 포함하고 있습니다.

Training set과 test set에 median_house_value_is_high의 새 column을 만듭니다. median_house_value가 특정 임계값(threshold)보다 높은 경우 median_house_value_is_high를 1로 설정합니다. 그렇지 않으면 median_house_value_is_high를 0으로 설정합니다.

 

# median_house_value를 임계값에 따라 0과 1로 인코딩합니다.
threshold_in_Z = 1.0
train_df_norm["median_house_value_is_high"] = (train_df_norm["median_house_value"] > threshold_in_Z).astype(float)
test_df_norm["median_house_value_is_high"] = (test_df_norm["median_house_value"] > threshold_in_Z).astype(float)
# 변환이 잘 되었는지 확인해봅니다.
train_df_norm["median_house_value_is_high"].unique()

array([0., 1.])

# California housing dataset은 label이 불균형한 데이터입니다. 불균형한 정도를 확인해봅니다.
train_df_norm["median_house_value_is_high"].value_counts()

0.0     14223

1.0      2777

Name: median_house_value_is_high, dtype: int64

# 비율 확인
train_df_norm["median_house_value_is_high"].value_counts() / train_df_norm.shape[0]

0.0    0.836647

1.0    0.163353

Name: median_house_value_is_high, dtype: float64

# train/test의 X와 y를 분리합니다.
X_train = train_df_norm.drop(columns=["median_house_value_is_high", "median_house_value"])
X_test = test_df_norm.drop(columns=["median_house_value_is_high", "median_house_value"])
y_train = train_df_norm["median_house_value_is_high"]
y_test = test_df_norm["median_house_value_is_high"]

 

6-4. Train a classifier

# MLPClassifier을 생성하고 훈련시킵니다.
from sklearn.neural_network import MLPClassifier

clf = MLPClassifier(random_state=42, max_iter=1000, learning_rate_init=0.01)
clf.fit(X_train, y_train)
# 모델 평가를 위해서 예측값과 예측된 확률값을 만듭니다.
y_pred = clf.predict(X_test)
y_prob = clf.predict_proba(X_test)[:, 1]
y_pred[:10]

array([1., 0., 0., 1., 0., 0., 0., 0., 0., 0.])

y_prob[:10]

array([9.82404974e-01, 2.98570280e-02, 1.28038433e-01, 8.36820676e-01,

             5.01304492e-04, 4.41176707e-05, 5.58867550e-05, 1.17181582e-02,

             3.26754469e-02, 1.27698942e-04])

 

6-5. Evaluate the model

Confusion Matrix

 

sklearn.metrics.confusion_matrix

Examples using sklearn.metrics.confusion_matrix: Visualizations with Display Objects Label Propagation digits active learning

scikit-learn.org

# confusion matrix를 출력해봅니다.
from sklearn.metrics import confusion_matrix

confusion_matrix(y_test, y_pred)

array([[2416, 110], [ 149, 325]])

 

 

sklearn.metrics.ConfusionMatrixDisplay

Examples using sklearn.metrics.ConfusionMatrixDisplay: Visualizations with Display Objects Examples using sklearn.metrics.ConfusionMatrixDisplay.from_estimator: Faces recognition example using eige...

scikit-learn.org

# confusion matrix를 시각화해봅니다.
from sklearn.metrics import ConfusionMatrixDisplay

ConfusionMatrixDisplay.from_predictions(y_test, y_pred)

 

Precision-Recall

모델의 분류 작업을 결정하는 임계값을 바꾸는 것은 해당 분류기의 precision과 recall의 상충 관계를 조정하는 일 입니다.

  • 예를 들어 양성 샘플의 실수(FN)을 10%보다 작게 하여 90% 이상의 recall을 원할 수 있습니다. 이런 결정은 데이터와 애플리케이션에 따라 다르며 목표에 따라 달리 결정됩니다.
  • 어떤 목표가 선택되면 (즉, 어떤 클래스에 대한 특정 recall 또는 precision의 값) 적절한 임계값을 지정할 수 있습니다. 다시 말해 90% recall과 같은 특정 목적을 충족하는 임계값을 설정하는 것은 언제든 가능합니다.

어려운 부분은 이 임계값을 유지하면서 적절한 precision을 내는 모델을 만드는 일 입니다.

  • 만약 모든 것을 양성이라고 분류하면 recall이 100이 되지만 이러한 모델은 쓸모가 없을 것 입니다.

새로운 모델을 만들 때에는 임계값이 명확하지 않은 경우가 많습니다. 이런 경우에는 문제를 더 잘 이해하기 위해 모든 임계값을 조사해보거나, 한번에 precision과 recall의 모든 장단점을 살펴보는 것이 좋습니다. 이를 위해 precision-recall curve를 사용합니다.

 

sklearn.metrics.PrecisionRecallDisplay

Examples using sklearn.metrics.PrecisionRecallDisplay: Visualizations with Display Objects Precision-Recall Examples using sklearn.metrics.PrecisionRecallDisplay.from_estimator: Precision-Recall Ex...

scikit-learn.org

from sklearn.metrics import PrecisionRecallDisplay

PrecisionRecallDisplay.from_estimator(clf, X_test, y_test)

  • 곡선의 각 포인트는 가능한 임계값에 대하여 precision과 recall입니다.
  • 곡선이 오른쪽 위로 갈 수록 더 좋은 분류기입니다.
    • 오른쪽 위 지점은 한 임계값에서 precision과 recall이 모두 높은 곳 입니다. 곡선은 임계값이 매우 높아 전부 양성 클래스가 되는 왼쪽 위에서 시작합니다. 임계값이 작아지면서 곡선은 recall이 높아지는 쪽으로 이동하게 되지만 precision은 낮아집니다.
  • precision이 높아져도 recall이 높게 유지될수록 더 좋은 모델입니다.

 

ROC and AUC

ROC Curve는 여러 임계값에서 분류기의 특성을 분석하는데 널리 사용하는 도구입니다. Precision-Recall curve와 비슷하게 ROC curve는 분류기의 모든 임계값을 고려하지만, 정밀도와 재현률 대신 True Positive Rate(TPR)에 대한 False Positive Rate(FPR)을 나타냅니다. True Positive Rate은 Recall의 다른 이름이여, False Positive Rate은 전체 음성 샘플 중에서 거짓 양성으로 잘못 분류한 비율입니다. ROC curve는 roc_curve 함수를 사용하여 만들 수 있습니다. ROC curve는 왼쪽 위에 가까울 수록 이상적입니다. False Positive Rate이 낮게 유지되면서 recall이 높은 분류기가 좋은 것 입니다.

 

import matplotlib.pyplot as plt
from sklearn.metrics import roc_curve

def plot_roc(name, labels, predictions, **kwargs):
  fp, tp, _ = roc_curve(labels, predictions)
  plt.plot(100*fp, 100*tp, label=name, linewidth=2, **kwargs)
  plt.xlabel('False positives [%]')
  plt.ylabel('True positives [%]')
  plt.xlim([-5,100])
  plt.ylim([0,105])
  plt.grid(True)
  ax = plt.gca()
  ax.set_aspect('equal')
plot_roc('Baseline Classifier', y_test, y_prob)

곡선 아래의 면적값 하나로 ROC curve를 요약할 수 있습니다. 이 면적을 보통 AUC(Area Under the Curve)라고 합니다. 이 ROC curve 아래 면적은 roc_auc_score 함수로 계산합니다.

# roc_auc_score 출력하기
from sklearn.metrics import roc_auc_score

roc_auc_score(y_test, y_prob)

0.9421551727017916

 

scikit-learn의 classification_report는 accuracy, precision, recall, f1 score의 점수 모두를 한번에 계산해서 깔끔하게 출력해줍니다.

# classification report 출력하기
from sklearn.metrics import classification_report

print(classification_report(y_test, y_pred))

                               precision     recall      f1-score     support

0.0                          0.94               0.96         0.95             2526

1.0                          0.75               0.69         0.72             474

accuracy                                                       0.91             3000

macro avg           0.84               0.82         0.83             3000

weighted avg      0.91              0.91          0.91            3000

 

7. Cross Validation

Machine Learning model training process:

성능이 좋은 모델을 만들기 위해 데이터에 반복적으로 모델을 학습시키는 것은 잘못된 방법입니다. 이렇게 훈련된 모델은 train data에만 매우 잘 작동하는 모델이 되며 이전에 학습하지 않은 데이터를 예측해야 할 경우에는 성능이 떨어지게 됩니다. Overfitting(과적합)을 방지하기 위해서 test set을 따로 분류하여 모델의 성능을 확인하게 됩니다.

Cross Validation:

모델 훈련 과정 중 test data을 활용하여 최적의 모델 파라미터를 찾는 방법은 적합하지 않습니다. 그 이유는 test data에 가장 좋은 성능을 낼 때까지 모델 파라미터를 조정하게 되기 때문에, test data에 과적합될 가능성이 있기 때문입니다. 따라서 test data에 존재하는 분포가 모델에 노출되어 모델의 일반화 성능을 떨어뜨릴 수 있기 때문입니다. 이와 같은 문제를 해결하기 위해서 파라미터를 조정하기 위한 validation data을 따로 구성합니다. 최적의 모델 파라미터를 validation data을 통해 찾고, 최적의 파라미터로 훈련된 모델의 최종 성능은 test data로 평가하게 됩니다. 비율은 train:test=8:2, 분리된 train data에서 다시 train:val=8:2로 설정되거나 train:val:test=6:2:2 또한 자주 사용됩니다. 데이터 크기에 따라 그 비율 또한 조정되어야 합니다.

데이터 크기가 작아 validation data을 따로 구성할 수 없거나, 특정한 분포의 데이터가 train 혹은 test data에 포함되어 학습에 영향을 주는 것을 방지하기 위해 cross validation을 사용합니다. train data은 k개의 fold으로 나뉘게 되며, k번 반복되어 훈련되지만 각 훈련마다 valiation set을 k 번째 fold으로 사용합니다. k=5가 가장 흔하며, 5-fold CV라고 부릅니다. 성능은 validation data로 사용된 fold들의 평균 정확도로 계산됩니다.

 

 

sklearn.model_selection.cross_val_score

Examples using sklearn.model_selection.cross_val_score: Release Highlights for scikit-learn 1.3 Model selection with Probabilistic PCA and Factor Analysis (FA) Imputing missing values before buildi...

scikit-learn.org

from sklearn.model_selection import cross_val_score

clf = MLPClassifier(random_state=42, max_iter=1000, learning_rate_init=0.01)
scores = cross_val_score(clf, X_train, y_train, cv=5)
print(scores)

[0.85852941 0.91382353 0.87264706 0.87852941 0.51 ]

scores.mean()

0.8067058823529412

 

 

sklearn.model_selection.KFold

Examples using sklearn.model_selection.KFold: Feature agglomeration vs. univariate selection Comparing Random Forests and Histogram Gradient Boosting models Gradient Boosting Out-of-Bag estimates N...

scikit-learn.org

# 데이터셋을 train과 validation으로 나누어주는 KFold를 알아봅니다.
from sklearn.model_selection import KFold

kf = KFold(n_splits=5)
for train_index, val_index in kf.split(X_train):
  X_train_cv, X_val_cv = X_train.iloc[train_index], X_train.iloc[val_index]
  y_train_cv, y_val_cv = y_train.iloc[train_index], y_train.iloc[val_index]
  print(y_train_cv.value_counts())

0.0 11171

1.0 2429

Name: median_house_value_is_high, dtype: int64

0.0 11194

1.0 2406

Name: median_house_value_is_high, dtype: int64

0.0 11742

1.0 1858

Name: median_house_value_is_high, dtype: int64

0.0 11041

1.0 2559

Name: median_house_value_is_high, dtype: int64

0.0 11744

1.0 1856

Name: median_house_value_is_high, dtype: int64

 

 

sklearn.model_selection.StratifiedKFold

Examples using sklearn.model_selection.StratifiedKFold: Recursive feature elimination with cross-validation GMM covariances Receiver Operating Characteristic (ROC) with cross validation Test with p...

scikit-learn.org

from sklearn.model_selection import StratifiedKFold

skf = StratifiedKFold(n_splits=5)
for train_index, val_index in skf.split(X_train, y_train):
  X_train_cv, X_val_cv = X_train.iloc[train_index], X_train.iloc[val_index]
  y_train_cv, y_val_cv = y_train.iloc[train_index], y_train.iloc[val_index]
  print(y_train_cv.value_counts())

0.0    11378

1.0    2222

Name: median_house_value_is_high, dtype: int64

0.0   11378

1.0   2222

Name: median_house_value_is_high, dtype: int64

0.0   11378

1.0   2222

Name: median_house_value_is_high, dtype: int64

0.0   11379

1.0   2221

Name: median_house_value_is_high, dtype: int64

0.0   11379

1.0   2221

Name: median_house_value_is_high, dtype: int64