본문 바로가기

Deep Learning(강의 및 책)/밑바닥부터 시작하는 딥러닝

6. 학습 관련 기술들

728x90

밑바닥부터 시작하는 딥러닝을 바탕으로 제작하였습니다.

 

6.1. 매개변수 갱신

신경망 학습의 목적은 손실 함수의 값을 가능한 한 낮추는 매개변수를 찾는 것입니다. 이는 곧 매개변수의 최적값을 찾는 문제이며, 이러한 문제를 푸는 것을 최적화(optimizer)라 합니다. 이번에는 가중치 매개변수의 최적값을 효율적으로 탐색하는 방법을 알아볼 것이고, 매개변수 갱신을 구현하겠습니다.

 

 ■ 확률적 경사 하강법(SGD, Stochastic Gradient Descent)

이 전에 사용했던 방식으로 손실함수 변화량과 학습률의 곱을 이용해 가중치를 갱신하는 방식입니다.

 

이러한 SGD를 구현해보겠습니다.

class SGD:

    """확률적 경사 하강법(Stochastic Gradient Descent)"""

    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] 

이와 같이 구현할 수 있습니다. 초기화 시 받는 인수인 lr은 learning rate를 의미합니다. update 메서드는 SGD 과정에서 반복해서 불립니다. 인수인 params와 grads는 딕셔너리 변수입니다. key값에 맞춰 각각 가중치 매개변수와 기울기를 저장합니다.

 

 ■ SGD의 단점

SGD는 단순하고 구현도 간단하지만, 문제에 따라서 비효율적일 때가 있습니다. 예를 들어

이와 같은 식이 존재하고 그래프로 그리면

 

 

 

이와 같이 그릴 수 있습니다. 오른쪽 그림은 그래프에 대한 등고선입니다. 이 함수의 기울기를 그려보면

이와 같이 생겼습니다. 이 기울기는 y축 방향은 크고 x축 방향은 작다는 것이 특징입니다. 즉 y축 방향은 가파른데 x축 방향은 완만합니다. 여기서 최솟값은 (x, y) = (0, 0)이지만 위 그림을 보면 대부분 (0, 0)을 가리키지 않습니다.

이번에는 시작점을 (x, y) = (-7.0, 2.0)으로 설정하고 SGD를 사용해 탐색을 해보면

이와 같이 지그재그로 이동하면서 최솟값을 찾습니다. 이와 같이 심하게 굽이진 움직임을 보이면 상당히 비효율적입니다. 즉, SGD의 단점은 비등방성 함수(방향에 따라 성질, 즉 여기에서는 기울기가 달라지는 함수)에서는 탐색 경로가 비효율적이라는 것입니다. 이러한 단점을 개선하는 방법을 보겠습니다.

 

 ■ 모멘텀(Momentum)

모멘텀은 '운동량'을 뜻하는 단어입니다. 모멘텀 기법은 수식으로 나타내면

이와 같이 표현할 수 있습니다. SGD와는 다르게 v라는 변수가 등장합니다. 이는 물리에서 말하는 속도에 해당합니다. av항은 물체가 아무런 힘을 받지 않을 때 서서히 하강시키는 역할을 합니다(a는 0.9 등의 값으로 설정합니다). 이러한 모멘텀을 구현하겠습니다.

 

 

class 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]

인스턴스 변수 v가 물체의 속도입니다. v는 초기화 때는 아무 값도 담지 않고, 대신 update()가 처음 호출될 때 매개변수와 같은 구조의 데이터를 딕셔너리 변수로 저장합니다. 이 모멘텀을 사용해 최적화 문제를 풀면

 

이와 같이 SGD보다 지그재그 정도가 덜한 것을 볼 수 있습니다. x축의 힘은 아주 작지만 방향은 변하지 않아서 한 방향으로 일정하게 가속하기 때문입니다. 거꾸로 y의 힘은 크지만 위아래로 번갈아 받아서 상층하여 y축 방향의 속도는 안정적이지 않습니다. SGD보다 x축 방향으로 빠르게 다가가 지그재그 움직임이 줄어듭니다.

 

 ■ AdaGrad

신경망 학습에서는 learning rate의 값이 중요합니다. 너무 작으면 학습이 되지 않고 너무 크면 발산합니다. 이 학습률을 정하는 효과적 기술로 학습률 감소가 있습니다. 이는 학습을 진행하면서 학습률을 점차 줄여가는 방법입니다.

AdaGrad는 각각의 매개변수에 맞춤형 값을 만들어줍니다. 개별 매개변수에 적응적으로 학습률을 조정하면서 학습을 진행합니다. 식으로 나타내면

