🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

A

AI Generated

2025. 12. 17. · 49 Views

과적합과 교차 검증 완벽 가이드

머신러닝 모델이 학습 데이터에만 과도하게 최적화되는 과적합 문제를 이해하고, 교차 검증을 통해 모델의 일반화 성능을 평가하는 방법을 실무 중심으로 알아봅니다. K-Fold 교차 검증과 정규화 기법까지 단계별로 학습합니다.


목차

  1. 과적합이란_무엇인가
  2. 학습_곡선_이해하기
  3. 교차_검증의_필요성
  4. K-Fold_교차_검증
  5. cross_val_score_사용법
  6. 과적합_방지_전략
  7. 정규화_기법_소개

1. 과적합이란 무엇인가

어느 날 데이터 분석팀에 입사한 지 2개월 차 김개발 씨가 자랑스럽게 선배에게 보고했습니다. "선배님, 제 모델이 학습 데이터에서 99% 정확도를 달성했어요!" 그런데 박시니어 씨의 표정이 밝지 않습니다.

"테스트 데이터로는 확인해봤어요?"

**과적합(Overfitting)**은 모델이 학습 데이터에만 지나치게 최적화되어 새로운 데이터에서는 성능이 떨어지는 현상입니다. 마치 시험 문제와 정답만 달달 외운 학생이 유형이 조금만 바뀌어도 문제를 풀지 못하는 것과 같습니다.

실무에서 가장 조심해야 할 함정 중 하나입니다.

다음 코드를 살펴봅시다.

from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score

# 데이터 분리: 학습용과 테스트용
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 과적합이 발생하기 쉬운 모델 생성
model = DecisionTreeClassifier(max_depth=None)  # 깊이 제한 없음
model.fit(X_train, y_train)

# 학습 데이터와 테스트 데이터의 정확도 비교
train_accuracy = accuracy_score(y_train, model.predict(X_train))
test_accuracy = accuracy_score(y_test, model.predict(X_test))

print(f"학습 데이터 정확도: {train_accuracy:.2f}")  # 0.99
print(f"테스트 데이터 정확도: {test_accuracy:.2f}")  # 0.75 - 큰 차이 발생!

김개발 씨는 자신의 모델이 학습 데이터에서 99%라는 놀라운 정확도를 보이자 매우 기뻤습니다. 드디어 머신러닝 모델을 제대로 만들었다는 성취감이 밀려왔습니다.

하지만 박시니어 씨의 반응은 예상과 달랐습니다. "테스트 데이터로 평가해보니까 정확도가 75%밖에 안 나오네요." 김개발 씨가 당황하며 다시 확인했습니다.

분명히 코드는 맞게 작성했는데, 왜 이런 결과가 나오는 걸까요? 과적합이란 정확히 무엇일까요?

쉽게 비유하자면, 과적합은 마치 수능 기출문제만 달달 외운 학생과 같습니다. 그 학생은 기출문제를 풀면 만점을 받지만, 실제 수능 시험장에서는 유형이 조금만 바뀌어도 당황하게 됩니다.

문제의 본질을 이해한 것이 아니라 정답을 암기한 것이기 때문입니다. 머신러닝 모델도 마찬가지입니다.

과적합이 발생하면 모델은 학습 데이터의 패턴뿐만 아니라 노이즈까지 학습하게 됩니다. 실제 의미 있는 패턴과 우연히 발생한 특이점을 구분하지 못하는 것입니다.

과적합이 없던 머신러닝 초창기에는 어땠을까요? 사실 과적합은 머신러닝이 발전하면서 자연스럽게 발견된 문제입니다.

초기 연구자들은 모델을 학습시킨 후 같은 데이터로 평가했습니다. 당연히 좋은 결과가 나왔습니다.

하지만 실제 서비스에 적용하면 성능이 형편없었습니다. 더 큰 문제는 모델이 복잡할수록 과적합이 심해진다는 점이었습니다.

의사결정나무의 깊이를 무한정 늘리거나, 다항식 회귀의 차수를 높이면 학습 데이터는 완벽하게 맞출 수 있습니다. 하지만 새로운 데이터에는 전혀 대응하지 못했습니다.

바로 이런 문제를 인식하기 위해 학습 데이터와 테스트 데이터를 분리하는 개념이 등장했습니다. 데이터를 분리하면 모델이 한 번도 보지 못한 데이터로 성능을 평가할 수 있습니다.

이것은 마치 학생이 연습 문제로 공부한 후 실전 시험을 보는 것과 같습니다. 무엇보다 과적합 여부를 객관적으로 판단할 수 있다는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 train_test_split 함수로 데이터를 80:20으로 분리합니다.

이 부분이 핵심입니다. 다음으로 max_depth=None으로 설정하여 의사결정나무가 제한 없이 깊어질 수 있게 만듭니다.

이렇게 하면 과적합이 발생하기 쉽습니다. 마지막으로 학습 데이터와 테스트 데이터의 정확도를 각각 계산하여 차이를 확인합니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 고객 이탈 예측 모델을 개발한다고 가정해봅시다.

과거 1년간의 고객 데이터로 모델을 학습시켰는데, 그 데이터로만 평가하면 95% 정확도가 나옵니다. 하지만 실제로 다음 달 고객들에게 적용하니 정확도가 70%로 떨어졌습니다.

이것이 바로 과적합입니다. 많은 기업에서 이런 실수를 겪은 후, 반드시 별도의 테스트 데이터로 모델을 검증하는 프로세스를 도입했습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 테스트 데이터를 모델 튜닝에 사용하는 것입니다.

