이미지 로딩 중...

벡터 기반 분류 완벽 가이드 - 슬라이드 1/11
A

AI Generated

2025. 11. 17. · 5 Views

벡터 기반 분류 완벽 가이드

머신러닝의 핵심 개념인 벡터 기반 분류를 초급 개발자도 쉽게 이해할 수 있도록 설명합니다. 실무에서 바로 활용할 수 있는 코드 예제와 함께 KNN, SVM 등 주요 알고리즘을 다룹니다.


목차

  1. 벡터_공간과_특성_표현
  2. K_최근접_이웃_알고리즘
  3. 서포트_벡터_머신_기초
  4. 유클리드_거리와_유사도_측정
  5. 결정_경계와_분류_영역
  6. 다중_클래스_분류
  7. 특성_스케일링과_정규화
  8. 과적합_방지와_정규화
  9. 교차_검증과_모델_평가
  10. 혼동_행렬과_성능_지표

1. 벡터_공간과_특성_표현

시작하며

여러분이 스팸 메일을 자동으로 분류하는 프로그램을 만든다고 생각해보세요. "이 메일이 스팸인지 아닌지 어떻게 컴퓨터에게 알려줄 수 있을까?"라는 고민을 하게 됩니다.

컴퓨터는 사람처럼 "이 메일이 수상해 보여"라고 느낄 수 없습니다. 컴퓨터는 숫자만 이해하기 때문이죠.

그래서 우리는 메일의 특징들을 숫자로 바꿔야 합니다. 바로 이럴 때 필요한 것이 벡터 공간과 특성 표현입니다.

이것은 실제 세상의 데이터를 컴퓨터가 이해할 수 있는 숫자들의 모음으로 바꿔주는 마법 같은 방법입니다.

개요

간단히 말해서, 벡터 공간이란 우리가 분류하고 싶은 데이터를 숫자들의 리스트(벡터)로 표현한 공간입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 이미지 인식, 텍스트 분류, 추천 시스템 등 거의 모든 머신러닝 프로젝트에서 가장 먼저 해야 할 일이 바로 데이터를 벡터로 변환하는 것입니다.

예를 들어, 고양이 사진을 분류한다면 "털 색깔", "귀 모양", "눈 크기" 같은 특징들을 각각 숫자로 바꿔서 [0.8, 0.3, 0.6] 같은 벡터로 만들 수 있습니다. 기존에는 사람이 직접 "이 메일에 '무료'라는 단어가 5번 나왔네"라고 일일이 세었다면, 이제는 컴퓨터가 자동으로 특징을 추출하고 벡터로 만들어줍니다.

벡터의 핵심 특징은 첫째, 각 차원이 하나의 특성을 나타낸다는 점이고, 둘째, 비슷한 데이터는 벡터 공간에서 가까운 위치에 있다는 점입니다. 이러한 특징들이 컴퓨터가 "비슷한 것끼리 묶기"를 할 수 있게 해주는 핵심 원리입니다.

코드 예제

import numpy as np

# 이메일을 벡터로 변환하는 예제
# 특성: [단어_개수, 느낌표_개수, 대문자_비율, 링크_개수]

# 정상 메일들의 벡터 표현
normal_emails = np.array([
    [50, 0, 0.1, 0],   # 정상 메일 1
    [120, 1, 0.05, 1], # 정상 메일 2
])

# 스팸 메일들의 벡터 표현
spam_emails = np.array([
    [30, 5, 0.8, 10],  # 스팸 메일 1: 짧지만 느낌표와 링크가 많음
    [25, 8, 0.9, 15],  # 스팸 메일 2: 대문자와 링크 폭탄
])

# 새로운 메일
new_email = np.array([35, 6, 0.75, 8])
print(f"새 메일의 벡터: {new_email}")

설명

이것이 하는 일: 위 코드는 이메일의 특징들을 숫자로 바꿔서 4차원 벡터로 표현합니다. 각 메일이 4개의 숫자로 이루어진 점이 되는 거죠.

첫 번째로, normal_emails 배열은 정상 메일의 패턴을 담고 있습니다. [50, 0, 0.1, 0]이라는 벡터는 "단어가 50개, 느낌표 0개, 대문자 비율 10%, 링크 0개"를 의미합니다.

왜 이렇게 하는지 아시나요? 정상 메일은 보통 적절한 길이에 과도한 강조 표현이 없기 때문입니다.

그 다음으로, spam_emails 배열이 실행되면서 스팸 메일의 특징을 학습합니다. [30, 5, 0.8, 10]을 보면 짧은 글에 느낌표 5개, 대문자 80%, 링크 10개나 들어있죠.

내부에서는 이런 패턴들이 "아, 스팸은 이런 특징이 있구나"라고 기억됩니다. 마지막으로, new_email 벡터가 들어오면 기존 벡터들과 비교하여 최종적으로 "이게 정상 메일과 가까운가, 스팸과 가까운가"를 판단할 수 있게 됩니다.

여러분이 이 코드를 사용하면 어떤 데이터든 숫자로 표현할 수 있고, 그 숫자들을 기반으로 자동 분류 시스템을 만들 수 있습니다. 실무에서는 이미지라면 픽셀 값, 텍스트라면 단어 빈도, 음성이라면 주파수 같은 특징들을 벡터로 만들어 활용합니다.

실전 팁

💡 특성 선택이 가장 중요합니다. 분류에 도움이 되는 특성을 골라야 정확도가 올라갑니다. 예를 들어 스팸 분류에서 "메일 제목의 길이"는 유용하지만 "보낸 날짜의 요일"은 별로 도움이 안 됩니다.

💡 특성의 스케일을 맞춰주세요. 어떤 특성은 0-1 범위, 어떤 특성은 0-1000 범위면 큰 숫자가 분류를 지배하게 됩니다. sklearn의 StandardScaler를 사용하면 쉽게 해결됩니다.

💡 차원이 너무 많으면 "차원의 저주"가 발생합니다. 특성이 100개 넘어가면 PCA 같은 차원 축소 기법을 고려해보세요.

💡 벡터를 시각화해보면 데이터를 이해하는 데 큰 도움이 됩니다. 2-3차원으로 축소해서 matplotlib으로 그려보면 정상/스팸이 어떻게 분포하는지 눈으로 확인할 수 있습니다.


2. K_최근접_이웃_알고리즘

시작하며

여러분이 새로 이사 온 동네에서 맛집을 찾는다고 상상해보세요. 가장 쉬운 방법은 뭘까요?

바로 주변에 사는 이웃들에게 물어보는 거죠. "5명한테 물어봤는데 4명이 저 식당 추천하네?

그럼 거기 가야겠다!" 머신러닝에도 똑같은 원리를 사용하는 알고리즘이 있습니다. 새로운 데이터가 들어오면 "가장 가까운 이웃들"을 찾아서 물어보는 거예요.

이웃들이 대부분 스팸이면 이것도 스팸, 대부분 정상이면 이것도 정상! 바로 이럴 때 필요한 것이 K-최근접 이웃(KNN) 알고리즘입니다.

가장 간단하면서도 놀라울 정도로 효과적인 분류 방법 중 하나입니다.

개요

