0. 들어가면서
지금까지 SVM, 의사결정나무, 앙상블 등 다양한 분류 모델에 대해서 다뤄보았다. 물론, 이 후에도 분류 모델에 대해서는 추가적으로 다룰 예정이지만, 기본적으로 사용할 수 있는 분류 모델들에 대해서는 설명하였고, 신경망의 경우 추후 딥러닝을 다루는 과정에서 등장할 예정이기에 분류 모델에 대한 사용법은 1차적으로 종료하고 다음 분석기법에 대해 설명하기 전, 각 분류 모델을 학습했을 때의 성능 평가를 하는 방법을 설명하고자 작성하였다.
때문에 이번 장에서도 기본적으로 분류모델의 성능을 평가하는 방법과 대표적인 몇 가지 지표에 대해서 설명할 예정이며, 추후에 이번 장에서 설명하지 않은 지표나 평가 방법을 다룰 예정이니 참고하기 바란다.
1. 분류 모델의 성능평가
앞서 회귀에서 모델의 성능평가를 할 때는 모델이 통계적으로 유의미한지, 회귀 계수나 설명력이 유의미하거나 충분한지 등과 같은 지표들로 회귀 모델을 평가했었다. 이와 마찬가지로 분류 모델에 대해서도 모델의 성능을 평가하는 지표들이 있는데, 이들을 설명하기 앞서, 어떤 식으로 점검하는 지, 관련된 기준과 목표를 먼저 살펴보도록 하자.
분류모델의 평가는 예측 및 분류를 위해 생성한 모델이 임의의 모델보다 더 우수한 성능을 보이는지와 고려된 다른 모델들 중 가장 우수한 예측 및 분류 성과를 갖는 지 등을 비교분석하게 된다.
1.1 점검 사항
분류 모델의 성능평가를 위해서 아래 항목들을 사전에 점검하는 것이 좋다.
① 일반화의 가능성
같은 모집단 내의 다른 데이터를 적용하려는 경우, 안정적인 결과를 제공하는 가에 대한 지표로, 모델의 확장성을 살펴보는 지표이다.
② 효율성
분류 분석 모델이 얼마나 효과적으로 구축되었는지를 평가하며, 입력 변수의 수가 적음에도 잘 분류한다면, 효율성이 높다고 할 수 있다.
③ 예측 및 분류 정확성
구축된 모형의 정확성 측면에서 평가하는 것으로, 안정적이고 효율적인 모델을 구축해도, 실제 적용할 데이터를 직면했을 때, 정확한 결과를 예측하지 못한다면 의미가 없다.
위의 3가지를 기준으로 분류 모델에 대한 성능 평가를 진행하면 된다.
1.2 검증 (Validation)
그렇다면 어떻게 위의 3가지 기준을 확인할 수 있을까? 이를 위한 수행 방법 중 하나로 모델에 대한 검증을 진행하는 방법이 있다. 모델의 정확한 성능을 측정하고, 동시에 과대적합이 되지 않도록 방지를 하기 위한 수단이라고 볼 수 있다. 모델을 검증하는 방법 역시 여러가지 방법들이 존재하는 데 이번 장에서는 홀드아웃, 교차검증, 부트스트랩이라는 대표적인 3가지 검증 방법에 대해서 다뤄볼 것이다.
1.2.1 홀드 아웃 (Hold-Out) 기법
가장 기본이 되는 검증 방법으로, 크게는 모델 훈련에 사용할 훈련 세트와 모델의 성능을 추정하는데 사용할 테스트 세트로 나눈다. 하지만 예측 성능을 높이기 위해서는 하이퍼파라미터를 튜닝하고 비교해야 하는 데, 이 때 모델 선택에 같은 테스트 세트를 반복해 재사용하면 훈련 세트의 일부가 되는 현상이 있어 과적합의 원인이 될 수 있다. 이를 위해 검증 세트를 추가로 나눠서, 모델 학습 시 다른 하이퍼파라미터 값으로 반복 훈련하고 성능을 평가하여, 만족할 만한 성능이 나왔을 때, 테스트 데이터를 사용해 일반화 성능을 추정하는 식으로 모델 학습 및 평가를 진행할 수 있다.
만약 학습용, 테스트용으로만 데이터를 분할하는 경우라면 7:3 혹은 8:2 의 비율로 나누는 것이 일반적이고, 검증용 데이터 셋까지 포함한다면, 5:3:2 의 비율로 분할하는 것이 일반적이다.
특징으로는 우선 방법 자체가 전체 데이터를 일정 비율만큼 나누는 작업만 있기 때문에, 단순하고, 작업을 빠르게 할 수 있다는 장점이 있으며, 전체 데이터 양이 많은 경우에는 충분히 학습 및 평가를 진행할 수 있지만, 데이터가 적은 경우라면 어떻게 검증 데이터 셋과 테스트 데이터 셋을 선정하는 가에 따라 성능에 크게 영향을 미칠 수 있다는 단점도 존재한다.
1.2.2 K-Fold 교차 검증
방금 전에 본 홀드아웃 기법의 단점을 보완하기 위해서 고안된 검증 방법론으로, 전체 데이터 셋을 임의의 K개로 분할한 후, K 번 만큼 각 폴드 세트에 대해 학습과 평가를 반복하는 과정이다. 예시로, 아래는 k값을 5로 설정했을 때의 학습 진행 방법이다.
위의 내용을 코드로 구현해보자면, 다음과 같이 Python 과 R 로 각각 구현할 수 있다.
import numpy as np
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
from sklearn.model_selection import KFold
iris = load_iris()
scaler = StandardScaler()
x = scaler.fit_transform(iris.data)
y = (iris.target == 2).astype(np.float64) # Y = 1 / N = 0
kfold = KFold(n_splits=10)
predictions = []
train_count = 0
# 모델 정의
model = SVC()
for train_idx, test_idx in kfold.split(x, y):
train_count += 1
x_train, x_test = x[train_idx], x[test_idx]
y_train, y_test = y[train_idx], y[test_idx]
model.fit(x_train, y_train)
y_pred = model.predict(x_test)
total = len(x_test)
correct = 0
for i in range(len(y_pred)):
if y_pred[i] == y_test[i]:
correct+=1
print("{} 회 학습 결과 : ".format(train_count), round(100 * correct / total , 2), "%")
predictions.append(round(100 * correct / total , 2))
print("="*25)
print("{}-Folds 학습 결과 : ".format(train_count), round(np.mean(predictions), 2), "%")
[실행결과]
1 회 학습 결과 : 100.0 %
2 회 학습 결과 : 100.0 %
3 회 학습 결과 : 100.0 %
4 회 학습 결과 : 100.0 %
5 회 학습 결과 : 93.33 %
6 회 학습 결과 : 86.67 %
7 회 학습 결과 : 100.0 %
8 회 학습 결과 : 86.67 %
9 회 학습 결과 : 86.67 %
10 회 학습 결과 : 93.33 %
============================
10-Folds 학습 결과 : 94.67 %
# 필요한 라이브러리 설치
install.packages("e1071") # SVM 모델을 위한 패키지
install.packages("caret") # 교차 검증을 위한 패키지
# 라이브러리 로드
library(e1071)
library(caret)
# 데이터셋 로드 및 스케일링
data(iris)
x <- scale(iris[, -5]) # 피처 스케일링 (StandardScaler와 동일)
y <- ifelse(iris$Species == "virginica", 1, 0) # virginica를 1로, 나머지는 0으로
# 10-폴드 교차 검증 준비
set.seed(123) # 결과 재현성을 위해 시드 설정
folds <- cut(seq(1, nrow(iris)), breaks=10, labels=FALSE)
predictions <- c()
train_count <- 0
# 교차 검증 루프
for (i in 1:10) {
train_count <- train_count + 1
# 학습 및 테스트 데이터셋 나누기
test_idx <- which(folds == i, arr.ind = TRUE)
x_train <- x[-test_idx, ]
x_test <- x[test_idx, ]
y_train <- y[-test_idx]
y_test <- y[test_idx]
# SVM 모델 학습
model <- svm(x_train, as.factor(y_train), kernel = "radial", cost = 1, gamma = 1 / ncol(x_train)) # C=1, gamma='scale'
y_pred <- predict(model, x_test)
# 정확도 계산
total <- length(y_test)
correct <- sum(as.numeric(y_pred) == y_test)
accuracy <- round(100 * correct / total, 2)
cat(train_count, "회 학습 결과:", accuracy, "%\n")
predictions <- c(predictions, accuracy)
}
# 전체 교차 검증 결과 출력
cat(rep("=", 10), "\n")
cat(length(kfold), "-Folds 학습 결과:", round(mean(predictions), 2), "%\n")
[실행 결과]
1 회 학습 결과: 0 %
2 회 학습 결과: 0 %
3 회 학습 결과: 0 %
4 회 학습 결과: 0 %
5 회 학습 결과: 0 %
6 회 학습 결과: 0 %
7 회 학습 결과: 0 %
8 회 학습 결과: 13.33 %
9 회 학습 결과: 6.67 %
10 회 학습 결과: 6.67 %
위의 2개 코드를 실행해보면 알 수 있지만, 둘 간의 결과가 서로 다른 것을 알 수 있다. 이렇게 다른 결과가 나오는 이유로는 언어가 다르기 때문에 실행하는 구조가 다르다는 등 여러 이유가 있을 수 있지만, 가장 큰 이유는 선정한 클래스인 'Virginica' 클래스 수가 적은 것이 크다. Iris 데이터 상에서 약 1/3 을 차지하는데, 해당 클래스의 데이터 수가 SVM이 학습할 만큼 적당한 크기가 아닐 수 있으며, 때문에 학습이 원활히 이뤄지지 않아, 모델의 정확도가 매우 낮게 나오는 현상이 나온 것이다.
위와 같이 데이터의 클래스가 불균형이 심해 학습이 제대로 이뤄지지 않는 현상을 방지하기 위한 방법 중 하나로 계층적 교차검증 (Stratified K Folds) 을 수행해볼 수 있다.
1.2.3 계층적 교차 검증 (Stratified K-Folds)
일반적인 K-Folds 와 달리, 계층적 교차검증에서는 각 Fold에서 양성, 음석 레이블의 비율이 전체 데이터셋의 비율과 유사하게 유지되므로, 각 폴드가 대표적인 샘플로 이루어지며, 모델이 레이블의 편향을 학습하는 현상을 방지할 수 있다. 아래 코드를 확인하면 알 수 있지만, 기존 K-folds 의 코드에서와 가장 큰 차이점이 있다면, 학습부분에서 전체 데이터의 feature 와 label을 같이 짝지어 넣어줌으로써, 클래스 분포를 고려해 균등한 분포로 교차검증을 수행할 수 있다는 점이 개선되었다. 자세한 건 아래 코드를 확인하기 바란다.
import numpy as np
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
from sklearn.model_selection import StratifiedKFold
# 데이터셋 로드 및 스케일링
iris = load_iris()
scaler = StandardScaler()
x = scaler.fit_transform(iris.data)
y = (iris.target == 2).astype(np.float64) # Y = 1 / N = 0
# Stratified K-Fold 설정 (계층적 교차 검증)
skf = StratifiedKFold(n_splits=10)
predictions = []
train_count = 0
# 모델 정의
model = SVC()
# 계층적 교차 검증 수행
for train_idx, test_idx in skf.split(x, y):
train_count += 1
x_train, x_test = x[train_idx], x[test_idx]
y_train, y_test = y[train_idx], y[test_idx]
# 모델 학습
model.fit(x_train, y_train)
y_pred = model.predict(x_test)
# 정확도 계산
total = len(y_test)
correct = np.sum(y_pred == y_test)
# 각 fold 결과 출력
accuracy = round(100 * correct / total, 2)
print("{} 회 학습 결과 : ".format(train_count), accuracy, "%")
predictions.append(accuracy)
# 전체 교차 검증 결과 출력
print("="*10)
print("{}-Folds 학습 결과 : ".format(train_count), round(np.mean(predictions), 2), "%")
[실행 결과]
1 회 학습 결과 : 100.0 %
2 회 학습 결과 : 93.33 %
3 회 학습 결과 : 100.0 %
4 회 학습 결과 : 93.33 %
5 회 학습 결과 : 100.0 %
6 회 학습 결과 : 100.0 %
7 회 학습 결과 : 93.33 %
8 회 학습 결과 : 86.67 %
9 회 학습 결과 : 93.33 %
10 회 학습 결과 : 100.0 %
==========
10-Folds 학습 결과 : 96.0 %
# 패키지 로드
library(e1071)
library(caret)
# 데이터셋 로드 및 스케일링
data(iris)
x <- scale(iris[, -5]) # 피처 스케일링 (StandardScaler와 동일)
y <- ifelse(iris$Species == "virginica", 1, 0) # virginica를 1로, 나머지를 0으로 변환
# Stratified K-Fold 설정 (계층적 교차 검증)
set.seed(123) # 결과 재현성을 위해 시드 설정
folds <- createFolds(y, k = 10, list = TRUE, returnTrain = FALSE)
predictions <- c()
train_count <- 0
# 클래스 비율 계산 (불균형 해결을 위한 가중치 설정)
class_weights <- table(y) / length(y)
class_weights <- 1 / class_weights # 클래스 비율의 역수로 가중치 설정
# 계층적 교차 검증 수행
for (i in 1:10) {
train_count <- train_count + 1
# 학습 및 테스트 데이터셋 나누기
test_idx <- folds[[i]]
x_train <- x[-test_idx, ]
x_test <- x[test_idx, ]
y_train <- y[-test_idx]
y_test <- y[test_idx]
# 모델 학습 (각 fold마다 svm() 호출, class.weights 추가)
model <- svm(x_train, as.factor(y_train), kernel = "radial", cost = 1, gamma = 1 / ncol(x_train),
class.weights = class_weights) # 불균형한 클래스 가중치 적용
# 테스트 데이터에 대한 예측
y_pred <- predict(model, x_test)
# 정확도 계산
total <- length(y_test)
correct <- sum(as.numeric(y_pred) == y_test)
# 각 fold 결과 출력
accuracy <- round(100 * correct / total, 2)
cat(train_count, "회 학습 결과:", accuracy, "%\n")
predictions <- c(predictions, accuracy)
}
# 전체 교차 검증 결과 출력
cat(rep("=", 10), "\n")
cat("10-Folds 학습 결과:", round(mean(predictions), 2), "%\n")
위의 방법으로도 성능이 안나오는 경우라면, 사용된 SVM의 모델 내 하이퍼파라미터를 수정해 모델의 성능을 개선하거나, 다른 샘플링 기법을 사용해 검증을 하는 방법 등이 있다.
1.2.4 부트스트랩 기법
부트스트랩 기법은 평가를 반복한다는 측면에서, 앞서 본 교차 검증과 유사하지만, 훈련용 데이터를 반복적으로 재선정한다는 점에서 차이가 있다. 관측치를 한 번 이상 훈련용 자료로 사용하는 복원추출법에 기반한 학습 방법이다. 따라서 훈련용 자료 선정을 d번 반복할 때 하나의 관측치가 선정될 확률은 1/d 가 된다. 일반적으로 훈련용 데이터의 63.2%를 훈련으로, 나머지 36.8%는 선정되지 않았으므로, 검증용으로 사용된다.
Scikit Learn 의 경우, 이전에는 "cross_validation" 패키지 안에 Bootstrap 이라는 함수가 존재했으나, 최신 버전에서는 더 이상 존재하지 않으며, 대신 util 패키지의 resample 함수를 이용해서 부트스트랩을 구현할 수 있다. 만약 R을 사용하는 경우라면, createResample() 함수를 사용해서 구현할 수 있다.
import numpy as np
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
from sklearn.utils import resample
iris = load_iris()
scaler = StandardScaler()
x = scaler.fit_transform(iris.data)
y = (iris.target == 2).astype(np.float64) # Y = 1 / N = 0
x_train, y_train = resample(x, y, n_samples=int(x.shape[0] * 0.7))
x_test = np.array([value for value in x if x.tolist() not in x_train.tolist()])
y_test = np.array([value for value in y if y.tolist() not in y_train.tolist()])
....
2. 평가 지표 (Metrics)
다음으로는 평가 지표에 대해서 살펴보도록 하자. 자고로 좋은 모델을 만든다는 것은 어떤 모델이 좋은 지를 정해야한다. 이에 대해 다양한 평가 메트릭의 종류와 의미에 대해서 알아볼 예정이다.
설명에 앞서, 임의의 분류문제에 대해 분류체계가 참(True), 거짓(False) 만 존재한다고 가정하자. 이에 대해 실제 결과와 예측 결과에 대해서 조합을 고려하면, 아래와 같은 그림이 나오게 된다.
예측 결과 | |||
참 | 거짓 | ||
실제 값 | 참 | TP | FN |
거짓 | FP | TN |
위와 같은 표를 가리켜, 혼동행렬(Confusion Matrix)이라고 부르며, 위의 그림을 통해 알 수 있듯이 모델에서 계산한 결과인 예측 결과와 데이터의 실제 값의 발생 빈도를 나열한 것이다. 행렬 내 각각의 실제 값과 예측 결과 값의 조합에 따라 아래와 같이 부른다.
- 예측결과 참, 실제값 참 인 경우: TP(True Positive) / 예측이 정확했고, 예측 결과는 참(Positive) 임을 의미
- 예측결과 거짓, 실제값 참 인 경우: FP(False Positive) / 예측이 실패했고, 예측결과는 참(Positive) 임을 의미
- 예측결과 참, 실제값 거짓 인 경우: FN(False Negative) / 예측이 실패했고, 예측결과는 거짓(Negative) 임을 의미
- 예측결과 거짓, 실제값 거짓 인 경우: TN(True Negative) / 예측이 성공했고, 예측결과는 거짓(Negative) 임을 의미
위의 혼동행렬에 나오는 개념들을 통해 추가적으로 아래와 같은 평가 지표들을 계산할 수 있다.
평가 지표 | 계산식 | 의미 |
정분류율 (Accuracy) | $ Accuracy = \frac {(TP + TN)} {(TP + TN + FP + FN)} $ | 전체 예측(참, 거짓 무관하게 예측한 결과)에서 옳게 분류한 결과의 비율을 의미함 |
정밀도 (Precision) | $ Precision = \frac {TP} {TP + FP} $ | 참으로 예측한 결과 중 실제 참인 값의 비율을 의미함 |
재현율 또는 민감도 (Recall, Sensitivity) | $ Recall = \frac {TP} {TP + FN} $ | 실제 참인 값 중 예측 결과가 참인 값의 비율을 의미함 민감도와 동일한 의미이며, 모형의 완전성을 나타내는 지표임 |
특이도 (Specificity) | $ Specificity = \frac {TN} {TN + FP} $ | 실제 거짓인 값 중 예측 결과가 거짓인 값의 비율을 의미함 |
F1 Score | $ F1 = \frac {2 \times Precision \times Recall} {Precision + Recall} $ | Precision 과 Recall 의 조화평균으로, 시스템의 성능을 하나의 수치로 표현하기 위한 지표를 의미함 |
간혹 모델의 성능을 평가할 때, 정분류율(Accuracy)의 수치가 높다는 이유로 해당 모델의 성능이 우수하다고 단정짓는 경우를 볼 수 있는데, 분류 대상의 클래스가 한 쪽으로 치우쳐진 경우에도 정분류율이 높게 나올 수 있기 때문에, 여러 개의 지표를 통해 의미를 분석하여 성능을 결정하는 것이 좋다. 이 때, 지표들 중 정밀도(Precision) 과 재현율(Recall)은 모델의 성능을 나타내는 대표적인 평가지표로 사용할 수 있지만, 하나의 지표가 높아지면, 다른 지표는 감소하는 상호 반비례 관계에 있는 지표이므로 적절한 기준을 정하는 것이 중요하다.
3. ROC 커브 (ROC Curve)
끝으로 ROC 커브에 대해서 설명하고 마무리하겠다. ROC 커브는 본래 레이더 이미지 분석의 성과 측정을 위해 개발된 지표이며, 이 후 의학 및 머신러닝 분야, 특히 이진 분류의 문제에서 많이 활용되어졌다. 주로 다양한 임계값 설정에서 분류 모델의 성능을 평가하기 위해 사용되며, X 축은 (1 - 특이도) 값으로 모델이 실제 참인 값을 얼마나 잘 식별하는지를, Y축은 민감도 (혹은 재현율)로 실제 거짓인 값을 얼마나 잘 식별하는지를 수치로 설정한다.
또한 ROC 커브의 중요한 부분 중 하나로 곡선 아래 면적(AUC, Area Under the Curve)이 있는데, 모델의 성능을 요약하는 수치라고 할 수 있다. 해당 값은 0~1 사이의 값을 가지며, 1에 가까울 수록 완벽한 모델임을 의미하고, 0.5에 가까울수록 무작위 추측으로 예측한다고 해석할 수 있다. 때문에 ROC 커브의 곡선 끝이 좌상단 끝에 위치할수록 좋은 분류 모델이라고 해석한다. 파이썬의 경우 scikit-learn 내 metrics의 RocCurveDisplay 클래스를 사용하면 가능하며, R을 사용하는 경우라면, Epi 패키지나, pROC, ROCR 패키지를 사용해서 모델 평가 및 시각화를 구현할 수 있다.
'Data Science > 데이터 분석 📊' 카테고리의 다른 글
[데이터 분석] 20. 차원 축소 (Dimensional Reduction) Ⅱ : 커널 PCA, LLE, LDA (0) | 2024.10.07 |
---|---|
[데이터 분석] 19. 차원 축소 (Dimensional Reduction) Ⅰ : PCA (0) | 2024.09.29 |
[데이터 분석] 17. 랜덤 포레스트 (Random Forest) (0) | 2024.09.01 |
[데이터 분석] 16. 앙상블 (Ensemble) (0) | 2024.08.17 |
[데이터 분석] 15. 의사결정나무 (1) | 2024.08.14 |