본문 바로가기

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

Chapter 3. 신경망

이 게시물은 밑바닥부터 시작하는 딥러닝 1권을 바탕으로 작성되었습니다.


2장에서 퍼셉트론에 대해 배웠습니다. 하지만 퍼셉트론 구현 과정에서 가중치와 편향에 대한 작업을 우리가 직접 적절한 값을 넣어줬습니다. 신경망이 이 귀찮은 일을 해결해줍니다. 신경망의 중요한 성질이 바로 가중치 매개변수의 적절한 값을 데이터로부터 자동으로 학습하는 능력입니다. 

이번 3장에서는 신경망의 개요와 신경망이 입력 데이터가 무엇인지 식별하는 처리 과정을 자세히 알아보겠습니다.

 

 

신경망

그림으로 표현된 신경망은 입력층, 은닉층, 출력층으로 구성되는 2층 신경망입니다. 

신경망의 신호 전달 방법을 보기 전에 퍼셉트론에 대해 살짝 복습해보겠습니다. 

$$y=\begin{cases}0 & (w_1x_1 + w_2x_2 +b \leq  0)\\1 & (w_1x_1 + w_2x_2 + b> 0)\end{cases}$$

$b$는 편향을 나타내는 매개변수로, 뉴런이 얼마나 쉽게 활성화되느냐를 제어합니다. $w_1$, $w_2$는 각 신호의 가중치를 나타내는 매개변수로 각 신호의 영향력을 제어합니다.

 

위 식을 좀 더 간결한 형태로 나타내 보겠습니다.

$$y=h(b + w_1 + w_2)$$

$$h(x)=\begin{cases}0 & (x \leq  0)\\1 & (x> 0)\end{cases}$$

 

입력 신호의 총합이 $h(x)$라는 함수를 거쳐 변환되어, 그 변환된 값이 y의 출력이 됨을 보여주는 식입니다. 

 

활성화 함수

조금 전 $h(x)$라는 함수가 등장했는데, 이처럼 입력 신호의 총합을 출력 신호로 변환하는 함수를 활성화 함수(activation function)라고 합니다.

 

뉴런 하나를 활성화 함수를 이용해 표현해보면 아래 그림과 같습니다.

활성화 함수는 임계값을 경계로 출력이 바뀌는데, 이런 함수를 계단 함수(step function)라고 합니다. 

계단 함수를 한 번 구현하고 그래프의 모양을 살펴보겠습니다.

계단 함수와 그래프

def step_function(x):
  y = x > 0
  return y.astype(np.int)
import matplotlib.pyplot as plt
import numpy as np

x = np.arange(-5.0, 5.0, 0.1)
y = step_function(x)
plt.plot(x,y)
plt.grid()
plt.ylim(-0.1, 1.1)
plt.show()

시그모이드 함수와 그래프

신경망에서 자주 이용하는 활성화 함수인 시그모이드 함수(sigmoid function)를 나타낸 식은 다음과 같습니다.

$$h(x) = \frac{1}{1+exp(-x)}$$

시그모이드 함수를 구현해보고 그래프로 그려보겠습니다.

def sigmoid(x):
  y = 1 / (1 + np.exp(-x))
  return y
x = np.arange(-5.0, 5.0, 0.1)
y = sigmoid(x)
plt.plot(x,y)
plt.grid()
plt.ylim(-0.1, 1.1)
plt.show()

시그모이드 함수와 계단 함수를 같이 그려서 두 그래프의 차이를 한 번 보겠습니다.

x = np.arange(-5.0, 5.0, 0.1)
y1 = step_function(x)
y2 = sigmoid(x)
plt.cla()
plt.plot(x, y1)
plt.plot(x, y2)
plt.grid()
plt.legend(['step', 'sigmoid'])

 

ReLU 함수와 그래프

최근에는 시그모이드 함수 대신 ReLU(Rectified Linear Unit) 함수를 주로 이용합니다.

ReLU함수의 식은 아래와 같습니다.

$$h(x) =\begin{cases}0 & x  \leq  0\\x & x > 0\end{cases}$$

 

ReLU함수를 구현하고 그래프를 그려보겠습니다.

def relu(x):
  return np.maximum(0, x)
plt.cla()

# np.linspace(start, stop, num) 함수는 start에서부터 stop까지 num 개의 숫자를 등간격으로 나눠 넘파이 배열로 반환합니다.
x = np.linspace(-5.0, 5.0, 100)
y = relu(x)
plt.grid()
plt.plot(x, y);
plt.xlim(-6.0, 6.0);

신경망에서의 행렬 곱

