🤖

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

⚠️

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

이미지 로딩 중...

MNIST 손글씨 숫자 인식 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 6. · 13 Views

MNIST 손글씨 숫자 인식 완벽 가이드

머신러닝의 "Hello World"라 불리는 MNIST 데이터셋을 활용하여 손글씨 숫자를 인식하는 방법을 배웁니다. 데이터 시각화부터 KNN, SVM, 랜덤 포레스트까지 다양한 분류 알고리즘을 실습하고 혼동 행렬로 모델 성능을 분석합니다.


목차

  1. MNIST_데이터셋_시각화
  2. 픽셀_데이터_전처리
  3. KNN_분류기_적용
  4. SVM_분류기_적용
  5. 랜덤_포레스트_분류
  6. 혼동_행렬_분석

1. MNIST 데이터셋 시각화

김개발 씨는 회사에서 문서 자동화 프로젝트를 맡게 되었습니다. 손으로 작성된 숫자를 인식해야 하는데, 어디서부터 시작해야 할지 막막했습니다.

선배 박시니어 씨가 "MNIST부터 시작해봐요. 머신러닝의 기본이에요"라고 조언해 주었습니다.

MNIST는 손글씨 숫자 0부터 9까지의 이미지 70,000장으로 구성된 데이터셋입니다. 마치 외국어를 배울 때 알파벳부터 시작하듯, 머신러닝을 배울 때 MNIST부터 시작하는 것이 정석입니다.

각 이미지는 28x28 픽셀 크기의 흑백 이미지로, 컴퓨터가 숫자를 학습하기에 적합한 형태입니다.

다음 코드를 살펴봅시다.

from sklearn.datasets import fetch_openml
import matplotlib.pyplot as plt

# MNIST 데이터셋 불러오기
mnist = fetch_openml('mnist_784', version=1, as_frame=False)
X, y = mnist.data, mnist.target

# 첫 번째 이미지를 28x28 형태로 변환하여 시각화
digit_image = X[0].reshape(28, 28)
plt.imshow(digit_image, cmap='gray')
plt.title(f'Label: {y[0]}')
plt.axis('off')
plt.show()

# 여러 숫자를 한 번에 시각화
fig, axes = plt.subplots(2, 5, figsize=(10, 4))
for i, ax in enumerate(axes.flat):
    ax.imshow(X[i].reshape(28, 28), cmap='gray')
    ax.set_title(f'{y[i]}')
    ax.axis('off')
plt.tight_layout()
plt.show()

김개발 씨는 입사 6개월 차 주니어 개발자입니다. 최근 회사에서 수기로 작성된 주문서를 자동으로 처리하는 시스템을 개발해야 했습니다.

고객이 손으로 쓴 숫자를 컴퓨터가 읽어야 하는데, 도대체 어디서부터 시작해야 할까요? 팀의 머신러닝 전문가인 박시니어 씨가 김개발 씨에게 다가왔습니다.

"손글씨 인식을 배우고 싶다면 MNIST부터 시작하세요. 이게 바로 머신러닝계의 'Hello World'예요." 그렇다면 MNIST란 정확히 무엇일까요?

쉽게 비유하자면, MNIST는 마치 글씨 연습장과 같습니다. 아이들이 글씨 쓰기를 배울 때 같은 글자를 여러 번 써보는 연습장이 있듯이, 컴퓨터도 수많은 손글씨 예제를 보면서 숫자를 구분하는 법을 배웁니다.

MNIST에는 0부터 9까지의 숫자가 각각 수천 장씩, 총 70,000장의 이미지가 담겨 있습니다. MNIST 데이터셋이 특별한 이유가 있습니다.

첫째, 모든 이미지가 28x28 픽셀이라는 동일한 크기로 정규화되어 있습니다. 마치 모든 서류가 A4 용지에 맞춰져 있으면 정리하기 쉬운 것처럼, 일정한 크기의 이미지는 컴퓨터가 처리하기 훨씬 수월합니다.

둘째, 흑백 이미지라서 복잡한 색상 정보를 다룰 필요가 없습니다. 각 픽셀은 0부터 255 사이의 값을 가지며, 0은 완전한 검정색, 255는 완전한 흰색을 나타냅니다.

위의 코드를 살펴보겠습니다. 먼저 fetch_openml 함수로 MNIST 데이터를 불러옵니다.

