본문 바로가기
Study/책『밑바닥부터 시작하는 딥러닝』

3-2. 신경망

by 코드포휴먼 2020. 3. 9.

3.4. 3층 신경망 구현하기

3층 신경망에서 수행되는, 입력부터의 출력까지의 처리(순방향 처리)를 구현하겠다.

넘파이의 다차원 배열을 사용한다.

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

 

 

3.4.1. 표기법 설명

표기

 

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

입력층에서 '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층의 활성화 함수에서의 처리를 살펴보겠다. 

입력층에서 1층으로의 신호 전달

은닉층에서의 가중치 합(가중 신호와 편향의 총합)을 a로 표기하고 활성화 함수 h()로 변환된 신호를 z로 표기한다.

여기에서는 활성화 함수로 시그모이드 함수를 사용하기로 한다. 

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

Z1 = sigmoid(A1)
print(A1)   #[0.3 0.7 1.1]
print(Z1)   #[0.57444252 0.66818777 0.75026011]

 

이어서 1층에서 2층으로 가는 과정과 구현을 보겠다.

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층에서 출력층으로의 신호 전달

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

(σ는 '시그마'라고 읽는다.)

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()을 정의하고, 이를 출력층의 활성화 함수로 이용했다.

항등 함수는 입력을 그대로 출력하는 함수다. 

이 예에서는 identity_function()을 굳이 정의할 필요는 없지만, 그동안의 흐름과 통일했다. 

 

더보기

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

예를 들어 회귀에는 항등 함수를, 2클래스 분류에는 시그모이드 함수를, 다중 클래스 분류에는 소프트맥스 함수를 사용하는 것이 일반적이다. 

 

 

3.4.3. 구현 정리

지금까지의 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()와 forward()라는 함수를 정의했다.

init_network() 함수는 가중치와 편향을 초기화하고 이들을 딕셔너리 변수인 network에 저장한다.

이 딕셔너리 변수 network에는 각 층에 필요한 매개변수(가중치와 편향)를 저장한다.

forward() 함수는 입력 신호를 출력으로 변환하는 처리 과정을 모두 구현하고 있다.

함수 이름을 forward라 한 것은 신호가 순방향(입력에서 출력 방향)으로 전달됨(순전파)을 알리기 위함이다. 

넘파이의 다차원 배열을 잘 사용하면 신경망을 효율적으로 구현할 수 있다.

 

 

3.5. 출력층 설계하기

신경망은 분류와 회귀 모두에 이용할 수 있다. 

일반적으로 회귀에는 항등 함수를, 분류에는 소프트맥스 함수를 사용한다.

 

3.5.1. 항등 함수와 소프트맥스 함수 구현하기

항등 함수(identity function)는 입력을 그대로 출력한다. 입력과 출력이 항상 같다는 뜻의 항등이다.

항등 함수

 

분류에서 사용하는 소프트맥스 함수(softmax function)의 식은 다음과 같다.

소프트맥스 함수 식

exp(x)는 e의 x제곱을 뜻하는 지수함수(exponential function)이다(e는 자연상수).

더보기
지수함수 그래프

n은 출력층의 뉴런 수, yk는 그중 k번째 출력임을 뜻한다. 

소프트맥스 함수의 분자는 입력 신호 ak의 지수 함수, 분모는 모든 입력 신호의 지수 함수의 합으로 구성된다. 

 

소프트맥스 함수를 그림으로 나타내면 아래와 같다. 

소프트맥스의 출력은 모든 입력 신호로부터 화살표를 받는다. 

식의 분모에서 확인했듯이 출력층의 각 뉴런이 모든 입력 신호에서 영향을 받기 때문이다.

 

소프트맥스 함수를 구현해본다.

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]

 

위의 논리 흐름을 파이썬 함수로 정의해서 나중에 활용하겠다.

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

 

 

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

앞 절에서 구현한 sofmax() 함수는 컴퓨터 계산 시 결함이 발생한다. 바로 오버플로 문제다.

