딥러닝 과적합 해결하기: 학습곡선, EarlyStopping, Dropout, ModelCheckpoint

Alchemist·3일 전

KT AIVLE

목록 보기
128/130

학습 정확도가 높아질수록 좋은 모델일까? 항상 그렇지는 않다. 학습 데이터에서는 99%를 맞히지만 처음 보는 사용자 데이터에서는 결과가 나빠지는 경우가 있다. 이것이 과적합(Overfitting) 의 대표적인 모습이다.

과적합을 다루는 핵심은 모델을 무조건 크게 만들거나 에포크를 계속 늘리는 것이 아니라, 검증 성능을 확인하면서 적절한 복잡도와 학습 시점을 찾는 것이다.

과소적합과 과적합을 학습곡선으로 구분하기

  • 과소적합(Underfitting): 모델이 패턴을 충분히 학습하지 못해 학습·검증 손실이 함께 높다.
  • 적절한 학습: 학습·검증 손실이 모두 낮아지고 새로운 데이터에서도 성능이 유지된다.
  • 과적합(Overfitting): 학습 손실은 낮아지지만 검증 손실이 다시 올라가거나 둘 사이의 차이가 커진다.

History.history에는 에포크별 손실 기록이 들어 있다.

import matplotlib.pyplot as plt

def draw_learning_curve(history):
    plt.figure(figsize=(8, 4))
    plt.plot(history.history["loss"], label="Train loss")
    plt.plot(history.history["val_loss"], label="Validation loss")
    plt.xlabel("Epoch")
    plt.ylabel("Loss")
    plt.legend()
    plt.grid(alpha=0.3)
    plt.show()

검증 손실은 데이터 규모가 작을 때 크게 출렁일 수 있다. 따라서 한 번의 상승만으로 곧바로 과적합이라고 단정하지 않고 전반적인 추세를 살펴본다.

모델 복잡도는 어떻게 결정될까?

Dense 신경망에서는 주로 입력 특성 수, 은닉층 수, 각 층의 노드 수가 학습 가능한 파라미터 수를 결정한다.

예를 들어 입력이 20개, 은닉층이 64개, 출력이 1개면 다음과 같다.

(20×64+64)+(64×1+1)=1,409(20\times64+64)+(64\times1+1)=1,409

파라미터가 많을수록 다양한 함수를 표현하기 쉬워지지만 과적합 위험·메모리 요구량·연산량도 증가할 수 있다. model.summary()로 층별 파라미터 수를 점검할 수 있다.

참고로 batch_size와 epochs는 학습 방법과 횟수를 조절하지만 모델 구조의 파라미터 수를 직접 늘리지 않는다.

실험용 데이터 준비

특성 수가 많고 샘플이 상대적으로 적으면 과적합이 생기기 쉬운 조건을 만들 수 있다.

import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

X, y = make_classification(
    n_samples=950,
    n_features=100,
    n_informative=12,
    n_redundant=8,
    flip_y=0.06,
    random_state=42,
)

X_train_all, X_test, y_train_all, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)
X_train, X_val, y_train, y_val = train_test_split(
    X_train_all, y_train_all,
    test_size=0.25, random_state=42, stratify=y_train_all,
)

scaler = StandardScaler()
X_train = scaler.fit_transform(X_train).astype("float32")
X_val = scaler.transform(X_val).astype("float32")
X_test = scaler.transform(X_test).astype("float32")

이는 규제 방법을 비교하기 위한 합성 데이터이며 특정 성능 수치를 보장하려는 목적은 아니다.

EarlyStopping: 개선이 없으면 자동 종료

EarlyStopping은 검증 손실이 더 이상 개선되지 않을 때 학습을 멈춘다.

from tensorflow import keras

keras.utils.set_random_seed(42)

model = keras.Sequential([
    keras.Input(shape=(X_train.shape[1],)),
    keras.layers.Dense(128, activation="relu"),
    keras.layers.Dense(64, activation="relu"),
    keras.layers.Dense(1, activation="sigmoid"),
])
model.compile(
    optimizer=keras.optimizers.Adam(learning_rate=0.001),
    loss="binary_crossentropy",
    metrics=["accuracy"],
)

early_stopping = keras.callbacks.EarlyStopping(
    monitor="val_loss",
    patience=12,
    min_delta=0.001,
    restore_best_weights=True,
)

history = model.fit(
    X_train, y_train,
    validation_data=(X_val, y_val),
    epochs=200,
    batch_size=32,
    callbacks=[early_stopping],
    verbose=0,
)

draw_learning_curve(history)
print("실제 수행한 Epoch:", len(history.history["loss"]))
  • monitor="val_loss": 검증 손실이 개선되는지 관찰한다.
  • patience=12: 개선이 없는 상태를 일정 횟수 허용한다.
  • min_delta=0.001: 개선으로 인정할 최소 변화를 정한다.
  • restore_best_weights=True: 종료 후 관찰 기준에서 가장 좋았던 가중치로 되돌린다.

주의: EarlyStopping에 restore_best_weights=True를 지정하지 않으면 일반적으로 마지막 에포크의 가중치가 남는다. 가장 좋은 검증 시점의 모델을 사용하고 싶다면 옵션을 명시하는 편이 안전하다.

Dropout: 학습 중 일부 노드를 임시로 비활성화

