신경망 학습의 절차

 

0. 전제 : 신경망에는 적응 가능한 가중치와 편향이 있고, 이 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정을 학습이라 한다.

1. 미치배치 : 훈련 데이터 중 일부를 무작위로 가져온다. 이렇게 선별한 데이터를 미니배치라 하며, 그 미니배치의 손실함수 값을 줄이는 것이 목표.

2. 기울기 산출 : 미니배치의 손실 함수 값을 줄이기 위해 각 가중치 매개변수의 기울기를 구한다. 기울기는 손실 함수의 값을 가장 작게 하는 방향을 제시한다. 

3. 매개변수 갱신 : 가중치 매개변수를 기울기 방향으로 아주 조금 갱신한다.

4. 반복 : 1-3 단계를 반복한다.

 

위의 절차는 경사 하강법으로 매개변수를 갱신하는 방법이며, 데이터를 미니배치로 무작위로 선정하기 때문에 확률적 경사 하강법(stochastic gradient descent, SGD)이라고 한다. 확률적으로 무작위로 골라낸 데이터에 대해 수행하는 경사 하강법이라는 의미이다. 

 

1. 2층 신경망 클래스 구현

import sys, os
sys.path.append(os.pardir)
from common.functions import *
from common.gradient import numerical_gradient

class TwoLayerNet:
    def __init__(self, input_size, hidden_size, output_size, 
                weight_init_std=0.01):
        #가중치 초기화
        self.params = {}
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
        self.params['b2'] = np.zeros(output_size)
        
    def predict(self, x):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        
        a1 = np.dot(x, W1)+ b1
        z1 = sigmod(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        return y
    
    # x: 입력 데이터, t : 정답 레이블
    def loss(self, x, t):
        y = self.predict(x)
        return cross_entropy_error(y, t)
    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        t = np.argmax(t, axis=1)
        accuracy = np.sum(y==t) / float(x.shape[0])
        
    # x: 입력 데이터, t : 정답 레이블
    def numerical_gradient(self, x, t):
        loss_W = lambda W: self.loss(x, t)
        
        grads = {}
        grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient(loss_W, self.params['W1'])
        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
        
        return grads

 

  • TwoLayerNet 클래스가 사용하는 변수
변수 설명
params 신경망의 매개변수를 보관하는 딕셔너리 변수(인스턴스 변수)
  params['W1']은 1번째 층의 가중치, params['b1']은 1번째 층의 편향
  params['W2']는 2번째 층의 가중치, params['b2']는 2번째 층의 편향
grads 기울기를 보관하는 딕셔너리 변수(numerical_gradient() 메서드의 반환 값)
  grads['W1']은 1번째 층이 가중치의 기울기, grads['b1']은 1번째 층의 편향의 기울기
  grads['W2']는 2번째 층의 가중치의 기울기, grads['b2]는 2번째 층의 편향의 기울기

 

  • TwoLayerNet 클래스의 메소드
메소드 설명
__init__(self, input_size, hidden_size, output_size) 초기화 수행, 인수는 입력층의 뉴런 수, 은닉층의 뉴런 수, 출력층의 뉴런 수
predict(self, x) 예측(추론)을 수행한다. 인수 x는 이미지 데이터
loss(self, x, t) 손실 함수의 값을 구한다. 인수 x는 이미지 데이터, t는 정답 레이블
accuracy(self, x, t) 정확도를 구한다.
numerical_gradient(self, x, t) 가중치 매개변수의 기울기를 구한다.
gradient(self, x, t) 가중치 매개변수의 기울기를 구한다. numerical_gradient()의 개선판. 구현은 다음에.

n번째 층의 가중치 매개변수는 params['Wn']키에 넘파이 배열로 저장된다.

 

ex)

net = TwoLayerNet(input_size= 784, hidden_size=100, output_size=10)
print(net.params['W1'].shape)
print(net.params['b1'].shape)
print(net.params['W2'].shape)
print(net.params['b2'].shape)
[실행 결과]
(784, 100)
(100,)
(100, 10)
(10,)

이처럼 params 변수에는 신경망에 필요한 매개변수가 모두 저장된다. 그리고 params 변수에 저장된 가중치 매개변수가 예측 처리(순방향 처리)에서 사용된다. 

 

예측 처리 코드

x = np.random.rand(100, 784) #더미 입력 데이터(100장 분량)
y = net.predict(x)

 

grads 변수에는 params 변수에 대응하는 각 매개변수의 기울기가 저장된다. numerical_gradient() 메소드를 이용해서 기울기를 계산하면 grads 변수에 기울기 정보가 저장된다.

x = np.random.rand(100, 784) #더미 입력 데이터(100장 분량)
t = np.random.rand(100, 10) #더미 정답 레이블 (100장 분량)

grads = net.numerical_gradient(x, t) #기울기 계산

print(grads['W1'].shape)
print(grads['b1'].shape)
print(grads['W2'].shape)
print(grads['b2'].shape)
[실행 결과]
(784, 100)
(100,)
(784, 100)
(10,)

손글씨 숫자 인식에서 크기가 28X28인 입력 이미지가 총 784개이고, 출력은 10개가 된다. 이에 따라 값을 지정해주고, 은닉층의 갯수인 hidden_size는 적당한 값을 설정한다.

초기화 메소드는 가중치 매개변수도 초기화한다. 가중치 매개변수의 초기값을 무엇으로 설정하느냐가 신경망 학습의 성공을 좌우한다.

predict(self, x)와 accuracy(self, x, t)의 구현은 앞부분의 신경망의 추론 처리와 거의 같다.

loss(self, x, t)는 predict()의 결과와 정답 레이블을 바탕으로 교차 엔트로피 오차를 구하도록 구현했다.

numerical_gradient(self, x, t)는 수치 미분 방식으로 각 매개변수의 손실 함수에 대한 기울기를 계산한다.

gradient(self, x, t)는 오차역전파법을 사용하여 기울기를 계산한다.

 

신경망 학습은 시간이 오래 걸리니, 시간을 절약하려면 numerical_gradient(self, x, t) 대신 gradient(self, x, t)를 쓰는 것이 좋다.

'Deep Learning 1 > 신경망 학습' 카테고리의 다른 글

4. 기울기  (0) 2020.07.20
3. 수치 미분  (0) 2020.05.04
2. 손실 함수  (0) 2020.04.29
1. 데이터에서 학습  (0) 2020.04.28

 

$x_0$과 $x_1$의 편미분을 동시에 계산하기 위해 ($x_0$, $x_1$) 양쪽의 편미분을 묶어서 $\left({\partial f \over \partial x_0} , {\partial f \over \partial x_1}\right)$ 을 계산한다.

$\left({\partial f \over \partial x_0} , {\partial f \over \partial x_1}\right)$ 처럼 모든 변수의 편미분을 벡터로 정리한 것을 기울기(gradient) 라고 한다.

 

  • 기울기의 구현
def function_2(x):
    return x[0]**2 + x[1]**2

def numerical_gradient(f,x):
    h = 1e-4 #0.0001
    grad = np.zeros_like(x) #x와 형상이 같은 배열을 생성
    
    for idx in range(x.size):
        tmp_val =x[idx]
        
        #f(x+h)계산
        x[idx] = tmp_val+h
        fxh1 = f(x)
        
        #f(x-h)계산
        x[idx] = tmp_val-h
        fxh2 = f(x)
        
        grad[idx] = (fxh1 - fxh2)/(2*h)
        x[idx] = tmp_val #값 복원
    return grad

 

복잡해보이지만, 동작 방식은 변수가 하나일 때의 수치 미분과 거의 같다. np.zeros_like(x) 는 x와 형상이 같고 그 원소가 모두 0인 배열을 만든다.

위에서 구현한 함수의 인수인 f는 함수이고 x는 넘파이 배열이므로 넘파이 배열 x의 각 원소에 대해서 수치 미분을 구한다.

 

print(numerical_gradient(function_2, np.array([3.0,4.0])))
print(numerical_gradient(function_2, np.array([0.0,2.0])))
print(numerical_gradient(function_2, np.array([3.0,0.0])))
[실행 결과]
[6. 8.]
[0. 4.]
[6. 0.]

 

이처럼 ($x_0$, $x_1$) 의 각 점에서의 기울기를 계산할 수 있다.

[참고] 실제로는 [6.00000000000037801, 7.99999999999991189] 라는 값이 나오지만, 넘파이가 배열을 출력할 때 수치를 보기 쉽도록 가공하기 때문에 [6. 8.]의 결과가 나왔다.

 

기울기 그림은 아래의 그림처럼 방향을 가진 벡터(화살표)로 그려진다. 기울기는 함수의 가장 낮은 장소(최솟값)을 가리킨다. 그리고 최솟값에서 멀어질수록 화살표의 크기가 커지는 것을 알 수 있다.

 

$f$($x_0$, $x_1$) = $x_0^2$+$x_1^2$의 기울기

사실 기울기는 각 지점에서 낮아지는 방향을 가리킨다. 즉 기울기가 가리키는 쪽은 각 장소에서 함수의 출력 값을 가장 크게 줄이는 방향이다.

 

1. 경사 하강법

머신러닝 문제 대부분은 학습 단계에서 최적의 매개변수를 찾아낸다. 신경망 역시 최적의 매개변수인 가중치와 편향을 학습 시에 찾아야 한다. 최적은 손실 함수가 최솟값이 될 때의 매개변수 값이다. 이때, 기울기를 이용하여 함수의 최솟값 혹은 가능한 한 작은 값을 찾으려는 것이 경사 하강법이다.

여기서 각 지점에서 함수의 값을 낮추는 방안을 제시하는 지표가 기울기라는 것이다. 하지만 기울기가 가리키는 곳에 정말 함수의 최솟값이 있는지 보장할 수 없다. 실제로 복잡한 함수에서는 기울기가 가리키는 방향에 최솟값이 없는 경우가 대부분이다.

 

함수가 극솟값, 최소값, 안장점(saddle point) 이 되는 장소에서는 기울기가 0이다. 극솟값은 국소적인 최솟값, 즉 한정된 범위에서의 최솟값인 점이다. 안장점은 어느 방향에서 보면 극댓값이고 다른 방향에서 보면 극솟값이 되는 점이다. 경사 하강법은 기울기가 0인 장소를 찾지만 극솟값이나 안장점일 가능성 때문에 그것이 반드시 최솟값이라고 할 수 없다. 그리고 복잡한 함수라면 평평한 곳으로 파고들면서 고원(plateau)이라는 학습이 진행되지 않는 정체기에 빠질 수 있다.

 

기울어진 방향이 꼭 최솟값을 가리키는 것은 아니지만, 그 방향으로 가야 함수의 값을 줄일 수 있다. 그래서 최솟값이 되는 장소를 찾는 문제에서는 기울기 정보를 단서로 나아갈 방향을 정해야 한다.

 

경사 하강법은 현 위치에서 기울어진 방향으로 일정 거리만큼 이동한다. 그런 다음 이동한 곳에서도 마찬가지로 기울기를 구하고, 또 그 기울어진 방향으로 나아가기를 반복한다. 이렇게 해서 함수의 값을 점차 줄이는 것이 경사법(gradient descent method) 라고 한다. 

 

경사법은 최솟값을 찾느냐, 최댓값을 찾느냐에 따라 이름이 다르다. 전자를 경사 하강법, 후자를 경사 상승법이라고 하는데, 손실 함수의 부호를 반전시키면 최솟값을 찾는 문제와 최댓값은 찾는 문제는 같은 것이니 하강이냐 상승이냐는 중요하지 않다.

 

경사법의 수식

$\eta$는 갱신하는 양을 나타내는데, 이를 신경망 학습에서는 학습률(learning rate)라고 한다. 한 번의 학습으로 얼마만큼 학습해야 할지, 즉 매개변수 값을 얼마나 갱신하느냐를 정하는 것이 학습률이다.

 

위의 식은 1회에 해당하는 갱신이고, 이 단계를 반복하면서 서서히 함수의 값을 줄인다. 위 수식은 변수가 2개이지만, 변수의 수가 늘어도 같은 식(각 변수의 편미분 값)으로 갱신한다.

 

또한 학습률 값은 0.01이나 0.001 등 미리 특정 값으로 정해두어야 하는데, 일반적으로 이 값이 너무 크거나 작으면 좋은 장소를 찾아갈 수 없다. 신경망 학습에서는 보통, 이 학습률 값을 변경하면서 올바르게 학습하고 있는지를 확인하면서 진행한다.

 

  • 경사하강법 구현
def gradient_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x
    for i in range(step_num):
        grad = numerical_gradient(f,x)
        x -= lr * grad
    return x

 

인수 f는 최적화하려는 함수, init_x는 초기값, lr은 learning late, step_num은 경사하강법에 따른 반복 횟수를 뜻한다. 함수의 기울기는 numerical_gradient(f, x)로 구하고, 그 기울기에 학습률을 곱한 값으로 갱신하는 처리를 step_num번 반복한다. 이 함수를 사용하면 함수의 극솟값이나 최솟값을 구할 수 있다.

 

  • 경사법으로 $f(x_0,x_1)$=$x_0^2+x_1^2$의 최솟값을 구해보자
def function_2(x):
    return x[0]**2 + x[1]**2
init_x = np.array([-3.0,4.0])
print(gradient_descent(function_2, init_x=init_x, lr=0.1, step_num=100))
[실행 결과]
[-6.11110793e-10  8.14814391e-10]

초기값을 (-3.0, 4.0)으로 설정한 후 경사법을 사용해 최솟값 탐색을 시작한다. 결과는 거의 (0, 0)에 가까운 결과이다. 실제 최솟값은(0, 0)이므로 거의 정확한 결과를 얻었다.

 

경사법에 의한 위 수식의 갱신과정. 점선은 함수의 등고선을 나타낸다.

값이 가장 낮은 장소인 원점에 점차 가까워지고 있다.

 

#학습률이 너무 큰 예 : lr =10.0
init_x = np.array([-3.0, 4.0])
print(gradient_descent(function_2, init_x=init_x, lr=10.0, step_num=100))

#학습률이 너무 작은 예 : lr = 1e-10
init_x = np.array([-3.0, 4.0])
print(gradient_descent(function_2, init_x=init_x, lr=1e-10, step_num=100))
[실행 결과]
[-2.58983747e+13 -1.29524862e+12]
[-2.99999994  3.99999992]

 

학습률이 너무 크면 큰 값으로 발산하고, 너무 작으면 거의 갱신되지 않은 채로 끝난다.

 

학습률 같은 매개변수를 하이퍼 파라미터라고 하는데, 가중치와 편향 같은 신경망의 매개변수와는 성질이 다르다. 신경망의 가중치 매개변수는 훈련 데이터와 학습 알고리즘에 의해서 자동으로 획득되는 반면, 하이퍼 파라미터는 사람이 직접 설정해야 한다.

 

2. 신경망에서의 기울기

신경망 학습에서의 기울기는 가중치 매개변수에 대한 손실 함수의 기울기이다. 예를 들어 형상이 2X3, 가중치가 W, 손실 함수가 L인 신경망의 수식은 $\partial L \over \partial W$로 나타낼 수 있다.

 

경사의 수식

$\partial L \over \partial W$의 원소는 각각의 원소에 대한 편미분이다. 예를 들면 아래 수식의 1행 1번째 원소는 $w_{11}$을 조금 변경했을 때 손실 함수 L이 얼마나 변화하느냐를 나타낸다. 이때, $\partial L \over \partial W$의 형상이 W와 같다는 것이다. 실제로 W와 $\partial L \over \partial W$의 형상은 모두 2X3이다.

 

  • 간단한 신경망을 예로 들어 기울기를 구하는 코드 구현
# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient

class simpleNet:
    def __init__(self):
        self.W = np.random.randn(2,3) # 정규분포로 초기화

    def predict(self, x):
        return np.dot(x, self.W)

    def loss(self, x, t):
        z = self.predict(x)
        y = softmax(z)
        loss = cross_entropy_error(y, t)

        return loss

x = np.array([0.6, 0.9])
t = np.array([0, 0, 1])

net = simpleNet()

f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)

