[밑딥] 3장. 신경망

Speedwell🍀·2022년 4월 10일
0
post-thumbnail

앞 장에서 퍼셉트론으로 복잡한 함수도 표현이 가능해서, 예시로 컴퓨터가 수행하는 복잡한 처리도 이론상 표현할 수 있다고 했다.

🤔 하지만 원하는 결과를 출력하도록 가중치 값을 설정하는 작업은 여전히 사람이 한다. (앞 장에서 AND, OR 게이트의 진리표를 보면서 사람이 적절한 가중치 값을 정하는 것을 얘기했었다.)
➡️ 신경망이 이를 해결 가능!


신경망은 가중치 매개변수의 적절한 값을 데이터로부터 자동으로 학습하는 능력이 있다.

이번 장에서는 신경망의 개요신경망이 입력을 식별하는 처리 과정을 알아본다.

데이터에서 가중치 매개변수 값을 학습하는 방법은 다음 장에서 다룬다.


1. 퍼셉트론에서 신경망으로

신경망과 퍼셉트론은 공통점이 많다.
퍼셉트론과의 차이점을 중심으로 신경망의 구조를 보자!!

1-1) 신경망의 예

신경망에는 입력층(Input layer), 은닉층(Hidden layer), 출력층(Output layer)가 있다.

입력층/출력층과 달리 은닉층의 뉴런은 사람에게는 보이지 않는다.

입력층에서 출력층 방향으로 0층, 1층, 2층이라고 해보자. (파이썬 배열 인덱스도 0부터 시작하니까 짝짓기 좋음)
➡️ 0층: 입력층, 1층: 은닉층, 2층: 출력층

3개의 층으로 이루어진 신경망이지만 가중치를 갖는 층은 2개뿐이라 2층 신경망이다.
📌 가중치를 갖는 층의 개수 = 입력층 + 출력층 + 은닉층 - 1


🤔 뉴런이 연결되는 방식은 앞장의 퍼셉트론과 동일한데, 신경망에서는 신호를 어떻게 전달할까?



1-2) 퍼셉트론 복습

두 신호 x₁, x₂를 입력받아 y를 출력하는 퍼셉트론을 살펴보자. 이 퍼셉트론을 수식으로 나타내면 아래와 같다.

  • 편향 b: 뉴런이 얼마나 쉽게 활성화되는지 제어
  • 가중치 w: 각 신호의 영향력을 제어

편향을 명시한 퍼셉트론은 아래와 같다.

처음에 봤던 구조에서 가중치가 b이고 입력이 1인 뉴런이 추가되었다. (편향의 입력 신호는 항상 1)



1-3) 활성화 함수의 등장

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


위에서 본 퍼셉트론 수식을 더 간결한 형태로 작성해보면 아래와 같다.

조건 분기의 동작 (0을 넘으면 1 출력, 아니면 0 출력)을 하나의 함수인 h(x)로 나타냈다.

입력 신호의 총합이 h(x)를 거쳐 변환된 값이 y의 출력이 된다.


위의 y식을 아래처럼 2개로 쪼개보자.

a = b + w₁w₂ =+ x₁w₂
y = h(a)

➡️ 가중치가 달린 입력 신호와 편향의 총합을 계산해서 a에 저장하고, a를 함수 h()에 넣어 y를 출력한다!


이러한 활성화 함수의 처리 과정을 그려보면 아래와 같다.

기존 뉴런의 원을 키우고 그 안에 활성화 함수의 처리 과정
➡️ 가중치 신호를 조합한 결과가 a 노드가 되고, 활성화 함수 h()를 통과하여 y 노드로 변환

뉴런노드 같은 의미로 사용 (위 그림에서 원)


단층 퍼셉트론: 단층 네트워크에서 계단 함수 (임계값을 경계로 출력이 바뀌는 함수)를 활성화 함수로 사용한 모델
다층 퍼셉트론: 신경망(여러 층으로 구성되고 시그모이드 함수 등의 매끈한 활성화 함수를 사용하는 네트워크)


2. 활성화 함수 (Activation Function)

위에서 본 활성화 함수는 임계값을 경계로 출력이 바뀌는 계단 함수(step function)이었다.

📌 퍼셉트론에서는 활성화 함수로 계단 함수를 이용한다!

