HyeM

[Do it] 검증세트와 전처리 과정, 과대적합과 과소적합 본문

Study/AI&DeepLearning

[Do it] 검증세트와 전처리 과정, 과대적합과 과소적합

Hailey_HyeM207 2021. 6. 2. 05:30

CH 05-1 검증 세트를 나누고 전처리 과정을 배웁니다.

1. 테스트 세트로 모델을 튜닝하기

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.linear_model import SGDClassifier
import numpy as np

#1. cancer 데이터 읽어들여 훈련 세트와 테스트 세트로 나누기
cancer = load_breast_cancer()
x = cancer.data
y = cancer.target

x_train_all, x_test, y_train_all, y_test = train_test_split(x, y, stratify=y, test_size=0.2, random_state=42)

 

 

1-01. SGDClassifier로 로지스틱 회귀 모델 훈련

 

 

1-02. SGDClassifier로 서포트 벡터 머신 훈련

 

 

loss는 하이퍼파라미터로 loss 값에 따라 성능이 달라질 수 있음

  • log : 로지스틱 회귀 모델
  • hinge : 서포트 벡터 머신
  • => 위의 예제에서는 서포트 벡터 머신으로 모델 훈련시 성능이 더 좋음

 

 

 

2. 검증 세트

테스트 세트로 모델을 튜닝시, 실전에서 좋은 성능을 기대하기 어렵다.
즉, 모델 튜닝시 테스트 세트를 사용하지 않으면 된다.
=> 모델 튜닝을 위한 세트는 따로 준비해야 됨 (검증세트)

검증 세트는 훈련 세트를 조금 떼어 만든다.

 

 

2-01. 데이터 셋 준비하기

2-02. 검증 세트 분할하기

2-03. 검증 세트 사용해 모델 평가하기

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.linear_model import SGDClassifier

# 1. 데이터 셋 준비하기
# 훈련 세트 : 테스트 세트 = 8:2
cancer = load_breast_cancer()
x = cancer.data
y = cancer.target

x_train_all, x_test, y_train_all, y_test = train_test_split(x, y, stratify=y, test_size=0.2, random_state=42)


# 2. 검증 세트 분할하기 
# 훈련세트(x_train) : 검증세트(x_val) = 8:2
x_train, x_val, y_train, y_val = train_test_split(x_train_all, y_train_all, stratify=y_train_all, test_size=0.2, random_state=42)
print(len(x_train), len(x_val))

# 3. 검증 세트 사용해 모델 평가하기 
sgd = SGDClassifier(loss='log', random_state=42)
sgd.fit(x_train, y_train)
sgd.score(x_val, y_val)

훈련 세트의 크기가 줄어서 앞의 평가 점수보다 낮아짐.

 

데이터 양이 너무 적은 경우에는 검증 세트를 나누지 않고 교차 검증 을 사용하기도 함. 하지만 일반적으로 검증과 테스트 세트의 샘플 수를 1만개 이상 확보할 수 있다면, 데이터 셋을 많이 할당하는 것이 좋음

 

 

 

3. 데이터 전처리와 특성의 스케일 알아보기

정리되어 있지 않은 데이터는 '데이터 전처리' 필요함
잘 정리되어 있는 데이터도 특성의 스케일(값의 범위) 이 다른 경우 전처리가 필요함

 

경사 하강법은 스케일에 민감한 알고리즘이므로 스케일을 적절히 조정해야됨

 

 

 

 

4. 스케일 조정 없이 모델 훈련해보기

4-1. 훈련데이터 준비하기 스케일 비교하기

유방암 데이터의 mean perimeter과 mean area 특성의 스케일을 확인해보니, 두 특성의 스케일의 차이가 큰 것을 확인함

 

 

4-2. 가중치를 기록할 변수와 학습률 파라미터 추가하기

4-3. 가중치를 기록하고 업데이트 양 조절하기

class SingleLayer:
    
    def __init__(self,learning_rate=0.1):
        self.w = None
        self.b = None
        self.losses = []  # < 손실함수의 결과값을 저장할 리스트 >
        self.w_history = []
        self.lr = learning_rate # 학습률 파라미터

    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) # 안전한 np.exp() 계산을 위해
        a = 1 / (1 + np.exp(-z))  # 시그모이드 계산
        return a
        
    def fit(self, x, y, epochs=100):
        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):                    # 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) # 역방향 계산
                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))
    
    def predict(self, x):
        z = [self.forpass(x_i) for x_i in x]     # 정방향 계산
        return np.array(z) > 0                   # 스텝 함수 적용
    
    def score(self, x, y):
        return np.mean(self.predict(x) == y)

