본문 바로가기
ML & DL/책 & 강의

[밑시딥1] CHAPTER 6 학습 관련 기술들

by 공부하는 무니 2023. 6. 21.
반응형

이 장에서는 가중치 매개변수의 최적값을 탐색하는 최적화 방법, 가중치 매개변수 초기값, 하이퍼파라미터 설정 방법 등 중요한 주제를 다룬다. 오버피팅의 대응책인 가중치 감소와 드롭아웃 등 정규화 방법도 설명하고 구현해보자. 배치 정규화도 알아보자.

6.1 매개변수 갱신

신경망 학습의 목적: 손실 함수의 값을 가능한 한 낮추는 매개변수를 찾는 것

최적화: 이러한 문제를 푸는 것

SGD: 매개변수의 기울기를 구해서, 기울어진 방향으로 매개변수 값을 갱신하는 일을 반복하면서 최적의 값에 다가갔다.

6.1.1 모험가 이야기

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

SGD는 수식으로 다음과 같이 나타낼 수 있었다.

파이썬 클래스로 구현해보자.

optimizer.py

매개변수 갱신은 위 클래스가 하니, 우리는 optimizer에 매개변수와 기울기 정보만 넘겨주면 된다. 

이렇게 최적화를 담당하는 클래스를 분리해서 구현하면 기능을 모듈화하기 좋다. 

6.1.3 SGD의 단점

SGD는 단순하고 구현도 쉽지만, 문제에 따라 비효율적일 때가 있다.

위 식의 그래프(왼쪽)와 그 등고선(오른쪽)

함수의 기울기를 그려보면 아래와 같다. 

이 기울기는 y축 방향은 크고 x축 방향은 작다는 것이 특징이다. 

이제 SGD를 적용해보자.

SGD에 의한 최적화 갱신 경로: 최솟값인 (0,0)까지 지그재그로 이동하니 비효율적이다.

SGD는 심하게 굽이진 움직임을 보여준다. 비효율적이다. 즉, SGD의 단점은 비등방성(anisotropy)함수에서는 탐색 경로가 비효율적이라는 것이다 

6.1.4 모멘텀

모멘텀은 '운동량'을 뜻하는 단어.

모멘텀의 이미지: 공이 그릇의 곡면(기울기)을 따라 구르듯 움직인다.
모멘텀에 의한 최적화 갱신 경로

모멘텀의 갱신 경로는 공이 그릇 바닥을 구르듯 움직인다. SGD와 비교하면 지그재그 정도가 덜하다. 이는 x축의 힘은 아주 작지만 방향은 변하지 않아서 한 방향으로 일정하게 가속하기 때문이다. 거꾸로 y축의 힘은 크지만 위아래로 번갈아 받아서 상충하여 y축 방향의 속도는 안정적이지 않다. 

6.1.5 AdaGrad

신경망 학습에서는 학습률 값이 중요하다. 이 학습률을 정하는 효과적 기술로 학습률 감소가 있다. 학습을 진행하면서 학습률을 점차 줄여가는 방법이다. AdaGrad는 각각의 매개변수에 맞춤형 값을 만들어준다.

AdaGrad에 의한 최적화 갱신 경로

최솟값을 향해 효율적으로 움직인다. y축 방향은 기울기가 커서 처음에는 크게 움직이지만, 큰 움직임에 비례해 갱신 정도도 큰 폭으로 작아지도록 조정된다. 

6.1.6 Adam

모멘텀과 AdaGrad 기법을 융합한 것

Adam 에 의한 최적화 갱신 경로

모멘텀과 비슷한 패턴인데, 모멘텀 보다는 공의 좌우 흔들림이 적다. 이는 학습의 갱신 강도를 적응적으로 조정하기 때문이다. 

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

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
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)
    
    # 외곽선 단순화
    mask = Z > 7
    Z[mask] = 0
    
    # 그래프 그리기
    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()

최적화 기법 비교: SGD, 모멘텀, AdaGrad, Adam

가장 좋은 최적화 방법이란 없다. 각자의 장단이 있어 잘 푸는 문제와 서툰 문제가 있다.

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

# coding: utf-8
import os
import sys
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
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. 실험용 설정==========
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()

MNIST 데이터셋에 대한 학습 진도 비교

6.2. 가중치의 초깃값

6.2.1 초깃값을 0으로 하면?

가중치 감소(weight decay): 오버피팅을 억제해 범용 성능을 높이는 테크닉 중 하나, 가중치 매개변수의 값이 작아지도록 학습하는 방법이다. 

가중치를 작게 만들고 싶으면 초깃값도 최대한 작은 값에서 시작하는 것이 정공법.