넘파이 행렬을 써서 신경망을 구현해보겠습니다. 아래 그림과 같은 간단한 신경망을 가정하고 구현하겠습니다. 이 신경망은 편향과 활성화 함수를 생략하고 가중치만을 갖습니다.

import numpy as np
x = np.array([1,2])
w = np.array([[1,3,5],[2,4,6]])
print(x.shape)
print('---'*5)
print(w.shape)
print('---'*5)
print(w)
print('---'*5)
y = np.dot(x,w)
print(y)
# outputs

(2,)
---------------
(2, 3)
---------------
[[1 3 5]
 [2 4 6]]
---------------
[ 5 11 17]

 

3층 신경망 구현

이제 좀 더 복잡한 3층 신경망을 구현해보겠습니다.

3층 신경망이 다음 그림과 같다고 가정하고 구현을 해보겠습니다.

입력층은 2개, 첫 번째 은닉층은 3개, 두 번째 은닉층은 2개, 출력층은 2개의 뉴런으로 구성됩니다.

 

먼저 계산의 편의를 위해 표기법을 정하고 가겠습니다.

오른쪽 위의 괄호는 층을 의미하고, 오른쪽 아래의 두 숫자는 차례대로 다음 층 뉴런과 앞 층 뉴런의 인덱스 번호입니다. 

 

이제 앞서 봤던 3층 신경망 그림에서 표기법을 이용하고, 편향을 추가하여 신호의 전달 과정을 살펴보겠습니다.

첫 번째 은닉층의 계산 식은 다음과 같습니다.

$$a_{1}^{(1)} = w_{11}^{(1)}x_1 + w_{12}^{(1)}x_2 + b_{1}^{(1)}$$
$$a_{2}^{(1)} = w_{21}^{(1)}x_1 + w_{22}^{(1)}x_2 + b_{2}^{(1)}$$
$$a_{3}^{(1)} = w_{31}^{(1)}x_1 + w_{32}^{(1)}x_2 + b_{3}^{(1)}$$

 

여기서 행렬의 곱을 이용하면 위 식을 좀 더 간소화할 수 있습니다.

$$A^{(1)} = XW^{(1)} + B^{(1)}$$

 

여기서 

$$A^{(1)} = (a_{1}^{(1)} a_{2}^{(1)} a_{3}^{(1)}), X = (x_1 x_2), B^{(1)} = (b_{1}^{(1)} b_{2}^{(1)} b_{3}^{(1)})$$

$$W^{(1)} = \begin{pmatrix}
 w_{11}^{(1)} & w_{21}^{(1)} & w_{31}^{(1)}\\
 w_{12}^{(2)} & w_{22}^{(2)} & w_{32}^{(2)}
 \end{pmatrix}$$

입니다.

넘파이의 다차원 배열을 통해 구현해보겠습니다.

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

print(W1.shape) # (2, 3)
print(X.shape) # (2,)
print(B1.shape) # (3,)

A1 = np.dot(X, W1) + B1

이어서 1층의 활성화 함수에서의 처리를 살펴보겠습니다. 활성화 함수 처리를 그림으로 나타내면 다음과 같습니다.

그림과 같이 은닉층에서의 가중치 합(가중 신호와 편향의 총합)을 $a$로 표기하고 활성화 함수 $h()$로 변환된 신호를 $z$로 표기합니다. 활성화 함수로 시그모이드 함수를 사용한다고 가정하고 파이썬으로 구현해보겠습니다.

Z1 = sigmoid(A1)

print(A1) # [0.3, 0.7, 1.1]
print(Z1) # [0.57444252, 0.66919777, 0.75026011]

 

이어서 1층에서 2층으로 과정과 그 구현을 살펴보겠습니다.

행렬식으로 나타낸다면 아래와 같고

$$A^{(2)} = Z^{(1)}W^{(2)} + B^{(2)}$$

 

이때 행렬은 각각

$$A^{(2)} = (a_{1}^{(2)} a_{2}^{(2)}), Z^{(1)} = (z_{1}^{(1)}  z_{2}^{(1)} z_{3}^{(1)}), B^{(2)} = (b_{1}^{(2)} b_{2}^{(2)})$$

$$W^{(2)} = \begin{pmatrix}
 w_{11}^{(2)} & w_{21}^{(2)} \\
 w_{12}^{(2)} & w_{22}^{(2)} \\
 w_{13}^{(2)} & w_{23}^{(2)}
 \end{pmatrix}$$

입니다.

 

파이썬 코드로 구현해보겠습니다.

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

print(Z1.shape) # (3,)
print(W2.shape) # (3,2)
print(B2.shape) # (2,)