sklearn 라이브러리에서 제공하는 이 함수는 인터넷에서 자동으로 데이터를 다운로드해 줍니다. X에는 이미지 데이터가, y에는 각 이미지가 어떤 숫자인지를 나타내는 레이블이 저장됩니다.

불러온 이미지는 784개의 숫자가 일렬로 나열된 형태입니다. 왜 784일까요?

28 곱하기 28이 바로 784이기 때문입니다. 이것을 다시 28x28 형태로 reshape하면 우리 눈에 익숙한 이미지 형태가 됩니다.

matplotlib의 imshow 함수는 이 숫자 배열을 실제 이미지로 보여줍니다. cmap='gray'를 지정하면 회색조로 표시되어 원본 손글씨와 비슷하게 보입니다.

실제 현업에서 데이터 시각화는 매우 중요합니다. 모델을 학습시키기 전에 데이터가 어떻게 생겼는지 눈으로 확인해야 합니다.

혹시 데이터에 오류가 있거나 이상한 패턴이 있다면, 시각화를 통해 미리 발견할 수 있습니다. "쓰레기가 들어가면 쓰레기가 나온다"는 머신러닝의 격언을 기억하세요.

김개발 씨는 코드를 실행해 보았습니다. 화면에 손으로 쓴 숫자들이 나타나자 신기한 마음이 들었습니다.

"이런 데이터로 컴퓨터가 숫자를 배우는군요!" 박시니어 씨가 고개를 끄덕였습니다. "맞아요.

이제 이 데이터를 어떻게 전처리하고 학습시키는지 알아볼까요?"

실전 팁

💡 - fetch_openml은 처음 실행 시 데이터를 다운로드하므로 시간이 걸릴 수 있습니다

  • 여러 이미지를 한 번에 보려면 subplots를 활용하세요
  • 데이터 분포를 확인하려면 각 숫자별 개수를 세어보는 것도 좋습니다

2. 픽셀 데이터 전처리

김개발 씨가 MNIST 데이터를 시각화해 보니 이미지가 잘 보였습니다. 하지만 박시니어 씨가 "잠깐, 그 데이터를 바로 모델에 넣으면 안 돼요.

전처리가 필요해요"라고 말했습니다. 전처리라니, 데이터를 왜 손봐야 하는 걸까요?

데이터 전처리는 원본 데이터를 머신러닝 모델이 더 잘 학습할 수 있는 형태로 변환하는 과정입니다. 마치 요리 전에 재료를 씻고 손질하듯이, 데이터도 모델에 넣기 전에 정제해야 합니다.

MNIST의 경우 픽셀 값을 0과 1 사이로 정규화하고, 학습용과 테스트용 데이터를 분리하는 것이 핵심입니다.

다음 코드를 살펴봅시다.

from sklearn.model_selection import train_test_split
import numpy as np

# 데이터 타입 확인
print(f'원본 데이터 범위: {X.min()} ~ {X.max()}')

# 정규화: 0-255 범위를 0-1 범위로 변환
X_normalized = X / 255.0
print(f'정규화 후 범위: {X_normalized.min()} ~ {X_normalized.max()}')

# 학습 데이터와 테스트 데이터 분리 (80:20 비율)
X_train, X_test, y_train, y_test = train_test_split(
    X_normalized, y,
    test_size=0.2,
    random_state=42,
    stratify=y  # 클래스 비율 유지
)

print(f'학습 데이터: {X_train.shape[0]}개')
print(f'테스트 데이터: {X_test.shape[0]}개')

김개발 씨는 바로 모델을 학습시키려 했습니다. 하지만 박시니어 씨가 손을 저었습니다.

"잠깐요, 김 개발자. 데이터 전처리를 먼저 해야 해요.

이 과정을 건너뛰면 나중에 큰 문제가 생깁니다." 전처리가 왜 필요한 걸까요? 쉽게 비유하자면, 전처리는 마치 요리 전 재료 손질과 같습니다.

아무리 좋은 재료도 흙이 묻어 있거나 크기가 제각각이면 맛있는 요리가 되기 어렵습니다. 마찬가지로 데이터도 모델에 넣기 전에 깔끔하게 정리해야 좋은 결과를 얻을 수 있습니다.

MNIST 전처리에서 가장 중요한 것은 정규화입니다. 원본 픽셀 값은 0부터 255 사이입니다.

