내일배움캠프

[본캠프] 데이터기반 QA/QC 부트캠프 33일차

min0jun 2026. 6. 26. 20:46

1. 오늘의 학습 목표

오늘은 머신러닝 모델을 만들 때 데이터를 어떻게 나누고, 어떻게 평가해야 하는지를 중심으로 학습했다.

이전에는 데이터 수집, EDA, 이상치, 결측치, 인코딩, 스케일링처럼 모델링 전에 필요한 전처리 과정을 정리했다면, 오늘은 그 다음 단계인 데이터 분리와 모델 검증 과정을 다뤘다.

오늘의 목표는 크게 세 가지다.

첫 번째는 과적합이 무엇인지 이해하는 것이다. 모델이 학습 데이터에만 지나치게 잘 맞고, 새로운 데이터에는 약해지는 상황을 배웠다.

두 번째는 train/test 데이터를 분리해서 모델을 평가하는 흐름을 익히는 것이다. 모델을 학습하는 데이터와 평가하는 데이터를 나눠야 실제 예측 성능을 조금 더 객관적으로 확인할 수 있다.

세 번째는 교차 검증과 GridSearch의 개념을 정리하는 것이다. 단순히 한 번 데이터를 나누는 것에서 끝나는 것이 아니라, 여러 번 나누어 검증하고 하이퍼파라미터를 자동으로 탐색하는 방법까지 학습했다.


2. 오늘 학습한 내용

머신러닝

과적합이란 무엇인가

과적합은 머신러닝에서 반드시 조심해야 하는 문제다.

과적합이란 모델이 학습 데이터에 너무 과하게 맞춰진 나머지, 새로운 데이터를 제대로 예측하지 못하는 상태를 말한다.

쉽게 생각하면 특정 모의고사 문제만 외워서 그 시험은 잘 보지만, 실제 수능에서는 점수가 잘 나오지 않는 상황과 비슷하다. 학습 데이터에 대해서는 점수가 높게 나오지만, 처음 보는 데이터에서는 성능이 떨어지는 것이다.

모델이 너무 복잡하면 과적합이 발생할 수 있다. 반대로 모델이 너무 단순하면 데이터의 패턴을 제대로 잡지 못하는 과소적합이 발생할 수 있다.

결국 중요한 것은 학습 데이터에만 잘 맞는 모델이 아니라, 새로운 데이터에도 어느 정도 잘 맞는 모델을 만드는 것이다.

과적합의 원인은 여러 가지가 있다.

모델의 복잡도가 너무 높을 수도 있고, 데이터 양이 충분하지 않을 수도 있다. 딥러닝처럼 반복 학습이 많은 모델에서는 학습을 너무 오래 해서 과적합이 생길 수도 있다. 또한 정상 데이터와 비정상 데이터의 비율이 크게 차이 나는 데이터 불균형도 과적합이나 성능 왜곡의 원인이 될 수 있다.

train/test 데이터 분리

과적합을 줄이기 위해 가장 기본적으로 사용하는 방법이 데이터 분리다.

전체 데이터를 한 번에 모두 학습에 사용하는 것이 아니라, 일부는 모델 학습용으로 사용하고 일부는 평가용으로 남겨둔다.

학습 데이터는 모델을 학습시키는 데 사용하고, 테스트 데이터는 학습이 끝난 모델을 평가하는 데 사용한다.

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.3,
    shuffle=True,
    random_state=42
)

여기서 test_size는 테스트 데이터 비율을 의미한다. test_size=0.3이면 전체 데이터 중 30%를 테스트 데이터로 사용한다.

shuffle=True는 데이터를 섞어서 나누겠다는 의미이고, random_state=42는 실행할 때마다 같은 방식으로 데이터가 나뉘도록 난수 값을 고정하는 역할을 한다.

처음에는 random_state가 왜 필요한지 크게 와닿지 않았는데, 실습을 하다 보면 같은 코드를 실행해도 데이터가 다르게 나뉘면 결과가 계속 달라질 수 있다. 그래서 비교나 재현을 위해 고정값을 주는 것이 중요하다.

