[TIL] 데이터분석 데브코스 50일차 (2) - 검증/교차검증/성능평가/성능 평가 metric
검증(Validation)
: 모델의 학습이 잘 진행되었는지(=일반화 능력이 좋은지)를 판단하는 평가 과정 ► 학습의 종료 시점을 판가름할 수 있음
📌 일반화 능력 : 내가 학습한 데이터가 아닌 새로운 다른 데이터로도 모델이 잘 작동하는 능력
교차 검증 (Cross Validation; CV)
: 다음의 문제를 회피(혹은 감수)하면서도 검증의 원래 의미를 살리는 평가 방법으로,
전체 데이터를 여러 개의 하위 데이터로 나누고 이 하위 데이터 세트들의 조합은 서로 다른 방법으로 훈련&검증에 사용해서 모델의 일반화 능력을 충분히 측정한다.
► 각 조합의 결과 개수 = 하위 데이터 셋들의 조합 수
※ 머신러닝 모델 학습 과정에서 발생할 수 있는 문제
- 너무 쉬운 데이터로의 편향
- 전체적인 데이터 양의 부족
장점 :
- 일반화 능력 추정
- 데이터 활용 최대화
- 과정합 방지
단점 :
- 시간 복잡도가 큼
교차 검증 방법
학습 데이터와 검증 데이터를 구성하는 방법에 따라 다르다.
1. K-Fold CV
: 전체 데이터 셋을 총 K개의 덩어리(=fold)로 나누고, 각 덩어리를 순차적으로 검증 데이터로 사용하는 방법
► 모든 데이터가 학습 & 평가로 사용됨
프로세스
- 데이터 셋을 K개의 덩어리(fold)로 나눔 → K개의 조합이 생성됨
- 하나의 조합 당 1개 fold만 평가 데이터, 나머지 K-1개 fold는 훈련 데이터로 사용
- 총 K번의 학습 & 평가 과정이 반복됨
2. Stratified Cross-Validation(계층적 교차 검증)
: K-Fold CV + 각 폴드에서 클래스의 비율을 원본 데이터셋의 클래스 비율과 유사하게 유지하면서 검증하는 방법
► K-Fold CV의 장점을 갖고 가면서 클래스 사이 불균형이 있는 경우의 편향까지 고려
프로세스
- 클래스별로 데이터를 분할
- 각 클래스를 K개의 fold로 나눔
- 각 클래스에 존재하는 K개의 fold를 하나씩 조합 → K개의 조합이 생성됨
- 하나의 조합 당 1개 fold만 평가 데이터, 나머지 K-1개 fold는 훈련 데이터로 사용
- 총 K번의 학습 & 평가 과정이 반복됨
↓
3. LOOCV (Leave-One-Out Cross Validation)
한번에 하나의 데이터 포인트만 평가 데이터로, 나머지는 학습 데이터로 사용하는 방법
► 매우 정확하지만, 데이터 크기가 크다면 많은 시간 소요
프로세스
- 데이터 셋을 전체 데이터 수(=K개)만큼의 덩어리(fold)로 나눔 → K(=전체 데이터 수)개의 조합이 생성됨
- 하나의 조합 당 1개 fold만 평가 데이터, 나머지 K-1개(=전체 데이터 수 - 1) fold는 훈련 데이터로 사용
- 총 K번의 학습 & 평가 과정이 반복됨
선형 분류 교차 검증 실습
1단계. 데이터 생성
import numpy as np
seed = 1234
np.random.seed(seed)
# 데이터 생성
num_sample_per_class = 250
X_class1 = np.random.normal(2, 2, (num_sample_per_class, 2))
X_class2 = np.random.normal(-2, 2, (num_sample_per_class, 2))
X_ = np.vstack([X_class1, X_class2]) # feature 두 개를 묶어주고 (250, 2) + (250, 2) -> (500, 2)
X = np.hstack([np.ones((num_sample_per_class * 2, 1)), X_]) # 1개의 bias를 추가 (500, 2) -> (500, 3)
y = np.array([1] * num_sample_per_class + [0] * num_sample_per_class) # 클래스 레이블 생성
2단계. 클래스별 데이터 시각화
import matplotlib.pyplot as plt
# 클래스별 데이터 시각화
plt.figure(figsize=(8, 6))
plt.scatter(X_class1[:, 0], X_class1[:, 1], alpha=0.5, label='Class 1')
plt.scatter(X_class2[:, 0], X_class2[:, 1], alpha=0.5, label='Class 2')
# 그래프 설정
plt.title('2D dummy data for linear classification')
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.legend()
# 그래프 출력
plt.show()
3단계. Logistic Regression 모델 생성 및 학습
from sklearn.linear_model import LogisticRegression
logistic_reg = LogisticRegression()
logistic_reg.fit(X, y)
4단계. 선형 분류 결과 확인
plt.figure(figsize=(8, 6))
plt.scatter(X_class1[:, 0], X_class1[:, 1], alpha=0.5, label='Class 1')
plt.scatter(X_class2[:, 0], X_class2[:, 1], alpha=0.5, label='Class 2')
w0 = logistic_reg.intercept_[0]
w1, w2 = logistic_reg.coef_[0][1:]
feauter1_value = np.array([X[:, 1].min(), X[:, 1].max()])
feature2_value = -(w0 + w1 * feauter1_value) / w2
plt.plot(feauter1_value, feature2_value, color='red', linestyle='--', label='logistic regression')
plt.title('2D dummy data for linear classification with classification line ')
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.margins(0.2)
plt.legend()
plt.show()
5단계. 성능 평가 - (0) Random Split으로 분할 후 정확도를 구하는 일반적인 방법
`train_test_split()` : 학습 및 평가 데이터 분할 함수
- `arrays` : 분할시킬 데이터셋
- `test_size`: 테스트 데이터셋의 비율 or 갯수 (디폴트값 = `0.25`)
- `train_size` : 학습 데이터셋의 비율 or 갯수 (디폴트값 = `test_size`의 나머지)
- `random_state` : 데이터 분할시 이루어지는 셔플을 위한 시드 값 설정(매번 동일한 훈련 및 평가 데이터 생성)
- 리턴값 :
- `X_train`, `X_test`, `Y_train`, `Y_test` : `arrays`에 데이터와 레이블(정답)을 둘 다 넣었을 경우의 반환값(데이터와 레이블의 순서쌍 유지됨)
- `X_train`, `X_test` : `arrays`에 레이블 없이 데이터만 넣었을 경우의 반환값
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=seed)
logistic_reg.fit(X_train, y_train)
y_pred = logistic_reg.predict(X_test)
accuracy_random_split = accuracy_score(y_test, y_pred)
print(f'Random Split 결과 정확도 : {accuracy_random_split*100:.2f} %')
5단계. 성능 평가 - (1) K-Fold CV
`cross_val_score()` : 교차 검증을 위한 함수
- `estimator` : 사용한 분류(Classifier) 혹은 회귀(Regressor) 알고리즘
- `X` : feature 데이터 셋
- `Y` : 레이블(정답) 데이터 셋
- `scoring` : 예측 성능 평가 지표
- `cv` : 교차 검증 덩어리(fold) 갯수
- 리턴값 : `scoring` 파라미터로 지정된 성능 평가값을 arrray 형태로 반환
from sklearn.model_selection import cross_val_score
k_fold_score = np.mean(cross_val_score(logistic_reg, X, y, cv=5))
print(f'K-Fold CV 결과 정확도 : {k_fold_score*100:.2f} %')
►사실 일반적인 방법과 큰 성능 차이는 보이지 않는다.
즉, 교차 검증으로 성능이 좋아지는 것이 아니라, 성능을 평가하는 과정 자체가 좋다는 것을 알아두자!
5단계. 성능 평가 - (2) Stratified CV
계층적 교자 검증을 사용하기 위해서는 `from sklearn.model_selection import StratifiedKFold`로 필요한 라이브러리를 불러온 후,
`cross_val_score()` 함수 내 `cv=` 파라미터만 `StratifiedKFold(폴드 갯수)`로 변경해주면 된다.
from sklearn.model_selection import cross_val_score, StratifiedKFold
stratified_cv_score = np.mean(cross_val_score(logistic_reg, X, y, cv=StratifiedKFold(5)))
print(f'Stratified CV 결과 정확도 : {stratified_cv_score*100:.2f} %')
5단계. 성능 평가 - (3) LOOCV
LOOCV를 사용하기 위해서는 `from sklearn.model_selection import LeaveOneOut`로 필요한 라이브러리를 불러온 후,
`cross_val_score()` 함수 내 `cv=` 파라미터만 `LeaveOneOut()`로 변경해주면 된다.
from sklearn.model_selection import cross_val_score, LeaveOneOut
loocv_score = np.mean(cross_val_score(logistic_reg, X, y, cv=LeaveOneOut()))
print(f'LOOCV 결과 정확도 : {loocv_score*100:.2f} %')
[정리]
클래스 내 불균형이 심해보이면 Stratified CV를 사용, 그렇지 않으면 K-Fold CV를 사용하는 것이 일반적이다.
교차검증을 하면서 다른 모델들과 비교하고 가장 좋은 성능을 보이는 모델을 찾아낸다.
이후, 해당 모델의 구조를 활용해서 실제 학습을 진행한다.
성능 평가
: 머신러닝 모델의 성능을 객관적으로 측정하고 비교하는 과정에서 사용됨
Metric (메트릭)
: 성능 평가에 사용되는 지표
Metric 종류
지도 학습
- 분류 문제 :
- 정확도 (Accuracy)
- 혼동 행렬 (Confusion Matrix)
- 정밀도 (Precision)
- 재현율 (Recall)
- F1 score
- 회귀 문제 :
- 평균 제곱 오차 (Mean Squared Error; MSE)
비지도 학습
- 군집화 :
- SSE(Sum of Squared Error)
- 실루엣 계수 (Silhouette Coefficient)
- 이상치 탐지 :
- 분류 문제 metric 활용
분류 문제 Metric
Confusion Matrix(혼동 행렬)
: 실제 정답(True Label)과 예측값(Predicted Label)을 비교해서 모델의 성능을 시각적으로 표현한 행렬
📌 구성 요소 `[맞았는지(T)틀렸는지(F) | 예측(P/N)]` :
- 진짜 양성 (True Positive, TP) : 양성으로 예측했는데 정답도 양성일 경우
- 거짓 양성 (False Positive, FP) : 양성으로 예측했는데 정답은 음성일 경우
- 진짜 음성 (True Negative, TN) : 음성으로 예측했는데 정답도 음성일 경우
- 거짓 음성 (False Negative, FN) : 음성으로 예측했는데 정답은 양성일 경우
► 혼동 행렬로 알 수 있는 다른 metric :
ROC(Receiver Operating Characteristic) curve
: 양성과 음성을 나누는 임계값의 변화에 따른 성능(←혼동 행렬로부터 알 수 있음)을 시각화한 그래프
(임계값에 따라서 Confusion Matrix가 달라지며, 이에 따라 Accuracy / Precision / Recall / F1 score도 바뀐다.0
► 머신러닝 모델이 양성을 최대한 많이 & 잘 찾아내면서 noise(잡음)에 의한 거짓 탐지를 최소화하는지를 바탕으로 적절한 임계치를 찾는 도구
📌 threshold(임계값) : 음성(N)과 양성(P)을 나누어주는 경곗값
좋은 분류기 : 왼쪽 상단에 포인트가 존재하게 하는 임계치를 선택하는 것이 좋다 (= FPR은 작아야하고, TPR은 커야함)
나쁜 분류기 : y=x 그래프에 수렴
- X축(가로) : 거짓 양성률(False Positive Rate; FPR)
► 전체 음성 중에서 실제로는 음성인데 양성이라고 잘못 예측한 비율
- Y축(세로) : 진짜 양성률(True Positive Rate; TPR)
► 실제 양성중에 예측을 양성이라고 찾은 비율 = Recall(재현율)
AUC(Area Under the Curve) 점수
: 분류기의 전반적인 성능(=음성과 양성을 잘 구분할 수 있는가?)을 알 수 있는 척도로, ROC curve의 아랫부분 면적
- 최댓값 : 1
- 최솟값 : 0.5
선형 분류 성능 평가 Metric 실습
※ 1단계(데이터 생성) ~ 4단계(모델 학습 결과)까지는 위의 '선형 분류 교차 검증 실습' 과정과 동일하다.
5단계. 성능 평가 (1) - Confusino Matrix
from sklearn.metrics import confusion_matrix
y_pred = logistic_reg.predict(X_test)
cm = confusion_matrix(y_test, y_pred)
# 혼동 행렬 시각화
plt.figure(figsize=(6, 6))
plt.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues)
plt.colorbar()
tick_marks = np.arange(2)
plt.xticks(tick_marks, ['Predicted Negative', 'Predicted Positive'])
plt.yticks(tick_marks, ['Actual Negative', 'Actual Positive'])
plt.xlabel('Predicted Label')
plt.ylabel('True Label')
plt.title('Confusion Matrix with Labels')
labels = ['TN', 'FP', 'FN', 'TP']
label_values = [cm[0, 0], cm[0, 1], cm[1, 0], cm[1, 1]]
label_colors = ['white', 'black', 'black', 'white']
indices = [(0, 0), (0, 1), (1, 0), (1, 1)]
for label, value, color, (i, j) in zip(labels, label_values, label_colors, indices):
plt.text(j, i, f'{label}\n{value}', ha='center', va='center', color=color)
plt.show()
5단계. 성능 평가 (2) - ROC curve, AUC
`predict_proba` : 예측 확률을 구할 수 있음
from sklearn.metrics import roc_curve
# 각 데이터가 양성일 확률, 양성만 취할 예정이므로 1 위치의 값만 가져옴
y_pred_proba = logistic_reg.predict_proba(X_test)[:, 1]
fpr, tpr, thresholds = roc_curve(y_test, y_pred_proba)
# ROC 커브 시각화
plt.plot(fpr, tpr, color='orange', label=f'ROC curve')
plt.plot([0, 1], [0, 1], color='navy', linestyle='--') # y=x 직선
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic (ROC) Curve')
plt.legend(loc="lower right")
plt.show()
최적의 임계값(threshold) 위치를 찾아보자.
`np.argmin()` : 괄호 안의 값(f(x))을 최솟값으로 만들기 위한 값(x)을 구하는 함수
# 최적 threshold 위치 찾기
# tpr은 크고 fpr은 작아야하므로 아래와 같은 코드로 위치를 선정
closest_leftupper_conner = np.argmin(np.abs(tpr - 1) + fpr)
optimal_threshold = thresholds[closest_leftupper_conner]
print(f'가장 좌측 상단에 위치한 포인트의 threshold 값 : {optimal_threshold}')
# Optimal Point를 포함하는 ROC 커브 시각화
plt.plot(fpr, tpr, color='orange', label=f'ROC curve')
plt.plot([0, 1], [0, 1], color='navy', linestyle='--') # y=x 직선
plt.scatter(fpr[closest_leftupper_conner],
tpr[closest_leftupper_conner],
color='red', label=f'Optimal Threshold = {optimal_threshold:.2f}')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic (ROC) Curve')
plt.legend(loc="lower right")
plt.show()
from sklearn.metrics import auc
roc_auc = auc(fpr, tpr)
print(f'ROC 커브로부터 구한 AUC : {roc_auc}')
회귀 문제 Metric
R2 score (결정계수)
: 모델이 데이터의 변동성을 얼마나 잘 설명하는지를 나타내는 통계적 지표
► 데이터 포인트와 회귀선 얼만큼 퍼져있는지(=변동성)를 평가
► 모델이 설명해야 하는 전체 변동량 중에 얼만큼 현재 모델이 설명하고 있는지
※ 데이터가 복잡해지면 설명력은 증가하지만 과적합의 위험에 빠질 수 있다.
즉, R2 score이 클수록 설명력은 좋지만 과적합 상태로 빠지는 것을 주의해야 한다.
회귀 문제 성능 평가 Metric 실습
1단계. 데이터 생성
# 데이터 생성 변수
w0 = 2.3
w1 = 1
num_data = 100
noise = np.random.normal(0, 3, num_data)
# 데이터 생성
X = np.linspace(0, 5, num_data)
y = w0 + w1 * (X ** 2) + noise
# 시각화
plt.figure(figsize=(8, 6))
plt.scatter(X, y)
plt.xlabel('X')
plt.ylabel('y')
plt.margins(0.2)
plt.show()
2단계. 선형 회귀 모델 생성 및 학습
from sklearn.linear_model import LinearRegression
X = X.reshape(-1, 1) # 학습을 위한 차원 변환 진행
linear_reg = LinearRegression()
linear_reg.fit(X, y)
3단계. 회귀 결과 확인
# 회귀선 시각화
y_pred = linear_reg.predict(X)
plt.figure(figsize=(8, 6))
plt.scatter(X, y, alpha=0.5, label='Data')
plt.plot(X, y_pred, color='red', label=f'Linear Regression')
plt.title('Regression Line On Data')
plt.xlabel('X')
plt.ylabel('y')
plt.legend()
plt.show()
4단계. 성능평가 - R2 score 확인
from sklearn.metrics import r2_score
r2 = r2_score(y, y_pred)
print(f'R² 값 : {r2:.4f}')
# 시각화
plt.figure(figsize=(8, 6))
plt.scatter(X, y, alpha=0.5, label='Data')
plt.plot(X, y_pred, color='red', label=f'Linear Regression (R² = {r2:.2f})')
y_mean = np.mean(y)
plt.axhline(y=y_mean, color='green', linestyle='--', label=f'Mean (y = {y_mean:.2f})')
plt.title('Regression Line with R² and Data Mean')
plt.xlabel('X')
plt.ylabel('y')
plt.legend()
plt.show()