교차 검증
이전에 전체 데이터 샘플수가 부족하여 검증 세트를 훈련 세트에서 분리하여 훈련 세트의 샘플 개수가 부족하여 모델을 충분히 훈련시키는데 문제가 된 경우가 있었다.
이러한 문제를 해결할 방법으로 교차 검증이 쓰인다.
교차 검증(Cross Validation) :
이전에는 전체 데이터 세트를 훈련 세트와 테스트 세트로 나눈 후 훈련세트를 다시 훈련세트와 검증세트로 나누었다
하지만 데이터가 부족한 경우에 나누어진 전체 데이터 세트를 훈련 세트와 테스트 세트로 나눈 후 나누어진 훈련 세트를 다시 작은 덩어리로 나눈다.
이런 작은 덩어리를 폴드라고 부른다.
그리고 나서 작은 덩어리를 1번씩 검증에 사용하고 나머지 덩어리를 훈련에 사용하는 검증 방법을 교차 검증이라고 한다.
그림으로 나타내면
위 방법은 폴드를 5개로 나눈 5-폴드 교차 검증이다.
교차 검증 과정
- 훈련 세트를 k개의 폴드로 나눈다.
- 첫 번쨰 폴드를 검증 세트로 사용하고, 나머지 폴드를 훈련 세트로 사용한다.
- 모델을 훈련하고 검증 세트로 평가한다.
- 차례대로 다음 폴드를 검증 세트로 사용하여 반복한다.
- k개의 검증 세트로 k번 성능을 평가한 후 계산된 성능의 평균을 내어 최종 성능을 계산한다.
k-폴드 교차 검증 :
훈련 세트를 k개의 폴드로 나누는 교차 검증
교차 검증은 기존의 훈련 방법보다 더 많은 데이터로 훈련할 수 있기떄문에 훈련 데이터가 부족한 경우에 사용하는 방법이다.
이제는 k-폴드 교차 검증을 직접 구현해보자.
import numpy as np
# SingleLayer 클래스를 사용할 것이기 때문에
class SingleLayer:
def __init__(self, learning_rate = 0.1, l1 = 0, l2 = 0):
self.w = None
self.b = None
self.losses = []
self.val_losses = []
self.w_history = []
self.lr = learning_rate
self.l1 = l1
self.l2 = l2
def forpass(self, x):
z = np.sum(x * self.w) + self.b
return z
def backprop(self, x, err):
w_grad = x * err
b_grad = 1 * err
return w_grad, b_grad
def activation(self, z):
z = np.clip(z, -100, None)
a = 1 / (1 + np.exp(-z))
return a
def fit(self, x, y, epochs = 100, x_val = None, y_val = None):
self.w = np.ones(x.shape[1])
self.b = 0
self.w_history.append(self.w.copy())
np.random.seed(42)
for i in range (epochs):
loss = 0
indexes = np.random.permutation(np.arange(len(x)))
for i in indexes:
z = self.forpass(x[i])
a = self.activation(z)
err = -(y[i] - a)
w_grad, b_grad = self.backprop(x[i], err)
w_grad += self.l1 * np.sign(self.w) + self.l2 * self.w
self.w -= self.lr * w_grad
self.b -= b_grad
self.w_history.append(self.w.copy())
a = np.clip(a, 1e-10, 1 - 1e-10)
loss += -(y[i] * np.log(a) + (1 - y[i]) * np.log(1 - a))
self.losses.append(loss/len(y) + self.reg_loss())
self.update_val_loss(x_val, y_val)
def predict(self, x):
z = [self.forpass(x_i) for x_i in x]
return np.array(z) >= 0
def update_val_loss(self, x_val, y_val):
if x_val is None:
return
val_loss = 0
for i in range(len(x_val)):
z = self.forpass(x_val[i])
a = self.activation(z)
a = np.clip(a, 1e-10, 1 - 1e-10)
val_loss += -(y_val[i] * np.log(a) + (1 - y_val[i]) * np.log(1 - a))
self.val_losses.append(val_loss / len(y_val) + self.reg_loss())
def score(self, x, y):
return np.mean(self.predict(x) == y)
def reg_loss(self):
return self.l1 * np.sum(np.abs(self.w)) + self.l2 / 2 * np.sum(self.w ** 2)
# 훈련 세트를 사용하기
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
cancer = load_breast_cancer()
x = cancer.data
y = cancer.target
# 먼저 전체 데이터 세트를 8 : 2로 나누어 훈련 세트와 테스트 세트를 얻는다.
x_train_all, x_test, y_train_all, y_test = train_test_split(x, y, stratify = y, test_size = 0.2, random_state = 42)
# 각 폴드의 검증 점수를 저장하기 위해 리스트를 하나 정의
validation_scores = []
# k-폴드 교차 검증을 구현해보자
k = 10 # 10개의 폴드
bins = len(x_train_all) // k # 훈련 세트를 k개로 나눔
# k-폴드 교차 검증을 위해 반복문 사용
for i in range(k):
start = i * bins
end = (i + 1) * bins # start와 end는 검증 폴드 샘플의 시작과 끝 인덱스를 의미
val_fold = x_train_all[start:end]
val_target = y_train_all[start:end]
train_index = list(range(0, start)) + list(range(end, len(x_train_all))) # 검증 폴드 샘플 이외의 부분이 훈련 폴드
train_fold = x_train_all[train_index]
train_target = y_train_all[train_index]
# 훈련 데이터의 표준화 전처리를 폴드를 나눈 후에 수행한다!!!
# => 만약 폴드를 나누기 전에 전체 훈련 데이터를 전처리하면 검증 폴드의 정보를 누설하게 되는 셈이기 때문
train_mean = np.mean(train_fold, axis = 0)
train_std = np.std(train_fold, axis = 0)
train_fold_scaled = (train_fold - train_mean) / train_std
val_fold_scaled = (val_fold - train_mean) / train_std
lyr = SingleLayer(l2 = 0.01)
lyr.fit(train_fold_scaled, train_target, epochs = 50)
score = lyr.score(val_fold_scaled, val_target)
validation_scores.append(score)
print(np.mean(validation_scores)) # validatoin_scores 값들의 평균값이 최종 검증 점수
0.9711111111111113
교차 검증도 당연하게도? 사이킷런에서 제공한다.
사이킷런의 model_selection 모듈에는 교차 검증을 위한 cross_validate() 함수가 있다.
지금까지 구현해놓은 SingleLayer 클래스와 cross_validate() 함수를 같이 사용하기 위해서는 SingleLayer 클래스에 몇 가지 기능을 추가해야 한다.
하지만 cross_validate() 함수를 사용하기 위해 추가해야하는 기능들이 현재 알기에는 어렵기 때문에 추후에 배우도록 하고
대신 SGDClassifier 클래스와 cross_validate() 함수를 같이 사용해서 구현해보자
# cross_validate() 함수로 교차 검증 점수를 계산해보자
from sklearn.linear_model import SGDClassifier
from sklearn.model_selection import cross_validate
sgd = SGDClassifier(loss = 'log', penalty = 'l2', alpha = 0.001, random_state = 42)
# cross_validate() 함수의 매개변수 값으로 교차 검증을 하고 싶은 모델의 객체와 훈련 데이터, 타깃 뎅이터를 전달하고
# 매개변수 cv에 교차 검증을 수행할 폴드 수를 지정하면 된다.
scores = cross_validate(sgd, x_train_all, y_train_all, cv = 10)
# cross_validate() 함수는 파이썬 딕셔너리를 반환하기 때문에
# 검증 점수는 scores['test score']에 저장 되어 있다.
print(np.mean(scores['test_score']))
# 아니 cross_validate() 안쓰고 직접 구현한 이전 보다 왜 정확도가 이렇게 낮지?
# 표준화 전처리를 아직 수행하지 않았기 때문이지..
0.850096618357488
훈련 세트를 표준화 전처리하기 전에 조심해야할 부분이 있다.
이전에 교차 검증을 직접 구현할 때에는 폴드를 나눈 후에 훈련 폴드의 통계치(평균, 표준편차)로 검증 폴드를 전처리 하였다.
그 이유는 폴드를 나누기 전에 전처리하면 검증 폴드의 정보를 전처리 단계에서 누설하게 되기 때문이다.
이처럼 cross_validate() 함수를 이용하여 교차 검증을 수행할때 전처리를 위해서 전체 데이터를 전처리 후에 cross_validate() 함수 매개변수 값으로 전달하면 검증 폴드의 정보를 전처리 단계에서 누설 하게된다.
따라서 이러한 방법이 아닌 새로운 방법을 찾아야 cro_validate() 함수를 사용하여 교차 검증을 수행할때 전처리 단계를 포함할 수 있다.
Pipeline 클래스 사용해서 교차 검증 수행하기
사이킷런에서는 검증 폴드가 전처리 단계에서 누설되지 않도록 전처리 단계와 모델 클래스를 하나로 연결해주는 클래스를 제공하는데 그 클래스가 Pipeline 클래스 이다.
# Pipeline 클래스와 SGDClassifier 클래스가 어떻게 작동하는지 알아보자.
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
# 먼저 표준화 전처리 단계(평균, 표준편차 계산)와 SGDClassifier 클래스 객체를
# Pipeline 클래스로 감싸 cross_validate() 함수에 전달
pipe = make_pipeline(StandardScaler(), sgd) # 파이프라인 객체 만듬
# 표준화 전처리 단계 : StandardScaler()
# SGDClassifier 클래스 객체 : sgd
# Pipeline 클래스 : make_pipeline
# 사이킷런에서 표준화 전처리를 수행하는 클래스는 preprocessing 모듈 밑에 있는 StandardScaler 클래스
scores = cross_validate(pipe, x_train_all, y_train_all, cv = 10, return_train_score = True)
# 위 방식으로 하면
# cross_validate() 함수는 훈련 세트를 훈련 폴드와 검증 폴드로 나누기만 하고
# 전처리 단계와 SGDClassifier 클래스 객체의 호출은 Pipeline 클래스 객체에서 이뤄진다
# 이렇게 하면 검증 폴드가 전처리 단계에 누설되지 않게 된다.
# cross_validate() 함수의 매개변수 return_train_score를 True로 설정하면 훈련 폴드의 점수도 얻을 수 있음.
print(np.mean(scores['test_score']))
# 전처리 하기전 보다 확실히 정확도가 높아졌다. (검증 폴드에 대한 정확도)
0.9694202898550724
print(np.mean(scores['train_score']))
# 훈련 폴드에 대한 정확도
0.9875478561631581
지금까지는 단일층 신경망을 만들고 여러 유용한 훈련 방법에 대해 알아 보았다.
다음부터는 여러개의 층이 있는 신경망 구조인 다층 신경망 알고리즘에 대해 알아볼 것이다
Reference
박해선, 딥러닝 입문, 이지스퍼블리싱, 2019, 149 ~ 155pg