728x90
반응형

하나의 층에 여러 개의 뉴런 사용

  • 하나의 층에 여러 개의 뉴런을 사용하면 신경망은 입력층에서 전달되는 특성이 각 뉴런에 모두 전달될 것이다.
  • 입력값이 나열된 부분을 입력층이라고 부른다.

[그림1] 하나의 층에 여러 개의 뉴런
[그림2] 행렬 곱셈 변경

 

  • 출력은 (1, 2) 크기의 행렬이다. 
  • 여러 개의 뉴런을 사용함으로써 가중치가 1개의 열을 가진 벡터가 아닌 2차원 행렬이 되었다는 점이다.
  첫 번째 특성에 대한 두 번째 특성에 대한  세 번째 특성에 대한
첫 번째 뉴런 가중치  W11 W21 W31
두 번째 뉴런 가중치  W21 W22 W32
  • 가중치 행렬의 크기는(입력의 개수, 출력의 개수)로 생각하면 된다.
  • 2개의 뉴런을 사용하고 있으므로 출력의 개수는 2개다.
  • 하나의 뉴런만 사용한 경우 출력의 개수가 1이므로 가중치는 열 벡터가 된다.

 

샘플 전체에 대한 수식

[그림3] 행렬 전체에 대한 수식

 

 

출력을 하나로 모으기

  • 위스콘신 유방암 데이터 세트로 우리가 해결한 문제는 악성 종양인지 정상 종양인지 구분하는 경우 이진 분류 문제이므로 각 뉴런에서 출력된 값(z1,z2...zn)을 하나의 뉴런으로 다시 모아야 한다.
  • 유방암 데이터 1개의 샘플에 있는 여러 특성의 값을 각 뉴런에 통과시키면 여러 개의 출력값(a1,a2...)이 나오는데, 이 값들 중 하나만 골라 이진 분류에 사용할 수는 없다. 그래서 출력값을 다시 모아 이진 분류 수행할 기준값(z)를 만드는 것이다.

[그림4] z

 

[그림5] 단일층 신경 선형 방정식과 행렬 곱셈 표현

 

샘플 전체에 대한 행렬 곱셈 표현

[그림6] 샘플 전체

행렬 A1의 크기는(전체 샘플 수, 2)이므로 (m, 2)이고 W2의 크기는 (입력의 크기, 출력의 크기) (2,1)이다.

 

 

은닉층이 추가된 신경망 

 

구조

[그림7] 은닉층이 추가된 신경망

 

 

입력과 출력은 행렬로 표기한다.

 

[그림8] 입력과 출력은 행렬로 표기

 

다층 신경망의 개념 정리

  • 일반적으로 다층 신경망의 각 층은 2개 이상의 뉴런으로 구성한다.
  • 신경망의 은닉층의 뉴런 개수를 2개가 아닌 m으로 늘려 생각해보면 활성화 함수는 뉴런 오른쪽에 작은 원으로 붙여서 표현한다.
  • n개의 입력이 m개의 뉴런으로 입력된다.

 

[그림9] 다층 신경망의 개념

 

활성화 함수는 층마다 다를 수 있지만 같은 층에서는 같은 값이어야 한다.

  • 은닉층과 출력층에 있는 모든 뉴런에는 활성화 함수가 필요하며 문제에 맞는 활성화 함수를 사용해야 한다.
  • 단, 같은 층에 있는 뉴런은 모두 같은 활성화 함수를 사용해야 한다.
  • 예) 이진 분류 문제에는 출력층의 활성화 함수로 시그모이드 함수를 사용해야 한다.

 

