[AI 101] 과적합과 과소적합 – 완벽한 균형으로 AI 성능 2배 높이기
핵심 요약
“AI 성능의 적은 과적합이다”는 말이 있습니다. 과적합(Overfitting)은 훈련 데이터에만 99% 정확하지만 실제 테스트에서는 60%로 폭락하는 현상입니다. 반대로 과소적합(Underfitting)은 훈련과 테스트 모두 60%로 모델이 제대로 학습하지 못한 상태입니다. 해결책은 정규화(L1/L2), 드롭아웃(Dropout), 조기종료(Early Stopping)라는 3가지 무기입니다. L2 정규화는 가중치를 작게 유지하고, 드롭아웃은 뉴런의 50%를 랜덤하게 끄며, 조기종료는 최적 타이밍에 학습을 멈춥니다. 이 3가지만 잘 사용해도 모델 성능을 20~30%p 향상시킬 수 있습니다.
📍 목차
- 과적합(Overfitting)이란?
- 과소적합(Underfitting)의 문제
- 정규화(Regularization) 기법: L1, L2
- 드롭아웃(Dropout)과 조기종료(Early Stopping)
- 실전 과적합 방지 전략
1. 과적합(Overfitting)이란?
1-1. 과적합의 정의
과적합(Overfitting)은 모델이 훈련 데이터에만 지나치게 적응하여 새로운 데이터에 대한 예측 성능이 저하되는 현상입니다.
비유: 시험 준비
| 학생 유형 | 공부 방법 | 모의고사 | 수능 | 문제 |
|---|---|---|---|---|
| 과적합 학생 | 기출문제만 암기 | 100점 | 60점 | 새 문제 못 풀음 |
| 적절한 학생 | 개념 이해 + 문제 풀이 | 85점 | 85점 | 일반화 성공 |
AI에서의 과적합:
Training Accuracy: 99% ← 훈련 데이터에 완벽
Test Accuracy: 65% ← 실전에서 실패1-2. 과적합이 발생하는 이유
1️⃣ 모델이 너무 복잡함
예시: 다항식 회귀
| 모델 복잡도 | 수식 | 결과 |
|---|---|---|
| 단순 | (y = w_1x + b) | 과소적합 |
| 적절 | (y = w_1x + w_2x^2 + b) | 최적 |
| 복잡 | (y = w_1x + w_2x^2 + \cdots + w_{20}x^{20} + b) | 과적합 |
시각적 이해:
데이터: . . . . .
. .
. . .
과소적합: ───────── (직선, 단순)
적절: ───╭──╮── (곡선, 적절)
과적합: ─╭╮─╰─╮─╰ (지그재그, 복잡)2️⃣ 데이터가 너무 적음
| 데이터 개수 | 모델 파라미터 | 비율 | 결과 |
|---|---|---|---|
| 100개 | 10개 | 10:1 | ✅ 안정적 |
| 100개 | 1,000개 | 1:10 | ❌ 과적합 |
원칙: 파라미터 1개당 데이터 최소 10개 이상
3️⃣ 훈련 시간이 너무 길음
학습 곡선:
Loss
│
│ Training Loss
│ ╲
│ ╲___________
│
│ Validation Loss
│ ╲
│ ╲_╭───────── ← 이 시점 이후 과적합!
│ ╰───────
└──────────────────> Epoch
최적 지점1-3. 과적합 진단 방법
1. Training vs Test 성능 비교
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
# 데이터 분할
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 모델 학습
model = RandomForestClassifier(n_estimators=100, max_depth=20)
model.fit(X_train, y_train)
# 평가
train_acc = model.score(X_train, y_train)
test_acc = model.score(X_test, y_test)
print(f"Training Accuracy: {train_acc:.2%}")
print(f"Test Accuracy: {test_acc:.2%}")
print(f"Gap: {(train_acc - test_acc)*100:.1f}%p")
# 진단
if train_acc - test_acc > 0.1: # 10%p 이상 차이
print("⚠️ 과적합 의심!")출력 예시:
Training Accuracy: 99.5%
Test Accuracy: 72.3%
Gap: 27.2%p
⚠️ 과적합 의심!2. 학습 곡선 (Learning Curve)
import matplotlib.pyplot as plt
history = model.fit(X_train, y_train,
validation_split=0.2,
epochs=100)
plt.figure(figsize=(12, 4))
# Loss 곡선
plt.subplot(1, 2, 1)
plt.plot(history.history['loss'], label='Train Loss')
plt.plot(history.history['val_loss'], label='Val Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.title('Loss Curve')
# Accuracy 곡선
plt.subplot(1, 2, 2)
plt.plot(history.history['accuracy'], label='Train Acc')
plt.plot(history.history['val_accuracy'], label='Val Acc')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend()
plt.title('Accuracy Curve')
plt.show()과적합 징후:
- Train Loss ↓, Val Loss ↑
- Train Acc ↑, Val Acc ↓
- 두 곡선이 벌어짐 (Diverge)
2. 과소적합(Underfitting)의 문제
2-1. 과소적합의 정의
과소적합(Underfitting)은 모델이 너무 단순해서 데이터의 패턴을 제대로 학습하지 못하는 현상입니다.
비유: 시험 준비
| 학생 유형 | 공부 방법 | 모의고사 | 수능 | 문제 |
|---|---|---|---|---|
| 과소적합 학생 | 전혀 안 함 | 50점 | 50점 | 기본도 모름 |
| 적절한 학생 | 개념 이해 + 문제 풀이 | 85점 | 85점 | 일반화 성공 |
AI에서의 과소적합:
Training Accuracy: 60% ← 훈련도 못함
Test Accuracy: 58% ← 테스트도 못함2-2. 과소적합 vs 과적합 비교
| 항목 | 과소적합 (Underfitting) | 적절 (Good Fit) | 과적합 (Overfitting) |
|---|---|---|---|
| 모델 복잡도 | 너무 낮음 | 적절 | 너무 높음 |
| Training 성능 | 낮음 (60%) | 높음 (85%) | 매우 높음 (99%) |
| Test 성능 | 낮음 (58%) | 높음 (85%) | 낮음 (65%) |
| 편향(Bias) | 높음 | 적절 | 낮음 |
| 분산(Variance) | 낮음 | 적절 | 높음 |
| 문제 | 학습 부족 | – | 일반화 실패 |
2-3. 과소적합 발생 원인
1️⃣ 모델이 너무 단순
예시: 비선형 데이터를 선형 모델로
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
# 비선형 데이터 생성
X = np.linspace(-3, 3, 100).reshape(-1, 1)
y = X**2 + np.random.randn(100, 1) * 0.5
# 1. 선형 모델 (과소적합)
model_linear = LinearRegression()
model_linear.fit(X, y)
score_linear = model_linear.score(X, y)
print(f"선형 모델 R² Score: {score_linear:.4f}") # 0.02 (매우 낮음)
# 2. 다항식 모델 (적절)
poly = PolynomialFeatures(degree=2)
X_poly = poly.fit_transform(X)
model_poly = LinearRegression()
model_poly.fit(X_poly, y)
score_poly = model_poly.score(X_poly, y)
print(f"다항식 모델 R² Score: {score_poly:.4f}") # 0.98 (높음)2️⃣ Feature가 부족
# 부족한 Feature
X_poor = df[['나이']] # 1개만
model.fit(X_poor, y)
print(f"Score: {model.score(X_test, y_test):.2%}") # 65%
# 충분한 Feature
X_rich = df[['나이', '연봉', '신용점수', '직업']] # 4개
model.fit(X_rich, y)
print(f"Score: {model.score(X_test, y_test):.2%}") # 85%3️⃣ 학습 시간이 부족
# 부족한 Epoch
model.fit(X_train, y_train, epochs=5) # 5 epoch
# → 과소적합
# 충분한 Epoch
model.fit(X_train, y_train, epochs=100) # 100 epoch
# → 적절2-4. 과소적합 해결 방법
| 방법 | 설명 | 예시 |
|---|---|---|
| 모델 복잡도 증가 | 더 복잡한 모델 사용 | Linear → Polynomial, Decision Tree → Random Forest |
| Feature 추가 | 더 많은 변수 사용 | Feature Engineering |
| 학습 시간 증가 | Epoch 늘리기 | 10 → 100 epoch |
| 정규화 완화 | λ 값 줄이기 | λ=0.1 → λ=0.01 |
3. 정규화(Regularization) 기법: L1, L2
3-1. 정규화란?
정규화(Regularization)는 모델 복잡도에 패널티를 부여하여 과적합을 방지하는 기법입니다.
핵심 아이디어:
“가중치가 크면 모델이 복잡하다 → 가중치를 작게 유지하자”
정규화 수식:
[
J_{reg} = J_{original} + \lambda \times \text{Penalty}
]
- (J_{original}): 원래 손실함수 (MSE 등)
- (\lambda): 정규화 강도 (0~1)
- Penalty: L1 or L2
3-2. L1 정규화 (Lasso Regression)
L1 Penalty: 가중치의 절댓값 합
수식:
[
J_{L1} = J_{original} + \lambda \sum_{i=1}^{n}|w_i|
]
특징:
- 일부 가중치를 정확히 0으로 만듦
- Feature Selection 효과
- 희소한 모델 (Sparse Model)
Python 구현:
from sklearn.linear_model import Lasso
# L1 정규화
model = Lasso(alpha=0.1) # alpha = λ
model.fit(X_train, y_train)
# 가중치 확인
print("가중치:", model.coef_)
print("0인 가중치 개수:", (model.coef_ == 0).sum())출력 예시:
가중치: [0. 2.3 0. 1.5 0. 0. 3.1]
0인 가중치 개수: 4효과: 7개 중 4개 Feature를 자동으로 제거 (Feature Selection)
3-3. L2 정규화 (Ridge Regression)
L2 Penalty: 가중치의 제곱 합
수식:
[
J_{L2} = J_{original} + \lambda \sum_{i=1}^{n}w_i^2
]
특징:
- 가중치를 작게 유지 (0은 아님)
- 모든 Feature 사용
- 일반적으로 L1보다 성능 좋음
Python 구현:
from sklearn.linear_model import Ridge
# L2 정규화
model = Ridge(alpha=0.1) # alpha = λ
model.fit(X_train, y_train)
# 가중치 확인
print("가중치:", model.coef_)출력 예시:
가중치: [0.15 2.30 0.08 1.52 0.03 0.11 3.05]효과: 모든 가중치가 작지만 0은 아님
3-4. L1 vs L2 비교
| 항목 | L1 (Lasso) | L2 (Ridge) |
|---|---|---|
| Penalty | (\sum | w_i |
| 가중치 | 일부 0으로 | 모두 작게 |
| Feature Selection | ✅ 가능 | ❌ 불가능 |
| 희소성 | ⭐⭐⭐⭐⭐ | ⭐ |
| 일반화 성능 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 이상치 민감도 | 낮음 (강건) | 높음 (민감) |
| 사용 시기 | Feature 많을 때 | 일반적인 경우 |
시각적 비교:
L1 (마름모): L2 (원):
╱│╲ ╭─╮
╱ │ ╲ ╱ ╲
╱ │ ╲ │ │
╱ │ ╲ │ │
─────────── ╰─────╯
╲ │ ╱
╲ │ ╱
╲ │ ╱
╲│╱
축과 만나는 점 축과 만나지 않음
→ 가중치 0 → 가중치 작지만 0 아님3-5. Elastic Net (L1 + L2)
L1과 L2를 동시에 사용
수식:
[
J_{ElasticNet} = J_{original} + \lambda_1 \sum |w_i| + \lambda_2 \sum w_i^2
]
from sklearn.linear_model import ElasticNet
model = ElasticNet(alpha=0.1, l1_ratio=0.5) # 50% L1, 50% L2
model.fit(X_train, y_train)장점:
- L1의 Feature Selection + L2의 안정성
- 최고의 일반화 성능
4. 드롭아웃(Dropout)과 조기종료(Early Stopping)
4-1. 드롭아웃(Dropout)
드롭아웃은 학습 시 일부 뉴런을 랜덤하게 제거하여 과적합을 방지하는 기법입니다.
핵심 아이디어:
“특정 뉴런에 의존하지 않도록 매번 다른 조합으로 학습”
작동 방식:
원본 신경망:
Input → [O O O O] → [O O O] → Output
은닉층 1 은닉층 2
Dropout (50%) 적용:
Epoch 1: Input → [X O X O] → [O X O] → Output
Epoch 2: Input → [O X O X] → [X O X] → Output
Epoch 3: Input → [O O X X] → [O O X] → Output
...Python 구현 (PyTorch):
import torch
import torch.nn as nn
class Model(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(100, 50)
self.dropout1 = nn.Dropout(p=0.5) # 50% 드롭
self.fc2 = nn.Linear(50, 10)
self.dropout2 = nn.Dropout(p=0.3) # 30% 드롭
self.fc3 = nn.Linear(10, 2)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = self.dropout1(x) # 학습 시에만 작동
x = torch.relu(self.fc2(x))
x = self.dropout2(x)
x = self.fc3(x)
return x
model = Model()
# 학습 모드
model.train() # Dropout 활성화
# 평가 모드
model.eval() # Dropout 비활성화Keras 구현:
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout
model = Sequential([
Dense(50, activation='relu', input_shape=(100,)),
Dropout(0.5), # 50% 드롭
Dense(10, activation='relu'),
Dropout(0.3), # 30% 드롭
Dense(2, activation='softmax')
])효과:
| 지표 | Dropout 없음 | Dropout 50% |
|---|---|---|
| Training Acc | 98% | 92% |
| Test Acc | 72% | 88% |
| 성능 향상 | – | +16%p |
4-2. 조기종료(Early Stopping)
조기종료는 Validation Loss가 더 이상 개선되지 않으면 학습을 중단하는 기법입니다.
핵심 아이디어:
“과적합이 시작되기 전에 멈추자”
작동 방식:
Validation Loss
│
0.5 │ ●
│ ●
0.4 │ ●
│ ● ← 최저점
0.3 │ ●
│ ●
0.4 │ ●
│ ● ← 3 epoch 연속 개선 없음
0.5 │ ●
│ STOP! (Early Stopping)
└───────────────────> Epoch
1 2 3 4 5 6 7 8Python 구현 (Keras):
from tensorflow.keras.callbacks import EarlyStopping
# Early Stopping 설정
early_stop = EarlyStopping(
monitor='val_loss', # 모니터링할 지표
patience=5, # 5 epoch 동안 개선 없으면 중단
mode='min', # val_loss는 작을수록 좋음
restore_best_weights=True, # 최적 가중치 복원
verbose=1 # 로그 출력
)
# 학습
history = model.fit(
X_train, y_train,
validation_split=0.2,
epochs=100, # 최대 100 epoch
callbacks=[early_stop] # Early Stopping 적용
)
print(f"실제 학습 Epoch: {len(history.history['loss'])}")출력 예시:
Epoch 1/100: loss: 0.45 - val_loss: 0.40
Epoch 2/100: loss: 0.35 - val_loss: 0.32
...
Epoch 23/100: loss: 0.05 - val_loss: 0.18
Epoch 24/100: loss: 0.04 - val_loss: 0.19
Epoch 25/100: loss: 0.03 - val_loss: 0.20
Early stopping triggered. Restoring weights from epoch 23.
실제 학습 Epoch: 25PyTorch 구현:
best_loss = float('inf')
patience = 5
counter = 0
for epoch in range(100):
# 학습
train_loss = train_one_epoch()
val_loss = validate()
# Early Stopping 체크
if val_loss = patience:
print(f"Early stopping at epoch {epoch}")
model.load_state_dict(torch.load('best_model.pth'))
break4-3. 드롭아웃 vs 조기종료 비교
| 항목 | 드롭아웃 (Dropout) | 조기종료 (Early Stopping) |
|---|---|---|
| 방법 | 뉴런 랜덤 제거 | 학습 시점 제어 |
| 적용 시기 | 학습 중 매 배치 | 학습 전체 |
| 장점 | 앙상블 효과, 강력한 정규화 | 시간 절약, 간단 |
| 단점 | 학습 시간 증가 | Patience 조정 필요 |
| 추천 사용 | 딥러닝 (은닉층 많음) | 모든 모델 |
| 하이퍼파라미터 | Dropout 비율 (0.2~0.5) | Patience (3~10) |
5. 실전 과적합 방지 전략
5-1. 단계별 체크리스트
1단계: 데이터 확보
- [ ] 파라미터 1개당 데이터 최소 10개
- [ ] Training/Validation/Test 분할 (60/20/20)
- [ ] 데이터 증강 (이미지의 경우)
2단계: 모델 선택
- [ ] 간단한 모델부터 시작 (Linear → Tree → Neural Network)
- [ ] Baseline 모델 성능 확인
3단계: 정규화 적용
- [ ] L2 정규화 (λ=0.01부터 시작)
- [ ] Feature 많으면 L1 고려
4단계: 드롭아웃 (딥러닝)
- [ ] 은닉층에 Dropout 0.5 적용
- [ ] 출력층 직전은 Dropout 0.2~0.3
5단계: 조기종료
- [ ] Early Stopping (patience=5)
- [ ] 최적 가중치 자동 저장
6단계: 평가
- [ ] Training vs Test 성능 비교
- [ ] Gap
5-2. 과적합 방지 기법 조합
상황 1: 데이터 부족 (
model = Ridge(alpha=0.1) # L2 정규화
model.fit(X_train, y_train)상황 2: 딥러닝 (은닉층 많음)
model = Sequential([
Dense(128, activation='relu'),
Dropout(0.5), # 드롭아웃
Dense(64, activation='relu'),
Dropout(0.3),
Dense(10, activation='softmax')
])
model.compile(
optimizer='adam',
loss='categorical_crossentropy',
metrics=['accuracy']
)
early_stop = EarlyStopping(patience=5) # 조기종료
model.fit(X_train, y_train,
validation_split=0.2,
epochs=100,
callbacks=[early_stop])상황 3: Feature 많음 (> 100개)
from sklearn.linear_model import Lasso
model = Lasso(alpha=0.01) # L1 정규화 (Feature Selection)
model.fit(X_train, y_train)
# 중요한 Feature만 선택
important_features = X.columns[model.coef_ != 0]
print(f"선택된 Feature: {important_features}")5-3. 하이퍼파라미터 튜닝 가이드
| 하이퍼파라미터 | 권장 범위 | 조정 방향 |
|---|---|---|
| L1/L2 α | 0.0001 ~ 1.0 | 과적합 심하면 ↑ |
| Dropout 비율 | 0.2 ~ 0.5 | 과적합 심하면 ↑ |
| Early Stop Patience | 3 ~ 10 | 데이터 많으면 ↑ |
| Learning Rate | 0.0001 ~ 0.1 | 과적합이면 ↓ |
5-4. 실전 예제: Kaggle Titanic
과적합 버전:
from sklearn.ensemble import RandomForestClassifier
# 과도하게 복잡한 모델
model = RandomForestClassifier(
n_estimators=1000,
max_depth=None, # 제한 없음
min_samples_split=2
)
model.fit(X_train, y_train)
train_acc = model.score(X_train, y_train)
test_acc = model.score(X_test, y_test)
print(f"Train: {train_acc:.2%}, Test: {test_acc:.2%}")
# Train: 100.00%, Test: 72.00% (과적합!)최적화 버전:
# 정규화된 모델
model = RandomForestClassifier(
n_estimators=100,
max_depth=5, # 깊이 제한
min_samples_split=10, # 분할 제한
min_samples_leaf=5, # 리프 크기 제한
max_features='sqrt' # Feature 제한
)
model.fit(X_train, y_train)
train_acc = model.score(X_train, y_train)
test_acc = model.score(X_test, y_test)
print(f"Train: {train_acc:.2%}, Test: {test_acc:.2%}")
# Train: 85.00%, Test: 82.00% (적절!)성능 향상: Test 72% → 82% (+10%p)
FAQ: 초보자가 자주 묻는 질문
Q1. 과적합과 과소적합 중 어느 것이 더 나쁜가요?
A. 과적합이 더 위험합니다. 과소적합은 “학습이 부족”하다는 것을 바로 알 수 있지만, 과적합은 Training에서 99% 정확하여 “잘 학습되었다”고 착각하게 만듭니다. 실전에서 65%로 폭락할 때 뒤늦게 발견합니다.
Q2. L1과 L2 중 뭘 써야 하나요?
A. 일반적으로 L2 (Ridge)를 먼저 시도하세요. L2가 성능이 더 좋고 안정적입니다. Feature가 100개 이상으로 많으면 L1 (Lasso)로 Feature Selection하세요. 최고 성능은 Elastic Net (L1+L2).
Q3. Dropout 비율은 어떻게 정하나요?
A. 기본값 0.5부터 시작하세요. 과적합이 심하면 0.6~0.7로 증가, 과소적합이면 0.3~0.4로 감소. 출력층 직전은 0.2~0.3 권장 (너무 높으면 정보 손실).
Q4. Early Stopping의 Patience는 몇이 적당한가요?
A. 데이터 크기에 비례: 데이터 100,000개: patience=10~20. 너무 작으면 조기 중단, 너무 크면 과적합 위험.
Q5. 정규화, 드롭아웃, 조기종료를 동시에 사용해도 되나요?
A. Yes! 오히려 함께 사용하면 더 효과적입니다. L2 정규화 + Dropout + Early Stopping 조합이 실무 표준입니다. 단, λ와 Dropout 비율을 동시에 너무 크게 하면 과소적합 위험이 있으니 조심하세요.
외부 참고 자료
과적합과 정규화를 더 깊게 배우고 싶다면:
- IBM – 과적합 vs 과소적합 – 개념 설명
- scikit-learn – Regularization – Ridge, Lasso
- TensorFlow – Dropout – 공식 문서
- Keras – Callbacks – Early Stopping
- Google ML – Overfitting – 정규화 가이드
정리: 이 글에서 배운 것
✅ 과적합: Training 99%, Test 65% → 일반화 실패
✅ 과소적합: Training 60%, Test 58% → 학습 부족
✅ L1 정규화: 가중치 일부를 0으로, Feature Selection
✅ L2 정규화: 가중치를 작게, 일반적으로 더 좋음
✅ 드롭아웃: 뉴런 50% 랜덤 제거, 앙상블 효과
✅ 조기종료: Val Loss 개선 없으면 중단, 시간 절약
다음 편에서는 “딥러닝의 핵심 – CNN과 RNN”에 대해 자세히 알아봅니다. 특히 합성곱 신경망(CNN), 순환 신경망(RNN), LSTM을 완벽 설명하겠습니다.
내부 참고 자료
- 과적합 해결 못하면 AI 프로젝트 70% 실패 – AI가 세상 모든 걸 외우면 왜 쓸모가 없을까?
- 활성화 함수 완전 정복: ReLU부터 GELU·SwiGLU·TeLU까지
- 손실함수 완전 정복: MSE·Cross-Entropy부터 Focal Loss·Huber Loss까지