이와 같이 작성할 수 있습니다. 여기서 새로운 변수 h가 등장합니다. h는 기존 기울기 값을 제곱하여 계속 더해줍니다. 그리고 매개변수를 갱신할 때 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)

여기에서 주의할 것은 마지막 줄에서 1e-7이라는 작은 값을 더하는 부분입니다. 이 작은 값은 self.h[key]에 0이 담겨 있다 해도 0으로 나누는 사태를 막아줍니다. 이제 AdamGrad를 이용해 최적화 문제를 풀어보면

이와 같이 최솟값을 향해 효율적으로 이동하는 것을 볼 수 있습니다. y축 방향은 기울기가 커서 처음에는 크게 움직이지만, 그 큰 움직임에 비례해 갱신 정도도 큰 폭으로 작아지도록 조정됩니다. 그래서 y축 방향으로 갱신 강도가 빠르게 약해지고, 지그재그 움직임이 줄어듭니다.

 

 ■ Adam

Adam은 모멘텀과 AdamGrad를 융합하기 위해 생성된 기법입니다. 두 방법의 이점을 조합했으며 하이퍼파라미터의 '편향 보정'이 진행됩니다. 이러한 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)

이와 같이 구현할 수 있으며 이를 이용해 최적화 갱신 경로를 확인해보면

이와 같이 나타납니다. 모멘텀과 비슷한 패턴을 보여주지만 공의 좌우 흔들림이 적습니다. 이는 AdamGrad와 같이 learning rate를 갱신하기 때문입니다.

 

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

여태까지 본 결과로 AdamGrad가 가장 효과적으로 갱신하는 것 같지만 풀어야 하는 문제에 따라 효과적인 갱신 방법은 다릅니다. 즉 상황에 따라 효과적인 방법을 사용하면 됩니다.

 

6.2. 가중치의 초깃값

신경망 학습에서 특히 중요한 것은 가중치의 초깃값입니다. 가중치의 초깃값을 무엇으로 설정하느냐가 신경망 학습의 성패가 가르는 일이 설제로 자주 있습니다. 이번에는 초깃값에 따른 학습을 살펴보겠습니다.

 ■ 초깃값을 0으로 하면?

초깃값을 모두 0으로 해서는 안됩니다.(정확히는 가중치를 균일한 값으로 설정해서는 안됩니다). 왜냐하면 오차역전파법에서 모든 가중치의 값이 똑같이 갱신되기 때문입니다. 예를 들어 2층 신경망에서 첫 번째와 두 번째 층의 가중치가 0이라고 가정하겠습니다. 그럼 순전파 때는 입력층의 가중치가 0이기 때문에 두 번째 층의 뉴런에 모두 같은 값이 전달됩니다. 즉 역전파 때 두 번째 층의 가중치가 모두 똑같이 갱신된다는 말이 됩니다. 그래서 가중치들은 같은 초깃값에서 시작하고 갱신을 거쳐도 여전히 같은 값을 유지하게 됩니다. 이러한 이유로 초깃값을 무작위로 설정해야 합니다.

 

 ■ 은닉층의 활성화값 분포

가중치가 똑같이 갱신되는 것을 방지하려면 가중치가 고르게 분포되어 있어야 합니다. 이번에는 초깃값에 따라 은닉층 활성화 값들이 어떻게 변하는지 보겠습니다. 

활성화 함수로 sigmoid함수를 사용하는 5층 신경망에 무작위로 생성한 입력 데이터를 흘려 각 츠으이 활성화값 분포를 확인해보겠습니다.

import numpy as np
import matplotlib.pyplot as plt