모든 뉴런이 연결되어 있으면 완전 연결 신경망이라고 한다.

  • 입력층, 은닉층, 은닉층과 출력층 사이의 모든 뉴런들이 연결되어 있으면 완전 연결(fully-connected)신경망이라고 부른다. 예) 그림9
  • 완전 연결 신경망은 인공신경망의 한 종류이며, 가장 기본적인 신경망 구조다. 이렇게 뉴런이 모두 연결되어 있는 층을 완전 연결층이라고 한다.
  • 다른 신경망의 종류
    • 데이터 흐름이 순환되는 순환 신경망
    • 정방향으로 데이터가 흐르지만 완전히 연결되어 있지 않은 합성곱 신경망
  • 완전 연결 신경망은 다층 퍼셉트론이라고도 부른다. 뉴런들이 빠찜 없이 연결되어 있어 밀집 연결(densely-connected)신경망 또는 층과 층 사이의 흐름이 한쪽 방향으로만 진행되는 특징이 있어 피드 포워드(feed forward)신경망이라고 부르기도 한다.

 

다층 신경망에 경사 하강법 적용

 

[그림10] 다층 신경망에 경사 하강법 적용

 

  • 입력 데이터 X와 가중치 W1을 곱하고 절편 b1을 더해 Z1이 된다. 
  • Z1은 호라성화 함수(시그모이드 함수)를 통과해 A1이 된다. (여기까지가 첫 번째 은닉층)
  • 첫 번째 은닉층의 활성화 출력 A1과 출력층의 가중치 W2를 곱하고 절편 b2를 더해 Z2를 만든다.
  • 그 다음 Z2는 다시 활성화 함수(시그모이드 함수)를 통과해 A2가 된다. (여기까지 출력층)
  • A2의 값을 보고 0.5보다 크면 양성, 그렇지 않으면 음성으로 예측한다.
  • 여기서 경사 하강법을 적용하려면 W2와 b2그리고 W1, b1에 대한 손실 함수 L의 도함수를 구해야 한다.

 

가중치에 대하여 손실 함수 미분(출력층)

  • W2에 대한 손실 함수의 미분 연쇄 법칙 

[그림11] 가중치에 대하여 손실 함수 미분

 

신경망에 적용시

[그림12] 신경망 적용

 

 

[그림13] 행렬 확장
[그림14] -(y-a)
[그림15] A1

 

도함수 곱하기(출력층)

  • -(Y-A2)와 A1은 그냥 곱하면 안된다. 행렬의 크기와 순서에 주의하며 곱해야 한다.
  • -(Y-A2)의 행렬 크기는 Y는 (m, 1)이고 A2도 (m,1)크기이므로 (m,1)이다.

 

A1

[그림16] A1 활성화 출력

 

  • A1의 크기는 (m,2)이고 -(Y-A2)의 크기는 (m,1)이므로 A1을 전치해 -(Y-A2)와 곱해야 한다. W2와 같은(2,1)크기의 그레이디언트 행렬을 얻을 수 있다.

[그림17] 그레이디언트 정렬

 

 

절편에 대하여 손실 함수 미분(출력층)

  • 신경망에 절편에 대한 손실 함수의 미분 표시

[그림18] 신경망에 절편에 대한 손실 함수의 미분 표시

 

  • Z2를 절편에 대하여 미분하면 1이다. 모든 원소가 1인 벡터 강조하기 위해 1을 볼드로 표기
  • 여기서 구한 값 역시 모든 샘플에 대한 그레이디언트의 합이다.

 

가중치에 대하여 손실 함수 미분(은닉층)

  • W1에 대하여 손실 함수 미분
  • 연쇄 법칙으로 나타내기

[그림19] 손실 함수의 미분을 연쇄 법칙으로 나타내기

 

[그림20]

 

 

도함수를 곱하기(은닉층)

 

[그림21] 원소별 곱셈

 

 

절편에 대하여 손실 함수를 미분하고 도함수를 곱하기

[그림22] 절편에 대하여 손실 함수 미분하고 도함수 곱하기

 

 

2개의 층을 가진 신경망 구현하기

  • SingleLayer 클래스의 은닉층 부분의 계산을 제외하면 대부분 비슷한 기능을 가지고 있으므로 SingleLayer 클래스를 상송해 DualLayer 클래스를 만들고 필요한 메서드만 재정의

 

1. SingleLayer 클래스를 상속한 DualLayer 클래스 만들기

  • 은닉층의 뉴런 개수를 지정하는 units 매개변수 추가
# 1. SingleLayer 클래스를 상속한 DualLayer 클래스 만들기