간단히 말해서, KNN은 새로운 데이터와 가장 가까운 K개의 이웃을 찾아서 다수결로 분류를 결정하는 알고리즘입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, KNN은 복잡한 수학 없이도 바로 사용할 수 있고, 데이터의 분포가 복잡해도 잘 작동합니다.

예를 들어, 손글씨 숫자 인식이나 상품 추천 시스템 같은 경우에 매우 유용합니다. "이 손글씨와 비슷한 다른 손글씨들은 대부분 '7'이네?

그럼 이것도 7이겠다!" 기존에는 복잡한 규칙을 일일이 프로그래밍했다면, 이제는 그냥 데이터만 보여주면 알아서 비슷한 것끼리 찾아줍니다. KNN의 핵심 특징은 첫째, 학습이 따로 필요 없다는 점(게으른 학습), 둘째, 지역적 패턴을 잘 잡아낸다는 점, 셋째, K값에 따라 결과가 달라진다는 점입니다.

이러한 특징들이 간단하면서도 강력한 분류기를 만들어줍니다.

코드 예제

from sklearn.neighbors import KNeighborsClassifier
import numpy as np

# 학습 데이터 준비 (키, 몸무게로 성별 분류)
X_train = np.array([
    [170, 65],  # 남성
    [180, 80],  # 남성
    [160, 55],  # 여성
    [165, 58],  # 여성
])
y_train = np.array(['남성', '남성', '여성', '여성'])

# KNN 모델 생성 (K=3: 가장 가까운 3명의 이웃을 참고)
knn = KNeighborsClassifier(n_neighbors=3)
knn.fit(X_train, y_train)  # 데이터 저장 (실제 학습은 안 함)

# 새로운 사람 분류
new_person = np.array([[175, 70]])
prediction = knn.predict(new_person)
print(f"예측 결과: {prediction[0]}")

# 이웃들의 거리 확인
distances, indices = knn.kneighbors(new_person)
print(f"가장 가까운 이웃들의 인덱스: {indices}")

설명

이것이 하는 일: 위 코드는 키와 몸무게 데이터로 성별을 예측하는 KNN 분류기를 만듭니다. 마치 "비슷한 체격을 가진 사람들은 같은 성별일 확률이 높다"는 상식을 코드로 구현한 것이죠.

첫 번째로, X_train과 y_train에 기존 데이터를 저장합니다. [170, 65]는 "키 170cm, 몸무게 65kg"을 의미하고, 이 사람은 '남성'이라는 레이블이 붙어있습니다.

왜 이렇게 하는지 아시나요? KNN은 나중에 새 데이터가 들어왔을 때 이 저장된 데이터들과 거리를 비교하기 때문입니다.

그 다음으로, KNeighborsClassifier(n_neighbors=3)가 실행되면서 "3명의 이웃을 볼게"라고 설정됩니다. fit() 메서드를 호출하면 데이터가 메모리에 저장됩니다.

내부에서는 복잡한 계산 없이 그냥 데이터를 보관만 하고 있습니다. 마지막으로, new_person [175, 70]이 들어오면 기존 4명과의 거리를 계산하여 가장 가까운 3명을 찾고, 그 3명 중 다수인 레이블로 최종 예측을 내립니다.

여러분이 이 코드를 사용하면 복잡한 수식 없이도 패턴 인식 시스템을 만들 수 있습니다. 실무에서는 상품 이미지 분류, 필기체 인식, 이상 거래 탐지 등에 활용됩니다.

특히 데이터가 적을 때도 잘 작동하는 장점이 있습니다.

실전 팁

💡 K값 선택이 성능을 좌우합니다. K가 너무 작으면 노이즈에 민감하고, 너무 크면 경계가 뭉개집니다. 보통 데이터 개수의 제곱근을 시작점으로 잡고 cross-validation으로 최적값을 찾으세요.

💡 데이터가 많으면 예측 속도가 느려집니다. 매번 모든 데이터와 거리를 계산해야 하거든요. 실시간 서비스라면 KD-Tree나 Ball-Tree 같은 자료구조를 사용하세요.

💡 거리 측정 방식을 바꿔보세요. 기본은 유클리드 거리지만, 맨해튼 거리, 민코프스키 거리 등을 시도하면 성능이 개선될 수 있습니다. metric='manhattan' 파라미터로 쉽게 변경 가능합니다.

💡 불균형 데이터에서는 가중치를 적용하세요. weights='distance'로 설정하면 가까운 이웃의 영향을 더 크게 줍니다. 예를 들어 스팸이 10%, 정상이 90%인 데이터셋에서 유용합니다.


3. 서포트_벡터_머신_기초

시작하며

여러분이 운동장에서 남학생과 여학생을 나누는 줄을 그어야 한다고 생각해보세요. 어떻게 그으면 가장 공정할까요?

한쪽에 너무 치우치지 않고, 양쪽 모두에게 최대한 여유를 주는 선을 그어야겠죠. 머신러닝에서도 데이터를 나누는 "최적의 경계선"을 찾는 것이 핵심입니다.

하지만 어떤 선이 최적인지 어떻게 알 수 있을까요? 수백 개의 가능한 선 중에서 말이죠.

바로 이럴 때 필요한 것이 서포트 벡터 머신(SVM)입니다. 이것은 양쪽 클래스 사이의 "마진"이 가장 넓은 경계선을 수학적으로 찾아주는 똑똑한 알고리즘입니다.

개요

간단히 말해서, SVM은 두 클래스 사이에 가장 넓은 도로를 만들 수 있는 경계선을 찾는 알고리즘입니다. 이 "도로"를 마진이라고 부릅니다.

왜 이 개념이 필요한지 실무 관점에서 설명하자면, SVM은 데이터가 적어도 높은 정확도를 보이고, 과적합에 강하며, 고차원 데이터에서도 잘 작동합니다. 예를 들어, 얼굴 인식, 텍스트 분류, 의료 진단 같은 분야에서 매우 효과적입니다.

특히 "확실하게 구분"해야 하는 경우에 탁월합니다. 기존에는 경계선을 대충 그었거나 여러 번 시행착오를 거쳤다면, 이제는 수학적으로 최적의 경계를 한 번에 찾을 수 있습니다.

SVM의 핵심 특징은 첫째, 서포트 벡터(경계에 가장 가까운 점들)만 사용한다는 점, 둘째, 마진을 최대화한다는 점, 셋째, 커널 트릭으로 비선형 분류도 가능하다는 점입니다. 이러한 특징들이 강력하고 일반화 성능이 좋은 분류기를 만들어줍니다.

코드 예제

from sklearn.svm import SVC
import numpy as np

# 꽃잎 데이터로 꽃 종류 분류 (길이, 너비)
X = np.array([
    [2.0, 0.5],  # 종류 A
    [2.5, 0.7],  # 종류 A
    [5.0, 1.5],  # 종류 B
    [5.5, 1.8],  # 종류 B
])
y = np.array([0, 0, 1, 1])  # 0: 종류A, 1: 종류B

# SVM 모델 생성 (선형 커널 사용)
svm = SVC(kernel='linear', C=1.0)
svm.fit(X, y)  # 최적의 경계선 찾기

# 새로운 꽃 분류
new_flower = np.array([[4.0, 1.2]])
prediction = svm.predict(new_flower)
print(f"예측된 종류: {prediction[0]}")