소프트맥스 함수는 지수 함수를 사용하는데, 지수 함수는 쉽게 아주 큰 값을 반환한다.

가령 e의 10제곱은 20000이 넘고, e의 100제곱은 0이 40개 이상이다. e의 1000제곱은 무한대 inf가 되어 돌아온다.

이런 큰 값끼리 나눗셈을 하면 결과 수치가 불안정해진다. 

더보기

컴퓨터는 수(number)를 4바이트나 8바이트와 같이 크기가 유한한 데이터로 다룬다. 표현 범위가 한정되어 있어서 너무 큰 값은 표현할 수 없다. 이것을 오버플로(overflow)라고 하며, 컴퓨터로 수치를 계산할 때 주의할 점이다.

 

오버플로 문제를 해결하기 위해 소프트맥스 함수 구현을 개선하면 다음 수식이 된다.

소프트 맥수 함수식 개선

첫 번째 변형에서는 C라는 임의의 정수를 분자와 분모 양쪽에 곱했다.

그 다음으로 C를 지수 함수 exp() 안으로 옮겨 logC로 만든다. 마지막으로 logC를 C' 라는 새로운 기호로 바꾼다.

개선된 수식이 말하는 것은 소프트맥스의 지수 함수를 계산할 때 어떤 정수를 더해도 결과는 바뀌지 않는다는 것이다.

여기서 C' 에 어떤 값을 대입해도 상관없지만, 오버플로를 막을 목적으로는 입력 신호 중 최댓값을 이용하는 것이 일반적이다.

 

아래는 예시다.

a = np.array([1010, 1000, 990])
print( np.exp(a) / np.sum(np.exp(a)) )   #[nan nan nan]   소프트맥스 함수의 계산이 제대로 되지 않는다.

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

print( np.exp(a - c) / np.sum(np.exp(a - c)) )   #[9.99954600e-01 4.53978686e-05 2.06106005e-09]

overflow 오류

아무런 조치 없이 그냥 계산하면 nan이 출력된다(nan은 not a number의 약자).

