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

[밑바닥부터 시작하는 딥러닝] CH.6 학습 관련 기술들

by Minding 2021. 5. 9.
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

 

- 이미지 사용 출처

github.com/ExcelsiorCJH/DLFromScratch

 

ExcelsiorCJH/DLFromScratch

Deep Learning From Scratch. Contribute to ExcelsiorCJH/DLFromScratch development by creating an account on GitHub.

github.com


6.1 매개변수 갱신

  • 신경망 학습의 목적 : 손실함수 낮추는 매개변수 찾는것 = 이 문제를 푸는 것을 '최적화 라고 함
  • 확률적 경사 하강법(SGD) : 매개변수의 기울기를 구하여 기울어진 방향으로 매개변수 값을 갱신하는 것

6.1.2 확률적 경사 하강법(SGD)

  • SGD 수식 
  • W는 갱신할 가중치 매개변수, aL/aW은 W에 대한 손실함수의 기울기 , n은 학습률
  • <--는 우변의 값으로 좌변의 값을 갱신한다는 뜻
# SGD 파이썬 구현
class SGD:
  def __init__(self, lr=0.01):
    self.lr = lr

  def update(self, params, grads):
    for key in params.keys():
      params[key] -= self.lr * grads[key]
  • update 메서드는 SGD 과정에서 반복해서 호출됨
  • params와 grads는 딕셔너리 변수 (각각 가중치 매개변수와 기울기 저장)

 

6.1.3 SGD의 단점

  • 문제에 따라서는 비효율적일 때가 있음 
  • 위 그림에서 y축 방향은 가파른데 x축 방향은 완만
  • 최솟값이 되는 장소는 (0,0)이지만, 기울기 대부분은 그곳을 가리키지 않음
  • 갱신 경로가 상당히 비효율적인 움직임
  • SGD의 단점은 비등방성 함수
    • 방향에 따라 기울기(성질)가 달라지는 함수에서는 탐색경로가 비효율적

 

6.1.4 모멘텀

  • 모멘텀은 운동량을 뜻하는 단어로, 물리와 관계가 있음 
class Momentum:

    """Momentum SGD"""

    def __init__(self, lr=0.01, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = None
        
    def update(self, params, grads):
        if self.v is None:
            self.v = {}
            for key, val in params.items():                                
                self.v[key] = np.zeros_like(val)
                
        for key in params.keys():
            self.v[key] = self.momentum*self.v[key] - self.lr*grads[key] 
            params[key] += self.v[key]

  • SGD와 비교하면 '지그재그 정도'가 덜함
  • x축의 힘은 아주 작지만 방향은 변하지 않아서 한 방향으로 일정하게 가속
  • y축의 힘은 크지만 위아래로 번갈아 받아 상충하여 y축 방향의 속도는 안정적이지 않음

6.1.5 AdaGrad

  • 신경망 학습에는 학습률이 중요 : 학습률을 정하는 효과적 기술로 학습률 감소 (학습을 진행하면서 학습률 점차 줄여나감)
  • 학습률을 서서히 낮추는 가장 간단한 방법은 매개변수 '전체'의 학습률 값을 일괄적으로 낮추는 것 --> AdaGrad
  • AdaGrad는 각각의 매개변수에 맞춤형 값을 만들어줌 
  • W는 갱신할 가중치 매개변수, aL/aW는 W에 대한 손실함수에 기울기, n은 학습률
  • h는 기존 기울기값을 제곱하여 계속 더해줌
  • 매개변수 갱신할 때 1/route(h)를 곱해 학습률 조정
# AdaGrad 구현
class AdaGrad:

    """AdaGrad"""

    def __init__(self, lr=0.01):
        self.lr = lr
        self.h = None
        
    def update(self, params, grads):
        if self.h is None:
            self.h = {}
            for key, val in params.items():
                self.h[key] = np.zeros_like(val)
            
        for key in params.keys():
            self.h[key] += grads[key] * grads[key]
            params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
  • AdaGrad는 과거의 기울기를 제곱하여 계속 더해감
  • 어느 순간 갱신량이 0이 되어 갱신 X
  • 문제개선기법 : RMSProp, 먼 과거의 기울기 서서히 잊고 새로운 기울기 정보를 크게 반영
  • 지수이동평균(Exponential Moving Average,EMA) 사용하는 optimizer

 

6.1.6 Adam

  • AdaGrad 와 모멘텀 기법 융합
  • 하이퍼파라미터의 '편향 보정'이 진행
# Adam 구현
class Adam:

    """Adam (http://arxiv.org/abs/1412.6980v8)"""

    def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.iter = 0
        self.m = None
        self.v = None
        
    def update(self, params, grads):
        if self.m is None:
            self.m, self.v = {}, {}
            for key, val in params.items():
                self.m[key] = np.zeros_like(val)
                self.v[key] = np.zeros_like(val)
        
        self.iter += 1
        lr_t  = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)         
        
        for key in params.keys():
            #self.m[key] = self.beta1*self.m[key] + (1-self.beta1)*grads[key]
            #self.v[key] = self.beta2*self.v[key] + (1-self.beta2)*(grads[key]**2)
            self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
            self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])
            
            params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
            
            #unbias_m += (1 - self.beta1) * (grads[key] - self.m[key]) # correct bias
            #unbisa_b += (1 - self.beta2) * (grads[key]*grads[key] - self.v[key]) # correct bias
            #params[key] += self.lr * unbias_m / (np.sqrt(unbisa_b) + 1e-7)

  • Adam은 하이퍼파라미터를 3개 설정 (학습률, 일차모멘텀용 계수, 이차모멘텀용 계수)