그러면 가중치의 초깃값을 모두 0으로 설정하면 어떨까? 결론은 학습이 올바로 이뤄지지 않는다.

초깃값을 0으로 해서는 안되는 이유? 정확히는 가중치를 균일한 값으로 설정해서는 안된다. 이유는 오차역전파법에서 모든 가중치의 값이 똑같이 갱신되기 때문. 가중치들은 같은 초깃값에서 시작하고 갱신을 거쳐도 여전히 같은 값을 유지한다. 이는 가중치를 여러 개 갖는 의미를 사라지게 한다. 따라서 초깃값을 무작위로 설정해야 한다.

6.2.2 은닉층의 활성화값 분포

은닉층의 활성화값의 분포를 관찰하면 중요한 정보를 얻을 수 있다.

가중치의 초깃값에 따라 은닉층 활성화값들이 어떻게 변하는지 간단한 실험을 해보자.

# coding: utf-8
import numpy as np
import matplotlib.pyplot as plt


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


def ReLU(x):
    return np.maximum(0, x)


def tanh(x):
    return np.tanh(x)
    
input_data = np.random.randn(1000, 100)  # 1000개의 데이터
node_num = 100  # 각 은닉층의 노드(뉴런) 수
hidden_layer_size = 5  # 은닉층이 5개
activations = {}  # 이곳에 활성화 결과를 저장

x = input_data

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

    # 초깃값을 다양하게 바꿔가며 실험해보자!
    w = np.random.randn(node_num, node_num) * 1
    # w = np.random.randn(node_num, node_num) * 0.01
    # w = np.random.randn(node_num, node_num) * np.sqrt(1.0 / node_num)
    # w = np.random.randn(node_num, node_num) * np.sqrt(2.0 / node_num)


    a = np.dot(x, w)


    # 활성화 함수도 바꿔가며 실험해보자!
    z = sigmoid(a)
    # z = ReLU(a)
    # z = tanh(a)

    activations[i] = z

# 히스토그램 그리기
for i, a in activations.items():
    plt.subplot(1, len(activations), i+1)
    plt.title(str(i+1) + "-layer")
    if i != 0: plt.yticks([], [])
    # plt.xlim(0.1, 1)
    # plt.ylim(0, 7000)
    plt.hist(a.flatten(), 30, range=(0,1))
plt.show()

가중치를 표준편차가 1인 정규분포로 초기화할 때의 각 층의 활성화값 분포

각 층의 활성화값들이 0과 1에 치우쳐있다. 여기서 사용한 시그모이드 함수는 0이나 1에 가까워지면 미분은 0에 가까워진다. 따라서 기울기 값이 사라지게 된다. 이것이 기울기 소실이라고 알려진 문제이다. 

이번에는 가중치의 표준편차를 0.01로 바꿔 같은 실험을 반복해보겠다.

가중치를 표준편차가 0.01인 정규분포로 초기화할 때의 각 층의 활성화값 분포

이번에는 0.5 부근에 집중되어 있다. 기울기 소실 문제는 일어나지 않지만 활성화값들이 치우쳤다는 것은 표현력 관점에서 문제가 있다. 뉴런을 여러 개 둔 의미가 없어진다. 

이어서 사비에르 글로로트, 요슈아 벤지오의 논문에서 권장하는 Xavier 초깃값을 사용해보자.

Xavier 초깃값: 초깃값의 표준편차가 1/루트n이 되도록 설정(n은 앞 층의 노드 수)

앞 층에 노드가 많을수록 대상 노드의 초깃값으로 설정하는 가중치가 좁게 퍼진다.

가중치의 초깃값으로 'Xavier 초깃값'을 이용할 때의 각 층의 활성화값 분포

층이 깊어지면서 형태가 다소 일그러지지만, 앞에서 본 방식보다는 확실히 넓게 분포되어 있다. 

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

Xavier 초깃값은 활성화 함수가 선형인 것을 전제로 이끈 결과이다. sigmoid와 tanh 함수는 좌우 대칭이라 중앙 부근이 선형인 함수로 볼 수 있다. 그래서 Xavier 초깃값이 적당하다.

반면, ReLU를 이용할 때는 ReLU에 특화된 He 초깃값을 이용하라고 권장한다. He 초깃값은 앞 계층의 노드가 n개일 때, 표준편차가 루트2/n 인 정규분포를 사용한다. Xavier초기값은 루트 1/n 이었음! ReLU는 음의 영역이 0이라서 더 넓게 분포시키기 위해 2배의 계수가 필요하다고 직감적으로 해석할 수 있다.