def sigmoid(x):
    return 1 / (1 + np.exp(-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
    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")
    if i != 0: plt.yticks([], [])
    # plt.xlim(0.1, 1)
    # plt.ylim(0, 7000)
    plt.hist(a.flatten(), 30, range=(0,1))
plt.show()

이와 같이 코드를 작성할 수 있습니다. 층이 5개가 있고 각 층의 뉴런은 100개씩입니다. 입력 데이터로서 1000개의 데이터를 정규분포로 무작위로 생성하여 이 5층 신경망에 흘립니다. 활성화 결과는 activations 변수에 저장합니다. 그리고 표준편차를 1로 정규분포를 사용했습니다. 결과를 확인해보면

이와 같이 나옵니다. sigmoid 함수를 활성화 함수로 사용했기 때문에 값이 0과 1에 치우쳐 분포되어 있습니다. sigmoid함수는 0과 1 근처에 가까워지면 미분 결과는 0에 가까워집니다. 그래서 데이터가 0과 1에 치우쳐 분포하게 되면 역전파의 기울기 값이 점점 작아지다가 사라집니다. 이것이 기울기 소실(gradient vanishing)이라 알려진 문제입니다.

그럼 이번에는 표준편차를 0.01로 바꿔 진행을 해보겠습니다.

실행 결과는 이와 같이 나옵니다. 이번에는 0.5 부근에 집중되어 있습니다. 0과 1에 치우치진 않았으니 기울기 소실 문제는 일어나지 않지만 활성화값들이 치우쳤다는 것은 표현력 관점에서는 큰 문제가 있는 것입니다. 다수의 뉴런이 거의 같은 값을 출력하고 있으니 뉴런을 여러 개 둔 의미가 없어집니다. 예를 들어 거의 같은 값을 출력하는 뉴런 100개는 뉴런 1개짜리와 별반 다를 게 없습니다.

이번에는 사비에르 글로로트(Xavier Glorot)와 요슈아 벤지오(Yoshua Bengio)의 논문에서 권장하는 가중치 초깃값인, 일명 Xavier 초깃값을 사용해보겠습니다.

Xavier 초깃값은 앞 계층의 노드가 n개라면 표준편차가 1/(n^(1/2))인 분포를 사용합니다.

실행결과는 이와 같이 나옵니다. 확실히 넓게 분포됨을 볼 수 있습니다. 데이터가 적당히 펴져 있으므로 sigmoid 함수의 표현력도 제한받지 않고 학습이 효율적으로 이뤄질 것으로 기대됩니다.

 

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

Xavier 초깃값은 활성화 함수가 선형인 것을 전제로 이끈 결과입니다. sigmoid 함수와 tanh 함수는 좌우 대칭이라 중앙 부근이 선형인 함수로 볼 수 있습니다. 그래서 Xavier 초깃값이 적당합니다. 반면 ReLU를 이용할 때는 ReLU에 특화된 초깃값을 이용하는 것이 좋습니다.

ReLU함수는 He초깃값을 이용하는 것이 좋습니다. He초깃값은 앞 계층의 노드가 n개일 때, 표준편차가 (2/n)^(1/2)인 정규분포를 사용합니다.

위 그래프를 보면 초깃값에 따른 분포를 그리고 있습니다. 첫 번째 표준편차가 0.01인 정규분포를 가중치로 초기화한 경우 아주 작은 값들을 보여줍니다. 이는 신경망에 작은 데이터가 흐른다는 것이고 역전파 때 가중치의 기울기 역시 작아진다는 뜻입니다.

Xavier 초깃값을 사용한 경우 층이 깊어지면서 치우침이 조금씩 커집니다. 실제로 층이 깊어지면 활성화값들의 치우침도 커지고, 학습할 때 기울기 소실 문제를 일으킵니다.

마지막으로 He초깃값을 사용한 경우 모든 층에서 균일하게 값들이 분포되어 있습니다. 층이 깊어져도 분포가 균일하게 유지되기에 역전파 때도 적절한 값이 나올 것으로 기대할 수 있습니다.

 

6.3. 배치 정규화

 ■ 배치 정규화 알고리즘

배치 정규화를 이용하면 학습 속도를 개선할 수 있고 초깃값에 크게 의존하지 않아도 되며 오버피팅을 억제합니다. 배치 정규화의 기본 아이디어는 각 층에서의 활성화 값이 적당히 분포되도록 조정하는 것입니다. 그래서 데이터 분포를 정규화하는 '배치 정규화 계층'을 신경망에 삽입합니다.

배치 정규화는 그 이름과 같이 학습 시 미니배치를 단위로 정규화합니다. 구체적으로는 데이터 분포가 평균이 0, 분산이 1이 되도록 정규화합니다. 수식은 다음과 같습니다.

여기에는 미니배치 B라는 m개의 입력 데이터의 집합에 대해 평균과 분산을 구합니다. 그리고 입력 데이터를 평균이 0, 분산이 1이 되게 정규화를 합니다. 이 처리를 활성화 함수의 앞에 삽입함으로써 데이터 분포가 덜 치우치게 할 수 있습니다.

 

 

 

 

 

 

 

그리고 배치 정규화 계층마다 이 정규화된 데이터에 고유한 확대와 이동 변환을 수행합니다. 수식으로는 다음과 같습니다.

이 식은 정규화된 데이터의 확대와 이동을 표현합니다. 이것이 배치 정규화의 알고리즘입니다. 이 알고리즘이 신경망에서 순전파 때 적용됩니다. 이를 계산 그래프로 그리면

 

 

이와 같습니다. 이러한 배치 정규화 계층을 사용해 MNIST 데이터셋 학습 진도를 확인해보겠습니다.

이 그래프를 보면 배치 정규화를 사용한 경우 학습을 빨리 진전시키는 것을 볼 수 있습니다.

 

6.4. 바른 학습을 위해

 ■ 오버피팅(overfitting)

기계학습에서는 오버피팅이 문제가 되는 일이 많습니다. 오버피팅은 신경망이 훈련 데이터에만 지나치게 적응되어 그 외의 데이터에는 제대로 대응하지 못하는 상태를 말합니다. 오버피팅은 주로 매개 변수가 많고 표현력이 높은 모델이거나 훈련 데이터가 적은 경우 일어납니다.

이번에는 MNIST 데이터셋의 훈련 데이터 중 300개만 사용하고 7층 네트워크를 사용해 네트워크 복잡성을 높이겠습니다. 각  층의 뉴런은 100개, 활성화 함수는 ReLU를 사용하겠습니다.

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

# 오버피팅을 재현하기 위해 학습 데이터 수를 줄임
x_train = x_train[:300]
t_train = t_train[:300]

먼저 mnist 데이터를 불러와 300장을 x_train과 t_train에 저장했습니다.

network = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100, 100, 100], output_size=10)
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

