본문으로 바로가기

5-1 검증 세트를 나누고 전처리 과정 배우기

category AI 2020. 11. 25. 12:06
728x90

 

 

  • 로지스틱 회귀로 모델 훈련하고 평가하기

 

- 훈련 데이터 세트와 테스트 세트로 분류

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

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, startify=y, test_size=0.2, random_state=43)

- SDGClassifier 클래스를 이용해 모델 훈련

from sklearn.linear_model import SGDClassifier

sgd = SGDClassifier(loss='log', random_state=42)
sgd.fit(x_train_all, y_train_all)
sgd.score(x_test, y_test)

- 약 88%의 정확도를 가지고 있다

- 정확도를 올리기 위해 다른 손실 함수를 이용하여도 된다.

- loss와 같은 매개변수 값은 사용자가 직접 결정하는 것

- loss와 같은 매개변수를 '하이퍼 파라미터' 라 함  

 

  • 서포트 벡터 머신으로 모델 훈련하고 평가하기

- SGDClassifier 클래스의 loss 매개변수를 log에서 hinge로 변형 시 선형 서포트 벡터 머신 문제를 푸는 모델 생성

from sklearn.linear_model import SGDClassifier

sgd = SGDClassifier(loss='hinge', random_state=42)
sgd.fit(x_train_all, y_train_all)
sgd.score(x_test, y_test)

- 약 90%의 정확도로 측정되고 있는데 기본 로지스틱 회귀모델보다 성능이 좋아졌다.

- 정확도 같은경우 각자 환경에 따라 변동이 있을 수 있다.

- 지금처럼 'log'를 'hinge'로 변경하는 것처럼 매개변수를 변경하는 행위를 '모델 튜닝' 이라한다.

 


테스트 세트로 모델 튜닝 시 실전에서 좋은 성능을 기대할 수 없다.

- 테스트 세트 모델 튜닝시 실전에서는 원하는 성능을 기대할 수 없다

- 이유는 테스트 세트 모델을 튜닝하여 정확도를 올린 경우 테스트 세트에 대해서 정확도가 올라간 행위이기 때문이다.

- 이런 행위를 '테스트 세트 정보가 모델에서 새어나갔다'라고 한다.

- 모델의 일반화 성능(genetalization performance)이 왜곡된다.

 

검증 세트 준비

- 모델을 튜닝하는 행위를 하는 곳은 테스트 세트를 사용하지 않도록 한다.

- 그러나 튜닝에는 성능 점수가 필요하다.

- 테스트 세트는 실전 투입 전 단 한 번만 사용하며, 튜닝용 세트를 따로 준비한다.

- 이를 '검증 세트'라고 한다.

- 검증 세트(vaildation set)는 훈련 세트의 일부를 이용


  • 데이터 세트 준비
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split

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)

 

  • 검증 세트 분할

- 훈련, 검증, 테스트의 비율이 6 : 2 : 2로 사용하지만 실제로는 훈련, 테스트 8 : 2로 제작 후 훈련을 8 : 2로 재분할

- 훈련 세트를 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))

- 455개의 훈련 세트가 8 : 2의 비율로 나눠져 훈련 세트(x_train) 364개, 검증 세트(y_train) 91개로 분류 완료

 

  • 검증 세트 사용해 모델 평가

 

from sklearn.linear_model import SGDClassifier

sgd = SGDClassifier(loss='log', random_state=42)
sgd.fit(x_train_all, y_train_all)
sgd.score(x_test, y_test)

- 이전에 테스트한 것보다 정확도가 감소

- 이유는 훈련 세트의 크기가 줄어듦 -> 검증 세트 생성

 


데이터 전처리와 특성의 스케일

- 사이킷런과 같은 머신러닝 패키지에 준비되어 있는 데이터는 대부분 실습을 위한 것으로 잘 가공됨

- 실전 데이터는 가공은커녕 누락이나 데이터 형태의 불균형 등의 문제가 발생할 수 있음

- 이를 보완하기 위한 데이터 전처리(data preprocessing) 과정이 필요하다

 

  • 특성의 스케일은 알고리즘에 영향을 준다

- 특성의 스케일(scale)은 어떤 특성이 가지고 있는 값의 범위를 말한다.

  당도 무게
사과 1 4 540
사과 2 8 700
사과 3 2 480

