안녕, 세상!

6. 학습 관련 기술들 본문

It공부/Deep learning

6. 학습 관련 기술들

dev_Lumin 2020. 6. 21. 16:18

(1) 확률적 경사 하강법(SDG) 단점

이전에 가중치W에 대한 손실함수의 기울기를 통해 기울어진 방향으로 매개변수 값을 갱신하는 학습을 하는 방식을 SDG라고 합니다.

SDG의 단점을 설명하기 이전에 기본 수식 및 코드를 복습하겠습니다.

SDG의 수식은 다음과 같습니다.

SDG 수식

SDG를 class로 간략하게 표현하면 다음과 같습니다.

이를 동작 시키는 코드의 일부는 다음과 같습니다. (설명용으로 일부만 보여진 코드입니다.)

optimizer 변수는 '최적화를 행하는 자'라는 의미를 가진 변수입니다.

여기서 그 역할을 SDG가 한다는 것입니다.

 

 

SDG 단점

다음 예시 수식을 통해서 설명하겠습니다.

위 식을 그래프로 나타내면 다음과 같습니다.

그리고 위 함수의 기울기를 그려보면 다음과 같습니다.

이 함수의 최솟값이 되는 장소는 (x, y) = (0, 0) 이지만, 보여지는 대부분의 기울기는 (0, 0) 방향을 가리키지 않습니다.

그러므로 최솟값인 (0, 0) 까지 이동하는데 비효율적입니다.

(-7, 2)가 초깃값일 경우 위 함수에 SDG를 적용하면 다음과 같은 자취로 접근하게 됩니다.

따라서 SDG의 단점은 비등방성 함수(방향에 따라 성질(기울기)이 달라지는 함수)에서는 탐색 경로가 비효율적이라는 것입니다.

 

 

 

 

 

 

(2) SDG 이외의 최적화 방법

①모멘텀

모멘텀(Momentum)은 물리와 관계있는 '운동량'을 뜻하는 단어이고 수식은 다음과 같습니다.

모멘텀 수식

W는 갱신할 가중치 매개변수, 에타(n)는 학습률, dL/dW는 기울기 입니다.

새로 나온 변수 V는 물리에서 말하는 속도에 해당됩니다.

첫번째 식은 기울기 방향으로 힘을 받아 물체가 가속된다는 물리법칙을 나타내고

두번째 식은 공이 그릇 바닥을 구르는듯한 움직임을 보여주는 식입니다.

aV는 물체가 아무런 힘을 받지 않을 때 하강하는 속도입니다.

(알파는 물리에서 지면 마찰이나 공기 저항에 해당됨. 보통 0.9 등 값으로 설정)

 

모멘텀을 코드로 나타나면 다음과 같습니다.

모멘텀 코드

for key, val in x.items() - 딕셔너리 변수x로 부터 키값과 해당 값을 한번에 추출해서 반복문을 실행시킬 때 사용

  

np.zeros_like() - 주어진 넘파이배열과 같은 크기의 넘파이배열을 생성 후 0으로 초기화 시키는 함수

                      딕셔너리변수는 다음과 같이 0으로 초기화 시키면 됩니다.

 

모멘텀 코드를 설명하자면 self.v가 None일 경우에 처음 한번 params 변수와 같은 딕셔너리형태인 넘파이 배열에 값들을 0으로 초기화 시키고 모멘텀식을 적용시켜서 반복문을 통해 모멘텀을 구현시킵니다.

이를 초깃값이 (-7, -2)일 경우 그래프로 표현하면 다음과 같이 나옵니다.

SDG와 비교하면 '오락가락'을 덜하고 좀 더 효율적으로 최솟값에 접근하는 것을 볼 수 있습니다.

 

 

 

 

 

AdaGrad

신경망에서 학습률(에타) 값을 너무 작으면 학습시간이 너무 길어지고, 너무 크면 발산하여 올바른 학습을 할 수 없다고 이전에 설명을 했습니다. 

