ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [NLP] 오버피팅을 막아보자: 가중치 감소, 드롭아웃
    ML/NLP 2020. 4. 24. 14:07

     

    기계학습에서는 오버피팅이 문제가 되는 일이 많다.

     

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

     

    기계학습은 범용 성능을 지향하므로 훈련 데이터에는 포함되지 않는, 아직 보지 못한 데이터가 주어져도 바르게 식별해내는 모델이 바람직하다.

     

     

     

     

    오버피팅


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

    • 매개변수가 많고 표현력이 높은 모델
    • 훈련 데이터가 적음

    먼저 이 두 요건을 일부러 충족시켜 오버피팅을 일으켜보겠다.

     

    60,000개의 MNIST 데이터셋의 훈련 데이터 중 300개만 사용하고, 7층 네트워크를 사용해 네트워크의 복잡성을 높이겠다.

     

    각 층의 뉴런은 100개, 활성화 함수는 ReLU를 사용한다.

    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_lambda = 0
    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) # 학습률이 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'}
    x = 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(0, 1.0)
    plt.legend(loc='lower right')
    plt.show()

    Out:

    그림 1

    훈련 데이터를 사용하여 측정한 정확도는 100 에폭을 지나는 무렵부터 거의 100%이다.

     

    그러나 시험 데이터에 대해서는 큰 차이를 보인다.

     

    이처럼 정확도가 크게 벌어지는 것은 훈련 데이터에만 적응(fitting)해버린 결과이다.

     

    훈련 때 사용하지 않은 범용 데이터(시험 데이터)에는 제대로 대응하지 못하는 것을 이 그래프에서 확인할 수 있다.

     

     

     

     

     

    가중치 감소


    오버피팅을 억제할 수 있는 방법들 중 가중치 감소(weight decay)가 있다.

     

    이는 학습 과정에서 큰 가중치에 대해서는 그에 상응히는 큰 페널티를 부과하여 오버피팅을 억제하는 것이다.

     

    원래 오버피팅은 가중치 매개변수의 값이 커서 발생하는 경우가 많기 때문에 효과적이다.

     

     

     

    신경망 학습의 목적은 손실 함수의 값을 줄이는 것이다. 

     

    이때, 예를 들어 가중치의 제곱 norm(L2 norm)을 손실 함수에 더한다.

     

    그러면 가중치가 커지는 것을 억제할 수 있다.

     

    가중치를 W라 하면 L2 norm에 따른 가중치 감소는 1/2λW^2이 되고, 

     

    1/2λW^2을 손실 함수에 더한다.

     

    여기서 λ는 정규화의 세기를 조절하는 하이퍼파라미터이다.

     

    λ를 크게 설정할수록 큰 가중치에 대한 페널티가 커진다.

     

     

     

    가중치 감소는 모든 가중치 각각의 손실 함수에 1/2λW^2을 더한다. 

     

    따라서 가중치의 기울기를 구하는 계산에서는 그동안의 오차역전파법에 따른 결과에 정규화 항을 미분한 λW를 더한다.

     

     

     

    위에서 방금 수행한 실험에서 λ = 0.1로 가중치 감소를 적용해보자.

     

    위에서 봤던 코드에서 아래 부분만 바꾸고 실행하면 된다.

    # weight_decay_lambda = 0.1
    weight_decay_lambda = 0

    결과는 [그림 2]와 같다.

     

    그림 2

    [그림 2]와 같이 훈련 데이터에 대한 정확도와 시험 데이터에 대한 정확도에는 여전히 차이가 있지만, 가중치 감소를 이용하지 않은 [그림 1]과 비교하면 그 차이가 줄었다.

     

    다시 말해 오버피팅이 억제됐다는 소리다.

     

    또한 앞과 달리 훈련 데이터에 대한 정확도가 100%(1.0)에 도달하지 못한 점도 주목해야 하겠다.

     

     

     

     

     

    드롭아웃


    신경망 모델이 복잡해지면 가중치 감소만으로는 대응하기 어려워진다.

     

    이럴 때는 흔히 드롭아웃(Dropout)이라는 기법을 사용한다.

     

     

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

     

    훈련 때 은닉층의 뉴런을 무작위로 골라 삭제한다.

     

    삭제된 뉴런은 [그림 3]과 같이 신호를 전달하지 않게 된다.

     

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

     

    단, 시험 때는 각 뉴런의 출력에 훈련 때 삭제 안 한 비율을 곱하여 출력하는데, 보통 안 곱하기도 한다.

     

     

     

    그림 3

     

     

     

    이제 드롭아웃을 구현해보자.

    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

    훈련 시에는 순전파 때마다 self.mask에 삭제할 뉴런을 False로 표시한다.

     

    self.mask는 x와 형상이 같은 배열을 무작위로 생성하고,

     

    그 값이 dropout_ratio보다 큰 원소만 True로 설정한다.

     

    역전파 때의 동작은 ReLU와 같다.

     

    즉, 순전파 때 신호를 통과시키는 뉴런은 역전파 때도 신호를 그대로 통과시키고, 순전파 때 통과시키지 않은 뉴런은 역전파 때도 신호를 차단한다.

     

     

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

     

    앞의 실험과 마찬가지로 7층 네트워크 (각 층의 뉴런 수는 100개, 활성화 함수는 ReLU)를 써서 진행했다.

     

    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.15
    # ====================================================
    
    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)
    trainer.train()
    
    train_acc_list, test_acc_list = trainer.train_acc_list, trainer.test_acc_list
    
    # 그래프 그리기==========
    markers = {'train': 'o', 'test': 's'}
    x = 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(0, 1.0)
    plt.legend(loc='lower right')
    plt.show()

    Out:

    그림 4

    참고로 Trainer라는 클래스는 지금까지 해 온 것과 같은 네트워크 학습을 대신 해준다.

     

    결과는 [그림 4]와 같다.

     

     

    위를 use_dropout = False로 고치면 결과는 [그림 5]와 같은 결과가 나온다.

     

    그림 5

     

    드롭아웃을 적용하니 훈련 데이터와 시험 데이터에 대한 정확도 차이가 줄었다.

     

    또한, 훈련 데이터에 대한 정확도가 100%에 도달하지도 않게 되었다.

     

    이처럼 드롭아웃을 이용하면 표현력을 높이면서도 오버피팅을 억제할 수 있다.

     

     


    기계학습에서는 앙상블 학습(ensemble learning)을 이용한다.
    앙상블 학습은 개별적으로 학습시킨 여러 모델의 출력을 평균 내어 추론하는 방식이다.

    신경망의 맥락에서 얘기하면, 같거나 비슷한 구조의 네트워크를 5개 중비하여 따로따로 학습시키고,
    시험 때는 그 5개의 출력을 평균 내어 답하는 것이다.

    앙상블 학습은 드롭아웃과 밀접하다.
    드롭아웃이 학습 때 뉴런을 무작위로 삭제하는 행위를 매번 다른 모델을 학습시키는 것으로 해석할 수 있기 때문이다.

    즉, 드롭아웃은 앙상블 학습과 같은 효과를 대략 하나의 네트워크로 구현했다고 생각할 수 있다.

    댓글

dokylee's Tech Blog