이어서 훈련을 수행하는 코드입니다. 에폭마다 모든 훈련 데이터와 모든 시험 데이터 각각에서 정확도를 산출합니다. train_acc_list와 test_acc_list에는 에폭 단위의 정확도를 저장합니다. 이 두 리스트를 그려보겠습니다.

훈련 데이터를 사용하여 측정한 정확도는 100 에폭을 지나는 무렵부터 거의 100%입니다. 그러나 시험 데이터에 대해서는 큰 차이를 보입니다. 이처럼 정확도가 크게 벌어지는 것은 훈련 데이터에만 적응해버린 결과입니다.

 

 ■ 가중치 감소

오버피팅 억제용으로 예로부터 많이 이용해온 방법 중 가중치 감소(weight decay)라는 것이 있습니다. 이는 학습 과정에서 큰 가중치에 대해서는 그에 상응하는 큰 페널티를 부과하여 오버피팅을 억제하는 방법입니다.

가중치 감소를 이용해 그래프를 그려보면

훈련 데이터에 대한 정확도와 시험 데이터에 대한 정확도에는 여전히 차이가 있지만, 가중치 감소를 이용하지 않은 결과와 비교하면 그 차이는 줄었습니다. 즉 오버피팅이 어느 정도 억제됐다는 소리입니다. 그리고 훈련 데이터가 100%에 도달하지 못한 점도 포인트입니다.

 

 ■ 드롭아웃(Dropout)

오버피팅을 억제하는 또 다른 방법입니다. 드롭아웃은 뉴런을 임의로 삭제하면서 학습하는 방법입니다. 훈련 때 은닉층의 뉴런을 무작위로 골라 삭제합니다. 그래프로 보면 아래와 같습니다.

삭제된 뉴런은 신호가 전달되지 않습니다. 훈련 때는 데이터를 흘릴 때마다 삭제할 뉴런을 무작위로 선택하고, 시험 때는 모든 뉴런에 신호를 전달합니다. 단, 시험 때는 각 뉴런의 출력에 훈련 때 삭제 안 한 비율을 곱하여 출력합니다.

 

 

 

 

 

 

 

 

 

이번에는 Dropout을 구현해보겠습니다.

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
            return x * self.mask
        else:
            return x * (1.0 - self.dropout_ratio)
        
    def backward(self, dout):
        return dout * self.mask

forward 메서드에서는 훈련 때 (train_flg = True)만 잘 계산해두면 시험 때는 단순히 데이터를 흘리기만 하면 됩니다. 삭제 안 한 비율은 곱하지 않아도 됩니다. 위 코드에서 핵심은 훈련 시에는 순전파 때마다 self.mask에 삭제할 뉴런을 False로 표시한 다는 것입니다. self.mask는 x와 형상이 같은 배열을 무작위로 생성하고, 그 값이 dropout_ratio보다 큰 원소만 True로 설정합니다. 순전파때 신호를 통과시키는 뉴런은 역전파 때도 신호를 그대로 통과시키고, 순전파 때 통과시키지 않은 뉴런은 역전파 때도 신호를 차단합니다.

그림과 같이 드롭아웃을 적용하니 훈련 데이터와 시험 데이터에 대한 정확도 차이가 줄었습니다. 또, 훈련 데이터에 대한 정확도가 100%에 도달하지도 않습니다. 이렇게 드롭아웃을 이용하면 표현력을 높이면서도 오버피팅을 억제할 수 있습니다.

'Deep Learning(강의 및 책) > 밑바닥부터 시작하는 딥러닝' 카테고리의 다른 글

8. 딥러닝  (0) 2022.02.18
7. 합성곱 신경망(CNN)  (0) 2022.02.18
5. 오차역전파법  (0) 2022.02.14
4. 신경망 학습  (0) 2022.02.07
3. 신경망  (0) 2022.02.07