6.1.7 어느 갱신 방법을 이용할 것인가?

  • 네 기법의 결과비교
# coding: utf-8
import sys, os
sys.path.append('/deep-learning-from-scratch')  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import matplotlib.pyplot as plt
from collections import OrderedDict
from common.optimizer import *


def f(x, y):
    return x**2 / 20.0 + y**2


def df(x, y):
    return x / 10.0, 2.0*y

init_pos = (-7.0, 2.0)
params = {}
params['x'], params['y'] = init_pos[0], init_pos[1]
grads = {}
grads['x'], grads['y'] = 0, 0


optimizers = OrderedDict()
optimizers["SGD"] = SGD(lr=0.95)
optimizers["Momentum"] = Momentum(lr=0.1)
optimizers["AdaGrad"] = AdaGrad(lr=1.5)
optimizers["Adam"] = Adam(lr=0.3)

idx = 1

for key in optimizers:
    optimizer = optimizers[key]
    x_history = []
    y_history = []
    params['x'], params['y'] = init_pos[0], init_pos[1]
    
    for i in range(30):
        x_history.append(params['x'])
        y_history.append(params['y'])
        
        grads['x'], grads['y'] = df(params['x'], params['y'])
        optimizer.update(params, grads)
    

    x = np.arange(-10, 10, 0.01)
    y = np.arange(-5, 5, 0.01)
    
    X, Y = np.meshgrid(x, y) 
    Z = f(X, Y)
    
    # for simple contour line  
    mask = Z > 7
    Z[mask] = 0
    
    # plot 
    plt.subplot(2, 2, idx)
    idx += 1
    plt.plot(x_history, y_history, 'o-', color="red")
    plt.contour(X, Y, Z)
    plt.ylim(-10, 10)
    plt.xlim(-10, 10)
    plt.plot(0, 0, '+')
    #colorbar()
    #spring()
    plt.title(key)
    plt.xlabel("x")
    plt.ylabel("y")
    
plt.show()

  • 문제가 무엇인지, 하이퍼파라미터가 어떻게 설정되어있는지에 따라 결과 다름
  • 모든 문제에서 항상 뛰어난 기법은 아직까지 없음

6.1.8 MNIST 데이터셋으로 본 갱신 방법 비교