A2 = np.dot(Z1, W2) + B2
Z2 = sigmoid(A2)

print(A2) # [0.3 0.7 1.1]
print(Z2) # [0.62624937 0.7710107 ]

이 구현은 1층의 출력 $Z1$이 2층의 입력이 된다는 점을 제외하면 조금 전의 구현과 똑같습니다. 

 

마지막으로 2층에서 출력층으로의 신호전달과 구현을 보겠습니다. 출력층의 구현도 그동안의 구현과 거의 같습니다. 딱 하나, 활성화 함수만 지금까지의 은닉층과 다릅니다.

 

3층에서의 활성화 함수 $\sigma()$는 항등 함수로 아래와 같습니다.

$$\sigma(x) = x$$

 

def identity_function(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
Y = identity_function(A3) # or Y = A3

구현 정리

지금까지 3층 신경망에 대해 구현한 코드를 정리해보도록 하겠습니다. 

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_function(a3)
  return y
    
network = init_network()
x = np.array([1.0, 0.5])
y = forward(network, x)
print(y)
    
[0.31682708 0.69627909]

init_network() 함수에서는 가중치와 편향을 초기화하고 이들을 딕셔너리 변수인 network에 저장합니다. 그리고 forward() 함수는 입력 신호를 출력으로 변환하는 처리 과정을 구현하고 있습니다.

 

출력층 설계하기

신경망은 분류, 회귀 모두에 이용할 수 있습니다. 다만 둘 중 어떤 문제냐에 따라 출력층에서 사용하는 활성화 함수가 달라집니다. 일반적으로 회귀에서는 항등 함수를, 분류에서는 소프트맥스 함수를 사용합니다.

 

소프트맥스 함수(softmax function)

소프트맥스 함수의 식은 다음과 같습니다.

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

 

위 식을 간단하게 파이썬으로 구현하겠습니다.

def softmax(a):
  exp_a = np.exp(a)
  sum_exp_a = np.sum(exp_a)
  y = exp_a / sum_exp_a
  
  return y

softmax함수를 코드로 구현할 때는 주의할 점이 하나 있습니다. 바로 오버플로 문제입니다. 소프트맥스 함수는 지수함수를 사용하기 때문에 아주 큰 값을 내뱉어서 결과가 제대로 나오지 않는 경우가 있습니다.

이러한 문제를 해결하기 위해 일종의 트릭을 사용하는데 입력 신호 중 최댓값을 빼주고, 그 값을 입력으로 하여 계산하면 오버플로를 막을 수 있습니다.

아래는 트릭을 적용한 소프트맥스 함수입니다.

def softmax(a):
  c = np.max(a)
  exp_a = np.exp(a-c) # 오버플로 해결 트릭
  sum_exp_a = np.sum(exp_a)
  y = exp_a / sum_exp_a
  
  return y

 

소프트맥스 함수의 특징 중 하나는 소프트맥스 함수 출력의 총합이 1이 된다는 점입니다. 이 성질 덕분에 소프트맥스 함수의 출력을 '확률'로 해석할 수 있습니다. 

 

출력층의 뉴런 수 정하기

출력층의 뉴런 수는 본인이 풀고 싶은 문제에 맞게 적절히 정해야 합니다. 분류에서는 분류하고 싶은 클래스의 수로 설정하는 것이 일반적입니다. 예를 들어 이미지를 숫자 0부터 9 중 하나로 분류하는 문제라면 출력층의 뉴런을 10개로 설정합니다.

 

손글씨 숫자 인식

MNIST 데이터셋

손글씨 숫자 이미지 집합으로 0~9까지의 숫자 이미지로 구성되어 있습니다. 훈련 이미지가 60,000장, 시험 이지미가 10,000장 준비되어 있습니다. 

이미지는 28X28 크기의 회색조 이미지(1 채널)이며, 각 픽셀은 0에서 255까지의 값을 취합니다. 그리고 각 이미지에는 그 이미지가 실제 의미하는 숫자가 레이블로 붙어 있습니다.

제공되는 mnist.py 파일에 있는 load_mnist() 함수를 이용하면 MNIST 데이터를 쉽게 가져올 수 있습니다.

load_mnist() 함수는 데이터를 "(훈련 이미지, 훈련 레이블), (시험 이미지, 시험 레이블)" 형식으로 변환해줍니다. 인수로는 normalize, flatten, one_hot_label 세 가지를 설정할 수 있습니다.

  • normalize : 입력 이미지의 픽셀 값을 0.0 ~ 1.0 사이의 값으로 정규화할지를 정함
    이처럼 데이터를 특정 범위로 변환하는 처리를 정규화(normalization)라 하고, 이러한 변환을 해주는 것을 전처리(pre-processing)라 합니다. 여기서는 전처리로 정규화 방법을 사용했다고 볼 수 있습니다.
  • flatten : 입력 이미지를 평탄하게, 즉 1차원 배열로 만들지를 정함
  • one_hot_label : 레이블을 원-핫 인코딩(정답을 뜻하는 원소만 1이고 나머지는 모두 0인 배열) 형태로 저장할지를 정함
    예를 들어 레이블이 5이면 [0,0,0,0,0,1,0,0,0,0]인 배열이 된다는 뜻입니다.

신경망 구현

이 신경망은 입력층 뉴런을 784개, 출력층 뉴런을 10개로 구성합니다. 입력 이미지의 크기가 28X28=784 이므로 입력층 뉴런은 784개, 0~9까지의 레이블을 예측해야 하므로 출력층 뉴런은 10개입니다. 그리고 두 개의 은닉층에서 각각의 뉴런 개수를 50, 100개로 정했는데 이는 임의로 정한 값입니다.

 

먼저 세 함수 get_data(), init_netword(), predict()를 정의합니다.

def get_data():
  (X_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=False)
  return x_test, t_test
  
def init_network():
  with open("sample_weight.pkl", 'rb') as f:
    network = pickle.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, W2) + b3
  y = softmax(a3)
  
  return y
  • init_network() : pickle 파일에서 '학습된 가중치 매개변수'를 읽어옵니다. 이 파일에는 가중치와 편향 매개변수가 딕셔너리 변수로 저장되어 있습니다.
  • 나머지 두 함수는 지금까지 구현한 함수와 거의 비슷하니 설명을 생략하겠습니다.

