지난번에는 데이터 준비부터 학습 및 평가까지, 신경망을 만드는 절차를 살펴보았습니다.
학습용 데이터를 사용하여 가중치를 업데이트하고, 검증용 데이터로 중간 과정을 확인합니다. 그리고 마지막으로 테스트용 데이터로 성능을 확인하는 흐름이었습니다.
이번에는 그 과정에서 조금 다루었던 **과적합(Overfitting)**에 대해 살펴보겠습니다.
학습을 계속하면 학습용 데이터에 대한 손실은 작아집니다. 그렇다면 가능한 한 오래 학습시키는 것이 좋을 것 같다고 생각할 수 있습니다.
'학습을 계속하면 처음 보는 데이터에서도 예측이 잘 될까?'
사실, 그렇지 않을 수도 있습니다.
이번에는 작은 실험을 실행하여 학습 곡선과 예측의 형태를 통해 그 이유를 확인해 보겠습니다.
과적합이란 학습용 데이터에 너무 맞춰져서, 학습에 사용하지 않은 데이터에서는 제대로 예측할 수 없게 되는 현상입니다.
예를 들어, 연습 문제의 답만 통째로 암기한 상태를 상상해 보세요. 연습했던 문제가 그대로 출제되면 답할 수 있습니다. 하지만 숫자나 듣는 방식이 조금 바뀌면 풀지 못할 수도 있습니다.
신경망에서도 학습용 데이터에 포함된 공통적인 규칙뿐만 아니라, 그 데이터 고유의 변동성까지 모두 맞춰버리는 경우가 생깁니다.
반면에, 학습에 사용하지 않은 데이터에도 대응할 수 있는 것을 **일반화(Generalization)**라고 부릅니다.
우리가 목표로 하는 것은 새로운 데이터에서도 유용한 예측을 할 수 있는 상태입니다.
과적합과 일반화의 기본적인 관계는 Google의 머신러닝 입문 자료에서도 설명되어 있습니다. 여기서 지난번의 데이터 분할 방법을 되돌아보겠습니다.
| 데이터 | 역할 |
|---|
| 학습용 데이터 | 기울기(Gradient)를 계산하고, 가중치와 편향을 업데이트함 |
| ... | |
| 학습용 데이터에 대한 손실만 보고 있으면, 그 데이터에 얼마나 잘 맞는지만 알 수 있습니다. | |
그래서 학습용과 검증용의 손실을 나란히 확인합니다.
학습 중에 검증용 정답도 참조하지만, 이는 성능을 측정하기 위함입니다. 검증용 데이터의 손실로 가중치를 업데이트하는 것은 아닙니다.
이번 학습 곡선은 가로축을 에포크(Epoch) 수, 세로축을 손실로 한 그래프입니다.
에포크는 학습용 전체 데이터를 1회 순환하는 단위를 의미했습니다.
그래프에는 다음 두 개의 선이 그려집니다.
- 학습용 데이터에 대한 손실
- 검증용 데이터에 대한 손실
이번에는 둘 다 지난번과 동일한 평균 제곱 오차(MSE)를 사용합니다. 같은 데이터를 비교한다면, 작을수록 예측이 정답에 가까운 값입니다.
대략적인 해석은 다음과 같습니다.
| 곡선의 변화 | 읽어낼 수 있는 것 |
|---|
| 학습용도 검증용도 하락 | 새로운 데이터에 대한 예측도 개선되고 있음 |
| ... | |
| 두 선에 차이가 있다는 것만으로 과적합이라고 단정할 필요는 없습니다. | |
데이터의 난이도나 개수가 다르면 손실에도 차이가 발생합니다.
학습을 진행했을 때, 각각의 손실이 어떻게 변화하는지를 살펴보겠습니다.
지난번과 마찬가지로 다음 관계를 사용하여 데이터를 만듭니다.
정답 = 입력 × 2 + 1 + 무작위 변동성
이번에는 과적합을 관찰하기 쉽도록, 적은 학습 데이터에 대해 지난번보다 큰 모델을 준비합니다.
| 항목 | 지난번 | 이번 |
|---|---|
| 학습용 데이터 | 180건 | 20건 |
| ... |
이것은 과적합을 관찰하기 위한 설정입니다. 실제 개발에서 학습용 데이터를 이 비율로 하는 것을 권장하는 것은 아닙니다.
이번에 추가되는 변동성은 입력과 독립적으로 만들어지는 무작위 값입니다. 따라서, 입력만으로는 새로운 데이터에 더해질 변동성을 정확하게 맞출 수 없습니다.
예를 들어, 비슷한 정도의 입력이라도 우연히 정답 값이 위로 치솟거나 아래로 떨어질 수 있습니다.
표현력이 높은 모델은 이러한 미세한 변화에도 맞춰질 수 있습니다. 학습용 데이터의 손실은 작게 만들 수 있지만, 그 맞추는 방식이 다른 데이터에서도 유용할 것이라고 장담할 수는 없습니다.
다음 환경에서 GPU 실행과 CPU 실행을 확인했습니다.
| 항목 | 확인한 환경 |
|---|
| Python | 3.10.6 |
| ... | |
| 지난번의 PyTorch 환경을 사용합니다. | |
이번에는 그래프를 그리기 위해 Matplotlib도 사용합니다. 설치되어 있지 않다면, 다음 명령어로 추가해 주세요.
python -m pip install matplotlib==3.10.9
코드는 CUDA를 이용할 수 있는 경우 GPU를 사용하고, 사용할 수 없는 경우는 CPU를 사용합니다. 외부 데이터 다운로드는 필요 없습니다.
모델은 다음 구성을 갖습니다.
입력 1개
↓
은닉층(Hidden Layer) 뉴런 64개 + ReLU
...
이번에는 20개 샘플 전체를 한 번에 사용하므로, 에포크(epoch)당 가중치를 한 번 업데이트합니다.
이전처럼 하나의 에포크 내에서 미니 배치(mini-batch)를 여러 번 가져오는 형태는 아닙니다.
업데이트 방법으로는 Adam을 사용합니다. 이는 SGD와 마찬가지로 기울기(gradient)를 사용하는 방식이지만, 과거의 기울기 정보도 활용하여 업데이트 양을 조정합니다.
여기서는 업데이트 방법 비교는 하지 않고, 동일한 설정으로 학습을 계속했을 때의 변화를 관찰합니다.
아래 코드를 neural_network_overfitting.py로 저장해 주세요.
"""학습 곡선과 예측 형태로부터 과적합(Overfitting)을 관찰하는 입문 예제"""
from copy import deepcopy
import csv
...
evaluate() 함수에서는 가중치를 업데이트하지 않고 MSE를 계산하고 있습니다. 핵심은 학습용과 검증용 모두 해당 에포크의 업데이트가 끝난 시점의 모델로 평가한다는 점입니다.
또한, 검증 MSE가 최소가 된 시점의 상태를 deepcopy(model.state_dict())로 저장합니다. 이는 이후 학습 과정에서 해당 상태까지 덮어쓰여지지 않도록 하기 위함입니다. 이 처리 방식은 PyTorch의 저장 및 불러오기(save/load) 설명에서도 소개된 내용입니다.
저장한 폴더에서 다음 명령어를 실행합니다.
python neural_network_overfitting.py
스크립트와 같은 위치에 overfitting_assets 폴더가 생성되고, 다음 파일들이 저장됩니다.
learning_curve.png: 학습용과 검증용의 손실(loss) 변화 -
prediction_comparison.png: 중간 모델과 최종 모델의 예측 차이 -
learning_history.csv: 에포크별 MSE 기록
그래프는 창으로 표시하지 않고 이미지로 저장합니다.
아래는 GPU로 실행한 결과입니다.
Python: 3.10.6
PyTorch: 2.10.0+cu130
Matplotlib: 3.10.9
...
난수 시드(random seed)를 고정했지만, CPU와 GPU, 라이브러리 버전에 따라 미세한 수치는 달라질 수 있습니다. 같은 수치가 되는 것보다, 학습용과 검증용 손실의 변화에 주목해 주십시오.
다음 그림은 실행 중에 기록된 MSE를 그대로 그린 것입니다. 왼쪽이 3,000 에포크 전체이고, 오른쪽이 처음 300 에포크를 확대한 그래프입니다.
- 녹색 선: 학습용 데이터의 MSE
- 주황색 선: 검증용 데이터의 MSE
- 파란색 점선: 검증 MSE가 최소가 된 에포크
세로축은 작은 값의 변화도 볼 수 있도록 로그 스케일(log scale)로 설정했습니다. 10⁰는 1, 10⁻¹는 0.1, 10⁻²는 0.01입니다.
학습이 진행됨에 따라 녹색 선은 전체적으로 하락하고 있습니다. 반면, 주황색 선은 처음에 하락한 후 거의 수평을 유지하다가 후반에는 약간 악화되고 있습니다.
수치를 비교하면 차이가 더 명확하게 보입니다.
| 모델 | 학습 MSE | 검증 MSE |
|---|
| 검증에서 선택한 48 에포크째 | 0.052015 | 0.102445 |
| 마지막 3,000 에포크째 | 0.009335 | 0.124815 |
3,000 에포크째가 학습용 데이터에서는 더 잘 맞습니다. 하지만 검증용 데이터에서는 48 에포크째보다 오차가 커지고 있습니다.
학습용 데이터에 대한 적합도가 높아져도, 다른 데이터에서의 예측이 좋아진다는 보장은 없습니다.
이번 결과에서는 이러한 불일치(discrepancy)가 과적합의 징후로 나타나고 있습니다. 참고로, 도중에 손실이 일시적으로 급등할 수도 있습니다. 한 번의 상승과 하강만 보고 판단하지 말고, 여러 에포크에 걸친 경향을 확인합니다.
다음으로, 입력을 0부터 1까지 조금씩 변화시키면서 각 모델이 무엇을 예측하는지 그립니다.
| 그림 요소 | 의미 |
|---|
| 회색 점 | 학습용 20개 입력 및 정답 |
| 회색 점선 | 데이터를 만들 때의, 산포 중심 관계 2x + 1 |
| 파란색 선 | 검증에서 선택한 48 에포크째 모델의 예측 |
| 주황색 선 | 3,000 에포크째 모델의 예측 |
파란색 선은 대체로 직선에 가까운 형태를 보입니다. 주황색 선은 학습용 점에 가깝게 세밀하게 구불거리고 있습니다. 학습용 점을 보면 주황색 선이 더 잘 맞는 것처럼 보일 수 있습니다. 하지만 새로운 데이터의 산포는 학습용 점과 같은 위치에 나타나지 않을 수도 있습니다.
이번에는 미세한 오차에 맞춰 예측 형태가 변하는 반면, 검증 MSE는 악화되고 있습니다.
이 두 가지를 함께 보면 과적합(Overfitting)의 이미지를 파악하기 쉽습니다.
다만, 선이 휘어져 있을 뿐이라고 해서 반드시 과적합인 것은 아닙니다. 본래 관계가 곡선이라면, 곡선으로 예측하는 것이 필요합니다.
겉모습뿐만 아니라, 학습에 사용하지 않은 데이터에서의 결과까지 확인하는 것이 중요합니다.
회색 점선은 이 인공 데이터를 설명하기 위해 그려진 것입니다. 모델의 예측이나 채택할 에포크(epoch) 선택에는 사용되지 않습니다.
이번 코드에서는 검증 MSE가 최소였던 48 에포크째의 모델을 선택했습니다. 이후 처음으로 테스트용 200건으로 평가를 진행했습니다. 테스트 MSE는 0.115580이었습니다.
여기서 순서는 다음과 같습니다:
학습 데이터로 가중치를 업데이트한다
↓
검증 데이터로 모델을 선택한다
...
'48 에포크가 최적'이라고 모든 문제에 적용할 수 있는 것은 아닙니다. 이번 데이터와 모델, 설정에서 검증 MSE가 최소였던 것이 48 에포크째라는 결과일 뿐입니다.
또한, 테스트 MSE를 보고 학습 횟수나 설정을 여러 번 수정하면, 테스트 데이터까지 선택에 사용하게 됩니다. 조정에는 검증용 데이터를 사용하고, 테스트용 데이터는 마지막 확인을 위해 남겨두어야 합니다.
지금까지의 실험에서는 후반부 변화를 관찰하기 위해 3,000 에포크 전체를 실행했습니다. 실제로는 개선이 보이지 않게 된 단계에서 학습을 멈추는 방법도 있습니다. 이를 **Early Stopping(조기 종료)**이라고 부릅니다.
예를 들어, 다음과 같은 규칙입니다:
검증 MSE가 이전의 최소값을 갱신했다
→ 가중치를 유지한다
→ 개선하지 않은 횟수를 0으로 되돌린다
...
이 '개선을 기다리는 길이'는 일반적으로 patience라고 불립니다. 한 번 나빠졌다고 바로 멈추지 않고, 일정 기간의 상태를 보기 위한 설정입니다.
생각 방식은 Early Stopping 공식 문서에서도 확인할 수 있습니다. 이번 기록에 이 규칙을 적용하면, 48 에포크 이후 100회 개선이 없기 때문에 148 에포크에서 멈추게 됩니다. 이때 사용되는 가중치는 멈춘 148 에포크째가 아니라, 유지해 두었던 48 에포크째의 것입니다.
'학습을 멈춘 시점'과 '채택하는 모델의 시점'은 같지 않을 수 있습니다.
이는 이번 기록에 대한 해석이며, 게재된 코드 자체에는 조기 종료를 넣지 않았습니다. 또한, 기다리는 횟수를 너무 작게 설정하면, 그 이후에 발생할 개선을 기다리지 않고 멈추는 경우가 생길 수 있습니다.
학습 횟수를 재검토하는 것 외에도 대책은 몇 가지가 더 있습니다.
| 대책 | 생각 방식 |
|---|
| 학습 데이터를 늘린다 | 실제로 사용하는 상황과 가까운, 다양한 예시를 모은다 |
| ... | |
| 예를 들어 L2 정규화(regularization)는 가중치가 커지는 것에 페널티를 주는 방법입니다. Dropout은 학습 시 일부 출력을 무작위로 0으로 만드는 방법입니다. | |
하지만 대책을 강하게 할수록 좋은 것은 아닙니다. 모델을 너무 작게 만들어서 필요한 관계까지 배울 수 없게 되면, **과소적합(Underfitting)**에 연결될 수도 있습니다.
학습용과 검증용 양쪽을 보면서 변경이 도움이 되고 있는지 확인해야 합니다.
검증용 성적이 나쁠 때, 원인이 반드시 과적합인 것은 아닙니다. 예를 들어, 학습용은 밝은 장소에서 촬영한 이미지들만 있고, 검증용은 어두운 장소의 이미지들만 있다면, 데이터 조건 자체가 다릅니다.
입력과 정답의 조합이 어긋나 있거나, 학습용과 검증용에서 다른 전처리를 했거나 할 수도 있습니다. 우선 다음 점을 확인합니다:
- 입력과 정답이 올바르게 대응하는지
- 학습용과 검증용에 실제로 다루는 데이터의 특징이 포함되어 있는지
- 학습용과 검증용에서 동일한 기준의 전처리를 적용했는지
- 평가할 때
model.eval()로 전환했는지
특히 Dropout 등을 사용하는 모델은 학습 시와 평가 시 동작이 달라집니다.
이번 모델은 Linear와 ReLU만 사용했지만, 향후 구현에서도 평가 모드로 전환하는 것을 잊지 않도록 합시다.
이번에는 과적합에 대해 학습 곡선과 예측 형태를 통해 확인했습니다. 학습을 계속하면서 학습용 데이터의 손실은 작아졌습니다. 하지만 검증용 데이터에서의 예측은 중간 모델 쪽이 더 좋은 결과를 보였습니다.
중요한 것은, 학습용 데이터에 얼마나 맞추었는지뿐만 아니라, 새로운 데이터에서도 유용한지 확인하는 것입니다.
다음 시간에는 이번에 다룬 '정규화'에 대해 가중치 제약이나 Dropout이 어떻게 작동하는지를 좀 더 자세히 살펴볼 예정입니다.
다시 천천히 글을 쓰겠습니다.
이 글에서는 샘플 코드 작성에 생성 AI를 사용했습니다.
_______
주식회사 ONE WEDGE는 웹 시스템 개발, SES(System Engineering Service), AI/DX 지원을 수행하는 IT 기업입니다.
생성 AI를 활용한 업무 효율화와 차세대 시스템 개발에도 주력하고 있으며, 기업의 과제 해결뿐만 아니라 엔지니어 개개인의 성장에도 진심으로 임하고 있습니다.
또한, 기술은 '혼자 배우는 것'이 아니라 '동료와 함께 성장하는 것'이라고 생각하여 사내외 커뮤니티 조성에도 힘쓰고 있습니다.