# 100층 뉴런으로 구성된 5층신경망 / activation_func : ReLU
# coding: utf-8
import os
import sys
sys.path.append('/deep-learning-from-scratch')
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from common.util import smooth_curve
from common.multi_layer_net import MultiLayerNet
from common.optimizer import *


# 0:MNIST 데이터셋 다운로드 ==========
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)

train_size = x_train.shape[0]
batch_size = 128
max_iterations = 2000


# 1:optimizer 딕셔너리 설정==========
optimizers = {}
optimizers['SGD'] = SGD()
optimizers['Momentum'] = Momentum()
optimizers['AdaGrad'] = AdaGrad()
optimizers['Adam'] = Adam()
#optimizers['RMSprop'] = RMSprop()

networks = {}
train_loss = {}
for key in optimizers.keys():
    networks[key] = MultiLayerNet(
        input_size=784, hidden_size_list=[100, 100, 100, 100],
        output_size=10)
    train_loss[key] = []    


# 2: 학습 시작==========
for i in range(max_iterations):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    
    for key in optimizers.keys():
        grads = networks[key].gradient(x_batch, t_batch)
        optimizers[key].update(networks[key].params, grads)
    
        loss = networks[key].loss(x_batch, t_batch)
        train_loss[key].append(loss)
    
    if i % 100 == 0:
        print( "===========" + "iteration:" + str(i) + "===========")
        for key in optimizers.keys():
            loss = networks[key].loss(x_batch, t_batch)
            print(key + ":" + str(loss))


# 3. 그래프==========
markers = {"SGD": "o", "Momentum": "x", "AdaGrad": "s", "Adam": "D"}
x = np.arange(max_iterations)
for key in optimizers.keys():
    plt.plot(x, smooth_curve(train_loss[key]), marker=markers[key], markevery=100, label=key)
plt.xlabel("iterations")
plt.ylabel("loss")
plt.ylim(0, 1)
plt.legend()
plt.show()

 

6.2 가중치의 초깃값

6.2.1 초깃값을 0으로 하면?

  • 가중치 감소 기법 : 가중치 매개변수의 값이 작아지도록 하여 오버피팅이 일어나지 않도록 하는 기법
  • 가중치를 작게 만들고 싶으면 초깃값도 작은 값에서 시작 --> 0으로 설정은 안됨
  • 초깃값을 0으로 설정하게 되면 오차역전파에서 모든 가중치값이 독같이 갱신되기 때문
  • 순전파 때 모든 뉴런에 같은 값이 입력되므로, 역전파 두번째 층의 가중치가 모두 똑같이 갱신된다는 뜻(학습이 안됨)
  • (가중치를 균일한 값으로 설정X) 가중치 초깃값을 무작위로 설정해야함

6.2.2 은닉층의 활성화값 분포

  • 은닉층의 활성화값(활성화 함수의 출력데이터)의 분포를 관찰하면 중요한 정보를 얻을 수 있음
  • 가중치 초깃값에 따라 은닉층 활성화값들이 어떻게 변화하는지 실험
  • 시그모이드 함수 사용 5층 신경망에 무작위 생성 입력데이터를 흘리며각 층의 활성화 값 분포를 히스토그램으로 그림
import numpy as np
import matplotlib.pyplot as plt

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

x = np.random.randn(1000, 100) # 1000개의 데이터
node_num = 100 # 각 은닉층 노드의 수
hidden_layer_size = 5 # 은닉층 5개
activations = {} # 활성화 값 저장할 공간

for i in range(hidden_layer_size):
  if i != 0:
    x = activations[i-1]

  w = np.random.randn(node_num, node_num) * 1 # 가중치 분포의 표준편차 = 1
  a = np.dot(x, w)
  z = sigmoid(a)
  activations[i] = z
# 히스토그램 그리기
for i, a in activations.items():
  plt.subplot(1, len(activations), i + 1)
  plt.title(str(i+1) + '-layer')
  plt.hist(a.flatten(), 30, range=(0,1))