"테스트 정확도를 높이기 위해 하이퍼파라미터를 조정하자"라고 생각하면, 결국 테스트 데이터에도 과적합이 발생합니다. 따라서 테스트 데이터는 최종 평가에만 사용해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.

"아, 학습 정확도만 보고 좋아하면 안 되는군요!" 과적합을 제대로 이해하면 모델의 진짜 성능을 객관적으로 평가할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 학습 정확도와 테스트 정확도의 차이가 크면 과적합을 의심하세요

  • 모델의 복잡도를 줄이면 과적합을 방지할 수 있습니다
  • 항상 데이터를 학습/테스트로 분리하여 평가하세요

2. 학습 곡선 이해하기

박시니어 씨가 김개발 씨에게 그래프 하나를 보여줬습니다. "이 그래프를 보면 모델이 과적합됐는지 한눈에 알 수 있어요." 가로축에는 에포크(epoch), 세로축에는 정확도가 표시되어 있습니다.

신기하게도 학습 정확도는 계속 올라가는데 검증 정확도는 어느 순간부터 떨어지고 있었습니다.

**학습 곡선(Learning Curve)**은 모델의 학습 과정을 시각화한 그래프로, 에포크나 학습 데이터 크기에 따른 성능 변화를 보여줍니다. 이 곡선을 분석하면 모델이 과적합인지, 과소적합인지, 적절한지 판단할 수 있습니다.

실무에서 모델 진단의 핵심 도구입니다.

다음 코드를 살펴봅시다.

import matplotlib.pyplot as plt
from sklearn.model_selection import learning_curve
import numpy as np

# 학습 곡선 데이터 생성
train_sizes, train_scores, val_scores = learning_curve(
    DecisionTreeClassifier(max_depth=10), X, y,
    train_sizes=np.linspace(0.1, 1.0, 10),  # 10%, 20%, ... 100% 데이터 사용
    cv=5, scoring='accuracy'
)

# 평균과 표준편차 계산
train_mean = train_scores.mean(axis=1)
val_mean = val_scores.mean(axis=1)

# 학습 곡선 시각화
plt.plot(train_sizes, train_mean, label='Training Score')
plt.plot(train_sizes, val_mean, label='Validation Score')
plt.xlabel('Training Set Size')
plt.ylabel('Accuracy')
plt.legend()
plt.show()

김개발 씨는 그래프를 보면서 고개를 갸우뚱했습니다. "학습 정확도는 계속 올라가는데 검증 정확도는 왜 떨어지는 거죠?" 이것은 머신러닝을 처음 배우는 사람들이 가장 많이 하는 질문 중 하나입니다.

박시니어 씨가 친절하게 설명을 시작했습니다. "학습 곡선을 보면 모델의 건강 상태를 알 수 있어요.

마치 병원에서 심전도 그래프를 보는 것처럼요." 학습 곡선이란 정확히 무엇일까요? 쉽게 비유하자면, 학습 곡선은 마치 학생의 성적 추이표와 같습니다.

매 시험마다 성적을 기록하면 학생의 학습 상태를 파악할 수 있습니다. 연습 문제는 계속 100점인데 실전 시험은 점점 떨어진다면, 그 학생은 문제 암기에만 집중하고 있다는 신호입니다.

머신러닝 모델도 마찬가지입니다. 학습 곡선에는 주로 두 개의 선이 그려집니다.

**학습 점수(Training Score)**와 **검증 점수(Validation Score)**입니다. 학습 곡선이 없던 시절에는 어땠을까요?

초기 머신러닝 연구자들은 모델이 제대로 학습되고 있는지 확인하기 어려웠습니다. 학습을 오래 시키면 좋아질 것 같았지만, 어느 정도가 적절한지 알 수 없었습니다.

많은 시행착오를 거쳐야 했습니다. 더 큰 문제는 과적합이 발생해도 모르고 지나가는 경우가 많았다는 점입니다.

학습 정확도만 보면 모델이 점점 좋아지는 것처럼 보였기 때문입니다. 프로젝트가 완료 단계에 이르러서야 문제를 발견하곤 했습니다.

바로 이런 문제를 해결하기 위해 학습 곡선 시각화가 표준 관행이 되었습니다. 학습 곡선을 그리면 모델의 학습 상태를 실시간으로 모니터링할 수 있습니다.

또한 언제 학습을 멈춰야 하는지 판단할 수 있습니다. 무엇보다 데이터가 충분한지, 모델이 적절한지를 한눈에 파악할 수 있다는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 learning_curve 함수는 데이터 크기를 점진적으로 늘려가며 모델을 학습시킵니다.

전체 데이터의 10%, 20%, ... 100%를 순차적으로 사용하는 것입니다.

이 부분이 핵심입니다. 다음으로 각 크기마다 교차 검증을 5번 수행하여 평균 점수를 계산합니다.

마지막으로 matplotlib으로 두 곡선을 그려서 차이를 시각적으로 확인합니다. 학습 곡선의 패턴을 해석하는 방법을 알아봅시다.

패턴 1: 두 곡선이 높은 점수에서 만남 - 이상적인 상태입니다. 모델이 적절하게 학습되었습니다.

패턴 2: 학습 곡선은 높지만 검증 곡선은 낮음 - 과적합 상태입니다. 모델이 너무 복잡하거나 데이터가 부족합니다.

패턴 3: 두 곡선 모두 낮음 - 과소적합 상태입니다. 모델이 너무 단순하거나 학습이 부족합니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 이미지 분류 모델을 개발한다고 가정해봅시다.

