[DL from Scratch] Chapter 3: Neural Networks

[DL from Scratch] Chapter 3: Neural Networks

해당 Post는 밑바닥부터 시작하는 딥러닝 1권을 읽으면서 정리한 내용들로 구성되어 있다.

밑바닥부터 시작하는 딥러닝
직접 구현하고 움직여보며 익히는 가장 쉬운 딥러닝 입문서

이번 Chapter 3에서는 Neural Network에 대해 설명한다.

앞의 Chapter 2 Perceptron에서는 아래의 좋은 점과 나쁜 점들이 있었다:

1) 임의의 복잡한 continuous function $f$ 을 하나의 hidden layer만으로도 이론상 표현할 수 있다.

2) 원하는 결과를 출력하도록 weight을 적절히 조정하는 작업은 사람이 여전히 수동으로 해야한다. (AND, OR Gate 등)

여기서 Neural Network를 이용하여 데이터로부터 자동으로 가중치 매개변수의 적절한 값을 학습할 수 있게 된다.


3.1) Peceptron에서 Neural Network으로

3.1)은 Perceptron과 Neural Network의 다른 점을 위주로 설명한다.

💡
3.1.1) Neural Network의 예시

  • Input Layer
  • Hidden Layer
  • Output Layer

각 Layer들은 사이의 연결하는 선 / weight가 부여되는 부분이 아니라, 세로로 쌓인 Node들의 집합이다.

위 그림의 경우 Input Layer부터 Output Layer까지 차례로 0층, 1층, 2층이라 하며,

Hidden layer는 사람의 눈에 보이지 않기에 'Hidden'이라 한다.