하지만 입력 신호 중 최댓값(이 예에서는 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

 

 

3.5.3. 소프트맥스 함수의 특징

softmax() 함수를 사용하면 신경망의 출력은 다음과 같이 계산할 수 있다.

#softmax() 함수를 사용한 신경망 출력
a = np.array([0.3, 2.9, 4.0])
y = softmax(a)
print(y)   #[0.01821127 0.24519181 0.73659691]
np.sum(y)   #1.0

소프트맥스 함수의 출력0에서 1.0 사이의 실수다.

또, 소프트맥스 함수 출력의 총합1 이다.

이 성질 덕분에 소프트맥스 함수의 출력을 "확률"로 해석할 수 있다.

 

앞의 예에서 y[0]의 확률은 0.018(1.8%), y[1]의 확률은 0.245(24.5%), y[2]의 확률은 0.737(73.7%)로 해석할 수 있다. 

그리고 이 결과 확률들로부터 "2번째 원소 y[2]의 확률이 가장 높으니, 답은 2번째 클래스다"라고 할 수 있다.

혹은 "74%의 확률로 2번째 클래스, 25%의 확률로 1번째 클래스, 1%의 확률로 0번째 클래스다"라고 결론 낼 수 있다.

즉, 소프트맥스 함수를 통해 문제를 확률적(통계적)으로 대응할 수 있게 된다.

 

여기서 주의점으로, 소프트맥스 함수를 적용해도 각 원소의 대소 관계는 변하지 않는다.

이는 지수 함수 y = exp(x) 가 단조 증가 함수이기 때문이다. 

단조 증가 함수란 정의역 원소 a, b가 a<=b일 때, f(a)<=f(b)가 성립하는 함수다. 

실제로 앞의 예에서 a의 원소들 사이의 대소 관계가 y의 원소들 사이의 대소 관계로 그대로 이어진다.

 

신경망을 이용한 분류에서는 일반적으로 가장 큰 출력을 내는 뉴런에 해당하는 클래스로만 인식한다.

그리고 소프트맥스 함수를 적용해도 출력이 가장 큰 뉴런의 위치는 달라지지 않는다.

결과적으로 신경망으로 분류할 때는 출력층의 소프트맥스 함수를 생략해도 된다. 

더보기

기계학습 문제 풀이는 학습추론(inference)의 두 단계를 거쳐 이뤄진다. 

학습 단계에서 모델을 학습하고(직업 훈련을 받고), 추론 단계에서 앞서 학습한 모델로 미지의 데이터에 대해서 추론(분류)을 수행한다(현장에 나가 진짜 일을 한다). 

신경망을 학습시킬 때는 출력층에서 소프트맥스 함수를 사용한다.

 

 

3.5.4. 출력층의 뉴런 수 정하기

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

분류에서는 분류하고 싶은 클래스 수로 설정하는 것이 일반적이다.

 

예를 들어 입력 이미지를 숫자 0부터 9 중 하나로 분류하는 문제라면 출력층의 뉴런을 10개로 설정한다. 

출력층의 뉴런은 각 숫자에 대응한다.

출력층 뉴런은 위에서부터 차례로 숫자 0, 1, ..., 9에 대응하며, 뉴런의 회색 농도가 해당 뉴런의 출력 값 크기를 의미한다.

이 예에선 색이 가장 짙은 y2 뉴런이 가장 큰 값을 출력한다.

그래서 이 신경망이 선택한 클래스는 y2, 즉 입력 이미지를 숫자 '2'로 판단했음을 의미한다. 

 

 

3.6. 손글씨 숫자 인식

신경망의 구조를 손글씨 숫자 분류에 적용해볼 수 있다.

이미 학습된 매개변수를 사용하여 학습 과정은 생략한다.

추론 과정만 구현할 건데, 추론 과정을 신경망의 순전파(forward propagation)라고도 한다.

 

3.6.1. MNIST 데이터셋

MNIST라는 손글씨 숫자 이미지 집합을 데이터셋으로 사용한다.

MNIST는 기계학습이나 이미지 인식 분야에서 실험용 데이터도 자주 등장한다.

 

MNIST 데이터셋은 0부터 9까지의 숫자 이미지로 구성된다. 

훈련 이미지가 60000장, 시험 이미지가 10000장 준비돼 있다. 

MNIST 이미지 데이터셋의 예

MNIST 이미지 데이터는 28x28 크기의 회색조 이미지(1채널)이며, 각 픽셀은 0에서 255까지의 값을 취한다.

각 이미지에는 '7', '2' 같이 이미지가 실제 의미하는 숫자가 레이블로 붙어있다.

 

책 『밑바닥부터 시작하는 딥러닝』에서는 MNIST 데이터셋을 내려받아 이미지를 넘파이 배열로 변환해주는 스크립트를 제공한다.

mnist.py 파일에 정의된 load_mnist() 함수를 이용하면 MNIST 데이터를 쉽게 가져올 수 있다.

(깃허브 https://github.com/code4human/deep-learning-from-scratch 에서 dataset/mnist.py파일)

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

가장 먼저 부모 디렉터리의 파일을 가져올 수 있게 설정하고, dataset/mnist.py load_mnist 함수를 임포트한다.

그런 뒤 load_mnist 함수로 MNIST 데이터셋을 읽는다. 최소 실행 시에는 인터넷에 연결된 상태여야 한다.

두 번째부터는 로컬에 저장된 파일(pickle 파일)을 읽기 때문에 빨리 끝난다.

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

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

 

  • normalize는 입력 이미지의 픽셀 값을 0.0~1.0 사이의 값으로 정규화할지를 정한다. False로 설정하면 입력 이미지의 픽셀은 원래 값 그대로 0~255 사이의 값을 유지한다. 
  • flatten은 입력 이미지를 평탄하게, 즉 1차원 배열로 만들지를 정한다. False로 설정하면 입력 이미지를 1X28X28의 3차원 배열로, True로 설정하면 784개의 원소로 이뤄진 1차원 배열로 저장한다.
  • one_hot_label은 레이블을 원-핫 인코딩(one_hot encoding) 형태로 저장할지를 정한다. one_hot_label이 False면 '7'이나 '2'와 같이 숫자 형태의 레이블을 저장하고, True일 땐 레이블을 원-핫 인코딩하여 저장한다. 

원-핫 인코딩이란, 예를 들어 [0,0,1,0,0,0,0,0,0,0]처럼 정답을 뜻하는 원소만 1이고(hot)하고 나머지는 모두 0인 배열이다. 

더보기

파이썬에는 pickle이라는 편리한 기능이 있다. 이는 프로그램 실행 중에 특정 객체를 파일로 저장하는 기능이다.

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

MNIST 데이터셋을 읽는 load_mnist() 함수에서도 2번째 이후의 읽기 시에 pickle을 이용한다.

pickle 덕분에 MNIST 데이터를 순식간에 준비할 수 있다. 

 

MNIST 이미지를 화면으로 불러오겠다. 이미지 표시에는 PIL(Python Image Library) 모듈을 사용한다. 

첫 번째 훈련 이미지를 모니터 화면에 표시해보겠다.

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)

flatten=True로 설정해 읽은 이미지는 1차원 넘파이 배열로 저장되어 있다. 

그래서 이미지를 표시할 때 원래 형상인 28X28 크기로 다시 변형해야 한다.

reshape() 메서드에 원하는 형상을 인수로 지정하면 넘파이 배열의 형상을 바꿀 수 있다. 

또한 넘파이로 저장된 이미지 데이터를 PIL용 데이터 객체로 변환해야 하며, 이 변환은 Image.fromarray()가 수행한다.

 

 

3.6.2. 신경망의 추론 처리

MNIST 데이터셋을 가지고 추론을 수행하는 신경망을 구현할 차례다. 

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

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

 

은닉층은 총 두 개로, 첫 번째 은닉층에는 50개의 뉴런을, 두 번째 은닉층에는 100개의 뉴런을 배치할 것이다.

50과 100은 임의로 정한 값이다.

 

순서대로 작업을 처리해줄 get_data(), init_network(), predict()를 정의한다.

sample_weight.pkl 파일은 책에서 제공하는 미리 학습해둔 가중치 매개변수의 값들이다.

(깃허브 https://github.com/code4human/deep-learning-from-scratch 에서 ch03/sample_weight.pkl 파일)

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'], 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_networt()에서는 pickle 파일인 sample_weight.pkl에 저장된 '학습된 가중치 매개변수'를 읽는다.

이 파일엔 가중치와 편향 매개변수가 딕셔너리 변수로 저장돼있다. 

세 함수를 사용해 신경망에 의한 추론을 수행해보고 정확도(accuracy, 분류가 얼마나 올바른가)도 평가해본다.

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)))   #Accuracy:0.9352

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

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

