본문 바로가기

딥러닝/딥러닝 기초 개념

과적합과 정규화 (Overfitting, Dropout, BatchNorm)

들어가며

딥러닝 모델을 학습시키다 보면 훈련 데이터에서는 성능이 좋은데 실제 데이터에서는 성능이 떨어지는 현상을 자주 만난다. 이게 바로 과적합이다. 웹 개발로 비유하면 특정 브라우저에서만 완벽하게 동작하는 코드를 짠 것과 비슷하다. 범용적으로 동작해야 하는데 특정 케이스에만 최적화된 것이다. 과적합을 이해하고 해결하는 방법을 정리해둔다.


과적합 (Overfitting)이란?

모델이 훈련 데이터를 너무 잘 외워서 새로운 데이터에 일반화하지 못하는 현상이다.

과적합 신호:
- 훈련 손실은 계속 감소
- 검증 손실은 어느 순간부터 증가
- 훈련 정확도 >> 검증 정확도

과적합 vs 과소적합

                    훈련 손실    검증 손실    상태
과소적합 (Underfitting): 높음         높음         모델이 너무 단순
적절한 적합 (Good fit):  낮음         낮음         이상적
과적합 (Overfitting):   매우 낮음     높음         모델이 너무 복잡

과적합 원인

1. 모델이 너무 복잡 (파라미터 수가 데이터보다 훨씬 많음)
2. 훈련 데이터가 너무 적음
3. 훈련을 너무 오래 함 (에폭 수가 많음)
4. 데이터 다양성 부족 (특정 패턴만 있는 데이터)

해결 방법 1: 데이터 증강 (Data Augmentation)

훈련 데이터를 인위적으로 늘리는 방법이다. 이미지 분류에서 가장 효과적이다.

import torchvision.transforms as transforms

# 이미지 데이터 증강
train_transform = transforms.Compose([
    transforms.RandomHorizontalFlip(p=0.5),     # 50% 확률로 좌우 반전
    transforms.RandomRotation(degrees=15),       # ±15도 회전
    transforms.RandomCrop(224, padding=4),       # 랜덤 크롭
    transforms.ColorJitter(
        brightness=0.2, contrast=0.2, saturation=0.2
    ),                                           # 밝기/대비/채도 조정
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                         std=[0.229, 0.224, 0.225])
])

# 테스트 시에는 증강 없이
test_transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                         std=[0.229, 0.224, 0.225])
])

해결 방법 2: 드롭아웃 (Dropout)

학습 중에 랜덤하게 일부 뉴런을 비활성화하는 방법이다.

def dropout(x, p=0.5, training=True):
    if not training:
        return x  # 추론 시에는 드롭아웃 적용 안 함

    # p 확률로 뉴런을 0으로 만들고 1/(1-p)로 스케일링
    mask = np.random.binomial(1, 1 - p, size=x.shape)
    return x * mask / (1 - p)

x = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
print(f"원본:           {x}")
print(f"드롭아웃 p=0.5: {dropout(x, p=0.5).round(2)}")
print(f"추론 시:        {dropout(x, p=0.5, training=False)}")

PyTorch에서 드롭아웃

import torch.nn as nn

class ModelWithDropout(nn.Module):
    def __init__(self, dropout_rate=0.5):
        super().__init__()
        self.network = nn.Sequential(
            nn.Linear(784, 512),
            nn.ReLU(),
            nn.Dropout(p=dropout_rate),    # 드롭아웃 추가
            nn.Linear(512, 256),
            nn.ReLU(),
            nn.Dropout(p=dropout_rate),    # 드롭아웃 추가
            nn.Linear(256, 10)
        )

    def forward(self, x):
        return self.network(x)

model = ModelWithDropout(dropout_rate=0.5)

model.train()   # 학습 모드: 드롭아웃 활성화
model.eval()    # 추론 모드: 드롭아웃 비활성화 (필수!)
드롭아웃 비율 설정:
- FC 레이어:       0.5
- Conv 레이어:     0.1 ~ 0.3
- Transformer:     0.1
- 너무 크면 과소적합 위험

드롭아웃으로 학습하면 매번 다른 뉴런 조합이 학습되어 마치 여러 모델의 앙상블 효과를 낸다. 각 뉴런이 독립적으로 유용한 특성을 학습하게 된다.


해결 방법 3: L1/L2 정규화 (Weight Decay)

손실 함수에 가중치 크기에 대한 패널티를 추가한다.

# L2 정규화: Loss_total = Loss + λ * Σ w²
# L1 정규화: Loss_total = Loss + λ * Σ |w|

# PyTorch에서 L2 정규화: weight_decay 파라미터
optimizer = torch.optim.Adam(
    model.parameters(),
    lr=0.001,
    weight_decay=1e-4   # L2 정규화 강도
)

optimizer_sgd = torch.optim.SGD(
    model.parameters(),
    lr=0.01,
    momentum=0.9,
    weight_decay=1e-4
)
L1 정규화:
- 일부 가중치를 정확히 0으로 만듦 (희소성)
- 중요하지 않은 특성 제거에 효과적

L2 정규화:
- 가중치를 0에 가깝게 만들지만 정확히 0은 안 됨
- 딥러닝에서 더 일반적으로 사용

weight_decay 일반적인 값:
- 0.01   (강한 정규화)
- 0.001  (중간)
- 0.0001 (약한 정규화, 자주 쓰임)

해결 방법 4: 배치 정규화 (Batch Normalization)