딥러닝 모델을 100 에포크 동안 학습시키는데, 학습 곡선을 보니 30 에포크 이후부터 검증 정확도가 떨어지기 시작합니다. 이것은 **조기 종료(Early Stopping)**를 적용해야 한다는 신호입니다.

30 에포크에서 멈추면 최적의 모델을 얻을 수 있습니다. 많은 기업에서 학습 곡선을 실시간으로 모니터링하는 대시보드를 구축해 사용하고 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 학습 곡선을 한두 번만 보고 판단하는 것입니다.

난수 시드(random seed)에 따라 결과가 달라질 수 있으므로, 여러 번 실험하여 일관된 패턴을 확인해야 합니다. 따라서 교차 검증과 함께 사용하는 것이 좋습니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 감탄했습니다.

"그래프 하나로 이렇게 많은 정보를 알 수 있다니 놀랍네요!" 학습 곡선을 제대로 활용하면 모델의 문제를 조기에 발견하고 해결할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 학습 곡선은 모델 개발 초기부터 그려보세요

  • 두 곡선의 간격이 벌어지면 과적합을 의심하세요
  • 조기 종료(Early Stopping) 전략과 함께 사용하면 효과적입니다

3. 교차 검증의 필요성

"선배님, 데이터를 한 번만 나누면 편향된 결과가 나올 수도 있지 않나요?" 김개발 씨가 날카로운 질문을 던졌습니다. 박시니어 씨가 빙그레 웃으며 대답했습니다.

"좋은 지적이에요. 그래서 실무에서는 교차 검증을 사용합니다."

**교차 검증(Cross Validation)**은 데이터를 여러 번 다르게 나누어 모델을 평가하는 기법입니다. 한 번의 분할로 평가하면 우연히 쉬운 데이터나 어려운 데이터가 테스트셋에 몰릴 수 있습니다.

교차 검증은 이런 편향을 제거하고 모델의 일반화 성능을 더 정확하게 측정합니다.

다음 코드를 살펴봅시다.

from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier

# 모델 생성
model = RandomForestClassifier(n_estimators=100, random_state=42)

# 5-Fold 교차 검증 수행
# 데이터를 5개로 나누어 각각을 테스트셋으로 사용
scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')

# 각 폴드의 정확도 출력
print(f"각 폴드 정확도: {scores}")
print(f"평균 정확도: {scores.mean():.3f}")  # 0.847
print(f"표준편차: {scores.std():.3f}")      # 0.023

# 표준편차가 작을수록 모델이 안정적입니다

김개발 씨의 질문은 매우 중요한 포인트를 짚었습니다. 데이터를 한 번만 나누면 어떤 문제가 생길까요?

예를 들어 100개의 데이터를 80:20으로 나눈다고 가정해봅시다. 운이 좋게도 쉬운 데이터 20개가 테스트셋에 들어갔다면 정확도가 높게 나올 것입니다.

반대로 어려운 데이터가 몰렸다면 정확도가 낮게 나올 것입니다. 결과가 운에 좌우되는 셈입니다.

교차 검증이란 정확히 무엇일까요? 쉽게 비유하자면, 교차 검증은 마치 여러 선생님에게 채점을 받는 것과 같습니다.

한 선생님에게만 채점받으면 그분의 주관이 반영될 수 있습니다. 하지만 다섯 명의 선생님에게 채점받아 평균을 내면 더 객관적인 평가가 가능합니다.

머신러닝 모델 평가도 마찬가지입니다. 교차 검증의 핵심 아이디어는 모든 데이터가 한 번씩은 테스트셋으로 사용된다는 것입니다.

교차 검증이 없던 시절에는 어땠을까요? 연구자들은 데이터를 한 번만 나누어 모델을 평가했습니다.

그런데 같은 모델인데도 데이터를 어떻게 나누느냐에 따라 정확도가 70%가 나오기도 하고 85%가 나오기도 했습니다. 논문을 쓸 때마다 결과가 달라져서 재현성 문제가 심각했습니다.

더 큰 문제는 데이터가 적을 때였습니다. 100개 데이터 중 20개만 테스트로 사용하면, 나머지 80개로만 학습해야 했습니다.

귀중한 데이터를 충분히 활용하지 못하는 셈이었습니다. 바로 이런 문제를 해결하기 위해 K-Fold 교차 검증이 표준 기법으로 자리 잡았습니다.

교차 검증을 사용하면 데이터 분할의 우연성을 제거할 수 있습니다. 또한 모든 데이터를 학습과 테스트에 활용할 수 있습니다.

무엇보다 **모델 성능의 분산(variance)**까지 측정할 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 cross_val_score 함수에 모델과 데이터를 전달합니다. cv=5는 5-Fold 교차 검증을 의미합니다.

이 부분이 핵심입니다. 함수는 자동으로 데이터를 5개로 나누고, 각 부분을 한 번씩 테스트셋으로 사용하여 5번 평가합니다.

마지막으로 5개의 정확도를 반환하고, 평균과 표준편차를 계산합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 신용 평가 모델을 개발한다고 가정해봅시다. 고객 데이터 1000개로 모델을 학습시키는데, 한 번의 평가에서 83% 정확도가 나왔습니다.

이것이 진짜 성능일까요? 5-Fold 교차 검증을 수행하니 [0.81, 0.85, 0.82, 0.84, 0.80]이라는 결과가 나왔습니다.

평균은 83%지만, 폴드마다 80~85% 사이에서 변동한다는 것을 알 수 있습니다. 많은 금융 기관에서 모델 승인 전에 반드시 교차 검증 결과를 요구합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 시계열 데이터에 일반 K-Fold를 적용하는 것입니다.