plt.show()

  • 각 층의 활성화 값들이 0과 1에 치우쳐 있음
  • 시그모이드 함수는 출력이 0 (또는 1)에 가까워지자 그 미분은 0에 다가감
  • 기울기 소실 : 데이터가 0과 1에 치우쳐 분포하게 되면 역전파의 기울기 값이 점점 작아지다가 사라짐
# 가중치 표준편차를 0.01로 바꿔 진행
def sigmoid(x):
  return 1 / (1 + np.exp(-x))

x = np.random.randn(1000, 100) # 1000개의 데이터
node_num = 100 # 각 은닉층 노드의 수
hidden_layer_size = 5 # 은닉층 5개
activations = {} # 활성화 값 저장할 공간

for i in range(hidden_layer_size):
  if i != 0:
    x = activations[i-1]

  w = np.random.randn(node_num, node_num) * 0.01 # 가중치 분포의 표준편차 = 0.01
  a = np.dot(x, w)
  z = sigmoid(a)
  activations[i] = z

for i, a in activations.items():
  plt.subplot(1, len(activations), i + 1)
  plt.title(str(i+1) + '-layer')
  plt.hist(a.flatten(), 30, range=(0,1))
plt.show()

  • 0.5 부근에서 값 집중
  • 기울기 소실 문제는 없으나, 활성화 값이 치우친 것은 표현력 관점에서 큰 문제
  • 노드 100개가 거의 같은 값을 출력한다면 노드가 많아도 노드 하나 모델과 다를게 없음
  • Xavier 초깃값 : 일반적인 딥러닝 프레임워크에서 표준적으로 이용하는 초깃값
  • 앞 계층의 노드가 n개라면 표준편차가 1/sqrt(n) 분포를 사용하면 된다는 것을 이용
  • 앞 층의 노드가 많을수록 대상 노드이 초깃값으로 설장하는 가중치가 좁게 퍼짐
# 가중치 표준편차를 Xavier초깃값(1/sqrt(n))로 바꿔 진행
def sigmoid(x):
  return 1 / (1 + np.exp(-x))

x = np.random.randn(1000, 100) # 1000개의 데이터
node_num = 100 # 각 은닉층 노드의 수
hidden_layer_size = 5 # 은닉층 5개
activations = {} # 활성화 값 저장할 공간

for i in range(hidden_layer_size):
  if i != 0:
    x = activations[i-1]

  w = np.random.randn(node_num, node_num) / np.sqrt(node_num) # 가중치 분포의 표준편차 1/sqrt(100) (모든 층의 노드 수 100개로 단순화)
  a = np.dot(x, w)
  z = sigmoid(a)
  activations[i] = z

for i, a in activations.items():
  plt.subplot(1, len(activations), i + 1)
  plt.title(str(i+1) + '-layer')
  plt.hist(a.flatten(), 30, range=(0,1))
plt.show()

  • 층이 깊어지며 형태는 일그러지지만 앞 방식보다 넓게 분포됨
  • 각 층의 데이터가 적당히 퍼져있으므로 함수의 표현력 제한 받지 않고 효율적 학습 진행

6.2.3 ReLU를 사용할 때의 가중치 초깃값

  • ReLU 이용 시에는 ReLU에 특화된 초깃값 이용 권장
  • He 초깃값 : 앞 층의 노드가 n개 일 때, 표준편차가 sqrt(2/n)인 정규분포 사용
  • ReLU는 음의 영역이 0이기 때문에 더 넓게 분포시키기 위해 2배의 계수가 필요
  • ReLU를 이용한 경우 활성화 값 분포 (0.01std, Xavier, He)

  • std=0.01일 때의 각 층의 활성화 값들은 아주 작은 값
  • 신경망에 아주 작은 데이터가 흐른다는 것은 가중치의 기울기 역시 작아져 학습이 제대로 이루어지지 않음
  • Xavier는 층이 깊어지면서 치우침이 조금씩 커짐 = 기울기 소실문제 일으킴
  • He초깃값은 모든 층에서 균일분포 = 역전파 때도 적절값 나올 가능성 높음
  • ReLU 사용 시 He, sigmoid, tanh 등의 S자 모양 곡선에서는 Xavier초깃값 사용

