본문 바로가기

Deep Learning

신경망-2 (Neural Network)

1. 3층 신경망 구현하기

아래와 같은 2개의 은닉층을 가진 3층 신경망을 구현해보자.

 

 

먼저 1층의 첫번째 뉴런으로 가는 신호를 살펴보자.

아래 그림을 보면 편향이 하나 추가되어있는것을 확인 할 수 있다.

 

 

그림에서 $a_1^{(1)}$ 를 수식으로 나타내면 $a_1^{(1)} = w_{11}^{(1)}x_1 + w_{12}^{(1)}x_2 + b_1^{(1)}$ 으로 나타낼 수가 있다.

그리고 여기서 행렬의 곱을 이용하면 1층의 "가중치 부분" 을 다음 식처럼 간소화 할 수 있다.

 

 

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

 

그리고 활성화 함수를 사용해서 변환된 신호 $z$ 를 출력해 낸다. 여기서는 시그모이드 함수를 활성화 함수로 사용한다.

 

2층의 구현 또한 1층과 동일하며, 마지막 출력층에서는 활성화 함수로써 시그모이드 함수가 아닌 항등함수를 사용한다.

 


출력층의 활성화 함수는 풀고자 하는 문제의 성질에 맞게 정한다.

예를 들어,

 

회귀에는 항등함수를,

2클래스  분류에는 시그모이드 함수를,

다중 클래스 분류에는 소프트맥스 함수를 사용하는 것이 일반적이다.


 

아래 코드는 위 3층 신경망에 대한 구현코드이다.

 

import numpy as np


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

    return network


def identity_function(x):
    return x


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


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]

 

 

2. 출력층 설계하기

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

2-1. 항등 함수와 소프트맥스 함수 구현하기

항등함수는 입력을 그대로 출력하는 함수이며 소프트맥스 함수의 식은 다음과 같다.

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

  • exp(x) : $e^x$ 를 뜻하는 지수함수
  • n : 출력층의 뉴런 수
  • $y_k$ : 그 중 k 번째 출력

위의 식과 같이 소프트맥스 함수의 분자는 입력 신호 $a_k$ 의 지수 함수, 분모는 모든 입력 신호의 지수 함수의 합으로 구성된다.

 

import numpy as np


def softmax(a):
    exp_a = np.exp(a)  # 지수 함수
    sum_exp_a = np.sum(exp_a)  # 지수함수의 합
    y = exp_a / sum_exp_a
    return y

 

2-2. 소프트 맥스 함수 구현시 주의점

소프트맥스 함수는 지수 함수를 사용하는데, 지수 함수는 쉽게 오버플로를 발생할 수 있으니 주의해야 한다.

이 문제를 해결하도록 위의 수식을 개선해보자.

 

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

 

위 식의 전개과정을 보면,

  1. $C$ 라는 임의의 정수를 분자와 분모 양쪽에 곱한다.
  2. 그 다음으로 $C$ 를 지수함수 $exp()$ 안으로 옮겨 $logC$ 로 만든다.
  3. 마지막으로 $logC$ 를 $C'$ 라는 새로운 기호로 바꾼다.

위의 식이 말하는 것은 소프트맥스의 지수 함수를 계산할 때 어떤 정수를 더하거나 빼도 결과는 바뀌지 않는다는 것이다. $C'$ 에는 어떤 값을 대입해도 상관없지만, 오버플로를 막을 목적으로는 입력 신호 중 최대값 을 사용하는 것이 일반적이다.

 

import numpy as np


def softmax(a):
    c = np.max(a)  # 오버플로 방지
    exp_a = np.exp(a)  # 지수 함수
    sum_exp_a = np.sum(exp_a)  # 지수함수의 합
    y = exp_a / sum_exp_a
    return y


# softmax 함수를 사용한 신경망 출력의 예
a = np.array([0.3, 2.9, 4.0])
y = softmax(a)

print(y)  # [0.01821127 0.24519181 0.73659691]
print(np.sum(y))  # 1.0

 

2-3. 소프트맥스 함수의 특징

  • 소프트맥스 함수의 출력은 0에서 1.0 사이의 실수이다.
  • 소프트맥스 함수의 출력의 총합은 1이다. 이 성질 덕분에 소프트맥스 함수의 출력을 확률 로 해석할 수 있다.
  • 소프트맥스 함수를 적용해도 각 원소의 대소 관계는 변하지 않는다. 이는 지수 함수 $y = exp(x)$ 가 단조 증가 함수이기 때문이다.
  • 추론 단계에서 소프트맥스 함수를 적용해도 출력이 가장 큰 뉴런의 위치는 달라지지 않기 때문에 출력층의 소프트맥스 함수를 생략하는 것이 일반적이다.
  • 반대로, 신경망을 학습시킬 때는 출력층에서 소프트맥스 함수를 사용한다.

 

3. 손글씨 숫자 인식

손글씨 숫자 분류의 순전파(forward propagation, 학습 과정이 생략된 추론과정) 를 구현해보자.

