콘텐츠로 이동
Data Prep
상세

과적합과 정규화 (Overfitting and Regularization)

모델이 훈련 데이터에 지나치게 맞춰지는 것을 방지하고 일반화 성능을 높이는 기법.

과적합 이해하기

과적합 vs 과소적합

모델 복잡도 낮음                                    모델 복잡도 높음
    |                                                    |
    v                                                    v
+--------+          +--------+          +--------+
|과소적합|          |적정 적합|          | 과적합  |
|(Under) |          |(Good)  |          | (Over) |
+--------+          +--------+          +--------+
|        |          |        |          |        |
|높은    |          |낮은    |          |낮은    |
|훈련오차|          |훈련오차|          |훈련오차|
|높은    |          |낮은    |          |높은    |
|검증오차|          |검증오차|          |검증오차|
+--------+          +--------+          +--------+

편향-분산 트레이드오프

\[Error = Bias^2 + Variance + Noise\]
항목 의미 관련
Bias 모델의 단순화 오차 과소적합
Variance 데이터 변화에 대한 민감도 과적합
Noise 줄일 수 없는 오차 데이터 품질
# 과적합 탐지
def detect_overfitting(train_losses, val_losses):
    """훈련/검증 손실 비교"""
    gap = val_losses[-1] - train_losses[-1]

    if gap > 0.1 * train_losses[-1]:  # 10% 이상 차이
        print("과적합 가능성 높음")

    # 검증 손실 상승 추세
    if len(val_losses) > 5:
        recent_trend = np.polyfit(range(5), val_losses[-5:], 1)[0]
        if recent_trend > 0:
            print("검증 손실 상승 중")

    return gap

L1/L2 정규화

L2 정규화 (Ridge, Weight Decay)

큰 가중치에 페널티 부여.

\[L_{total} = L_{data} + \lambda \sum w_i^2\]
# PyTorch: weight_decay 파라미터
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=0.01)

# 수동 구현
def l2_regularization(model, lambda_=0.01):
    l2_loss = 0
    for param in model.parameters():
        l2_loss += torch.sum(param ** 2)
    return lambda_ * l2_loss

loss = criterion(output, target) + l2_regularization(model)

베이지안 해석: 가중치에 대한 가우시안 사전 분포

L1 정규화 (Lasso)

가중치를 희소(sparse)하게 만듦.

\[L_{total} = L_{data} + \lambda \sum |w_i|\]
def l1_regularization(model, lambda_=0.01):
    l1_loss = 0
    for param in model.parameters():
        l1_loss += torch.sum(torch.abs(param))
    return lambda_ * l1_loss

# 또는 ProximalGradient 기반 옵티마이저 사용

특성 선택 효과: 불필요한 특성의 가중치를 0으로

Elastic Net

L1 + L2 결합.

\[L_{total} = L_{data} + \lambda_1 \sum |w_i| + \lambda_2 \sum w_i^2\]
def elastic_net_regularization(model, l1_lambda=0.01, l2_lambda=0.01):
    l1_loss = sum(torch.sum(torch.abs(p)) for p in model.parameters())
    l2_loss = sum(torch.sum(p ** 2) for p in model.parameters())
    return l1_lambda * l1_loss + l2_lambda * l2_loss

정규화 비교

정규화 효과 사용 사례
L2 가중치 축소 일반적
L1 희소화, 특성 선택 고차원 데이터
Elastic Net L1+L2 장점 상관된 특성

Dropout

학습 중 무작위로 뉴런 비활성화.

원리

훈련 시:
[o] [x] [o] [o] [x] [o]  <- p 확률로 비활성화
 |       |   |       |
 v       v   v       v
[o] [o] [o] [o] [o] [o]

추론 시:
모든 뉴런 활성화, 가중치 * (1-p)로 스케일링
(또는 훈련 시 / (1-p)로 스케일링 = Inverted Dropout)
import torch.nn as nn

# PyTorch
class Model(nn.Module):
    def __init__(self, dropout_rate=0.5):
        super().__init__()
        self.fc1 = nn.Linear(784, 256)
        self.dropout = nn.Dropout(p=dropout_rate)
        self.fc2 = nn.Linear(256, 10)

    def forward(self, x):
        x = torch.relu(self.fc1(x))
        x = self.dropout(x)  # 훈련 시에만 적용
        x = self.fc2(x)
        return x