그래서 학습률이 중요한데 학습을 진행하면서 학습률을 점차 줄여가는 학습률 감소로 효율적으로 최솟값에 접근할 수 있습니다.

학습률을 서서히 낮추는 간단한 방법은 매개변수 전체의 학습률을 일괄적으로 낮추는 것이겠지만 여기서 더욱 발달시킨 것이 AdaGrad로 각각의 매개변수에 맞춤형 값을 만들어서 학습률을 낮춥니다.

AdaGrad를 수식으로 표현한것은 다음과 같습니다.

h의 값은 가중치에 대한 손실함수의 변화량 즉 미분의 제곱을 더한 값인데('동그라미에 점' 기호는 행렬의 원소별 곱셈을 의미함) , dL/dW값이 클 수록 학습률이 작아지는 변화로 학습률을 조정합니다.

이를 코드로 표현하면 다음과 같습니다.

np.sqrt() - 루트를 표현하는 넘파이 배열 

 

1e-7 값은 루트안의 값이 0이 되는것을 방지하기 위해 아주 작은값을 넣은 용도입니다.

 

위의 코드를 초깃값이 (-7, -2)일 경우 그래프로 나타내면 다음과 같습니다.

그래프를 확인해보면 최솟값을 향해 효율적으로 움직이는 것을 알 수 있습니다.

 

 

 

 

 

③ Adam

모멘텀과 AdaGrad 이 두 기법을 융합해서 만든 최적화 방법입니다.

코드는 다음과 같습니다.

위 코드를 초깃값 (-7, -2)일 경우의 그래프를 그리면 다음과 같습니다.

 

 

위의 SDG, 모멘텀, AdaGrad, Adam 중 그림만 보면 AdaGrad가 가장 나은것 같지만, 사실은 풀어야 할 문제에 따라서 효율성이 달라지므로 무엇이 가장 나은 최적화 모델이라고 할 수 없습니다.

 

 

 

 

 

 

 

 

(3) 가중치의 초깃값

신경망 학습에서 가중치의 초깃값은 정말 중요합니다. 

가중치의 초깃값을 잘 설정하면 신경망 학습이 신속하게 이뤄질 수 있습니다.

가중치가 크면 즉, 가중치들의 차이가 크면 크기가 큰 값을 가진 가중치가 영향력이 강해버려져서 오버피팅이 일어날 수 있습니다.

오버피팅을 억제해 범용 성능을 높이는 방법인 가중치 감소 기법이 있습니다.

가중치 값을 작게 하여 오버피팅이 일어나지 않게 하는 방법입니다. 

가중치를 작게 만들고 싶으면 초깃값도 최대한 작은 값에서 시작하는 것이 맞긴 합니다.

그렇다고 가중치를 모두 0으로 설정하면 안됩니다.

 정확하게는 가중치의 초깃값이 모두 균일한 값으로 설정해서는 안됩니다. 

그 이유는 오차역전법에서 모든 가중치의 값이 똑같이 갱신되기 때문입니다.

첫 번째 층 가중치값이 모두 같으면 두 번째 층의 출력값이 동일하게 되고 두번째층의 역전파로 가중치에 대한 손실함수의 값의 원소들이 동일하게 되므로 두 번째층의 가중치가 모두 똑같이 갱신이 됩니다.

( 갱신코드:  .params[key] -= learning_rate * grad[key] )

똑같이 갱신이되면 그 그다음 출력층의 값도 모두 동일할 것이고 계속 반복이 됩니다.

이는 가중치를 여러 개 갖는 의미를 사라지게 합니다.

그러므로 가중치가 고르게 되어버리는 상황을 막으려면 초깃값을 무작위로 설정해야 합니다.

 

 

 

은닉층의 활성화값 분포

가중치의 초기값에 따라 은닉층의 활성화값들이 어떻게 변하는지 알아야 가중치 초깃값을 효율적으로 설정할 수 있습니다.

