본문 바로가기
Minding's Reading/밑바닥부터 시작하는 딥러닝

[밑바닥부터 시작하는 딥러닝] CH.4 신경망 학습

by Minding 2021. 4. 6.
728x90
반응형

데이터셋

github.com/WegraLee/deep-learning-from-scratch

 

WegraLee/deep-learning-from-scratch

『밑바닥부터 시작하는 딥러닝』(한빛미디어, 2017). Contribute to WegraLee/deep-learning-from-scratch development by creating an account on GitHub.

github.com


- 학습 : 훈련 데이터로부터 가중치 매개변수의 최적값을 자동으로 획득 손실함수

   = 신경망이 학습할 수 있도록 해주는 지표

- 손실함수의 결과값을 가장 작게 만드는 가중치 매개변수를 찾는 것이 학습의 목표

 

4.1 데이터에서 학습한다!

신경망의 특징 - 데이터를 보고 학습할 수 있음 (가중치 매개변수의 값을 데이터를 보고 자동으로 결정)

  • 선형분리가 가능한 문제라면, 데이터로부터 자동으로 학습 가능함 - 유한 번의 학습을 통해 풀 수 있다는 사실 증명 (퍼셉트론 수렴 정리)
  • 비선형 분리 문제는 자동으로 학습할 수 없음

4.1.1 데이터 주도학습

기계학습은 데이터가 생명이자 중심

예) MNIST 숫자 '5'를 제대로 분류하는 프로그램

  • 사람이 인식하기는 쉽지만, 그 패턴(규칙성)을 찾아내기는 쉽지 않음
  • 데이터 활용 방법의 하나 : 이미지에서 특징 추출하고 특징패턴 ML기술로 학습
  • 이미지의 특징은 보통 벡터로 기술, CV분야에서는 SIFT, SURF, HOG 등의 특징 사용
  • 지도학습의 대표 분류 기법인 SVM, KNN등으로 학습 가능
  • 규칙 찾아내는 역할은 기계가 담당하나, 문제에 적합한 특징 찾아내는 것은 사람
  • 적합한 특징 사용하지 않으면(설계하지 않으면) 좋은 결과를 얻을 수 없음

딥러닝은 이미지를 '있는 그대로' 학습 (중요한 특징까지 스스로 학습)

  • 딥러닝을 '종단간 기계학습(end-to-end machine learning)이라고도 함.
  • 데이터(입력)에서 목표한 결과(출력)를 개입없이 얻는다는 뜻

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

ML문제는 데이터를 훈련데이터(training data)와 시험데이터(test data)로 나눠 학습과 실험 수행

  • 훈련데이터만 사용하여 최적의 매개변수 찾음
  • 시험데이터를 이용하여 훈련한 모델의 실력 평가

훈련-시험 데이터를 나누는 이유 : 범용능력을 평가하기 위해

  • 범용능력 : 아직 보지 못한 데이터로도 문제를 올바르게 풀어내는 능력

데이터셋 하나로만 매개변수의 학습 및 평가를 수행하면 올바른 평가 될 수 없음

  • 오버피팅(overfitting) : 한 데이터셋에만 지나치게 과적화된 상태

4.2 손실함수

손실함수 : 신경망이 최적의 매개변수 값을 탐색하는 기준 (지표)

  • 임의의 함수 사용가능하지만 일반적으로 오차제곱합과 교차 엔트로피 오차 사용
  • 손실함수는 신경망 성능의 '나쁨'을 나타내는 지표

4.2.1 오차제곱합 (sum of squares for error)

 

  •  : 신경망 모델이 추정한 값
  •  : 정답 레이블 또는 데이터
  • k는 차원의 수
  • 1/2 곱해주는 이유는 미분식 때문 (1/2 * 2 = 1)
import numpy as np

# MSE 함수 구현
def mean_squared_error(y, t):
	return 0.5 * np.sum((y-t)**2)
# 정답이 2일때
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

# 예 1 : 2일 확률이 가장 높다고 했을 때
y = [0.1,0.2,0.6,0.0,0.05,0.02,0.0,0.0,0.0,0.03]
sum_squares_error(np.array(y), np.array(t))

>>> 0.10690000000000004
------------------------------------------------------
# 예 2 : 5일 확률이 가장 높다고 했을 때
y = [0.1,0.2,0.05,0.0,0.5,0.1,0.05,0.0,0.0,0.0]
sum_squares_error(np.array(y), np.array(t))

>>> 0.6075

손실 함수의 출력이 작을수록 정답에 가까울 것으로 판단할 수 있음

 

