과적합과 정규화 (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\]
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. 데이터 증강을 정규화로 생각 안 함¶
데이터 증강도 정규화의 일종. 다른 정규화와 함께 조절해야 함.