다음 코드는 시그모이드 함수를 활성화 함수로 사용하고 은닉층이 5개 있으며 각 은닉층은 100개의 뉴런을 가지고 있습니다. 입력층은 1000개의 데이터가 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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(1000100)  # 1000개의 데이터
node_num = 100  # 각 은닉층의 노드(뉴런) 수
hidden_layer_size = 5  # 은닉층이 5개
activations = {}  # 이곳에 활성화 결과를 저장
 
= 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(1len(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(), 30range=(0,1))
plt.show()
cs

 

plt.hist() - 히스토그램 형식으로 그래프를 만드는 함수

 

위의 plt.hist의 a.flatten은 다차원 형식인 a의 행렬을 1차원으로 바꾸고 30은 분포하는 막대기의 수, range는 그래프 x축의 범위를 나타냅니다.

 

 

활성화 함수가 시그모이드이고 표준편차가 1인 위의 코드 결과는 다음과 같습니다.

각 층의 활성화값들이 0과 1에 치우쳐 분포되어 있습니다. 

시그모이드 함수의 역전파는 dL/dy*(1-y)*y 입니다. 

활성화값인 y가 0과 1에 치우친다는 말은 출력이 0혹은 1에 가까워지자 미분이 0에 다가가게되어 역전파의 기울기 값이 점점 작아지다가 사라지게 된다는 의미입니다.

이를 기울기 소실이라고 합니다.

 

앞의 코드에서 활성화함수는 시그모이드로 그대로 하고 표준편차를 0.01로 바꿨을 경우는 다음과 같은 히스토그램을 얻을 수 있습니다.

이 경우엔 0.5부근에 밀집되어 있습니다. 

앞의 경우처럼 0과 1에 치우치진 않았으니 기울기 소실은 발생하지 않지만 활성화 값들이 치우쳐서 다수의 뉴런이 거의 같은 값을 출력하고 있는 상황이니 뉴런을 여러 개 둔 의미가 없어지게 됩니다.

이렇게 활성화값들이 치우치면 표현력을 제한한다는 문제가 발생합니다.

 

기울기소실과 표현력 제한이라는 문제로 부터 벗어나기 위한 방법이 있습니다.

가중치의 초깃값을 Xavier초깃값으로 사용하는 것입니다.

Xavier초깃값의 특징은 앞 계층의 노드가 n개라면 표준편차가 1/n(루트n분의 일) 입니다.

위의 코드를 이용해서 히스토그램을 출력하면 다음과 같습니다.

층이 깊어지면서 형태가 약간 일그러지지만, 확실히 넓게 분포되어 있습니다.

따라서 시그모닝드 함수의 표현력도 제한받지 않고 학습이 효율적으로 이뤄질것을 기대할 수 있습니다.

층이 깊어질수록 일그러짐은 sigmoid함수대신 tanh함수를 이용하면 개선됩니다. 

( tanh함수는 (0,0)에서 대칭인 쌍곡선 함수입니다. 참고로 sigmoid는 (0, 0.5)에서 대칭인 곡선입니다. )

 

 

활성화 함수를 ReLU를 사용할 때는 ReLU에 특화된 초깃값인 He 초깃값을 이용합니다. 

He초깃값은 앞 계층의 노드가 n개일 때, 표준편차가 √(2/n)인 정규분포를 사용합니다.

ReLU는 음의 영역이 0이라서 더 넓게 분포시키기 위해 2배의 계수가 필요하다고 직감적으로 해석할 수 있습니다.

활성화 함수가 ReLU일 때 초깃값의 표준편차가 0.01, 초깃값이 Xavier, He 초깃값일 경우 그래프는 다음과 같습니다.

표준편차가 0.01경우

각 층의 활성화 값들이 아주 작은 값입니다. 신경망에 아주 작은 데이터가 흐른다는 것은 역전파 때 가중치의 기울기가 작아진다는 뜻입니다. 따라서 실제로 학습이 거의 이뤄지지 않습니다.

초깃값이 Xavier일 경우

층이 깊어지면서 치우침이 조금씩 커집니다. 

실제로 층이 깊어지면서 활성화값들의 치우침이 커지고, '기울기 소실' 문제를 일으킵니다.

초깃값이 He초깃값일 경우

모든층에서 균일하게 분포되며 층이 깊어져도 분포가 균일하게 유지되어서 역전파 때도 적절한 값이 나올 것으로 기대할 수 있습니다.

 

 

위의 가중치 초깃값이 학습에 얼마나 영향을 미치는지 숫자 글씨 분류 데이터를 기반으로 실제 데이터를 가지고 코드를 작성하면 다음과 같은 그래프가 나옵니다.

표준편차가 0.01일 경우에는 거의 학습이 되지 않았고, Xavier과 He는 학습이 순조롭게 이뤄진것을 볼 수 있습니다.

지금까지 보았듯이 가중치의 초깃값은 신경망 학습에 아주 중요한 요소입니다.

 



Sigmoid함수가 아닌 ReLU함수를 쓰는이유

Sigmoid함수는 기울기 소실이라는 치명적인 문제 때문에 사용을 하지 않습니다.

Sigmoid함수는 출력 값이 무조건 0에서 1사이의 숫자입니다.

신경망의 은닉층이 약 2~3개 층으로 이뤄져 있다면 활성화 함수로 Sigmoid함수를 이용해도 크게 문제가 없겠지만, 깊은 층의 신경망을 구현 할 경우 역전파 시 Sigmoid출력값의 곱들이 축적되어 미분값이 0에 가까운 값에 가까워져서 기울기 소실 문제가 발생하게 됩니다.

기울기 소실이 발생하게 되면 가중치의 변화량이 거의 없어서 갱신하기 이전과 차이가 거의 없습니다.

따라서 이를 해결하기 위해 활성화 함수를 ReLU함수로 사용하여 문제점을 보완합니다.

 

 

 

 

(4) 배치 정규화

배치 정규화(Batch Nomalization)은 각 층의 활성화를 적당히 퍼뜨리는 행위를 '강제'로 행하는 방법입니다.

배치 정규화를 하게되면 다음과 같은 이점이 있습니다.

1. 학습을 빨리 진행할 수 있습니다.

2. 초깃값에 크게 의존하지 않습니다.

3. 오버피팅을 억제합니다. 

배치 정규화는 학습 시 미니배치를 단위로 정규화하며 데이터의 분포가 평균이 0이고 적절하게 분포가 되게 정규화합니다.

수식은 다음과 같습니다.

예시로 미니배치 A=[1,2,3,4,5] 가 있다고 할 때 이를 정규화 시키면 A=[-√2, -√2/2, 0, √2/2, √2]가 됩니다. 

평균이 0이고 적절히 분산이 되었습니다.

배치 정규화 계층마다 정규화되 데이터에 고유한 확대와 이동변환을 수행합니다.

수식은 다음과 같습니다.

곱하는 값이 확대, 더하는 값이 이동을 담당합니다.

 

배치 정규화를 진행하면 거의 모든 경우에서 학습 진도가 빠릅니다.

코드는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
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=[100100100100100], output_size=10
                                    weight_init_std=weight_init_std, use_batchnorm=True)
    network = MultiLayerNetExtend(input_size=784, hidden_size_list=[100100100100100], 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)
