검색
← 모듈 로드맵

직접 해 보기: scikit-learn으로 손글씨 숫자 분류기 만들기

강의 · 입문자 · KAITABASE 정리

load_digits로 기준선 · 교차 검증 · GridSearchCV · 혼동 행렬 · 오류 분석까지 실행하는 프로젝트

먼저 알아야 할 것

파이썬 기초scikit-learn fit · predict교차 검증혼동 행렬SVM · 랜덤 포레스트 개념

다루는 내용

scikit-learn 내장 8×8 손글씨 숫자 1,797장으로 분류기를 처음부터 끝까지 만드는 실습입니다. 층화 분리와 기준선(0.1), 로지스틱 회귀 · SVM · 랜덤 포레스트의 5겹 교차 검증 비교, GridSearchCV 탐색, 테스트 정확도 0.975와 혼동 행렬 · 틀린 예 분석을 실제 실행 출력과 함께 보여 줍니다. 표준화 유무와 학습 데이터 양을 바꾼 추가 실험으로 전처리도 검증 대상임을 확인합니다.

해설

직접 해 보기: scikit-learn으로 손글씨 숫자 분류기 만들기

머신러닝 트랙에서 배운 것 — 데이터 나누기, 기준선, 여러 모델 비교, 교차 검증, 하이퍼파라미터 탐색, 혼동 행렬, 오류 분석 — 을 코드 한 벌로 묶습니다. scikit-learn에 들어 있는 8×8 손글씨 숫자 1,797장을 쓰므로 인터넷에서 아무것도 내려받지 않아도 됩니다. 아래 출력은 모두 실제로 실행한 결과입니다.

항목내용
참고 자료scikit-learn 예제 'Recognizing hand-written digits' · scikit-learn.org/stable/auto_examples/classification/plot_digits_classification.html
트랙 · 수준머신러닝 · 기초(프로젝트)
만드는 것숫자 0~9를 맞히는 분류기, 모델 비교표, 혼동 행렬, 틀린 예 목록
걸리는 시간코드 실행 1분 이내(노트북 CPU 기준), 읽고 바꿔 보기 1~2시간
먼저 알면 좋은 것파이썬 기초, 로지스틱 회귀 · SVM · 랜덤 포레스트의 개념, 혼동 행렬 · 교차 검증

들어가기 전에

한눈에 보기

  • 데이터 모양을 확인하고 "아무것도 안 배운 모델"의 점수(기준선)부터 잽니다.
  • 로지스틱 회귀 · SVM · 랜덤 포레스트를 같은 5겹 교차 검증으로 공정하게 비교합니다.
  • GridSearchCV로 SVM의 C · gamma를 고르고, 테스트 데이터는 맨 마지막에 한 번만 씁니다.
  • 혼동 행렬과 틀린 예를 보고 어떤 숫자끼리 헷갈리는지 해석합니다.
  • 전처리(표준화)도 하이퍼파라미터처럼 검증해야 한다는 것을 실험으로 확인합니다.

준비물

항목이 글을 실행한 환경
파이썬3.14.3 (Windows, CPU)
라이브러리scikit-learn 1.9.0, numpy 2.5.1
데이터sklearn.datasets.load_digits (패키지에 포함, 내려받기 없음)
난수 시드random_state=42로 고정

설치가 안 되어 있다면 pip install scikit-learn이면 numpy도 함께 설치됩니다. 아래 코드를 digits_project.py 한 파일에 차례로 붙여 실행하면 됩니다.

mermaid
flowchart LR
  A[load_digits 1797장] --> B[학습 80% · 테스트 20% 층화 분리]
  B --> C[기준선: 최빈 클래스]
  B --> D[세 모델 5겹 교차 검증]
  D --> E[GridSearchCV로 SVM C · gamma 탐색]
  E --> F[최적 모델을 학습 데이터 전체로 다시 학습]
  F --> G[테스트로 한 번 평가: 정확도 · 혼동 행렬]
  G --> H[틀린 예 분석 · 다음 실험 계획]