print(dW)
[실행 결과]
[[ 0.25448428  0.03691667 -0.29140095]
 [ 0.38172642  0.05537501 -0.43710143]]

위 클래스는 형상이 2X3인 가중치 매개변수 하나를 인스턴스 변수로 갖는다. 메소드는 2개인데, 하나는 예측을 수행하는 predict(x)이고, 다른 하나는 손실 함수의 값을 구하는 loss(x, t)이다. 인수 x는 입력데이터, t는 정답 레이블이다. 

 

net = simpleNet()
print("net.W : \n",net.W) #가중치 매개변수

x = np.array([0.6, 0.9])
p = net.predict(x)
print("p : \n",p)

print(np.argmax(p)) #최댓값의 인덱스

t = np.array([0,0,1]) #정답 레이블
net.loss(x, t)
[실행 결과]
net.W : 
 [[ 0.00387155 -0.12577174  0.95361921]
 [ 1.8080852  -0.55346652 -0.22063305]]
p : 
 [ 1.62959961 -0.57358291  0.37360178]
0
1.5890656555481064

기울기는 numerical_gradient(f, x)를 써서 구하면 된다.

 

def f(W):
    return net.loss(x, t)
#f 함수를 람다식으로 구현
# f = lambda w : net.loss(x, t)
dW = numerical_gradient(f, net.W)
print(dW)
[실행 결과]
[[ 0.43003252  0.04749756 -0.47753008]
 [ 0.64504879  0.07124634 -0.71629513]]