= 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(01.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()
cs

 

배치 정규화의 역전파 과정은 아래에서 참고하면 됩니다.

https://kratzert.github.io/2016/02/12/understanding-the-gradient-flow-through-the-batch-normalization-layer.html

 

Understanding the backward pass through Batch Normalization Layer

At the moment there is a wonderful course running at Standford University, called CS231n - Convolutional Neural Networks for Visual Recognition, held by Andrej Karpathy, Justin Johnson and Fei-Fei Li. Fortunately all the course material is provided for fre

kratzert.github.io

 

 

 

(5) 오버피팅

오버피팅은 신경망이 훈련 데이터에만 지나치케 적응되어 그 외의 데이터에는 제대로 대응하지 못하는 상태를 말합니다.

보통 주로 다음의 두 경우에 일어납니다.

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

2. 훈련데이터가 적음

위 두 조건을 일부로 충족하여 오버피팅을 일으키는 손글씨 분류 코드는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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.multi_layer_net import MultiLayerNet
from common.optimizer import SGD
 
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
 
# 오버피팅을 재현하기 위해 학습 데이터 수를 줄임
x_train = x_train[:300]
t_train = t_train[:300]
 
# weight decay(가중치 감쇠) 설정 =======================
weight_decay_lambda = 0 # weight decay를 사용하지 않을 경우
#weight_decay_lambda = 0.1 # 람다가 0.1인 경우
# ====================================================
 
network = MultiLayerNet(input_size=784, hidden_size_list=[100100100100100100], output_size=10,
                        weight_decay_lambda=weight_decay_lambda)
optimizer = SGD(lr=0.01# 학습률이 0.01인 SGD로 매개변수 갱신
 
max_epochs = 201
train_size = x_train.shape[0]
batch_size = 100
 
train_loss_list = []
train_acc_list = []
test_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]
 
    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)
        test_acc = network.accuracy(x_test, t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print("epoch:" + str(epoch_cnt) + ", train acc:" + str(train_acc) + ", test acc:" + str(test_acc))
 
        epoch_cnt += 1
        if epoch_cnt >= max_epochs:
            break
 
 
# 그래프 그리기==========
markers = {'train''o''test''s'}
= np.arange(max_epochs)
plt.plot(x, train_acc_list, marker='o', label='train', markevery=10)
plt.plot(x, test_acc_list, marker='s', label='test', markevery=10)
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(01.0)
plt.legend(loc='lower right')
plt.show()
cs

 위의 결과 그래프는 다음과 같습니다.

훈련 데이터와 실험데이터의 정확도가 차이나는 것을 볼 수 있습니다.

 

이를 해결하기위해 가중치 감소와 드롭아웃 방식이 있습니다.

 

① 가중치 감소

앞서 말했듯이 가중치가 크면 즉, 가중치들의 차이가 크면 크기가 큰 값을 가진 가중치가 영향력이 강해버려져서 오버피팅이 일어날 수 있습니다.

여기서 크기는 수치값의 절댓값을 말하는 것입니다.

이를 해결하기 위해서 손실함수에 1/2*W**2 을 더해서 가중치가 큰 값이 나오면 가중치 갱신 과정에서 가중치의 크기를 크게 줄일 수 있습니다.

여기서 람다(⋋)는 정규화의 세기를 조절하는 하이퍼파라미터입니다.

자세히 설명하자면 가중치 갱신 과정은 다음과 같습니다.

코드식

코드식의 틀에 수학적 표현을 하여 다음과 같이 표현했습니다.

갱신과정은 반복문에 의해 계속 반복될 것입니다.

손실함수에 1/2*W**2을 더했으니 1/2*W**2을 역전파 과정으로 미분을 하면 깔끔하게 W가 됩니다. 

위의 그림과 같이 갱신과정에서 W의 값이 개입되면서 가중치의 W의 크기가 크면 W의 크기를 줄여주는 역할을 하게되는 것입니다.

예시로 확실하게 설명하겠습니다.

W의 가중치가 양수일 경우 그 크기가 너무 크면 W라는 값을 빼줌으로써 양수의 크기를 줄여서 갱신할 것입니다.

W의 가중치가 음수일 경우 그 크기가 너무 크면 W라는 값을 더해줌으로써 음수의 크기를 줄여서 갱신할 것입니다.

 

손실함수에 값을 1/2*W**2이라는 수치를 더해준 것은 위에서 언급했듯이 미분하면 깔끔하게 가중치의 상수배가 되는 값이 나오기 때문에 다음과 같은 수치를 설정한것 같습니다.

 

위의 오버피팅 부분을 일부로 일으키는 코드에서 람다의 값을 0.1로 설정하고 코드를 실행시키면 다음과 같은 결과가 나옵니다.

그래도 훈련데이터와 실험데이터의 차이가 있지만 차이가 줄어든것을 위와 비교했을 때 확인할 수 있습니다.

 

 

② 드롭아웃

신경망 모델이 복잡해지면 가중치 감소만으로는 오버피팅을 대응하기 어려워집니다.

이러한 경우 드롭아웃을 이용합니다.

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

훈련 때 은닉층의 뉴런을 무작위로 골라 삭제하고 삭제된 뉴런은 신호를 전달하지 않게 됩니다.

훈련 때 데이터 한 번 흘릴 때 마다 삭제할 뉴런을 무작위로 선택하고, 시험 때는 모든 뉴런에 신호를 전달합니다.

단, 시험 때는 각 뉴렁늬 출력에 훈련 때 삭제한 비율을 곱하여 출력합니다.

무작위로 선택함으로써 가중치 차이가 큰 경우를 피해서 정확도를 높일 수도 있고, 반복문을 통해 반복 될 때 마다 매번 무작위로 선택하면서 무작위 선택으로 인한 오차를 줄여 시험데이터의 정확도를 높여줄 수 있습니다.

기계학습에서는 앙상블 학습을 애용합니다.

앙상블 합습은 개별적으로 학습시킨 여러 모델의 출력을 평균 내어 추론하는 방식입니다.

앙상블 학습을 수행하면 신경망의 정확도가 개선된다는 것이 실험적으로 알려져있습니다.

드롭아웃은 앙승블 학습과 밀접합니다. 

드롭아웃 학습 때 뉴런을 무작위로 삭제하는 행위를 매번 다른 모델을 학습시키는 것으로 해석할 수 있기 때문입니다.

추론 때 뉴런의 출력에 삭제한 비율을 곱함으로써 앙상블 학습에서 여러 모델의 평균을 내는 것과 같은 효과를 얻는 것입니다.

 

코드는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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.multi_layer_net_extend import MultiLayerNetExtend
from common.trainer import Trainer
 
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
 
# 오버피팅을 재현하기 위해 학습 데이터 수를 줄임
x_train = x_train[:300]
t_train = t_train[:300]
 
# 드롭아웃 사용 유무와 비율 설정 ========================
use_dropout = True  # 드롭아웃을 쓰지 않을 때는 False
dropout_ratio = 0.2
# ====================================================
 
network = MultiLayerNetExtend(input_size=784, hidden_size_list=[100100100100100100],
                              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)
trainer.train()
 
train_acc_list, test_acc_list = trainer.train_acc_list, trainer.test_acc_list
 
# 그래프 그리기==========
markers = {'train''o''test''s'}
= np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, marker='o', label='train', markevery=10)
plt.plot(x, test_acc_list, marker='s', label='test', markevery=10)
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(01.0)
plt.legend(loc='lower right')
plt.show()
cs

 드롭아웃을 사용하지 않은 경우와 사용한 경우의 결과는 다음과 같습니다.

