본문 바로가기
Deep Learning/밑바닥부터 시작하는 딥러닝

신경망

by 대소기 2021. 11. 23.

 

 

 

 

* 퍼셉트론을 통해 논리 연산을 하기 위해서는 사람이 직접 가중치, 편향을 설정해야 했다.

* 하지만, 신경망은 학습을 통해 자동으로 가중치를 갱신하기 때문에 인간이 가중치를 직접 설정할 필요가 없다.

 

 

3층 신경망 구현하기

* input layer, hidden layer1, hidden layer2, output layer(전달된 값을 forwarding만 해주는 층)로 이뤄진 3층 신경망 모형은 위와 같다.

* 오늘은 x1, x2의 입력이 신경망을 거쳐 output y1, y2으로 도출되는 과정을 살펴보고 python code로 구현해보겠다.

 

 

1) input layer to hidden layer1

* 위 $a_1^{(1)}$은  다음과 같은 수식으로 나타낼 수 있다.

$a_1^{(1)} = w_11^{(1)} x_1 + w_12^{(1)} x_2 + b_1^{(1)}$

 

* 위와 같이 표현한 1층의 가중치 부분은 아래와 같이 행렬의 곱을 통해 나타낼 수 있다.

$A_1^{(1)} = X W^{(1)} + B^{(1)}$

 

* 각 요소들은 아래와 같다.

$A^{(1)} = \begin{bmatrix} a_1^{(1)} \\ a_2^{(1)} \\ a_3^{(1)} \end{bmatrix}$, $X = \begin{bmatrix} x_1 \\ x_2 \end{bmatrix}$, $B^{(1)} = \begin{bmatrix} b_1^{(1)} \\ b_2^{(1)} \\ b_3^{(1)} \end{bmatrix}$, $W^{(1)} = \begin{bmatrix} w_11^{(1)}  & w_21^{(1)} & w_31^{(1)} \\ w_12^{(1)}  & w_22^{(1)} & w_32^{(1)} \end{bmatrix}$

 

import numpy as np

X = np.array([1.0, 0.5])
B1 = np.array([0.1, 0.2, 0.3])
W1 = np.array([[0.1,0.3,0.5],
               [0.2,0.4,0.6]])

A1 = np.dot(X, W1) + B1
A1 #weighted sum
#array([0.3, 0.7, 1.1])

* 이 weighted sum이 activation function인 sigmoid를 거치면 아래와 같이 값이 바뀐다.

def sigmoid(X):
  return 1/(1+np.exp(-X))

Z1=sigmoid(A1)
Z1
#array([0.57444252, 0.66818777, 0.75026011])

 

2) hidden layer1 to hidden layer2

*hidden layer1의 output을 input으로 사용하여 hidden layer2의 output을 도출한다.

W2=np.array([[0.1, 0.4],
             [0.2, 0.5],
             [0.3, 0.6]])
B2=np.array([0.1, 0.2])

A2=np.dot(Z1, W2) + B2
Z2=sigmoid(A2)
Z2
#array([0.62624937, 0.7710107 ])

 

3) hidden layer2 to output layer

 

* 우리가 구현하는 output layer는 acitvation function이 특별한 역할을 하지 않고 전달된 값을 forwarding하기만 하므로 새로운 activation function을 정의한다.

def identity(X):
  return X

W3=np.array([[0.1, 0.3],
             [0.2, 0.4]])
B3=np.array([0.1, 0.2])

A3=np.dot(Z2, W3) + B3
Z3=identity(A3)
Z3 #array([0.31682708, 0.69627909])

 

3층 신경망 구현 정리 코드

def sigmoid(X):
  return 1/(1+np.exp(-X))

def identity(X):
  return X

def init_network():
  network={}
  network['W1']=np.array([[0.1, 0.3, 0.5],
                       [0.2, 0.4, 0.6]])
  network['B1']=np.array([0.1, 0.2, 0.3])
  network['W2']=np.array([[0.1, 0.4],
                       [0.2, 0.5],
                       [0.3, 0.6]])
  network['B2']=np.array([0.1, 0.2])
  network['W3']=np.array([[0.1, 0.3],
                       [0.2, 0.4]])
  network['B3']=np.array([0.1, 0.2])

  return network