정확도 평가

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

accuracy_cnt = 0
for i in range(len(x)):
  y = predict(network, x[i])
  p = np.argmax(y) # 확률이 가장 높은 원소의 인덱스를 얻음
  if p == t[i]:
    accuracy_cnt += 1
    
print("Accuracy: " + str(float(accuracy_cnt) / len(x)))

# outputs

Accuracy: 0.9352
  1.  get_data()로 x = x_test, t = t_test를 할당해줌
  2. pickle 파일로 학습된 매개변수들 가져옴
  3. predict 함수를 이용해서 예측을 하는데 len(x) (테스트 데이터의 크기 : 10000) 만큼을 반복해줌
    반복하면서 예측 값과 실제 레이블(t)이 동일하면 accuracy_cnt에 1을 더해줌
  4. 마지막으로 올바르게 분류한 횟수를 전체 데이터 크기로 나눠서 정확도를 뽑아줌

 

배치 처리

앞에서 예측을 할 때 입력으로 이미지를 하나씩 넣어서 총데이터의 크기만큼 반복했습니다. 이렇게 되면 데이터가 아주 많아질 때 효율이 떨어지므로 한 번 예측을 할 때 여러 개의 데이터를 묶어서 예측을 진행하는 방법으로 효율을 높일 수 있습니다. 이처럼 하나로 묶은 입력 데이터를 배치(batch)라 합니다.

 

배치를 이용한 예측

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: " + str(float(accuracy_cnt)/len(x)))

# outputs
Accuracy: 0.9352
  1.  반복을 진행할 때 batch_size만큼의 간격으로 증가하면서 반복함
  2. 입력 데이터를 x[i : i+batch_size]를 이용해 묶어줍니다.
  3. 묶음을 바로 predict 함수에 넣어서 출력을 뽑습니다. y_batch.shape 은 위 그림에서와 같이 (100,10)이 될 것입니다.
  4. axis=1 파라미터를 통해 출력 값에서 행 방향으로 가장 큰 값을 뽑아서 p에 할당합니다. p.shape 은 (100,1)이 될 것입니다.
  5. 똑같이 정확도를 계산 후 출력합니다.

이번 장에서는 신경망의 순전파를 살펴봤습니다. 몇 가지 활성화 함수도 살펴보았고, 순전파를 행렬로 표현하는 것에 대해서도 배웠습니다. 그리고 순전파시 데이터를 배치 단위로 묶어서 입력을 하는 방법에 대해서도 알아보았습니다.

 

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

Chapter 7. 합성곱 신경망(CNN)  (0) 2022.09.16
Chapter 5. 오차역전파법  (0) 2022.09.16
Chapter 4. 신경망 학습  (1) 2022.09.16
Chapter 2. 퍼셉트론  (0) 2022.09.15