🤔 활성화 함수를 계단 함수에서 다른 함수로 변경하면?
➡️ 신경망으로 나아가는 열쇠!


2-1) 시그모이드 함수 (Sigmoid Function)

신경망에서는 활성화 함수로 시그모이드!
➡️ 시그모이드를 이용해 신호를 변환하고, 변환된 신호를 다음 뉴런에 전달

📌 앞 장에서 본 퍼셉트론과 이번 장의 신경망의 주된 차이는 활성화 함수뿐!!
뉴런이 여러 층으로 이어지는 구조와 신호를 전달하는 방법은 둘 다 동일!



2-2) 계단 함수 구현

입력이 0을 넘으면 1을 출력하고, 그 외에는 0을 출력하는 계단 함수를 파이썬으로 구현해보자!

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

단순하고 쉽지만 인수 x는 실수(부동소수점)만 받아들인다.
➡️ 넘파이 배열을 인수로 넣을 수 없다.


넘파이 배열도 지원하도록 구현해보자!!

def step_function(x):
	y = x > 0
    return y.astype(np.int)
>>> import numpy as np
>>> x = np.array([-1.0, 1.0, 2.0])
>>> x
array([-1.,  1.,  2.])
>>> y = x > 0
>>> y
array([False, True, True], dtype=bool)

넘파이 배열에 부등호 연산을 수행하면 배열의 원소 각각에 부등호 연산을 수행한 bool 배열이 생성된다.

위의 코드에서는 배열 x의 원소 각각이 0보다 크면 True, 0 이하면 False로 변환된 배열 y가 생성된다.


우리가 원하는 계단 함수는 bool형이 아닌, 0이나 1의 int형을 출력하는 함수!

int형으로 바꿔주자!

>>> y = y.astype(np.int)
>>> y
array([0, 1, 1])

📌 넘파이 배열의 자료형을 변환할 때는 astype() 메서드 이용! 괄호 안에 np.int와 같이 원하는 자료형을 넣어주면 된다.


계단 함수의 그래프

앞에서 정의한 계단 함수를 matplotlib 라이브러리를 사용해 그래프로 그려보자!

import numpy as np
import matplotlib.pylab as plt

def step_function(x):
	return np.array(x > 0, dtype-np.int)

x = np.arange(-5.0, 5.0, 0.1)
y = step_function(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1) # y축의 범위 지정
plt.show()

np.arange(-5.0, 5.0, 0.1)은 -5.0에서 5.0 전까지 0.1 간격의 넘파이 배열을 생성한다.
➡️ [-5.0, -4.0, ..., 4.9]

step_function()은 인수로 받은 넘파이 배열의 원소 각각을 인수로 계단 함수를 실행한 결과를 다시 배열로 만들어 돌려준다.

➡️ 0을 경계로 출력이 0에서 1 (또는 1에서 0)로 바뀐다.


2-3) 시그모이드 함수 구현

def sigmoid(x):
	return 1 / (1 + np.exp(-x))
>>> x = np.array([-1.0, 1.0, 2.0])
>>> sigmoid(x)
array([ 0.26894142,  0.73105858,  0.88079708])

시그모이드 함수의 그래프

앞에서 작성한 계단 함수 그래프 코드에서 y를 출력하는 함수만 sigmoid로 변경해주면 된다!

x = np.arange(-5.0, 5.0, 0.1)
y = sigmoid(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1) # y축 범위 지정
plt.show()



2-4) 시그모이드 함수 vs. 계단 함수

차이점

"매끄러움의 차이"

  • 계단 함수는 0을 경계로 출력이 갑자기 바뀌는 반면, 시그모이드 함수는 부드러운 곡선이며 입력에 따라 출력이 연속적으로 변화한다.
    ➡️ 시그모이드의 이 매끈함이 신경망 학습에서 아주 중요한 역할!

  • 계단 함수는 0과 1 중 하나의 값만 돌려주는 반면 시그모이드 함수는 실수를 돌려준다.
    ➡️ 퍼셉트론에서는 뉴런 사이에 0 혹은 1이 흘렀다면, 신경망에서는 연속적인 실수가 흐른다.


공통점