각 레이어의 입력을 정규화해서 학습을 안정화하는 방법이다.

def batch_norm_manual(x, gamma, beta, epsilon=1e-5):
    mean = x.mean(axis=0)
    var = x.var(axis=0)
    x_norm = (x - mean) / np.sqrt(var + epsilon)
    return gamma * x_norm + beta   # 스케일 & 이동 (학습 가능)

x = np.random.randn(4, 3) * 5 + 10
gamma = np.ones(3)
beta = np.zeros(3)

out = batch_norm_manual(x, gamma, beta)
print(f"입력 평균: {x.mean(axis=0).round(2)}")   # ≈ [10, 10, 10]
print(f"출력 평균: {out.mean(axis=0).round(4)}")  # ≈ [0, 0, 0]
print(f"출력 분산: {out.var(axis=0).round(4)}")   # ≈ [1, 1, 1]

PyTorch에서 배치 정규화

# FC 레이어
model_bn = nn.Sequential(
    nn.Linear(784, 512),
    nn.BatchNorm1d(512),    # 배치 정규화
    nn.ReLU(),
    nn.Linear(512, 256),
    nn.BatchNorm1d(256),
    nn.ReLU(),
    nn.Linear(256, 10)
)

# CNN 레이어
cnn_model = nn.Sequential(
    nn.Conv2d(3, 64, kernel_size=3, padding=1),
    nn.BatchNorm2d(64),    # 채널별 배치 정규화
    nn.ReLU(),
)
배치 정규화 효과:
1. 각 레이어 입력을 정규화 → 학습 안정
2. 더 큰 학습률 사용 가능 → 빠른 학습
3. 가중치 초기화에 덜 민감
4. 학습 속도 2~10배 향상

주의사항:
- 배치 크기가 너무 작으면 (1~4) 효과 없음
- 작은 배치에는 LayerNorm, GroupNorm 사용
- model.train() / model.eval() 필수

해결 방법 5: 조기 종료 (Early Stopping)

검증 손실이 더 이상 개선되지 않으면 학습을 중단한다.

class EarlyStopping:
    def __init__(self, patience=10, min_delta=0.001):
        self.patience = patience
        self.min_delta = min_delta
        self.counter = 0
        self.best_loss = float('inf')
        self.early_stop = False
        self.best_model_state = None

    def __call__(self, val_loss, model):
        if val_loss < self.best_loss - self.min_delta:
            self.best_loss = val_loss
            self.counter = 0
            self.best_model_state = {
                k: v.clone() for k, v in model.state_dict().items()
            }
        else:
            self.counter += 1
            if self.counter >= self.patience:
                self.early_stop = True

# 학습 루프에서 사용
early_stopping = EarlyStopping(patience=10)

for epoch in range(200):
    train_loss = train_one_epoch(model, optimizer, criterion)
    val_loss = validate(model, criterion)

    early_stopping(val_loss, model)

    if early_stopping.early_stop:
        print(f"Early stopping at epoch {epoch}")
        model.load_state_dict(early_stopping.best_model_state)
        break

해결 방법 6: Layer Normalization

배치 정규화의 대안이다. Transformer에서 주로 사용된다.

# BatchNorm: 배치 방향으로 정규화 (각 특성의 배치 평균/분산)
# LayerNorm: 특성 방향으로 정규화 (각 샘플의 전체 특성 평균/분산)
# LayerNorm은 배치 크기에 독립적 → Transformer에서 적합

layer_norm = nn.LayerNorm(512)
batch_norm = nn.BatchNorm1d(512)

x = torch.randn(4, 512)
out_ln = layer_norm(x)
print(f"LayerNorm 출력 평균: {out_ln[0].mean().item():.4f}")  # ≈ 0
print(f"LayerNorm 출력 std:  {out_ln[0].std().item():.4f}")   # ≈ 1

정규화 기법 사용 위치

BatchNorm1d  : 완전연결층 (FC Layer)
BatchNorm2d  : CNN 컨볼루션 레이어
LayerNorm    : Transformer, RNN, NLP 모델
GroupNorm    : 배치 크기 작을 때, 세그멘테이션
InstanceNorm : 이미지 스타일 변환 (Style Transfer)

과적합 방지 체크리스트

1. 훈련/검증 손실 그래프 확인
   → 검증 손실이 올라가는 시점 파악

2. 데이터 확인
   → 훈련 데이터가 충분한가? (최소 1000개 이상)
   → 데이터 증강 적용했는가?

3. 모델 복잡도 줄이기
   → 레이어 수 또는 유닛 수 감소

4. 정규화 추가
   → Dropout (FC: 0.5, Conv: 0.1~0.3)
   → L2 정규화 (weight_decay=1e-4)
   → BatchNorm 추가

5. 조기 종료
   → patience=10~20으로 Early Stopping 적용

6. 학습률 조정
   → 학습률 스케줄러 적용

정리

방법 설명 효과 주의사항
데이터 증강 훈련 데이터 인위적으로 늘림 매우 효과적 도메인에 맞는 증강 선택
Dropout 랜덤 뉴런 비활성화 효과적 추론 시 반드시 eval()
L2 정규화 가중치 크기 패널티 효과적 weight_decay로 강도 조절
BatchNorm 레이어 입력 정규화 학습 안정화 배치 크기 최소 16 이상
Early Stopping 검증 손실 기준 학습 중단 간단하고 효과적 patience 적절히 설정
LayerNorm 샘플 단위 정규화 Transformer에 적합 배치 크기 무관