[밑바닥부터 시작하는 딥러닝] 4. 신경망 학습 part2 - 수치 미분, 기울기

Yejin Kim·2022년 3월 9일
0

🌿 수치 미분

numerical differentiation

💡 미분이란 ?

한순간의 변화량

x의 '작은 변화'가 함수 f(x)를 얼마나 변화시키는지를 의미
이때 시간의 작은 변화, 즉 시간을 뜻하는 h를 한없이 0에 가깝게 한다는 의미를 표현하기 위해 lim을 사용

이는 다음과 같이 구현이 가능하다

def numerical_diff(f, x):
	h = 1e-50
    return (f(x + h) - f(x)) / h

하지만 이 코드에는 개선해야 할 점이 2가지가 있다.

  • 반올림 오차
    h에 가급적 작은 값을 대입하기 위해 1e-50이라는 작은 값을 이용
    하지만 이 값은 반올림 오차 문제를 일으킴
    >>> np.float32(1e-50)
    0.0
    이 미세한 값 h로 1e-4를 이용하면 좋은 결과를 얻는다고 알려져 있음
  • 함수 f의 차분
    진정한 미분은 x 위치의 함수 기울기(접선)에 해당하지만 이 구현에서는 (x+h)와 x 사이의 기울기에 해당 → 엄밀히 일치하지 않음

    오차를 줄이기 위해 (x+h)와 (x-h)일 때의 함수 f의 차분을 계산하는 방법을 쓰기도 함 (중심 차분 또는 중앙 차분)

개선 후 코드는 다음과 같다.

def numerical_diff(f, x):
	h = 1e-4 # 0.0001
    return (f(x+h) - f(x-h)) / (2*h)

편미분


위의 함수는 변수가 2개인 함수이다.
이 코드는 다음과 같이 구현이 가능하다.

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


이 함수는 변수가 2개이므로 어느 변수에 대한 미분이냐, 즉 x0과 x1 중 어느변수에 대한 미분인가를 구분해야 함
이처럼 변수가 여럿인 함수에 대한 미분을 편미분이라고 함

여러 변수 중 목표 변수 하나에 초점을 맞추고 다른 변수는 값을 고정 후 목표 변수에 대해 미분 진행

ex ) x0=3, x1=4일 때, x0에 대한 편미분

>>> def function_tmp1(x0):
		return x0*x0 + 4.0**2.0
...
...
>>> numerical_diff(function_tmp1, 3.0)
6.00000000000378

기울기

gradient

모든 변수의 편미분을 벡터로 정리한 것

다음과 같이 구현이 가능하다.

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
  • x와 형상이 같고 그 원소가 모두 0인 배열을 만듦
  • 넘파이 배열 x의 각 원소에 대해서 수치 미분을 구함

위의 과정을 통해 (x0, x1, ..., xn)의 각 점에서의 기울기를 계산할 수 있다.
이 기울기가 의미하는 바를 그림으로 표현하면 다음과 같다.
** 그림은 기울기 결과에 마이너스를 붙인 벡터를 나타냄


기울기는 방향을 가진 벡터로 표현

위 그림을 보면 다음을 알 수 있다.

  • 기울기는 함수의 '가장 낮은 장소(최솟값)'을 가리킨다고 볼 수 있음
  • 또한, '가장 낮은 곳'에서 멀어질수록 화살표의 크기가 커짐

기울기는 각 지점에서 낮아지는 방향을 가리킴 (반드시 최솟값은 아님 - local minimum)
정확히는 기울기가 가리키는 쪽은 각 장소에서의 함수 출력 값을 가장 크게 줄이는 방향이라고 할 수 있다.

경사법(경사 하강법)

gradient method

기계학습은 학습에서 최적의 매개변수를 찾아야 함.
최적의 매개변수를 구하기 위해서는 손실 함수가 최소가 되는 때의 매개변수를 얻어야 하는데,
기울기를 활용하여 함수의 최솟값을 찾도록 하는 방법이 경사법

  • 현 위치에서 기울어진 방향으로 일정 거리 이동
  • 다음 이동한 곳에서도 기울기를 구하고 기울어진 방향으로 나아감

위의 과정을 반복하여 함수의 값을 점차 줄여 나감

η 기호(에타)는 갱신하는 양을 나타냄 = 학습률(learning rate)
→ 매개변수 값을 얼마나 갱신할지를 결정

위의 식은 1번의 갱신에 해당하고 이를 반복하면서 서서히 함수의 값을 줄이게 된다.

경사 하강법은 다음과 같이 구현이 가능하다.

# f : 최적화하녀는 함수
# init_x : 초깃값
# lr : learning rate
# step_num : 반복 횟수

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

경사법을 사용한 갱신 과정을 그림으로 나타내면 다음과 같다.

학습률이 너무 크면 발산할 수 있고, 반대로 너무 작으면 거의 갱신되지 않은 채로 학습이 끝나기 때문에 학습률을 적절히 설정하는 것이 중요하다.

학습률같은 매개변수를 하이퍼파라미터(hyper parameter)라고 함

가중치와 편향 같은 매개변수가 학습 알고리즘에 의해 '자동'으로 획득되는 반면,
하이퍼파라미터는 사람이 직접 설정해야 함
→ 여러 후보 값 중 시험을 통해 최적의 값을 선정하는 과정 필요

신경망에서의 기울기

신경망에서는 가중치 매개변수에 대한 손실 함수의 기울기를 계산한다

학습 알고리즘의 구현