class DualLayer(SingleLayer):
    def __init__(self, units=10, learning_rate=0.01, l1=0, l2=0):
        self.units = units # 은닉층 뉴런 개수
        self.w1 = None # 은닉층의 가중치
        self.b1 = None # 은닉층의 절편
        self.w2 = None # 출력층의 가중치
        self.b2 = None # 출력층의 절편
        self.a1 = None # 은닉층의 활성화 출력
        self.losses = [] # 훈련 손실
        self.val_losses = [] # 검증 손실
        self.lr = learning_rate # 학습률
        self.l1 = l1 # L1 손실 하이퍼파라미터
        self.l2 = l2 # L2 손실 하이퍼파라미터
        
# SingleLayer 클래스는 학습 과정을 이해하기 쉽도록 w_history변수에 가중치 변화를 기록했지만
# DualLayer 클래스에서는 가중치 변화를 기록하지 않는다.

 

2. forpass() 메서드 수정

  • forpass() 메서드에서는 은닉층과 출력층의 정방향 계산 수행
# 2. forpass() 메서드 수정
# forpass() 메서드에서는 은닉층과 출력층의 정방향 계산 수행

def forpass(self, w):
    z1 = np.dot(x, self.w1) + self.b1 # 첫 번째 층의 선형식을 계산
    self.a1 = self.activation(z1) # 활성화 함수 적용
    z2 = np.dot(self.a1, self.w2) + self.b2 # 두 번째 층의 선형식 계산
    return z2

 

3. backprop() 메서드 수정

# 3. backprop() 메서드 수정
# 그레이디언트를 계산하는 backprop 
# 모델 훈련에서 오차를 역전파하는 역할을 하므로 주의해서 구현

def backprop(self, x, err):
    m = len(x) # 샘플 개수
    # 출력층의 가중치와 절편에 대한 그레이디언트를 계산한다.
    w2_grad = np.dot(self.a1.T, err) / m
    b2_grad = np.sum(err) / m
    # 시그모이드 함수까지 그레이디언트를 계산한다.
    err_to_hidden = np.dot(err, self.w2.T) * self.a1 * (1-self.a1)
    # 은닉층의 가중치와 절편에 대한 그레이디언트를 계산
    w1_grad = np.dot(x.T, err_to_hidden) / m
    b1_grad = np.sum(err_to_hidden, axis=0) /m
    return w1_grad, b1_grad, w2_grad, b2_grad

    

 

[그림23] backprop() 식

 

 5~6 fit() 메서드 수정

  • fit() 메서드는 모델 훈련 담당
  • 은닉층과 출력층의 가중치, 절편 초기화 에포크마다 정방향 계산 수행해 오차 계산
  • 오차를 역전파해 가중치와 절편의 그레이디언트를 계산
  • 손실 계산 누적
  • 3개의 작은 메서드로 쪼갬
# 4~5. fit() 메서드 수정
# 모델 훈련 담당 
# 은닉층과 출력층의 가중치, 절편 초기화 에포크마다 정방향 계산 수행해 오차 계산
# 오차를 역전파해 가중치와 절편의 그레이디언트를 계산
# 손실 계산 누적
# 3개의 작은 메서드로 쪼갬


def init_weights(self, n_features):
    self.w1 = np.ones((n_features, self.units)) # (특성 개수, 은닉층의 크기)
    self.b1 = np.zeros(self.units) # 은닉층의 크기
    self.w2 = np.ones((self.units, 1))# 은닉층의 크기, 1
    self.b2 = 0
    

 

6. fit() 메서드의 for문 안에 있는 코드 일부 training() 메서드로 분리