이 값을 그대로 사용하면 어떤 문제가 생길까요? 머신러닝 알고리즘 대부분은 큰 숫자를 다룰 때 계산이 불안정해집니다.

마치 저울로 코끼리와 개미를 동시에 재려는 것처럼, 스케일이 맞지 않으면 정확한 측정이 어렵습니다. 255로 나누면 모든 값이 0과 1 사이로 변환됩니다.

이렇게 하면 모델이 훨씬 안정적으로 학습할 수 있습니다. 작은 변화 하나가 모델 성능을 크게 좌우하기도 합니다.

두 번째로 중요한 것은 데이터 분리입니다. 70,000개의 데이터를 모두 학습에 사용하면 안 됩니다.

왜일까요? 학생이 시험 문제를 미리 알고 공부하면 진짜 실력을 알 수 없는 것처럼, 모델도 한 번도 보지 못한 데이터로 평가해야 진짜 성능을 알 수 있습니다.

train_test_split 함수가 바로 이 역할을 합니다. 데이터를 학습용과 테스트용으로 나눠줍니다.

보통 80대 20 또는 70대 30 비율을 많이 사용합니다. 여기서 stratify 매개변수가 중요합니다.

만약 무작위로 나누면 테스트 데이터에 특정 숫자만 몰릴 수 있습니다. 예를 들어 테스트 데이터에 7이 너무 많고 3이 너무 적다면, 공정한 평가가 어렵습니다.

stratify=y를 지정하면 원본 데이터의 클래스 비율이 학습 데이터와 테스트 데이터에 동일하게 유지됩니다. random_state는 재현성을 위한 것입니다.

같은 숫자를 지정하면 코드를 여러 번 실행해도 항상 같은 방식으로 데이터가 분리됩니다. 실험 결과를 다른 사람과 공유할 때 매우 유용합니다.

김개발 씨가 코드를 실행했습니다. 출력 결과를 보니 학습 데이터 56,000개, 테스트 데이터 14,000개로 깔끔하게 나뉘었습니다.

"이제 진짜 모델을 학습시킬 준비가 된 거네요?" 김개발 씨가 물었습니다. 박시니어 씨가 웃으며 대답했습니다.

"맞아요. 이제 다양한 알고리즘을 적용해 볼 차례예요."

실전 팁

💡 - 정규화는 0-1 범위가 일반적이지만, -1에서 1 범위를 사용하기도 합니다

  • random_state를 고정하면 실험 재현이 가능합니다
  • 데이터가 불균형할 경우 stratify 옵션을 반드시 사용하세요

3. KNN 분류기 적용

박시니어 씨가 첫 번째 알고리즘으로 KNN을 소개했습니다. "KNN은 가장 직관적인 알고리즘이에요.

새로운 데이터가 들어오면 가장 비슷한 이웃들을 찾아서 다수결로 결정하죠." 김개발 씨는 "이웃을 찾는다고요?"라며 고개를 갸웃했습니다.

**KNN(K-Nearest Neighbors)**은 새로운 데이터가 들어왔을 때 가장 가까운 K개의 이웃을 찾아 그들의 다수결로 분류하는 알고리즘입니다. 마치 동네에서 길을 물었을 때 주변 사람들의 의견을 종합하여 결정하는 것과 같습니다.

단순하지만 놀라울 정도로 효과적인 방법입니다.

다음 코드를 살펴봅시다.

from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
import time

# KNN 분류기 생성 (이웃 5개 기준)
knn = KNeighborsClassifier(n_neighbors=5)

# 학습 시간 측정
start_time = time.time()
knn.fit(X_train, y_train)
train_time = time.time() - start_time

# 예측 및 정확도 측정
start_time = time.time()
y_pred = knn.predict(X_test)
pred_time = time.time() - start_time

accuracy = accuracy_score(y_test, y_pred)
print(f'KNN 정확도: {accuracy * 100:.2f}%')
print(f'학습 시간: {train_time:.2f}초')
print(f'예측 시간: {pred_time:.2f}초')

김개발 씨는 KNN이라는 이름을 처음 들어보았습니다. K-Nearest Neighbors, 한국어로 하면 'K개의 가장 가까운 이웃'이라는 뜻입니다.

도대체 이웃이 분류와 무슨 관계가 있는 걸까요? 박시니어 씨가 비유를 들어 설명했습니다.