활성화 함수로 ReLU를 사용한 경우의 가중치 초깃값에 따른 활성화값 분포 변화

He 초깃값을 사용했을 때 층이 깊어져도 분포가 균일하게 유지되기에 역전파 때도 적절한 값이 나오리라 기대할 수 있다.

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

실제 데이터를 가지고 가중치의 초깃값을 주는 방법이 신경망 학습에 얼마나 영향을 미치는지 보자.

# coding: utf-8
import os
import sys

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


# 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. 실험용 설정==========
weight_init_types = {'std=0.01': 0.01, 'Xavier': 'sigmoid', 'He': 'relu'}
optimizer = SGD(lr=0.01)

networks = {}
train_loss = {}
for key, weight_type in weight_init_types.items():
    networks[key] = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100],
                                  output_size=10, weight_init_std=weight_type)
    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 weight_init_types.keys():
        grads = networks[key].gradient(x_batch, t_batch)
        optimizer.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 weight_init_types.keys():
            loss = networks[key].loss(x_batch, t_batch)
            print(key + ":" + str(loss))


# 3. 그래프 그리기==========
markers = {'std=0.01': 'o', 'Xavier': 's', 'He': 'D'}
x = np.arange(max_iterations)
for key in weight_init_types.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, 2.5)
plt.legend()
plt.show()

MNIST 데이터셋으로 살펴본 '가중치의 초깃값'에 따른 비교

가중치의 초깃값은 신경망 학습에 아주 중요하며, 성패가 갈리는 경우도 많다. 

6.3 배치 정규화

각 층이 활성화를 적당히 퍼뜨리도록 강제하는 아이디어: 배치 정규화

6.3.1 배치 정규화 알고리즘

배치 정규화가 주목받는 이유

- 학습을 빨리 진행할 수 있다(학습 속도 개선)

- 초깃값에 크게 의존하지 않는다(골치 아픈 초깃값 선택 장애여 안녕!)

- 오버피팅을 억제한다(드롭아웃 등의 필요성 감소)

 

배치 정규화의 기본 아이디어: 각 층에서의 활성화값이 적당히 분포되도록 조정하는 것

배치 정규화를 사용한 신경망의 예

미니배치 단위로 평균이 0, 분산이 1이 되도록 정규화한다. 수식으로는 다음과 같다.

입실론은 분모가 0이 되는 것을 예방한다.

 

또한, 정규화 계층마다 정규화된 데이터에 고유한 확대와 이동 변환을 수행한다.

계산 그래프로는 아래와 같이 그릴 수 있다.

6.3.2 배치 정규화의 효과

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from common.multi_layer_net_extend import MultiLayerNetExtend
from common.optimizer import SGD, Adam

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

# 학습 데이터를 줄임
x_train = x_train[:1000]
t_train = t_train[:1000]

max_epochs = 20
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.01


def __train(weight_init_std):
    bn_network = MultiLayerNetExtend(input_size=784, hidden_size_list=[100, 100, 100, 100, 100], output_size=10, 
                                    weight_init_std=weight_init_std, use_batchnorm=True)
    network = MultiLayerNetExtend(input_size=784, hidden_size_list=[100, 100, 100, 100, 100], output_size=10,
                                weight_init_std=weight_init_std)
    optimizer = SGD(lr=learning_rate)
    
    train_acc_list = []
    bn_train_acc_list = []
    
    iter_per_epoch = max(train_size / batch_size, 1)
    epoch_cnt = 0
    
    for i in range(1000000000):
        batch_mask = np.random.choice(train_size, batch_size)
        x_batch = x_train[batch_mask]
        t_batch = t_train[batch_mask]
    
        for _network in (bn_network, network):
            grads = _network.gradient(x_batch, t_batch)
            optimizer.update(_network.params, grads)
    
        if i % iter_per_epoch == 0:
            train_acc = network.accuracy(x_train, t_train)
            bn_train_acc = bn_network.accuracy(x_train, t_train)
            train_acc_list.append(train_acc)
            bn_train_acc_list.append(bn_train_acc)
    
            print("epoch:" + str(epoch_cnt) + " | " + str(train_acc) + " - " + str(bn_train_acc))
    
            epoch_cnt += 1
            if epoch_cnt >= max_epochs:
                break
                
    return train_acc_list, bn_train_acc_list


# 그래프 그리기==========
weight_scale_list = np.logspace(0, -4, num=16)
x = np.arange(max_epochs)

