왜 학습률과 에폭이 과적합을 만드는가
파인튜닝에서 과적합은 대부분 두 가지 하이퍼파라미터의 조합에서 발생한다. 학습률이 너무 크면 사전학습 가중치를 급격히 덮어써 일반화 능력을 잃고(catastrophic forgetting), 에폭이 과하면 소수의 학습 샘플을 그대로 암기한다. 특히 파인튜닝 데이터셋은 수천~수만 건으로 작기 때문에, 프리트레이닝 대비 훨씬 빠르게 train loss는 떨어지지만 validation loss는 특정 지점부터 반등한다. 이 반등 지점을 넘긴 체크포인트를 배포하면 벤치마크 점수는 높아 보여도 실제 분포 밖 입력에서 급격히 무너진다.
과적합의 신호를 어떻게 읽는가
단일 지표로 판단하지 말고 train/validation loss의 갭과 추세를 함께 본다. 아래는 전형적인 진단 패턴이다.
| 패턴 | train loss | val loss | 해석 |
|---|---|---|---|
| 정상 수렴 | 완만히 하강 | 함께 하강 | 학습 지속 가능 |
| 과적합 | 계속 하강 | 바닥 후 상승 | 조기 종료 필요 |
| 학습률 과대 | 진동/발산 | 불안정 | LR 하향, 워밍업 추가 |
| 과소적합 | 높게 정체 | 높게 정체 | LR 상향, 에폭 증가 |
조기 종료(Early Stopping) 구현
핵심은 patience(개선 없는 평가 횟수)와 min_delta(유의미한 개선 폭)를 명시하는 것이다. validation loss 기준으로 best 체크포인트를 저장하고, 그 시점으로 롤백해 배포한다.
from transformers import TrainingArguments, EarlyStoppingCallback
args = TrainingArguments(
output_dir="out",
learning_rate=1e-5, # 파인튜닝은 프리트레이닝보다 1~2자릿수 낮게
num_train_epochs=5, # 상한만 크게, 실제 종료는 콜백이 결정
eval_strategy="steps",
eval_steps=50,
save_strategy="steps",
save_steps=50,
load_best_model_at_end=True, # best 체크포인트로 롤백
metric_for_best_model="eval_loss",
greater_is_better=False,
warmup_ratio=0.06,
weight_decay=0.01,
)
callbacks = [EarlyStoppingCallback(
early_stopping_patience=3, # 3회 연속 개선 없으면 종료
early_stopping_threshold=1e-3,
)]
학습률 스케줄과 워밍업
고정 학습률보다 워밍업 후 코사인 감쇠가 안정적이다. 초반 워밍업은 스텝 초기의 큰 그래디언트로 사전학습 가중치가 파괴되는 것을 막고, 후반 감쇠는 최적점 근처의 진동을 줄인다. LoRA 등 어댑터 방식은 전체 파인튜닝보다 학습률을 높게(예: 1e-4~2e-4) 잡아도 base 가중치가 고정되어 있어 과적합 위험이 상대적으로 낮다.
lr_scheduler_type: cosine
warmup_ratio: 0.06
learning_rate: 0.0001 # LoRA 기준
num_train_epochs: 8
per_device_train_batch_size: 8
gradient_accumulation_steps: 4 # 실효 배치 32
evaluation:
eval_steps: 40
early_stopping_patience: 4
검증셋 설계의 함정
조기 종료는 validation셋의 품질에 전적으로 의존한다. 학습셋과 분포가 겹치거나(중복 문장, 동일 소스) 크기가 너무 작으면 val loss가 노이즈로 진동해 잘못된 시점에 종료된다. 최소 수백 건 이상을 확보하고, 시간·소스·라벨 분포를 실제 서비스 트래픽과 맞춘다. 데이터 누수 여부는 아래처럼 해시로 교차 확인한다.
import hashlib
def norm_hash(t):
return hashlib.md5(t.strip().lower().encode()).hexdigest()
train_h = {norm_hash(x) for x in train_texts}
leak = [x for x in val_texts if norm_hash(x) in train_h]
print(f"leaked samples: {len(leak)}") # 0이 아니면 검증 신뢰도 하락
실무 튜닝 순서와 주의점
권장 순서는 (1) 누수 없는 검증셋 확보 → (2) 낮은 학습률에서 워밍업+감쇠 적용 → (3) 에폭 상한은 넉넉히 두고 조기 종료에 맡김 → (4) best 체크포인트로 롤백 배포다. 주의할 점은 세 가지다. 첫째, patience를 너무 작게 잡으면 일시적 진동을 과적합으로 오인해 조기 종료한다. 둘째, eval_steps가 배치 대비 지나치게 촘촘하면 평가 비용이 학습 시간을 압도한다. 셋째, val loss가 개선돼도 실제 다운스트림 지표(정확도·F1·사람 평가)는 다를 수 있으므로, 최종 선택은 loss가 아닌 태스크 지표로 재검증한다.