w_history리스트를 추가하여 가중치를 기록한다.
넘파이 배열을 리스트에 추가하면 실제 값이 복사되는것이 아니라 배열을 참조하기 때문에, 가중치 변수 self.w의 값이 바뀔때마다 그 값을 복사하여 w_history 리스트에 추가한다.
또한 w_grad에 학습률 self.lr을 곱하는 연산이 추가되어 가중치 업데이트 양을 조절함.

 

 

 

4-4. 모델 훈련하고 평가하기

정확도는 약 91% 정도이다.

 

 

4-5. mean_perimeter와 meanarea특성에 대한 가중치 그래프로 그리기

그래프를 보면 mean_perimeter에 비해 mean area의 스케일이 커서 w3값이 학습과정에서 큰 폭으로 흔들리며 변화하고 있다. 반면 w2는 조금씩 최적값에 가까워진다.
=> 'w3에 대한 그레이디언트가 크기 때문에 w3축을 따라 가중치가 크게 요동치고 있다.'
--> 해결방법 : 스케일 조정

 

 

 

 

 

5. 스케일 조정해 모델 훈련하기

스케일을 조정하는 방법은 많지만 신경망에서 자주 사용하는 스케일 조정 방법 중 하나는 표준화 이다.
표준화 특성값에서 평균을 빼고 표준편차로 나누면 됨. (평균이 0, 분산이 1)

사이킷런에 표준화를 위해 StandardScaler 클래스가 있지만, 이번 단계에선 직접 표준화를 구현해본다.

 

 

5-01. 넘파이로 표준화 구현하기

train_mean = np.mean(x_train, axis=0)
train_std = np.std(x_train, axis=0)
x_train_scaled = (x_train - train_mean) / train_std

넘파이의 mean(), std() 함수를 사용하여 평균과 표준편차를 계산하여 표준화를 구한다.
그런 다음 특성별로 스케일을 조정한다. (axix=0 으로 지정하면 2차원 배열의 열을 기준으로 통계치를 계산하여 하나의 행 벡터로 반환해준다.)

 

 

5-02. 모델 훈련하기

  • 이전의 그래프와는 다르게 w2와 w3의 변화 비율이 비슷하여 대각선 방향으로 가중치가 이동되고 있음을 확인할 수 있다.
  • 두 특성의 스케일이 비슷하게 맞추어져 최적값에 빠르게 근접하고 있음을 알 수 있다.
    => 경사 하강법에서는 서로 다르 특성의 스케일을 맞추는 것이 매우 중요함

 

 

5-03. 모델 성능 평가하기

성능이 좋지 않다. 이유는 검증세트의 스케일을 바꾸지 않았기 때문이다.

 

 

 

5-04. 검증 세트의 스케일의 바꾼다.(표준화 전처리)

스케일을 바꾸고 나니 정확도가 96%로 5-03과정보단 높아졌다.
하지만 조심해야할 사항이 있다. 6번 과정을 통해 알아보자

 

 

 

 

 

6. 스케일을 조정한 후 실수하기 쉬운 함정 알아보기

위에서 말한 함정이란 훈련세트와 검증세트가 다른 비율로 스케일이 조정된 경우 를 말한다.
이해를 돕기 위해, 원본과 전처리된 훈련세트와 검증세트에서 데이터를 50개씩 뽑아 산점도를 그려 살펴본다.

 

 

6-01. 원본 훈련 세트와 검증 세트로 산점도 그리기

파란점이 훈련세트고 빨간점이 검증세트이다.

 

 

 

6-02. 전처리한 훈련 세트와 검증 세트로 산점도 그리기

과정 1과 산점도를 비교해보면 아주 미세한 차이로 훈련 세트와 검증세트가 각각 다른 비율로 변환됨을 알 수 있다. (원본 훈련 세트와 검증 세트의 점과 점 사이의 거리가 변환된 이후에 그대로 유지되지 않음)

 