- 위의 표처럼 형태가 균일하고 누락도 없지만 특성의 스케일(값의 범위)이 다르다

- 당도의 범위가 1 ~ 10이고 무게 범위는 400 ~ 1000

- 위 같은 범위를 '두 특성의 스케일 차이가 크다' 라 한다

- 계속 배우고 있는 경사 하강법은 스케일에 민감한 알고리즘으로 특성의 스케일을 맞추는 전처리 같은 행위를 해야 한다.

- 이를 '스케일의 조정' 이라 한다

 

  • 훈련 데이터를 준비하고 스케일 비교

- 유방암 데이터와 4에서 만든 단일층 신경망 모델을 이용해 스케일을 비교

import matplotlib.pyplot as plt
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split

cancer = load_breast_cancer()

x = cancer.data
y = cancer.target

x_train, x_test, y_train, y_test = train_test_split(x, y, stratify=y, test_size=0.2, random_state=42)

print(cancer.feature_names[[2,3]])
plt.boxplot(x_train[:, 2:4])
plt.xlabel('feature')
plt.ylabel('value')
plt.show()

- mean perimeter는 주로 100~200 사이에 값이 있으며 

- mean area는 주로 200~2000 사이에 값들이 집중되어 있음을 알 수 있다.

 

  • 가중치를 기록할 변수와 학습률 파라미터 추가 하기

- SingleLayer 클래스에 인스턴스 변수를 추가하여 에포크마다 가중치의 값을 저장하여 가중치의 변화를 관찰

- init() 메서드에서 인스턴스 변수 w_history를 만들고 학습률 파라미터 learning_rate추가

def __init__(self, leaning_rate=0.1) :
    self.w = None
    self.b = None
    self.losses = []
    self.w_history = []
    self.lr = learning_rate

- learning_rate는 하이퍼 파라미터이며 이 값으로 가중치의 업데이트 양을 조절

- 손실 함수는 복잡한 굴곡을 가진 다차원 공간의 초평면(hyperplane)이다

- 가중치가 큰 폭으로 업데이트될 경우 손실 함수의 최소 지점을 지나칠 경우 최적의 해를 구할 수 없다

- 전역 최솟값을 놓치지 않도록 가중치의 업데이트 양을 조절할 필요가 있다

- 그림과 같이 높은 학습률을 적용할 경우 전역 최솟값을 지나치는 것을 알 수 있다.

- 적절한 학습률을 적용하여 천천히 전역 최솟값을 찾는다

 

  • 가중치 기록 후 업데이트 양 조절

- fit() 메서드에서 가중치가 바뀔 때마다 w_history 리스트에 가중치를 기록

- 넘 파이 배열을 리스트에 추가하면 실제 값이 복사되는 것이 아니라 배열을 참조

- 가중치 변수 self.w의 값이 바뀔 때마다 그 값을 복사하여 w_history 리스트에 추가

- w_grad에 학습률 self.lr을 곱하는 연산 추가

 

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 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 activation(self, z) :
        a = 1 / (1+np.exp(-z))      # 시그모이드 계산
        return a

    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)

 

  • 모델 훈련, 평가
layer1 = SingleLayer()
layer1.fit(x_train, y_train)
layer1.score(x_val, y_val)

- 약 91%의 정확도

 

- layer1 객체의 인스턴스 변수 w_history에는 100번의 에포크 동안 변경된 가중치가 모두 기록

- 세 번째, 네 번째 요소 (w [2], w [3])는 각각 mean perimeter, mean area특성에 대한 가중치 

w2 = []
w3 = []
for w in layer1.w_history :
    w2.append(w[2])
    w3.append(w[3])

plt.plot(w2, w3)
plt.plot(w2[-1], w3[-1], 'ro')
plt.xlabel('w[2]')
plt.ylabel('w[3]')
plt.show()

- mean perimeter에 비해 mean area의 스케일이 크므로 w3 값이 학습 과정에서 큰 폭으로 흔들며 변화

- w2의 값이 0부터 시작하여 조금씩 최적 값에 가까워짐

- w3에 대한 그레이더 언트가 크기 때문에 w3 축을 따라 가중치가 크게 요동치는 중

- 빨간 점은 최종 가중치를 의미하고 w3가 최종 가중치에 도달하는 동안 위아래로 크게 요동친다.