큰 관점에서 보면 같은 모양
(입력이 작을 때의 출력은 0에 가깝거나 0이고, 입력이 커지면 출력이 1에 가깝거나 1이 되는 구조)

  • 입력이 중요하면 큰 값을 출력하고, 입력이 중요하지 않으면 작은 값을 출력한다.
  • 입력이 아무리 작거나 커도 출력은 0~1
  • 비선형 함수


2-5) 비선형 함수

계단 함수와 시그모이드 함수의 중요한 공통점은 비선형 함수

선형 함수: 무언가 입력했을 때 출력이 입력의 상수배만큼 변하는 함수
비선형 함수: 직선 1개로는 그릴 수 없는 함수

📌 신경망에서는 활성화 함수로 비선형 함수를 사용해야 한다.

선형 함수를 쓰게 되면 신경망의 층을 깊게 하는 의미가 없어진다.
👎선형 함수의 문제는 층을 아무리 깊게 해도 은닉층이 없는 네트워크로도 똑같은 기능을 할 수 있다는 점이다.

예를 들어, h(x) = cx를 활성화 함수로 사용한 3층 네트워크는 y(x)=h(h(h(x)))가 된다. 계산하면 y(x)=c*c*c*x이 되는데, 이는 y(x)=ax와 똑같은 식이다.
➡️ 은닉층이 없는 네트워크로 표현 가능하다.
➡️ 층을 쌓는 혜택을 얻고 싶다면 활성화 함수로 반드시 비선형 함수를 써야 한다.



2-6) ReLU 함수

신경망에서 활성화 함수로 시그모이드 함수를 오래 이용해왔으나, 이제는 ReLU(Rectified Linear Unit) 함수를 주로 이용한다.


ReLU는 입력이 0을 넘으면 그 입력을 그대로 출력하고, 0 이하이면 0을 출력한다.

수식으로 나타내면 아래와 같다.


ReLU 함수 구현

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


3. 다차원 배열의 계산

넘파이의 다차원 배열을 사용한 계산법을 숙달하면 신경망을 효율적으로 구현할 수 있다!

3-1) 다차원 배열

'숫자의 집합'

  • 숫자가 한 줄로 늘어선 것, 직사각형으로 늘어놓은 것, 3차원으로 늘어놓은 것, N차원으로 나열하는 것을 통틀어 다차원 배열
>>> import numpy as np
>>> A = np.array([1, 2, 3, 4])
>>> print(A)
[1 2 3 4]
>>> np.ndim(A)
1
>>> A.shape
(4,)
>>> A.shape[0]
4
  • np.ndim(): 배열의 차원 수
  • shape: 배열의 형상
    • 📌튜플을 반환하는 것 주의
      ➡️ 1차원 배열이라도 다차원 배열일 때와 동일한 형태로 결과를 반환하기 위함
      • ex) 2차원 배열 (4, 3), 3차원 배열 (4, 3, 2)

3-2) 행렬의 곱

>>> A = np.array([[1,2], [3,4]])
>>> A.shape
(2, 2)
>>> B = np.array([[5,6], [7,8]])
>>> B.shape
(2, 2)
>>> np.dot(A, B)
array([[19, 22],
	   [43, 50]])

📌두 행렬의 곱은 넘파이 함수 np.dot()으로 계산한다.

입력이 1차원 배열이면 벡터를, 2차원 배열이면 행렬 곱을 계산한다.
np.dot(A, B) ≠ np.dot(B, A) 주의하기



만약 차원의 원소 수를 일치시키지 않으면 아래처럼 오류가 발생한다.

>>> C = np.array([[1,2], [3,4]])
>>> C.shape
(2, 2)
>>> A.shape
(2, 3)
>>> np.dot(A, C)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: shapes (2,3) and (2,2) not aligned: 3 (dim 1) != 2 (dim 0)

3-3) 신경망에서의 행렬 곱

넘파이 행렬을 써서 신경망을 구현해보자.
이번 예시에서는 편향과 활성화 함수를 생략하고 가중치만 갖는 간단한 신경망이라고 가정하자!

📌 X와 W의 대응하는 차원의 원소 수가 같아야 한다!!

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

다차원 배열의 스칼라곱을 구해주는 np.dot 함수를 사용하면 Y의 원소의 개수에 상관 없이 한 번의 연산으로 계산할 수 있다!
➡️ 행렬의 곱으로 한꺼번에 계산해주는 기능은 신경망을 구현할 때 매우 중요!