타이타닉 데이터 전체 실습 흐름

오늘 실습에서는 타이타닉 데이터를 활용해서 전체 모델링 흐름을 다시 정리했다.

전체 과정은 다음과 같이 진행된다.

데이터 로드 → 데이터 분리 → EDA → 데이터 전처리 → 모델 수립 → 평가

타이타닉 데이터는 생존 여부를 예측하는 분류 문제다. 즉, 종속변수는 Survived이고, 생존했는지 아닌지를 예측하는 것이 목표다.

먼저 데이터를 불러오고 기본 정보를 확인한다.

import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

train_df = pd.read_csv("train.csv")
test_df = pd.read_csv("test.csv")

train_df.info()
train_df.describe(include='all')

info()를 통해 결측치와 데이터 타입을 확인할 수 있고, describe(include='all')을 통해 수치형과 범주형 변수의 기본 통계를 함께 확인할 수 있다.

EDA에서는 Age, Fare, Family 같은 변수를 살펴보았다. 특히 SibSpParch를 더해서 가족 수를 나타내는 Family 변수를 만드는 방식이 나왔다.

def get_family(df):
    df['Family'] = df['SibSp'] + df['Parch'] + 1
    return df

단순히 기존 변수만 사용하는 것이 아니라, 기존 변수를 조합해서 새로운 변수를 만드는 과정도 모델 성능에 영향을 줄 수 있다. 이 부분은 피처 엔지니어링의 시작처럼 느껴졌다.

이상치와 결측치 처리

타이타닉 실습에서는 Fare의 이상치를 처리했다.

train_df_2 = train_df_2[train_df_2['Fare'] < 512]

Fare가 지나치게 큰 데이터는 전체 분포에 영향을 줄 수 있기 때문에 조건을 걸어 제거했다.

결측치는 Age, Fare, Embarked 등을 중심으로 처리했다. 수치형 변수는 평균값으로 채우고, 범주형 변수는 최빈값이나 별도 방식으로 처리할 수 있다.

def get_non_missing(df):
    Age_mean = train_df_2['Age'].mean()
    Fare_mean = train_df_2['Fare'].mean()

    df['Age'] = df['Age'].fillna(Age_mean)
    df['Fare'] = df['Fare'].fillna(Fare_mean)

    return df

결측치를 처리하지 않으면 모델 학습 과정에서 오류가 발생할 수 있다. 그래서 모델을 만들기 전에 결측치를 어떻게 처리할지 정해야 한다.

다만 평균값으로 대치하는 것이 항상 정답은 아니다. 데이터의 분포나 이상치 여부에 따라 중앙값, 최빈값, 알고리즘 기반 대치 등 다른 방법도 고려할 수 있다.

인코딩과 스케일링

머신러닝 모델은 숫자를 기반으로 학습하기 때문에 문자열 데이터는 숫자로 바꿔야 한다.

타이타닉 데이터에서는 Sex, Pclass, Embarked 같은 범주형 변수를 처리했다.

Sex처럼 남자/여자 형태로 나뉘는 변수는 레이블 인코딩을 사용할 수 있다. Embarked처럼 항구 정보는 원-핫 인코딩을 사용할 수 있다.

from sklearn.preprocessing import LabelEncoder, OneHotEncoder

le = LabelEncoder()
df['Sex_le'] = le.fit_transform(df['Sex'])

원-핫 인코딩은 범주를 각각의 열로 분리해서 0과 1로 표현한다.

embarked_dummies = pd.get_dummies(df['Embarked'])

수치형 변수는 스케일링을 적용했다. Age, Fare, Family처럼 단위나 범위가 다른 변수들은 모델이 특정 변수에 지나치게 영향을 받지 않도록 값을 조정해줄 수 있다.

from sklearn.preprocessing import StandardScaler, MinMaxScaler

sd_sc = StandardScaler()
mm_sc = MinMaxScaler()