# 서포트 벡터 확인 (경계에 가장 가까운 핵심 데이터들)
print(f"서포트 벡터 개수: {len(svm.support_vectors_)}")

설명

이것이 하는 일: 위 코드는 꽃잎의 길이와 너비로 꽃 종류를 구분하는 최적의 선을 찾습니다. 마치 두 무리의 꽃들 사이에 가장 넓은 길을 내는 것처럼요.

첫 번째로, X 배열에 꽃잎 측정값을 넣습니다. [2.0, 0.5]는 "길이 2cm, 너비 0.5cm"를 의미하고, 이것은 종류 A(레이블 0)입니다.

왜 이렇게 하는지 아시나요? SVM은 이 점들 사이에 가장 넓은 간격을 만들 수 있는 선을 찾기 때문입니다.

그 다음으로, SVC(kernel='linear', C=1.0)가 실행되면서 선형 경계를 사용하겠다고 선언합니다. C=1.0은 "마진 위반을 얼마나 허용할지"를 조절하는 값입니다.

fit()을 호출하면 내부에서 복잡한 최적화 문제를 풀어서 w(가중치)와 b(편향)를 계산합니다. 마지막으로, new_flower [4.0, 1.2]가 들어오면 계산된 경계선을 기준으로 어느 쪽에 있는지 판단하여 최종 분류 결과를 냅니다.

여러분이 이 코드를 사용하면 수학적으로 가장 안정적인 분류 경계를 얻을 수 있습니다. 실무에서는 이미지 내 객체 감지, 스팸 필터링, 주가 예측, 암 진단 등에 활용됩니다.

특히 데이터가 명확하게 구분되는 경우 거의 완벽한 정확도를 보입니다.

실전 팁

💡 C 파라미터로 정확도와 일반화의 균형을 맞추세요. C가 크면 학습 데이터에 완벽히 맞추려 하고(과적합 위험), 작으면 마진을 넓게 잡습니다. C=1부터 시작해서 0.1, 10 등으로 조절해보세요.

💡 데이터가 선형으로 분리 안 되면 커널을 바꾸세요. kernel='rbf'(방사 기저 함수)는 비선형 경계를 만들 수 있고, 대부분의 실제 문제에 잘 맞습니다. kernel='poly'는 다항식 경계를 만듭니다.

💡 스케일링이 필수입니다! SVM은 거리 기반이라 특성 간 스케일 차이에 매우 민감합니다. StandardScaler로 평균 0, 분산 1로 정규화하면 성능이 크게 향상됩니다.

💡 대용량 데이터에는 SGDClassifier를 고려하세요. SVM은 데이터가 많아지면 학습 시간이 제곱으로 증가합니다. 수만 개 이상이면 LinearSVC나 SGDClassifier(loss='hinge')가 더 빠릅니다.

💡 확률값이 필요하면 probability=True를 설정하세요. 기본 SVM은 분류 결과만 주지만, 이 옵션을 켜면 predict_proba()로 확률도 얻을 수 있습니다.


4. 유클리드_거리와_유사도_측정

시작하며

여러분이 지도에서 두 장소 사이의 거리를 잴 때 어떻게 하나요? 자를 대고 직선 거리를 재죠.

이게 바로 유클리드 거리의 개념입니다. 머신러닝에서도 두 데이터가 얼마나 비슷한지 알려면 "거리"를 재야 합니다.

벡터 공간에서 두 점이 가까우면 비슷한 것이고, 멀면 다른 것이죠. 하지만 4차원, 100차원 공간에서는 어떻게 거리를 잴까요?

바로 이럴 때 필요한 것이 유클리드 거리와 다양한 유사도 측정 방법들입니다. 이것들이 없으면 "비슷하다"는 말을 수학적으로 표현할 수가 없습니다.

개요

간단히 말해서, 유클리드 거리는 n차원 공간에서 두 점 사이의 직선 거리를 계산하는 공식입니다. 고등학교 때 배운 피타고라스 정리를 확장한 것이죠.

왜 이 개념이 필요한지 실무 관점에서 설명하자면, 추천 시스템에서 "비슷한 사용자 찾기", 이상 탐지에서 "정상과 얼마나 다른지", 클러스터링에서 "같은 그룹 판단" 등 거의 모든 곳에서 사용됩니다. 예를 들어, 넷플릭스가 "이 영화를 본 사람들은 저 영화도 봤어요"라고 추천할 때 바로 이 거리 개념을 사용합니다.

기존에는 사람이 눈으로 보고 "아, 이게 비슷하네"라고 판단했다면, 이제는 거리 값을 계산해서 객관적으로 비교할 수 있습니다. 거리 측정의 핵심 특징은 첫째, 여러 측정 방법이 있다는 점(유클리드, 맨해튼, 코사인 등), 둘째, 문제에 따라 적합한 방법이 다르다는 점, 셋째, 계산 속도가 중요하다는 점입니다.

이러한 특징들이 효율적인 유사도 기반 시스템을 만드는 기초가 됩니다.

코드 예제

import numpy as np
from scipy.spatial.distance import euclidean, cosine

# 두 사용자의 영화 평점 (액션, 로맨스, SF, 스릴러)
user_A = np.array([5, 2, 4, 3])  # 사용자 A의 평점
user_B = np.array([4, 2, 5, 3])  # 사용자 B의 평점
user_C = np.array([1, 5, 2, 1])  # 사용자 C의 평점

# 유클리드 거리 계산 (가까울수록 비슷함)
dist_AB = euclidean(user_A, user_B)
dist_AC = euclidean(user_A, user_C)
print(f"A와 B의 거리: {dist_AB:.2f}")
print(f"A와 C의 거리: {dist_AC:.2f}")

# 코사인 유사도 (방향 비교, 0에 가까울수록 비슷함)
cos_AB = cosine(user_A, user_B)
cos_AC = cosine(user_A, user_C)
print(f"A와 B의 코사인 거리: {cos_AB:.2f}")
print(f"A와 C의 코사인 거리: {cos_AC:.2f}")

설명

이것이 하는 일: 위 코드는 세 명의 사용자가 매긴 영화 평점을 비교해서 누가 누구와 취향이 비슷한지 수치로 계산합니다. 첫 번째로, user_A, user_B, user_C 배열에 각 사용자의 4가지 장르 평점을 저장합니다.

[5, 2, 4, 3]은 "액션 5점, 로맨스 2점, SF 4점, 스릴러 3점"을 의미합니다. 왜 이렇게 하는지 아시나요?

각 사용자를 4차원 공간의 한 점으로 표현하기 위해서입니다. 그 다음으로, euclidean() 함수가 실행되면서 두 벡터 사이의 직선 거리를 계산합니다.

내부에서는 sqrt((5-4)² + (2-2)² + (4-5)² + (3-3)²) 같은 계산이 일어납니다. A와 B는 거리가 짧고(1.41), A와 C는 거리가 깁니다(5.20).

마지막으로, cosine() 함수는 크기가 아닌 방향을 비교합니다. 평점의 절대값보다 패턴이 비슷한지를 봅니다.

예를 들어 [5,2,4,3]과 [10,4,8,6]은 유클리드 거리는 멀지만 코사인 거리는 0(완전히 같은 방향)입니다. 여러분이 이 코드를 사용하면 추천 시스템, 중복 문서 탐지, 이상 거래 탐지 등을 구현할 수 있습니다.