실제로 위 그림은 3개의 Layer로 구성되지만, 가중치를 갖는 층은 2개뿐이므로 `2층 신경망' 이라고도 한다.


1) 신경망을 구성하는 층수를 기준으로 '3층 신경망': 총 Layer의 개수

2) 실제 가중치를 갖는 층의 개수를 기준으로 '2층 신경망': 총 Layer 개수 - 1

💡
3.3.1) Perceptron 복습

Perceptron에 대해 보다 자세한 설명은 아래의 포스트를 참고하자.

[DL from Scratch] Chapter 2: Perceptron
해당 Post는 밑바닥부터 시작하는 딥러닝 1권을 읽으면서 정리한 내용들로 구성되어 있다. 밑바닥부터 시작하는 딥러닝직접 구현하고 움직여보며 익히는 가장 쉬운 딥러닝 입문서 이번 Chapter 2에서는 Perceptron 알고리즘을 설명한다. 이 perceptron 알고리즘은 Neural Network의 기원이 되는 알고리즘이기에, Perceptron의 구조를 배우는 것은 Neural Network와 Deep Learning으로 나아가는데 중요한 아이디어를 제공해준다. 따라서 해당 Chapter

https://images.app.goo.gl/snFXuSxk7qbUBgk77

위의 그림은 입력으로 총 $d$개의 input signal을 받은 Perceptron의 예시이다.

  • $x_i$: Input Signal
  • $w_i$: Weight(가중치, $i = 1, 2, ... , d$)
  • $w_0$: Bias(편향)

Weight(Bias)은 학습에 의해 Neural Network가 알아서 설정하는 값이므로 Bias ($b = w_0 x_0 = w_0$)Weight($w_0$)이 아닌 Input Signal ($x_0 = 1$)을 1로 fix시킨다.

위 그림의 을 Neuron or Node라 하고, Input Signal이 Output Neuron으로 전달될 때 각각 고유한 weight가 곱해진다.

즉, 각 weight($w_i$)와 input signal($x_i$)간의 linear combination인 신호의 총합 $\mathbf{w}^T\mathbf{x}$가 Output Neuron으로 전달된다.

$\mathbf{w}$와 $\mathbf{x}$가 모두 column vector이므로 앞의 $\mathbf{w}$에 Transpose $\mathbf{w}^T$를 취한다.

그리고 이 $\mathbf{w}^T\mathbf{x}$가 정해진 한계를 넘어설 때만 1을 출력하고, 그렇지 않으면 0을 출력한다.

1을 출력할 때, 뉴런이 활성화되었다고도 표현한다.

책에서는 그 한계를 임계값이라 하며 Threshold $\theta$로 표현한다.

$$y = \begin{cases} 0 \: (\mathbf{w}^T\mathbf{x} \leq \theta) \\ 1 \:(\mathbf{w}^T\mathbf{x} > \theta) \end{cases}$$

위의 식에서 $\theta$를 $-b$로 치환하면 Perceptron의 동작이 아래와 같이 바뀐다.

$$y = \begin{cases} 0 \: (\mathbf{w}^T\mathbf{x} + b \leq 0) \\ 1 \:(\mathbf{w}^T\mathbf{x} + b > 0) \end{cases}$$

weight $\mathbf{w}$는 input signal이 output에 주는 영향력(중요도)를 조절하는 paramter고, bias $b=w_0$는 뉴런이 얼마나 쉽게 활성화(민감도)하느냐를 조정하는 parameter이다.

bias $b$는 Neuron이 얼마나 쉽게 활성화되는지에 대한 민감도를 나타낸다고 볼 수 있다.

최종적으로 아래와 같이 heaviside step function $h$을 이용하여 표현가능하다.

$$y = h(\mathbf{w}^T\mathbf{x} + b)$$

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

💡
3.1.3) Activation Function

위의 식에서 function $h$를 activation function이라 한다.

  • 입력 신호의 총합을 출력 신호로 변환하는 함수
  • 입력 신호의 총합이 활성화를 일으키는지 정하는 역할

1) input signal $x$와 weight matrix $\mathbf{w}$과의 linear transformation

$$a = \mathbf{w}^T\mathbf{x} + b$$

2) applying non-linear activation function $h$

$$y = h(a)$$


3.2) Activation Function

지금까지 위에서 설명한 Perceptron은 heaviside step function을 activation function $h$로 사용하고 있음을 알 수 있다.

그렇다면 위의 함수가 아닌 다른 함수들도 activation function이 될 수 있지 않을까?

지금부터는 서로 다른 activation function에 대해 알아볼 것이다.

💡
3.2.1) Sigmoid Function

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

Sigmoid Function은 다음과 같이 수식으로 표현된다.

실수 space에서 ${0, 1}$의 구간으로 mapping해주는 특징이 있다.

💡
3.2.2) Heaviside Step Function 구현하기

python을 이용하여 step function을 구현할 수 있다.

$x$가 양수면 1을 출력하고, 그 외에는 0을 출력하면 된다.

def step_function(x):
    if x > 0:
        return 1
    else:
        return 0

위와 같이 간단하게 구현할 수 있으나, 위의 함수는 $x$가 실수일 경우에만 작동한다.

즉, $x$가 ndarray와 같은 numpy array일 때도 작동할 수 있도록 코드를 수정하면 다음과 같다.

def step_function(x):
    return (x > 0).astype(int)

step_function(np.array([1, 2, 3]))

  • $x>0$을 처리하면 boolean list로 [True or False, ... ,] 의 형태로 구현이 된다
  • 여기서 astype을 이용하여 int로 type을 바꾸면 1또는 0으로 변환되어 기존의 목적을 달성할 수 있다.

필자가 생각한 코드는 아래와 같다. ndarray의 모든 원소에 접근하는 거라면, np.where도 써볼만하다.

def step_function(x):
    return np.where(x > 0, 1, 0)

step_function(np.array([1, 2, 3]))

💡
3.2.3) Heaviside step function graph

이를 python을 이용하여 아래와 같이 시각화할 수 있다.

import numpy as np
import matplotlib.pyplot as plt

def step_function(x):
    return np.where(x > 0, 1, 0)

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

0을 경계로 출력이 0에서 1로 급격하게 바뀌어서 모양이 계단같다고 하여 이를 step function이라 한다.

💡
3.2.4) Sigmoid Function graph

sigmoid function을 python으로 다음과 같이 구현가능하다.

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

  • np.exp()를 사용하여 ndarray를 반환했기에 $x$가 ndarray 여도 올바른 결과를 출력한다
  • 이는 numpy의 broadcasting 기능 덕분이다

Broadcasting이란 numpy의 array와 scalar value의 연산을, numpy array의 각 원소와 scalar value의 연산으로 바꿔 처리하는 것이다.

sigmoid(np.array([-1.0, 1.0, 2.0]))

# array([0.26894142, 0.73105858, 0.88079708])

이제 sigmoid function을 graph로 시각화해보자.

import numpy as np
import matplotlib.pyplot as plt

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

x = np.arange(-5, 5, 0.1)
y = sigmoid(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1)
plt.show()

여기서 두 그래프를 비교해보자.

1) Difference

sigmoid는 부드러운 곡선이며, 입력에 따라 출력이 연속적으로 변한다.

step function은 0을 경계로 출력이 갑자기 바뀌어버린다.

즉, perceptron에서는 0 또는 1이 흘렀다면, neural network에서는 0과 1사이의 연속적인 실수가 흐른다.

2) Similarity

큰 관점에서 보면 비슷한 모양을 하고 있다. sigmoid가 x축을 기준으로 압축을 많이 할수록 비슷해진다.

둘 다 입력이 작을 수록 0에 가깝고, 커지면 1에 가까워진다.

또한 입력이 아무리 작거나 커도 출력은 0과 1 사이이다.

💡
3.2.6) Non-Linear Function

위에서 설명한 sigmoidstep function은 모두 non-linear function이다.

Neural Network에서는 activation function으로 반드시 Non-Linear function을 사용해야 한다.

그 이유는 Linear Function을 사용할 경우, Deep Neural Network처럼 Layer를 깊게 쌓았을 때 그 의미가 사라지기 때문이다.

즉, Layer를 아무리 깊게 쌓아도 Hidden Layer가 없는 network 하나로도 똑같은 기능을 수행할 수 있다는 것이다.

즉, Multi-layer effect를 사용하고 싶다면 반드시 activation function을 non-linear하게 설정해야 한다.

💡
3.2.7) ReLU Function

Sigmoid보다 최근에 더 많이 사용하는 함수가 있다.

이를 ReLU(Rectified Linear Unit)이라 한다.

수식으로는 아래와 같이 나타낼 수 있다.

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

or

$$h(x) = \text{max}(0, x)$$

python으로 다음과 같이 구현가능하다.

import numpy as np
import matplotlib.pyplot as plt


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


x = np.arange(-5, 5, 0.1)
y = relu(x)
plt.plot(x, y)
plt.ylim(-0.1, 5.1)
plt.show()

-np.max(): 단일 array내 최대값을 찾는 함수


- np.maximum(): 서로 다른 array 사이에서 각 element별 최대값을 가져오는 함수


3.3) 다차원 배열의 계산

  • 2D ndarray
b = np.array([[1, 2], [3, 4], [5, 6]])
print(b)
print(np.ndim(b))
print(np.shape(b))

# [[1 2]
#  [3 4]
#  [5 6]]
# 2
# (3, 2)

  • Matrix Multiplication: RR, RC, CR, CC (4-way)
    • RC: row vector & colum vector 내적
    • RR: 왼쪽 row vector의 각 element를 오른쪽 row vector각각에 분배하여 합
    • CC: 오른쪽 column vector의 각 element를 왼쪽 column vector각각에 분배하여 합
    • CR: column vector(=row vector)의 개수만큼 각각 rank 1 matrix를 만들어서 합

np.dot()@np.inner()간의 차이를 알고 싶다면 다음의 포스트를 살펴보자.

[Tips] Numpy Array 곱연산
1. Numpy Array 곱연산 스칼라곱(*) 두 Matrix (Array)가 Shape이 같을 때 Cross-Correlation 연산을 사용 (각 원소끼리의 곱의 합을 성분으로 갖는다) [n,m] * [n,m] = [n,m] Array간 shape이 같을 때 or Broadcasting 가능 각 행렬의 원소끼리의 곱을 성분으로 갖는 행렬을 반환 Broadcasting 가능: Matrix * (column, row) Vector ONLY [1,m] * [n,m] = [n,m] [m,1] * [m,n] = [m,n] [m,n] * [m,1] = [m,n] [n,m] * [1,m] = [n,m] 내적(np.dot) 둘 다 1D Vector일 때 np.dot( )으로 내적처리 Inner Product: $…

- a, b가 모두 1D: np.dot()을 사용


- a, b중 하나라도 2D 존재: @을 사용 (Matrix Multiplication shape 맞춰야 함)

  • Matrix Multiplication @ Neural Networks

$$\mathbf{W}^T \mathbf{x} = \mathbf{y}$$

위와 같이 식을 작성해야 $\mathbf{x}, \mathbf{y}$를 column vector로 처리할 수 있다.


$\mathbf{x}\mathbf{W} = \mathbf{y}$ 이렇게 처리하면 둘다 row vector이다.

  • Weight Matrix $\mathbf{W}$
    • Size = $M \times N$ (Input Node $M$, Output Node $N$)
    • \Node $x_i$에서 Node $y_i$로 갈 때 곱해지는 matrix의 원소는 $\mathbf{W}_{x_i y_i}$이다
    • index count의 방향은 2D 좌표계 기준 -y, +x 방향을 count x, y 방향으로 설정한다.

따라서, 저 위의 weight를 보고 weight matrix $\mathbf{W}$는 다음과 같이 작성할 수 있다.

그렇기에 기존의 코드인 row vector대로라면, 아래와 같다.

X = np.array([[1, 2,]])
print(X.shape)
W = np.array([[1, 3, 5], [2, 4, 6]])
print(W.shape)
y = np.dot(X, W)
print(y)

# (1, 2)
# (2, 3)
# [[ 5 11 17]]

하지만 column vector를 고려한다면 아래와 같다.

X = np.array([[1, 2]]).T
print(X.shape)
W = np.array([[1, 3, 5], [2, 4, 6]])
print(W.shape)
y = np.dot(W.T, X)
print(y)

# (2, 1)
# (2, 3)
# [[5]
# [11]
# [17]]


3.4) 3층 신경망 구현하기

지금부터 설명하는 Notation은 기존의 책과 사뭇 다르다.

그 이유는 필자는 column vector를 기준으로 하여 연산을 진행하기 때문이다.

💡
3.4.1) Notations

$\mathbf{W}_{ij}^{(k)}$

  • $i$: 앞 neuron index
  • $j$: 뒤 neuron index
  • $k$: $k$번째 Layer

$\mathbf{a}_{i}^{k}$

  • $i$: i번째 Neuron on $k^{\text{th}}$ layer

$\mathbf{b}_{i}^{k}$

  • $i$: i번째 Neuron에 더해지는 bias on $k^{\text{th}}$ layer

💡
3.4.2) 각 층의 신호 전달 구현하기

Weight W의 notation 중 ij의 순서를 바꾸면 된다.

$$a_1^{(1)} = \mathbf{W}_{11}^{(1)}x_1 + \mathbf{W}_{12}^{(1)}x_2 + b_1^{(1)}$$

여기서 행렬을 사용하면 간단하게 표현할 수 있다.

$$\mathbf{A}^{(1)} = (\mathbf{W}^{(1)})^T\mathbf{X} + \mathbf{B}^{(1)} $$

  • $\mathbf{A}^{(1)} = \left [ a_1^{(1)}, a_2^{(1)}, a_3^{(1)}\right ]^T$
  • $\mathbf{X} = \left [x_1, x_2 \right ]^T$
  • $\mathbf{B}^{(1)} = \left [ b_1^{(1)}, b_2^{(1)}, b_3^{(1)}\right ]^T$

이에 대한 구현은 다음과 같다.

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

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

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

이어서 1번째 Layer에서의 activation function의 처리를 살펴보자.

Weight W의 notation 중 ij의 순서를 바꾸면 된다.

Z1 = sigmoid(A1)
print(A1)
print(Z1)

# [0.3 0.7 1.1]
# [0.57444252 0.66818777 0.75026011]

이어서 1층에서 2층으로 가는 과정과 그 구현을 살펴보자.

$Z1$이 2nd Layer의 입력이 된다는 점을 제외하면 이전의 구현과 동일하다.

Weight W의 notation 중 ij의 순서를 바꾸면 된다.

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

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

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

마지막으로 2층에서 Output Layer으로의 신호 전달이다.

activation function만 linear function으로, 지금까지와 다르다.

Weight W의 notation 중 ij의 순서를 바꾸면 된다.

def identity_function(x):
    return x

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

A3 = np.dot(W3.T,Z2) + B3
Y = identity_function(A3)  # 혹은 Y = A3

💡
3.4.3) 구현 정리

이제 3개의 layer로 이루어진 Neural Network에 대한 설명은 끝났다.

지금까지의 구현을 코드로 정리하면 다음과 같다.

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]).T
    network["W2"] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
    network["b2"] = np.array([0.1, 0.2]).T
    network["W3"] = np.array([[0.1, 0.3], [0.2, 0.4]])
    network["b3"] = np.array([0.1, 0.2]).T
    
    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(W1.T, x) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(W2.T, z1) + b2
    z2 = sigmoid(a2)
    a3 = np.dot(W3.T, z2) + b3
    y = identity_function(a3)
    
    return y

network = init_network()
x = np.array([1.0, 0.5]).T
y = forward(network, x)
print(y)

# [0.31682708 0.69627909]

  • init_network: weight, bias를 초기화하고, newtork에 dictionary로 저장
  • forward: input 신호를 output으로 출력하는 처리과정을 모두 구현


3.5) 출력층 설계하기

Hidden Layer과 Output Layer Activation은 주로 다음과 같은 상황에서 사용된다.

  • Hidden Layer Activation: 사용하는 연산에 따라 결정 (MLP, CNN, RNN 등)
  • Output Layer Activation: 해결하는 문제(Task)에 따라 결정 (Regression, Classification)

💡
3.5.1) Identity Function과 Softmax Function 구현하기

  • identity function: input signal을 그대로 output signal로 사용
    • output layer의 $k$번째 node는 오직 $k$번째 input node에만 영향을 받음

$$y_k = a_k$$

  • softmax function: $e^x$ (exponential function)을 사용하여 각 class에 $\left [0, 1 \right]$ 사이의 weight 부여
    • output layer의 $k$번째 node는 모든 input node에 영향을 받음

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

  • $n$: output layer의 node 개수
  • $y_k$: k번째 node의 output
  • $a_k$: k번째 input signal

a = np.array([0.3, 2.9, 4.0])

exp_a = np.exp(a)
print(exp_a)  # [ 1.34985881 18.17414537 54.59815003]

sum_exp_a = np.sum(exp_a)
print(sum_exp_a)  # 74.1221542101633

y = exp_a / sum_exp_a
print(y)  # [0.01821127 0.24519181 0.73659691]

이제 위의 process를 바탕으로 softmax function을 구현해보자.

def softmax(a):
    return np.exp(a) / np.sum(np.exp(a))

💡
3.5.2) Caution for Softmax Function

softmax function을 사용할 때는, exponential function인 지수함수를 사용하기 때문에 매우 큰 값을 내받는 경우가 있다.

이때, 이러한 큰 값들끼리 나눗셈을 할 경우 overflow 문제가 발생한다.

이러한 문제를 해결하기 위해 아래와 같은 변형을 이용한다.

softmax의 특징 중 하나는, 지수함수 계산시 지수 안에 어떠한 정수를 더하거나 빼도 결과는 바뀌지 않는다는 것이다.


따라서 지수 안에 기존의 input $a_i$에 input의 maximum을 빼서 최종 지수 안에 넣음으로써, overflow를 방지할 수 있다.

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

overflow 문제를 실제 코드로 구현하면 다음과 같다.

a = np.array([1010, 1000, 990])
y = softmax(a)
print(y) # [nan nan nan]

c = np.max(a)
print(a - c) # [  0 -10 -20]

y = softmax(a - c)
print(y) # [9.99954600e-01 4.53978686e-05 2.06106005e-09]

이를 해결하는 softmax는 python으로 다음과 같이 구현가능하다.

def softmax(a):
    array = np.exp(a - np.max(a))
    return array / np.sum(array)

💡
3.5.3) Softmax function의 특징

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

softmax 함수의 총합은 1이고, 각 index는 0에서 1사이의 값을 가지기 때문에 이를 probability로 해석할 수 있다.

즉, Multi-Class Classification task에서는 $y$의 index=2에 있는 원소의 value가 73.7%로 가장 높기 때문에, 결국 2번째 class에 속하는 값이다라고 해석할 수 있다.

주의할 점은, softmax function을 사용해도 각 원소의 대소관계는 변하지 않는다는 것이다.


$e^x$ function이 단조 증가 함수이므로, $a$에서의 maximum 원소가 $y$에서의 maximum element와 동일하다.

이때, 일반적으로 가장 큰 value를 내는 원소에 해당하는 class로 classification이 이루어진다.

그리고 softmax를 적용해도 value의 대소관계는 바뀌지 않기에, classification task의 경우 computational cost를 줄이기 위해 output layer activation에서 softmax를 주로 생략하곤 한다.

여기서 softmax를 생략한다고 말하는 것은:


- training: output layer activation에 softmax 사용


- inference: output layer activation에 softmax 생략

💡
3.5.4) Output layer의 neuron 수 정하기

  • Multi-class Classification: Class의 개수
  • Binary Classification: 2개
  • Regression: 1개


3.6) 손글씨 숫자 인식

해당 section에서는 backpropagation을 이용한 training을 생략하고, inference만 구현한다.

이를 forward propagation이라고도 한다.

  • training: 학습 데이터를 사용하여 weight parameter를 학습
  • inference: trained weight을 이용하여 input data에 대해 작업 수행

💡
3.6.1) MNIST Dataset

MNIST Dataset은 사람이 쓴 손글씨 숫자 이미지 집합으로, 0부터 9까지의 숫자 이미지로 구성되어 있다.

Train data가 60,000장, Test data가 10,000장으로 준비되어 있으며, Train data로 모델을 학습하고, 학습한 모델을 Test data를 통해 성능을 평가한다.

각 이미지는 $28 \times 28$의 size이며, 각 pixel은 0부터 255까지의 값을 가진다.

아래의 코드는 MNIST Dataset을 다운받아 numpy배열로 변환해주는 코드이다.

import sys, os
sys.path.append(os.pardir) # 부모 디렉터리의 파일을 가져올 수 있도록 설정
from dataset.mnist import load_mnist

(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)

# 각 데이터의 형상 출력
print(x_train.shape) # (60000, 784)
print(t_train.shape) # (60000,)
print(x_test.shape) # (10000, 784)
print(t_test.shape) # (10000,)

  • x_train: train data
  • t_train: train data label
  • x_test: test data
  • t_test: test data label

아래는 load_mnist()함수의 parameter에 대한 설명이다.

normalize는 $[0, 255]$ 범위의 배열을 $[0, 1]$ 사의의 값으로 normalize 하는 지의 여부이고,

flatten은 False로 $1 \times 28 \times 28$을 할지, True로 $1 \times 784$로 할 지 결정한다.


one_hot_label은 label을 저장할 때 one-hot encoded 형태로 저장할 지, 숫자(0~9) 형태로 저장할지를 정한다.

그럼 이제, MNIST Dataset을 가져왔으니 시각화해서 확인을 해보자.

import sys, os

sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
from dataset.mnist import load_mnist
from PIL import Image


def img_show(img):
    pil_img = Image.fromarray(np.uint8(img))
    pil_img.show()


(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)

img = x_train[0]
label = t_train[0]
print(label)  # 5

print(img.shape)  # (784,)
img = img.reshape(28, 28)
print(img.shape)  # (28, 28)

img_show(img)

5와 같은 숫자의 형상을 확인할 수 있다.

💡
3.6.2) Neural Network의 Inference

이제 MNIST Dataset을 이용하여 Inference를 수행하는 Neural Network를 구현해보자.

  • Input Layer: 784개의 Node ($28 \times 28 = 784$)
  • Output Layer: 10개의 Node ($10$개의 Class: 0 ~ 9)
  • Hidden Layer: 1st는 50개, 2nd는 100개의 Node

pickle은 프로그램 실행 중에 특정 객체를 파일로 저장하는 기능이다.


즉, 저장한 pickle 파일을 로드하면 실행 당시의 객체를 즉시 복원할 수 있다.

해당 pickle 파일은 아래의 공식 github에서 찾을 수 있다.

GitHub - WegraLee/deep-learning-from-scratch: 『밑바닥부터 시작하는 딥러닝』(한빛미디어, 2017)
『밑바닥부터 시작하는 딥러닝』(한빛미디어, 2017). Contribute to WegraLee/deep-learning-from-scratch development by creating an account on GitHub.

import pickle


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"].T, network["b2"].T, network["b3"].T

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

    return y

그렇다면 이제 실제 trained neural network의 classification 성능을 측정하기 위해 아래의 코드를 작성하자.

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

accuracy_cnt = 0
for i in range(len(x)):
    y = predict(network, x[i])  # x[i] shape: (784,)
    p = np.argmax(y)
    if p == t[i]:
        accuracy_cnt += 1

print(f"Accuracy: {float(accuracy_cnt) / len(x)}")

Accuracy: 0.9352

이는 학습된 Neural Network의 분류 성능이 93.52%라는 것이다.

  • Preprocessing: Input Data에 특정 변환을 가하는 것
  • Normalization: Data를 특정 범위로 변환하는 처리

위의 Neural Network는 Image Data에 대한 preprocessing으로 Normalization을 수행한 것이다.

💡
3.6.3) Batch 처리

이전의 코드에서 사용한 가중치들의 shape을 살펴보자.

x, _ = get_data()
network = init_network()
W1, W2, W3 = network["W1"], network["W2"], network["W3"]
print(x.shape)  # (10000, 784)
print(W1.shape)  # (784, 50)
print(W2.shape)  # (50, 100)
print(W3.shape)  # (100, 10)

이때 최종적으로 $y$는 $(10000, 10)$의 shape을 갖고, 그건 하나로 묶인 input data의 단위를 batch라고 하는데, batch단위로 데이터를 처리하기 때문이다.

batch 처리까지 반영한 코드는 아래와 같다.

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

batch_size = 100  # 배치 크기
accuracy_cnt = 0

for i in range(0, len(x), batch_size):
    # for loop 안은 하나의 batch에 대한 처리
    x_batch = x[i : i + batch_size]  # (100, 784)
    y_batch = np.array(
        [predict(network, x[j]) for j in range(i, i + batch_size)]
    )  # (100, 10)
    p = np.argmax(y_batch, axis=1)  # (100,)
    accuracy_cnt += np.sum(p == t[i : i + batch_size])  # True: 1, False: 0

print(f"Accuracy: {float(accuracy_cnt) / len(x)}")

for loop안은 하나의 batch에 대한 처리로 해석하자.

이처럼 데이터를 batch로 묶어서 처리하면 효율적이고 빠르게 처리할 수 있다.