# 6. fit() 메서드의 for문 안에 있는 코드 일부 traning() 메서드로 분리

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.init_weights(x.shape[1])  # 은닉층과 출력층의 가중치 초기화
    # epochs 만큼 반복
    for i in range(epochs):
        a = self.training(x, y, m)
        # 안전한 계산 위해 클리핑
        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 training(self, x, y, m):
    z = self.forpass(x)  # 정방향 계산 수행
    a = self.activation(z) # 활성화 함수 적용
    err = -(y-a) # 오차 계산
    # 오차 역전파하여 그레이디언트 계산
    w1_grad, b1_grad, w2_grad, b2_grad = self.backprop(x, err)
    # 그레이디언트에서 페널티 항의 미분값 빼기
    w1_grad += (self.l1 * np.sign(self.w1) + self.l2 *self.w1) /m
    w2_grad += (self.l1 * np.sign(self.w2) + self.l2 *self.w2) /m
    # 은닉층의 가중치와 절편 업데이트
    self.w1 -= self.lr * w1_grad
    self.b1 -= self.lr * b1_grad
    
    # 출력층의 가중치와 절편 업데이트
    self.w2 -= self.lr * w2_grad
    self.b2 -= self.lr * b2_grad
    return a

# 정방향 계산과 그레이디언트를 업데이트하는 코드를 training() 메서드로 옮겼다.
# 이 메서드는 훈련 데이터 x,y와 훈련 샘플의 개수 m을 매개변수로 받고 마지막 출력층의 활성화 출력 a를 반환한다.

 

7. reg_loss() 메서드 수정

  • reg_loss() 메서드는 은닉층과 출력층의 가중치에 대한 L1, L2 손실을 계산한다.
# 7. reg_loss() 메서드 수정하기
# reg_loss() 메서드는 은닉층과 출력층의 가중치에 대한 L1,L2 손실을 계산한다.

def reg_loss(self):
    # 은닉층과 출력층의 가중치에 규제를 적용
    return self.l1 * (np.sum(np.abs(self.w1) + np.sum(np.abs(self.w2)))) + \
                self.l2 / 2 * (np.sim(self.w1**2) + np.sum(self.w2**2))

 

 

모델 훈련하기

class DualLayer(SingleLayer):
    
    def __init__(self, units=10, learning_rate=0.1, l1=0, l2=0):
        self.units = units         # 은닉층의 뉴런 개수
        self.w1 = None             # 은닉층의 가중치
        self.b1 = None             # 은닉층의 절편
        self.w2 = None             # 출력층의 가중치
        self.b2 = None             # 출력층의 절편
        self.a1 = None             # 은닉층의 활성화 출력
        self.losses = []           # 훈련 손실
        self.val_losses = []       # 검증 손실
        self.lr = learning_rate    # 학습률
        self.l1 = l1               # L1 손실 하이퍼파라미터
        self.l2 = l2               # L2 손실 하이퍼파라미터

    def forpass(self, x):
        z1 = np.dot(x, self.w1) + self.b1        # 첫 번째 층의 선형 식을 계산합니다
        self.a1 = self.activation(z1)            # 활성화 함수를 적용합니다
        z2 = np.dot(self.a1, self.w2) + self.b2  # 두 번째 층의 선형 식을 계산합니다.
        return z2

    def backprop(self, x, err):
        m = len(x)       # 샘플 개수
        # 출력층의 가중치와 절편에 대한 그래디언트를 계산합니다.
        w2_grad = np.dot(self.a1.T, err) / m
        b2_grad = np.sum(err) / m
        # 시그모이드 함수까지 그래디언트를 계산합니다.
        err_to_hidden = np.dot(err, self.w2.T) * self.a1 * (1 - self.a1)
        # 은닉층의 가중치와 절편에 대한 그래디언트를 계산합니다.
        w1_grad = np.dot(x.T, err_to_hidden) / m
        b1_grad = np.sum(err_to_hidden, axis=0) / m
        return w1_grad, b1_grad, w2_grad, b2_grad

    def init_weights(self, n_features):
        self.w1 = np.ones((n_features, self.units))  # (특성 개수, 은닉층의 크기)
        self.b1 = np.zeros(self.units)               # 은닉층의 크기
        self.w2 = np.ones((self.units, 1))           # (은닉층의 크기, 1)
        self.b2 = 0
        
    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.init_weights(x.shape[1]) # 은닉층과 출력층의 가중치를 초기화합니다.
        # epochs만큼 반복합니다.
        for i in range(epochs):
            a = self.training(x, y, m)
            # 안전한 로그 계산을 위해 클리핑합니다.
            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 training(self, x, y, m):
        z = self.forpass(x)       # 정방향 계산을 수행합니다.
        a = self.activation(z)    # 활성화 함수를 적용합니다.
        err = -(y - a)            # 오차를 계산합니다.
        # 오차를 역전파하여 그래디언트를 계산합니다.
        w1_grad, b1_grad, w2_grad, b2_grad = self.backprop(x, err)
        # 그래디언트에서 페널티 항의 미분 값을 뺍니다
        w1_grad += (self.l1 * np.sign(self.w1) + self.l2 * self.w1) / m
        w2_grad += (self.l1 * np.sign(self.w2) + self.l2 * self.w2) / m
        # 은닉층의 가중치와 절편을 업데이트합니다.
        self.w1 -= self.lr * w1_grad
        self.b1 -= self.lr * b1_grad
        # 출력층의 가중치와 절편을 업데이트합니다.
        self.w2 -= self.lr * w2_grad
        self.b2 -= self.lr * b2_grad
        return a
    
    def reg_loss(self):
        # 은닉층과 출력층의 가중치에 규제를 적용합니다.
        return self.l1 * (np.sum(np.abs(self.w1)) + np.sum(np.abs(self.w2))) + \
               self.l2 / 2 * (np.sum(self.w1**2) + np.sum(self.w2**2))

 