def forward(network, x):  
  W1, W2, W3 = network['W1'], network['W2'], network['W3']
  b1, b2, b3 = network['B1'], network['B2'], network['B3']

  a1=np.dot(x,W1)+b1
  z1=sigmoid(a1)
  a2=np.dot(z1,W2)+b2
  z2=sigmoid(a2)
  a3=np.dot(z2,W3)+b3
  y=identity(a3)

  return y

network=init_network()
x=np.array([1.0, 0.5])
y=forward(network, x)
y 
#array([0.31682708, 0.69627909])

 

출력층 설계하기

* 출력층의 acitvation function은 model의 목적에 따라 달라진다.

* 만약 회귀를 위한 모델을 생성한다면 출력층의 activation function은 항등 함수(전달되는 값을 그대로 forwarding해주는)등을 사용한다.

* 분류를 위한 모델에는 softmax함수 등을 사용한다.

 

소프트맥스 함수(softmax function)

$y_k = \frac{exp(a_k)}{\sum\limits_{i=1}^n {exp(a_i)}}$

* n은 출력층의 뉴런 수(분류할 class 개수)이다.

* $y_k$는 output y 중 k번째라는 뜻과 같다.

 

#softmax

def softmax(x):
  exp_x=np.exp(x)
  sum_exp_x=np.sum(exp_x)
  result=exp_x/sum_exp_x
  return result

a=np.array([0.3, 2.9, 4.0])
softmax(a)
#array([0.01821127, 0.24519181, 0.73659691])

 

소프트맥스 함수 구현시 overflow 문제

* softmax함수를 구현할 때 문제점은 지수함수이기 때문에 exp()안에 큰 수가 주어진 상태에서 연산을 수행하는 것이 불가능하다는 점이다(값이 너무 커져서 계산이 불가하다 - overflow).

* 이 문제는 다음과 같이 해결 가능하다.

* 소프트맥수 함수는 다음이 성립한다.

 

$y_k = \frac{exp(a_k)}{\sum\limits_{i=1}^n {exp(a_i)}} = \frac{C exp(a_k)}{C \sum\limits_{i=1}^n {exp(a_i)}} = \frac{exp(a_k + logC)}{\sum\limits_{i=1}^n {exp(a_i+logC)}} = \frac{exp(a_k + C')}{\sum\limits_{i=1}^n {exp(a_i+C')}}$ 

 

* 즉, exponential 지수에 어떤 값을 더하거나 빼도 softmax함수의 결과는 동일하다는 것이다.

* 이 idea를 활용하여 input값에 일정 값을 빼준다면 overflow를 방지하면서 동일한 결과를 얻을 수 있다.

* 이 때, input값 중에 가장 큰 값을 빼주는 것이 일반적이다.

 

def softmax(x):
  C=np.max(x)
  exp_x=np.exp(x-C)
  sum_exp_x=np.sum(exp_x)
  result=exp_x/sum_exp_x
  return result

a=np.array([1010, 1000, 990])
softmax(a)
#array([9.99954600e-01, 4.53978686e-05, 2.06106005e-09])

 

소프트맥스 함수의 특징

* 소프트맥스 함수의 출력은 0에서 1.0 사이의 실수이다. 또한 모든 출력 값을 더하면 1이된다. 이 성질은 소프트맥스 출력값을 확률로 간주할 수 있게 해준다.

 * 소프트맥스의 또 다른 특징은 exponential 함수가 단조함수이기 때문에(a, b가 a<=b일 때 f(a) <= f(b)) 원소간의 대소 관계와는 관계가 없다는 점이다. 다시 말하면, 가장 높은 값을 가지는 원소의 class로 최종 분류를 하고 싶다면, 소프트맥스 함수를 사용하는게 의미가 없다는 뜻이다. 소프트맥스 함수를 취함으로서 얻을 수 있는 것은 0~1 사이의 실수로 표현되는 확률 값 뿐이다. 원소들의 scale이 조정될 뿐  대소관계는 변하지 않기 때문에 필요한 경우만 소프트맥스 함수를 사용하기로 한다(보통 훈련 단계에서는 소프트맥스 함수를 사용하고 추론 단계에서는 사용하지 않는 것이 일반적이다).

 