Dropout은 학습 단계에서 일부 노드의 출력을 무작위로 0으로 만들어 특정 노드에 지나치게 의존하는 것을 줄이는 정규화 방법이다. 모델을 영구적으로 제거하는 기능이 아니다. 일반적인 추론 단계에서는 Dropout이 비활성화된다.

keras.utils.set_random_seed(42)

regularized_model = keras.Sequential([
    keras.Input(shape=(X_train.shape[1],)),
    keras.layers.Dense(128, activation="relu"),
    keras.layers.Dropout(0.3),
    keras.layers.Dense(64, activation="relu"),
    keras.layers.Dropout(0.3),
    keras.layers.Dense(1, activation="sigmoid"),
])

regularized_model.compile(
    optimizer=keras.optimizers.Adam(learning_rate=0.001),
    loss="binary_crossentropy",
    metrics=["accuracy"],
)

history_dropout = regularized_model.fit(
    X_train, y_train,
    validation_data=(X_val, y_val),
    epochs=200,
    batch_size=32,
    callbacks=[keras.callbacks.EarlyStopping(
        monitor="val_loss", patience=12, restore_best_weights=True
    )],
    verbose=0,
)

draw_learning_curve(history_dropout)

Dropout(0.3)은 학습할 때 약 30%의 활성값을 임의로 제외한다는 의미다. 비율을 크게 한다고 언제나 일반화 성능이 좋아지는 것은 아니다. 과도한 Dropout은 과소적합을 유발할 수 있다.

L1과 L2 규제도 있다

Dropout 외에도 가중치의 크기를 제한하는 정규화(Regularization) 기법을 사용할 수 있다.

  • L1 규제: 가중치 절댓값의 합에 페널티를 주며 희소한 가중치 구성을 유도할 수 있다.
  • L2 규제: 가중치 제곱합에 페널티를 주어 큰 가중치가 만들어지는 것을 억제한다.
from tensorflow import keras

layer = keras.layers.Dense(
    32,
    activation="relu",
    kernel_regularizer=keras.regularizers.l2(0.001),
)

L1을 사용하면 일부 가중치가 0에 가까워질 수 있지만, 특정 입력 변수가 반드시 완전히 제거된다고 보장하는 것은 아니다.

ModelCheckpoint: 가장 좋은 모델을 파일로 보관

학습 도중 검증 성능이 가장 좋았던 모델을 남겨 두고 싶다면 ModelCheckpoint를 사용한다.

from pathlib import Path

Path("saved_models").mkdir(exist_ok=True)

checkpoint = keras.callbacks.ModelCheckpoint(
    filepath="saved_models/best_churn_model.keras",
    monitor="val_loss",
    mode="min",
    save_best_only=True,
    verbose=0,
)

# 기존에 만든 regularized_model을 다시 학습하기보다는
# 실험을 새로 시작할 때 새 모델을 생성한 뒤 적용하는 편이 비교에 명확하다.
keras.utils.set_random_seed(123)
checkpoint_model = keras.Sequential([
    keras.Input(shape=(X_train.shape[1],)),
    keras.layers.Dense(64, activation="relu"),
    keras.layers.Dropout(0.2),
    keras.layers.Dense(1, activation="sigmoid"),
])
checkpoint_model.compile(optimizer="adam", loss="binary_crossentropy")

checkpoint_model.fit(
    X_train, y_train,
    validation_data=(X_val, y_val),
    epochs=100,
    callbacks=[checkpoint],
    verbose=0,
)

best_model = keras.models.load_model("saved_models/best_churn_model.keras")
loss = best_model.evaluate(X_test, y_test, verbose=0)
print("Test loss:", float(loss))

중요한 점: 체크포인트 파일을 불러왔다면 예측도 best_model.predict(...)로 수행해야 한다. load_model()을 해놓고 실수로 기존의 model.predict(...)를 호출하면 저장된 최적 모델을 평가한 것이 아니다.

model.save("model.keras")는 호출 시점의 모델을 저장한다. 반면 ModelCheckpoint(save_best_only=True)는 지정한 모니터 기준으로 개선된 모델을 저장한다. 둘은 목적이 다르다.

현실적인 튜닝 순서

먼저 작은 모델과 합리적인 학습률(예: Adam의 기본 0.001)로 시작한다. 학습곡선과 검증 지표를 확인하고 필요하다면 모델 크기, 학습률, 배치 크기를 조정한다. 과적합 징후가 보이면 EarlyStopping, Dropout, L2 규제 등을 비교한다. 여러 실험에 같은 테스트셋을 반복 사용하지 않고, 모델 선택을 마친 뒤 테스트셋에서 최종 평가한다.

핵심 정리

  • 학습 손실만 낮은 모델은 새로운 데이터에서 실패할 수 있다.
  • 과적합 여부는 학습·검증 손실의 추세를 함께 분석한다.
  • 모델 복잡도는 층 수·노드 수·입력 특성 수 등에 영향을 받는다.
  • EarlyStopping은 개선이 없을 때 학습을 종료하며 restore_best_weights로 좋은 가중치를 복원할 수 있다.
  • Dropout은 학습 중 일부 활성값을 무작위로 제외하는 정규화 방식이다.
  • L1과 L2는 가중치의 크기에 페널티를 부여하는 방법이다.
  • ModelCheckpoint는 지정한 모니터 기준으로 좋은 모델을 파일로 저장한다.
  • 최종 평가에서는 학습한 모델과 불러온 모델이 같은 객체인지 확인해야 한다.
profile
html_programming_language

0개의 댓글