주식 가격이나 센서 데이터처럼 시간 순서가 중요한 경우, 미래 데이터로 학습하고 과거 데이터로 테스트하는 문제가 생길 수 있습니다. 따라서 TimeSeriesSplit 같은 전용 교차 검증을 사용해야 합니다.

또 다른 실수는 교차 검증 전에 데이터 전처리를 하는 것입니다. 정규화나 스케일링을 전체 데이터에 먼저 적용하면, 테스트셋의 정보가 학습 과정에 새어 들어갑니다.

**파이프라인(Pipeline)**을 사용하여 폴드마다 독립적으로 전처리해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "단순히 정확도 하나만 보면 안 되고, 표준편차도 확인해야 하는군요!" 교차 검증을 제대로 사용하면 모델의 진짜 성능과 안정성을 모두 파악할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 일반적으로 5-Fold 또는 10-Fold 교차 검증을 사용합니다

  • 표준편차가 크면 모델이 불안정하다는 신호입니다
  • 시계열 데이터는 TimeSeriesSplit을 사용하세요

4. K-Fold 교차 검증

"5-Fold라는 게 정확히 어떻게 동작하는 거예요?" 김개발 씨가 손으로 데이터를 나누는 시늉을 하며 물었습니다. 박시니어 씨가 종이를 꺼내 그림을 그리기 시작했습니다.

"데이터를 5등분한다고 생각해보세요."

K-Fold 교차 검증은 데이터를 K개의 동일한 크기로 나눈 후, 각 부분을 한 번씩 테스트셋으로 사용하는 방법입니다. 5-Fold라면 데이터를 5등분하여 5번 반복 평가합니다.

이를 통해 모든 데이터가 한 번은 학습에, 한 번은 테스트에 사용되어 평가의 신뢰도가 높아집니다.

다음 코드를 살펴봅시다.

from sklearn.model_selection import KFold
from sklearn.linear_model import LogisticRegression
import numpy as np

# K-Fold 객체 생성 (5개로 분할)
kfold = KFold(n_splits=5, shuffle=True, random_state=42)

model = LogisticRegression(max_iter=1000)
fold_scores = []

# 각 폴드를 순회하며 학습과 평가 수행
for fold_idx, (train_idx, test_idx) in enumerate(kfold.split(X), 1):
    # 폴드별 데이터 분할
    X_train, X_test = X[train_idx], X[test_idx]
    y_train, y_test = y[train_idx], y[test_idx]

    # 모델 학습 및 평가
    model.fit(X_train, y_train)
    score = model.score(X_test, y_test)
    fold_scores.append(score)

    print(f"Fold {fold_idx}: {score:.3f}")

print(f"\n평균 정확도: {np.mean(fold_scores):.3f}")

박시니어 씨가 그린 그림을 보니 김개발 씨도 이해가 되기 시작했습니다. 데이터를 1번부터 5번까지 다섯 블록으로 나눈 후, 첫 번째는 1번을 테스트로 사용하고, 두 번째는 2번을 테스트로 사용하는 방식이었습니다.

"아하, 그러면 총 5번을 반복하는 거네요!" 김개발 씨가 무릎을 쳤습니다. K-Fold 교차 검증이란 정확히 무엇일까요?

쉽게 비유하자면, K-Fold는 마치 반 학생들을 5개 조로 나눈 후 각 조가 돌아가며 발표를 평가하는 것과 같습니다. 1조가 발표할 때 나머지 2~5조는 듣고 배웁니다.

그다음은 2조가 발표하고 1, 3~5조가 듣습니다. 이렇게 모든 조가 한 번씩 발표하고 한 번씩 평가받습니다.

공평하고 체계적인 방법입니다. K-Fold의 K는 데이터를 몇 개로 나눌지를 의미합니다.

K=5면 5등분, K=10이면 10등분입니다. K-Fold가 없던 시절에는 어떤 방법을 썼을까요?

초기에는 홀드아웃(Holdout) 방법을 사용했습니다. 데이터를 단순히 70:30이나 80:20으로 한 번만 나누는 방식입니다.

간단하지만 앞서 말했듯이 운에 좌우되는 문제가 있었습니다. 더 큰 문제는 데이터가 적을 때였습니다.

환자 데이터 100명으로 질병 예측 모델을 만든다고 가정해봅시다. 20명을 테스트로 빼면 80명으로만 학습해야 합니다.

귀중한 의료 데이터를 충분히 활용하지 못하는 것입니다. 바로 이런 문제를 해결하기 위해 K-Fold 교차 검증이 개발되었습니다.

K-Fold를 사용하면 100명 전체를 학습에 활용할 수 있습니다. 물론 한 번에 전부는 아니지만, 5번 반복하면서 각각 80명씩 학습하고 20명씩 테스트합니다.

또한 평가의 신뢰도가 크게 향상됩니다. 무엇보다 작은 데이터셋에서도 효과적이라는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 KFold 객체를 생성할 때 n_splits=5로 5개 폴드를 지정합니다.

shuffle=True는 데이터를 섞어서 나눈다는 뜻입니다. 이 부분이 핵심입니다.

다음으로 kfold.split(X)는 각 폴드의 학습 인덱스와 테스트 인덱스를 반환합니다. for 루프를 돌면서 5번 반복하여 모델을 학습하고 평가합니다.

마지막으로 5개 점수의 평균을 계산합니다. K값은 어떻게 정하면 좋을까요?

일반적으로 K=5 또는 K=10을 많이 사용합니다. K가 너무 작으면 평가가 불안정하고, 너무 크면 계산 시간이 오래 걸립니다.