f(W) 함수의 인수 W는 더미로 만든 것이다. numerical_gradient(f, x) 내부에서 f(x)를 실행하는데, 그와의 일관성을 위해 f(W)를 정의한 것이다.

여기에서는 net.W를 인수로 받아 손실 함수를 계산하는 새로운 함수 f를 정의하여 numerical_gradient에 넘긴다.

dW는 numerical_gradient(f, net.W)의 결과로, 형상은 2X3의 2차원 배열이다. 

dW의 내용을 보면, $\partial L \over \partial W_{11}$은 대략 0.4인데 이는 $w_{11}$을 $h$만큼 늘리면 손실 함수의 값을 0.4$h$만큼 증가한다는 의미이다. 마이너스가 붙은 값을 손실 함수의 값이 그만큼 감소한다는 뜻이다.

그래서 손실 함수를 줄인다면 $\partial L \over \partial W$의 값 중 양수의 값은 음의 방향으로 갱신하고, 음수의 값은 양의 방향으로 갱신해야 한다. 

갱신되는 양에는 $\partial L \over \partial W$의 값의 절댓값으로 하기 때문에 $w_{23}$이 $w_{11}$보다 더 크게 기여한다.

 

신경망의 기울기를 구한 다음에는 경사법에 따라 가중치 매개변수를 갱신하기만 하면 된다.

 