4.2.2 교차 엔트로피 오차 (cross entropy error)

 

  •  : 밑이 e인 자연로그(loge)
  •  : 신경망 모델의 출력
  •  : 정답 레이블
  • 실질적으로 정답일 때의 추정의 자연로그 계산 (정답이 아닌 것은 tk가 0 이므로 결과영향X)
  • 교차 엔트로피 오차는 정답일 때의 출력이 전체 값을 정하게 됨
  • 자연로그 y=logx의 그래프 : 정답출력이 커질수록 0에 다가가다가, 그 출력이 1일 때 0이 됨

 

Cross entropy 구현

def cross_entropy_error(y, t):
    delta = 1e-7  # log함수에 0을 입력하면 -inf 출력되므로 이를 막기위해 아주 작은 값 더해줌
    return -np.sum(t * np.log(y + delta))
# 정답이 2일때 (0.6의 확률로 2라고 예측)
y = [0.1,0.2,0.6,0.0,0.05,0.02,0.0,0.0,0.0,0.03]
cross_entropy_error(np.array(y), np.array(t))

>>> 0.510825457099338

교차 엔트로피 오차도 마찬가지로 출력이 작을수록 정답에 가까움

 

4.2.3 미니배치 학습

훈련 데이터 모두에 대한 손실함수의 합을 구하는 방법

  •  : 미니배치 크기
  •  : n번째 데이터의 k차원 째의 값을 의미(ynk -추정, tnk -정답)
  • 수식 : 마지막에 N(데이터의 갯수)으로 나누어 '평균 손실 함수' 구함 --> 훈련 데이터 개수와 관계없이 언제든 통일된 지표 얻을 수 있음

미니 배치 : 데이터 일부를 추려 전체의 '근사치'로 이용하는 것

  • 빅데이터를 대상으로 일일이 손실함수 계산하는 것은 현실적이지 않음
  • 예) 60000장 중 100장 랜덤으로 뽑아 학습
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)
                                                  
# 무작위로 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]

np.random.choice() --> 지정한 범위의 수 중에서 무작위로 원하는 개수만 꺼낼 수 있음

무작위로 선택한 인덱스를 사용 / 손실함수도 미니배치로 계산

 

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

# 데이터가 하나인 경우와 데이터가 배치로 묶여 입력될 경우 모두 처리가능하게 구현
def cross_entropy_error(y, t) :
  if y.ndim == 1: #y가 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
# 정답레이블이 원-핫 인코딩이 아닌 경우 (숫자 레이블로 주어진 경우)
def cross_entropy_error(y, t) :
  if y.ndim == 1: #y가 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[np.arange(batch_size), t] + 1e-7)) / batch_size

원-핫 인코딩일 때 t가 0인 원소는 교차 엔트로피 오차도 0이므로, 그 계산은 무시해도 좋다는 것이 핵심

  • 정답에 해당하는 신경망의 출력만으로 교차 엔트로피 오차 계산가능
  • t * np.log(y) 부분을 np.log(y[np.arange(batch_size), t])로 구현
    • np.arange(batch_size)는 0부터 batch_size -1까지 배열 생성
    • 각 데이터의 정답레이블에 해당하는 신경망의 출력 추출
    • 예) 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이 되면 가중치 매개변수의 갱신이 멈춤

정확도를 지표로 삼으면 매개변수의 미분이 대부분의 장소에서 0이 됨

  • 정확도는 매개변수의 미소한 변화에는 거의 반응을 보이지 않고, 반응이 있더라도 그 값이 불연속적으로 갑자기 변화함
  • 계단함수를 활성화함수로 사용하지 않는 것과 같은 이유
  • 매개변수의 작은 변화가 주는 파장을 계단 함수가 말살하여 손실함수의 값에는 아무런 변화가 나타나지 않기 때문

어느 장소라도 함수의 미분(기울기)은 0이 되지 않는게 신경망의 중요한 성질

 

4.3 수치미분

경사법에서는 기울기값을 기준으로 나아갈 방향 정함

 

4.3.1 미분

예 ) 10분에 2km를 달렸다고 했을 때

  • 10분 동안의 '평균속도'를 구한 것
  • 시간을 가능한 한 줄여 '한 순간'의 변화량을 얻는 것이 미분(어느 순간의 속도)
  • 시간을 뜻하는 h를 한없이 0에 가깝게 한다는 의미로 lim로 나타냄
# 미분 구현 (식을 곧이 곧대로 구현할 때)
# 미분 구현의 나쁜 예
def numerical_diff(f, x):
  h = 10e-50
  retrun (f(x+h) - f(x)) / h