극단적으로 K를 데이터 개수와 같게 하면 **LOOCV(Leave-One-Out Cross Validation)**가 됩니다. 데이터가 100개라면 100번을 반복하는 것입니다.

정확하지만 시간이 많이 걸립니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 희귀병 진단 모델을 개발한다고 가정해봅시다. 환자 데이터가 겨우 200명밖에 없습니다.

이럴 때 홀드아웃으로 40명을 테스트로 빼면 너무 아깝습니다. 10-Fold 교차 검증을 사용하면 매번 180명으로 학습하고 20명으로 테스트하여, 데이터를 최대한 활용하면서도 신뢰할 수 있는 평가를 할 수 있습니다.

의료 AI 분야에서는 K-Fold 교차 검증이 거의 필수입니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 클래스 불균형 데이터에 일반 K-Fold를 사용하는 것입니다. 예를 들어 불량품이 5%밖에 없는 데이터를 무작위로 나누면, 어떤 폴드에는 불량품이 하나도 없을 수 있습니다.

따라서 StratifiedKFold를 사용하여 각 폴드의 클래스 비율을 동일하게 유지해야 합니다. 또 다른 실수는 K를 너무 크게 설정하는 것입니다.

K=100으로 설정하면 100번을 반복해야 하므로 시간이 오래 걸립니다. 딥러닝처럼 학습이 오래 걸리는 모델에서는 K=5 정도가 적당합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 감탄했습니다.

"데이터를 5번 재활용하는 셈이네요. 정말 효율적이에요!" K-Fold 교차 검증을 제대로 사용하면 적은 데이터로도 신뢰할 수 있는 모델 평가가 가능합니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 일반적으로 K=5 또는 K=10을 사용합니다

  • 클래스 불균형이 있다면 StratifiedKFold를 사용하세요
  • 계산 시간과 평가 신뢰도의 균형을 고려하여 K값을 선택하세요

5. cross val score 사용법

"매번 for 루프를 작성하는 게 번거로운데, 더 간단한 방법은 없나요?" 김개발 씨가 물었습니다. 박시니어 씨가 웃으며 대답했습니다.

"scikit-learn에서 제공하는 cross_val_score 함수를 사용하면 한 줄로 끝납니다."

cross_val_score() 함수는 교차 검증을 자동으로 수행해주는 scikit-learn의 편리한 도구입니다. 모델, 데이터, 폴드 수, 평가 지표만 지정하면 내부적으로 K-Fold를 수행하고 각 폴드의 점수를 배열로 반환합니다.

실무에서 가장 많이 사용하는 교차 검증 함수입니다.

다음 코드를 살펴봅시다.

from sklearn.model_selection import cross_val_score, cross_validate
from sklearn.svm import SVC
import numpy as np

# 기본 사용법: 정확도만 반환
model = SVC(kernel='rbf', C=1.0)
scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')

print(f"각 폴드 정확도: {scores}")
print(f"평균: {scores.mean():.3f} (+/- {scores.std():.3f})")

# 고급 사용법: 여러 지표를 동시에 평가
scoring = ['accuracy', 'precision', 'recall', 'f1']
results = cross_validate(model, X, y, cv=5, scoring=scoring)

print(f"\n정확도: {results['test_accuracy'].mean():.3f}")
print(f"정밀도: {results['test_precision'].mean():.3f}")
print(f"재현율: {results['test_recall'].mean():.3f}")
print(f"F1 점수: {results['test_f1'].mean():.3f}")

김개발 씨는 앞서 배운 K-Fold 코드를 보며 생각했습니다. for 루프로 5번 반복하고, 인덱스로 데이터를 나누고, 점수를 리스트에 추가하는 과정이 매번 반복된다면 코드가 길어질 것 같았습니다.

"더 간단한 방법이 있다면 좋겠는데..." 바로 그때 박시니어 씨가 cross_val_score 함수를 소개했습니다. cross_val_score란 정확히 무엇일까요?

쉽게 비유하자면, cross_val_score는 마치 자동 세탁기와 같습니다. 옛날에는 손빨래를 했습니다.

물을 받고, 비누칠하고, 헹구고, 탈수하는 모든 과정을 직접 해야 했습니다. 하지만 세탁기는 옷과 세제만 넣으면 모든 과정을 자동으로 처리해줍니다.

cross_val_score도 마찬가지입니다. 이 함수는 K-Fold 분할, 모델 학습, 평가, 점수 수집을 모두 내부적으로 처리합니다.

cross_val_score가 없던 시절에는 어땠을까요? 연구자들은 교차 검증을 할 때마다 비슷한 코드를 반복해서 작성했습니다.

KFold 객체를 만들고, for 루프를 돌리고, 데이터를 나누고, 모델을 학습시키고, 평가하고, 점수를 저장하는... 이 과정이 너무 번거로웠습니다.

더 큰 문제는 실수하기 쉽다는 점이었습니다. 인덱스를 잘못 지정하거나, 데이터를 섞지 않거나, 난수 시드를 설정하지 않는 등의 실수가 빈번했습니다.

코드 리뷰 때마다 이런 부분을 체크해야 했습니다. 바로 이런 문제를 해결하기 위해 cross_val_score 함수가 표준화되었습니다.

이 함수를 사용하면 단 한 줄로 교차 검증을 수행할 수 있습니다. 또한 실수 가능성이 크게 줄어들고 코드가 간결해집니다.

무엇보다 다양한 평가 지표를 쉽게 사용할 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 모델 객체를 생성합니다. 아직 학습하지 않은 상태입니다.

cross_val_score에 모델, X, y를 전달하고 cv=5로 5-Fold를 지정합니다. 이 부분이 핵심입니다.