'Deep Learning 1 > 신경망 학습' 카테고리의 다른 글

5. 학습 알고리즘 구현  (0) 2020.07.21
3. 수치 미분  (0) 2020.05.04
2. 손실 함수  (0) 2020.04.29
1. 데이터에서 학습  (0) 2020.04.28

 

경사법에서는 기울기(경사) 값을 기준으로 나아갈 방향을 정한다.

 

1. 미분

미분은 특정 순간의 변화량을 뜻한다.

 

함수의 미분을 나타낸 수식

좌변은 $f(x)$의 $x$에 대한 미분($x$에 대한 $f(x)$의 변화량)을 나타내는 기호이다. 결국 $x$의 작은 변화가 함수 $f(x)$를 얼마나 변화시키느냐를 의미한다. 이때 시간의 작은 변화, 즉 시간을 뜻하는 $h$를 한없이 0에 가깝게 한다는 의미를 $\lim\limits_{h \to 0}$ 으로 나타낸다.

 

  • 함수를 미분하는 계산을 구현
#나쁜 구현의 예
def numerical_diff(f,x): #수치미분이라는 뜻
    h = 10e-50
    return (f(x+h)-f(x))/h

h를 0으로 무한히 가깝게 하기 위해서 가급적 작은 값을 대입하기 위해 10e-50이라는 작은 값을 이용했지만 이는 반올림 오차(rounding error) 문제를 일으킨다. 반올림 오차는 작은 값이 생략도어 최종 계산 결과에 오차가 생기게 한다.

import numpy as np
print(np.float32(1e-50))
[실행 결과]
0.0

32비트 부동소수점으로 나타내면 0이 되어, 너무 작은 값을 이용하면 컴퓨터로 계산하는 데 문제가 된다. 이것을 개선할 첫번째 포인트는 h를 $10^{-4}$정도의 값을 사용한다.

 

두번째 개선은 함수 f의 차분(임의의 두 점에서의 함수 값들의 차)과 관련된 것이다. 앞의 구현에서는 x+h와 x 사이의 함수 f의 차분을 계산하지만, 이 계산에는 오차가 있다. $x$ 위치의 함수의 기울기와 ($x+h$)와 $x$ 사이의 기울기는 일치하지 않는다. 이 차이는 $h$를 무한히 0으로 좁히는 것이 불가능해 생기는 한계이다.

 

진정한 미분(진정한 접선)과 수치 미분(근사로 구한 접선)의 값은 다르다.

수치 미분에는 오차가 포함된다. 이 오차를 줄이기 위해 ($x+h$)와 ($x-h$)일 때의 함수 $f$의 차분을 계산하는 방법을 쓰기도 한다. 이 차분은 $x$를 중심으로 그 전후의 차분을 계산한다는 의미에서 중심 차분 혹은 중앙 차분이라고 한다. ($x+h$)와 $x$의 차분은 전방 차분이라고 한다.

  • 두 개선점을 적용한 수치 미분의 구현
def numerical_diff(f,x):
    h = 1e-4
    return (f(x+h)-f(x-h))/(2*h)

 

아주 작은 차분으로 미분하는 것을 수치 미분이라고 하고, 수식을 전개하여 미분하는 것은 해석적 미분이라고 한다. 해석적 미분은 오차를 포함하지 않는 진정한 미분 값을 구해준다.

 

2. 수치 미분의 예

 

2차 함수

위의 2차 함수 수식을 구현한다.

def function_1(x):
    return 0.01*x**2+0.1*x

위의 함수를 그려본다.

import numpy as np
import matplotlib.pylab as plt
x = np.arange(0.0,20.0,0.1) #0에서 20까지 0.1 간격의 배열 x를 만든다(20은 미포함)
y = function_1(x)
plt.xlabel("x")
plt.ylabel("f(x)")
plt.plot(x,y)
plt.show()

이제 x=5일 때와 x=10일 때의 함수의 미분을 계산해본다.

print(numerical_diff(function_1,5))
print(numerical_diff(function_1,10))
[실행 결과]
0.1999999999990898
0.2999999999986347

이렇게 계산한 수치 미분 값이 $x$에 대한 $f(x)$의 변화량(함수의 기울기)이다. 해석적 미분의 값은 0.2와 0.3으로 수치 미분의 결과 값과 오차가 매우 작다.

 

x=5, x=10에서의 접선 : 직선의 기울기는 수치 미분에서 구한 값을 사용했다.

수치 미분 값을 기울기로 하는 직선은 함수의 접선에 해당한다.

 

3. 편미분

변수가 2개인 인수들의 제곱 합

def function_2(x):
    return x[0]**2 + x[1]**2
    #또는 return np.sum(x**2)

인수 x는 넘파이 배열이라고 가정한다.

 

위 수식의 그래프

위의 식을 미분해보자면 변수가 2개이기 때문에 $x_0$과 $x_1$ 중 어느 변수에 대한 미분이냐를 구별해야 한다. 이와 같이 변수가 여럿인 함수에 대한 미분을 편미불이라고 한다. ${\partial f \over \partial x_0}$ 이나 ${\partial f \over \partial x_1}$처럼 쓴다.

 

이처럼 편미분은 변수가 하나인 미분과 마찬가지로 특정 장소의 기울기를 구한다. 단, 여러 변수 중 목표 변수 하나에 초점을 맞추고 다른 변수는 값을 고정한다. 그래서 목표 변수를 제외한 나머지를 특정 값에 고정하기 위해서 새로운 함수를 정의하여 그 함수에 대해 수치 미분 함수를 적용하여 편미분을 구한다.                                                                           