1단계: 데이터 살펴보기

python
import numpy as np
from sklearn.datasets import load_digits
digits = load_digits()
X, y = digits.data, digits.target
print("X 모양:", X.shape, "y 모양:", y.shape)
print("픽셀 값 범위:", X.min(), "~", X.max())
print("클래스별 개수:", np.bincount(y))
print("첫 이미지(8x8):")
print(digits.images[0].astype(int))
print("첫 라벨:", y[0])

실제 출력:

코드
X 모양: (1797, 64) y 모양: (1797,)
픽셀 값 범위: 0.0 ~ 16.0
클래스별 개수: [178 182 177 183 181 182 181 179 174 180]
첫 이미지(8x8):
[[ 0  0  5 13  9  1  0  0]
 [ 0  0 13 15 10 15  5  0]
 [ 0  3 15  2  0 11  8  0]
 [ 0  4 12  0  0  8  8  0]
 [ 0  5  8  0  0  9  8  0]
 [ 0  4 11  0  1 12  7  0]
 [ 0  2 14  5 10 12  0  0]
 [ 0  0  6 13 10  0  0  0]]
첫 라벨: 0

해석: 이미지 한 장은 8×8 = 64개 픽셀이고, 이것을 한 줄로 편 64차원 벡터가 특성입니다. 데이터 설명서에 따르면 32×32 비트맵을 4×4 칸으로 나눠 켜진 점을 센 값이라 0~16 정수입니다. 숫자 0의 동그라미 모양이 숫자 배열에서도 보입니다. 클래스는 거의 균형(174~183장)이라 정확도를 주 지표로 써도 무리가 없습니다.

2단계: 학습 · 테스트 나누기와 기준선

python
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, stratify=y, random_state=42)
print("학습:", X_train.shape, "테스트:", X_test.shape)

from sklearn.dummy import DummyClassifier
dummy = DummyClassifier(strategy="most_frequent").fit(X_train, y_train)
print("기준선(최빈 클래스) 정확도:", round(dummy.score(X_test, y_test), 4))

실제 출력:

코드
학습: (1437, 64) 테스트: (360, 64)
기준선(최빈 클래스) 정확도: 0.1

왜 이렇게 하나: stratify=y는 학습 · 테스트 모두에서 숫자별 비율이 같도록 나눕니다. 기준선 0.1(10%)은 "늘 같은 숫자로 찍기"의 점수입니다. 어떤 모델이든 이보다 확실히 높아야 의미가 있습니다. 테스트 데이터 360장은 이제 6단계 전까지 쓰지 않습니다.

3단계: 세 모델을 교차 검증으로 비교

python
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score, StratifiedKFold
models = {
    "로지스틱 회귀": make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000)),
    "SVM(RBF)": make_pipeline(StandardScaler(), SVC()),
    "랜덤 포레스트": RandomForestClassifier(random_state=42),
}
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for name, model in models.items():
    scores = cross_val_score(model, X_train, y_train, cv=cv)
    print(f"{name}: 5겹 평균 {scores.mean():.4f} (표준편차 {scores.std():.4f})")

실제 출력:

코드
로지스틱 회귀: 5겹 평균 0.9680 (표준편차 0.0074)
SVM(RBF): 5겹 평균 0.9812 (표준편차 0.0061)
랜덤 포레스트: 5겹 평균 0.9729 (표준편차 0.0081)

해석과 설계 이유

  • 셋 모두 기준선 0.1을 크게 넘습니다. 이 데이터에서는 SVM이 가장 높고, 차이(약 1~1.3%p)는 표준편차보다 큽니다.
  • 파이프라인(make_pipeline)을 쓰면 교차 검증의 각 겹에서 표준화의 평균 · 표준편차를 그 겹의 학습 부분으로만 계산합니다. 전체 데이터로 먼저 표준화하면 검증 부분 정보가 새어 들어갑니다(특성 공학의 누수 이야기).
  • 랜덤 포레스트는 트리라 스케일에 영향을 받지 않아 표준화를 넣지 않았습니다.
  • 로지스틱 회귀는 기본 max_iter=100에서는 수렴 경고가 날 수 있어 2000으로 늘렸습니다.