for i, w in enumerate(weight_scale_list):
    print( "============== " + str(i+1) + "/16" + " ==============")
    train_acc_list, bn_train_acc_list = __train(w)
    
    plt.subplot(4,4,i+1)
    plt.title("W:" + str(w))
    if i == 15:
        plt.plot(x, bn_train_acc_list, label='Batch Normalization', markevery=2)
        plt.plot(x, train_acc_list, linestyle = "--", label='Normal(without BatchNorm)', markevery=2)
    else:
        plt.plot(x, bn_train_acc_list, markevery=2)
        plt.plot(x, train_acc_list, linestyle="--", markevery=2)

    plt.ylim(0, 1.0)
    if i % 4:
        plt.yticks([])
    else:
        plt.ylabel("accuracy")
    if i < 12:
        plt.xticks([])
    else:
        plt.xlabel("epochs")
    plt.legend(loc='lower right')
    
plt.show()

배치 정규화의 효과: 배치 정규화가 학습 속도를 높인다.

6.4 바른 학습을 위해

6.4.1 오버피팅

오버피팅은 주로 다음의 경우에 많이 일어난다.

- 매개변수가 많고 표현력이 높은 모델

- 훈련 데이터가 적음

훈련 데이터(train)와 시험 데이터(test)의 에폭별 정확도 차이

6.4.2 가중치 감소

신경망 학습의 목적은 손실 함수의 값을 줄이는 것이다. 예를 들어 가중치의 L2 노름을 손실함수에 더하면 가중치가 커지는 것을 억제할 수 있다.

가중치 감소를 이용한 훈련 데이터(train)와 시험 데이터(test)에 대한 정확도 추이

6.4.3 드롭아웃

앞에서 배운 오버피팅을 억제하는 방식은 손실 함수에 가중치의 L2 노름을 더한 가중치 감소 방법이었다.

그러나 신경망이 복잡해지면 가중치 감소만으로는 대응하기 어렵다. 이럴 때는 흔히 드롭아웃 이라는 기법을 이용한다.

드롭아웃은 뉴런을 임의로 삭제하면서 학습하는 방법. 

드롭아웃의 개념: 왼쪽이 일반적인 신경망, 오른쪽이 드롭아웃을 적용한 신경망, 드론아웃은 뉴런을 무작위로 선택하여 신호 전달한다.

드롭아웃의 효과를 MNIST데이터셋으로 확인해보자.

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

각 층의 뉴런 수, 배치 크기, 매개변수 갱신 시의 학습률과 가중치 감소 등이 하이퍼파라미터일수도 있다.

6.5.1 검증 데이터

앞으로 하이퍼파라미터를 다양한 값으로 설정하고 검증할 텐데 테스트 데이터를 그 검증에 사용할수가 없다. 따라서 하이퍼파라미터 확인 전용 데이터가 필요하다. 이러한 데이터를 검증 데이터(validation data)라고 부른다. 

- 훈련 데이터: 매개변수 학습

- 검증 데이터: 하이퍼파라미터 성능 평가

- 시험 데이터: 신경망의 범용 성능 평가

MNIST데이터셋은 훈련데이터와 시험 데이터로만 분리되어있다. 따라서 직접 검증 데이터를 분리해야 한다. 

-> 가장 간단한 방법은 훈련 데이터 중 20% 정도를 검증 데이터로 미리 분리해놓는 것이다. 

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

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의 거듭제곱 단위로 범위를 지정하는데, 이를 '로그 스케일(log scale)로 지정'한다고 한다.

0단계

하이퍼파라미터 값의 범위를 설정

1단계

설정된 범위에서 하이퍼파라미터의 값을 무작위로 추출

2단계

1단계에서 샘플링한 하이퍼파라미터 값을 사용하여 학습하고, 검증 데이터로 정확도를 평가한다(단, 에폭은 작게!)

3단계

1단계와 2단계를 특정 횟수(ex. 100회) 반복하여, 그 정확도를 보고 하이퍼파라미터의 범위를 좁힌다.

->

이렇게 하이퍼파라미터의 범위가 좁아지면 이중에서 값을 하나 골라내는 것이 하이퍼파라미터 최적화의 한 방법이다. 

더 세련된 방법을 원한다면 베이즈 최적화가 있다. https://arxiv.org/abs/1206.2944

 

Practical Bayesian Optimization of Machine Learning Algorithms

Machine learning algorithms frequently require careful tuning of model hyperparameters, regularization terms, and optimization parameters. Unfortunately, this tuning is often a "black art" that requires expert experience, unwritten rules of thumb, or somet

arxiv.org

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

MNIST 데이터셋을 사용하여 하이퍼파라미터를 최적화해보자. 여기에서는 학습률, 가중치 감소 계수를 최적화할 것이다.