'Deep Learning 1 > 신경망 학습' 카테고리의 다른 글

5. 학습 알고리즘 구현  (0) 2020.07.21
4. 기울기  (0) 2020.07.20
2. 손실 함수  (0) 2020.04.29
1. 데이터에서 학습  (0) 2020.04.28

 

신경망 학습에서는 현재의 상태를 하나의 지표로 표현하며, 그 지표를 가장 좋게 만들어주는 가중치 매개변수의 값을 탐색하는 것이다. 신경망 학습에서 사용하는 지표는 손실 함수(오차함수, loss function)라고 한다. 손실 함수는 일반적으로는 평균 제곱 오차와 교차 엔트로피 오차를 사용한다.

 

손실 함수는 신경망 성능의 나쁨을 나타내는 지표로, 현재의 신경망이 훈련 데이터를 얼마나 잘 처리하지 못하느냐를 나타낸다. 손실 함수에 마이너스만 곱하면 얼마나 나쁘지 않냐(얼마나 좋으냐)라는 지표로 변한다. 

 

1. 평균 제곱 오차

가장 많이 쓰이는 손실 함수는 평균 제곱 오차(mean squared error, MSE)이다.

 

평균 제곱 오차의 수식

$y_k$는 신경망의 출력(신경망이 추정한 값), $t_k$는 정답 레이블, $k$는 데이터의 차원 수를 나타낸다.

 

y = [0.1,0.05,0.6,0.0,0.05,0.1,0.0,0.1,0.0,0.0]
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

이 배열들의 원소는 첫번째 인덱스부터 순서대로 숫자 0,1,2..일 때의 값이다. 여기에서 신경망의 출력y는 소프트맥스 함수의 출력이다. 소프트맥스 함수의 출력은 확률로 해석 할 수 있다. t는 정답 레이블이고, 정답인 원소는 1, 아닌 원소들은 0으로 나타내므로 원-핫 인코딩이다.

 

평균 제곱 오차는 각 원소의 출력(추정 값)과 정답 레이블(참 값)의 차($y_k-t_k$)를 제곱한 후 그 총합을 구한다.

def mean_squared_error(y,t):
    return 0.5 * np.sum((y-t)**2)

여기에서 인수 y와 t는 넘파이 배열이다. 위의 식을 이용하여 실제로 구현해 본다.

import numpy as np

y1 = [0.1,0.05,0.6,0.0,0.05,0.1,0.0,0.1,0.0,0.0]
y2 = [0.1,0.05,0.1,0.0,0.05,0.1,0.0,0.6,0.0,0.0]
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

def mean_squared_error(y,t):
    return 0.5 * np.sum((y-t)**2)

print(mean_squared_error(np.array(y1),np.array(t)))
print(mean_squared_error(np.array(y2),np.array(t)))
[실행 결과]
0.09750000000000003
0.5975

위 코드의 결과로 y1의 손실 함수 쪽 출력이 작으며 정답 레이블과의 오차도 작은 것을 알 수 있다. 즉, 평균 제곱 오차를 기준으로는 y1의 추정 결과가 오차가 더 작으니 정답에 더 가까울 것으로 판단할 수 있다.

 

2. 교차 엔트로피 오차

다른 손실 함수로서 교차 엔트로피 오차(cross entropy error, CEE)도 자주 사용한다.

 

교차 엔트로피 오차의 수식

여기에서 log는 밑이 $e$인 자연로그($log_e$)이고, $y_k$는 신경망의 출력, $t_k$는 정답 레이블이다. 또 $t_k$는 원-핫 인코딩이므로 실질적으로 정답일 때의 추정($t_k$가 1일 때의 $y_k$)의 자연로그를 계산하는 식이 된다.

[참고] 정답이 아닌 나머지 모두는 $t_k$가 0이므로 $log y_k$와 곱해도 0이 되어 결과에 영향을 주지 않는다.

예를 들어, 정답 레이블은 2가 정답이라 하고 이때 신경망 출력이 0.6이라면 교차 엔트로피 오차는 $-log0.6$=0.51이 된다. 즉, 교차 엔트로피 오차는 정답일 때의 출력이 전체 값을 정하게 된다.

 

자연로그 y = logx의 그래프

그림에서 $x$가 1일 때 $y$는 0이 되고, $x$가 0에 가까워질수록 $y$의 값은 점점 작아진다. 식과 마찬자기로 정답에 해당하는 출력이 커질수록 0에 다가가다가, 그 출력이 1일 때 0이 된다. 반대로 정답일 때의 출력이 작아질수록 오차는 커진다.

 

교차 엔트로피 오차 구현

def cross_entropy_error(y,t):
    delta = 1e-7
    return -np.sum(t*np.log(y+delta))

여기서 y와 t는 넘파이 배열이다. delta를 더해주는 이유는 np.log() 함수에 0을 입력하면 마이너스 무한대를 뜻하는 -inf가 되어 더 이상 계산을 진행할 수 없기 때문에 아주 작은 값을 더해서 절대 0이 되지 않도록 만든 것이다.

간단한 계산을 해보기로 한다.

import numpy as np

y1 = [0.1,0.05,0.6,0.0,0.05,0.1,0.0,0.1,0.0,0.0]
y2 = [0.1,0.05,0.1,0.0,0.05,0.1,0.0,0.6,0.0,0.0]
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

def cross_entropy_error(y,t):
    delta = 1e-7
    return -np.sum(t*np.log(y+delta))

print(cross_entropy_error(np.array(y1),np.array(t)))
print(cross_entropy_error(np.array(y2),np.array(t)))
[실행 결과]
0.510825457099338
2.302584092994546

오차 값이 더 작은 y1이 정답일 가능성이 높다고 판단한 것으로 평균 제곱 오차의 판단과 일치한다.

 

3. 미니배치 학습

머신러닝은 훈련 데이터에 대한 손실 함수의 값을 구하고, 그 값을 최대한 줄여주는 매개변수를 찾아낸다. 이렇게 하려면 모든 훈련 데이터를 대상으로 손실 함수 값을 구해서 합한 값을 지표로 삼아야 한다.

 