신경망 학습의 절차는 다음과 같이 정리할 수 있다.

  • 전제
    • 신경망에는 적응 가능한 가중치와 평향이 있음
      학습은 이 가중치와 편향을 훈련 데이터에 적응 하도록 조정하는 과정
  • 1단계 - 미니배치
    • 훈련 데이터 중 일부를 무작위로 선별 (= 미니배치)
      미니배치의 손실 함수 값을 줄이는 것이 목표
  • 2단계 - 기울기 산출
    • 각 가중치 매개변수의 기울기를 구함
      기울기는 손실 함수의 값을 가장 작게 하는 방향을 제시
  • 3단계 - 매개변수 갱신
    • 가중치 매개변수를 기울기 방향으로 아주 조금 갱신
  • 4단계 - 반복
    • 1~3단계 반복

데이터를 미니배치로 무작위 선정하기 때문에 확률적 경사 하강법(stochastic gradient descent, SGD)이라고 부름

2층 신경망 클래스 구현

MNIST 데이터셋을 사용하여 손글씨 숫자를 학습하는 신경망을 구현해보자

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 = sigmoid(a1)
        a2 = np.dot(x, 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])
        return accuracy
        
    # x : 입력 데이터, t : 정답 레이블
    def numerical_gradient(self, x, t):
    	loss_W = lambda W: self.loss(x, t)
        
        grads = {}
        # 손실함수에 대해서 가중치와 편향에 대한 기울기를 계산 !
        grads['W1'] = nemerical_gradient(loss_W, self.params['W1'])
        grads['b1'] = nemerical_gradient(loss_W, self.params['b1'])
        grads['W2'] = nemerical_gradient(loss_W, self.params['W2'])
        grads['b2'] = nemerical_gradient(loss_W, self.params['b2'])
        
        return grads

수치 미분 방식으로 각 매개변수의 손실 함수에 대한 기울기 계산까지 구현했다.

다음 장 오차역전파법 을 배우고 gradient(self, x, t)를 구현하여 기울기 계산을 고속으로 수행할 수 있도록 한다.

미니배치 학습 구현

훈련 데이터 중 일부를 무작위로 꺼내고(미니배치), 그 미니배치에 대해 경사법으로 매개변수 갱신

import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

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

train_loss_list = []

# 하이퍼파라미터
iters_num = 10000 # 반복 횟수
train_size = x_train.shape[0]
batch_size = 100 # 미니배치 크기
learning rate = 0.1

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

for i in range(iters_num):
	# 미니배치 획득
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = x_train[batch_mask]
    
    # 기울기 계산
    grad = network.numerical_gradient(x_batch, t_batch)
    # grad = network.gradient(x_batch, t_batch) # 성능 개선판 !
    
    # 매개변수 갱신
    for key in ('W1', 'b1', 'W2', 'b2'):
    	network.params[key] -= learning_rate * grad[key]
        
    # 학습 경과 기록
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)

미니배치 크기를 100으로 함
→ 매번 60000 개의 훈련 데이터에서 임의로 100개의 데이터 추출

추출된 100개의 미니배치를 대상으로 확률적 경사하강법 수행, 매개변수 갱신
10000번 갱신을 반복하고, 갱신 때마다 훈련 데이터에 대한 손실 함수를 계산하여 배열에 저장한 뒤 그 값의 변화 추이를 그래프로 살펴보면 다음과 같다.

학습 횟수가 늘어나면서 손실 함수의 값이 줄어듦
데이터를 반복 학습하면서 최적 가중치 매개변수로 다가가고 있음을 알 수 있다.

시험 데이터로 평가하기

신경망 학습에서는 훈련 데이터 외의 데이터를 올바르게 인식하는지를 확인해야 함
'오버피팅'을 일으키지 않는지 확인해야 함

오버피팅은 훈련 데이터만 제대로 구분하고, 그렇지 않은 데이터들은 식별할 수 없는 경우를 말한다.
신경망 학습의 원래 목표가 범용적인 능력을 익히는 것이기 때문에 시험 데이터에 대한 정확도를 파악해야 한다.

1에폭별로 훈련 데이터와 시험 데이터에 대한 정확도를 기록하는 코드를 살펴보자

에폭(epoch)은 하나의 단위
1에폭 : 학습에서 훈련 데이터를 모두 소진했을 때의 횟수
ex)
훈련 데이터 10000개를 100개의 미니배치로 학습하는 경우, SGD를 100회 반복하면 훈련 데이터를 모두 '소진' 한 것
→ 100회가 1에폭

import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

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

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

# 하이퍼파라미터
iters_num = 10000 # 반복 횟수
train_size = x_train.shape[0]
batch_size = 100 # 미니배치 크기
learning rate = 0.1

train_loss_list = []
# 정확도 추이 저장할 list
train_acc_list = []
test_acc_list = []

# 1에폭당 반복 수
iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
	# 미니배치 획득
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = x_train[batch_mask]
    
    # 기울기 계산
    grad = network.numerical_gradient(x_batch, t_batch)
    # grad = network.gradient(x_batch, t_batch) # 성능 개선판 !
    
    # 매개변수 갱신
    for key in ('W1', 'b1', 'W2', 'b2'):
    	network.params[key] -= learning_rate * grad[key]
        
    # 학습 경과 기록
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)
    
    # 1에폭당 정확도 계산
    if i % iter_per_epoch == 0:
    	train_acc = network.accuracy(x_train, t_train)
        test_acc = network.accuracy(x_test, t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print("train acc, test acc | " + str(train_acc) + ", " + str(test_acc))

위의 코드로 얻은 결과는 다음과 같다.

훈련 데이터와 시험 데이터에 대한 정확도 추이를 보면
학습이 진행될수록 정확도가 높아짐을 알 수 있다.
또 두 정확도에 차이가 없는 것으로 보아 이 학습에서는 오버피팅이 일어나지 않음을 알 수 있다.

profile
The World Is My Oyster 🌏

0개의 댓글