4단계: GridSearchCV로 SVM 조율하기

python
from sklearn.model_selection import GridSearchCV
pipe = make_pipeline(StandardScaler(), SVC())
grid = {"svc__C": [0.1, 1, 10, 100], "svc__gamma": ["scale", 0.001, 0.01, 0.1]}
search = GridSearchCV(pipe, grid, cv=cv, n_jobs=1)
search.fit(X_train, y_train)
print("최적 설정:", search.best_params_)
print("최적 교차 검증 정확도:", round(search.best_score_, 4))

실제 출력:

코드
최적 설정: {'svc__C': 1, 'svc__gamma': 'scale'}
최적 교차 검증 정확도: 0.9812

해석: 16가지 조합 × 5겹 = 80번 학습해 가장 좋은 조합을 골랐는데, 결과는 scikit-learn 기본값(C=1.0, gamma='scale')과 같았습니다. 기본값이 이미 좋은 출발점이라는 뜻입니다. C는 오분류 벌점의 세기, gamma는 RBF 커널의 폭입니다(서포트 벡터 머신과 커널). 파이프라인 안의 설정은 단계이름__설정이름 형식으로 지정합니다. GridSearchCV는 기본적으로 최적 조합을 찾은 뒤 학습 데이터 전체로 다시 학습한 모델을 best_estimator_에 둡니다.

5단계: 테스트로 한 번 최종 평가

python
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
best = search.best_estimator_
y_pred = best.predict(X_test)
print("테스트 정확도:", round(accuracy_score(y_test, y_pred), 4))
print("혼동 행렬(행=정답, 열=예측):")
print(confusion_matrix(y_test, y_pred))
print(classification_report(y_test, y_pred, digits=3))

실제 출력(분류 보고서는 일부만):

코드
테스트 정확도: 0.975
혼동 행렬(행=정답, 열=예측):
[[36  0  0  0  0  0  0  0  0  0]
 [ 0 35  0  0  1  0  0  0  0  0]
 [ 0  0 35  0  0  0  0  0  0  0]
 [ 0  0  0 37  0  0  0  0  0  0]
 [ 0  0  0  0 35  0  0  1  0  0]
 [ 0  0  0  0  0 37  0  0  0  0]
 [ 0  0  0  0  0  0 36  0  0  0]
 [ 0  0  0  0  0  1  0 35  0  0]
 [ 0  2  0  0  1  0  0  0 32  0]
 [ 0  0  0  0  0  0  1  2  0 33]]
              precision    recall  f1-score   support
           7      0.921     0.972     0.946        36
           8      1.000     0.914     0.955        35
           9      1.000     0.917     0.957        36
    accuracy                          0.975       360
   macro avg      0.976     0.975     0.975       360

해석

  • 테스트 정확도 0.975(360장 중 351장)로, 교차 검증 추정 0.981과 가깝습니다. 교차 검증이 테스트 성능을 잘 예측했다는 뜻입니다.
  • 혼동 행렬의 대각선 밖이 오류입니다. 8을 1로 2번, 9를 7로 2번 틀린 것이 가장 많습니다.
  • 8과 9는 정밀도는 1.000이지만 재현율이 0.91대입니다. "8이라고 하면 늘 맞지만 8을 다 찾지는 못한다"는 뜻입니다. 7은 반대로 다른 숫자가 7로 잘못 들어와 정밀도가 0.921로 가장 낮습니다.

6단계: 틀린 예와 모델이 보는 곳

python
wrong = np.where(y_pred != y_test)[0]
print("틀린 개수:", len(wrong))
for i in wrong:
    print(f"  테스트 #{i}: 정답 {y_test[i]} -> 예측 {y_pred[i]}")

rf = RandomForestClassifier(random_state=42).fit(X_train, y_train)
imp = rf.feature_importances_.reshape(8, 8)
print("픽셀 중요도(x1000, 반올림):")
print(np.round(imp * 1000).astype(int))