하이퍼파라미터의 검증은 보통 0.001 ~ 1,000 사이의 값을 무작위로 추출해 수행하는데 파이썬 코드로는 10 ** np.random.uniform(-3, 3)처럼 작성할 수 있다. 이번 예제에서는 가중치 계수를 10^-8 ~ 10^-4, 학습률을 10^-6 ~10^-2 범위에서 시작한다.

weight_decay = 10 ** np.random.uniform(-8, -4)
lr = 10 ** np.random.uniform(-6, -2)
import numpy as np
import matplotlib.pyplot as plt

from mnist import load_mnist
from multi_layer_net import MultiLayerNet
from util import shuffle_dataset
from trainer import Trainer

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

# 결과를 빠르게 얻기 위해 훈련 데이터를 줄임
x_train = x_train[:500]
t_train = t_train[:500]

# 20%를 검증 데이터로 분할
validation_rate = 0.20
validation_num = int(x_train.shape[0] * validation_rate)
x_train, t_train = shuffle_dataset(x_train, t_train)
x_val = x_train[:validation_num]
t_val = t_train[:validation_num]
x_train = x_train[validation_num:]
t_train = t_train[validation_num:]

def __train(lr, weight_decay, epocs=50):
    network = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100, 100, 100],
                            output_size=10, weight_decay_lambda=weight_decay)
    trainer = Trainer(network, x_train, t_train, x_val, t_val,
                      epochs=epocs, mini_batch_size=100,
                      optimizer='sgd', optimizer_param={'lr': lr}, verbose=False)
    trainer.train()

    return trainer.test_acc_list, trainer.train_acc_list

# 하이퍼파라미터 무작위 탐색======================================
optimization_trial = 100
results_val = {}
results_train = {}
for _ in range(optimization_trial):
    # 탐색한 하이퍼파라미터의 범위 지정===============
    weight_decay = 10 ** np.random.uniform(-8, -4)
    lr = 10 ** np.random.uniform(-6, -2)
    # ================================================

    val_acc_list, train_acc_list = __train(lr, weight_decay)
    print("val acc:" + str(val_acc_list[-1]) + " | lr:" + str(lr) + ", weight decay:" + str(weight_decay))
    key = "lr:" + str(lr) + ", weight decay:" + str(weight_decay)
    results_val[key] = val_acc_list
    results_train[key] = train_acc_list
    
# 그래프 그리기
print("=========== Hyper-Parameter Optimization Result ===========")
graph_draw_num = 20
col_num = 5
row_num = int(np.ceil(graph_draw_num / col_num))
i = 0

for key, val_acc_list in sorted(results_val.items(), key=lambda x:x[1][-1], reverse=True):
    print("Best-" + str(i+1) + "(val acc:" + str(val_acc_list[-1]) + ") | " + key)

    plt.subplot(row_num, col_num, i+1)
    plt.title("Best-" + str(i+1))
    plt.ylim(0.0, 1.0)
    if i % 5: plt.yticks([])
    plt.xticks([])
    x = np.arange(len(val_acc_list))
    plt.plot(x, val_acc_list)
    plt.plot(x, results_train[key], "--")
    i += 1

    if i >= graph_draw_num:
        break

plt.show()

 

검증 데이터의 정확도가 높은 순서로 학습 추이를 나열하면 다음과 같다.

hyperparameter_optimization.py 실행 결과
실선은 검증 데이터에 대한 정확도, 점선은 훈련 데이터에 대한 정확도

결과를 보면 학습이 잘 진행될 때의 학습률은 0.001 ~ 0.01, 가중치 감소 계수는 10^-8 ~ 10^-6 정도라는 것을 알 수 있다.

이렇게 범위를 좁혀가다가 특정 단계에서 최종 하이퍼파라미터 값을 하나 선택한다.

6.6 정리

  • 매개변수 갱신 방법에는 확률적 경사 하강법(SGD) 외에도 모멘텀, AdaGrad, Adam 등이 있다.
  • 가중치 초깃값을 정하는 방법은 올바른 학습을 하는 데 매우 중요하다.
  • 가중치의 초깃값으로는 ‘Xavier 초깃값’과 ‘He 초깃값’이 효과적이다.
  • 배치 정규화를 이용하면 학습을 빠르게 진행할 수 있으며, 초깃값에 영향을 덜 받게 된다.
  • 오버피팅을 억제하는 정규화 기술로는 가중치 감소와 드롭아웃이 있다.
  • 하이퍼파라미터 값 탐색은 최적 값이 존재할 법한 범위를 점차 좁히면서 하는 것이 효과적이다.
반응형

댓글