모든 훈련 데이터에 대한 손실 함수의 합 (교차 엔트로피 오차)

데이터가 N개라면 $t_{nk}$는 $n$번째 데이터의 $k$번째 값을 의미한다($y_{nk}$는 신경망의 출력, $t_{nk}$는 정답 레이블). 다만 마지막에 N으로 나누어 정규화하고 있는데, 이로써 평균 손실 함수를 구하는 것이다. 이렇게 평균을 구해 사용하면 훈련 데이터 개수와 관계없이 언제든 통일된 지표를 얻을 수 있다.

 

데이터가 너무 많을 경우 데이터 일부를 추려 전체의 근사치로 이용할 수 있다. 신경망 학습에서도 훈련 데이터로부터 일부만 골라 학습을 수행하는데, 이 일부를 미니배치(mini-batch)라고 한다. 훈련 데이터 중에서 n개를 무작위로 뽑아 n개만 사용하여 학습하는 방법을 미니배치 학습이라고 한다.

 

  • 미니배치 구현 코드
import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist

(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

print(x_train.shape)
print(t_train.shape)
[실행 결과]
(60000, 784)
(60000, 10)

훈련 데이터와 시험 데이터를 읽고, 호출할 때 원-핫 인코딩인 배열을 얻는다.

훈련 데이터는 60,000개이고, 입력 데이터는 784열(28X28)인 이미지 데이터이고, 정답 레이블은 10줄짜리 데이터이다.

 

- 무작위로 10장만 빼내는 코드

train_size = x_train.shape[0] #60000
batch_size = 10
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
print(batch_mask)
[실행 결과]
[25229 10848  7581 12277 13056 35338 31862  5374 25169 29799]

np.random.choice()로 지정한 범위의 수 중에서 무작위로 원하는 갯수만 꺼낼 수 있다. 무작위로 선택한 이 인덱스를 사용해 미니매치를 뽑야낸다. 손실 함수도 이 미니배치로 계산한다.

 

미니배치의 손실 함수도 일부 표본 데이터로 전체를 비슷하게 계측한다. 즉, 전체 훈련 데이터의 대표로서 무작위로 선택한 작은 덩어리(미니배치)를 사용하는 것이다.

 

4. (배치용) 교차 엔트로피 오차 구현하기

  • 미니배치 같은 배치 데이터를 지원하는 교차 엔트로피 오차의 구현
def cross_entropy_error(y,t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
        
    batch_size = y.shape[0]
    return -np.sum(t*np.log(y+1e-7))/batch_size

y는 신경망의 출력, t는 정답 레이블이다. y가 1차원(데이터 하나당 교차 엔트로피 오차를 구하는 경우)이라면, reshape 함수로 데이터의 형상을 바꿔준다. 그리고 배치의 크기로 나눠 정규화하고 이미지 1장당 평균의 교차 엔트로피 오차를 계산한다.

  • 정답 레이블이 숫자 레이블로 주어졌을 때의 교차 엔트로피 오차 구현
def cross_entropy_error(y,t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
        
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size),t]+1e-7))/batch_size

이 구현에서 원-핫 인코딩일 때 t가 0인 원소는 교차 엔트로피 오차도 0이므로, 그 계산은 무시해도 좋다는 것이 핵심이다. 그래서 두 코드 간에 바뀌는 부분이 생긴 것이다.

 