출력층의 뉴런 수 정하기

* 출력 층의 뉴런 개수는 분류하고 싶은 class의 개수대로 정하는 것이 일반적이다. 만약 0~9까지 써있는 손 글씨를 분류하고 싶다면 10개의 출력층 뉴런을 설정해 놓아야 할 것이다.

* 주어진 문제가 회귀 문제일 경우 단일 값을 출력해야 하므로 출력층의 뉴런 개수는 1이 된다.

 

배치 처리

* input layer, hidden layer1, hidden layer2, output layer로 구성되어 있는 모델을 생각해보자.

* 데이터는 mnist dataset을 사용한다고 가정한다.

* input layer는 28x28 image를 flatten한 784개의 픽셀 정보를 받아야 하므로 neuron 개수는 784개이다.

* hidden layer1은 neuron 개수가 50개라고 해보자.

* hidden layer2는 neuron 개수가 100개라고 해보자.

* output layer는 10개의 class를 구분해야 하므로 뉴런이 10개이다.

 

* 모델의 가중치 행렬은 아래와 같을 것이다.

W1 : 784 x 50

W2 : 50 x 100

W3 : 100 x 10

 

* 만약 784 차원의 벡터를 1개 입력하면 output은 10차원의 벡터가 될 것이다.

* 1 배치에 100개의 데이터가 들어가는 배치처리를 거친 데이터셋을 사용한다면 output은 100x10 행렬이 될 것이다.

 

배치 처리의 장점

* 수치 계산 라이브러리들이 큰 배열을 효율적으로 처리할 수 있도록 최적화 되어 있기 때문에 데이터를 1개씩 계산하는 것 보다 여러 개를 배치로 묶어 계산하는게 속도가 더 빠르다.

* 여러 데이터를 한 번에 읽어오는 것을 통해 I/O 횟수가 줄어든다. I/O시에는 CPU가 연산을 멈추고 I/O처리를 하게 되는데, 이러한 I/O 횟수를 줄임으로서 훈련에 더욱 집중할 수 있게 된다(I/O는 CPU입장에서 매우 시간이 오래 걸리는 task이다).

 

def get_data():
  (x_train, y_train), (x_test, y_test) = \
  load_mnist(normalize=True, flatten=True, one_hot_label=False)

def init_network():
  with open("sample_weight.pkl", 'rb') as f: #pickle file에 저장된 weight 불러오기
    network=picle.load(f)
  
  return network

def predict(network, x):
  W1, W2, W3 = network['W1'], network['W2'], network['W3']
  b1, b2, b3 = network['b1'], network['b2'], network['b3']

  a1 = np.dot(x, W1) + b1
  z1 = sigmoid(a1)
  a2 = np.dot(z1, W2) + b2
  z2 = sigmoid(a2)
  a3 = np.dot(z2, W3) + b3
  y = softmax(a3)

  return y

x, t = get_data()
network=init_network()

batch_size=100
accuracy_cnt=0

for i in range(0, len(x), batch_size):
  x_batch=x[i:i+batch_size]
  y_batch=predict(network, x_batch)
  p=np.argmax(y_batch, axis=1)
  accuracy_cnt += np.sum(p==t[i:i+batch_size])

print('Accuracy: {}'.format(float(accuracy_cnt)/len(x)))

참고자료 : 

https://book.naver.com/bookdb/book_detail.nhn?bid=11492334

 

밑바닥부터 시작하는 딥러닝

직접 구현하고 움직여보며 익히는 가장 쉬운 딥러닝 입문서!『밑바닥부터 시작하는 딥러닝』은 라이브러리나 프레임워크에 의존하지 않고, 딥러닝의 핵심을 ‘밑바닥부터’ 직접 만들어보며

book.naver.com

 

 

 

'Deep Learning > 밑바닥부터 시작하는 딥러닝' 카테고리의 다른 글

Backpropagation  (0) 2022.01.27
경사하강법(Gradient Descent method)  (0) 2022.01.25
수치 미분  (0) 2022.01.25
손실 함수  (0) 2022.01.25
퍼셉트론  (0) 2021.11.17