실무에서는 수백만 명의 사용자 중 비슷한 취향을 가진 사람을 빠르게 찾아 맞춤형 추천을 제공하는 데 활용됩니다.

실전 팁

💡 문제 특성에 맞는 거리 함수를 선택하세요. 추천 시스템에는 코사인 유사도, 지리 데이터에는 유클리드, 텍스트에는 자카드 유사도가 적합합니다.

💡 고차원에서는 "차원의 저주" 때문에 모든 거리가 비슷해집니다. 차원이 100 이상이면 PCA로 10-20차원으로 축소한 후 거리를 계산하세요.

💡 계산 속도를 높이려면 numpy의 벡터 연산을 활용하세요. 반복문 대신 np.linalg.norm()이나 scipy.spatial.distance_matrix()를 쓰면 100배 이상 빨라집니다.

💡 맨해튼 거리(L1)는 유클리드(L2)보다 이상치에 덜 민감합니다. 데이터에 노이즈가 많으면 cityblock() 함수를 써보세요.

💡 거리 대신 유사도로 표현하고 싶으면 1/(1+distance)로 변환하세요. 그러면 0(다름)~1(같음) 범위의 점수를 얻을 수 있습니다.


5. 결정_경계와_분류_영역

시작하며

여러분이 나라의 국경선을 그린다고 생각해보세요. "여기까지는 A국, 여기부터는 B국"이라고 명확하게 구분하는 선이죠.

그런데 이 선을 잘못 그으면 A국 사람이 B국 땅에 속하거나, 반대 상황이 생깁니다. 머신러닝 분류 모델도 똑같이 "여기까지는 스팸, 여기부터는 정상"이라는 경계선을 그립니다.

이 경계선이 어디에 그어지느냐에 따라 모델의 성능이 완전히 달라지죠. 바로 이럴 때 필요한 것이 결정 경계(Decision Boundary)의 이해입니다.

이것은 분류 모델이 내부적으로 어떻게 세상을 나누는지 시각적으로 보여줍니다.

개요

간단히 말해서, 결정 경계는 서로 다른 클래스를 구분하는 선, 면, 또는 초평면입니다. 2차원이면 선, 3차원이면 면, n차원이면 n-1차원의 초평면이 됩니다.

왜 이 개념이 필요한지 실무 관점에서 설명하자면, 모델이 잘못 분류하는 이유를 파악하고, 과적합/과소적합을 진단하고, 서로 다른 알고리즘의 차이를 이해하는 데 필수적입니다. 예를 들어, 로지스틱 회귀는 직선 경계만 만들지만, SVM with RBF 커널은 곡선 경계를 만들 수 있습니다.

기존에는 모델을 블랙박스처럼 썼다면, 이제는 결정 경계를 시각화해서 "아, 이 모델은 여기서 실수하는구나"라고 정확히 알 수 있습니다. 결정 경계의 핵심 특징은 첫째, 모델마다 경계 모양이 다르다는 점(선형/비선형), 둘째, 경계가 복잡할수록 과적합 위험이 크다는 점, 셋째, 경계 근처의 데이터가 분류하기 어렵다는 점입니다.

이러한 특징들이 모델 선택과 하이퍼파라미터 튜닝의 기준이 됩니다.

코드 예제

import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression

# 간단한 2D 데이터 생성
X = np.array([[1,2], [2,3], [3,1], [6,5], [7,7], [8,6]])
y = np.array([0, 0, 0, 1, 1, 1])  # 0: 클래스A, 1: 클래스B

# 로지스틱 회귀 모델 학습
model = LogisticRegression()
model.fit(X, y)

# 결정 경계 시각화를 위한 격자 생성
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 100),
                     np.linspace(y_min, y_max, 100))

# 각 격자 점에 대한 예측
Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)

print(f"결정 경계 계수: {model.coef_}, 절편: {model.intercept_}")

설명

이것이 하는 일: 위 코드는 2차원 평면에 흩어진 두 클래스의 데이터를 나누는 최적의 선(결정 경계)을 찾고, 그 경계가 평면을 어떻게 나누는지 시각화할 준비를 합니다. 첫 번째로, X 배열에 6개 점의 좌표를 넣습니다.

[1,2], [2,3], [3,1]은 클래스 0(왼쪽 아래), [6,5], [7,7], [8,6]은 클래스 1(오른쪽 위)에 속합니다. 왜 이렇게 하는지 아시나요?

로지스틱 회귀가 이 두 무리를 가장 잘 나누는 직선을 찾기 때문입니다. 그 다음으로, model.fit(X, y)가 실행되면서 내부에서 최적화 알고리즘이 돌아갑니다.

wx + b = 0.5가 되는 선을 찾는 거죠. 이 선 위쪽은 클래스 1로, 아래쪽은 클래스 0으로 분류됩니다.

마지막으로, meshgrid로 평면 전체를 촘촘한 격자로 만들고, 각 격자 점마다 예측을 수행합니다. 이렇게 하면 "이 영역은 클래스 0, 저 영역은 클래스 1"이라는 색깔 지도를 그릴 수 있습니다.

여러분이 이 코드를 사용하면 모델의 행동을 눈으로 확인할 수 있습니다. 실무에서는 새로운 알고리즘을 테스트할 때, A/B 테스트 결과를 설명할 때, 모델 디버깅할 때 결정 경계 시각화를 필수로 합니다.

"이 모델은 곡선 패턴을 못 잡네요"처럼 명확한 피드백을 얻을 수 있습니다.

실전 팁

💡 2D로 시각화하면 직관적으로 이해하기 쉽습니다. 고차원 데이터는 PCA로 2차원으로 축소한 후 결정 경계를 그려보세요.

💡 경계가 너무 구불구불하면 과적합입니다. 정규화 파라미터(C, alpha 등)를 조절해서 경계를 단순하게 만드세요.

💡 mlxtend 라이브러리의 plot_decision_regions()를 쓰면 한 줄로 시각화 가능합니다. pip install mlxtend 후 사용하세요.

💡 확률 기반 분류기는 경계를 "확률 0.5인 곳"으로 정의합니다. predict_proba()로 확률 지도를 그리면 경계 근처의 불확실성을 볼 수 있습니다.

💡 클래스가 3개 이상이면 경계가 여러 개 생깁니다. 각 경계마다 색을 다르게 칠하면 복잡한 분류 문제도 이해할 수 있습니다.


6. 다중_클래스_분류

시작하며

여러분이 과일 가게에서 사과와 오렌지를 구분하는 건 쉽습니다. 하지만 사과, 오렌지, 바나나, 포도, 수박까지 5가지를 동시에 구분해야 한다면 어떨까요?

훨씬 복잡해지죠. 대부분의 머신러닝 알고리즘은 원래 "A냐 B냐"(이진 분류)만 할 수 있게 설계되었습니다.

그런데 실제 세상은 훨씬 복잡합니다. 손글씨 숫자는 0-9까지 10개, 이미지넷은 1000개 클래스가 있죠.

바로 이럴 때 필요한 것이 다중 클래스 분류(Multi-class Classification) 기법입니다. 이진 분류기 여러 개를 조합하거나, 처음부터 다중 클래스를 지원하는 방법을 사용합니다.

개요