1. 다층 신경망 모델 훈련하고 평가

#1. 다층 신경망 모델 훈련하고 평가

# L2 규제는 0.01만큼, 에포크는 20,000번 지정

dual_layer = DualLayer(l2 = 0.01)
dual_layer.fit(x_train_scaled, y_train, x_val = x_val_scaled, y_val=y_val, epochs=20000)
dual_layer.score(x_val_scaled, y_val)

[그림24] 다층 신경망 평가

 

2. 훈련 손실과 검증 손실 그래프 분석

# 2. 훈련 손실과 검증 손실 그래프 분석

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

# 손실 그래프가 이전보다 천천히 감소하는 이유는 SingleLayer보다 
# 가중치의 개수가 훨씬 많아져 학습하는데 시간이 오래 걸리기 때문
# 위스콘신 유방암 데이터의 특성이 30개이므로 SingleLayer 클래스를 사용했을 때는 가중치 30개와 절편 1개가 필요하다.

# 은닉층 뉴런이 10개였으므로 30 * 10개의 가중치와 10개의 절편이 필요하고 
# 출력층 역시 10개의 가중치와 1개의 절편이 필요하다. 
# DualLayer 모델은 총 321개의 가중치를 학습해야 한다.

[그림25] 훈련 손실과 검증 손실 그래프

 

가중치 초기화 개선

 

1. 가중치 초기화를 위한 init__weights() 메서드 수정하기

  • RandominitNetwork 클래스 만들고 가중치 초기화하는 init__weights() 메서드만 고치기
# 1. 가중치 초기화를 위한 init__weights() 메서드 수정하기
# RandomInitNetwork 클래스 만들고 가중치 초기화하는 init__weights() 메서드만 고치기



class RandomInitNetwork(DualLayer):
    
    def init_weights(self, n_features):
        np.random.seed(42)
        self.w1 = np.random.normal(0, 1, (n_features, self.units))  #특성 개수, 은닉층의 크기
        self.b1 = np.zeros(self.units) # 은닉층의 크기
        self.w2 = np.random.normal(0, 1, (self.units, 1)) # (은닉층의 크기, 1)
        self.b2 = 0
        

 

2. 객체 생성 후 모델 훈련 시키기

# 2. 객체 생성 후 모델 훈련 시키기
random_init_net = RandomInitNetwork(l2 =0.01)
random_init_net.fit(x_train_scaled, y_train, x_val=x_val_scaled, 
                    y_val=y_val, epochs=500)


plt.plot(random_init_net.losses)
plt.plot(random_init_net.val_losses)
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train_loss','val_loss'])
plt.show()
# 손실 함수가 감소하는 곡선이 매끄럽다,.

[그림26] 가중치 초기화 후 모델 훈련  그래프 

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