predict() 함수는 각 레이블의 확률을 넘파이 배열로 반환한다.

예를 들어 [0.1, 0.3, 0.2, ..., 0.04] 같은 배열이 반환되며, 이는 이미지가 숫자 '0'일 확률이 0.1, 숫자 '1'일 확률이 0.3, ... 식으로 해석한다. 

그런 다음 np.argmax() 함수로 이 배열에서 값이 가장 큰(확률이 가장 높은) 원소의 인덱스를 구한다. 이것이 예측 결과다.

신경망이 예측한 답변과 정답 레이블을 비교하여 맞힌 숫자(accuracy_cnt)를 세고, 이를 전체 이미지 숫자로 나눠 정확도를 구한다.

 

이 코드를 실행하면 정확도는 0.9352로 출력되는데, 올바르게 분류한 비율이 93.52%라는 뜻이다. 

여기서는 학습된 신경망을 돌려보는 것까지라서 정확도에 대해선 고민하지 않는다. 4장에서는 신경망 구조와 학습 방법을 궁리한다.

 

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

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

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

 

 

3.6.3. 배치 처리

입력 데이터와 가중치 매개변수의 "형상"에 주의해서 조금 전의 구현을 다시 보겠다. 

앞서 구현한 신경망 각 층의 가중치 형상을 출력해본다.