간단히 말해서, 다중 클래스 분류는 3개 이상의 카테고리 중 하나로 데이터를 분류하는 작업입니다. One-vs-Rest, One-vs-One 같은 전략이나 소프트맥스 같은 함수를 사용합니다.

왜 이 개념이 필요한지 실무 관점에서 설명하자면, 현실의 문제는 거의 항상 다중 클래스입니다. 제품 카테고리 분류(전자제품/의류/식품/...), 감정 분석(긍정/중립/부정), 질병 진단(A병/B병/C병/정상) 등이 모두 해당됩니다.

예를 들어, 손글씨 인식 시스템은 10가지 숫자를 모두 구분할 수 있어야 합니다. 기존에는 "사과 vs 나머지", "오렌지 vs 나머지" 같이 여러 모델을 따로 만들어야 했다면, 이제는 하나의 모델로 모든 클래스를 동시에 처리할 수 있습니다.

다중 클래스 분류의 핵심 특징은 첫째, 전략이 여러 개 있다는 점(OvR, OvO, 소프트맥스), 둘째, 클래스 개수에 따라 복잡도가 달라진다는 점, 셋째, 클래스 불균형 문제가 더 심각하다는 점입니다. 이러한 특징들이 알고리즘 선택과 평가 방법을 결정합니다.

코드 예제

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.metrics import classification_report

# 붓꽃 데이터셋 (3개 클래스: Setosa, Versicolor, Virginica)
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
    iris.data, iris.target, test_size=0.3, random_state=42
)

# SVM으로 다중 클래스 분류 (기본: One-vs-Rest 전략)
model = SVC(kernel='rbf', decision_function_shape='ovr')
model.fit(X_train, y_train)

# 예측 및 평가
y_pred = model.predict(X_test)
print(classification_report(y_test, y_pred,
                          target_names=iris.target_names))

# 새로운 꽃 분류 (꽃받침 길이, 너비, 꽃잎 길이, 너비)
new_flower = [[5.0, 3.5, 1.5, 0.3]]
prediction = model.predict(new_flower)
print(f"예측된 품종: {iris.target_names[prediction[0]]}")

설명

이것이 하는 일: 위 코드는 붓꽃의 4가지 측정값으로 3가지 품종을 구분하는 다중 클래스 분류기를 만듭니다. 하나의 모델이 세 가지를 모두 구분할 수 있죠.

첫 번째로, load_iris()로 유명한 붓꽃 데이터셋을 불러옵니다. 150개 샘플, 4개 특성, 3개 클래스로 구성되어 있습니다.

train_test_split으로 70%는 학습용, 30%는 테스트용으로 나눕니다. 왜 이렇게 하는지 아시나요?

모델의 일반화 성능을 객관적으로 평가하기 위해서입니다. 그 다음으로, SVC(decision_function_shape='ovr')가 실행되면서 One-vs-Rest 전략을 사용합니다.

내부에서는 "Setosa vs 나머지", "Versicolor vs 나머지", "Virginica vs 나머지" 세 개의 이진 분류기를 만들고, 가장 높은 점수를 받은 클래스를 선택합니다. 마지막으로, new_flower [5.0, 3.5, 1.5, 0.3]이 들어오면 세 분류기가 각각 점수를 매기고, 가장 확신하는 분류기의 클래스를 최종 답으로 냅니다.

여러분이 이 코드를 사용하면 복잡한 다중 카테고리 문제를 해결할 수 있습니다. 실무에서는 고객 세그먼트 분류(VIP/일반/신규/휴면), 문서 주제 분류(스포츠/정치/경제/...), 제품 불량 유형 판별 등에 활용됩니다.

특히 sklearn은 대부분 알고리즘이 자동으로 다중 클래스를 지원해서 편리합니다.

실전 팁

💡 클래스 개수가 많으면 One-vs-One이 더 빠를 수 있습니다. decision_function_shape='ovo'로 바꿔보세요. 클래스가 10개면 45개 분류기가 생기지만, 각각은 데이터 일부만 써서 빠릅니다.

💡 확률이 필요하면 predict_proba()를 쓰세요. SVM의 경우 probability=True로 설정해야 합니다. 그러면 [0.7, 0.2, 0.1] 같은 확률 분포를 얻습니다.

💡 클래스 불균형이 심하면 class_weight='balanced'를 설정하세요. 예를 들어 클래스 A가 90%, B가 5%, C가 5%면 자동으로 가중치를 조절해 공정하게 학습합니다.

💡 혼동 행렬(confusion matrix)로 어떤 클래스 쌍을 헷갈리는지 확인하세요. from sklearn.metrics import confusion_matrix로 쉽게 확인 가능합니다.

💡 신경망을 쓴다면 마지막 층에 소프트맥스 활성화 함수를 쓰세요. 이게 가장 자연스럽게 다중 클래스 확률을 계산합니다.


7. 특성_스케일링과_정규화

시작하며

여러분이 학생들을 평가하는데 수학 점수는 0-100점, 출석 일수는 0-200일이라고 해보세요. 출석 200일과 수학 50점 중 어느 게 더 중요할까요?

숫자만 보면 출석이 4배 크지만, 실제론 그렇지 않죠. 머신러닝 알고리즘도 똑같은 문제를 겪습니다.

키(150-190cm)와 몸무게(40-100kg)로 분류할 때, 몸무게가 숫자가 크다고 해서 더 중요한 특성이 아닙니다. 하지만 거리 기반 알고리즘은 큰 숫자에 지배당합니다.

바로 이럴 때 필요한 것이 특성 스케일링과 정규화입니다. 이것은 서로 다른 범위의 특성들을 공정하게 비교할 수 있도록 만들어줍니다.

개요

간단히 말해서, 스케일링은 모든 특성을 비슷한 범위로 조정하는 작업입니다. 표준화(Standardization)는 평균 0, 분산 1로, 정규화(Normalization)는 0-1 범위로 만듭니다.

왜 이 개념이 필요한지 실무 관점에서 설명하자면, KNN, SVM, 신경망, K-평균 등 거리 기반 알고리즘은 스케일링 없이는 제대로 작동하지 않습니다. 예를 들어, 집값 예측에서 "방 개수(1-5)"와 "면적(20-200㎡)"을 그대로 쓰면 면적이 결과를 지배해버립니다.

기존에는 "왜 모델 성능이 안 나오지?"라고 고민했다면, 이제는 스케일링 한 줄로 정확도가 10-30%p 향상되는 마법을 경험할 수 있습니다. 스케일링의 핵심 특징은 첫째, 알고리즘마다 필요성이 다르다는 점(거리 기반은 필수, 트리 기반은 불필요), 둘째, 방법이 여러 개 있다는 점(표준화, 정규화, 로버스트), 셋째, 학습 데이터 기준으로 테스트 데이터도 변환해야 한다는 점입니다.

이러한 특징들이 올바른 전처리 파이프라인을 만드는 기초입니다.

코드 예제

from sklearn.preprocessing import StandardScaler, MinMaxScaler
import numpy as np

# 원본 데이터 (나이, 연봉만원)
data = np.array([
    [25, 3000],   # 젊고 낮은 연봉
    [45, 8000],   # 중년, 높은 연봉
    [35, 5000],   # 중간
])