df['Fare_sd_sc'] = sd_sc.fit_transform(df[['Fare']])
df['Age_mm_sc'] = mm_sc.fit_transform(df[['Age']])

스케일링은 단순히 값의 모양만 바꾸는 것이 아니라, 모델이 변수들을 더 공정하게 비교할 수 있게 만드는 과정이라고 볼 수 있다.

로지스틱 회귀 모델 학습

타이타닉 생존 여부는 분류 문제이기 때문에 로지스틱 회귀 모델을 사용할 수 있다.

from sklearn.linear_model import LogisticRegression

model = LogisticRegression()

X = df[['Age_mm_sc', 'Fare_sd_sc', 'Family_mm_sc', 'Pclass_le', 'Sex_le']]
y = df['Survived']

model.fit(X, y)

모델을 학습한 뒤에는 예측값을 만들고, 정확도나 F1-score로 성능을 평가할 수 있다.

from sklearn.metrics import accuracy_score, f1_score

y_pred = model.predict(X)

accuracy = accuracy_score(y, y_pred)
f1 = f1_score(y, y_pred)

정확도는 전체 예측 중 맞춘 비율을 의미하고, F1-score는 정밀도와 재현율을 함께 고려한 지표다.

분류 문제에서는 데이터 불균형이 있을 수 있기 때문에 정확도만 보는 것이 항상 충분하지 않다. 그래서 F1-score도 함께 확인하는 것이 중요하다.

테스트 데이터에 모델 적용하기

Kaggle 제출 실습에서는 train 데이터로 만든 전처리 함수를 test 데이터에도 동일하게 적용했다.

이 부분이 중요했다. train 데이터와 test 데이터에 서로 다른 기준의 전처리를 적용하면 모델 입력 구조가 달라질 수 있다.

test_df_2 = get_family(test_df)
test_df_2 = get_non_missing(test_df_2)
test_df_2 = get_numeric_sc(test_df_2)
test_df_2 = get_category(test_df_2)

그 다음 학습된 모델로 test 데이터를 예측하고, 제출 파일의 Survived 컬럼에 예측 결과를 넣는다.

test_X = test_df_2[['Age_mm_sc', 'Fare_sd_sc', 'Family_mm_sc', 'Pclass_le', 'Sex_le']]

y_test_pred = model.predict(test_X)

sub_df = pd.read_csv('gender_submission.csv')
sub_df['Survived'] = y_test_pred
sub_df.to_csv('./result.csv', index=False)

이 과정은 실제 머신러닝 프로젝트의 흐름과 꽤 비슷하게 느껴졌다. train 데이터로 모델을 만들고, test 데이터에 같은 전처리 흐름을 적용한 뒤, 예측 결과를 파일로 저장하는 방식이다.

교차 검증

train/test 분리는 기본적인 평가 방법이지만, 한 번만 나눈 테스트 데이터에 성능이 의존할 수 있다는 한계가 있다.

이 문제를 보완하기 위해 교차 검증을 사용한다.

교차 검증은 Train Data를 여러 개의 하위 집합으로 나누고, 각 하위 집합을 돌아가면서 검증 데이터로 사용하는 방법이다.

대표적으로 K-Fold 방식이 있다.

예를 들어 데이터를 5개로 나누면, 1번째 fold를 검증용으로 쓰고 나머지를 학습용으로 사용한다. 그 다음에는 2번째 fold를 검증용으로 사용하고, 이런 식으로 5번 반복한다.

from sklearn.model_selection import KFold

kfold = KFold(n_splits=5)
scores = []

이렇게 하면 특정 데이터 분할에만 의존하지 않고 모델 성능을 조금 더 안정적으로 확인할 수 있다.

데이터가 많지 않을 때는 교차 검증이 특히 유용하다. 데이터를 여러 번 나누어 학습과 검증에 활용하기 때문이다.

