처음부터 하는 딥러닝

[딥러닝 기초] Overfitting과 모델 규제(regularization)

빈이름 2023. 6. 14. 22:12
1. Overfitting (과적합)
2. Regularization (모델 규제)
    2-1. L2 regularization
    2-2. L1 regularization
    2-3. Regularization이 효과 있는 이유
    2-4. Dropout
3. EarlyStopping (학습 조기 종료)
4. Data Augmentation
5. 실험
    5-1. Regularize 실험
    5-2. Data Augmentation 실험
6. 마무리

1. Overfitting (과적합)

인공지능 모델링은 모델을 주어진 데이터들에 맞추는(fit) 과정이라고 볼 수 있습니다. 잘 학습된 모델은 주어진 데이터셋에 있는 통계적인 경향성을 잘 반영한 모델이라고 볼 수 있을 것입니다. Overfitting(과적합)은 모델이 훈련 데이터셋의 경향성을 너무 잘 반영해서 생기는 문제입니다. 인공지능 모델이 훈련 데이터셋을 잘 표현하는게 뭐가 문제냐 라고 생각할 수도 있지만 문제는 훈련에 사용된 데이터셋이 실제 데이터들을 완벽히 반영할 수 없기 때문에 문제가 될 수 있습니다.

 

극단적인 예시로, 고양이와 개 사진을 구분하는 인공지능 모델을 만들기 위해 데이터를 수집하는 과정에서 개 사진 중 털이 갈색인 개의 사진이 90% 이상 있었다고 생각해 봅시다. 이를 모델이 학습할 경우, 모델은 '털이 갈색이면 개구나'라고 생각하게 될 수도 있습니다. 따라서 갈색 고양이를 개라고 판별하거나, 노란 털을 가진 개를 고양이라고 판별하는 등의 부작용이 생길 수 있습니다.

이런 경우, 모델이 훈련 데이터에 overfitting되었다고 볼 수 있습니다. 훈련 셋에 갈색 털을 가진 개 사진이 많았지만 실제로 개의 털 색은 다양합니다. 훈련셋이 실제 데이터들의 분포를 제대로 반영하지 못한 것이죠. 데이터셋을 구축하는 과정에서 다양한 색의 개 사진을 모두 포함할 수 있었으면 좋았겠지만, 현실적으로 이런 것들을 모두 반영하기 매우 어렵습니다.

 

그렇기 때문에 모델의 overfitting 상태를 확인하기 위해 테스트셋을 따로 구축하는 것입니다. 테스트셋은 실제로 모델이 절대 틀려서는 안되는, 혹은 최대한 실제 데이터들을 반영하도록 구성하여 모델의 '실제 성능'을 체크하기 위한 역할을 합니다. 모델이 아무리 훈련 데이터셋에서 100% 정확도를 달성하더라도, 실제 데이터(test셋)에서 정확도가 50%밖에 안 나온다면 좋은 모델이라고 볼 수 없겠죠.

 

모델이 훈련 데이터에 과적합되는 이유로 3가지 정도가 있고, 이를 완화할 수 있는 방법이 존재합니다.

 

1. 모델이 너무 복잡함.

 

모델이 복잡하다는 것과 overfitting과 무슨 관계인지 생각하기 어려울 수 있습니다. 알기 쉽게 overfitting 그래프를 그려봤습니다.

위 그래프를 봤을 때, 모델A와 모델B 중에서 test data에 더 좋은 성능을 보일 모델은 A일 겁니다. 모델 B는 그야말로 훈련 데이터에 과적합되었다 라고 볼 수 있겠죠.

모델 A와 모델 B를 비교해보면 모델 B가 좀 더 복잡한 형태를 가질 것입니다. 모델 A는 단순한 직선 형태이기 때문에 학습을 계속해도 훈련 데이터에 과적합되긴 어려울 겁니다.

따라서 overfitting을 해결하기 위한 방법 중 하나로 모델을 '단순화'하는 방법도 사용할 수 있습니다. 모델을 단순화하기 위해서 모델을 의도적으로 '규제(regularize)' 하는 방식을 많이 사용합니다.

 

2. 훈련 데이터에 너무 오래 학습됨.

 

인공지능의 학습과정은 훈련 데이터에 대한 loss가 최소가 되도록 진행됩니다. 그렇기 때문에 학습을 오래할수록 모델은 자연스럽게 훈련셋에 overfitting될 수 밖에 없습니다.