x, _ = get_data()
network = init_network()
W1, W2, W3 = network['W1'], network['W2'], network['W3']

print(x.shape)   #(10000, 784)
print(x[0].shape)   #(784,)
print(W1.shape)   #(784, 50)
print(W2.shape)   #(50, 100)
print(W3.shape)   #(100, 10)

이 결과에서 다차원의 배열의 대응하는 차원의 원소 수가 일치함을 확인할 수 있다(편향은 생략).

 

그림으로는 아래처럼 된다. 최종 결과로는 원소가 10개인 1차원 배열 y가 출력된다.

신경망 각 층의 배열 형상의 추이

원소 784개로 구성된 1차원 배열(원래는 28X28인 2차원 배열)이 입력되어 마지막에는 원소가 10개인 1차원 배열이 출력되는 흐름이다.

이는 이미지 데이터를 1장만 입력했을 떄의 처리 흐름이다. 

 

만약 이미지 100장을 묶어 predict() 함수에 한꺼번에 넘기면 어떻게 될까?

x의 형상을 100X784로 바꿔서 100장 분량의 데이터를 하나의 입력 데이터로 표현하면 된다. 

배치 처리를 위한 배열들의 형상 추이

입력 데이터의 형상은 100X784, 출력 데이터의 형상은 100X10이 된다.

100장 분량 입력 데이터의 결과가 한 번에 출력된다.

x[0]과 y[0]에는 0번째 이미지와 추론 결과가, x[1]과 y[1]에는 1번째 이미지와 그 결과가 저장된다.

 

이처럼 하나로 묶은 입력 데이터를 배치(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)))   #Accuracy:0.9352

for문은 우선 range() 함수다. range() 함수가 반환하는 리스트를 바탕으로 x[i:i+batch_size]에서 입력 데이터를 묶는다. 

x[i:i+batch_size]는 입력 데이터의 i번째부터 i+batch_size번째까지의 데이터를 묶는다는 의미다.

윗 줄에서 batch_size를 100으로 정했으므로 x[0:100], x[100:200], ...와 같이 앞에서 100장씩 묶어 꺼내게 된다.

 

argmax()는 최댓값의 인덱스를 가져온다. axis=1 이라는 인수를 추가해줬는데, 이는 100X10의 배열 중 1번째 차원을 구성하는 각 원소에서(1번째 차원을 축으로) 최댓값의 인덱스를 찾도록 한 것이다(인덱스가 0부터 시작하니 0번째 차원이 가장 처음 차원이다).

 

argmax()의 예시는 아래와 같다.

# argmax의 예시
x = np.array([[0.1, 0.8, 0.1], [0.3, 0.1, 0.6], [0.2, 0.5, 0.3], [0.8, 0.1, 0.1]])
y = np.argmax(x, axis =1)

print(y)   #[1 2 1 0]

 

마지막으로 배치 단위로 분류한 결과를 실제 답과 비교한다.

이를 위해 == 연산자를 사용해 넘파이 배열끼리 비교하여 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

이상으로 배치 처리 구현에 대해 알아보았다.

데이터를 배치로 처리함으로써 효율적이고 빠르게 처리할 수 있다. 

 


<참고 문헌>

사이토 고키(2019), 『밑바닥부터 시작하는 딥러닝』, 한빛미디어, pp.83-105.

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

4-2. 신경망 학습  (0) 2020.03.25
4-1. 신경망 학습  (0) 2020.03.16
보충) numpy 행렬의 형상 차이 - (N,) (N,1)(1,N)  (0) 2020.03.08
3-1. 신경망  (0) 2020.03.07
2. 퍼셉트론(Perceptron)  (0) 2020.03.02

댓글