4. 3층 신경망 구현

⭐ 이번 절의 핵심은 신경망에서의 계산을 행렬 계산으로 정리할 수 있다는 것!

위에서 본 간단한 신경망에서, 이번엔 3층 신경망에서 수행되는 입력부터 출력까지의 처리(순방향 처리)를 구현해보자!
➡️ 이를 위해 넘파이의 다차원 배열을 사용한다.

3층 신경망: 입력층(0층)은 2개, 첫 번째 은닉층(1층)은 3개, 두 번째 은닉층(2층)은 2개, 출력층(3층)은 2개의 뉴런으로 구성된다.


4-1) 각 층의 신호 전달 구현

입력층에서 1층의 첫 번째 뉴런으로 가는 신호를 살펴보자.

편향을 뜻하는 뉴런이 추가되었다. 편향은 오른쪽 아래 인덱스가 하나밖에 없다는 것 주의하기! (앞 층의 편향 뉴런이 하나뿐이기 때문)


아래는 가중치를 곱한 신호 두 개와 편향을 합한 것이다.

여기에서 행렬의 곱을 이용하면 1층의 '가중치 부분'을 아래 식처럼 간소화할 수 있다.

이때 각각의 행렬은 아래와 같다.


이제 넘파이의 다차원 배열을 사용해 위의 식을 구현해보자!
(입력 신호, 가중치, 편향은 임의의 적당한 값으로 설정했다.)

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.66818777, 0.75026011]

이어서 1층에서 2층으로 가는 과정을 구현해보자!

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)

1층의 출력 Z1이 2층의 입력이 된다는 점만 제외하면 이전의 구현과 똑같다.

이처럼 넘파이 배열을 사용하면서 층 사이의 신호 전달을 쉽게 구현할 수 있다.


마지막으로 2층에서 출력층으로의 신호 전달을 구현해보자!
출력층의 구현은 활성화 함수만 지금까지의 은닉층과 다르다!

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) # 혹은 Y = A3

출력층의 활성화 함수로 항등 함수인 identity_function()을 사용했다.
(항등함수: 입력을 그대로 출력하는 함수)

위의 그림에서는 출력층의 활성화 함수를 σ()로 표시하여 은닉층의 활성화 함수 h()와는 다름을 명시했다.

출력층의 활성화 함수는 풀고자 하는 문제의 성질에 맞게 정한다. (뒤에서 더 다룰 예정)

  • 회귀에는 항등 함수
  • 2클래스 분류에는 시그모이드 함수
  • 다중 클래스 분류에는 소프트맥수 함수

4-2) 구현 정리

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(): 입력 신호를 출력으로 변환하는 처리 과정을 모두 구현
    • 신호가 순방향(입력→출력)으로 전달됨 (순전파)


5. 출력층 설계하기

신경망은 분류(classification)회귀(regression) 모두에 이용할 수 있다.

하지만 출력층에서 사용하는 활성화 함수는 다르다.
➡️ 일반적으로 회귀에는 항등 함수, 분류에는 소프트맥스 함수를 사용한다.

기계학습 문제는 분류와 회귀로 나뉜다.

  • 분류: 데이터가 어느 클래스에 속하는지의 문제
  • 회귀: 입력 데이터에서 (연속적인) 수치를 예측하는 문제

5-1) 항등 함수와 소프트맥스 함수 구현

  • 항등 함수(identity function): 입력을 그대로 출력
    ➡️ 출력층에서 항등 함수를 사용하면 입력 신호가 그대로 출력 신호가 된다.

  • 소프트맥스 함수(softmax function)
    • 분자: 입력 신호 a_k의 지수 함수
    • 분모: 모든 입력 신호의 지수 함수의 합
      ➡️ 출력층의 각 뉴런이 모든 입력 신호에서 영향을 받는다.

(n은 출력층의 뉴런 수, y_k는k번째 출력)

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

소프트맥스 함수 표현 시 주의점

앞서 구현한 softmax() 코드는 컴퓨터로 계산할 때 오버플로가 발생할 수 있는 문제점이 있다.

소프트맥스 함수는 지수 함수를 사용하기 때문에 값이 쉽게 커진다.
➡️ 큰 값끼리 나눗셈을 하면 결과 수치가 불안정해진다.