그렇기 때문에 모델이 훈련셋에 overfitting되기 전에 학습을 종료하는 방법을 사용해 볼 수 있습니다.

 

3. 데이터 부족

 

가장 큰 원인으로, 학습에 사용되는 데이터의 양이 부족해서 모델이 실제 데이터 전체를 표현하기 위한 정보가 부족할 수가 있습니다. 그렇기 때문에 데이터를 더 많이 확보하면 더 좋아지는 경우가 많습니다. 실제로 현재 성능 좋은 모델들의 최우선 조건은 '최대한 많은 데이터'인 경우가 많죠.

그러나 실제로 문제들을 인공지능으로 해결하려고 하다보면 데이터를 더 수집하기 어려운 경우가 많습니다. 이런 경우엔 data augmentation(데이터 증강) 방법을 생각해 볼 수 있습니다. Data augmentation은 이미 수집된 데이터를 이용해 더 많은 데이터를 만드는 방법입니다.

 

물론 overfitting을 해결하는 가장 좋은 방식은 더 좋은 데이터를 더 많이 수집하는 것이지만 현실적으로 힘들고, overfitting 상황에 대응할 방법들에 대해서 하나씩 알아봅시다.

2. Regularization (모델 규제)

Regularization은 모델을 의도적으로 '단순화'하는 것입니다. 모델을 단순화하는 방법은 직접 레이어 수를 줄이거나 차원 수를 줄일 수도 있지만, 아래와 같은 방법들을 사용할 수도 있습니다.

2-1. L2 Regularization

L2 Regularization은 loss에 모델의 weight들의 l2 norm을 추가하는 방식입니다. 가장 흔히 사용되는 방법으로 weight decay라고도 합니다.

$$Loss=L(w)+{\alpha\over2}||W||^2_2=L(W)+{\alpha\over2}\Sigma_i\Sigma_jW^2_{ij}$$

Loss에 weight들의 제곱의 총합을 더하며, $\alpha$를 통해 모델의 규제 정도를 조절할 수 있습니다.

L2 regularization을 적용했을 때 모델의 역전파는 다음과 같이 이뤄질 겁니다.

$$w_{\text{new}}=w-\epsilon{d\over{dw}}(L(w)+{\alpha\over2}w^2)=(1-\alpha)w-\epsilon\nabla L(w)$$

$\epsilon$은 learning rate를 나타냅니다. l2 norm을 추가한 loss를 미분하면 위와 같은 수식이 만들어지게 됩니다. 이 역전파 수식의 앞 항($(1-\alpha)w$)으로 인해 $\alpha$가 커질수록 weight 값들이 작아지게 됩니다.

 

PyTorch에선 아래와 같이 training loop에서 직접 구현할 수 있습니다.

alpha = 1e-5

for inputs, labels in dataloader:
    output = model(inputs)
    
    loss = criterion(output, labels)
    l2_norm = sum(
        p.pow(2.0).sum() for p in model.parameters()
    )
    loss = loss + (alpha/2) * l2_norm
    
    optim.zero_grad()
    loss.backward()
    optim.step()

 

또는 PyTorch에서 제공하는 optimizer의 weight_decay 옵션을 이용해서 구현할 수도 있습니다.

optim = Adam(model.parameters(), lr=1e-3, weight_decay=1e-5)

2-2. L1 Regularization

L1 Regularization은 loss에 l1 norm을 추가하는 방법입니다.

$$Loss=L(w)+\alpha||W||_1=L(w)+\alpha\Sigma_i\Sigma_j|w_{ij}|$$

Loss에 weight들의 절댓값의 총합을 더하며, 역시 $\alpha$를 통해 모델의 규제 정도를 조절할 수 있습니다.

L1 regularization을 적용했을 때 모델 역전파는 아래와 같이 이뤄질 겁니다.

$$w_{new}=w-\epsilon{d\over{dw}}(L(w)+\alpha|w|)=w-\epsilon(\nabla L(w)+\alpha)$$

L1 regularization 역시 $\alpha$값이 커질수록 weight가 줄어드는 것을 알 수 있습니다.

 

L1 regularization은 training loop에서 직접 구현해야 합니다.

alpha = 1e-5

for inputs, labels in dataloader:
    output = model(inputs)
    
    loss = criterion(output, labels)
    l1_norm = sum(
        p.abs().sum() for p in model.parameters()
    )
    loss = loss + alpha * l1_norm
    
    optim.zero_grad()
    loss.backward()
    optim.step()