드롭아웃을 적용했을 때 정확도의 차이가 줄어든 것을 확인할 수 있습니다.

 

 

 

(6) 적절한 하이파라미터

하이퍼파라미터는 사용자가 직접 설정해야된는 값으로 신경망에서는 각 층의 뉴런수, 배치크기, 매개변수 갱신 시의 학습률과 가중치 감소 등이 있습니다.

이러한 하이퍼파라미터 값을 적절히 설정하지 않으면 모델의 성능이 떨어집니다.

효율적으로 적절한 하이퍼파라미터 설정방법은 다음과 같습니다.

 

 

① 검증 데이터

데이터셋을 훈련 데이터와 시험 데이터 두 가지로 분류해서 범용 성능을 평가해 왔습니다.

하이퍼파라미터를 다양한 값으로 설정하고 검증할 경우 주의 할 점은 하이퍼파라미터의 성능을 평가 할 때는 시험 데이터를 사용해서는 안됩니다.

그 이유는 시험데이터를 사용하여 하이퍼파라미터를 조정하면 하이퍼파라미터 값이 시험 데이터에 오버피팅되기 때문입니다.

따라서 하이퍼파라미터를 조정할 때는 하이퍼파라미터 전용 확인 데이터가 필요합니다.

이를 일반적으로 검증 데이터(Validation data)라고 부릅니다.

 