"새로운 동네로 이사 왔다고 상상해 보세요. 어디서 맛있는 밥을 먹을 수 있는지 알고 싶으면 어떻게 하죠?

가장 가까이 사는 이웃 다섯 명에게 물어보고, 그들이 가장 많이 추천하는 식당에 가면 되잖아요. KNN도 똑같은 원리예요." 새로운 손글씨 숫자가 들어오면, KNN은 학습 데이터 중에서 가장 비슷한 이미지 K개를 찾습니다.

그리고 그 K개 이미지의 레이블을 확인합니다. 5개 중 3개가 '7'이고 2개가 '1'이라면, 다수결에 의해 새 이미지도 '7'로 분류됩니다.

여기서 K 값이 중요합니다. K가 1이면 가장 가까운 이웃 하나의 의견만 듣습니다.

이러면 노이즈에 취약해집니다. 반대로 K가 너무 크면 멀리 있는 이웃의 의견까지 반영되어 정확도가 떨어질 수 있습니다.

보통 5나 7 같은 홀수를 사용합니다. 홀수를 쓰는 이유는 다수결에서 동점을 피하기 위해서입니다.

코드를 살펴보면, fit 메서드로 학습을 진행합니다. 하지만 여기서 재미있는 점이 있습니다.

KNN은 사실 학습 단계에서 아무것도 하지 않습니다. 그냥 데이터를 저장해 둘 뿐입니다.

진짜 계산은 predict 단계에서 일어납니다. 새 데이터가 들어올 때마다 모든 학습 데이터와의 거리를 계산해야 하기 때문입니다.

이것이 KNN의 장점이자 단점입니다. 학습이 빠르다는 장점이 있지만, 예측이 느리다는 단점이 있습니다.

MNIST처럼 데이터가 수만 개면 예측 한 번에 수만 번의 거리 계산이 필요합니다. 실시간 서비스에서는 이 점을 고려해야 합니다.

그럼에도 KNN은 놀라운 정확도를 보여줍니다. MNIST에서 약 97%의 정확도를 달성합니다.

단순한 알고리즘치고는 대단한 성능입니다. 김개발 씨가 코드를 실행해 보았습니다.

정확도 97%라는 결과에 놀랐습니다. "이렇게 간단한 방법으로 이 정도 성능이 나오다니요!" 박시니어 씨가 미소 지었습니다.

"KNN의 매력이죠. 하지만 더 빠르고 정확한 방법들도 있어요.

다음에는 SVM을 알아볼까요?"

실전 팁

💡 - K 값은 보통 3, 5, 7 같은 홀수를 사용합니다

  • 데이터가 많을수록 예측 시간이 길어지므로 주의하세요
  • 거리 계산 방식을 바꾸면 성능이 달라질 수 있습니다

4. SVM 분류기 적용

"이번에는 SVM이라는 알고리즘을 써볼게요." 박시니어 씨의 말에 김개발 씨가 물었습니다. "SVM이 뭐예요?" "Support Vector Machine, 서포트 벡터 머신이에요.

데이터 사이에 최적의 경계선을 그어주는 알고리즘이죠."

**SVM(Support Vector Machine)**은 데이터를 분류하는 최적의 경계선을 찾는 알고리즘입니다. 마치 두 나라 사이에 국경선을 긋듯이, 서로 다른 클래스의 데이터 사이에 가장 넓은 여백을 가진 경계를 찾습니다.

고차원 데이터에서도 뛰어난 성능을 발휘합니다.

다음 코드를 살펴봅시다.

from sklearn.svm import SVC
from sklearn.metrics import accuracy_score
import time

# 빠른 실험을 위해 일부 데이터만 사용
X_train_small = X_train[:10000]
y_train_small = y_train[:10000]

# SVM 분류기 생성 (RBF 커널 사용)
svm = SVC(kernel='rbf', gamma='scale', C=1.0)

# 학습
start_time = time.time()
svm.fit(X_train_small, y_train_small)
train_time = time.time() - start_time

# 예측
y_pred = svm.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)

print(f'SVM 정확도: {accuracy * 100:.2f}%')
print(f'학습 시간: {train_time:.2f}초')

SVM은 머신러닝에서 가장 강력한 알고리즘 중 하나로 꼽힙니다. 이름이 어렵게 느껴질 수 있지만, 핵심 아이디어는 생각보다 단순합니다.