2-3. Regularization이 효과 있는 이유

L1, L2 regularization 모두 loss에 norm을 추가함으로써 weight 값을 줄이는 역할을 한다는 것을 알 수 있습니다.

Weight 값들을 줄인다면, 모델에 영향을 덜 주던 작은 weight들은 0에 가까운 값을 갖게 될 것입니다. 즉, 모델이 해당 weight들을 사용하지 않는 것과 같은 효과를 갖게 됩니다. 추론에 사용되는 weight가 줄어듦으로써 모델이 '단순화'하는 효과를 갖게 되는 것입니다.

2-4. Dropout

Dropout은 대놓고 모델 레이어의 일부 뉴런들을 사용하지 않는 방법입니다. Dropout probability를 설정하여 레이어의 뉴런 중 몇 %의 뉴런들을 랜덤으로 사용하지 않을지를 결정합니다.

Dropout 설정

Dropout 역시 모델을 단순화하는 효과를 가집니다. 그러나 dropout은 L2, L1 regularization과 다르게 매 정전파(forward)마다 랜덤한 뉴런들을 사용하지 않습니다. 따라서 같은 데이터가 입력되더라도 매번 출력 결과가 달라지게 됩니다. 즉, dropout은 regularize의 역할을 수행하면서도, 일종의 data augmentation과 같은 역할도 할 수 있습니다.

Dropout은 같은 데이터에도 매번 출력 결과가 달라지기 때문에 모델 훈련에만 사용하고, 모델 test 시에는 사용하지 않아야 합니다.

 

Dropout은 nn.Module로 구현되어 있어 아래와 같이 레이어 형태로 사용할 수 있습니다. p를 조절하여 dropout probability를 결정합니다.

model = nn.Sequential(
    nn.Dense(256, 128),
    nn.Dropout(p=0.2),
    nn.ReLU(),
    nn.Dense(128, 3),
)

model.train()을 통해 dropout을 활성화할 수 있고, model.eval()을 통해 dropout을 비활성화할 수 있습니다.

3. Early stopping

Early stopping은 모델이 overfitting 되기 전에 학습을 종료하는 방법입니다. 보통 테스트셋에 대한 모델의 loss나 정확도와 같은 평가 지표를 기준으로 overfitting 여부를 판별합니다.

모델이 테스트셋에 대한 loss가 더 이상 줄어들지 않는다면 학습을 종료하는 식으로 사용합니다.

PyTorch에선 직접 테스트셋의 loss를 확인하고 if 문으로 종료할 수 있도록 직접 구현해야 합니다.

eval_losses = []

for epoch in range(epochs):
    model.train()
    for inputs, labels in train_loader:
        output = model(inputs)
        ...
        
    model.eval()
    eval_loss = []
    for inputs, labels in test_loader:
        with torch.no_grad():
            output = model(inputs)
        loss = criterion(output, labels)
        eval_loss.append(loss.cpu().item())
        
    eval_loss = sum(eval_loss)/len(eval_loss)
    eval_losses.append(eval_loss)
    
    # eval loss를 확인했을 때, 이전 eval loss보다 크다면 학습을 종료.
    if (len(eval_losses) > 2) and (eval_losses[-2] < eval_losses[-1}):
        break

Loss는 오락가락하는 경우가 많기 때문에 일정 step동안 유예기간을 두고 해당 step동안에도 loss가 줄어들지 않으면 학습을 종료하는 방식으로도 많이 사용합니다. 위 코드에서 유예 step 변수를 추가해서 구현하면 되겠죠.

4. Data Augmentation

Data Augmentation은 이미 수집한 데이터를 이용해서 더 많은 데이터를 만들어내는 방법입니다.

가장 쉽게 적용할 수 있는 부분은 비전 분야입니다. 이미지를 뒤집거나, 회전하거나, 일부를 자르거나 하는 방식으로 여러가지 변형을 주기 쉽기 때문이죠.

Data augmentation 예시.

이미지를 조금 돌리고 뒤집을 뿐인데 모델 성능에 얼마나 영향이 있겠나 싶을수도 있지만 실제로 사용해보면 성능이 꽤 많이 향상됩니다.