실제 출력:

코드
틀린 개수: 9
  테스트 #51: 정답 8 -> 예측 1
  테스트 #93: 정답 9 -> 예측 7
  테스트 #115: 정답 4 -> 예측 7
  테스트 #127: 정답 8 -> 예측 1
  테스트 #135: 정답 9 -> 예측 6
  테스트 #141: 정답 7 -> 예측 5
  테스트 #145: 정답 9 -> 예측 7
  테스트 #228: 정답 1 -> 예측 4
  테스트 #254: 정답 8 -> 예측 4
픽셀 중요도(x1000, 반올림):
[[ 0  2 16 10 10 20 11  1]
 [ 0 12 27  6 15 28  4  0]
 [ 0  7 20 25 31 51  9  0]
 [ 0 17 42 27 33 26 26  0]
 [ 0 25 29 16 47 19 31  0]
 [ 0 11 38 38 22 20 14  0]
 [ 0  2 16 23 13 23 26  2]
 [ 0  2 25 10 24 30 14  4]]

해석: 틀린 예의 번호로 digits.images에서 해당 이미지를 꺼내 matplotlib.pyplot.imshow로 그려 보면, 사람이 봐도 애매한 글씨인지 모델이 놓친 것인지 판단할 수 있습니다. 랜덤 포레스트 중요도를 보면 맨 왼쪽 · 오른쪽 열은 거의 0입니다. 글씨가 가운데 쓰여 가장자리 픽셀에는 정보가 없기 때문입니다. 가운데 칸들이 숫자를 구분하는 데 주로 쓰입니다.

7단계: 바꿔 보기 실험(실제 실행)

같은 분할로 두 가지를 더 시험했습니다.

python
print("SVM 스케일링 없음:", round(cross_val_score(SVC(), X_train, y_train, cv=cv).mean(), 4))
for n in [50, 100, 200, 400, 800, 1437]:
    m = make_pipeline(StandardScaler(), SVC()).fit(X_train[:n], y_train[:n])
    print(f"학습 {n}개 -> 테스트 정확도 {m.score(X_test, y_test):.4f}")

실제 출력:

코드
SVM 스케일링 없음: 0.9882
학습 50개 -> 테스트 정확도 0.7083
학습 100개 -> 테스트 정확도 0.8694
학습 200개 -> 테스트 정확도 0.9139
학습 400개 -> 테스트 정확도 0.9389
학습 800개 -> 테스트 정확도 0.9667
학습 1437개 -> 테스트 정확도 0.9750

해석

  • 표준화가 늘 이득은 아닙니다. 이 데이터에서는 표준화하지 않은 SVM의 교차 검증 정확도(0.9882)가 표준화한 것(0.9812)보다 높았습니다. 픽셀은 모두 같은 단위(0~16)라 스케일을 맞출 필요가 적고, 거의 늘 0인 가장자리 픽셀까지 표준화로 키우면 잡음이 커질 수 있기 때문으로 보입니다(해석은 추정). 교훈은 "전처리도 검증 데이터로 고르는 선택지"라는 것입니다. 이 결과를 보고 표준화 여부를 그리드에 넣어 다시 탐색해 볼 수 있습니다.
  • 데이터가 많을수록 좋아집니다. 학습 50장이면 71%, 1,437장이면 97.5%입니다. 곡선이 아직 오르는 중이므로 데이터를 더 모으면 더 좋아질 여지가 있습니다. 이것이 학습 곡선(learning curve)의 생각입니다.

흔한 오류와 해결

증상원인해결
ConvergenceWarning (로지스틱 회귀)반복 횟수 부족max_iter를 늘리거나 표준화
테스트 점수가 교차 검증보다 크게 낮음테스트를 보며 설정을 여러 번 바꿈, 또는 누수테스트는 마지막 한 번만, 전처리는 파이프라인 안에
ValueError: Invalid parameter (GridSearchCV)파이프라인 단계 이름 누락svc__C처럼 단계이름__설정 사용
결과가 실행마다 다름난수 시드 미고정random_state 고정

