HyeM

[Do it-Ch06] 신경망 알고리즘 벡터화 본문

Study/AI&DeepLearning

[Do it-Ch06] 신경망 알고리즘 벡터화

Hailey_HyeM207 2021. 6. 25. 17:33

06_1 신경망 알고리즘을 백터화하여 한 번에 전체 샘플을 사용합니다

사이킷런의 예제 데이터 세트는 2차원 배열로 저장되어 있다.
2차원 배열은 행을 햄플, 열을 특성으로 생각하면 행렬로 이해할 수 있다. 이번에는 행렬 개념을 신경망 알고리즘에 도입해 본다.

 

백터화 된 연산은 알고리즘의 성능을 올린다.

행렬 연산을 백터화 연산이라고 하는데, 백터화 된 연산은 알고리즘의 성능을 높일 수 있다.
배치 경사 하강법을 SingleLayer 클래스에 적용하면 백터화된 연산을 사용할 수 있다.

이전까지의 실습에서는 경사 하강법 알고리즘들이 알고지름을 1번 반복할 때 1개의 샘플을 사용하는 '확률적 경사 하강법'을 이용했다. 이는 손실 함수의 전역 최솟값을 불안정하게 찾는다.


하지만 배치 경사 하강법은 가중치를 1번 업데이트할 때 전체 샘플을 사용하므로 손실 함수의 전역 최솟깞을 안정적으로 찾는다. 단 배치 경사 하강법은 가중치를 1번 업데이트 할때 사용되는 데이터의 개수가 많으므로 알고리즘 1번 수행 당 계산 비용이 많이듦으로 주의해야된다.

 

SingleLayer 클래스에 배치 경사 하강법 적용하기

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

넘파이와 맷플롯립 import하기

 

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)
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)

위스콘신 유방암 데이터 세트를 cancer 변수에 대입하고 데이터 세트를 훈련, 검증, 테스트 세트로 나눈다.

 

