잘 맞히는 모델과 잘 외운 모델은 다르다
이전 장들에서 우리는 숫자 이미지, 이미지 분류, 순서 데이터, 텍스트 분류, 임베딩까지 차례로 살펴보았다. 이제 모델을 만드는 일에는 어느 정도 익숙해졌다. 데이터를 준비하고, 모델을 만들고, 손실을 계산하고, 역전파로 가중치를 수정한다. 이 흐름을 반복하면 훈련 손실은 대체로 내려간다.
그런데 여기서 아주 중요한 질문이 등장한다.
훈련 데이터에서 손실이 낮아졌다면, 정말 좋은 모델이 된 것일까?
아쉽게도 답은 항상 "그렇다"가 아니다. 딥러닝 모델은 생각보다 영리하지만, 동시에 생각보다 쉽게 꾀를 부린다. 우리가 원하는 것은 숫자 7의 모양을 이해하는 모델이다. 하지만 모델은 훈련 데이터에 나온 특정 7들의 픽셀 배치를 외워 버릴 수도 있다. 우리가 원하는 것은 영화 리뷰의 감정을 파악하는 모델이다. 하지만 모델은 훈련 데이터에 자주 등장한 특정 단어 조합만 기억하고 있을 수도 있다.
이처럼 모델이 훈련 데이터에는 매우 잘 맞지만, 처음 보는 데이터에는 약한 상태를 과적합(overfitting)이라고 한다. 반대로 모델이 훈련 데이터의 패턴조차 충분히 배우지 못한 상태를 과소적합(underfitting)이라고 한다.
좋은 모델은 훈련 데이터를 완벽하게 외우는 모델이 아니다. 좋은 모델은 훈련 데이터에서 배운 규칙을 바탕으로, 보지 못한 데이터에도 그럴듯하게 작동하는 모델이다. 이것을 일반화(generalization)라고 부른다.
딥러닝에서 성능을 높인다는 말은 결국 이 일반화 성능을 높인다는 뜻에 가깝다.

