25일차는 머신러닝 개념을 코드로 직접 연결해보는 흐름이었다. 전날에는 머신러닝이 결국 Y = f(X)에서 좋은 함수와 파라미터를 찾는 과정이라는 쪽에 가까웠다면, 이번에는 그 과정을 scikit-learn으로 어떻게 실제 코드화하는지 봤다. 데이터 준비, train/test 분리, 모델 선택, .fit(), .predict(), 평가까지 이어지는 기본 흐름을 손글씨 숫자 데이터로 실습했다.
앞부분에서는 scikit-learn의 전반적인 역할과 전처리의 중요성을 다시 짚었다. 머신러닝 모델은 결국 수학적인 함수를 최적화하는 구조라서, 문자열은 숫자로 바꿔야 하고 결측치는 그대로 둘 수 없다. 뒤쪽에서는 KNN, Decision Tree, Random Forest, Boosting처럼 전통적인 머신러닝 알고리즘이 어떤 아이디어로 움직이는지 봤다. 특히 KNN은 “가까운 친구를 보고 판단한다”는 개념이고, Decision Tree는 “어떤 변수로 나누면 가장 잘 갈라지는가”를 계산하는 구조였다.
1. scikit-learn은 머신러닝 프로세스를 코드로 묶어둔 패키지였다
Python에서 머신러닝을 할 때 기본적으로 많이 쓰는 패키지가 scikit-learn이었다. Anaconda나 Colab에는 기본 설치되어 있어서, Selenium처럼 별도로 설치하기보다는 바로 import해서 사용하는 흐름이었다. 강의록에서도 머신러닝은 scikit-learn을 기본으로 사용하고, 딥러닝은 TensorFlow나 PyTorch를 활용하는 흐름으로 정리되어 있었다.
scikit-learn은 단순히 모델만 제공하는 게 아니라, 머신러닝 프로세스에 필요한 여러 기능을 모듈별로 제공한다. 데이터셋 불러오기, train/test 분리, 전처리, 모델, 평가 지표까지 들어 있다. 이날 코드에서도 필요한 모듈을 먼저 불러왔다.
import numpy as np
import pandas as pd
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
이 구조가 머신러닝의 기본 흐름과 거의 대응됐다.
데이터 준비
→ train/test 분리
→ 모델 선택
→ 학습
→ 예측
→ 평가
이번에는 직접 수집한 데이터가 아니라 scikit-learn에서 제공하는 load_digits() 데이터를 사용했다. 원래 MNIST는 28×28 손글씨 숫자 이미지인데, 여기서는 간단한 실습을 위해 8×8 형태의 숫자 이미지 데이터를 사용했다. 코드 파일에서도 load_digits() 데이터는 샘플 1797개, feature 64개를 가진 2D matrix로 정리되어 있었다.
2. 전처리는 모델이 돌아가기 위한 기본 조건이었다
머신러닝 모델은 결국 수학 문제를 푸는 구조이기 때문에, 모델에 들어가는 데이터는 계산 가능한 형태여야 한다. 그래서 전처리에서 가장 기본적으로 신경 써야 할 것이 두 가지였다. 하나는 문자열을 숫자로 바꾸는 encoding이고, 다른 하나는 결측치를 처리하는 것이다.
머신러닝 전처리에서 먼저 봐야 할 것
1. 숫자가 아닌 값은 숫자로 바꿔야 한다.
2. 결측치는 채우거나 제거해야 한다.
예를 들어 성별이 남자, 여자처럼 들어가 있거나 지역이 서울, 경기, 부산처럼 들어가 있으면 모델은 그대로 계산할 수 없다. 결국 인코딩을 통해 숫자로 바꿔야 한다. 또 행렬 연산을 해야 하는데 중간에 빈 값이 있으면 계산이 깨지기 때문에, 결측치는 삭제하거나 평균/중앙값/특정 값으로 채워야 한다.
이 부분은 Pandas 단계에서 봤던 데이터 핸들링과 바로 연결됐다. 머신러닝 모델 자체도 중요하지만, 그 전에 데이터를 모델이 받을 수 있는 형태로 만드는 작업이 더 기본이었다. 강의록에서도 Pandas는 데이터 핸들링의 기본이고, 데이터 핸들링이 자유롭지 못하면 ML/DL 단계에서 제약이 생긴다고 정리되어 있었다.
3. digits 데이터는 이미지지만, 모델에는 2D matrix로 들어갔다
load_digits()로 데이터를 불러오면 딕셔너리 비슷한 구조로 데이터가 들어 있다. 여기서 data["data"]는 입력 feature이고, data["target"]은 정답이다.
data = load_digits()
data["data"].shape
(1797, 64)
data["target"].shape
(1797,)
여기서 data["data"]는 1797개의 샘플과 64개의 feature를 가진 2D matrix였다. 8×8 이미지를 한 줄로 펼치면 64개의 숫자가 되기 때문이다.
X = data["data"]
y = data["target"]
X.shape
y.shape
(1797, 64)
(1797,)
원래 이미지는 8×8 형태인데, 전통적인 머신러닝 모델에 넣기 위해 1D vector로 펼쳐져 있었다. 그래서 하나의 샘플은 (64,) 형태가 된다.
train_X[0].shape
(64,)
이 부분은 전날 배운 전통적인 머신러닝의 제약과 연결됐다. 전통적인 머신러닝은 한 샘플이 1D vector 형태로 들어가고, 전체 데이터셋은 2D matrix 형태로 들어가는 것이 기본이다. 이미지처럼 원래 공간 구조가 있는 데이터도 KNN 같은 전통 ML에 넣으려면 일단 vector로 펴야 한다.
그래도 사람이 보기에는 64개 숫자만 보면 감이 안 오기 때문에, 다시 8×8로 reshape해서 heatmap으로 확인했다.
import seaborn as sns
sns.heatmap(train_X[0].reshape(8, 8))
train_y[0]
np.int64(1)
heatmap으로 보니 숫자 1처럼 보이는 이미지였다. 모델은 64개의 숫자로 보지만, 사람은 다시 8×8로 봐야 이미지라는 느낌이 살아났다.
4. train/test 분리는 모델 평가를 위해 필요했다
전체 데이터를 그대로 학습에 다 쓰면, 모델이 처음 보는 데이터에 얼마나 잘하는지 알 수 없다. 그래서 학습용 train과 평가용 test를 나눴다. 강의록에서도 Test Set은 절대로 학습에 사용되면 안 되고, overfitting을 피하기 위해 train/validation/test를 나누어 사용한다고 정리되어 있었다.
train_X, test_X, train_y, test_y = train_test_split(
X,
y,
test_size=0.25,
random_state=1234
)
여기서 test_size=0.25는 전체 데이터의 약 25%를 평가용으로 사용하겠다는 뜻이다. 실제로 길이를 확인해보면 train은 약 75%, test는 약 25%로 나뉜다.
len(X)
1797
len(train_X)
len(train_X) / len(X)
len(test_X) / len(X)
1347
0.7495826377295493
0.25041736227045075
random_state=1234도 중요했다. train/test를 랜덤으로 나누기 때문에, seed를 고정하지 않으면 실행할 때마다 train과 test에 들어가는 샘플이 달라질 수 있다. 그러면 같은 코드인데도 성능이 조금씩 달라져서, 모델이 좋아진 건지 단순히 데이터 분리가 달라진 건지 헷갈릴 수 있다. 강의에서도 이 재현성 문제 때문에 random state를 지정해야 한다는 점을 강조했다.
5. 정답 분포를 먼저 확인하고 평가 지표를 생각했다
모델을 돌리기 전에 train_y에 어떤 정답이 있는지 확인했다.
np.unique(train_y)
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
정답은 0부터 9까지 총 10종류였다. 각 숫자가 몇 개씩 있는지도 확인했다.
np.unique(train_y, return_counts=True)
(array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
array([141, 135, 128, 139, 129, 133, 136, 139, 131, 136]))
각 클래스가 대략 130~140개 정도로 비슷했다. 즉 한쪽으로 심하게 치우친 데이터는 아니었다. 그래서 이번 실습에서는 accuracy를 평가 지표로 사용해도 큰 무리는 없다고 판단할 수 있었다.
이 부분이 전날 배운 Confusion Matrix와 연결됐다. 만약 암 진단이나 스팸 분류처럼 특정 클래스가 매우 적은 데이터라면 accuracy만 보면 위험할 수 있다. 그런데 이번 digits 데이터는 클래스 분포가 비교적 균형적이어서 accuracy를 baseline 평가 지표로 쓰기 괜찮아 보였다.
6. KNN 모델을 만들고
.fit()
으로 학습했다
이번에 사용한 모델은 KNN이었다. KNN은 “나와 가까운 데이터들을 보고 판단한다”는 방식이다. 즉, 새로운 샘플이 들어왔을 때 주변에 있는 k개의 샘플을 보고 그중 다수가 어떤 정답을 가지고 있는지 확인한다. 강의록에서도 KNN은 “끼리끼리”, “초록은 동색” 같은 일상적인 개념을 모델링한 방식이고, 거리 계산을 통해 판단하는 instance model이라고 설명되어 있었다.
knn = KNeighborsClassifier(n_neighbors=5, n_jobs=-1)
여기서 n_neighbors=5는 가까운 이웃 5개를 보겠다는 뜻이다. n_jobs=-1은 가능한 CPU를 활용하겠다는 옵션이다. 코드 메모에도 n_neighbors는 모델 자체의 특성을 결정하는 세부 파라미터이고, n_jobs는 계산 효율화와 관련된 옵션으로 정리되어 있었다.
이후 train 데이터를 이용해 모델을 학습했다.
knn.fit(train_X, train_y)
여기서 .fit()은 내가 정한 KNN이라는 함수 스타일에 대해, train 데이터에 맞게 내부적으로 필요한 정보를 준비하는 과정이다. KNN은 선형회귀처럼 명시적인 weight를 학습한다기보다는, train 데이터를 기준으로 거리 계산을 할 준비를 해둔다. 그래도 scikit-learn에서는 공통적으로 .fit()이라는 메서드를 사용한다.
7. 처음 보는 test 데이터로 예측하고 accuracy를 계산했다
학습이 끝났으면 test 데이터로 예측했다. test 데이터는 학습에 사용하지 않은 데이터다.
pred = knn.predict(test_X)
pred
이제 pred에는 test_X에 대한 모델의 예측값이 들어간다. 그 다음 실제 정답인 test_y와 비교해서 accuracy를 계산했다.
accuracy_score(test_y, pred)
0.9822222222222222
결과는 약 0.98이었다. 즉 test 데이터 기준으로 약 98% 정도를 맞춘 셈이다. 코드 메모에서도 KNN에서 k=5일 때 성능이 0.98 정도로 나왔고, 이를 baseline으로 잡은 뒤 k 값을 조절하면서 더 좋은 값을 찾아볼 수 있다고 정리되어 있었다.
KNN, k=5
accuracy ≈ 0.98
→ baseline으로 사용 가능
→ 이후 k 값을 조절하면서 HPT 가능
다만 여기서 바로 “KNN이 좋다”로 끝내면 안 된다. 메모에도 실제 원본 MNIST 28×28 데이터에서는 KNN 성능이 이 정도로 나오지 않고, 현실적으로 KNN을 자주 쓰는 모델로 보기는 어렵다고 적혀 있었다. 오히려 실제 ML에서는 Tree 기반 모델인 Random Forest, XGBoost, LightGBM 같은 모델이 더 자주 사용된다는 내용이 같이 정리되어 있었다.
8. KNN에서 k값은 모델 복잡도를 조절한다
KNN에서 중요한 것은 “가깝다”의 기준과 “몇 명을 볼 것인가”였다. 몇 명을 볼 것인지가 k다. k가 너무 작으면 아주 가까운 몇 명만 보고 판단하므로 입력이 조금만 달라져도 결과가 흔들릴 수 있다. 반대로 k가 너무 크면 너무 많은 사람의 의견을 평균처럼 보게 되어 경계가 둔해진다.
k가 작다
→ 가까운 일부 데이터에 민감
→ overfitting 쪽으로 갈 수 있음
k가 크다
→ 더 많은 데이터를 두루 반영
→ underfitting 쪽으로 갈 수 있음
이 부분은 bias-variance와 연결됐다. bias가 크면 모델이 너무 단순해서 제대로 학습하지 못한 상태이고, variance가 크면 모델이 train 데이터에 너무 민감하게 반응하는 상태다. 강의에서는 KNN에서 k값을 줄이면 더 타이트하게 보는 것이고, k값을 늘리면 더 느슨하게 보는 것이라고 설명했다.
결국 k=5가 최적인지는 따로 확인해야 한다. 이때 모든 조합을 다 실험하는 Grid Search, 일부만 랜덤하게 보는 Randomized Search, Bayesian Optimization 기반의 Optuna나 Ray Tune 같은 방법으로 hyperparameter tuning을 할 수 있다는 흐름도 같이 언급됐다.
9. Weighted KNN은 가까운 이웃에게 더 큰 가중치를 주는 아이디어였다
KNN은 기본적으로 가까운 k명의 이웃에게 동일하게 한 표씩 주는 방식이다. 그런데 실제로는 가까운 이웃과 조금 먼 이웃을 똑같이 보는 것이 애매할 수 있다. 그래서 Weighted KNN에서는 더 가까운 이웃에게 더 큰 가중치를 준다.
강의에서는 시험 답을 주변 사람에게 물어보는 비유가 나왔다. 4명 중 3명이 1번이라고 하고, 1명이 2번이라고 했더라도 그 1명이 전교 1등이라면 그냥 다수결로 1번을 고르기엔 찝찝해진다. 즉 모든 의견을 동일하게 보는 것이 아니라, 신뢰도나 가까움에 따라 가중치를 다르게 줄 수 있다는 것이다.
KNN에서는 이 가중치를 거리로 줄 수 있다. 가까울수록 거리가 작으므로, 작은 값을 큰 가중치로 바꾸기 위해 거리의 역수를 사용할 수 있다.
거리 작음
→ 더 가까움
→ 더 큰 가중치 필요
가중치 예시
= 1 / distance
이 아이디어는 뒤에서 ensemble의 voting으로도 이어졌다. 샘플 레벨에서 가까운 이웃의 의견을 종합하는 것이 KNN이라면, 모델 레벨에서 여러 모델의 의견을 종합하는 것이 voting이나 ensemble의 아이디어와 닮아 있었다.
10. Decision Tree는 잘 나누는 기준을 찾는 모델이었다
뒤쪽에서는 Decision Tree 개념도 다뤘다. Decision Tree는 데이터를 어떤 기준으로 나누면 가장 잘 분류되는지를 반복해서 찾는 모델이다. 예시로 날씨, 온도, 습도, 바람 정보를 보고 외출할지 말지를 예측하는 데이터가 나왔다. 이때 Outlook, Temperature, Humidity, Wind 중 어떤 변수를 첫 번째 기준으로 삼아야 가장 잘 나뉘는지 계산해야 했다.
분류가 잘 되었는지 수치화하는 방법으로 Gini 계수나 Entropy를 사용할 수 있다. 예를 들어 한 노드에 빨간색만 있으면 완전히 잘 분류된 상태이고, 빨간색과 파란색이 반반 섞여 있으면 분류가 잘 안 된 상태다.
완전히 한 클래스만 있음
→ 불순도 낮음
여러 클래스가 반반 섞임
→ 불순도 높음
Gini 계수는 확률의 제곱을 이용해 계산하고, 값이 작을수록 더 잘 분류된 상태로 볼 수 있다. 예시에서는 Outlook 기준으로 나눴을 때, Sunny, Overcast, Rain 각각에서 yes/no가 얼마나 섞였는지 계산하고, 전체 샘플 비율을 가중치로 반영해 종합적인 Gini 값을 구했다. 그 결과 여러 변수 중 Gini 계수가 가장 낮은 변수를 첫 번째 분기 기준으로 선택하는 흐름이었다.
이 부분에서 Decision Tree가 왜 feature importance를 줄 수 있는지도 연결됐다. 어떤 변수가 먼저 선택되고, 얼마나 분류를 잘했는지 계산하기 때문에, 변수들 사이의 중요도 순서를 어느 정도 볼 수 있다.
11. Tree 계열 모델은 변수 스케일에 덜 민감했다
Decision Tree는 값을 거리 기반으로 비교하는 것이 아니라, 어떤 기준으로 나눴을 때 분류가 잘 되는지를 본다. 그래서 전통적인 선형 모델처럼 변수의 분포나 스케일에 아주 민감하지는 않다. 강의에서도 Tree 계열 모델은 정규화나 스케일링을 반드시 해야 하는 모델은 아니라고 설명했다.
이건 KNN과 차이가 있다. KNN은 거리 기반 모델이므로 feature들의 scale이 다르면 거리 계산이 왜곡될 수 있다. 반면 Decision Tree는 각 변수별로 분기 기준을 찾는 방식이라 scale의 영향을 상대적으로 덜 받는다.
KNN
→ 거리 기반
→ 스케일 영향 큼
Decision Tree
→ 분기 기준 기반
→ 스케일 영향 상대적으로 작음
이런 차이를 알고 있어야 전처리도 모델에 맞게 할 수 있을 것 같다. 모든 모델에 무조건 같은 전처리를 적용하는 게 아니라, 모델이 어떤 방식으로 판단하는지를 보고 전처리를 결정해야 한다.
12. Ensemble은 여러 모델을 같이 쓰는 아이디어였다
마지막에는 ensemble 개념으로 넘어갔다. Ensemble은 하나의 모델만 쓰는 것이 아니라, 여러 모델을 같이 사용해 하나의 결과를 만드는 방식이다. 핵심은 many와 diversity였다. 여러 개가 있어야 하고, 그 여러 개가 서로 달라야 한다. 같은 모델이 같은 방식으로 똑같은 예측을 한다면 여러 개가 있어도 큰 의미가 없다.
Ensemble의 핵심
1. Many
여러 개의 모델을 사용한다.
2. Diversity
서로 다른 관점의 모델을 사용한다.
Voting은 여러 모델의 의견을 투표로 모으는 방식이다. KNN이 샘플 레벨에서 주변 이웃들의 의견을 모았다면, Voting은 모델 레벨에서 여러 모델의 예측을 모은다. 1인 1표처럼 다수결을 하면 hard voting이고, 모델별 확률이나 신뢰도를 반영하면 soft voting에 가깝다.
Bagging은 데이터셋 자체를 여러 개로 펌핑하는 방식이었다. 원본 데이터에서 샘플을 복원추출하고, 컬럼도 일부만 사용해서 조금씩 다른 데이터셋을 만든다. 그 데이터셋들로 여러 개의 모델을 만들고, 이 의견을 다시 종합한다. Random Forest는 이런 bagging 아이디어에 Decision Tree를 결합한 대표적인 모델이었다.
Random Forest
원본 데이터
→ 샘플과 컬럼을 랜덤하게 뽑아 여러 데이터셋 생성
→ 각 데이터셋으로 여러 Decision Tree 생성
→ 여러 Tree의 의견을 종합
Boosting은 여러 모델을 순차적으로 사용한다. 앞 모델이 못 맞춘 부분을 다음 모델이 더 신경 써서 학습하는 식이다. 성능은 잘 나올 수 있지만, 순차적으로 이어지는 구조라 병렬 처리가 어렵고 시간이 오래 걸릴 수 있다. 그래서 XGBoost, LightGBM 같은 모델이 속도와 효율을 개선하면서 많이 쓰이게 됐다는 흐름이었다.
마무리
25일차는 scikit-learn으로 머신러닝의 전체 흐름을 코드로 이어본 날이었다. load_digits()로 데이터를 불러오고, train_test_split()으로 train/test를 나눈 뒤, KNeighborsClassifier를 만들고 .fit(), .predict(), accuracy_score()로 평가했다. 코드 자체는 길지 않았지만, 그 안에 데이터 준비부터 모델 평가까지의 기본 구조가 다 들어 있었다.
개념적으로는 KNN, Decision Tree, Ensemble이 서로 연결됐다. KNN은 가까운 샘플을 보고 판단하는 모델이고, weighted KNN은 가까운 이웃에게 더 큰 가중치를 주는 방식이었다. Decision Tree는 어떤 변수로 나누면 가장 잘 분류되는지 Gini나 Entropy로 판단했고, Random Forest는 여러 Decision Tree를 만들어 의견을 종합했다.
이번에 가장 크게 남은 건 모델을 외우는 것보다, 모델이 어떤 기준으로 판단하는지를 봐야 한다는 점이었다. KNN은 거리, Decision Tree는 분기 기준, Random Forest는 여러 Tree의 종합, Boosting은 이전 모델의 오류 보완이라는 식으로 각 모델의 철학이 달랐다. 그래서 전처리나 평가, hyperparameter tuning도 모델의 성격에 맞춰 생각해야 한다.
25일차는 scikit-learn의 기본 흐름을 따라 데이터를 나누고 KNN을 학습·평가하면서, 전통적인 머신러닝 모델들이 어떤 기준으로 판단하고 어떻게 확장되는지 연결해서 본 날이었다.
'[SK플래닛] ASAC 빅데이터전문가 11기 > 학습기록' 카테고리의 다른 글
| [SK플래닛] ASAC 빅데이터전문가 11기 | 27일차 (0) | 2026.05.27 |
|---|---|
| [SK플래닛] ASAC 빅데이터전문가 11기 | 26일차 (0) | 2026.05.26 |
| [SK플래닛] ASAC 빅데이터전문가 11기 | 24일차 (0) | 2026.05.21 |
| [SK플래닛] ASAC 빅데이터전문가 11기 | 22일차 (0) | 2026.05.20 |
| [SK플래닛] ASAC 빅데이터전문가 11기 | 21일차 (0) | 2026.05.19 |