np.log(y[np.arange(batch_size),t] 를 설명하자면, np.arange(batch_size)는 0부터 batch_size-1까지 배열을 생성한다. t에는 레이블이 [2,7,0,9,4]와 같이 저장되어 있으므로 np.log(y[np.arange(batch_size),t]는 각 데이터의 정답 레이블에 해당하는 신경망의 출력을 추출한다. (예를 들어, [y[0,2], y[1,7], y[2,0]...] )

 

5. 손실 함수를 설정하는 이유

정확도라는 지표를 놔두고 손실 함수의 값이라는 우회적인 방법을 택하는 이유는?

 

신경망 학습에서의 미분의 역할에 주목하면 된다. 신경망 학습에서는 최적의 매개변수(가중치와 편향)를 탐색할 때 손실 함수의 값을 가능한 한 작게 하는 매개변수의 값을 찾는다. 이때 매개변수의 미분(기울기)을 계산하고, 그 미분 값을 단서로 매개변수의 값을 서서히 갱신하는 과정을 반복한다.

 

 

가중치 매개변수의 손실 함수의 미분이란 가중치 매개변수의 값을 아주 조금 변화 시켰을 때, 손실 함수가 어떻게 변하나라는 의미이다. 미분 값이 음수면 그 가중치 매개변수를 양의 방향으로 변화시켜 손실 함수의 값을 줄일 수 있다. 양수면 음의 방향으로 변화시키면 된다. 하지만 미분 값이 0이면 가중치 매개변수를 어느 쪽으로 움직여도 손실함수의 값은 줄어들지 않기 때문에 가중치 매개변수의 갱신은 거기서 멈춘다.

 

정확도를 지표로 삼아서 안 되는 이유는 매개변수의 미분이 대부분의 장소에서 0이 되기 때문이다. 그 이유는?

 

손실 함수의 값은 연속적이고, 변화했을 때도 값이 연속적이지만 정확도는 연속적이지 않고 불연속적인 띄엄띄엄한 값으로 바뀐다.

 

정확도는 매개변수의 미세한 변화에는 거의 반응을 보이지 않고, 반응이 있더라도 그 값이 불연속적으로 갑자기 변한다. 이는 계단 함수를 활성화 함수로 사용하지 않는 이유와도 맞는다. 계단 함수의 미분은 대부분의 장소(0 이외의 곳)에서 0이기 때문에 손실함수로 지표로 삼으면 아무 의미가 없게 된다.

 

계단함수는 대부분의 장소에서 기울기가 0이지만 시그모이드 함수는 함수의 기울기(접선)가 0이 아니다

계단함수는 한순간만 변화를 일으키지만, 시그모이드 함수의 미분은 출력(세로축의 값)이 연속적으로 변하고 곡선의 기울기도 연속적으로 변한다. 즉, 시그모이드 함수의 미분은 어느 장소라도 0이 되지 않는다. 그래서 기울기가 0이 되지 않는 덕분에 신경망이 올바르게 학습할 수 있는 것이다.

'Deep Learning 1 > 신경망 학습' 카테고리의 다른 글

5. 학습 알고리즘 구현  (0) 2020.07.21
4. 기울기  (0) 2020.07.20
3. 수치 미분  (0) 2020.05.04
1. 데이터에서 학습  (0) 2020.04.28

 

학습이란 훈련 데이터로부터 가중치 매개변수의 최적값을 자동으로 획득하는 것을 의미한다.

손실 함수란 신경망이 학습할 수 있도록 하는 지표이며, 손실 함수의 결과값을 가장 작게 만드는 가중치 매개변수를 찾는 것이 목표이다. 손실 함수의 값을 가급적 작게 만드는 기법으로 함수의 기울기를 활용하는 경사법이 있다.

 

신경망의 특징은 데이터를 보고 학습할 수 있다는 점이다. 데이터에서 학습한다는 것은 가중치 매개변수의 값을 데이터를 보고 자동으로 결정한다는 뜻이다. 신경망의 학습은 데이터로부터 매개변수의 값을 정하는 방법이다.

 

퍼셉트론도 직선으로 분리할 수 있는 문제라면 데이터로부터 자동으로 학습할 수 있지만 비선형 분리 문제는 자동으로 학습할 수 없다.

 

1. 데이터 주도 학습

이미지에서 특징(feature)을 추출하고 그 특징의 패턴을 머신러닝 기술로 학습하는 방법이 있다. 여기서 특징은 입력 데이터에서 본질적인 데이터(중요한 데이터)를 정확하게 추출할 수 있도록 설계된 변환기를 가리킨다. 이미지의 특징은 보통 벡터로 기술하고, 컴퓨터 비전 분야에서 SIFT, SURF, HOG 등의 특징을 많이 사용한다. 이런 특징을 사용하여 이미지 데이터를 벡터로 변환하고, 변환된 벡터를 가지고 지도 학습 방식의 분류 기법인 SVM, KNN 등으로 학습할 수 있다.

 

다만 이미지를 벡터로 변환할 때 사용하는 특징은 여전히 사람이 설계한다. 문제에 적합한 특징을 쓰지 않으면 좋은 결과를 얻을 수 없다.

 

회색 블록은 사람이 개입하지 않음을 의미한다.

신경망은 이미지를 있는 그대로 학습한다. 그래서 딥러닝은 종단간 기계학습(end-to-end machine learning)이라고도 한다. 신경망의 이점은 모든 문제를 같은 맥락에서 풀 수 있다는 점에 있다.

 

2. 훈련 데이터와 시험 데이터

머신러닝 문제에서 데이터는 훈련 데이터(training data)와 시험 데이터(test data)로 나눠 학습과 실험을 수행하는 것이 일반적이다. 우선 훈련 데이터만 사용하여 학습하면서 최적의 매개변수를 찾고 시험 데이터를 사용하여 앞서 훈련한 모델의 실력을 평가하는 것이다. 두개를 나누는 이유는 모델을 범용적으로 사용할 수 있어야 하기 때문에 이것을 제대로 평가하기 위해서이다.

 

범용 능력은 아직 보지 못한 데이터(훈련 데이터에 포함되지 않는 데이터)로도 문제를 올바르게 풀어내는 능력이다. 그래서 데이터셋 하나로만 매개변수의 학습과 평가를 수행하면 올바른 평가가 될 수 없다. 그렇게 된다면 오버피팅(overfitting, 한 데이터셋에만 지나치게 최적화된 상태)이 발생할 수 있다. 

'Deep Learning 1 > 신경망 학습' 카테고리의 다른 글

5. 학습 알고리즘 구현  (0) 2020.07.21
4. 기울기  (0) 2020.07.20
3. 수치 미분  (0) 2020.05.04
2. 손실 함수  (0) 2020.04.29

 

여기서는 이미 학습된 매개변수를 사용하여 학습 과정은 생략하고, 추론 과정만 구현한다. 이 추론 과정을 신경망의 순전파(forward propagation)라고 한다.

 

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

 

1. MNIST 데이터셋

MNIST 데이터셋은 0부터 9까지의 숫자 이미지로 구성되고, 훈련 이미지가 60,000장, 시험 이미지가 10,000장 준비된다.

 

MNIST 이미지 데이터셋의 예

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

 

MNIST 데이터셋을 내려받아 이미지를 넘파이 배열로 변환해주는 스크립트인 mnist.py 에 정의된 load_mnist() 함수를 이용하여 MNIST 데이터를 가져온다.

 

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

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

#각 데이터의 형상 출력
print(x_train.shape)
print(t_train.shape)
print(x_test.shape)
print(t_test.shape)
[실행 결과]
(60000, 784)
(60000,)
(10000, 784)
(10000,)

 

코드를 보면 가장 먼저 부모 디렉터리의 파일을 가져올 수 있도록 설정하고 dataset/mnist.py의 load_mnist 함수를 임포트한다. 그리고 load_mnist 함수로 MNIST 데이터셋을 읽는다. 최초 실행 시에는 인터넷에 연결된 상태여야 하고, 두번째 부터는 pickle 파일을 읽는다.

 

각 예제에서 mnist.py 파일을 찾으려면 부모 디렉터리로부터 시작해야 해서 sys.path.append(os.pardir) 문장을 추가했다.

 

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 형태로 저장할 지를 정한다. 원-핫 인코딩은 정답을 뜻하는 원소만 1이고 나머지는 모두 0인 배열이다. False면 숫자 형태의 레이블을 저장하고, True일 때는 레이블을 원-핫 인코딩하여 저장한다.
피클은 프로그램 실행 중에 특정 객체를 파일로 저장하는 기능이다. 저장해둔 pickle 파일을 로드하면 실행 당시의 객체를 복원할 수 있다.

 

이미지 표시에는 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)

print(img.shape)
img = img.reshape(28,28) #원래 이미지의 모양으로 변형
print(img.shape)
img_show(img)
[실행 결과]
5
(784,)
(28, 28)

 

MNIST 이미지 중 하나

주의 사항은, flatten=True로 설정해 읽어 들인 이미지는 1차원 넘파이 배열로 저장되어 있기 때문에 28X28 크기로 다시 변형해야 한다. reshape() 메서드는 원하는 형상을 인수로 지정하면 넘파이 배열의 형상을 바꿀 수 있다. 또, 넘파이로 저장된 이미지 데이터를 PIL용 데이터 객체로 변환해야 하며, Image.fromarray()은 이미지 변환을 수행한다.

 

2. 신경망의 추론 처리

구현할 신경망은 입력층 뉴런을 784개(이미지 크기가 28X28=784), 출력층 뉴런을 10개(0~9의 숫자 구분)로 구성한다. 은닉층은 총 두개로, 첫번째 은닉층에는 50개의 뉴런을, 두번째 은닉층에는 100개의 뉴런을 배치한다. 50과 100은 임의로 정한 값이다.

 

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
    z3 = sigmoid(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(str(float(accuracy_cnt)/len(x)))
[실행 결과]
0.9352

 

가장 먼저 MNIST 데이터셋을 얻고 네트워크를 생성한다. 이어 for 문을 돌며 x에 저장된 이미지 데이터를 1장씩 꺼내 predict() 함수로 분류한다. predict() 함수는 각 레이블의 확률을 넘파이 배열로 반환한다. 그리고 np.argmax() 함수로 이 배열에서 값이 가장 큰(확률이 가장 높은) 원소의 인덱스를 구하는데, 이것이 바로 예측 결과이다. 마지막으로, 신경망이 예측한 답변과 정답 레이블을 비교하여 맞힌 숫자(accuracy_cnt)를 세고, 이를 전체 이미지 숫자로 나눠 정확도를 구한다.

 

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

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

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

 

전처리의 예로, 전체 평균과 표준편차를 이용하여 데이터들이 0을 중심으로 분포하도록 이동하거나 데이터의 확산 범위를 제한하는 정규화를 수행한다. 그 외에도 전체 데이터를 균일하게 분포시키는 데이터 백색화(whitening) 등도 있다.

 

3. 배치 처리

이번에는 입력 데이터와 가중치 매개변수의 형상에 주의해서 살펴본다.

신경망 각 층의 가중치 형상을 출력

 

x, _ = get_data()
network = init_network()
W1, W2, W3 = network['W1'], network['W2'], network['W3']
print(x.shape)
print(x[0].shape)
print(W1.shape)
print(W2.shape)
print(W3.shape)
[실행 결과]
(10000, 784)
(784,)
(784, 50)
(50, 100)
(100, 10)

 

다차원 배열의 대응하는 차원의 원소 수가 일치한다. 최종 결과로는 원소가 10개인 1차원 배열 y가 출력된다.

 

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

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

 

이미지 여러 장을 한꺼번에 입력하는 경우에는 묶어서 predict() 함수에 한번에 넘긴다. x의 형상을 100X784로 바꿔서 100장 분량의 데이터를 하나의 입력 데이터로 표현하면 된다.

 

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

입력 데이터의 형상은 100X784, 출력 데이터의 형상은 100X10이 된다. 이는 100장 분량 입력 데이터의 결과가 한번에 출력됨을 의미한다. x[0]와 y[0]에는 0번째 이미지와 그 추론 결과가, x[1]과 y[1]에는 1번째의 이미지와 그 결과가 저장되는 식이다.

 

배치(batch)는 하나로 묶은 입력 데이터를 말한다. 배치가 곧 묶음이란 의미이다.

 

배치 처리는 컴퓨터로 계산할 때 큰 이점을 준다. 이미지 1장당 처리 시간을 대폭 줄여주는 것이다. 첫번째 이유는 수치 계산 라이브러리 대부분이 큰 배열을 효율적으로 처리할 수 있도록 고도로 최적화되어 있기 때문이다. 두번째 이유는 커다란 신경망에서는 데이터 전송이 병목으로 작용하는 경우가 자주 있는데, 배치 처리를 함으로써 버스에 주는 부하를 줄일 수 있기 때문이다. 즉, 배치 처리를 수행함으로써 큰 배열로 이뤄진 계산을 하게 되는데, 컴퓨터에서는 분할된 작은 배열을 여러번 계산하는 것보다 큰 배열을 한꺼번에 계산하는 것이 훨씬 빠르다.

 

배치 처리 구현

 

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(str(float(accuracy_cnt)/len(x)))
[실행 결과]
0.9352

 

range(start, end, step)처름 인수를 3개 지정하면 start에서 end-1까지 step 간격으로 증가하는 리스트를 반환한다.

 

이 range()함수가 반환하는 리스트를 바탕으로 x[i:i+batch_size]에서 입력 데이터를 묶는다. x[0:100], x[100:200]과 같은 방식으로 100장씩 묶어서 꺼낸다.

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

 

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]

 

마지막으로 배치 단위로 분류한 결과를 실제 답과 비교한다. 이를 위해 == 연산잘ㄹ 사용해 넘파이 배열끼리 비교하여 Bool 배열을 만들고, 이 결과에서 True의 갯수를 센다.

 

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

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

 

7. 정리

신경망의 순전파는 각 층의 뉴런들이 다음 층의 뉴런으로 신호를 전달한다는 점에서 퍼셉트론과 같지만, 다음 뉴런으로 갈 때 신호를 변화시키는 활성화 함수에 큰 차이가 있다. 신경망에서는 매끄럽게 변화하는 시그모이드 함수를, 퍼셉트론에서는 갑자기 변화하는 계단 함수를 활성화 함수로 사용했다.

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

'Deep Learning 1 > 신경망' 카테고리의 다른 글

5. 출력층 설계  (0) 2020.04.24
4. 3층 신경망 구현하기  (0) 2020.04.23
3. 다차원 배열의 계산  (0) 2020.03.31
2. 활성화 함수  (0) 2020.03.18
1. 퍼셉트론에서 신경망으로  (0) 2020.02.12

+ Recent posts