사람 입장에선 그냥 별 차이 없는 그림일 수도 있지만, 이미지를 픽셀 단위로 보는 모델 입장에선 이미지에 조금의 변형만 가해지게 되도 상당히 다른 input처럼 느껴집니다. 이미지가 좌우반전되기만 하더라도 input의 픽셀 값들이 크게 변화하겠죠. 그렇기 때문에 data augmentation을 통해 모델은 이미지의 좀 더 '일반적인' 특징을 익힐 수 있습니다. "왼쪽을 보고 있는 자동차나 오른쪽을 보고 있는 자동차나 똑같은 자동차다"라는 것을 저희는 직관적으로 알 수 있지만, 모델에겐 어려운 정보일 수도 있습니다.

주의할 점은 augmentation을 통해 원래 데이터의 본질을 해치지 않아야 한다는 것입니다. 예를 들어 만약 MNIST 데이터셋에 augmentation을 수행한다면 상하반전은 사용하지 않아야 할겁니다. 6이 9가 될 수도 있으니까요...

그렇기 때문에 이미지 외의 다른 데이터들에 대해선 augmentation을 적용하기 많이 난해한 경우가 많습니다. 텍스트의 경우 글자를 조금만 수정하거나 추가해도 그 뜻이 완전히 달라질 수 있으니까요... 효과 좋은 augmentation 방법에 대한 고민도 좋은 모델을 얻기 위해 필요한 부분 중 하나입니다.

5. 실험

소개한 방법들을 직접 실험해 보고 성능에 어떤 영향을 미치는지 직접 확인해 보겠습니다.