scoring='accuracy'는 정확도로 평가하라는 뜻입니다. 함수는 5개의 정확도를 배열로 반환하고, 평균과 표준편차를 계산할 수 있습니다.

더 고급 기능으로 cross_validate 함수도 있습니다. 이것은 여러 평가 지표를 동시에 계산할 수 있습니다.

평가 지표는 어떤 것들이 있을까요? 회귀 문제라면 'neg_mean_squared_error'(음수 MSE), 'r2'(결정계수) 등을 사용합니다.

분류 문제라면 'accuracy'(정확도), 'precision'(정밀도), 'recall'(재현율), 'f1'(F1 점수), 'roc_auc'(AUC) 등을 사용할 수 있습니다. 상황에 맞는 지표를 선택하는 것이 중요합니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 스팸 메일 분류 모델을 개발한다고 가정해봅시다.

정확도만으로는 부족합니다. 정상 메일이 99%라면 모든 메일을 정상으로 분류해도 99% 정확도가 나오기 때문입니다.

이럴 때는 정밀도와 재현율을 함께 평가해야 합니다. cross_validate로 세 가지 지표를 한 번에 계산하면 편리합니다.

많은 기업에서 모델 평가 보고서에 교차 검증 결과를 필수로 포함시킵니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 평가 지표의 의미를 제대로 이해하지 않고 사용하는 것입니다. 예를 들어 회귀 문제에 'accuracy'를 사용하면 에러가 발생합니다.

또한 'neg_mean_squared_error'는 음수 값이 반환되므로, 실제 MSE를 얻으려면 -1을 곱해야 합니다. 따라서 각 지표의 특성을 이해하고 사용해야 합니다.

또 다른 실수는 교차 검증에 너무 오래 걸리는 모델을 사용하는 것입니다. 딥러닝 모델을 5-Fold로 평가하면 5배의 시간이 걸립니다.

이럴 때는 K를 줄이거나, 데이터 일부만 사용하거나, 홀드아웃 방법을 고려해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 바로 코드를 수정했습니다. "정말 간단해졌네요!

앞으로는 이 함수를 애용해야겠어요." cross_val_score를 제대로 사용하면 교차 검증을 빠르고 정확하게 수행할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - scoring 매개변수로 다양한 평가 지표를 지정할 수 있습니다

  • cross_validate를 사용하면 여러 지표를 동시에 평가할 수 있습니다
  • 학습 시간도 측정하고 싶다면 cross_validate에서 return_train_score=True로 설정하세요

6. 과적합 방지 전략

"과적합을 발견했으면 이제 해결해야 하는데, 어떤 방법들이 있나요?" 김개발 씨가 노트를 펼치며 물었습니다. 박시니어 씨가 손가락을 하나씩 꼽으며 설명하기 시작했습니다.

"크게 네 가지 전략이 있어요."

과적합 방지 전략은 모델이 학습 데이터에만 과도하게 최적화되는 것을 막는 다양한 기법들입니다. 데이터를 늘리거나, 모델을 단순화하거나, 정규화를 적용하거나, 조기 종료를 사용하는 방법들이 있습니다.

실무에서는 여러 전략을 조합하여 사용합니다.

다음 코드를 살펴봅시다.

from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

# 전략 1: 모델 복잡도 제한
simple_tree = DecisionTreeClassifier(
    max_depth=5,           # 깊이 제한
    min_samples_split=20,  # 분할에 필요한 최소 샘플 수
    min_samples_leaf=10    # 리프 노드의 최소 샘플 수
)

# 전략 2: 앙상블 기법 (과적합 방지 효과)
ensemble = RandomForestClassifier(
    n_estimators=100,      # 트리 100개 사용
    max_features='sqrt'    # 피처 일부만 사용
)

# 전략 3: 드롭아웃과 조기 종료 (딥러닝)
# from tensorflow.keras.callbacks import EarlyStopping
# early_stop = EarlyStopping(monitor='val_loss', patience=10)
# model.fit(X_train, y_train, validation_split=0.2, callbacks=[early_stop])

김개발 씨는 지금까지 과적합을 진단하는 방법을 배웠습니다. 학습 곡선을 그려보고, 교차 검증을 수행하고, 학습 정확도와 테스트 정확도의 차이를 확인했습니다.

과적합이 발견되었다면 이제 해결해야 할 차례입니다. 박시니어 씨가 네 가지 손가락을 펼쳤습니다.

"데이터, 모델, 학습, 검증 이렇게 네 방향에서 접근할 수 있어요." 과적합 방지 전략이란 정확히 무엇일까요? 쉽게 비유하자면, 과적합 방지는 마치 학생의 암기 공부를 막는 것과 같습니다.

학생이 문제를 달달 외우지 못하도록 하려면 여러 방법이 있습니다. 다양한 유형의 문제를 풀게 하거나, 너무 어려운 문제는 빼거나, 적절한 시점에 공부를 멈추게 하거나, 중요한 개념 위주로 학습하게 하는 것입니다.

머신러닝도 마찬가지입니다. 과적합 방지 전략은 크게 네 가지 범주로 나눌 수 있습니다.

과적합 방지 전략이 체계화되기 전에는 어땠을까요? 초기 머신러닝 연구자들은 과적합이 발생하면 당황했습니다.

모델을 다시 만들거나, 데이터를 다시 수집하거나, 알고리즘 자체를 바꾸는 등 시행착오를 반복했습니다. 체계적인 해결 방법이 없었기 때문입니다.

더 큰 문제는 어떤 전략이 효과적인지 알 수 없다는 점이었습니다. 한 프로젝트에서 효과가 있던 방법이 다른 프로젝트에서는 통하지 않는 경우가 많았습니다.