점과 점 사이의 거리가 달라진 이유는 훈련 세트와 검증 세트를 각각 다르 비율로 전처리했기 때문으로, 데이터를 제대로 전처리했다면(스케일을 조정했다면) 훈련 세트와 검증세트의 거리가 그대로 유지되어야 한다.

 

 

 

 

6-03. 올바르게 검증 세트 전처리하기

검증 세트의 스케일이 훈련 세트의 스케일과 다른 비율로 조정되면 모델에 적용된 알고리즘들이 검증 세트의 샘플 데이터를 잘못 인식함.
검증 세트를 훈련세트와 같은 비율로 전처리 해야함
같은 비율로 전처리하는 방법은 훈련 세트의 평균, 표준 편차를 사용하여 검증 세트를 변환하면 됨.

이제 원본 데이터의 산점도와 스케일 조정 이후의 산점도가 같아짐(검증 세트와 훈련 세트가 동일한 비율로 변환됨)

 

 

 

6-04. 모델 평가하기

검증 세트로 모델의 성능을 평가한 결과이다. 만약 검증 세트가 클 경우 성능에 차이가 있을 수 있다.
하지만 이 예제의 경우는 데이터 세트가 크지 않기 때문에 검증 세트를 전처리하기 전과 후의 성능이 동일하다.

 

 

|| 모델 튜닝시 테스트 세트 대신 '검증 세트' 사용하기
|| 데이터 전처리시 훈련세트의 통계값으로 검증세트와 테스트 세트를 변환해야함

 

 

 

 

 

 

 

CH 05-2 과대적합과 과소적합 알아보기

  • 과대적합 : 모델이 훈련 세트에서는 좋은 성능을 내지만 검증세트에서 낮은 성능을 내는 경우
  • 과소적합 : 훈련세트와 검증세트의 성능에는 차이가 크지 않지만 모두 낮은 성능을 내는 경우
  • [ 과대적합 ]
    = 분산이 크다
    1. 더 많은 훈련 샘플 모으기on
    2. 가중치 제한
  • [ 과소적합 ]
    =편향이 크다
      1. 복잡도 더 높은 모델 사용

 

에포크와 손실함수 그래프
모델 복잡도와 손실함수 그래프

 

지금은 훈련세트의 크기나 모델의 복잡도를 변화시키기 어려우니 적절한 에포크 횟수를 찾아보자

 

1. 적절한 편향-분산 트레이드오프를 선택

과소적합된 모델(편향)과 과대적합된 모델(분산)사이의 관계를 편향-분산트레이드오프 라고 한다.
트레이드오프라는 말이 들어간 이유는 하나를 얻기위해서는 다른 하나를 희생해야 된다는 뜻으로,
분산이나 편향이 너무 커지지 않도록 적절한 중간지점을 선택한다.

여기 예제에서는 경사 하강법의 에포크횟수에 대한 모델의 손실을 그래프로 그려 '적절한 편향-분산 트레이드오프'를 선택해본다.

 

1-01. 검증 손실을 기록하기 위한 변수 추가

1-02. fit() 함수에 검증 세트 전달 받을 수 있도록 x_val, y_val 매개변수 추가

1-03. 검증 손실 계산

update_val_loss()함수에서 계산 (fit()메서드에서 훈련세트의 손실 계산하는 방식과 동일)

class SingleLayer:
    
    def __init__(self, learning_rate=0.1):
        self.w = None
        self.b = None
        self.losses = []
        self.val_losses = []
        self.w_history = []
        self.lr = learning_rate
        
    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) # 안전한 np.exp() 계산을 위해
        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):                    # 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) # 역방향 계산
                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.update_val_loss(x_val, y_val)
    
    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))
        
        
    def predict(self, x):
        z = [self.forpass(x_i) for x_i in x]     # 정방향 계산
        return np.array(z) > 0                   # 스텝 함수 적용
    
    def score(self, x, y):
        return np.mean(self.predict(x) == y)

 

 

1-04. 모델 훈련하기

 

1-05. 손실값으로 그래프 그려 에포크 횟수 지정

 

 

1-06. 훈련조기 종료하기

20번의 에포크까지 모델 훈련후 검증 세트의 성능 확인 결과 0.978 으로, 성능이 0.976 에서 조금 더 향상되었다.

Comments