데이터셋중엔 훈련 데이터, 검증 데이터, 시험 데이터를 미리 분리 해둔 것도 있지만 mnist 손글씨 숫자 분류는 훈련데이터, 시험데이터만 분리해뒀으므로 사용자가 직접 훈련데이터의 일부를 검증 데이터로 먼저 분리하면 됩니다.

 

 

② 하이퍼파라미터 최적화

하이퍼파라미터를 최적화할 때의 핵심은 '최적 값'이 존재하는 범위를 조금 씩 줄여간다는 것입니다.

대략적인 범위를 설정하고 그 범위에서 무작위로 하이퍼파라미터 값을 골라낸 후, 그 값으로 정확도를 평가합니다.

정확도를 확인하면서 이 작업을 여러번 반복하며 '최적 값'의 범위를 좁혀가는 것입니다.

대략적으로 지정하는 것을 '로그 스케일(log scale)'로 지정한다고 합니다.

하이퍼파라미터를 최적화 할 때는 딥러닝 학습에서 오랜시간(며칠이나 몇 주이상)이 걸린다는 점을 고려해야 합니다.

학습을 위한 에폭을 작게하여, 1회 평가에 걸리는 시간을 단축하는 방법이 있습니다.

 

단계를 정리하면 다음과 같습니다.

