728x90
반응형

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

  • 넘파이, 머신러닝, 딥러닝 패키지들은 다차원 배열의 계산을 빠르게 수행한다. - 행렬 연산 빠르게 수행
  • 이런 기능을 벡터화(vertorization)된 연산 - 벡터화 연산 사용시 알고리즘 성능을 높일 수 있다.

 

배치 경사 하강법으로 성능 올리기

  • 선형 회귀, 로지스틱 회귀는 알고리즘을 1번 반복할 때 1개의 샘플을 사용하는 '확률적 경사 하강법'을 사용했다.
  • 확률적 경사 하강법은 1번 업데이트시 1개의 샘플 사용하므로 손실 함수의 전역 최솟값을 불안정하게 찾는다.
  • 배치 경사 하강법은 가중치를 1번 업데이트할 때 전체 샘플을 사용하므로 손실 함수의 전역 최솟값을 안정적으로 찾는다. 
    • 단점 : 1번 업데이트시 사용되는 데이터의 개수가 많으므로 계산 비용이 많이든다. 
    • 전체 데이터 세트의 크기가 너무 크면 배치 경사 하강법을 사용하지 못하는 경우도 있다.

 

점 곱

단일층 신경망

[그림1] 점 곱

 

forpass() 메서드

z = np.sum(x * self.w) + self.b
  • 넘파이의 원소별 곱셈 기능 덕에 입력과 가중치의 곱을 x * self.w로 간단하게 표현 가능
x = [x1,x2,x3,x4....xn]
W = [W1,W2,W3,W4....Wn]
x * W = [x1*W1, x2*W2.....xn*Wn]
  • x(x1,x2...)와 W(W1,W2..)는 벡터라고 부르고 벡터는 볼드로 표기한다.
  • 두 벡터를 곱하여 합을 구하는 계산(np.sum(x*self.w))를 점 곱(dot product) 또는 스칼라 곱(scalar product)이라고 한다.

 

점 곱 행렬 곱셈으로 표현하기

 

[그림2] 행렬 곱셈 점 곱

z = np.dot(x, self.w) + self.b

 

 

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

 

1. 사용할 넘파이, 맷플롯립 임포트

# 1. 넘파이 맷플롯립 임포트
import numpy as np
import matplotlib.pyplot as plt

 

 

2. 위스콘신 유방암 데이터 세트를 훈련, 검증, 테스트 세트로 나누고 데이터 살펴보기

# 2. 위스콘신 유방암 데이터 세트를 훈련, 검증 , 테스트 세트로 나누고 데이터 살펴보기
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)

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)


 

3. 훈련 세트와 검증 세트 확인

# 3. cancer 데이터 세트 특성 개수 30개  
# shape으로 크기 확인

print(x_train.shape, x_val.shape)

 

 

4. 정방향 계산을 행렬 곱셈으로 표현하기

 

[그림3] 정방향 계산 행렬 곱셈

 

[그림4] 정방향 계산 행렬 곱셈

 

 

5. 그레이디언트 계산 이해

  • 그레이디언트는 오차와 입력 데이터의 곱이다.
  • XT제곱은 X를 전치한 것이고 E는 오차들을 모은 것이다.
  • X의 크기는 (364,30)이므로 전치하면 (30, 364)크기의 행렬이 된다.
  • 행렬의 계산은 (30, 364) * (364, 1) = (30, 1)와 같다. 
  • g1은 모든 샘플의 첫 번째 특성(x1(1), x1(2)...)과 오차(e(1),e(2)....e(364)를 곱해 더한 값이므로 그레이디언트 평균값을 계산할 때 이 값을 다시 전체 샘플 수로 나눈다.

[그림5] 그레이디언트 계산

 

 

6. forpass(), backprop() 메서드에 배치 경사 하강법 적용하기

  • 행렬 곱셈을 적용한 결과가 그레이디언트의 합이다.
  • 전체 샘플 개수로 나눠 평균 그레이디언트를 구한다.
  • len() 함수는 넘파이 배열의 행 크기를 반환한다.
# 6. forpass(), backprop() 메서드에 배치 경사 하강법 적용하기

def forpass(self, x):
    z = np.dot(x, self.w) + self.b # 선형 출력 계산
    return z

def backprop(slef, x, err):
    m = len(x)
    w_grad = np.dot(x.T, err) / m # 가중치에 대한 평균 그레이디언트를 계산한다.
    b_grad = np.sum(err) / m # 절편에 대한 평균 그레이디언트를 계산한다.
    
    return w_grad, b_grad

# 파이썬의 len() 함수는 넘파이 배열의 행 크기를 반환하므로 이 값을 이용해 그레이디언트의 평균을 계산한다.
# 절편의 그레이디언트는 오차이므로 오차 행렬의 평균값을 구한다.

 

 

7. fit() 메서드 수정하기

  • 배치 경사 하강법에서는 forpass()메서드와 backprop()메서드에서 전체 샘플을 한꺼번에 계산하므로 두 번째 for문이 삭제된다.
# 7. fit() 메서드 수정하기
# 배치 경사 하강법에서는 forpass() 메서드와 backprop() 메서드에서 전체 샘플을 한꺼번에 계산하므로 두 번째 for문이 삭제된다.

def fit(self, x, y, eprochs=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()) # 가중치를 기록한다.
    
    for i in range(epochs):
        z = self.forpass(x) # 정방향 계산을 수행한다.
        a = slef. 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) # 검증 세트에 대한 손실 계산
        
        