# 수동 구현
def dropout(x, p=0.5, training=True):
    if not training:
        return x
    mask = (torch.rand_like(x) > p).float()
    return x * mask / (1 - p)  # Inverted dropout

Dropout 변형

# Spatial Dropout (CNN용)
# 채널 전체를 드롭
spatial_dropout = nn.Dropout2d(p=0.3)

# Alpha Dropout (SELU와 함께)
alpha_dropout = nn.AlphaDropout(p=0.1)

# DropConnect (가중치 드롭)
# 직접 구현 필요

MC Dropout (불확실성 추정)

추론 시에도 드롭아웃 활성화하여 예측 분포 추정.

def mc_dropout_predict(model, x, n_samples=100):
    model.train()  # Dropout 활성화
    predictions = []

    with torch.no_grad():
        for _ in range(n_samples):
            pred = model(x)
            predictions.append(pred)

    predictions = torch.stack(predictions)
    mean = predictions.mean(dim=0)
    uncertainty = predictions.std(dim=0)

    return mean, uncertainty

조기 종료 (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.best_model = None

    def __call__(self, val_loss, model):
        if val_loss < self.best_loss - self.min_delta:
            self.best_loss = val_loss
            self.best_model = model.state_dict().copy()
            self.counter = 0
        else:
            self.counter += 1

        return self.counter >= self.patience

    def restore_best_model(self, model):
        model.load_state_dict(self.best_model)

# 사용
early_stopping = EarlyStopping(patience=10)

for epoch in range(num_epochs):
    train_loss = train_epoch(model)
    val_loss = validate(model)

    if early_stopping(val_loss, model):
        print(f"Early stopping at epoch {epoch}")
        early_stopping.restore_best_model(model)
        break

데이터 증강 (Data Augmentation)

훈련 데이터를 인위적으로 확장.

이미지 증강

from torchvision import transforms

# 기본 증강
train_transform = transforms.Compose([
    transforms.RandomHorizontalFlip(),
    transforms.RandomRotation(10),
    transforms.ColorJitter(brightness=0.2, contrast=0.2),
    transforms.RandomResizedCrop(224, scale=(0.8, 1.0)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], 
                        std=[0.229, 0.224, 0.225])
])

# AutoAugment
from torchvision.transforms import AutoAugment, AutoAugmentPolicy
auto_augment = AutoAugment(AutoAugmentPolicy.IMAGENET)

# RandAugment
from torchvision.transforms import RandAugment
rand_augment = RandAugment(num_ops=2, magnitude=9)

# MixUp
def mixup(x, y, alpha=0.2):
    lam = np.random.beta(alpha, alpha)
    idx = torch.randperm(x.size(0))
    mixed_x = lam * x + (1 - lam) * x[idx]
    return mixed_x, y, y[idx], lam