훈련세트와 검증 세트의 크기는 다음과 같고, cancer 데이터 세트의 특성 개수는 30개이다.

 

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               # L1 손실 하이퍼파라미터
        self.l2 = l2               # L2 손실 하이퍼파라미터

    '''
    forpass()와 backdrop()에 배치경사하강법 적용하기 
    forpass () : np.sum() 대신에 행렬 곱셈해주는 np.dot() 적용
    backdrop () : 행렬 곱셈을 적ㄷ용한 결과가 그레이디언트들의 합일므로 전체 샘플 개수로 나눠 평균 그레이디언트를 구한다.
    '''
    def forpass(self, x):
        z = np.dot(x, self.w) + self.b        # 선형 출력 계산
        return z

    def backprop(self, x, err):
        m = len(x)
        w_grad = np.dot(x.T, err) / m         # 가중치에 대한 경균 그래디언트 계산
        b_grad = np.sum(err) / m              # 절편에 대한 평균 그래디언트 계산
        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
        
        
    '''
    배치 경사 하강법에서는 forpass() 메서드와 backdrop()메서드에서 전체 샘플을 한꺼번에 계산하므로 두번째 for문이 삭제된다.
    활성화 출력 a가 열벡터이므로 이에 맞추어 타깃값을 (m,1) 크기의 열 벡터로 변화하였고 평균 손실을 구하기 위해 np.sum() 함수로 각 샘플의 손실을 더한 후 전체 샘플의 개수로 나눈다.
    '''
    def fit(self, x, y, epochs=100, x_val=None, y_val=None):
        y = y.reshape(-1, 1)                  # 타깃을 열 벡터로 바꿈
        y_val = y_val.reshape(-1, 1)
        m = len(x)                            # 샘플 개수저장 
        self.w = np.ones((x.shape[1], 1))     # 가중치 초기화합
        self.b = 0                            # 절편 초기화
        self.w_history.append(self.w.copy())  # 가중치 기록
        # epochs만큼 반복합니다.
        for i in range(epochs):
            z = self.forpass(x)               # 정방향 계산 수행
            a = self.activation(z)            # 활성화 함수 적용
            err = -(y - a)                    # 오차 계산
            w_grad, b_grad = self.backprop(x, err) # 오차를 역전파하여 그래디언트 계산
            w_grad += (self.l1 * np.sign(self.w) + self.l2 * self.w) / m # 그래디언트에 페널티 항의 미분 값을 더하기
            # 가중치와 절편을 업데이트합니다.
            self.w -= self.lr * w_grad
            self.b -= self.lr * b_grad
            self.w_history.append(self.w.copy()) # 가중치 기록
            a = np.clip(a, 1e-10, 1-1e-10) # 안전한 로그 계산을 위해 클리핑하기
            # 로그 손실과 규제 손실을 더하여 리스트에 추가
            loss = np.sum(-(y*np.log(a) + (1-y)*np.log(1-a)))
            self.losses.append((loss + self.reg_loss()) / m)
            # 검증 세트에 대한 손실 계산
            self.update_val_loss(x_val, y_val)
    
    def predict(self, x):
        z = self.forpass(x)      # 정방향 계산 수행 
        return z > 0             # 스텝 함수 적용
    
    def score(self, x, y):
        # 예측과 타깃 열 벡터를 비교하여 True의 비율반환 
        return np.mean(self.predict(x) == y.reshape(-1, 1))
    
    def reg_loss(self):
        # 가중치에 규제를 적용합니다.
        return self.l1 * np.sum(np.abs(self.w)) + self.l2 / 2 * np.sum(self.w**2)
    
    
    '''
    검증 손실 val_loss()를 계산할 때, np.sum() 함수를 적용함 
    '''
    def update_val_loss(self, x_val, y_val):
        z = self.forpass(x_val)            # 정방향 계산 
        a = self.activation(z)             # 활성화 함수 적용 
        a = np.clip(a, 1e-10, 1-1e-10)     # 출력 값을 클리핑
        # 로그 손실과 규제 손실을 더하여 리스트에 추가
        val_loss = np.sum(-(y_val*np.log(a) + (1-y_val)*np.log(1-a)))
        self.val_losses.append((val_loss + self.reg_loss()) / len(y_val))

 

훈련 데이터 표준화 전처리하기

안정적인 학습을 위하여 5장에서 보았던 사이킷런의 StandardScaler 클래스를 사용한다. 데이터 세트의 특성을 평균 0, 표준편차가 1이 되도록 변환한다.

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
scaler.fit(x_train)
x_train_scaled = scaler.transform(x_train)
x_val_scaled = scaler.transform(x_val)

StandardScaler 클래스로 scaler 객체를 만들고 fit()으로 변환 규칙을 익히고 transform()으로 데이터를 표준화 전처리한다. 그런다음 훈련세트와 검증세트에 표준화를 적용하여 x_train_scaled, x_val_scaled를 준비한다.

 

 

위의 데이터를 SingleLayer 클래스 객체에 전달하여 배치 경사 하강법을 적용한다.

  • l2 규제 매개변수 값을 0.01지정
  • 에포크 매개변수의 기본값을 100에서 10000으로 크게 늘림

 

검증 세트로 성능 측정하고 그래프로 비교하기

plt.ylim(0, 0.3)
plt.plot(single_layer.losses)
plt.plot(single_layer.val_losses)
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train_loss', 'val_loss'])
plt.show()

5장의 확률적 경사 하강법의 손실 그래프가 변동이 심했던 것에 비해, 배치 경사 하강법은 손실값이 안정적으로 감소한다.

 

w2 = []
w3 = []
for w in single_layer.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()

  • 배치 경사하강법 적용시, 가중치를 찾는 경로가 다소 부드러운 곡선의 형태를 띈다.
  • 가중치의 변화가 연속적이므로 손실 값도 안정적으로 수렴된다.
Comments