이 문제를 해결하기 위해 소프트맥스 함수 구현을 개선해보자!

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

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

소프트맥스 함수의 특징

  • 출력은 0에서 1.0 사이의 실수
  • 출력의 총합은 1
    ➡️ 소프트맥스 함수의 출력을 확률로 해석 가능!

즉, 소프트맥스 함수를 이용함으로써 문제를 확률적/통계적으로 대응할 수 있게 되었다!

주의할 점은 소프트맥스 함수를 적용해도 각 원소의 대소 관계는 변하지 않는다는 점이다.
(지수 함수 y=exp(x)가 단조 증가 함수이기 때문)

기계학습의 문제 풀이는 학습추론(inference)의 두 단계를 거쳐 이뤄진다.
학습 단계에서 모델을 학습하고, 추론 단계에서 앞서 학습한 모델로 미지의 데이터에 대해 추론(분류)을 수행한다.

신경망으로 분류(추론)할 때는 출력층의 소프트맥스 함수를 생략해도 된다.
(현업에서도 지수 함수 계산에 드는 자원 낭비를 줄이고자 생략하는 것이 일반적)
하지만 신경망을 학습시킬 때는 출력층에서 소프트맥스 함수를 사용한다.


5-2) 출력층의 뉴런 수 정하기

출력층의 뉴런 수는 풀려는 문제에 맞게 적절히 정해야 한다.

📌 분류에서는 분류하고 싶은 클래스 수로 설정!



6. 손글씨 숫자 인식

이미 학습된 매개변수를 사용하여 학습 과정은 생략하고, 추론 과정만 구현해보자!
➡️ 이 추론 과정을 신경망의 순전파(forward propagation라고도 한다.

기계학습과 마찬가지로 신경망도 두 단계를 거쳐 문제를 해결한다.
먼저 훈련 데이터(학습 데이터)를 사용해 가중치 매개변수를 학습하고, 추론 단계에서는 앞서 학습한 매개변수를 사용하여 입력 데이터를 분류한다.


6-1) MNIST 데이터셋

MNIST는 기계학습 분야에서 아주 유명한 데이터셋!

  • 0~9까지의 숫자 이미지로 구성

  • 훈련 이미지 60,000장, 시험 이미지 10,000장
    ➡️ 일반적으로 이 훈련 이미지들을 사용하여 모델을 학습하고, 학습한 모델로 시험 이미지들을 얼마나 정확하게 분류하는지를 평가

  • 이미지 데이터는 28x28 크기의 회색조 이미지(1채널)

    • 각 픽셀은 0~255
  • 각 이미지에는 '1', '2' 같이 그 이미지가 실제 의미하는 숫자가 레이블로 붙어 있다.


이 책에서는 MNIST 데이터셋을 내려받아 이미지를 넘파이 배열로 변환해주는 파이썬 스크립트를 제공한다. (github에서 dataset/mnist.py)
mnist.py를 임포트해서 사용하려면 작업 디렉터리를 ch01, ch02, ..., ch08 중 하나로 옮기면 된다.
mnist.py에 정의된 load_mnist() 함수를 이용하면 MNIST 데이터를 쉽게 가져올 수 있다.

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,)

sys.path는 파이썬 라이브러리들이 설치되어 있는 디렉터리 리스트
os.pardir는 현재 디렉터리의 부모 디렉터리

mnist.py 파일은 dataset 디렉터리에 있고, 이 파일을 이용하는 다른 예제들은 각각 ch01, ch02, ..., ch08 디렉터리에서만 수행한다고 가정한다. 즉, 각 예제에서 mnist.py 파일을 찾으려면 부모 디렉터리로부터 시작해야 해서 sys.path.append(os.pardir)를 추가한 것이다.

load_mnist 최초 실행 시에는 MNIST 데이터를 받아와야 하기 때문에 인터넷에 연결된 상태여야 한다. 이후부터는 로컬에 저장된 파일(pickle 파일)을 읽기 때문에 순식간에 실행된다.

pickle: 프로그램 실행 주에 특정 객체를 파일로 저장하는 파이썬의 편리한 기능
저장해둔 pickle 파일을 로드하면 실행 당시의 객체를 즉시 복원할 수 있다.