경험과 직관에 의존할 수밖에 없었습니다. 바로 이런 문제를 해결하기 위해 체계적인 과적합 방지 기법들이 연구되고 정리되었습니다.

전략 1: 데이터 확보 및 증강입니다. 과적합의 근본 원인 중 하나는 데이터 부족입니다.

데이터를 더 수집하거나, 이미지의 경우 회전/반전/크롭 등으로 데이터를 늘릴 수 있습니다. 또한 데이터 정제로 노이즈를 제거하면 모델이 깨끗한 패턴을 학습합니다.

전략 2: 모델 복잡도 제한입니다. 모델이 너무 복잡하면 과적합되기 쉽습니다.

의사결정나무의 깊이를 제한하거나, 신경망의 층 수를 줄이거나, 피처 수를 줄이는 방법입니다. 간단한 모델이 오히려 일반화 성능이 좋을 때가 많습니다.

전략 3: 정규화(Regularization) 적용입니다. 가중치가 너무 커지지 않도록 페널티를 부과하는 기법입니다.

L1 정규화, L2 정규화, Elastic Net 등이 있습니다. 딥러닝에서는 **드롭아웃(Dropout)**도 효과적인 정규화 기법입니다.

**전략 4: 조기 종료(Early Stopping)**입니다. 검증 손실이 증가하기 시작하면 학습을 멈추는 방법입니다.

과적합이 시작되기 직전에 학습을 멈춰서 최적의 모델을 얻습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 의사결정나무의 max_depth=5로 깊이를 제한합니다. 무한정 깊어지는 것을 막는 것입니다.

이 부분이 핵심입니다. 다음으로 min_samples_splitmin_samples_leaf로 노드 분할 조건을 까다롭게 만듭니다.

마지막으로 랜덤 포레스트는 여러 트리를 앙상블하여 과적합을 자연스럽게 방지합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 부동산 가격 예측 모델을 개발한다고 가정해봅시다. 처음에는 모든 피처를 사용하여 복잡한 모델을 만들었습니다.

과적합이 발생했습니다. 먼저 피처 선택으로 중요한 피처 10개만 남겼습니다.

그래도 과적합이면 L2 정규화를 적용했습니다. 학습 곡선을 보니 50 에포크 이후 검증 손실이 증가하여 조기 종료를 적용했습니다.

이렇게 여러 전략을 단계적으로 적용하는 것이 실무 패턴입니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 한꺼번에 모든 기법을 적용하는 것입니다. 정규화도 하고, 드롭아웃도 하고, 조기 종료도 하고...

이렇게 하면 **과소적합(Underfitting)**이 발생할 수 있습니다. 모델이 너무 단순해져서 학습 데이터조차 제대로 학습하지 못하는 상태입니다.

따라서 한 번에 하나씩 적용하며 효과를 확인하는 것이 좋습니다. 또 다른 실수는 테스트 데이터를 보면서 전략을 조정하는 것입니다.

테스트 정확도를 높이기 위해 하이퍼파라미터를 계속 바꾸면, 결국 테스트 데이터에 과적합됩니다. **검증 데이터(Validation Set)**를 별도로 만들어 사용해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 정리했습니다.

"데이터를 늘리고, 모델을 단순화하고, 정규화를 적용하고, 적절한 시점에 멈추면 되는군요!" 과적합 방지 전략을 제대로 조합하면 일반화 성능이 높은 모델을 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 한 번에 하나씩 전략을 적용하며 효과를 측정하세요

  • 과소적합과 과적합의 균형을 찾는 것이 중요합니다
  • 검증 데이터와 테스트 데이터를 구분하여 사용하세요

7. 정규화 기법 소개

"정규화라는 게 구체적으로 뭐예요? 데이터 스케일링이랑 다른 건가요?" 김개발 씨가 헷갈려하며 물었습니다.

박시니어 씨가 고개를 저었습니다. "완전히 다른 개념이에요.

정규화는 모델의 가중치를 제한하는 기법입니다."

**정규화(Regularization)**는 모델의 가중치가 너무 커지지 않도록 손실 함수에 페널티를 추가하는 기법입니다. L1 정규화는 가중치의 절댓값 합을 페널티로 부과하고, L2 정규화는 가중치의 제곱 합을 페널티로 부과합니다.

이를 통해 모델이 복잡해지는 것을 방지하여 과적합을 막습니다.

다음 코드를 살펴봅시다.

from sklearn.linear_model import Ridge, Lasso, ElasticNet
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

# L2 정규화 (Ridge): 가중치 제곱의 합에 페널티
ridge = Ridge(alpha=1.0)  # alpha가 클수록 정규화 강도가 강함
ridge.fit(X_train, y_train)

# L1 정규화 (Lasso): 가중치 절댓값의 합에 페널티
lasso = Lasso(alpha=0.1)  # 일부 가중치를 0으로 만들어 피처 선택 효과
lasso.fit(X_train, y_train)

# L1 + L2 정규화 (ElasticNet): 두 가지 장점을 결합
elastic = ElasticNet(alpha=0.5, l1_ratio=0.5)  # l1_ratio로 L1과 L2 비율 조정
elastic.fit(X_train, y_train)

print(f"Ridge 점수: {ridge.score(X_test, y_test):.3f}")
print(f"Lasso 점수: {lasso.score(X_test, y_test):.3f}")
print(f"ElasticNet 점수: {elastic.score(X_test, y_test):.3f}")

# Lasso는 일부 가중치를 0으로 만듦
print(f"\n0이 아닌 가중치 개수: {(lasso.coef_ != 0).sum()}")

