본문 바로가기
DataScience/딥러닝

딥러닝 :: 밑바닥부터 시작하는 딥러닝 Chap4. 신경망 학습

by 올커 2023. 5. 5.

밑바닥부터 시작하는 딥러닝 Chapter 04. 신경망 학습

본 포스팅은

「밑바닥부터 시작하는 딥러닝 - 사이토 고키, 한빛미디어」라는 서적을 참고하였으며,

작성자가 공부한 내용을 기록하는 목적으로 작성하였습니다


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

  신경망이 학습하기 위해서는 '손실함수'가 사용되며, 이 손실함수의 결과값을 최소로 하는 가중치 매개변수를 찾는 것이 학습의 목표이다. 본 포스팅에서는 이러한 방법 중 "경사법"을 다룬다.

 

 

4.1 데이터에서 학습한다!

 

4.1.1 데이터 주도 학습

기계학습은 주로 아래와 같이 두 가지 단계를 통해 데이터(Data)로부터 정답이나 패턴을 찾는다.

 · 예를들어 컴퓨터비전 분야에서는

   1) 데이터에서 특징(Feature)을 추출(SIFT, SURF, HOG 등)하고 이를 벡터로 변환 후 (설계를 사람이 함)

   2) SVM, KNN과 같은 머신러닝 분류기법을 통해 학습한다. 

신경망(딥러닝) 방식은 위의 절차를 '사람이 개입하지 않고' 이미지를 있는 그대로 학습한다. 신경망은 모든 문제를 주어진 데이터 그대로를 입력 데이터로 활용하여 'End-to-End'로 학습한다.

 

4.1.2 훈련 데이터와 시험 데이터

학습된 모델을 '범용적'으로 사용하기 위해 훈련 데이터로 학습시키고, 시험 데이터로 검증한다.

*하나의 데이터넷에 지나치게 최적화 할경우 오버피팅(Overfitting)이 발생할 수 있다.

 

 

4.2 손실 함수

최적의 매개변수 값을 탐색하기 위해 신경망에서 사용되는 '지표' 로 일반적으로 평균제곱오차(MSE)와 교차 엔트로피 오차가 사용된다.

4.2.1 평균 제곱 오차(Mean Squared Error, MSE)

평균제곱오차는 수식으로 나타내면 아래와 같다. (*y_k는 신경망의 출력, t_k는 정답 레이블, k는 데이터 차원의 수)

 

4.2.3 교차 엔트로피 오차(Cross Entropy Error, CEE)

교차 엔트로피 오차는 수식으로 나타내면 아래와 같다. (*y_k는 신경망의 출력, t_k는 정답 레이블, k는 데이터 차원의 수)

 

▲ y = logx의 그래프

아래는 크로스 엔트로피의 함수를 구현하였다. 여기서 delta는 아주 작은 수를 의미하며, 이는 np.log()함수에 0을 입력시 음의 방향으로 발산하는 것을 막기 위한 장치이다.

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

 

4.2.3 미니배치 학습

MNIST Dataset을 읽어와서 훈련 데이터와 테스트 데이터를 아래와 같이 나누어준 후 10개의 데이터가 있는 batch를 훈련 데이터, 테스트 데이터 각각 생성한다.

import sys, os
# sys.path.append(os.pardir)
sys.path.append(os.getcwd())

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)        # (60000, 784)
print(t_train.shape)        # (60000, 10)

# 훈련데이터에서 무작위로 10개 추출
train_size = x_train.shape[0]       # 범위
batch_size = 10                     # 원하는 갯수
batch_mask = np.random.choice(train_size, batch_size)       # 지정한 범위내에서 무작위로 원하는 갯수만큼 추출
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]

 

4.2.4 (배치용) 교차 엔트로피 오차 구현

교차 엔트로피 함수는 아래와 같이 구현하였다. (*y는 신경망의 출력, t는 정답 레이블)