박시니어 씨가 비유를 들었습니다. "운동장에 빨간 팀과 파란 팀 학생들이 뒤섞여 있다고 상상해 보세요.

두 팀을 나누는 줄을 그어야 해요. 어디에 그으면 좋을까요?

두 팀 사이의 간격이 가장 넓은 곳에 그으면 실수할 확률이 줄어들겠죠?" 바로 이것이 SVM의 핵심입니다. SVM은 **마진(margin)**이라 불리는 이 간격을 최대화하는 경계선을 찾습니다.

경계선에 가장 가까이 있는 데이터 포인트들을 서포트 벡터라고 부르는데, 이들이 경계선의 위치를 결정합니다. 하지만 실제 데이터는 직선 하나로 깔끔하게 나뉘지 않는 경우가 많습니다.

손글씨 숫자를 생각해 보세요. 784차원 공간에서 숫자들이 복잡하게 뒤섞여 있습니다.

이때 커널 트릭이 등장합니다. 커널은 데이터를 더 높은 차원으로 변환하여, 원래는 분리 불가능했던 데이터도 분리할 수 있게 만들어줍니다.

코드에서 **kernel='rbf'**가 바로 이 역할을 합니다. RBF는 Radial Basis Function의 약자로, 가장 널리 사용되는 커널입니다.

마치 각 데이터 포인트 주변에 언덕을 만들어서, 복잡한 경계도 표현할 수 있게 합니다. gammaC는 중요한 하이퍼파라미터입니다.

gamma는 각 데이터 포인트의 영향 범위를 결정합니다. 값이 크면 가까운 데이터만 고려하고, 작으면 멀리 있는 데이터도 영향을 줍니다.

C는 오분류에 대한 패널티입니다. C가 크면 학습 데이터를 완벽하게 분류하려 하고, 작으면 약간의 오류를 허용합니다.

SVM의 단점은 학습 시간입니다. 데이터가 많아지면 학습 시간이 급격히 늘어납니다.

그래서 코드에서 10,000개의 샘플만 사용했습니다. 전체 데이터로 학습하면 몇 시간이 걸릴 수도 있습니다.

하지만 한번 학습이 끝나면 예측은 빠릅니다. 김개발 씨가 결과를 확인했습니다.

일부 데이터만 사용했는데도 98%에 가까운 정확도가 나왔습니다. "와, KNN보다 정확하네요!" 박시니어 씨가 고개를 끄덕였습니다.

"SVM은 정말 강력한 알고리즘이에요. 다만 대용량 데이터에서는 학습 시간이 문제가 될 수 있죠.

이제 앙상블 방법도 알아볼까요?"

실전 팁

💡 - 대용량 데이터에서는 LinearSVC를 고려해 보세요

  • gamma='scale'은 sklearn 0.22 이후 기본값으로, 자동 조정됩니다
  • 그리드 서치로 최적의 C와 gamma 값을 찾을 수 있습니다

5. 랜덤 포레스트 분류

"혼자서 결정하는 것보다 여러 명이 모여 결정하면 더 정확하지 않을까요?" 박시니어 씨의 질문에 김개발 씨가 고개를 끄덕였습니다. "그게 바로 앙상블 학습의 핵심이에요.

랜덤 포레스트는 수백 개의 결정 트리가 모여 투표하는 방식이죠."

**랜덤 포레스트(Random Forest)**는 여러 개의 결정 트리를 만들고 그들의 예측을 종합하는 앙상블 알고리즘입니다. 마치 배심원단이 여러 명의 의견을 모아 판결을 내리듯, 수백 개의 트리가 각자 예측하고 다수결로 최종 결과를 결정합니다.

과적합에 강하고 성능이 안정적입니다.

다음 코드를 살펴봅시다.

from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
import time

# 랜덤 포레스트 분류기 생성
rf = RandomForestClassifier(
    n_estimators=100,      # 트리 100개
    max_depth=20,          # 최대 깊이 제한
    n_jobs=-1,             # 모든 CPU 코어 사용
    random_state=42
)

# 학습
start_time = time.time()
rf.fit(X_train, y_train)
train_time = time.time() - start_time

# 예측
y_pred = rf.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)

print(f'랜덤 포레스트 정확도: {accuracy * 100:.2f}%')
print(f'학습 시간: {train_time:.2f}초')