함수 f와 함수 f에 넘길 인수 x - 두 인수를 받음, 그러나 개선해야 할 점 있음

  • h에는 가급적 작은 값 대입하고 싶어 (0에 가까이 하고싶어) 너무 작은값 사용 : 반올림 오차문제(작은값 생략) --> 10e-4사용
  • f의 차분(임의 두 점에서의 함수 값들의 차이) 문제
  • x + h와 x 사이의 함수 f의 차분을 계산하지만, 이 계산에는 애당초 오차가 있음
  • '진정한 미분'은 x위치의 함수의 기울기(=접선)에 해당
  • 그러나 구현에서는 (x+h)와 x사이의 기울기이므로 엄밀히 일치하지는 않음
  • h를 무한히 0으로 좁히는게 불가능하여 생기는 한계

수치 미분에는 오차가 포함됨

  • 이 오차를 줄이기 위해 (x+h)와 (x-h)일 때의 함수f의 차분을 계산하는 방법을 쓰기도 함 = 중심차분 or 중앙차분
# 두 개선점 적용해 수치미분 다시 구현
def numerical_diff(f,x):
  h = 1e-4
  return (f(x+h) - f(x-h)) / (2*h)

4.3.2 수치 미분의 예

2차함수 미분해보기 --> y = 0.01x^2 + 0.1x

# 2차 함수 구현하기
def function_1(x):
  return 0.01*x**2 + 0.1*x
  
 # 함수 그려보기
import matplotlib.pyplot as plt

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일때와 10일 때 이 함수의 미분 계산

print(numerical_diff(function_1, 5))
print(numerical_diff(function_1, 10))

>>> 0.1999999999990898
>>> 0.2999999999986347

미분 값은 x에 대한 f(x)의 변화량 = 함수의 기울기

  • x가 5와 10일때 완벽한 미분은 0.2와 0.3
  • 오차가 매우 적어 거의 같은 값이라고 할 수 있음

4.3.3 편미분

f(x0, x1) = x0^2 + x1^2

  • 인수들의 제곱합을 계산하는 식이지만, 앞과달리 변수 2개
# 식 4.6 파이썬 구현
def function_2(x):
  return x[0]**2 + x[1]**2 # 또는 return np.sum(x**2)
  • 인수 x는 넘파이 배열이라고 가정
  • 그래프를 그려보면, 3차원으로 그려짐

[식 4.6] 미분하기

  • 주의할 점 : 변수가 2개라는 점
  • '어느 변수에 대한 미분이냐'를 나타내는 구별해야 함
  • 편미분 : 변수가 여럿인 함수에 대한 미분

편미분 풀어보기

  • 문제1 : x0=3, x1=4일때, x0에 대한 편미분 af/ax0를 구하라
def function_tmp1(x0):
  return x0*x0 + 4.0**2.0
  
  numerical_diff(function_tmp1, 3.0)
  
  >>> 6.00000000000378
  • 문제2 : x0 = 3, x1 = 4일때, x1에 대한 편미분 af/ax1을 구하라
def function_tmp2(x1):
  return 3.0**2.0 + x1*x1
  
  numerical_diff(function_tmp2, 4.0)
  
  >>> 7.999999999999119

편미분을 푸는 방법

    1. 변수가 하나인 함수를 정의
    1. 그 함수를 미분

편미분은 변수가 하나인 미분과 마찬가지로 특정 장소의 기울기를 구함

단, 여러 변수 중 목표 변수 하나에 초점을 맞추고 다른 변수는 값을 고정

 

4.4 기울기

x0과 x1의 편미분을 동시에 계산하기

  • 기울기 : (af/ax0, af/ax1)처럼 모든 변수의 편미분을 벡터로 정리한 것
# 기울기 구현
def numerical_gradient(f, x):
  h = 1e-4
  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
  • numerical_gradient(f,x) 함수의 인수인 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.]
  • 기울기 그림을 통해 의미 알아보기 (코드는 github 참고)

  • 기울기의 결과에 마이너스를 붙인 벡터 그리기
  • 그림의 기울기는 함수의 가장 낮은 장소(최솟값)를 가리킴
  • 최솟값에서 멀어질수록 화살표의 크기가 커짐
  • 그림처럼 반드시 기울기는 가장 낮은 장소를 가리키는 것은 아님
  • 기울기가 가리키는 쪽은 각 장소에서 함수의 출력 값을 가장크게 줄이는 방향

4.4.1 경사법 (경사하강법)

기울기를 이용해 함수의 최솟값(가능한 한 작은값)을 찾으려는 것

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

** 함수가 극솟값, 최솟값, 안장점이 되는 장소에서는 기울기가 0

  • 안장점은 방향에 따라 극댓값이 될 수 있고 극솟값이 될 수 있음
  • 복잡한 함수는 평평한 곳으로 파고들면서 '고원'상태 (학습이 진행되지 않는상태)가 될 수 있음

경사법 : 현 위치에서 기울어진 방향으로 일정 거리만큼 이동하여 함수의 값을 점차 줄이는 방법

  • 이동한 곳에서 기울기 구한 다음 기울어진 방향으로 이동 반복

