학습 정확도가 높아질수록 좋은 모델일까? 항상 그렇지는 않다. 학습 데이터에서는 99%를 맞히지만 처음 보는 사용자 데이터에서는 결과가 나빠지는 경우가 있다. 이것이 과적합(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개면 다음과 같다.
파라미터가 많을수록 다양한 함수를 표현하기 쉬워지지만 과적합 위험·메모리 요구량·연산량도 증가할 수 있다. 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은 검증 손실이 더 이상 개선되지 않을 때 학습을 멈춘다.
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은 학습 단계에서 일부 노드의 출력을 무작위로 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은 과소적합을 유발할 수 있다.
Dropout 외에도 가중치의 크기를 제한하는 정규화(Regularization) 기법을 사용할 수 있다.
from tensorflow import keras
layer = keras.layers.Dense(
32,
activation="relu",
kernel_regularizer=keras.regularizers.l2(0.001),
)
L1을 사용하면 일부 가중치가 0에 가까워질 수 있지만, 특정 입력 변수가 반드시 완전히 제거된다고 보장하는 것은 아니다.
학습 도중 검증 성능이 가장 좋았던 모델을 남겨 두고 싶다면 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은 학습 중 일부 활성값을 무작위로 제외하는 정규화 방식이다.ModelCheckpoint는 지정한 모니터 기준으로 좋은 모델을 파일로 저장한다.