# CutMix
def cutmix(x, y, alpha=1.0):
    lam = np.random.beta(alpha, alpha)
    idx = torch.randperm(x.size(0))

    # 패치 좌표 계산
    W, H = x.size(2), x.size(3)
    cut_rat = np.sqrt(1 - lam)
    cut_w = int(W * cut_rat)
    cut_h = int(H * cut_rat)

    cx = np.random.randint(W)
    cy = np.random.randint(H)

    bbx1 = np.clip(cx - cut_w // 2, 0, W)
    bbx2 = np.clip(cx + cut_w // 2, 0, W)
    bby1 = np.clip(cy - cut_h // 2, 0, H)
    bby2 = np.clip(cy + cut_h // 2, 0, H)

    x[:, :, bbx1:bbx2, bby1:bby2] = x[idx, :, bbx1:bbx2, bby1:bby2]
    lam = 1 - ((bbx2 - bbx1) * (bby2 - bby1) / (W * H))

    return x, y, y[idx], lam

텍스트 증강 (NLP)

# 동의어 치환
import random
from nltk.corpus import wordnet

def synonym_replacement(text, n=1):
    words = text.split()
    for _ in range(n):
        idx = random.randint(0, len(words) - 1)
        word = words[idx]
        synonyms = []
        for syn in wordnet.synsets(word):
            for lemma in syn.lemmas():
                synonyms.append(lemma.name())
        if synonyms:
            words[idx] = random.choice(synonyms)
    return ' '.join(words)

# 역번역 (Back Translation)
# 영어 -> 프랑스어 -> 영어

# 랜덤 삽입/삭제
def random_deletion(text, p=0.1):
    words = text.split()
    return ' '.join([w for w in words if random.random() > p])

앙상블 (Ensemble)

여러 모델의 예측을 결합.

# 평균 앙상블
def ensemble_average(models, x):
    predictions = [model(x) for model in models]
    return torch.stack(predictions).mean(dim=0)

# 가중 평균
def weighted_ensemble(models, weights, x):
    predictions = [w * model(x) for model, w in zip(models, weights)]
    return sum(predictions)

# 스냅샷 앙상블 (단일 학습에서 여러 모델)
# 학습 중 체크포인트 저장하여 앙상블

모델 정규화 레이어

Batch Normalization

\[\hat{x} = \frac{x - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}} \cdot \gamma + \beta\]
# CNN에서
nn.BatchNorm2d(num_features)

# FC에서
nn.BatchNorm1d(num_features)

Layer Normalization

Transformer에서 주로 사용.

nn.LayerNorm(normalized_shape)

# Transformer 블록
class TransformerBlock(nn.Module):
    def __init__(self, d_model):
        super().__init__()
        self.attention = MultiHeadAttention(d_model)
        self.norm1 = nn.LayerNorm(d_model)
        self.ffn = FeedForward(d_model)
        self.norm2 = nn.LayerNorm(d_model)

    def forward(self, x):
        x = x + self.attention(self.norm1(x))
        x = x + self.ffn(self.norm2(x))
        return x

정규화 전략 선택

상황 권장 기법
데이터 적음 강한 증강, Dropout, L2
데이터 많음 약한 정규화, 더 큰 모델
이미지 Dropout, MixUp, CutMix
NLP Dropout, Label Smoothing
LLM Weight Decay, Dropout

Scikit-learn에서의 정규화

from sklearn.linear_model import Ridge, Lasso, ElasticNet, LogisticRegression

# L2 정규화 (Ridge)
ridge = Ridge(alpha=1.0)  # alpha = lambda
ridge.fit(X_train, y_train)

# L1 정규화 (Lasso)
lasso = Lasso(alpha=0.1)
lasso.fit(X_train, y_train)
print(f"선택된 특성 수: {(lasso.coef_ != 0).sum()}")

# Elastic Net (L1 + L2)
elastic = ElasticNet(alpha=0.1, l1_ratio=0.5)  # l1_ratio: L1 비율
elastic.fit(X_train, y_train)

# Logistic Regression with regularization
log_reg = LogisticRegression(
    penalty='l2',      # 'l1', 'l2', 'elasticnet', 'none'
    C=1.0,             # C = 1/lambda (역수!)
    solver='lbfgs'
)
log_reg.fit(X_train, y_train)

# 정규화 강도 튜닝
from sklearn.linear_model import RidgeCV, LassoCV

ridge_cv = RidgeCV(alphas=[0.1, 1.0, 10.0], cv=5)
ridge_cv.fit(X_train, y_train)
print(f"최적 alpha: {ridge_cv.alpha_}")

하이퍼파라미터 튜닝 팁

정규화 강도 (lambda/alpha)

# 일반적인 탐색 범위 (로그 스케일)
alphas = [0.0001, 0.001, 0.01, 0.1, 1.0, 10.0, 100.0]

# 교차 검증으로 최적값 찾기
from sklearn.model_selection import GridSearchCV

param_grid = {'alpha': np.logspace(-4, 2, 50)}
ridge_cv = GridSearchCV(Ridge(), param_grid, cv=5, scoring='neg_mean_squared_error')
ridge_cv.fit(X_train, y_train)

Dropout 비율

레이어 권장 비율
입력 레이어 0.0 ~ 0.2
은닉 레이어 0.3 ~ 0.5
출력 직전 0.2 ~ 0.5
CNN (Spatial) 0.1 ~ 0.3
RNN 0.0 ~ 0.3 (주의 필요)
# 계층별 다른 Dropout 적용
class Network(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(784, 256)
        self.dropout1 = nn.Dropout(0.3)
        self.fc2 = nn.Linear(256, 128)
        self.dropout2 = nn.Dropout(0.5)
        self.fc3 = nn.Linear(128, 10)

Weight Decay (AdamW)

# 일반적인 값
# 0.01: Transformer, LLM 학습의 일반적인 값
# 0.0001 ~ 0.001: CNN
# 0.1: 강한 정규화 필요 시

optimizer = optim.AdamW(
    model.parameters(),
    lr=1e-4,
    weight_decay=0.01  # L2 정규화 강도
)

Early Stopping 파라미터

# patience: 개선 없이 기다릴 에폭 수
# 작은 데이터셋: 5-10
# 큰 데이터셋: 10-20
# 빠른 실험: 3-5

# min_delta: 개선으로 인정할 최소 변화
# 손실 스케일에 따라 조정
# 일반적으로: 0.0001 ~ 0.001

early_stopping = EarlyStopping(
    patience=10,
    min_delta=0.001,
    restore_best_weights=True
)

흔히 하는 실수

1. 테스트 시 Dropout 끄지 않음

# 나쁜 예
def predict(model, x):
    return model(x)  # 훈련 모드 그대로

# 좋은 예
def predict(model, x):
    model.eval()  # Dropout 비활성화
    with torch.no_grad():
        return model(x)

2. BatchNorm과 Dropout 순서 실수

# 논쟁이 있지만 일반적인 권장:
# Conv -> BatchNorm -> ReLU -> Dropout (드물게 사용)
# Linear -> Dropout -> (activation 전 또는 후)

# BatchNorm 뒤에 Dropout을 넣으면 분산 추정이 왜곡될 수 있음
# 대안: BatchNorm 없이 Dropout, 또는 그 반대

3. Adam에서 weight_decay와 L2 정규화 혼동

# Adam의 weight_decay는 L2 정규화와 같지 않다!
# Adam + weight_decay: 적응적 학습률에 영향
# AdamW + weight_decay: 올바른 L2 정규화 (decoupled)

# 나쁜 예
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=0.01)

# 좋은 예
optimizer = optim.AdamW(model.parameters(), lr=0.001, weight_decay=0.01)

4. 정규화 없이 바로 복잡한 모델 사용

# 나쁜 예: 처음부터 깊은 모델
model = ComplexDeepNetwork()  # 정규화 없음

# 좋은 예: 간단한 모델 → 정규화 추가 → 복잡성 증가
# 1. 기본 모델로 baseline
# 2. 과적합 발생 시 정규화 추가
# 3. 과소적합 시 모델 복잡성 증가

5. 모든 레이어에 동일한 Dropout 적용

# 나쁜 예
dropout = nn.Dropout(0.5)  # 모든 곳에 0.5

# 좋은 예: 레이어별 조정
# 초기 레이어: 낮은 dropout (정보 손실 방지)
# 후기 레이어: 높은 dropout (과적합 방지)

6. Early Stopping 없이 고정 에폭 학습

# 나쁜 예
for epoch in range(1000):  # 무조건 1000 에폭
    train(model)

# 좋은 예
for epoch in range(1000):
    train_loss = train(model)
    val_loss = validate(model)

    if early_stopping(val_loss, model):
        print(f"Early stopping at epoch {epoch}")
        break

7. 검증 없이 정규화 강도 설정

# 나쁜 예: 임의의 lambda
model = Ridge(alpha=1.0)  # 왜 1.0?

# 좋은 예: 교차 검증으로 결정
model = RidgeCV(alphas=np.logspace(-4, 2, 50), cv=5)
model.fit(X_train, y_train)
print(f"최적 alpha: {model.alpha_}")

8. 데이터 증강을 정규화로 생각 안 함

데이터 증강도 정규화의 일종. 다른 정규화와 함께 조절해야 함.

# 강한 증강 + 강한 정규화 = 과소적합 가능
# 적절한 균형 찾기

# 증강이 강할 때: Dropout 줄이기
# 증강이 약할 때: Dropout 늘리기

참고 자료