6.2.4 MNIST 데이터셋으로 본 가중치 초깃값 비교

[코드는 책 참고]

  • 층별 뉴런수 100개 / 5층 신경망 / 활성화 함수 = ReLU 사용
  • std=0.01에서는 전혀 학습되지 않음 (순전파 때 너무 작은 값이 흐르기 때문)
  • Xavier와 He는 학습 원할, 학습 진도는 He가 빠름
  • 가중치 초깃값에 따라 학습의 성패가 갈리는 경우 많음 = 초깃값 중요!

6.3 배치 정규화

  • 각 층이 활성화를 적당히 퍼뜨리도록 하는 방법

6.3.1 배치 정규화 알고리즘

  • 배치 정규화가 주목받는 이유
  1. 학습 속도 개선
  2. 초깃값에 크게 의존하지 않음
  3. 오버피팅 억제 (드롭아웃 등 필요성 감소)
  • 배치 정규화 계층 신경망에 삽입 
  • 학습 시 미니배치를 단위로 정규화 (데이터 분포 평균 0, 분산이 1이 되도록)
  • 배치 정규화 수식

  • 미니배치 B = {x1, x2 ... , xm}이라는 m개의 입력 데이터 집합에 대해 평균(uB)와 분산(a2B) 구함
  • 평균 0, 분산 1이 되도록 정규화
  • //normalize 단계에서 e(삼지창기호, epsilon)는 매우 작은 값으로, 0으로 나누는 사태를 예방
  • 배치처리 층을 활성화 함수 앞 또는 뒤에 삽입하여 데이터분포가 덜 치우치게 함
  • 배치 정규화 계층마다 정규화데이터에 고유한 확대와 이동변환 수행 (마지막 줄)
  • 단순히 평균0, 분산1로 만들어주면 활성화 함수의 비선형성이 없어질 수 있기 때문에 확대 및 이동변환 실시

6.3.2 배치 정규화의 효과

[코드 책 참고]

6.4 바른 학습을 위해

  • 오버피팅 억제하는 기술이 중요

6.4.1 오버피팅

  • 오버피팅은 다음의 경우에 일어남
  1. 매개변수가 많고 표현력이 높은 모델
  2. 훈련 데이터가 적음
  • 오버피팅 일부러 발생시키기 (위 요건 충족시키기) [코드 책 참고]

  • 100에폭 지나는 무렵부터 정확도 변화 거의 없음 (100%의 훈련 정확도)
  • 시험데이터에 대해서는 큰 차이 보임
  • 정확도가 크게 벌어지는 것은 훈련 데이터에만 적응 = 오버피팅

6.4.2 가중치 감소

  • 오버피팅 억제용으로 가중치 감소 방법 예로부터 많이 이용
  • 학습 과정에서 큰 가중치에 상응하는 큰 페널티 부과하여 오버피팅 억제
  • 가중치의 제곱노름(L2노름)을 손실함수에 더해 가중치 커지는 것 억제 
...

weight_decay_lambda = 0.1
# ====================================================

network = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100, 100, 100], output_size=10,
                        weight_decay_lambda=weight_decay_lambda)
optimizer = SGD(lr=0.01)

...

  • train acc와 test acc 차이는 여전히 있지만, 그 차이 줄음
  • 훈련데이터 정확도가 100% 도달하지 못함
  • 오버피팅 억제효과 있음

6.4.3 드롭아웃

  • 신경망 모델이 복잡할 때 사용하는 기법
  • 노드를 임의로 삭제하면서 학습하는 방법 (삭제된 노드는 신호전달 X)
  • test에는 모든 뉴런 신호 전달, 각 뉴런의 출력에 훈련 때 삭제 안 한 비율 곱하여 출력