3-1. MNIST 데이터셋 신경망 추론

손글씨 숫자 이미지 데이터셋으로 유명한 MINIST을 가지고 추론을 시작해보자.

이 신경망은 입력층 뉴런을 784개, 출력층 뉴런을 10개로 구성한다. 입력층 뉴런이 784개인 이유는 이미지 크기가 28 * 28 = 784 이기 때문이고, 출력층 뉴런이 10개인 이유는 이 문제가 0에서 9까지의 숫자를 구분하는 문제이기 때문이다.

 

 

import sys, os

sys.path.append(os.pardir)
import numpy as np
import pickle
from dataset.mnist import load_mnist


def get_date():
    (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 sigmoid(x):
    return 1 / (1 + np.exp(-x))


def softmax(a):
    c = np.max(a)  # 오버플로 방지
    exp_a = np.exp(a)  # 지수 함수
    sum_exp_a = np.sum(exp_a)  # 지수함수의 합
    y = exp_a / sum_exp_a
    return y


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_date()
# 학습된 가중치 매개변수를 가지고 있는 신경망을 가져온다.
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)))  # Accuracy:0.9352

 

 

위의 코드에서 load_mnist 함수의 인수인 normalize 를 True 로 설정했다. normalize 를 True 로 설정하면 0~255 범위인 각 픽셀의 값을 0.0~1.0 범위로 변환한다.(단순히 픽셀을 255로 나눈다)

 

이처럼 데이터를 특정 범위로 변환하는 처리를 정규화(normalization) 라 하고, 신경망의 입력 데이터에 특정 변환을 가하는 것을 전처리(pre-processing) 이라고 한다.

 

여기에서는 입력 이미지 데이터에 대한 전처리 작업으로 정규화를 수행한 셈이다.

 

앞의 예에서는 단순히 각 픽셀의 값을 255로 나누는 정규화를 수행했지만, 현업에서는 데이터 전체의 분포를 고려해 전처리하는 경우가 많다. 예를 들어, 데이터 전체 평균과 표준편차를 이용하여 데이터들이 0을 중심으로 분포하도록 이동하거나 데이터의 확산 범위를 제한하는 정규화를 수행한다. 그 외에도 전체 데이터를 균일하게 분포시키는 데이터 백색화(whitening) 도 있다.

 

3-3. 배치 처리

이미지 1장당 처리 시간을 처리 시간을 줄여주기 위해 배치 처리를 한다.

아래 코드는 원소 784 개로 처리된 1개의 이미지를 여러 묶음으로 묶어서 처리하는 예제이다.

 

 

import sys, os

sys.path.append(os.pardir)
import numpy as np
import pickle
from dataset.mnist import load_mnist


def get_date():
    (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 sigmoid(x):
    return 1 / (1 + np.exp(-x))


def softmax(a):
    c = np.max(a)  # 오버플로 방지
    exp_a = np.exp(a)  # 지수 함수
    sum_exp_a = np.sum(exp_a)  # 지수함수의 합
    y = exp_a / sum_exp_a
    return y


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

print(x.shape)  # (10000, 784) -> 1차원 배열 784 로 표현된 1개의 이미지가 10000개가 있다.
print(x[0].shape)  # (784,)

# 학습된 가중치 매개변수를 가지고 있는 신경망을 가져온다.
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)
    # 1번째 차원을 구성하는 각 원소에서 최대값의 인덱스를 찾게 해줌
    # 여기서 1번째 차원은 2차원이기 때문에, 2차원을 구성하는 각 열마다의 최대값을 구해줌(즉 배열 하나 안에서의 최대값을 구해줌)
    p = np.argmax(y_batch, axis=1)
    # 넘파이 배열끼리 비교하여 True/False bool 배열을 만들고, 이 결과 배열에서 True 가 몇개인지를 센다
    accuracy_cnt += np.sum(p == t[i:i+batch_size])


print("Accuracy:" + str(float(accuracy_cnt) / len(x)))  # Accuracy:0.9352

 

4. 정리

  • 신경망에서는 활성화 함수로 시그모이드 함수나 ReLU 함수 같은 매끄럽게 변화하는 함수를 이용한다.
  • 넘파이의 다차원 배열을 잘 사용하면 신경망을 효율적으로 구현할 수 있다.
  • 기계학습 문제는 크게 회귀와 분류로 나눌 수 있다.
  • 출력층의 활성화 함수로는 회귀에서 주로 항등 함수를, 분류에서는 주로 소프트맥스 함수를 사용한다.
  • 분류에서는 출력층의 뉴런 수를 분류하려는 클래스 수와 같에 설정한다.
  • 입력 데이터를 묶은 것을 배치라고 하며, 추론 처리를 이 배치 단위로 진행하면 결과를 훨씬 빠르게 얻을 수 있다.

 

참조

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

'Deep Learning' 카테고리의 다른 글

신경망 학습 - 1  (0) 2021.02.11
신경망-1 (Neural Network)  (0) 2021.01.28
Perceptron (퍼셉트론)  (0) 2021.01.17