# 전체 구조는 확률적 경사 하강법과 비슷하지만 for문이 한 단계 삭제되어 코드가 훨씬 간단해짐
# 활성화 출력 a가 열 벡터이므로 이에 맞추어 타깃값을(m,1)크기의 열 벡터로 변환하고 평균 손실 구하기 위해 np.sum()함수로 각 샘플의 손실을 더한 후 전체 샘플의 개수로 나눈다.



        

 

8,. 나머지 메서드 수정

# 8. 나머지 메서드 수정
#predict() 메서드에서 사용했던 리스트 내포와 update_val, loss() 메서드 간단하게 변환

def predict(self, x):
    z = self.forpass(x) # 정방향 계산을 수행한다.
    return z >0  # 스텝 함수 적용

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

 

 

#  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 손실 하이퍼파라미터

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

 

 

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

  • transformer() 메서드로 데이터를 표준화 전처리
# 9. 훈련 데이터 표준화 전처리
# 데이터 전처리 관련 클래스들을 sklearn.preprocessing 모듈 아래에 존재한다. 
# 이런 클래스들을 변환기(transformer) 라고 부른다.
import numpy as np
import matplotlib.pyplot as plt
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)

# fir() 메서드를 통해 변환 규칙 익히기
# transform() 메서드로 데이터를 표준화 전처리를 하고 
# 훈련 세트와 검증 세트에 표준화 적용해 x_train_scaled, x_val_scaled준비

 

 

10. SingleLayer 클래스 객체에 전달해 배치 경사 하강법 적용

# 10. SingleLayer 클래스 객체에 전달해 배치 경사 하강법 적용

single_layer = SingleLayer(l2=0.01)
single_layer.fit(x_train_scaled, y_train, x_val=x_val_scaled, y_val=y_val,
                 epochs=10000)

single_layer.score(x_val_scaled, y_val)

# 에포크를 늘린 이뉴는 확률적 경사 하강법과 배치 경사 하강법은 에포크마다 
# 가중치 업데이트를 하는 횟수에 차이가 있기 때문이다.
# 훈련 세트 샘플이 364개 일 경우 확률적 경사 하강법은 100번의 에포크를 수행하면 36,400번의 가중치 업데이트가 일어난다.
# 반면 배치 경사 하강법은 전체 훈련 세트를 한 번에 계산한 다음 오차를 역전파 하기 때문에 100번의 에포크를 수행하면 가중치는 100번만 업데이트 된다.
# 따라서 확률적 경사 하강법은 에포크횟수를 크게 늘려주어야 한다.

[그림6] 정확도

 

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

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

# 훈련 손실과 검증 손실 그래프로 출력해 확률적 경사 하강법과 비교

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

# 배치 경사 하강법은 전체 샘플을 사용해 가중치를 업데이트하기 때문에 손실값이 안정적으로 감소한다.

[그림7] 검증 세트 성능 측정 그래프

12. 가중치 변화 그래프로 보기

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

[그림8] 가중치 변화 그래프

728x90
반응형
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기