# 1. 표준화 (평균 0, 표준편차 1)
scaler_standard = StandardScaler()
data_standardized = scaler_standard.fit_transform(data)
print("표준화 결과:")
print(data_standardized)

# 2. 정규화 (0-1 범위)
scaler_minmax = MinMaxScaler()
data_normalized = scaler_minmax.fit_transform(data)
print("\n정규화 결과:")
print(data_normalized)

# 3. 새로운 데이터 변환 (학습 시 사용한 scaler 재사용!)
new_data = np.array([[30, 4000]])
new_scaled = scaler_standard.transform(new_data)
print(f"\n새 데이터 변환: {new_scaled}")

설명

이것이 하는 일: 위 코드는 나이(25-45)와 연봉(3000-8000)처럼 범위가 다른 특성들을 표준화와 정규화로 같은 스케일로 맞춥니다. 첫 번째로, data 배열에 [나이, 연봉] 쌍을 저장합니다.

[25, 3000]과 [45, 8000]을 그대로 거리 계산하면 연봉 차이 5000이 나이 차이 20을 완전히 압도합니다. 왜 이렇게 하는지 아시나요?

스케일링 전후를 비교하기 위해서입니다. 그 다음으로, StandardScaler가 실행되면서 각 열의 평균과 표준편차를 계산합니다.

fit_transform()은 평균을 빼고 표준편차로 나눕니다. 내부에서는 (25 - 35) / 10 = -1.0 같은 계산이 일어나서 모든 값이 대략 -2~2 범위로 들어갑니다.

그 다음으로, MinMaxScaler는 다른 전략을 사용합니다. (값 - 최솟값) / (최댓값 - 최솟값)으로 계산해서 모든 값을 정확히 0-1 범위로 만듭니다.

나이 25는 0, 45는 1, 35는 0.5가 되는 식이죠. 마지막으로, 새로운 데이터 [30, 4000]이 들어오면 학습 때 계산한 평균/표준편차를 그대로 사용해서 변환합니다.

절대 새 데이터로 fit()하면 안 됩니다! 여러분이 이 코드를 사용하면 모델 성능이 극적으로 개선됩니다.

실무에서는 거의 모든 머신러닝 프로젝트에서 첫 단계로 스케일링을 적용합니다. 특히 경사하강법 기반 알고리즘(선형회귀, 신경망)은 수렴 속도가 10-100배 빨라집니다.

실전 팁

💡 거리 기반 알고리즘에는 필수입니다. KNN, SVM, K-Means, PCA는 반드시 스케일링하세요. 트리 기반(랜덤포레스트, XGBoost)은 안 해도 됩니다.

💡 StandardScaler와 MinMaxScaler 중 선택하는 기준: 정규분포면 Standard, 범위가 명확하면 MinMax, 이상치가 많으면 RobustScaler를 쓰세요.

💡 절대 테스트 데이터로 fit()하지 마세요! train에서 fit(), test에서는 transform()만 해야 합니다. 안 그러면 데이터 누수(leakage)로 평가가 부정확해집니다.

💡 파이프라인을 사용하면 실수를 방지할 수 있습니다. from sklearn.pipeline import Pipeline으로 scaler + model을 묶으면 자동으로 올바른 순서로 적용됩니다.

💡 신경망에서는 배치 정규화(Batch Normalization)를 쓸 수도 있습니다. 입력뿐 아니라 중간층에서도 스케일링 효과를 줍니다.


8. 과적합_방지와_정규화

시작하며

여러분이 시험 공부를 할 때 문제집을 달달 외워버렸다고 생각해보세요. 똑같은 문제가 나오면 100점이지만, 조금만 바뀌어도 틀리죠.

진짜 이해한 게 아니라 암기한 거니까요. 머신러닝 모델도 똑같은 실수를 합니다.

학습 데이터를 너무 완벽하게 외워버려서 새로운 데이터에는 전혀 대응 못하는 거죠. "학습 데이터 정확도 99%, 테스트 정확도 60%"처럼 말이에요.

바로 이럴 때 필요한 것이 정규화(Regularization) 기법입니다. 이것은 모델이 너무 복잡해지는 것을 막아서 일반화 성능을 높여줍니다.

개요

간단히 말해서, 정규화는 모델의 복잡도에 페널티를 주어 과적합을 방지하는 기법입니다. L1(Lasso), L2(Ridge), Elastic Net 같은 방법이 있습니다.

왜 이 개념이 필요한지 실무 관점에서 설명하자면, 실제 서비스에서는 학습 데이터가 아닌 새로운 데이터에 대한 성능이 중요합니다. 예를 들어, 내일 들어올 고객의 이탈 여부를 예측하거나, 처음 보는 이미지를 분류해야 하죠.

정규화 없이는 학습 데이터만 잘 맞추고 실전에서 실패합니다. 기존에는 "데이터를 더 모으자"거나 "특성을 줄이자"처럼 수동적으로 대응했다면, 이제는 정규화 파라미터 하나로 과적합을 제어할 수 있습니다.

정규화의 핵심 특징은 첫째, 가중치 크기를 제한한다는 점, 둘째, L1은 특성 선택 효과가 있고 L2는 가중치를 골고루 줄인다는 점, 셋째, 하이퍼파라미터로 강도를 조절한다는 점입니다. 이러한 특징들이 강건하고 일반화된 모델을 만드는 핵심입니다.

코드 예제

from sklearn.linear_model import Ridge, Lasso
from sklearn.model_selection import train_test_split
import numpy as np

# 집값 예측 데이터 (방개수, 면적, 지하철거리, ...) -> 가격
np.random.seed(42)
X = np.random.randn(100, 10)  # 100개 샘플, 10개 특성
y = X[:, 0] * 3 + X[:, 1] * 2 + np.random.randn(100) * 0.5

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

# L2 정규화 (Ridge): 모든 가중치를 조금씩 줄임
ridge = Ridge(alpha=1.0)  # alpha가 클수록 강한 정규화
ridge.fit(X_train, y_train)
print(f"Ridge 학습 점수: {ridge.score(X_train, y_train):.3f}")
print(f"Ridge 테스트 점수: {ridge.score(X_test, y_test):.3f}")

# L1 정규화 (Lasso): 중요하지 않은 특성을 0으로 만듦
lasso = Lasso(alpha=0.1)
lasso.fit(X_train, y_train)
print(f"\nLasso 학습 점수: {lasso.score(X_train, y_train):.3f}")
print(f"Lasso 테스트 점수: {lasso.score(X_test, y_test):.3f}")
print(f"0이 아닌 계수 개수: {np.sum(lasso.coef_ != 0)}")

설명

이것이 하는 일: 위 코드는 똑같은 데이터에 L1과 L2 정규화를 적용해서 과적합 방지 효과를 비교합니다. 첫 번째로, X와 y를 생성합니다.

실제로는 첫 번째, 두 번째 특성만 중요하고 나머지 8개는 노이즈입니다. train_test_split으로 나눠서 일반화 성능을 테스트할 준비를 합니다.

왜 이렇게 하는지 아시나요? 정규화가 학습/테스트 점수 차이를 줄이는지 확인하기 위해서입니다.

그 다음으로, Ridge(alpha=1.0)가 실행되면서 L2 정규화를 적용합니다. 손실 함수에 '가중치 제곱의 합'이 추가됩니다.