- 모델이 불안정 함을 알고 스케일을 조정한다.

 


스케일 조정을 통한 모델 훈련

- 신경망에서 자주 사용하는 스케일 조정 방법 : 표준화(standardization)

- 표준화는 특성 값에서 평균을 빼고 표준 편차로 나누면 완성

- 표준화 시 평균이 0이고 분산이 1인 특성이 만들어진다.

표준화 공식

  • 넘 파이로 표준화 구현

- 넘 파이의 mean(), std() 함수를 통해 평균과 표준 편차 계산 시 표준화를 쉽게 할 수 있다.

- mean(), std()의 axis 매개변수를 0으로 지정 시 2차원 배열의 열을 기준으로 통계치를 계산 하나의 행 백터로 변환

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

 

  • 모델 훈련
layer2 = SingleLayer()
layer2.fit(x_train_scaled, y_train)
w2 = []
w3 = []
for w in layer2.w_history :
    w2.append(w[2])
    w3.append(w[3])
plt.plot(w2, w3)
plt.plot(w2[-1], w3[-1], 'ro')
plt.xlabel('w[2]')
plt.ylabel('w[3]')
plt.show()

  • 표준화 특징
    • w2와 w3의 변화율이 비슷하기에 대각선 방향으로 가중치의 이동
    • 특성의 스케일이 비슷함으로 최적 값에 빠르게 근접

 

  • 모델 성능 평가
layer2.score(x_val, y_val)

- 성능이 떨어지는 이유는 훈련 세트와 검증 세트의 스케일이 비슷하지 않기 때문

- 튜닝이 완료된 훈련 세트와 다르게 검증 세트는 스케일의 표준화가 이루어지지 않았기 때문이다.

val_mean = np.mean(x_val, axis=0)
val_std = np.std(x_val, axis=0)
x_val_scaled = (x_val - val_mean) / val_std
layer2.score(x_val_scaled, y_val)

- 정확도가 확 올라와 필자의 모델에서는 100%의 정확도를 보유한 것을 알 수 있다.

 


스케일 조정 후 함정에 대하여

- 스케일 조정 후 나타날 수 있는 함정은 훈련 세트와 검증 세트가 다른 비율로 스케일이 조정된 경우

 

  • 원본 훈련 세트와 검증 세트로 산점도 그리기

- 파란 점은 훈련 세트, 빨간 점은 검증 세트

plt.plot(x_train[:50, 0], x_train[:50, 1], 'bo')
plt.plot(x_val[:50, 0], x_val[:50, 1], 'ro')
plt.xlabel('fature1')
plt.ylabel('fature2')
plt.legend(['train set', 'val set'])
plt.show()

그림 1

  • 이어서 전 처리한 훈련 세트와 검증 세트로 산점도 그리기
plt.plot(x_train_scaled[:50, 0], x_train_scaled[:50,1], 'bo')
plt.plot(x_val_scaled[:50, 0], x_val_scaled[:50, 1], 'ro')
plt.xlabel('fature1')
plt.ylabel('fature2')
plt.legend(['train set', 'val set'])
plt.show()

그림 2

- 너무나도 비슷해 보이는 환경으로 구성되어있으나 실제 산점도를 보면 비율의 변화가 존재한다.

- 이유는 훈련 세트와 검증 세트를 각각 다른 비율로 전 처리하였기 때문

 

  • 올바르게 검증 세트 전처리 하기

- 검증 세트의 스케일이 훈련 세트의 스케일과 다른비율로 조정되면 모델에 적용된 알고리즘이 검증 세트의 샘플데이터를 잘못 인식

- 훈련세트의 편균, 표준 편차를 사용해 검증 세트를 변환

x_val_scaled = (x_val - train_mean) / train_std
plt.plot(x_train_scaled[:50, 0], x_train_scaled[:50, 1], 'bo')
plt.plot(x_val_scaled[:50, 0], x_val_scaled[:50, 1], 'ro')
plt.xlabel('fature1')
plt.ylabel('fature2')
plt.legend(['train set', 'val set'])
plt.show()

그림 3

- 위 세 가지 그림의 차이점이 보이는가

- 그림 1과 그림 3은 산점도의 분포나 모양이 동일

- 그러나 그림 2의 산점도는 1이나 3과는 미묘한 변화가 존재

 

 

728x90