# 특성 중요도 상위 10개 확인
importances = rf.feature_importances_
top_10_idx = importances.argsort()[-10:][::-1]
print(f'가장 중요한 픽셀 위치: {top_10_idx}')

앙상블(Ensemble)이라는 단어는 음악에서 왔습니다. 여러 악기가 함께 연주할 때 더 풍성한 소리가 나듯이, 여러 모델이 협력하면 더 좋은 예측을 할 수 있습니다.

박시니어 씨가 쉬운 비유로 설명을 시작했습니다. "퀴즈 대회에 참가한다고 생각해 보세요.

혼자 답을 맞추는 것보다 100명의 친구와 함께하면 어떨까요? 각자 독립적으로 생각하고, 가장 많이 선택된 답을 최종 답으로 하면 정확도가 높아지겠죠?" 랜덤 포레스트가 바로 이런 원리입니다.

결정 트리라는 간단한 모델을 수백 개 만들고, 그들의 예측을 종합합니다. 여기서 '랜덤'이라는 이름이 붙은 이유가 있습니다.

각 트리를 만들 때 두 가지 무작위성이 적용됩니다. 첫째, 부트스트랩 샘플링입니다.

전체 데이터에서 무작위로 일부를 뽑아 각 트리의 학습 데이터로 사용합니다. 덕분에 각 트리는 조금씩 다른 데이터로 학습합니다.

둘째, 특성 무작위 선택입니다. 각 분기점에서 모든 특성을 고려하는 것이 아니라, 무작위로 선택된 일부 특성만 고려합니다.

이렇게 하면 트리들이 서로 다른 관점에서 데이터를 바라보게 됩니다. 이 무작위성이 과적합을 방지합니다.

하나의 결정 트리는 학습 데이터에 너무 맞춰지기 쉽습니다. 마치 시험 문제만 달달 외운 학생처럼, 새로운 문제가 나오면 당황합니다.

하지만 여러 트리가 각자 다른 방식으로 학습하고 투표하면, 개별 트리의 실수가 상쇄됩니다. 코드에서 n_estimators=100은 트리를 100개 만들겠다는 의미입니다.

트리가 많을수록 성능이 안정적이지만, 그만큼 학습 시간도 늘어납니다. 보통 100~500개 정도가 적당합니다.

n_jobs=-1은 모든 CPU 코어를 활용하라는 옵션입니다. 랜덤 포레스트는 각 트리를 독립적으로 학습시킬 수 있어서 병렬 처리에 유리합니다.

랜덤 포레스트의 또 다른 장점은 특성 중요도를 알 수 있다는 것입니다. 어떤 픽셀이 숫자 인식에 중요한지 확인할 수 있습니다.

결과를 보면 이미지 중앙 부분의 픽셀이 중요하다는 것을 알 수 있습니다. 숫자의 핵심 특징이 대부분 중앙에 있기 때문입니다.

김개발 씨가 결과를 확인했습니다. "97% 정도의 정확도네요.

SVM보다는 조금 낮지만 학습 시간이 훨씬 짧아요!" 박시니어 씨가 덧붙였습니다. "맞아요.

그리고 랜덤 포레스트는 하이퍼파라미터 튜닝 없이도 괜찮은 성능을 내죠. 실무에서 먼저 시도해 볼 만한 알고리즘이에요."

실전 팁

💡 - n_estimators를 늘리면 성능이 안정적이지만 학습 시간이 증가합니다

  • max_depth로 트리 깊이를 제한하면 과적합을 방지할 수 있습니다
  • feature_importances_로 어떤 특성이 중요한지 분석해 보세요

6. 혼동 행렬 분석

모델들의 정확도를 확인한 김개발 씨가 물었습니다. "97%면 꽤 높은데, 나머지 3%는 어떤 숫자를 틀린 거예요?" 박시니어 씨가 웃으며 대답했습니다.

"좋은 질문이에요. 혼동 행렬을 보면 모델이 어디서 헷갈려 하는지 한눈에 알 수 있어요."

**혼동 행렬(Confusion Matrix)**은 분류 모델의 예측 결과를 표로 정리한 것입니다. 마치 시험 채점표처럼, 각 클래스별로 맞힌 개수와 틀린 개수를 보여줍니다.

어떤 숫자를 어떤 숫자로 잘못 분류했는지 파악하여 모델을 개선하는 데 활용합니다.