과적합은 언제 나타날까
학습 루프를 만들 때 우리는 보통 데이터를 두 부분으로 나눈다. 하나는 모델이 직접 보고 학습하는 훈련 데이터이고, 다른 하나는 모델이 학습 중에 직접 정답을 보고 가중치를 수정하지 않는 검증 데이터다. 검증 데이터는 모델에게 일종의 모의고사다.
훈련 손실은 모델이 공부한 문제집에서 얼마나 잘 맞히는지를 보여 준다. 검증 손실은 처음 보는 비슷한 문제에서 얼마나 잘하는지를 보여 준다.
학습 초반에는 보통 훈련 손실과 검증 손실이 함께 내려간다. 모델이 정말로 유용한 패턴을 배우고 있기 때문이다. 예를 들어 손글씨 숫자 분류 모델이 처음에는 아무 숫자나 찍다가, 점점 획의 방향과 위치를 이해하게 되는 과정이다.
그러다 어느 순간부터 이상한 일이 벌어진다. 훈련 손실은 계속 내려가는데 검증 손실은 더 이상 내려가지 않거나 오히려 올라간다. 이때 과적합을 의심해야 한다.
for epoch in range(num_epochs):
train_loss = train_one_epoch(model, train_loader, optimizer, loss_fn)
val_loss = evaluate(model, val_loader, loss_fn)
print(f"epoch={epoch}, train_loss={train_loss:.4f}, val_loss={val_loss:.4f}")출력이 다음과 같다고 해 보자.
epoch=0, train_loss=1.25, val_loss=1.30
epoch=1, train_loss=0.82, val_loss=0.90
epoch=2, train_loss=0.50, val_loss=0.62
epoch=3, train_loss=0.31, val_loss=0.58
epoch=4, train_loss=0.18, val_loss=0.71
epoch=5, train_loss=0.09, val_loss=0.95처음에는 둘 다 내려간다. 하지만 3번째 에폭 이후부터 훈련 손실은 계속 좋아지는데 검증 손실은 나빠진다. 모델이 훈련 데이터의 세부 사항을 점점 더 강하게 외우기 시작했다는 신호다.
이 현상을 그래프로 그리면 더 분명하다. 훈련 손실 곡선은 아래로 내려가는데, 검증 손실 곡선은 어느 지점부터 위로 고개를 든다. 그 갈라지는 순간이 바로 "이제 그만 외워도 된다"는 경고등이다.
정확도에서도 비슷한 일이 일어난다. 훈련 정확도는 99%인데 검증 정확도는 75%라면, 모델이 진짜 실력을 갖춘 것인지 다시 확인해야 한다. 시험 전날 친구가 문제와 답을 통째로 외워서 연습 문제는 전부 맞혔지만, 숫자만 조금 바뀐 문제에서 막히는 상황과 같다.
모델은 왜 외우고 싶어 할까
모델은 의도를 가진 존재가 아니다. 하지만 학습 과정만 보면 마치 외우려는 성향이 있는 것처럼 보인다. 이유는 간단하다. 모델의 목표가 훈련 손실을 낮추는 것이기 때문이다.
우리가 지금까지 반복해서 보았던 학습 흐름을 떠올려 보자.
모델이 예측한다. 틀린 정도를 손실로 계산한다. 어느 가중치가 오차에 얼마나 영향을 줬는지 역전파로 확인한다. 가중치를 조금 수정한다. 그리고 다시 반복한다.
이 과정 자체는 매우 합리적이다. 문제는 훈련 데이터가 세상의 모든 데이터를 대표하지 않는다는 데 있다. 훈련 데이터 안에는 진짜 패턴도 있지만 우연한 흔적도 있다. 사진의 배경, 특정 글씨체, 촬영 각도, 데이터 수집 방식, 자주 등장한 단어 조합 같은 것들이 그렇다.
예를 들어 고양이와 강아지를 분류하는 이미지 모델을 학습한다고 해 보자. 만약 훈련 데이터의 고양이 사진 대부분이 실내에서 찍히고, 강아지 사진 대부분이 잔디밭에서 찍혔다면 모델은 동물의 귀와 얼굴을 보는 대신 배경을 이용할 수 있다. 훈련 데이터에서는 잘 맞는다. 하지만 잔디밭 위의 고양이 사진을 만나면 당황한다.
텍스트 모델도 마찬가지다. 긍정 리뷰에 "완전"이라는 단어가 자주 나오고 부정 리뷰에 "진짜"라는 단어가 자주 나왔다면, 모델은 문장의 전체 의미보다 특정 단어의 출현에 과도하게 기대게 될 수 있다. 그러면 "진짜 재미있다" 같은 문장을 잘못 판단할 수 있다.
과적합은 모델이 너무 많이 배워서 생기는 문제가 아니다. 더 정확히 말하면, 모델이 배워야 할 것과 외우지 말아야 할 것을 구분하지 못할 때 생긴다.
모델 크기, 데이터 양, 학습 횟수
과적합을 이해할 때 특히 중요한 세 가지 요소가 있다. 모델 크기, 데이터 양, 학습 횟수다. 이 셋은 서로 연결되어 있다.
모델 크기는 모델이 가진 표현 능력과 관련 있다. 파라미터가 많은 모델은 더 복잡한 함수를 표현할 수 있다. 깊은 신경망, 많은 뉴런, 큰 임베딩 차원은 모델에게 더 큰 기억장치를 주는 것과 비슷하다. 이 능력은 좋은 방향으로 쓰이면 복잡한 패턴을 잡아낸다. 하지만 데이터가 부족하면 훈련 샘플 하나하나의 우연한 특징까지 외우는 데 쓰일 수 있다.
데이터 양은 모델이 세상을 얼마나 다양하게 보았는지를 결정한다. 같은 숫자 3이라도 사람마다 쓰는 방식이 다르다. 같은 "좋다"라는 감정도 문장마다 표현이 다르다. 데이터가 많고 다양할수록 모델은 특정 예시에만 맞는 규칙보다 넓게 통하는 규칙을 배울 가능성이 높아진다.
학습 횟수는 모델이 훈련 데이터에 얼마나 오래 머무는지를 뜻한다. 에폭이 많을수록 모델은 같은 데이터를 반복해서 본다. 적당히 보면 패턴을 익힌다. 너무 오래 보면 세부사항까지 외운다. 시험공부도 비슷하다. 개념을 이해할 만큼 반복하는 것은 좋지만, 특정 문제의 답 번호만 외우기 시작하면 새로운 문제에 약해진다.
이 관계를 아주 단순하게 말하면 이렇다. 큰 모델은 강력하지만 쉽게 외울 수 있다. 데이터가 많으면 외우기보다 일반 규칙을 배우기 쉬워진다. 학습을 너무 오래 하면 일반 규칙을 넘어 훈련 데이터의 잡음까지 따라갈 수 있다.
그래서 딥러닝 실험에서는 "모델을 크게 만들면 무조건 좋다"라고 말하기 어렵다. 모델 크기는 데이터 양, 문제 난이도, 정규화 기법, 학습 시간과 함께 봐야 한다.
정규화란 무엇인가
과적합을 줄이기 위한 여러 방법을 통틀어 정규화(regularization)라고 부른다. 여기서 정규화는 데이터를 평균 0, 표준편차 1로 맞추는 정규화(normalization)와 구분해야 한다. 한국어로는 둘 다 정규화라고 번역되는 경우가 있어 헷갈리지만, 이 장에서 말하는 정규화는 모델이 훈련 데이터에 지나치게 달라붙지 않도록 제약을 주는 방법이다. 때로는 규제라고 부르기도 한다.
정규화의 핵심은 모델에게 이렇게 말하는 것이다.
"훈련 데이터를 잘 맞히는 것은 좋다. 하지만 너무 복잡한 방식으로 맞히지는 마라."
모델이 훈련 데이터를 맞히는 방법은 여러 가지다. 단순하고 넓게 통하는 규칙으로 맞힐 수도 있고, 매우 복잡한 꼼수로 맞힐 수도 있다. 정규화는 보통 후자를 덜 선호하도록 학습 과정에 작은 압력을 넣는다.
대표적인 정규화 방법에는 가중치 감쇠, 드롭아웃, 데이터 증강, 조기 종료 등이 있다. 이 장에서는 그중 가중치 감쇠와 데이터 증강을 먼저 살펴본다. 드롭아웃과 배치 정규화는 다음 장들에서 더 자세히 다룰 것이다.
가중치 감쇠: 너무 큰 확신을 누그러뜨리기
신경망의 예측은 가중치에 의해 만들어진다. 어떤 입력 특징에 큰 가중치가 붙으면, 그 특징은 예측에 큰 영향을 미친다. 물론 중요한 특징에는 큰 가중치가 필요할 수 있다. 하지만 일부 가중치가 지나치게 커지면 모델이 특정 신호에 과도하게 의존할 위험이 있다.
예를 들어 텍스트 분류 모델이 어떤 단어 하나에 너무 큰 가중치를 부여한다고 해 보자. 그 단어가 나오면 거의 무조건 긍정이라고 판단한다. 훈련 데이터에서는 우연히 잘 맞을 수 있다. 하지만 실제 문장에서는 같은 단어가 비꼼, 부정, 질문 속에 등장할 수도 있다. 너무 큰 가중치는 모델의 판단을 뻣뻣하게 만든다.
가중치 감쇠(weight decay)는 가중치가 지나치게 커지는 것을 억제하는 방법이다. 손실을 계산할 때 예측이 틀린 정도만 보는 것이 아니라, 가중치의 크기도 함께 고려한다. 가중치가 커질수록 약간의 벌점을 주는 방식이다.
개념적으로는 손실 함수가 다음처럼 바뀐다고 볼 수 있다.
전체 손실 = 예측 손실 + 가중치 크기에 대한 벌점여기서 예측 손실은 우리가 이미 배운 교차 엔트로피나 평균제곱오차 같은 값이다. 가중치 크기에 대한 벌점은 보통 가중치 제곱의 합에 비례한다. 가중치가 조금 클 때보다 아주 클 때 벌점이 훨씬 커진다. 그래서 모델은 훈련 데이터를 맞히려고 하면서도, 가능하면 너무 큰 가중치를 만들지 않는 방향을 찾게 된다.
경사하강법 관점에서 보면 가중치 감쇠는 매 업데이트마다 가중치를 살짝 줄이는 힘처럼 작동한다.
가중치 ← 가중치 - 학습률 × 예측 손실의 기울기 - 학습률 × 감쇠 강도 × 가중치마지막 항이 바로 가중치를 0쪽으로 조금 당기는 역할을 한다. 그래서 이름도 감쇠다. 가중치가 클수록 더 강하게 당겨지고, 작을수록 덜 당겨진다.
PyTorch에서는 옵티마이저를 만들 때 weight_decay 값을 지정할 수 있다.
optimizer = torch.optim.AdamW(
model.parameters(),
lr=1e-3,
weight_decay=1e-4
)weight_decay=0이면 가중치 감쇠를 쓰지 않는 것이다. 1e-4, 1e-5, 1e-3 같은 값이 실험의 출발점으로 자주 사용된다. 정답은 문제와 모델에 따라 달라진다. 너무 작으면 효과가 거의 없고, 너무 크면 모델이 필요한 패턴조차 충분히 배우지 못할 수 있다.
가중치 감쇠를 사용할 때 기억해야 할 점은 이것이다. 가중치 감쇠는 모델을 바보로 만드는 장치가 아니다. 모델이 훈련 데이터를 맞히되, 너무 날카롭고 복잡한 방법으로 맞히는 것을 막아 주는 안전장치다.
데이터 증강: 같은 의미를 다른 모습으로 보여 주기
과적합을 줄이는 또 다른 강력한 방법은 데이터를 더 많이 보여 주는 것이다. 하지만 실제로 새로운 데이터를 수집하는 일은 비용이 많이 든다. 사진을 더 찍고, 라벨을 붙이고, 검수하는 과정은 쉽지 않다.
그래서 우리는 기존 데이터를 살짝 변형해서 새로운 예시처럼 사용할 수 있다. 이것을 데이터 증강(data augmentation)이라고 한다.
이미지 분류를 생각해 보자. 고양이 사진을 좌우로 살짝 뒤집어도 여전히 고양이다. 사진을 조금 자르거나, 밝기를 약간 바꾸거나, 위치를 조금 이동해도 라벨은 변하지 않는다. 이런 변형을 학습 중에 무작위로 적용하면 모델은 같은 대상을 여러 모습으로 보게 된다.
이 과정은 모델에게 중요한 메시지를 준다.
"고양이는 꼭 사진 중앙에만 있지 않아도 고양이다. 조금 어두워도 고양이다. 좌우가 바뀌어도 고양이다."
즉 데이터 증강은 모델이 중요하지 않은 변화에 덜 흔들리도록 돕는다. 배경의 작은 차이, 위치의 변화, 밝기의 변화 같은 것에 집착하지 않고 더 본질적인 특징을 보게 만든다.
PyTorch의 torchvision.transforms를 사용하면 이미지 증강을 쉽게 적용할 수 있다.
from torchvision import transforms
train_transform = transforms.Compose([
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomCrop(32, padding=4),
transforms.ColorJitter(brightness=0.2, contrast=0.2),
transforms.ToTensor(),
])
val_transform = transforms.Compose([
transforms.ToTensor(),
])중요한 점은 훈련 데이터에만 무작위 증강을 적용한다는 것이다. 검증 데이터에는 보통 고정된 변환만 적용한다. 그래야 검증 성능을 안정적으로 비교할 수 있다.
또 하나 중요한 원칙이 있다. 증강은 라벨을 바꾸지 않는 범위에서 해야 한다. 고양이 사진을 좌우 반전하는 것은 대체로 괜찮다. 하지만 손글씨 숫자 6을 180도 회전하면 9처럼 보일 수 있다. 의료 이미지나 위성 이미지처럼 방향 자체가 의미를 갖는 데이터에서는 좌우 반전도 조심해야 한다.
데이터 증강은 단순히 데이터 개수를 늘리는 꼼수가 아니다. 모델이 "이 정도 변화는 같은 것으로 봐도 된다"는 감각을 학습하게 만드는 방법이다. 그래서 이미지 모델의 일반화 성능을 높이는 데 특히 효과적이다.
조기 종료: 가장 좋을 때 멈추기
과적합은 학습을 너무 오래 했을 때 자주 나타난다. 그렇다면 가장 단순한 해결책은 적절한 시점에 멈추는 것이다. 이를 조기 종료(early stopping)라고 한다.
조기 종료는 훈련 손실이 아니라 검증 손실을 기준으로 삼는다. 검증 손실이 계속 좋아지는 동안에는 학습을 이어 간다. 하지만 일정 기간 동안 더 이상 좋아지지 않으면 학습을 멈춘다. 그리고 검증 성능이 가장 좋았던 시점의 모델을 사용한다.
best_val_loss = float("inf")
patience = 3
bad_count = 0
for epoch in range(num_epochs):
train_loss = train_one_epoch(model, train_loader, optimizer, loss_fn)
val_loss = evaluate(model, val_loader, loss_fn)
if val_loss < best_val_loss:
best_val_loss = val_loss
bad_count = 0
torch.save(model.state_dict(), "best_model.pt")
else:
bad_count += 1
if bad_count >= patience:
print("Early stopping!")
break여기서 patience는 몇 번의 에폭 동안 개선이 없을 때 기다릴지를 뜻한다. 검증 손실은 조금씩 출렁일 수 있으므로, 한 번 나빠졌다고 바로 멈추기보다는 몇 번 더 지켜보는 편이 안전하다.
조기 종료는 매우 실용적인 방법이다. 모델 구조를 바꾸지 않아도 되고, 학습 루프에 검증 손실 기록만 추가하면 된다. 특히 작은 데이터셋에서는 큰 도움이 된다.
과적합을 줄이는 실험 순서
실제 프로젝트에서 검증 손실이 올라가기 시작하면 무엇부터 해야 할까? 처음에는 모든 기법을 한꺼번에 넣고 싶어진다. 하지만 그렇게 하면 무엇이 효과가 있었는지 알기 어렵다. 실험은 한 번에 하나씩 바꾸는 것이 좋다.
먼저 학습 곡선을 확인한다. 훈련 손실과 검증 손실이 함께 높다면 과적합보다 과소적합일 가능성이 있다. 이때는 모델이 너무 작거나, 학습이 부족하거나, 입력 표현이 좋지 않을 수 있다. 반대로 훈련 손실은 낮은데 검증 손실이 높다면 과적합 가능성이 크다.
그다음 모델 크기를 살펴본다. 데이터가 적은데 모델이 지나치게 크다면 레이어 수나 뉴런 수를 줄이는 것만으로도 검증 성능이 좋아질 수 있다. 모델을 작게 만드는 일은 단순하지만 강력한 정규화가 될 수 있다.
이어서 가중치 감쇠를 추가하거나 조정한다. 이미 사용 중이라면 값을 조금 바꿔 본다. 1e-5, 1e-4, 1e-3처럼 로그 스케일로 실험하면 차이를 보기 쉽다.
이미지 데이터라면 데이터 증강도 확인한다. 증강이 전혀 없다면 모델은 훈련 이미지의 위치, 밝기, 배경에 쉽게 적응해 버릴 수 있다. 적절한 증강은 모델을 더 튼튼하게 만든다.
마지막으로 학습 에폭과 조기 종료를 조정한다. 가장 좋은 검증 성능이 8번째 에폭에서 나왔는데 30번째 에폭 모델을 저장하고 있다면, 사실 좋은 모델을 지나쳐 버린 셈이다.
이 모든 과정에서 기준은 하나다. 훈련 데이터 점수가 아니라 검증 데이터 점수를 보아야 한다. 모델의 진짜 실력은 외운 문제집이 아니라 처음 보는 문제에서 드러난다.
정규화가 너무 강하면 생기는 일
정규화는 좋은 도구지만, 강할수록 무조건 좋은 것은 아니다. 가중치 감쇠가 너무 크면 모델은 필요한 가중치도 충분히 키우지 못한다. 데이터 증강이 너무 과하면 원래 이미지의 의미가 깨질 수 있다. 조기 종료가 너무 성급하면 모델이 아직 배울 기회를 얻기도 전에 멈춘다.
이때는 과적합이 아니라 과소적합이 나타난다. 훈련 손실과 검증 손실이 모두 높고, 훈련 정확도조차 낮다. 모델이 외우지 못해서 문제가 아니라, 애초에 충분히 배우지 못한 것이다.
따라서 좋은 정규화란 모델을 묶어 두는 족쇄가 아니라 균형을 잡아 주는 손잡이에 가깝다. 모델이 훈련 데이터의 패턴을 배우도록 허용하되, 우연한 잡음에 지나치게 달라붙지 않도록 도와야 한다.
작은 예: 손글씨 숫자 모델에서 과적합 보기
MNIST 같은 손글씨 숫자 데이터에서는 큰 모델을 오래 학습시키면 훈련 정확도가 매우 높게 나올 수 있다. 하지만 우리가 일부러 훈련 데이터 양을 줄이고 큰 MLP를 사용하면 검증 정확도와 차이가 벌어지는 모습을 관찰할 수 있다.
model = nn.Sequential(
nn.Flatten(),
nn.Linear(28 * 28, 1024),
nn.ReLU(),
nn.Linear(1024, 1024),
nn.ReLU(),
nn.Linear(1024, 10)
)
optimizer = torch.optim.AdamW(
model.parameters(),
lr=1e-3,
weight_decay=1e-4
)여기서 weight_decay를 0으로 두고 학습한 결과와 1e-4로 두고 학습한 결과를 비교해 보면 좋다. 훈련 정확도만 보면 가중치 감쇠가 없는 쪽이 더 좋아 보일 수도 있다. 하지만 검증 정확도에서는 가중치 감쇠를 쓴 쪽이 더 안정적일 수 있다.
이미지 데이터에서는 증강도 함께 실험할 수 있다. 숫자 이미지는 과도한 회전이나 뒤집기가 위험할 수 있으므로, 작은 이동이나 약한 회전처럼 라벨을 유지하는 변형을 조심스럽게 선택해야 한다.
train_transform = transforms.Compose([
transforms.RandomAffine(
degrees=10,
translate=(0.1, 0.1)
),
transforms.ToTensor(),
])숫자 3이 조금 왼쪽으로 이동해도 여전히 3이다. 5도가량 기울어져도 여전히 3이다. 이런 변형을 통해 모델은 숫자의 위치와 각도보다 획의 모양에 더 집중하게 된다.
LLM으로 가는 길에서 과적합이 중요한 이유
과적합은 작은 이미지 분류 모델에서만 중요한 문제가 아니다. 앞으로 만날 Transformer와 LLM에서도 일반화는 핵심 주제다. 거대한 언어 모델도 훈련 데이터에 등장한 문장을 단순히 기억할 위험이 있다. 우리가 원하는 것은 문장을 복사하는 모델이 아니라, 언어의 패턴을 배워 새로운 상황에 맞게 답하는 모델이다.
임베딩에서도 마찬가지다. 어떤 단어의 의미를 특정 문장 몇 개에만 맞춰 외우면 좋은 표현이 되기 어렵다. Attention에서도 마찬가지다. 모델이 문맥의 중요한 관계를 배우는 대신 훈련 데이터의 표면적 단서에만 반응한다면, 처음 보는 문장에서 쉽게 흔들린다.
결국 딥러닝의 목표는 훈련 데이터에 정답을 맞히는 것에서 끝나지 않는다. 훈련 데이터는 세상을 배우기 위한 표본일 뿐이다. 모델은 그 표본을 통해 더 넓은 규칙을 발견해야 한다.
이 장의 핵심은 단순하다. 훈련 손실이 낮다고 안심하지 말자. 검증 손실을 함께 보자. 모델이 문제집을 외우는지, 개념을 이해하는지 확인하자. 그리고 필요하다면 가중치 감쇠, 데이터 증강, 조기 종료 같은 방법으로 모델이 더 넓게 통하는 규칙을 배우도록 도와주자.
다음 장에서는 과적합을 줄이는 대표적인 기법인 드롭아웃을 살펴본다. 드롭아웃은 학습 중에 뉴런 일부를 일부러 꺼 버린다. 처음 들으면 이상하게 느껴진다. 열심히 만든 신경망의 일부를 왜 꺼야 할까? 하지만 바로 그 불편함 덕분에 모델은 특정 뉴런 하나에 지나치게 의존하지 않는 법을 배우게 된다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.