# 교차 엔트로피 오차 함수구현
def cross_entropy_error(y, t):
    # y가 1차원일 경우 reshape를 통해 데이터 형상을 바꾸어준다.
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
    
    batch_size = y.shape[0]
    
    # 결과 리턴 (1) _ 배치 사이즈로 나누어 정규화(1장당 평균) - one-hot-encoding일 경우
    return -np.sum(t * np.log(y)) / batch_size

    # 결과 리턴 (2)_ 배치 사이즈로 나누어 정규화(1장당 평균) - one-hot-encoding이 아닐 경우
    return -np.sum(np.log(y[np.arange(batch_size), t])) / batch_size

코드 마지막줄인 아래 retrun 결과에 대해서 다시 보면

return -np.sum(np.log(y[np.arange(batch_size), t])) / batch_size

np.arrange(batch_size)는 0부터 batch_size-1까지 배열을 생성하고 이를 t와 함께 y의 인자로 넣는다.

예를 들면 batch_size가 5이고, t = [2, 7, 0, 9, 4]일 경우, y[0, 2], y[1, 7], y[2, 0], y[3, 9], y[4, 4]인 넘파이 배열을 생성한다.

 

4.2.5 왜 손실 함수를 설정하는가?

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

 신경망을 학습할 때 정확도를 지표로 하면 매개변수의 미분이 대부분의 장소에서 0이 되므로 이는 피하여야 한다.

  *정확도 → 매개변수의 미소한 변화에는 거의 반응을 보이지 않고, 반응이 있더라도 그 값이 불연속적으로 갑자기 변화

 

 

4.3 수치 미분

 

4.3.1 미분

미분의 기본 공식은 아래와 같다.

코드로 구현할 때 h를 무한히 0으로 좁히는 것은 불가능하기 때문에 계산 오차를 발생시킨다. 즉, 수치 미분(numerical differentiation)에는 오차가 포함된다. 이러한 오차를 줄이기 위해 (x-h)와 (x+h)일때의 함수 f의 차분을 계산하는 방법을 쓰기도 하며 이를 '중심 차분'이라고 한다.

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

 

4.3.2 수치 미분의 예

아래와 같은 2차함수가 있다고 했을 때 수치미분을 통한 미분을 구현해보자.

import numpy as np
import matplotlib.pylab as plt

# 함수 정의
def function_1(x):
    return 0.01*x**2 + 0.1*x

x = np.arange(0.0, 20.0, 0.1)
y = function_1(x)
plt.xlabel("x")
plt.ylabel("f(x)")
plt.plot(x, y)
plt.show()

x = 5, x = 10에 대한 미분값들을 구한 후 각 미분값을 통해 접선을 구하면 아래와 같다.

(*붉은색 선 : x = 5에 대한 접선, 초록색 선 : x = 10에 대한 접선)

 

4.3.3 편미분

 변수가 여럿인 함수에 대해서는 편미분이라는 개념을 가져와서 미분할 수 있다.

 예를들어 아래와 같은 식이 있다고 할 경우, 편미분의 수식은 아래와 같다.

 

위에서 미리 생성했던 numerical_diff를 아래와 같다고 하자.

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

위 식을 이용하여 코드를 작성하면 다음과 같이 편미분을 구할 수 있다.

 

4.4 기울기