내부에서는 모델이 "예측 오차를 줄이되, 가중치도 가능한 작게 유지하자"라고 균형을 맞춥니다. alpha가 클수록 가중치 페널티가 세져서 모델이 단순해집니다.

그 다음으로, Lasso(alpha=0.1)는 다른 전략을 씁니다. L1 페널티는 '가중치 절댓값의 합'을 추가하는데, 이게 신기하게도 중요하지 않은 특성의 계수를 완전히 0으로 만들어버립니다.

자동으로 특성 선택이 되는 거죠. 마지막으로, score()로 학습/테스트 성능을 비교하면 정규화 덕분에 두 점수 차이가 작아진 걸 확인할 수 있습니다.

일반화가 개선된 거죠. 여러분이 이 코드를 사용하면 복잡한 모델도 안정적으로 학습할 수 있습니다.

실무에서는 특성이 수백~수천 개인 텍스트 분류, 유전자 분석, 추천 시스템 등에서 필수로 사용됩니다. 특히 데이터가 적을 때 정규화 유무가 성패를 가릅니다.

실전 팁

💡 alpha 값은 교차검증으로 찾으세요. RidgeCV, LassoCV를 쓰면 자동으로 최적 alpha를 찾아줍니다. 보통 0.01, 0.1, 1, 10, 100 중에 최적값이 있습니다.

💡 특성이 많고 중요한 게 몇 개뿐이면 Lasso를 쓰세요. Ridge는 모든 특성을 조금씩 사용하지만, Lasso는 중요한 것만 골라냅니다.

💡 둘 다 쓰고 싶으면 ElasticNet을 사용하세요. L1과 L2를 섞어서 각각의 장점을 얻을 수 있습니다. l1_ratio로 비율 조절 가능합니다.

💡 신경망에서는 Dropout과 Weight Decay를 쓰세요. Keras에서는 kernel_regularizer=l2(0.01) 같은 식으로 간단히 추가할 수 있습니다.

💡 정규화 전에 스케일링은 필수입니다. 특성마다 범위가 다르면 페널티가 불공평하게 적용됩니다. StandardScaler를 먼저 적용하세요.


9. 교차_검증과_모델_평가

시작하며

여러분이 수학 실력을 평가받는데 단 한 번의 시험으로 결정된다면 억울하지 않나요? 그날 컨디션이 안 좋았을 수도, 운 좋게 아는 문제만 나왔을 수도 있으니까요.

머신러닝 모델 평가도 마찬가지입니다. 데이터를 한 번만 train/test로 나눠서 평가하면 운이 좋아서 높은 점수가 나왔을 수도, 운이 나빠서 낮게 나왔을 수도 있습니다.

바로 이럴 때 필요한 것이 교차 검증(Cross-Validation)입니다. 이것은 데이터를 여러 번 다르게 나눠서 평가해 평균을 내기 때문에 훨씬 신뢰할 수 있는 성능 측정이 가능합니다.

개요

간단히 말해서, 교차 검증은 데이터를 K개로 나눠서 K번 학습/평가를 반복하고 평균을 내는 기법입니다. 가장 흔한 건 5-Fold CV입니다.

왜 이 개념이 필요한지 실무 관점에서 설명하자면, 데이터가 적을 때 일부를 테스트용으로 빼면 아까운데, 교차 검증은 모든 데이터를 학습과 평가에 모두 사용할 수 있게 해줍니다. 예를 들어, 의료 데이터처럼 샘플 수집이 어려운 경우 매우 유용합니다.

또한 하이퍼파라미터 튜닝할 때 과적합을 방지합니다. 기존에는 "이 모델 정확도 95%!"라고 했는데 막상 실전에서는 70%만 나왔다면, 이제는 "5-Fold CV 평균 88% ± 3%"처럼 신뢰구간까지 알 수 있습니다.

교차 검증의 핵심 특징은 첫째, 모든 데이터가 테스트에 한 번씩 사용된다는 점, 둘째, 성능의 평균과 분산을 알 수 있다는 점, 셋째, 계산 비용이 K배 늘어난다는 점입니다. 이러한 특징들이 신뢰할 수 있는 모델 평가와 선택을 가능하게 합니다.

코드 예제

from sklearn.model_selection import cross_val_score, KFold
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_breast_cancer
import numpy as np

# 유방암 데이터셋 로드
data = load_breast_cancer()
X, y = data.data, data.target

# 랜덤 포레스트 모델
model = RandomForestClassifier(n_estimators=100, random_state=42)

# 5-Fold 교차 검증
cv = KFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=cv, scoring='accuracy')

print(f"각 Fold 정확도: {scores}")
print(f"평균 정확도: {scores.mean():.3f}{scores.std():.3f})")

# 다양한 평가 지표로 교차 검증
from sklearn.model_selection import cross_validate
scoring = ['accuracy', 'precision', 'recall', 'f1']
results = cross_validate(model, X, y, cv=cv, scoring=scoring)

print(f"\nF1 평균: {results['test_f1'].mean():.3f}")
print(f"Precision 평균: {results['test_precision'].mean():.3f}")
print(f"Recall 평균: {results['test_recall'].mean():.3f}")

설명

이것이 하는 일: 위 코드는 유방암 진단 데이터를 5번 다른 방식으로 나눠서 모델을 평가하고, 평균 성능과 표준편차를 계산합니다. 첫 번째로, load_breast_cancer()로 569개 샘플, 30개 특성을 가진 이진 분류 데이터를 불러옵니다.

악성/양성 종양을 구분하는 중요한 의료 문제입니다. 왜 이렇게 하는지 아시나요?

의료 데이터는 적고 중요해서 교차 검증이 필수이기 때문입니다. 그 다음으로, KFold(n_splits=5)가 실행되면서 데이터를 5개 그룹으로 나눕니다.

1번째 실행에서는 1번 그룹을 테스트, 나머지를 학습에 씁니다. 2번째는 2번 그룹을 테스트...

이런 식으로 5번 반복합니다. 내부에서는 매번 다른 80%로 학습하고 20%로 평가합니다.

그 다음으로, cross_val_score()가 이 과정을 자동으로 수행합니다. 모델을 5번 학습시키고, 5개의 정확도 점수를 반환합니다.

예를 들어 [0.95, 0.93, 0.96, 0.94, 0.95]처럼 나오면 평균 0.946 ± 0.011입니다. 마지막으로, cross_validate()로 여러 지표를 동시에 계산합니다.

정확도만으론 부족하니 정밀도, 재현율, F1 점수까지 봐야 의료 진단 모델의 진짜 성능을 알 수 있습니다. 여러분이 이 코드를 사용하면 모델 성능을 과신하거나 과소평가하는 실수를 피할 수 있습니다.

실무에서는 하이퍼파라미터 튜닝(GridSearchCV), 모델 선택, 논문 작성 시 필수로 사용됩니다. 특히 데이터가 수백~수천 개 수준일 때 가장 효과적입니다.

실전 팁

💡 데이터 크기에 따라 K를 조절하세요. 데이터가 많으면 K=3, 보통이면 K=5, 적으면 K=10을 씁니다. Leave-One-Out(LOO)은 데이터가 100개 이하일 때만 고려하세요.