[경사법 수식] n기호(에타)는 갱신하는 양 (학습률) : 한번의 학습으로 얼마나 갱신할지

  • 학습률과 같은 매개변수를 하이퍼파라미터라고 함 (사람이 직접 설정해야 하는 매개변수)
# 경사법 구현
# 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
# 경사법으로 f(x0, x1)= x0^2+x1^2의 최솟값 구하기
def function_2(x):
  return x[0]**2 + x[1]**2

init_x = np.array([-3.0, 4.0]) # 초깃값
gradient_descent(function_2, init_x=init_x, lr=0.1, step_num=100)

>>> array([-6.11110793e-10,  8.14814391e-10])
  • 실제 정답인 (0,0)에 가까운 값 결과 도출

 

  • 경사하강법 갱신과정 그림 그리기

학습률 적절히 설정하는 것이 중요

  • 학습률이 너무 클경우 최솟값을 찾지 못하고 건너뛰어버릴 수도 있음
  • 학습률이 너무 작을경우 거의 갱신되지 않은 채 반복종료될 수 있음

**학습률과 같은 매개변수를 하이퍼 파라미터라고 함

  • 사람이 직접 설정하는 매개변수
  • 여러 후보 값 중에서 시험을 통해 가장 잘 학습하는 값을 찾는 과정 거쳐야 함

4.4.2 신경망에서의 기울기

예) 형상이 2X3, 가중치가 W, 손실함수가 L인 신경망 (경사는 aL/aW)

[수식]

  • aL/aW의 각 원소는 각각의 원소에 대한 편미분
  • aL/aW의 형상은 W와 같음
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

simpleNet 클래스

  • 형상이 2X3인 가중치 매개변수 하나를 인스턴스 변수로 가짐
  • 메서드 : 예측수행(predict), 손실함수계산(loss)
  • 인수 x는 입력데이터, t는 정답레이블
# simpleNet 활용 시험
net = simpleNet()
print(net.W)

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

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

t = np.array([0, 0, 1]) # 정답레이블
net.loss(x, t)

>>>
[[ 0.12494685  0.5020375  -0.92335954]
 [-0.01197582  0.60362506 -0.95622184]]
[ 0.06418987  0.84448505 -1.41461538]
1
2.705523700978955

기울기 구하기 (numerical_gradient()사용)

# 함수의 인수 W는 더미로 만든 것 / numerical_gradeint(f, x) 내부에서 f(x)를 실행하는데, 그와의 일관성을 위해 정의
def f(W):
  return net.loss(x, t)

dW = numerical_gradient(f, net.W)
print(dW)

>>>
[[ 0.17595147  0.38394657 -0.55989803]
 [ 0.2639272   0.57591985 -0.83984705]]

기울기 해석

  • 0.1759...의 첫번째 기울기는 w11을 h만큼 늘리면 손실 함수의 값은 0.17h만큼 증가한다는 의미
  • -0.83...의 마지막 기울기는 w23을 h만큼 늘리면 손실함수의 값은 0.83h만큼 감소한다는 의미
  • 즉, 손실함수를 줄인다는 관점에서는 w23은 양의 방향으로, w11은 음의 방향으로 갱신해야함
  • 기울기 값이 클수록 갱신되는 양에 크게 기여
# lambda 기법 사용하면 더 편리하게 구현가능 (간단한 함수의 경우만 사용)
f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)

 

4.5 학습 알고리즘 구현하기

  • 신경망 학습의 절차
  1. 미니배치 : 훈련 데이터 중 일부를 무작위로 가져옴(미니배치) / 미니배치의 손실함수값을 줄이는 것이 목표
  2. 기울기 산출 : 각 가중치 매개변수의 기울기를 구함.
  3. 매개변수 갱신
  4. 반복
  • 경사 하강법으로 매개변수를 갱신
  • 데이터를 미니배치로 무작위 선정하기 때문에 확률적경사하강법(SGD)라고 부름
  • 대부분의 딥러닝 프레임워크는 SGD라는 함수로 이 기능 구현

* 코드구현은 저자의 Github 참고

코드구현 예에서는 1에폭마다 모든 훈련데이터와 시험데이터에 대한 정확도 계산하고, 그 결과 기록

  • for문안에서 매번 계산하기에는 시간이 오래걸림
  • 자주 기록할 필요 없음

위의 예시에서는 오버피팅이 일어나지 않았음

  • 오버피팅되면, 어느순간부터 시험데이터에 대한 정확도가 떨어짐
  • 오버피팅되는 순간을 포착해 학습 중단하면 오버피팅 예방 가능 (조기종료)
728x90
반응형

댓글