이번 실험에 사용할 데이터셋은 CIFAR-10(https://www.cs.toronto.edu/~kriz/cifar.html) 입니다.

CIFAR-10 데이터 예시

CIFAR-10은 32*32 크기의 칼라 이미지 6만장으로 구성되어 있습니다. 총 10개의 클래스가 존재하며, 입력 받은 이미지를 알맞은 클래스로 분류해야 하는 task입니다.

Overfitting 효과를 더 크게 얻기 위해, 6만 개 중 2000개의 이미지만 학습에 사용해볼 것입니다.

import torchvision.datasets as datasets
from sklearn.model_selection import train_test_split
from collections import Counter


cifar_train = datasets.CIFAR10(root='cifar_data/',
                          train=True,
                          download=True)

cifar_train, cifar_valid = train_test_split(
    cifar_train, train_size=2000, test_size=500, random_state=16
)

코드는 아래 colab 링크에서 확인할 수 있습니다.

https://colab.research.google.com/drive/1_S9749ezENfBE3mU2AuxIhxASRrPBIf7?usp=sharing

 

Overfitting.ipynb

Colaboratory notebook

colab.research.google.com

5-1. Regularize 실험

모델 구조는 다음과 같이 구성했습니다.

class CNN(nn.Module):
    def __init__(self, dropout_p=0.2):
        super(CNN, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=(3, 3), stride=1, padding=1)
        self.conv2 = nn.Conv2d(in_channels=64, out_channels=64, kernel_size=(3, 3), stride=1, padding=1)
        self.pool1 = nn.MaxPool2d(kernel_size=(2,2), stride=2)

        self.conv3 = nn.Conv2d(in_channels=64, out_channels=32, kernel_size=(3, 3), stride=1, padding=1)
        self.conv4 = nn.Conv2d(in_channels=32, out_channels=32, kernel_size=(3, 3), stride=1, padding=1)
        self.pool2 = nn.MaxPool2d(kernel_size=(2,2), stride=2)

        self.dense1 = nn.Linear(8*8*32, 1024)
        self.dense2 = nn.Linear(1024, 512)
        self.dense3 = nn.Linear(512, 10)
        self.relu = nn.ReLU()
        self.dropout = nn.Dropout(dropout_p)

    def forward(self, inputs):
        batch_size = inputs.shape[0]
        x = self.conv1(inputs)
        x = self.relu(x)
        x = self.conv2(x)
        x = self.relu(x)
        x = self.pool1(x)
        x = self.dropout(x)

        x = self.conv3(x)
        x = self.relu(x)
        x = self.conv4(x)
        x = self.relu(x)
        x = self.pool2(x)
        x = self.dropout(x)

        x = x.view(batch_size, -1)
        x = self.dense1(x)
        x = self.relu(x)
        x = self.dropout(x)

        x = self.dense2(x)
        x = self.relu(x)
        x = self.dropout(x)

        outputs = self.dense3(x)

        return outputs

아무것도 적용하지 않은 기본 모델과, L1, L2 regularize, Dropout을 각각 적용한 모델들의 loss와 accuracy를 비교해 보겠습니다. (모델 구조나 수치 변화에 따라 결과가 달라질 수 있습니다.)

Regularize 기법을 적용했을 때 성능 변화

기본 모델의 loss(맨 왼쪽 그래프)를 보면 eval loss가 7에포크부터 크게 증가하는 모습을 보이고 있습니다. Overfitting이 발생한다는 증거죠. L1, L2, Dropout을 적용한 모델들의 그래프를 보면 overfitting이 많이 완화된 모습을 확인할 수 있습니다.

L1 regularize 모델의 경우 아직 eval loss가 줄어들고 있고 다른 모델들에 비해 loss가 아직 큰 걸 봤을 때, 아직 모델이 underfitting(overfitting의 반대로 학습이 덜 된 상태) 된 것으로 보입니다. 이 경우 추가 학습을 통해 overfitting이 발생할 때까지 학습을 더 진행하면 더 좋은 성능을 얻을 수 있습니다. 지금은 비교를 위해 모두 같은 에포크로 실험 했습니다.

L2 regularize 모델이나 Dropout 모델을 보면 똑같이 overfitting이 발생하고는 있지만, 그 정도가 많이 완화된 것을 확인할 수 있습니다.

결과적으로 최종 accuracy도 모두 비교해 보면(맨 오른쪽 그래프), L2 regularize와 dropout을 적용한 모델들이 기본 모델보다 더 높은 accuracy를 얻은 것을 확인할 수 있습니다. (L1 regularize 모델은 아직 학습이 덜 되어 아직 기본 모델보다 accuracy가 떨어집니다.)

5-2. Data Augmentation 실험

다음으로 data augmentation 효과를 실험해 보겠습니다. 이미지 augmentation은 torchvision의 transforms 모듈을 이용해 쉽게 구현할 수 있습니다.

from torchvision import transforms


tf = nn.Sequential(
    transforms.RandomHorizontalFlip(0.5),
    transforms.RandomRotation(10),
)

transformed_image = tf(image)

PyTorch 홈페이지에서 더 많고 자세한 옵션들을 확인할 수 있습니다. 이번 실험에선 간단히 좌우반전과 이미지 회전만 적용하여 augmentation을 수행했습니다.

Data augmentation 적용한 모델과 적용하지 않은 모델의 성능 비교

Data augmentation을 적용하지 않고 학습한 모델(첫번째 그래프)의 경우, overfitting이 발생하는 것을 확인할 수 있습니다. 그러나 data augmentation을 적용한 모델(두번째 그래프)은 eval loss가 더 많이 줄어든 것을 확인할 수 있습니다. (게다가 좀 더 학습해도 될 것 같습니다.)

테스트셋에 대한 정확도(맨 오른쪽 그래프) 역시 augmentation을 적용한 모델이 더 높은 것을 확인할 수 있습니다. 역시 데이터의 힘일까요. Regularize보다 overfitting에 더 큰 효과를 보이는 것 같습니다.

6. 마무리

이렇게 overfitting 문제와 해결방법에 대해서 알아봤습니다.

Overfitting은 모델이 훈련 데이터에 과하게 맞춰져 오히려 실제 데이터에 성능이 잘 나오지 않는 문제를 말합니다.

이는 3가지 문제가 있을 수 있습니다.

  • 훈련 데이터 부족 : 데이터가 부족하면 모델이 훈련셋에 과적합되기 쉽습니다. 이 경우 data augmentation을 통해 데이터 수를 늘리는 식으로 완화해 볼 수 있습니다.
  • 모델이 너무 복잡함 : 모델이 너무 복잡하고 성능이 뛰어나서 되려 훈련셋에 overfitting 되기 쉬울 수도 있습니다. 이 경우 다양한 방식의 규제(L2, L1 regularize, Dropout)를 통해 모델을 단순화하여 완화할 수 있습니다.
  • 훈련 데이터에 너무 오래 학습됨 : 학습이 길어질수록 훈련셋에 overfitting될 수밖에 없습니다. 따라서 모델이 overfitting 되기 전에 학습을 종료할 필요가 있습니다.

이런 다양한 방법들을 사용해 모델의 성능을 최대한 끌어올려 볼 수 있습니다. 이와 더불어 테스트셋의 의미와 문제를 해결하기 위해 필요한 데이터셋이 무엇인가에 대해 한번씩 고민해 보는 것도 좋을 것 같습니다.