# 드롭아웃 구현
class Dropout:
  def __init__(self, dropout_ratio=0.5):
    self.dropout_ratio = dropout_ratio
    self.mask = None

  def forward(self, x, train_flg=True):
    if train_flg:
      self.mask = np.random.rand(*x.shape) > self.dropout_ratio # x와 형상 같은 배열 무작위 생성, dropout_ratio보다 큰 원소만 True로 설정
      return x * self.mask
    else:
      return x * (1.0 - self.dropout_ratio)

  def backward(self, dout):
    return dout * self.mask
  • 훈련 시에는 self.mask에 삭제할 뉴런을 False로 표시
  • x와 형상 같은 배열 무작위 생성, dropout_ratio보다 큰 원소만 True로 설정
  • 역전파 때는 순전파 때 신호 통과 노드는 통과, 그렇지 않으면 차단 (ReLU와 같음)
  • MNIST 데이터셋으로 확인 (네트워크 학습 대신해주는 Trainer.py 이용)
...

use_dropout = True
dropout_ratio = 0.2
# ====================================================

network = MultiLayerNetExtend(input_size=784, hidden_size_list=[100, 100, 100, 100, 100, 100],
                              output_size=10, use_dropout=use_dropout, dropout_ration=dropout_ratio)
trainer = Trainer(network, x_train, t_train, x_test, t_test,
                  epochs=301, mini_batch_size=100,
                  optimizer='sgd', optimizer_param={'lr': 0.01}, verbose=True)
                  
...

  • 훈련데이터와 시험데이터에 대한 정확도 차이 줄음
  • 훈련데이터 정확도 100% 도달X

6.5 적절한 하이퍼파라미터 값 찾기

6.5.1 검증 데이터

  • 하이퍼파라미터 성능 평가시에는 test data 사용해서는 안됨
  • 검증데이터(validation data) : 하이퍼파라미터 검증용 데이터
# 하이퍼 파라미터 평가용 검증데이터 분리하기 (mnist)
(x_train, t_train), (x_test, t_test) = load_mnist()

# 훈련데이터 섞기
x_train, t_train = shuffle_dataset(x_train, t_train)

# 그 중 20% 검증데이터로 분할
validation_rate = 0.20
validation_num = int(x_train.shape[0] * validation_rate)

x_val = x_train[:validation_num]
t_val = t_train[:validation_num]
x_train = x_train[validation_num:]
t_train = t_train[validation_num:]

6.5.2 하이퍼파라미터 최적화

  • 하이퍼파라미터 최적화의 핵심은 하이퍼파라미터의 '최적 값'이 존재하는 범위를 조금씩 줄여간다는 것
  • 대략적인 범위 정하고 그 범위에서 무작위로 값을 골라낸 후, 정확도 평가
  • 실제로도 로그스케일(10^-3 ~ 10^3)로 지정하기도 함
  • 하이퍼파라미터 최적화 단계
  1. 하이퍼파라미터 값의 범위 설정
  2. 설정된 범위에서 하이퍼파라미터 값 무작위 추출
  3. 1단계에서 샘플링한 하이퍼파라미터 값 사용하여 학습, 검증데이터로 정확도 평가 (에폭은 작게 설정)
  4. 1단계와 2단계를 특정횟수 반복하며, 정확도 결과보고 하이퍼파라미터의 범위 좁힘

베이즈 최적화

  • 베이즈 정리를 중심으로 한 수학이론 구사하여 엄밀, 효율적으로 최적화 수행

6.5.3 하이퍼파라미터 최적화 구현하기

  • 로그스케일 범위에서 무작위 추출
# 로그스케일 내 무작위추출 구현
weight_decay = 10 ** np.random.uniform(-8, -4) # 가중치 감소계수 10^-8 ~ 10^-4 범위
lr = 10 ** np.random.uniform(-6, -2) # 학습률 10^-6 ~ 10^-2 범위

[학습코드는 책 참고]


* 추가 팁 : Tensorflow Keras를 사용할 시 하이퍼파라미터 및 초깃값 설정 팁

https://www.tensorflow.org/api_docs/python/tf/keras/layers/Dense

 

tf.keras.layers.Dense  |  TensorFlow Core v2.4.1

Just your regular densely-connected NN layer.

www.tensorflow.org

 

728x90
반응형

댓글