load_mnist 함수는 읽은 MNIST 데이터를 (훈련 이미지, 훈련 레이블), (시험 이미지, 시험 레이블) 형식으로 반환한다.

인수로는 normalize, flatten, one_hot_label 세 가지를 설정할 수 있다. 세 인수 모두 bool 값.

  1. normalize: 입력 이미지의 픽셀 값을 0.0~1.0 사이의 값으로 정규화할지 정한다.

    • False로 설정하면 입력 이미지의 픽셀은 원래 값 그대로 0~255 사이의 값을 유지한다.
  2. flatten: 입력 이미지를 1차원 배열로 만들지 정한다.

    • False로 설정하면 입력 이미지를 1x28x28의 3차원 배열로 저장
    • True로 설정하면 784개의 원소로 이뤄진 1차원 배열로 저장
  3. one_hot_label: 레이블을 one-hot encoding 형태로 저장할지를 저장한다.

    • False로 설정하면 '7'이나 '2'와 같이 숫자 형태로 레이블을 저장
    • True로 설정하면 레이블을 원-핫 인코딩하여 저장

원-핫 인코딩(one-hot encoding): [0,0,1,0,0,0,0,0,0,0,]처럼 정답을 뜻하는 원소만 1이고(hot하고) 나머지는 모두 0인 배열


MNIST 이미지를 화면으로 불러와보자.

이미지 표시에는 PIL(Python Image Library 모듈을 사용한다.

ch03/mnist_show.py

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.unit8(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.shpae) # (28, 28)

img_show(img)

📌 이미지를 표시할 때는 원래 형상인 28x28 크기로 다시 변형해줘야 한다는 것에 주의하자. ➡️ reshape()
flatten=True로 설정해 읽어 들인 이미지는 1차원 넘파이 배열로 저장되어 있기 때문이다.

📌 넘파이로 저장된 이미지 데이터를 PIL용 데이터 객체로 변환해야 한다. ➡️ Image.fromarray()


6-2) 신경망의 추론 처리

MNIST 데이터셋을 가지고 추론을 수행하는 신경망을 구현해보자.

이 신경망은 입력층 뉴런 784개, 출력층 뉴런 10개로 구성한다.

입력층 뉴런이 784개인 이유는 이미지 크기가 28x28이기 때문이다.
출력층 뉴런이 10개인 이유는 이 문제가 0에서 9까지의 숫자를 구분하는 문제이기 때문이다.

은닉층은 총 두 개 (여기서 50, 100은 임의로 정한 값)

  • 첫 번째 은닉층: 50개의 뉴런을 배치
  • 두 번째 은닉층: 100개의 뉴런을 배치
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, W3) + b3
    y = softmax(a3)
    
    return y

init_network()에서는 pickle 파일인 sample_weight.pkl에 저장된 '학습된 가중치 매개변수'를 읽는다. 이 파일에는 가중치와 편향 매개변수가 딕셔너리 변수로 저장되어 있다.


이제 위에서 정의한 세 함수를 사용해 신경망에 의한 추론을 수행해보고, 정확도(분류가 얼마나 올바른가)도 평가해보자!

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

코드 설명)

  1. 먼저 MNIST 데이터셋을 얻고 네트워크를 생성한다.

  2. for문을 돌며 x에 저장된 이미지 데이터를 1장씩 꺼내 predict() 함수로 분류한다.

    • predict() 함수는 각 레이블의 확률을 넘파이 배열로 반환
      • [0.1, 0.3, 0.2, ..., 0.04] 같은 배열이 반환되면, 이는 이미지가 숫자 '0'일 확률이 0.1, '1'일 확률이 0.3 ... 으로 해석한다.
  3. np.argmax() 함수로 이 배열에서 값이 가장 큰 (확률이 가장 높은) 원소의 인덱스를 구한다. ➡️ 예측 결과를 의미

    • np.argmax(): 주어진 numpy 배열에서 가장 높은 값을 가진 원소의 인덱스를 반환
  4. 신경망이 예측한 답변과 정답 레이블을 비교하여 맞힌 숫자(accuracy_cnt)를 세고, 이를 전체 이미지 숫자로 나눠 정확도를 구한다.


이 코드를 실행하면 정확도가 0.9352라고 출력된다.
😆 다음 장부터 신경망 구조와 학습 방법을 궁리하여 정확도를 더 높여나갈 것! 99% 이상까지 만들어보자!