위의 편미분과 같이 계산한다고 했을 때, 이를 아래와 같이 벡터로 정리한 것을 기울기(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

세 점 (3, 4), (0, 2), (3, 0)에서의 기울기를 구해보면 아래와 같다.

이 기울기를 그래프를 그려 확인해보면 아래와 같다.

기울기는 각 지점에서 값이 낮아지는 방향 즉, 함수의 출력값을 가장 크게 줄이는 방향이다.

 

4.4.1 경사법(경사 하강법)

 최적의 매개변수를 찾아내는 것은 손실함수가 최소가 될때의 매개변수를 찾는 것과 같다. 그리고 최소가 되는 손실함수를 찾기 위해서는 위에서 다룬 미분을 활용한 방식인 경사법(경사 하강법)을 사용한다. 다만, 주의할 점으로 각 지점에서 함수의 값을 낮추는 방안을 제시하는 지표가 기울기인데, 이 기울기가 가리키는 곳이 '정말 함수의 최소값이 있는지'는 보장할 수 없다. (*찾아낸 값이 최솟값은 아닌 local 극소값, 또는 안장점, saddle point일 경우 미분값은 0이지만 최소가 아니다.)

 

경사법은 현 위치에서 기울어진 방향으로 일정 거리만큼 이동한다. 그런 다음 이동한 곳에서도 마찬가지로 기울기를 구하고, 또 그 기울어진 방향으로 나아가기를 반복한다. 이렇게 하여 함수의 값을 점차 줄이는 것을 경사법, gradient method라고 하며 그 수식은 아래와 같이 나타낸다. (*η, eta : 갱신하는 양, 학습률, learning rate)

위 식은 1회에 해당하는 갱신이며, 이 단계를 반복하면서 최솟값을 찾아나간다. 이를 파이썬으로 코드화하면 아래와 같다.

(*f : 최적화하려는 함수, init_x : 초깃값, lr : 학습률, 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

초깃값을 (-3.0, 4.0)으로 하여 아래 수식을 gradient descent를 이용하여 최솟값을 찾을 수 있다.

경사법에 의한 위 식의 갱신과정을 그래프로 표현하면 아래와 같다.

*주의할 점으로 학습률이 너무 크거나 작으면 아래와 같이 좋은 결과를 얻기 어렵다.

 

 

4.4.2 신경망에서의 기울기

 신경망 학습에서 기울기를 구하는 것을 생각해보자. 예를 들어 형상이 2×3, 가중치가 W, 손실함수가 L인 신경망의 경우 가중치와 경사는 아래와 같이 나타낼 수 있다.

▼ 신경망의 기울기를 구하는 간단한 simpleNet 클래스

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

가중치 매개변수 및 정답레이블을 구해보면 아래와 같다.

기울기를 구하면 아래와 같다.

4.5 학습 알고리즘 구하기

 신경망 학습의 절차

  - 1단계(미니배치) : 훈련 데이터 중 일부를 무작위로 가져와서 데이터를 선별한다.(미니배치 생성)

  - 2단계(기울기 산출) : 미니배치의 손실 함수 값을 줄이기 위해 가중치 매개변수의 기울기를 구한다.

 

  - 3단계(매개변수 갱신 및 1~3단계 반복) : 가중치 매개변수를 기울기 방향으로 조금씩 갱신하며 반복한다.

 

 경사하강법으로 매개변수를 갱신할 때, 데이터를 미니배치로 무작위로 선정하기 때문에 확률적 경사 하강법(stochastic gradient descent, SGD)이라고 부른다.

 

4.5.1 2층 신경망 클래스 구현하기

# coding: utf-8
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(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])
        return accuracy
        
    # 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['W2'])
        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
        
        return grads
        
    def gradient(self, x, t):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        grads = {}
        
        batch_num = x.shape[0]
        
        # forward
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        # backward
        dy = (y - t) / batch_num
        grads['W2'] = np.dot(z1.T, dy)
        grads['b2'] = np.sum(dy, axis=0)
        
        da1 = np.dot(dy, W2.T)
        dz1 = sigmoid_grad(a1) * da1
        grads['W1'] = np.dot(x.T, dz1)
        grads['b1'] = np.sum(dz1, axis=0)

        return grads

 

 

 

4.5.2 미니배치 학습 구현하기

 

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

 

 

 

4.5.3 시험 데이터로 평가하기

 

import sys, os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
import matplotlib.pyplot as plt
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 = []
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 = t_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))

# 그래프 그리기
markers = {'train': 'o', 'test': 's'}
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, label='train acc')
plt.plot(x, test_acc_list, label='test acc', linestyle='--')
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()

반응형

댓글