1. 하이퍼파라미터 값의 범위를 설정합니다.

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

3. '2'에서 추출한 하이퍼파라미터 값을 사용하여 학습하고, 검증 데이터로 정확도를 평가합니다.

4. '2'와 '3'단계를 특정 횟수 반복하며, 그 정확도를 보고 하이퍼파라미터의 범위를 좁힙니다.

 

하이퍼파라미터의 검증은 그 값을 로그 스케일 범위에서 무작위로 추출해 수행합니다.

예를 들어 0.0001~100 사이같은 로그 스케일 범위에서 무작위로 추출해 수행합니다

이를 파이썬 코드로는 10** np.random.uniform(-4, 2) 처럼 작성할 수 있습니다.

 

아래의 코드는 가중치 감소 계수를 10**-8 ~ 10**-4, 학습률을 10**-6 ~ 10**-2 범위로 한 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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 import MultiLayerNet
from common.util import shuffle_dataset
from common.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=[100100100100100100],
                            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))
= 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.01.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()
cs

 결과의 그래프는 다음과 같습니다.

위 결과는 정확도가 높은 순서대로 표기한 그래프입니다.

Best-1부터 Best-5를 확인하면 다음과 같습니다.

 

학습이 잘 진행될 때의 학습률은 0.001~0.01, 가중치 감소계수는 10**-8 ~ 10**-5 정도라는 것을 알 수 있습니다.

이처럼 잘된 값의 범위를 관찰하고 범위롤 좁혀서 축소된 범위로 똑같은 작업을 반복하는 것입니다.

반복해서 좁혀가다가 특정 단계에서 최종 하이퍼파라미터 값을 하나 선택합니다.

 

 

 

하이퍼파라미터 최적화 방법은 실용적인 방법이지만 과학이라기보다는 수행자의 '직관'에 의존한다는 느낌이 듭니다.

좀 더 세련된 기법은 베이즈 최적화(Bayesian optimization)가 있습니다.

베이즈 최적화는 베이즈 정리를 중심으로 한 수학 이론을 구사하여 더 엄밀하고 효율적으로 최적화를 수행합니다.

 

 

 

 

 

위의 코드에서 필요한 라이브러리 함수들은 깃허브에서 보고 참고하면 됩니다.

https://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

 

'It공부 > Deep learning' 카테고리의 다른 글

(2)-1 자연어 처리 하기 전에  (0) 2020.06.29
7. CNN  (0) 2020.06.26
5. 오차역전파법  (0) 2020.06.19
4. 신경망 학습  (0) 2020.06.17
3-(1) 손글씨 숫자 인식 분석  (1) 2020.06.16
Comments