또한 분류 문제에서 레이블 불균형이 있는 경우에는 StratifiedKFold를 사용할 수 있다. 이는 각 fold에 클래스 비율이 최대한 비슷하게 유지되도록 나누는 방식이다.

GridSearch

GridSearch는 여러 하이퍼파라미터 조합을 자동으로 실험해보는 방법이다.

하이퍼파라미터는 모델 학습 전에 사람이 직접 정해야 하는 값이다. 예를 들어 로지스틱 회귀에서는 solver나 max_iter 같은 값이 있다.

from sklearn.model_selection import GridSearchCV

params = {
    'solver': ['newton-cg', 'lbfgs', 'liblinear', 'sag', 'saga'],
    'max_iter': [100, 200]
}

grid_model = GridSearchCV(
    model,
    param_grid=params,
    scoring='accuracy',
    cv=5
)

GridSearch는 지정한 조합을 하나씩 실험하면서 가장 좋은 성능을 내는 조합을 찾는다.

직접 모든 조합을 하나하나 돌려보는 것보다 훨씬 효율적이고, 교차 검증과 함께 사용할 수 있어서 모델 튜닝 과정에서 자주 사용된다.

다만 GridSearch도 무작정 많이 돌리면 시간이 오래 걸릴 수 있다. 그래서 어떤 파라미터를 어느 범위에서 탐색할지 정하는 것도 중요하다.


3. 나의 간단 소감

오늘은 머신러닝 모델링 과정에서 “평가를 어떻게 믿을 것인가”에 가까운 내용을 배웠다. 단순히 모델을 만들고 점수 하나를 확인하는 것보다, 데이터를 어떻게 나누고 어떤 방식으로 검증하는지가 훨씬 중요하다는 생각이 들었다.

과적합 개념은 설명만 보면 어렵지 않은데, 실제 실습에 넣어보니 더 현실적으로 느껴졌다. 학습 데이터에서는 점수가 잘 나오는데 새로운 데이터에서는 성능이 낮아질 수 있다는 점이 모델링에서 가장 조심해야 할 부분이라는 생각이 들었다.

타이타닉 실습은 전체 흐름을 다시 연결해볼 수 있어서 좋았다. 데이터 로드부터 EDA, 이상치 처리, 결측치 처리, 인코딩, 스케일링, 모델 학습, 예측, 제출 파일 생성까지 이어지니 이전에 배운 내용들이 따로따로 있는 게 아니라 하나의 과정으로 이어진다는 느낌이 들었다.

특히 train 데이터와 test 데이터에 같은 전처리 흐름을 적용해야 한다는 점이 기억에 남았다. train에서는 Age를 채우고, Sex를 인코딩하고, Fare를 스케일링했는데 test에는 다르게 처리하면 모델이 제대로 예측할 수 없다. 전처리는 그냥 데이터를 예쁘게 정리하는 단계가 아니라, 모델 입력을 일관되게 만드는 과정이었다.

교차 검증은 처음에는 조금 복잡해 보였지만, 결국 여러 번 나눠서 더 안정적으로 평가하자는 개념이었다. 한 번의 train/test split 결과만 보고 모델이 좋다고 판단하면 위험할 수 있기 때문에, 여러 fold를 돌려보는 방식이 더 신뢰도 있는 평가로 이어진다는 점이 이해됐다.

GridSearch는 모델 튜닝을 자동화하는 도구처럼 느껴졌다. 사람이 직접 여러 설정을 바꿔가며 실험할 수도 있지만, GridSearch를 쓰면 후보 조합을 정해두고 자동으로 비교할 수 있다. 모델을 잘 만드는 것도 중요하지만, 좋은 설정을 찾는 과정도 모델링의 일부라는 생각이 들었다.

오늘 내용은 화려한 알고리즘을 새로 배우는 느낌보다는, 머신러닝 프로젝트의 기본 뼈대를 다지는 느낌이었다. 데이터를 나누고, 검증하고, 튜닝하는 과정이 있어야 모델 성능을 더 믿을 수 있다는 점이 오늘 가장 크게 남았다.