여기에서는 load_mnist 함수의 인수인 narmalize를 True로 설정했다.
➡️ 입력 이미지 데이터에 대한 전처리 작업으로 정규화를 수행한 것이다.

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

식별 능력 개선, 학습 속도 향상 등을 위해 현업에서도 신경망(딥러닝)에 전처리를 활발히 사용한다.
방금 본 예시에서는 각 픽셀의 값을 255로 나누는 단순한 정규화를 수행했지만, 현업에서는 데이터 전체의 분포를 고려해 전처리하는 경우가 많다. 예시를 살펴보자.

  • 데이터 전체 평균과 표준편차를 이용해 데이터들이 0을 중심으로 분포하도록 이동
  • 데이터 확산 범위를 제한하는 정규화
  • 전체 데이터를 균일하게 분포시키는 데이터 '백색화(whitening)'

6-3) 배치 처리

앞의 구현을 입력 데이터와 가중치 매개변수의 형상에 주의해서 다시 살펴보자.

전체적으로 보면 원소 784개로 구성된 1차원 배열(원래는 28x28인 2차원 배열)이 입력되어 마지막에는 원소가 10개인 1차원 배열이 출력되는 흐름이다.
❗이는 데이터를 1장만 입력했을 때의 처리 흐름


❓이미지 여러 장을 한꺼번에 입력하는 경우에는?

예시로 이미지 100개를 묶어 predict() 함수에 한 번에 넘기는 상황을 생각해보자.
100장 분량의 데이터를 하나의 입력 데이터로 표현하면 된다.

입력 데이터의 형상은 100x784, 출력 데이터의 형상은 100x10이 된다.
➡️ 100장 분량의 입력 데이터의 결과가 한 번에 출력된다.

📌 이렇게 하나로 묶은 입력 데이터를 배치(batch)라고 한다.

배치 처리는 컴퓨터로 계산할 때 큰 이점! ➡️ 이미지 1장당 처리 시간을 대폭 줄여준다.
이유1) 수치 계산 라이브러리 대부분이 큰 배열을 효율적으로 처리할 수 있도록 고도로 최적화되어 있다.
이유2) 배치 처리를 함으로써 버스에 주는 부하를 줄인다.

즉, 배치 처리를 수행함으로써 큰 배열로 이뤄진 계산을 하게 되는데, 컴퓨터에서는 큰 배열을 한꺼번에 계산하는 것이 분할된 작은 배열을 여러 번 계산하는 것보다 빠르다.


배치 처리를 구현해보자!

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)
    accuray_cnt += np.sum(p == t[i:i+batch_size])

print("Accuracy:" + str(float(accuracy_cnt) / len(x)))
  • range() 함수가 반환하는 반복자를 바탕으로 x[i:i+batch_size]에서 입력 데이터를 묶는다.
    ➡️ 입력 데이터의 i번째부터 i+batch_size번째까지의 데이터를 묶는다는 의미!
    (x[0:100], x[100:200], ...)

  • argmax()에서 axis=1이라는 인수가 추가된 것에 주의하자.
    ➡️ 100x10의 배열 중 1번째 차원을 구성하는 각 원소에서 최댓값의 인덱스를 찾도록 한 것!


마지막으로 배치 단위로 분류한 결과를 실제 답과 비교해보자.
📌 == 연산자를 사용해 넘파이 배열끼리 비교하여 True/False로 구성된 bool 배열을 만들고, 이 결과 배열에서 True가 몇 개인지 세면 된다!

아래는 처리 과정에 대한 간단한 예시이다.

>>> y = np.array([1, 2, 1, 0])
>>> t = np.array([1, 2, 0, 0])
>>> print(y==t)
[True True False True]
>>> np.sum(y==t)
3

7. 정리

신경망의 순전파에 대해 알아봤다.

신경망은 각 층의 뉴런들이 다음 층의 뉴런으로 전달한다는 점에서 퍼셉트론과 공통점이 있었다.

하지만 다음 뉴런으로 갈 때 신호를 변화시키는 활성화 함수에는 큰 차이점이 있었다.
신경망에서는 시그모이드 함수, 퍼셉트론에서는 활성화 함수를 사용한다.

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

0개의 댓글