💡 시계열 데이터는 일반 KFold를 쓰면 안 됩니다! 미래 데이터로 과거를 예측하게 되어 데이터 누수가 발생합니다. TimeSeriesSplit을 사용하세요.

💡 불균형 데이터는 StratifiedKFold를 쓰세요. 각 폴드에 클래스 비율을 동일하게 유지해줍니다. 암 진단처럼 양성이 10%뿐이면 필수입니다.

💡 계산 시간을 줄이려면 n_jobs=-1을 설정하세요. 모든 CPU 코어를 사용해서 병렬로 실행합니다. cross_val_score(model, X, y, cv=5, n_jobs=-1)처럼 쓰면 됩니다.

💡 Nested CV로 하이퍼파라미터 튜닝과 평가를 분리하세요. 바깥 루프는 성능 평가, 안쪽 루프는 파라미터 선택에 쓰면 과적합 없는 정확한 평가가 가능합니다.


10. 혼동_행렬과_성능_지표

시작하며

여러분이 암 진단 AI를 만들었는데 "정확도 95%!"라고 자랑한다고 해보세요. 그런데 알고 보니 암 환자를 10명 중 9명이나 놓쳤다면?

전체 중 5%만 암이라서 전부 "정상"이라고 찍어도 95%가 나온 거죠. 정확도 하나만 보면 이런 치명적인 문제를 발견할 수 없습니다.

"맞췄다/틀렸다"보다 "어떻게 틀렸는지"가 훨씬 중요한 경우가 많습니다. 바로 이럴 때 필요한 것이 혼동 행렬(Confusion Matrix)과 다양한 성능 지표들입니다.

이것들은 모델의 실수 패턴을 자세히 보여줘서 어디를 개선해야 할지 알려줍니다.

개요

간단히 말해서, 혼동 행렬은 "실제 A를 A로 맞춤", "실제 A를 B로 틀림" 같은 네 가지 경우를 표로 정리한 것입니다. 여기서 정밀도, 재현율, F1 점수 등을 계산합니다.

왜 이 개념이 필요한지 실무 관점에서 설명하자면, 문제마다 중요한 지표가 다릅니다. 스팸 필터는 정상 메일을 스팸으로 잘못 분류하면 안 되고(높은 정밀도), 암 진단은 암 환자를 놓치면 안 됩니다(높은 재현율).

예를 들어, 사기 거래 탐지는 재현율이 낮으면 사기꾼을 놓치고, 정밀도가 낮으면 정상 고객을 차단해서 둘 다 문제입니다. 기존에는 "정확도 90%면 좋은 거 아냐?"라고 생각했다면, 이제는 "우리 문제에서는 재현율을 95% 이상 올려야 해"처럼 구체적으로 목표를 세울 수 있습니다.

성능 지표의 핵심 특징은 첫째, 정확도는 불균형 데이터에서 오해를 준다는 점, 둘째, 정밀도와 재현율은 트레이드오프 관계라는 점, 셋째, F1은 둘의 조화평균이라는 점입니다. 이러한 특징들이 상황에 맞는 모델 최적화를 가능하게 합니다.

코드 예제

from sklearn.metrics import confusion_matrix, classification_report
from sklearn.metrics import precision_score, recall_score, f1_score
import numpy as np

# 실제 레이블과 예측 레이블 (암 진단 예시)
y_true = np.array([1,0,1,1,0,0,1,0,1,0,1,1,0,0,1])  # 1: 암, 0: 정상
y_pred = np.array([1,0,1,0,0,0,1,0,1,0,0,1,0,1,1])  # 모델 예측

# 혼동 행렬
cm = confusion_matrix(y_true, y_pred)
print("혼동 행렬:")
print(cm)
print("\n[TN  FP]")
print("[FN  TP]")

# 개별 지표 계산
precision = precision_score(y_true, y_pred)
recall = recall_score(y_true, y_pred)
f1 = f1_score(y_true, y_pred)

print(f"\n정밀도 (Precision): {precision:.3f}")
print(f"재현율 (Recall): {recall:.3f}")
print(f"F1 점수: {f1:.3f}")

# 상세 리포트
print("\n상세 분류 리포트:")
print(classification_report(y_true, y_pred,
                          target_names=['정상', '암']))

설명

이것이 하는 일: 위 코드는 암 진단 모델의 예측 결과를 혼동 행렬로 정리하고, 여러 각도에서 성능을 분석합니다. 첫 번째로, y_true와 y_pred 배열을 비교합니다.

y_true[0]=1, y_pred[0]=1이면 True Positive(암을 암으로 맞춤), y_true[1]=0, y_pred[1]=0이면 True Negative(정상을 정상으로 맞춤)입니다. 왜 이렇게 하는지 아시나요?

단순히 "맞았다"보다 "무엇을 어떻게 맞혔는지"가 중요하기 때문입니다. 그 다음으로, confusion_matrix()가 실행되면서 [[TN, FP], [FN, TP]] 형태의 2x2 행렬을 만듭니다.

내부에서는 15개 예측을 하나씩 세어서 True Negative(정상을 정상으로) 6개, False Positive(정상을 암으로) 1개, False Negative(암을 정상으로) 2개, True Positive(암을 암으로) 6개를 계산합니다. 그 다음으로, 정밀도 = TP/(TP+FP) = 6/(6+1) = 0.857을 계산합니다.

"암이라고 예측한 것 중 진짜 암의 비율"입니다. 재현율 = TP/(TP+FN) = 6/(6+2) = 0.750은 "실제 암 환자 중 찾아낸 비율"입니다.

마지막으로, F1 점수는 정밀도와 재현율의 조화평균으로 2/(1/정밀도 + 1/재현율) = 0.8입니다. 둘 중 하나만 높아서는 안 되고, 균형이 중요할 때 씁니다.

여러분이 이 코드를 사용하면 모델의 약점을 정확히 파악할 수 있습니다. 실무에서는 "FP를 줄이자"(스팸 필터), "FN을 줄이자"(암 진단), "F1을 최대화하자"(검색 엔진) 같은 명확한 목표를 세우고 모델을 개선합니다.

혼동 행렬 없이는 제대로 된 개선이 불가능합니다.

실전 팁

💡 클래스가 불균형하면 정확도는 무시하세요. 99% 정상, 1% 비정상 데이터에서는 F1이나 AUROC를 봐야 합니다. sklearn.metrics.balanced_accuracy_score도 유용합니다.

💡 정밀도와 재현율 중 뭘 우선할지 미리 정하세요. 의료/보안은 재현율(놓치면 안 됨), 추천/광고는 정밀도(잘못 보여주면 신뢰 하락)가 중요합니다.

💡 임계값을 조절해서 트레이드오프를 제어할 수 있습니다. predict_proba()로 확률을 얻고, threshold를 0.5 대신 0.3으로 낮추면 재현율이 올라갑니다.

💡 다중 클래스는 macro/micro/weighted 평균을 구분하세요. classification_report의 average 파라미터로 조절 가능합니다. 불균형이면 weighted를 추천합니다.

💡 ROC 곡선과 AUC를 함께 보세요. from sklearn.metrics import roc_auc_score로 계산하면 임계값에 무관한 전체적인 성능을 알 수 있습니다.


#AI#Classification#VectorSpace#MachineLearning#DataScience

댓글 (0)

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