더 해 볼 과제

  1. 표준화 여부를 GridSearchCV의 선택지로 넣어 보세요({"scaler": [StandardScaler(), "passthrough"]} 형식, 파이프라인을 Pipeline([("scaler", ...), ("svc", SVC())])로 직접 구성).
  2. k-최근접 이웃(KNeighborsClassifier)과 그래디언트 부스팅(HistGradientBoostingClassifier)을 비교표에 넣어 보세요.
  3. PCA로 64차원을 20차원으로 줄인 뒤 SVM을 돌려 정확도와 학습 시간을 비교해 보세요.
  4. 틀린 9장을 그림으로 그려, 사람이 봐도 헷갈리는지 직접 판정해 보세요.

스스로 점검

  1. 기준선 정확도를 먼저 재는 이유는?
    • 답: 모델이 실제로 무언가를 배웠는지 판단할 기준이 필요하기 때문입니다. 이 데이터에서 "늘 같은 숫자"는 10%이므로 97%는 큰 향상입니다.
  2. 표준화를 파이프라인 안에 넣는 이유는?
    • 답: 교차 검증의 각 겹에서 학습 부분만으로 평균 · 표준편차를 계산해, 검증 부분의 정보가 새어 들어가지 않게 하려는 것입니다.
  3. 7단계 결과에서 얻을 수 있는 교훈 두 가지는?
    • 답: 전처리 선택도 검증으로 정해야 한다는 것, 그리고 학습 데이터 양이 늘수록 성능이 오르므로 데이터를 더 모으는 것이 한 방법이라는 것입니다.

용어 정리

용어쉬운 뜻
기준선(baseline)비교의 출발점이 되는 가장 단순한 모델의 점수
층화 분리(stratified split)클래스 비율을 유지하며 데이터를 나누기
파이프라인(pipeline)전처리와 모델을 한 덩어리로 묶어 함께 학습 · 예측
교차 검증(cross-validation)데이터를 여러 겹으로 나눠 돌아가며 검증
그리드 탐색(grid search)하이퍼파라미터 조합을 전부 시험해 최고를 고르기
혼동 행렬(confusion matrix)정답 × 예측 개수를 센 표
분류 보고서(classification report)클래스별 정밀도 · 재현율 · F1 요약
특성 중요도(feature importance)모델이 각 특성을 얼마나 사용했는지
학습 곡선(learning curve)학습 데이터 양에 따른 성능 변화

다음에 볼 것

라이선스

이 글의 본문과 코드는 KAITABASE가 직접 작성했으며, 실행 출력은 위 환경에서 실제로 돌린 결과입니다. 참고한 scikit-learn 예제와 문서는 BSD-3-Clause 라이선스입니다. 숫자 데이터는 scikit-learn에 포함된 UCI 손글씨 숫자 데이터셋(E. Alpaydin 제작)의 사본입니다.

출처

  • 참고: scikit-learn.org/stable/auto_examples/cla… (같은 데이터로 SVC 분류하는 공식 예제 — 이 글은 분할 · 모델 비교 · 탐색을 새로 구성)
  • 데이터 설명: scikit-learn 1.9.0 load_digits().DESCR (1,797장, 64특성, 0~16 정수, 32×32 비트맵을 4×4 칸으로 센 값)
  • 기본값 확인: scikit-learn 1.9.0 SVC(C=1.0, gamma='scale') · LogisticRegression(max_iter=100) 생성자 서명

AI 원리부터 이해하기 · 머신러닝 모듈

머신러닝 모듈 학습

이 글은 머신러닝 모듈의 학습 자료입니다. 전체 17개 자료 중 14번째입니다.

AI 기초 과정은 현재 진도 저장을 지원하지 않습니다.

출처·원문

원문 보기

요약만

본문 · 코드 자체 작성 · 참고 자료 scikit-learn 예제(BSD-3)