김개발 씨는 정규화라는 용어를 처음 들었을 때 데이터 정규화(Normalization)를 떠올렸습니다. 데이터를 0~1 범위로 스케일링하는 그것 말입니다.

하지만 박시니어 씨가 말하는 정규화는 완전히 다른 개념이었습니다. "정규화는 영어로 Regularization이에요.

데이터 정규화는 Normalization이고요. 번역이 같아서 헷갈리죠." 박시니어 씨가 설명했습니다.

**정규화(Regularization)**란 정확히 무엇일까요? 쉽게 비유하자면, 정규화는 마치 자동차의 속도 제한과 같습니다.

제한이 없으면 운전자가 과속할 수 있습니다. 빠를수록 좋다고 생각하지만, 사고 위험이 커집니다.

속도 제한을 두면 안전하게 목적지에 도착할 수 있습니다. 머신러닝에서 가중치도 마찬가지입니다.

제한이 없으면 가중치가 극단적으로 커져서 과적합됩니다. 정규화는 손실 함수에 페널티 항을 추가하는 방식으로 동작합니다.

정규화가 없던 시절에는 어땠을까요? 초기 선형 회귀나 로지스틱 회귀는 단순히 손실을 최소화하는 것만 목표로 했습니다.

학습 데이터를 완벽하게 맞추려다 보니 가중치가 매우 커지는 경우가 많았습니다. 피처 100개를 사용하는 모델에서 일부 가중치가 1000이 넘는 일도 있었습니다.

더 큰 문제는 이런 모델이 새로운 데이터에 민감하게 반응한다는 점이었습니다. 입력값이 조금만 바뀌어도 예측값이 크게 변했습니다.

일반화 성능이 떨어지는 것입니다. 바로 이런 문제를 해결하기 위해 L1 정규화와 L2 정규화가 개발되었습니다.

**L2 정규화(Ridge)**는 가중치의 제곱 합을 페널티로 부과합니다. 수식으로는 손실 함수에 alpha * Σ(w²)를 더하는 것입니다.

이렇게 하면 가중치가 골고루 작아집니다. 또한 다중공선성 문제에 강합니다.

무엇보다 미분 가능하여 경사하강법으로 최적화하기 쉽다는 장점이 있습니다. **L1 정규화(Lasso)**는 가중치의 절댓값 합을 페널티로 부과합니다.

수식으로는 alpha * Σ|w|를 더하는 것입니다. L2와 다른 점은 일부 가중치를 정확히 0으로 만든다는 것입니다.

이것은 자동 피처 선택 효과가 있습니다. 중요하지 않은 피처의 가중치가 0이 되어 제거됩니다.

ElasticNet은 L1과 L2를 섞은 것입니다. l1_ratio로 비율을 조정할 수 있습니다.

두 가지 장점을 모두 취할 수 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 Ridge 모델을 생성할 때 alpha=1.0으로 정규화 강도를 지정합니다. alpha가 클수록 가중치가 작아집니다.

이 부분이 핵심입니다. 다음으로 Lasso는 alpha=0.1로 더 약한 정규화를 적용합니다.

Lasso는 가중치를 0으로 만들기 때문에 alpha를 너무 크게 하면 모든 가중치가 0이 될 수 있습니다. 마지막으로 ElasticNet은 l1_ratio=0.5로 L1과 L2를 반반 섞습니다.

alpha 값은 어떻게 정하면 좋을까요? alpha는 하이퍼파라미터입니다.

너무 작으면 정규화 효과가 없고, 너무 크면 과소적합됩니다. 일반적으로 0.01, 0.1, 1.0, 10.0 등 여러 값을 시도해보고 교차 검증으로 최적값을 찾습니다.

scikit-learn의 RidgeCV, LassoCV를 사용하면 자동으로 최적 alpha를 찾아줍니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 주택 가격 예측 모델을 개발한다고 가정해봅시다. 피처가 50개인데, 일부는 서로 상관관계가 높습니다.

일반 선형 회귀를 사용하니 과적합이 발생했습니다. Ridge 회귀를 적용하니 과적합이 줄어들었습니다.

그런데 50개 피처 중 실제로 중요한 것은 20개 정도인 것 같습니다. Lasso 회귀를 적용하니 30개 피처의 가중치가 0이 되어 자동으로 제거되었습니다.

금융, 의료, 마케팅 등 많은 분야에서 정규화는 필수 기법입니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 정규화 전에 데이터를 스케일링하지 않는 것입니다. 피처마다 범위가 다르면 정규화 페널티가 불공평하게 적용됩니다.

예를 들어 연봉(수천만 원)과 나이(수십)는 스케일이 다릅니다. 따라서 StandardScaler로 표준화한 후 정규화를 적용해야 합니다.

또 다른 실수는 L1과 L2를 잘못 선택하는 것입니다. 피처가 많고 대부분 불필요하다면 L1, 피처가 적고 대부분 유용하다면 L2, 잘 모르겠다면 ElasticNet을 사용하는 것이 좋습니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 감탄했습니다.

"가중치를 직접 제한하는 방식이군요. 정말 영리한 아이디어네요!" 정규화 기법을 제대로 사용하면 과적합을 효과적으로 방지하고 해석 가능한 모델을 만들 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 정규화 전에 반드시 데이터를 스케일링하세요

  • 피처 선택이 필요하면 Lasso, 그렇지 않으면 Ridge를 사용하세요
  • RidgeCV와 LassoCV로 최적 alpha를 자동으로 찾을 수 있습니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#MachineLearning#Overfitting#CrossValidation#KFold

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스