다음 코드를 살펴봅시다.

from sklearn.metrics import confusion_matrix, classification_report
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np

# 혼동 행렬 계산
cm = confusion_matrix(y_test, y_pred)

# 시각화
plt.figure(figsize=(10, 8))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
            xticklabels=range(10), yticklabels=range(10))
plt.xlabel('Predicted')
plt.ylabel('Actual')
plt.title('Confusion Matrix')
plt.show()

# 상세 리포트
print(classification_report(y_test, y_pred))

# 가장 많이 혼동한 쌍 찾기
np.fill_diagonal(cm, 0)  # 대각선(정답) 제외
max_confusion = np.unravel_index(cm.argmax(), cm.shape)
print(f'가장 많이 혼동: {max_confusion[0]}{max_confusion[1]}로 예측')

정확도 97%라는 숫자만으로는 모델을 제대로 평가할 수 없습니다. 100개 중 3개를 틀렸다면, 도대체 어떤 숫자를 틀린 걸까요?

특정 숫자만 유독 많이 틀린다면 문제가 될 수 있습니다. 박시니어 씨가 설명을 시작했습니다.

"시험 성적표를 생각해 보세요. 총점만 보면 어떤 과목이 약한지 알 수 없잖아요.

과목별 점수를 봐야 약점을 알 수 있죠. 혼동 행렬이 바로 그런 역할을 해요." 혼동 행렬은 10x10 크기의 표입니다.

행은 실제 레이블을, 열은 예측한 레이블을 나타냅니다. 대각선 위의 숫자가 정답입니다.

실제로 7인 데이터를 7이라고 예측하면 (7, 7) 위치의 숫자가 올라갑니다. 대각선 외의 숫자는 오답입니다.

실제로 4인데 9라고 예측하면 (4, 9) 위치의 숫자가 올라갑니다. 흥미로운 패턴이 보입니다.

MNIST에서 모델들이 자주 헷갈려 하는 쌍이 있습니다. 4와 9, 3과 8, 7과 1 같은 조합입니다.

생각해 보면 납득이 됩니다. 사람이 봐도 손글씨 4와 9는 비슷하게 생겼을 때가 있습니다.

3의 위아래가 붙으면 8처럼 보이기도 합니다. classification_report는 더 상세한 정보를 제공합니다.

**Precision(정밀도)**은 모델이 7이라고 예측한 것 중 실제로 7인 비율입니다. **Recall(재현율)**은 실제 7인 것 중 모델이 7이라고 맞힌 비율입니다.

F1-score는 이 둘의 조화 평균입니다. 실무에서는 상황에 따라 중요한 지표가 다릅니다.

예를 들어 은행 수표의 금액을 인식하는 시스템이라면, 0을 6으로 잘못 읽는 것은 치명적입니다. 이 경우 특정 숫자의 precision과 recall을 특히 신경 써야 합니다.

코드에서 가장 많이 혼동한 쌍을 찾는 부분도 유용합니다. np.fill_diagonal로 대각선 값을 0으로 만들어 정답을 제외합니다.

그다음 가장 큰 값의 위치를 찾으면 어떤 숫자를 어떤 숫자로 가장 많이 잘못 예측했는지 알 수 있습니다. 김개발 씨가 히트맵을 유심히 살펴보았습니다.

"아, 4와 9를 많이 헷갈려 하네요. 실제로 비슷하게 생긴 숫자들이잖아요." 박시니어 씨가 고개를 끄덕였습니다.

"맞아요. 이런 분석을 통해 모델을 개선할 방향을 찾을 수 있어요.

예를 들어 4와 9를 구분하는 특별한 전처리를 추가하거나, 데이터를 더 수집할 수도 있죠." 김개발 씨가 뿌듯한 표정을 지었습니다. "오늘 정말 많이 배웠어요.

MNIST로 머신러닝의 기초를 다진 것 같아요!"

실전 팁

💡 - 히트맵에서 대각선 외 밝은 부분이 모델의 약점입니다

  • precision과 recall의 균형을 보려면 F1-score를 확인하세요
  • 혼동 행렬 분석 결과를 바탕으로 데이터 증강 전략을 세울 수 있습니다

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

#Python#MNIST#MachineLearning#Classification#Scikit-learn#Machine Learning,Python

댓글 (0)

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