머신러닝

머신러닝 - 분류, 군집, SVM

haniru 2026. 1. 12. 20:46

 

1. 머신러닝의 학습 방법 (Learning Methods)

  • 지도학습 (Supervised Learning): 문제(Input, X)와 정답(Label, y)이 모두 존재함.
    • 회귀 (Regression): 결과값이 연속적인 실수 (예: 집값 예측, 온도 예측).
    • 분류 (Classification): 결과값이 이산적인 클래스 (예: 고양이 vs 강아지, 0 또는 1).
  • 비지도학습 (Unsupervised Learning): 정답(y) 없이 데이터(X)만 존재. 데이터의 패턴이나 구조를 스스로 학습함. (예: 군집화).
  • 사전학습 (Pre-training): 대량의 데이터로 미리 학습된 모델을 가져와서 내 목적에 맞게 조금 더 학습(Fine-tuning)시키는 것.
  • 강화학습 (Reinforcement Learning): 행동(Action)에 대한 보상(Reward)을 통해 학습. 시행착오를 겪으며 보상을 최대화하는 방향으로 감.

2. 회귀 vs 분류 (Regression vs Classification)

이 둘은 출력 형태와 **손실 함수(Loss Function)**에서 결정적인 차이가 있음.

구분 회귀 (Regression) 분류 (Classification)
출력값 연속적인 실수 (23.5, -1.2 등) 확률 또는 범주 (0 or 1, Dog or Cat)
정답 형태 숫자 그 자체 One-hot Encoding 등 ([0, 1, 0])
에러 측정 MSE (Mean Squared Error) Cross-Entropy (교차 엔트로피)
  • 왜 분류에서 MSE를 잘 안 쓰는가?
    • 분류의 정답은 숫자의 크기가 중요한 게 아니라 '맞냐 틀리냐(확률)'의 문제임. MSE를 쓰면 학습 속도가 느려지거나 로컬 미니멈(Local Minimum)에 빠질 수 있음.
    • 따라서 확률 분포의 차이를 계산하는 Cross-Entropy를 사용함.
  • 확률과 Softmax:
    • 모델의 출력값을 0~1 사이의 확률로 만들어주는 함수가 Softmax (다중 분류) 또는 Sigmoid (이진 분류).
    • (1-a)와 a의 합은 항상 1이 되어야 함 (확률의 총합은 1). 이는 이항 분포(Binomial)의 논리와 연결됨.

3. 분류 vs 군집화 (Classification vs Clustering)

  • 공통점: 대상을 '그룹'으로 묶는다는 행위.
  • 차이점 (결정적): 정답(Label)의 유무.
    • 분류 (Classification): 지도학습. "이건 A고 이건 B야"라고 알려주고 학습시킴.
    • 군집화 (Clustering): 비지도학습. 정답(y)이 없음. "비슷한 것끼리 뭉쳐봐"라고 시킴. 거리(Distance) 기반으로 가까운 데이터끼리 묶음 (K-Means 등).
    • 따라서 군집화는 y_train, y_test가 필요 없음.

 

코드

===================== 시그모이드=====================

 

시그모이드 함수란?: 아무리 크거나 작은 숫자가 들어와도 무조건 0과 1 사이로 결과를 바꿔버리는 함수입니다.

  • 결과가 1에 가까우면 "확실히 맞다!"
  • 결과가 0에 가까우면 "확실히 아니다!"
  • 결과가 0.5라면 "반반인데...?"라고 판단하는 기준
# 1. 수치 계산을 아주 잘하는 'numpy'라는 도구를 가져오기.
import numpy as np

# 2. 그래프를 그려주는 'matplotlib'이라는 도구를 가져오기.
import matplotlib.pyplot as plt

# 3. '시그모이드(sigmoid)'라는 이름의 수학 공식(함수)을 정의.
# 이 공식은 어떤 숫자든 0과 1 사이의 값으로 바꿔주는 마법의 공식.
def sigmoid(x):
   # x라는 숫자가 들어오면, 1 / (1 + e의 -x승)이라는 계산을 해서 결과를 돌려줌.
   return 1.0 / (1.0 + np.exp(-x))

# 4. 그래프의 가로축(x)에 쓸 숫자들을 준비.
# -5.0부터 5.0까지 0.1 간격으로 숫자들을 촘촘하게 나열. (-5.0, -4.9, -4.8 ...)
x = np.arange(-5., 5., 0.1)

# 5. 위에서 만든 x 숫자들을 시그모이드 공식에 넣어서 그래프를 그리기.
# 가로(x)는 우리가 만든 숫자들, 세로(y)는 그 숫자를 공식에 넣었을 때 나온 결과값.
plt.plot(x, sigmoid(x))

# 6. 완성된 그래프를 화면에 보여줌.
plt.show()

 

k는 그래프의 모양을 결정하는 아주 중요한 역할을 한. 통계나 AI에서는 보통 이 숫자를 **'가중치(Weight)'**라고 부르기도함

k는 **"인공지능이 얼마나 단호하게 판단하느냐"**를 결정

import numpy as np
import matplotlib.pyplot as plt

# 1. 시그모이드 함수에 'k'라는 변수를 추가
# k는 그래프가 얼마나 '가파르게' 꺾일지를 결정하는 숫자
def sigmoid(x, k):
    # x에 k를 곱해줌으로써 그래프의 경사도를 조절
   return 1.0/(1.0 + np.exp(-x*k))

# 2. x축 범위를 -10부터 10까지로 지난번보다 더 넓게 잡기
x = np.arange(-10., 10., 0.1)

# 3. 세 가지 다른 k값을 넣어서 그래프를 그리기
# 'r'은 빨간색(Red), 'g'는 초록색(Green), 'b'는 파란색(Blue)
# label은 오른쪽 위 '범례'에 표시될 이름
plt.plot(x, sigmoid(x, .5), 'r', label='k=.5')
plt.plot(x, sigmoid(x, 1), 'g', label='k=1')
plt.plot(x, sigmoid(x, 4), 'b', label='k=4')

# 4. 그래프 우측 상단에 각 선이 무엇인지 알려주는 '범례(Legend)'를 표시
plt.legend()

 

k가 곡선의 '가파르기'를 정했다면, 이번의 b는 곡선의 **'기준점'**을 정함

b라는 숫자는 인공지능에서 **'편향(Bias)'**이라고 부르는 아주 중요한 개념. 쉽게 말해, 인공지능이 판단을 내릴 때 **"기본적으로 깔고 가는 성향"**을 조절

# 1. 시그모이드 함수에 'b'라는 변수를 추가
# 이 b는 그래프를 왼쪽이나 오른쪽으로 '이동'시키는 역할
def sigmoid(x, b):
    # x에 b를 더해줌으로써 그래프의 중심 위치를 옮김
  return 1.0/(1.0 + np.exp(-x + b))

# 2. x축 범위를 -10부터 10까지 준비
x = np.arange(-10., 10., 0.1)

# 3. 세 가지 다른 b값을 넣어서 그래프를 그림
# b값에 따라 S자 곡선이 통째로 옆으로 움직이는 것을 볼 수 있다
plt.plot(x, sigmoid(x, b=-5), 'r', label='b=-5') # 왼쪽으로 이동
plt.plot(x, sigmoid(x, b=0), 'g', label='b=0') # 정중앙 (기본)
plt.plot(x, sigmoid(x, b=5), 'b', label='b=5') # 오른쪽으로 이동

# 4. 각 선이 어떤 b값인지 알려주는 설명 상자(범례)를 표시
plt.legend()

 

 

===================== 닥스훈트=====================

이 코드는 머신러닝의 핵심 분야인 **'분류(Classification)'**의 아주 기초적인 모습을 보여줌.

1. 데이터의 특징(Feature)

위 코드에서 Length(길이)와 Height(높이)는 강아지를 구분하기 위한 **특징(Feature)**

2. 시각적 분류

그래프를 그려보면 빨간색 점(닥스훈트)들은 아래쪽에 모여 있고, 파란색 점(진돗개)들은 위쪽에 모여 있다.

  • 닥스훈트: 길이에 비해 키가 작음 (아래쪽 그룹)
  • 진돗개: 길이에 비해 키가 큼 (위쪽 그룹)

3. 예측(Prediction)

초록색 오각형 빨간색 점들 근처에 있을 확률이 높다.

  • 사람의 판단: "빨간 점들이랑 가까우니까 얘도 닥스훈트겠네!"
  • AI의 판단: 이 '가깝다'라는 개념을 수학적으로 계산해서 정답을 맞히는 것이 바로 머신러닝 알고리즘(예: KNN)임.
import matplotlib.pyplot as plt

# 1. 닥스훈트(Dachshund)의 몸길이와 키 데이터
dach_length = [55, 57, 64, 63, 58, 49, 54, 61]
dach_height = [30, 31, 36, 30, 33, 25, 37, 34]
# 2. 진돗개(Jindo dog)의 몸길이와 키 데이터
jin_length = [56, 47, 56, 46, 49, 53, 52, 48]
jin_height = [52, 52, 50, 53, 50, 53, 49, 54]

# 3. 정체를 모르는 새로운 강아지 데이터
newdata_length = [59]
newdata_height = [35]

# 4. 산점도(Scatter plot, 점 찍기) 그리기

# 빨간색(r) 동그라미로 닥스훈트를 표시
plt.scatter(dach_length, dach_height, c='r', label='Dachshund')

# 파란색(b) 삼각형(^)으로 진돗개를 표시
plt.scatter(jin_length, jin_height,c='b',marker='^', label='Jindo dog')

# 초록색(g) 오각형(p)으로 정체 모를 '새 데이터'를 표시
plt.scatter(newdata_length, newdata_height, s=100, marker='p', c='g', label='new Data')

# 5. 그래프 꾸미기
plt.xlabel('Length') # 가로축 이름: 길이
plt.ylabel('Height') # 세로축 이름: 높이
plt.title("Dog size") # 그래프 제목
plt.legend(loc='upper right') # 오른쪽 위에 범례(이름표) 표시

 

데이터 가공 - **'컴퓨터가 공부할 수 있는 형태'**로 데이터를 가공

1. 문제지 만들기 (column_stack)

컴퓨터에게 "길이는 55고 높이는 30이야"라고 따로 알려주는 것보다, **"[55, 30]"**처럼 한 세트로 묶어서 주는 것이 훨씬 적이다. 이를 데이터 과학에서는 **특징 벡터(Feature Vector)**를 만든다고 한다.

2. 정답지 만들기 (Labeling)

컴퓨터는 "닥스훈트", "진돗개" 같은 문자를 직접 계산할 수 없다. 그래서 숫자로 바꿔주는 과정이 필요하다.

  • 0: 닥스훈트 (Dog A)
  • 1: 진돗개 (Dog B)

이렇게 이름을 숫자로 바꾸는 것을 **레이블 인코딩(Label Encoding)**이라고 한.

import numpy as np

# 1. 닥스훈트의 길이와 높이 정보를 하나로 묶기
# column_stack은 [길이] 리스트와 [높이] 리스트를 세로로 붙여서 (길이, 높이) 쌍으로 만듦
d_data = np.column_stack((dach_length, dach_height))

# 2. 닥스훈트 데이터에 '정답지'를 만들기
# np.zeros(len(d_data))는 데이터 개수만큼 0으로 채워진 리스트를 만들기
# 여기서는 '0'이 '닥스훈트'
d_label = np.zeros(len(d_data))

# 3. 진돗개 데이터도 똑같이 (길이, 높이) 쌍으로 묶기
j_data = np.column_stack((jin_length, jin_height))

# 4. 진돗개의 정답지는 '1'로 만들기
# np.ones는 데이터 개수만큼 1로 채워진 리스트를 만듦
# 여기서는 '1'이 '진돗개'라는 뜻
j_label = np.ones(len(j_data))

# 5. 생성된 정답지(레이블)를 출력해서 확인
print(d_label)
print(j_label)
종류 특징 (Feature) 정답 (Label)
닥스훈트 1번 [55, 30] 0
닥스훈트 2번 [57, 31] 0
... ... ...
진돗개 1번 [56, 52] 1
진돗개 2번 [47, 52] 1

 

지금까지 닥스훈트와 진돗개를 따로따로 정리했다면, 이제는 컴퓨터에게 "자, 여기 강아지들의 데이터(문제)와 정답이 섞여 있는 전체 교과서가 있어. 이걸 보고 공부하렴!" 하고 합쳐주는 과정

import numpy as np

# 1. 정체를 밝혀내고 싶은 새로운 강아지의 데이터
# 나중에 AI에게 "이 데이터(길이 59, 높이 35)는 무슨 개니?"라고 물어볼 용도
newdata = [[59, 35]] # 추론할 것

# 2. 닥스훈트 데이터(d_data)와 진돗개 데이터(j_data)를 하나로 합침
# np.concatenate는 여러 개의 리스트를 하나로 길게 이어 붙이는 함수
# 이제 'dogs'는 모든 강아지의 (길이, 높이)가 들어있는 커다란 문제집이 됨
dogs = np.concatenate((d_data, j_data))

# 3. 닥스훈트 정답(d_label)과 진돗개 정답(j_label)도 하나로 합침
# 'labels'는 모든 강아지의 정답(0 또는 1)이 들어있는 정답지가 됨
labels = np.concatenate((d_label, j_label))

# 4. 숫자로 되어 있는 정답을 우리가 읽기 편하게 글자로 바꿔주는 '사전'을 만듦
# 0이 나오면 '닥스훈트', 1이 나오면 '진돗개'라고 부르기로 약속
dog_classes = {0:'닥스훈트', 1:'진돗개'}

print(dogs)
print(labels)

 

AI 모델 생성 후 실제로 정답 맞히기(추론)

1. 로지스틱 회귀 (Logistic Regression)

이 모델은 내부적으로 시그모이드 함수(S자 곡선) 자 곡선을 그림.

  • 데이터가 곡선의 어느 지점에 위치하는지 보고, 0.5보다 크면 1(진돗개), **0.5보다 작으면 0(닥스훈트)**라고 판단

2. 계수(w)와 절편(b)

AI가 학습을 한다는 건, 결국 가장 완벽한 wb 값을 찾아내는 과정.

  • w (계수/가중치): "길이가 중요할까, 높이가 중요할까?"를 결정. 만약 높이에 곱해지는 w값이 크다면, AI는 "키가 큰 게 진돗개를 구분하는 데 더 중요하구나!"라고 판단
  • b (절편/편향): 판단의 기준점을 왼쪽이나 오른쪽으로 밀어주는 역할.

3. 왜 정확도가 1.0(100%)이 나오는지

현재 닥스훈트(다리 짧음)와 진돗개(다리 김)의 데이터 차이가 너무 명확하기 때문. 그래프를 그렸을 때 두 그룹이 멀리 떨어져 있으면 AI는 아주 쉽게 100% 정답을 맞히는 '필승 공식'을 찾아낼 수 있다.

4. predict vs predict_proba

  • predict: "얘는 0번(닥스훈트)이야!"라고 결론을 내림.
  • predict_proba: "음... 내가 보기에 98% 확률로 닥스훈트고, 2% 확률로 진돗개 같아."라고 확률을 보여줌. AI가 얼마나 확신하고 있는지 알 수 있다.
import numpy as np
from sklearn.linear_model import LogisticRegression# 1. AI 분류 모델 가져오기
from sklearn import metrics

# 2. 로지스틱 회귀(Logistic Regression)라는 이름의 AI 모델을 만들기
# 이름은 '회귀'지만, 사실 "이게 맞니 아니니?"를 판단하는 '분류' 모델
logistic = LogisticRegression()

# 3. 모델 학습 (공부시키기)
# dogs(문제)와 labels(정답)를 주고 규칙을 찾으라고 시킴
# *참고: 보통은 시험용 데이터를 따로 빼두지만, 여기선 데이터가 너무 적어(16마리) 전체로 공부
logistic.fit(dogs, labels)

# 4. AI가 찾아낸 필승 공식(수식) 확인하기
# w(계수)는 우리가 앞에서 배운 '가파르기(k)'와 같고, b(절편)는 '위치(b)'와 같음
w = logistic.coef_[0] # 가중치(Weight): 어떤 특징이 더 중요한가?
b = logistic.intercept_[0] # 편향(Bias): 기본적으로 어느 쪽으로 치우쳐 있는가?
print('계수:', w)
print('절편:', b)

# 5. 공부 결과 확인 (정확도)
# 1.0이 나오면 100% 다 맞혔다는 뜻 (지금은 두 강아지 특징이 뚜렷)
print('분류 정확도:', logistic.score(dogs, labels))

# 6. 새로운 데이터로 퀴즈 내기 (추론)
# 아까 만든 newdata([59, 35])가 누구인지 물어보기
y_pred = logistic.predict(newdata) # 결과값 (0 또는 1)

# 7. 확신 정도 확인하기
# [닥스훈트일 확률, 진돗개일 확률]을 알려줌
y_pred_prob = logistic.predict_proba(newdata)
print(f'데이터: {newdata}')
print(y_pred[0]) # 0이 나오면 닥스훈트
print(f'판정 결과: {dog_classes[y_pred[0]]}, 판정 확률: {y_pred_prob}')

 

 

시각화

 

  • 데이터 점들: 우리가 가진 강아지들의 실제 정보.
  • 흰색 선: AI가 공부해서 찾아낸 **"판단 기준선"**.
  • 초록색 오각형(새 데이터): 이 검은색 선보다 아래에 있으면 AI는 "닥스훈트"라고 부르고, 위에 있으면 "진돗개"라고 부름.

점수 = (길이*w0) + (높이*w1) + b

  • 이 점수가 **플러스(+)**면 진돗개!
  • 이 점수가 **마이너스(-)**면 닥스훈트!
  • 이 점수가 딱 0이면? "나도 잘 모르겠어(반반이야)" 하는 지점

0이되는 지점을 연결하면 선이됨

 

이항하면

plt.scatter(dach_length, dach_height, c='r', label='Dachshund')
plt.scatter(jin_length, jin_height,c='b',marker='^', label='Jindo dog')

# 새 데이터
plt.scatter(newdata_length, newdata_height, s=100, marker='p', c='g', label='new Data')

# 1. 그래프를 그릴 가로축(길이) 범위를 정함
# np.linspace(최솟값, 최댓값, 200) : 가장 짧은 개와 가장 긴 개의 길이 사이를 200등분
x0 = np.linspace(np.min([dach_length, jin_length]),\
                 np.max([dach_length, jin_length]), 200)

# 2. ★결정 경계(Decision Boundary) 계산★
# AI가 "이 선을 기준으로 위는 진돗개, 아래는 닥스훈트야!"라고 정한 선
# 수학적으로는 w0*x0 + w1*x1 + b = 0 이 되는 지점(확률 0.5)을 찾는 과정
# 우리가 중학교 때 배운 y = ax + b 형태로 바꾸기 위해 수식을 정리한 것
decision_boundary = -w[0]/w[1] * x0 - b/w[1]

# 3. 계산된 경계선을 검은색 실선("k-")으로 그림
plt.plot(x0, decision_boundary, "k-", linewidth=2)

# 4. 최종 그래프 출력
plt.show()

 

AI가 열심히 공부해서 찾아낸 **'판단 기준선(결정 경계)'**에 실제 강아지 데이터를 넣어보는 과정

AI가 특정 강아지를 보고 "음, 이 녀석은 확실히 닥스훈트군!" 혹은 "이 녀석은 진돗개네!"라고 느끼는 **'확신의 정도'**를 숫자로 계산

1. 부호의 의미 (방향)

  • 결과가 마이너스(-): 결정 경계선 아래쪽에 있다는 뜻. 우리 AI에게는 "닥스훈트 영역".
  • 결과가 플러스(+): 결정 경계선 위쪽에 있다는 뜻. 우리 AI에게는 "진돗개 영역".

2. 숫자의 크기 (거리/확신)

  • 숫자가 0에 아주 가깝다면: 경계선 바로 근처에 있다는 뜻. AI도 "음... 좀 헷갈리는데?"라고 생각하는 상태.
  • 숫자가 0에서 아주 멀다면: 경계선에서 멀리 떨어져 있다는 뜻. AI가 "이건 누가 봐도 확실한 닥스훈트(또는 진돗개)네!"라고 매우 확신하는 상태.
# 1. 첫 번째 닥스훈트 데이터를 AI의 필승 공식(w1*x1 + w2*x2 + b)에 넣기
# 결과값(dach_0)이 0보다 작으면 AI는 이 강아지를 '0번(닥스훈트)' 그룹으로 분류
dach_0 = w[0]*dach_length[0] + w[1]*dach_height[0] + b

# 2. 첫 번째 진돗개 데이터를 똑같은 공식에 넣기
# 결과값(jin_0)이 0보다 크면 AI는 이 강아지를 '1번(진돗개)' 그룹으로 분류
jin_0 = w[0]*jin_length[0] + w[1]*jin_height[0] + b

# 3. 계산된 '점수'를 출력
# 이 점수가 0에서 멀어질수록 AI는 "이건 진짜 확실해!"라고 자신감을 가짐
print('닥스훈트 샘플 대입 결과:', dach_0)
print('진돗개 샘플 대입 결과:', jin_0)

 

 

k-NN

로지스틱 회귀가 '선'을 그어 구역을 나누는 방식이었다면, k-NN은 아주 단순하게 **"유유상종(끼리끼리 논다)"**의 원리를 이용

k-NN은 새로운 데이터(초록색 오각형)가 들어왔을 때, 지도 위에서 가장 가까이 있는 k개의 이웃을 찾아 그중 가장 머릿수가 많은 쪽으로 정체를 결정하는 방식

 

**k**는 "이웃을 몇 명까지 물어볼 것인가?"를 정하는 숫자입니다. 이 숫자에 따라 정답이 바뀔 수 있다.

k는 보통 홀수로 정한다 (짝수면 무승부 나올 수 있음)

k의 값 설명 특징
k=1 가장 가까운 이웃 1명에게만 물어봄. 판단이 매우 빠르고 예민하다. (데이터가 조금만 튀어도 결과가 확 바뀜)
k=3 가장 가까운 3명에게 물어봐서 다수결로 결정. 보통 가장 많이 사용되는 안정적인 방식이다.
k=15 거의 모든 데이터에게 물어봄. 너무 많은 이웃을 고려하다 보니, 정작 내 근처의 특징을 무시하게 된다.

 

컴퓨터는 '가깝다'는 것을 수학적으로 거리를 계산해서 알아낸다. 이때 주로 피타고라스의 정리와 비슷한 유클리드 거리(Euclidean Distance) 공식을 사용한다.

 

우리가 그린 그래프에서 새 데이터(59, 35) 주변을 살펴보자.

  1. k=1일 때: 가장 가까운 점 하나가 **빨간색(닥스훈트)**이라면? AI는 "닥스훈트!"라고 외침.
  2. k=3일 때: 가장 가까운 3개를 찾았는데 [빨강, 빨강, 파랑]이 나왔다면? 2:1 다수결로 **"닥스훈트"**가 이김.
  3. k가 너무 크면?: 만약 k를 16(전체 데이터 개수)으로 잡으면, 그냥 숫자가 더 많은 종으로 결론을 내려버리는 오류가 생김.

가장 가까운 것 알아내기 - '원(Circle)을 크게 그리기'

  1. 새로운 데이터(초록색 오각형)를 중심에 둔다.
  2. 원을 아주 작게 그렸다가 점점 키운다.
  3. 원에 이웃 데이터(점)가 하나씩 들어오기 시작한다.
  4. 우리가 설정한 k개(예: 3개)의 점이 원 안에 들어올 때까지 원을 키운다.
  5. 원 안에 들어온 3개의 점이 무엇인지 확인하고 다수결을 낸다.

이때 컴퓨터는 **'피타고라스의 정리'**를 활용해 점 사이의 직선거리를 계산한다.

import numpy as np
from sklearn.neighbors import KNeighborsClassifier # 1. k-NN 분류 도구를 가져오기
from sklearn import metrics

# 닥스훈트의 길이와 높이 데이터
dach_length = [55, 57, 64, 63, 58, 49, 54, 61]
dach_height = [30, 31, 36, 30, 33, 25, 37, 34]

# 진돗개의 길이와 높이 데이터
jin_length = [56, 47, 56, 46, 49, 53, 52, 48]
jin_height = [52, 52, 50, 53, 50, 53, 49, 54]
newdata = [[59, 35]]

d_data = np.column_stack((dach_length, dach_height))
d_label = np.zeros(len(d_data))   # 닥스훈트는 0으로 레이블링
j_data = np.column_stack((jin_length, jin_height))
j_label = np.ones(len(j_data))   # 진돗개는 1로 레이블링

dogs = np.concatenate((d_data, j_data))
labels = np.concatenate((d_label, j_label))

dog_classes = {0:'닥스훈트', 1:'진돗개'}

# 2. k-NN 모델 만들기 (n_neighbors = k)
# k=3은 "가장 가까운 이웃 3개를 보고 결정해!"라는 뜻
k = 3
knn = KNeighborsClassifier(n_neighbors = k)

# 3. 모델 학습 (fit)
# 준비된 강아지 데이터(dogs)와 정답(labels)을 AI에게 학습시킴
# k-NN은 학습할 때 데이터를 머릿속에 그대로 '기억'해둠
knn.fit(dogs, labels)

# 4. 새로운 데이터(newdata)를 보고 정체를 예측
# AI가 지도 위에서 새로운 점과 가장 가까운 점 3개를 찾아 다수결을 내림
y_pred = knn.predict(newdata)

# 5. 결과 출력
# 예측한 번호(y_pred[0])를 사전(dog_classes)을 통해 한글 이름으로 바꿈
print('데이터', newdata, ', 판정 결과:', dog_classes[y_pred[0]])

# 6. 모델의 정확도 확인
# 16마리 데이터를 얼마나 잘 맞히는지 점수를 냄 (1.0이면 100점)
print(knn.score(dogs, labels))

 

===================== 붓꽃=====================

knn - 붓꽃 데이터

1. 무엇을 조사했는가? (특징, Features)

컴퓨터가 꽃의 종류를 구분할 수 있도록 4가지 수치를 측정했다.

  • Sepal Length: 꽃받침의 길이
  • Sepal Width: 꽃받침의 너비
  • Petal Length: 꽃잎의 길이
  • Petal Width: 꽃잎의 너비

2. 정답은 무엇인가? (레이블, Target/Label)

총 3가지 종류의 붓꽃이 섞여 있다.

  • 0: 세토사 (Setosa)
  • 1: 버시컬러 (Versicolor)
  • 2: 버지니카 (Virginica)

3. 왜 150개인가?

각 꽃의 종류마다 50송이씩 공평하게 데이터를 수집했기 때문에 50 * 3 = 150개의 데이터가 들어있는 것이다.

import numpy as np
from sklearn.datasets import load_iris # 1. 사이킷런에서 제공하는 붓꽃 데이터를 가져온다.

# 2. 붓꽃 데이터를 불러와서 'iris'라는 변수에 저장한다.
# 이 데이터 안에는 꽃잎/꽃받침의 크기와 그 꽃의 종류(정답)가 모두 들어있다.
iris = load_iris()

# 3. 데이터 전체를 출력한다.
# 결과는 파이썬의 '딕셔너리'라는 형태와 비슷하게 나온다.
print(iris)

# 4. 데이터셋에 대한 상세한 설명을 출력한다.
# 누가 조사했는지, 어떤 속성이 있는지 등을 영어로 친절하게 설명해준다.
print(iris.DESCR)

# 5. 실제 숫자 데이터 중 앞부분 3개만 살짝 본다.
# [꽃받침 길이, 꽃받침 너비, 꽃잎 길이, 꽃잎 너비] 순서로 숫자가 들어있다.
print(iris.data[:3])

# 6. 데이터가 총 몇 개인지, 속성은 몇 개인지 형태(Shape)를 확인한다.
# (150, 4)라고 나오는데, 이는 '150송이의 꽃에 대해 4가지 특징을 조사했다'는 뜻이다.
print('iris 데이터의 형태:', iris.data.shape)

# 7. 4가지 속성의 진짜 이름이 무엇인지 확인한다.
print('iris 데이터의 속성들:', iris.feature_names)

# 8. 각 꽃의 정답(레이블)을 확인한다.
# 0은 Setosa, 1은 Versicolor, 2는 Virginica라는 꽃 종류를 의미한다.
print('iris 데이터의 레이블:', iris.target)

 

Pandas 사용

import pandas as pd # 1. 데이터를 표 형태로 아주 쉽게 다루게 해주는 'pandas'를 가져옴

# 2. 넘파이 배열(숫자 뭉치)이었던 데이터를 예쁜 표(DataFrame)로 바꿈
# columns=iris.feature_names를 써서 '꽃잎 너비' 같은 제목을 각 칸에 붙여줌
iris_df = pd.DataFrame(iris.data, columns=iris.feature_names)

# 3. 'target'이라는 이름의 새로운 열(Column)을 추가
# 여기에 0, 1, 2라는 정답지(꽃의 종류)를 쏙 집어넣는 과정
iris_df['target'] = pd.Series(iris.target)

# 4. 데이터프레임의 가장 앞부분 5행을 보여달라고함
# 표가 잘 만들어졌는지 맛보기로 확인하는 명령
iris_df.head()

 

데이터 요약

iris_df.describe()

 

'target' 열(정답)에 들어있는 각 종류별 데이터가 몇 개인지 세어줌

iris_df['target'].value_counts()

 

iris_df라는 표에서 인덱스(행 번호)와 컬럼명(열 이름)을 모두 제외하고, 오직 안에 들어있는 '숫자 데이터'만 추출하여 넘파이 배열(Array) 형태로 바꾸기

iris_df.values

 

AI 모델의 성능을 검증(Validation)

# 1. 데이터 나누기
# iloc[:, :4] : 앞의 4개 컬럼(꽃받침/꽃잎 크기)은 문제지(X)
# iloc[:, -1] : 마지막 컬럼(target)은 정답지(y)
X = iris_df.iloc[:, :4]
y = iris_df.iloc[:, -1]

from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn import metrics
from sklearn.metrics import confusion_matrix

def iris_knn(X, y, k):
    # 2. 데이터를 학습용(70%)과 시험용(30%)으로 쪼개기
    # 시험 문제를 미리 보면 커닝이 되니까, AI가 한 번도 못 본 문제를 만드는 과정
    X_train,X_test,y_train,y_test = train_test_split(X, y, test_size=0.3)

    # 3. 모델 만들고 학습시키기
    knn = KNeighborsClassifier(n_neighbors = k)
    knn.fit(X_train, y_train)

    # 4. 시험 보기
    # X_test(시험 문제)를 보고 정답(y_pred)을 찍음
    y_pred = knn.predict(X_test)

    print("AI가 찍은 정답:", y_pred)
    print("실제 정답(지표):", y_test.values) # 비교하기 편하게 values로 출력

    # 1. 판다스로 나란히 붙여서 보기
    comparison = pd.DataFrame({'실제정답': y_test, 'AI예측': y_pred})
    print(comparison.head(10)) # 앞의 10개만 비교

    # 2. 혼동 행렬(Confusion Matrix) 그리기
    # 대각선에 숫자가 몰려있으면 잘 맞힌 것이고, 다른 칸에 숫자가 있으면 그 꽃들끼리 헷갈려 했다는 뜻
    print(confusion_matrix(y_test, y_pred))

    #  **"문제지(X)"**와 **"진짜 정답(y)"**
    # AI가 X_test로 문제를 풀어보고 나온 답이랑 y_test를 비교해서 점수 알려줌
    knn.score(X_test, y_test)

    # 5. 채점하기
    # metrics.accuracy_score는 '실제 정답'과 'AI가 찍은 답'을 비교해서 맞은 비율을 알려줌
    return metrics.accuracy_score(y_test, y_pred) # y_test랑 y_pred랑 정답을 비교

k = 3
scores = iris_knn(X, y, k)
print('n_neighbors가 {0:d}일때 정확도: {1:.3f}'.format(k, scores))

 

다양한 k 값을 한꺼번에 테스트하여 가장 똑똑한 모델을 찾는 과정

import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn import metrics

# 1. 데이터 준비 및 분할
iris = load_iris()
# random_state를 고정하면 매번 실행할 때마다 데이터가 섞이는 방식이 같아져 결과가 일정해집니다.
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, test_size=0.3, random_state=42)

k_range = range(1, 31) # k를 1부터 30까지 테스트해보겠다는 뜻
scores = []

# 2. 루프(Loop)를 돌며 k값별로 성적 확인
for k in k_range:
    knn = KNeighborsClassifier(n_neighbors=k)
    knn.fit(X_train, y_train)
    y_pred = knn.predict(X_test)
    scores.append(metrics.accuracy_score(y_test, y_pred))

# 3. 결과 시각화
plt.plot(k_range, scores, marker='o')
plt.xlabel('Value of K for KNN')
plt.ylabel('Testing Accuracy')
plt.title('Finding the Best K Value')
plt.show()
k의 크기 모델의 성격 수학적 표현 실제 영향
매우 작음 (k=1) 매우 복잡하고 예민함 고분산 (High Variance) 훈련 데이터만 잘 맞히고 실전엔 약함
적당함 (k=5~15) 안정적이고 균형 잡힘 일반화 (Generalization) 가장 성능이 좋은 지점
매우 큼 (k=50...) 너무 단순하고 둔감함 고편향 (High Bias) 데이터를 대충 보고 판단함

 

AI에게 **"공부"**를 시킨 뒤, 정체를 모르는 **"새로운 꽃"**을 가져와서 정답을 맞춰보라고 시키는 과정

1. knn.predict(X) : AI의 첫 실전

학습된 모델에게 **한 번도 본 적 없는 특징(X)**을 던져주었을 때, AI가 "이건 setosa 같아요!"라고 대답하는 과정이다. 내부적으로는 이 새로운 점 근처에 있는 이웃 3개를 찾아서 다수결을 내렸을 것이다.

2. metrics.accuracy_score (정확도)

이 코드는 AI가 얼마나 똑똑한지 측정한다. 만약 결과가 1.000이 나온다면, AI가 공부한 150개의 꽃 문제를 다시 풀었을 때 단 하나도 틀리지 않고 다 맞혔다는 뜻이다.

3. 주의할 점 (훈련 데이터로 시험 보기)

지금 코드는 **공부한 문제집(iris.data)**으로 다시 시험을 본 셈이다.

  • 이건 마치 "어제 푼 수학 문제집을 오늘 다시 풀어서 100점 맞은 상황"과 같다.
  • 그래서 점수가 매우 높게 나오지만, 실전에서도 이렇게 잘할지는 아직 확신할 수 없다. 그래서 우리는 보통 앞서 배웠던 train_test_split을 써서 공부용과 시험용을 분리하는 것이다.
from sklearn.datasets import load_iris
from sklearn.neighbors import KNeighborsClassifier
from sklearn import metrics # ★ 중요: accuracy_score를 쓰려면 이 임포트가 꼭 필요

# 1. 설정 및 데이터 불러오기
k = 3
iris = load_iris()

# 2. AI 모델 만들기 및 학습
# n_neighbors=3: "가장 가까운 이웃 3개를 보고 판단해라"
knn = KNeighborsClassifier(n_neighbors = k)
# fit: 준비된 150개의 붓꽃 데이터(data)와 정답(target)을 주고 공부 시킨다.
knn.fit(iris.data, iris.target)

# 3. 숫자로 된 정답을 글자로 바꿔줄 사전 준비
classes = {0:'setosa', 1:'versicolor', 2:'virginica'}

# 4. 새로운 데이터(듣도 보도 못한 꽃) 제시
# [꽃받침 길이, 꽃받침 너비, 꽃잎 길이, 꽃잎 너비] 순서의 데이터 2세트이다.
X = [[4, 2, 1.3, 0.4], # 첫 번째 꽃
     [4, 3, 3.2, 2.2]] # 두 번째 꽃

# 5. 새로운 데이터 예측하기 (추론)
# predict: 학습한 내용을 바탕으로 이 꽃들이 어떤 품종일지 AI가 답을 낸다.
y = knn.predict(X)

# 6. 결과 출력
# 첫 번째 꽃(X[0])의 예측값(y[0])을 이름표(classes)에서 찾아 출력한다.
print('{} 특성을 가지는 품종: {}'.format(X[0], classes[y[0]]))
print('{} 특성을 가지는 품종: {}'.format(X[1], classes[y[1]]))

# 7. 전체 데이터에 대한 정확도 확인
# AI에게 공부했던 데이터(iris.data)를 다시 다 풀어보게 한다.
y_pred_all = knn.predict(iris.data)

# accuracy_score: 실제 정답(iris.target)과 AI가 푼 답(y_pred_all)을 비교해서 점수를 낸다.
scores = metrics.accuracy_score(iris.target, y_pred_all)
print('n_neighbors가 {0:d}일때 정확도: {1:.3f}'.format(k, scores))

 

혼동 행렬(Confusion Matrix)  시각화

1. 표를 보는 법 (기본 구조)

보통 가로(행)는 실제 정답을 의미하고, 세로(열)는 AI가 예측한 값을 의미한다.

  예측: 세토사(0) 예측: 버시컬러(1) 예측: 버지니카(2)
실제: 세토사(0) 맞힌 개수 틀린 개수 틀린 개수
실제: 버시컬러(1) 틀린 개수 맞힌 개수 틀린 개수
실제: 버지니카(2) 틀린 개수 틀린 개수 맞힌 개수
  • 대각선 숫자: AI가 정답을 맞힌 경우이다. 이 숫자들이 클수록 공부를 잘한 것이다.
  • 나머지 숫자: AI가 엉뚱한 답으로 헷갈려한 경우이다.
import matplotlib.pyplot as plt
plt.hist2d(iris.target, y_pred_all, bins=(3,3), cmap=plt.cm.jet)

import matplotlib.pyplot as plt
plt.hist2d(iris.target, y_pred_all, bins=(3,3), cmap=plt.cm.gray)

from sklearn.metrics import confusion_matrix
conf_result = confusion_matrix(iris.target,y_pred_all)
print(np.flip(conf_result, axis=0)) # flip: 보통 수학 그래프(plt.hist2d 등)를 그릴 때 y축은 아래가 0이고 위로 갈수록 숫자가 커지기 때문에, 그래프와 표의 시각적인 방향을 맞추려고 사용

[[ 0  3 47]
 [ 0 47  3]
 [50  0  0]]

 

만약 conf_result가 아래처럼 나왔다면?

[[50,  0,  0],
 [ 0, 47,  3],
 [ 0,  2, 48]]
  1. 세토사(0번): 50마리 모두 정확히 맞힘. (완벽!)
  2. 버시컬러(1번): 47마리는 맞혔지만, 3마리는 버지니카(2번)라고 오답을 냄.
  3. 버지니카(2번): 48마리는 맞혔지만, 2마리는 버시컬러(1번)라고 오답을 냄.

결론: 이 AI는 세토사는 완벽히 구분하지만, 버시컬러와 버지니카는 서로 약간 헷갈려한다는 것을 알 수 있다.

 

 

knn 정확도 확인하기

과적합(Overfitting)이란? 학생이 문제집의 문제와 정답을 통째로 외워서, 정작 수능(새로운 데이터)에서는 낮은 점수를 받는 현상이다. 보통 k 값이 너무 작을 때 발생하기 쉽다.

from sklearn.datasets import load_iris
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# 1. 데이터 준비
iris = load_iris()

# 2. 데이터 쪼개기 (공부용 7 : 시험용 3)
# AI가 공부한 문제로 시험을 보면 당연히 100점을 맞음. 그건 실력이 아니라 '암기'이다. 한 번도 보지 못한 X_test로 시험을 봐야 AI의 **진짜 실력(일반화 성능)**을 알 수 있다.
# random_state=2021: 데이터를 섞을 때 사용하는 '랜덤 시드' 번호 -> 내일 실행해도, 누가 실행해도 똑같은 훈련/시험지가 만들어져서 실험 결과의 일관성
X_train, X_test, y_train, y_test = train_test_split(
    iris.data, iris.target, test_size=0.3, random_state=2021
)

# 3. 문제에서 요구한 k 값들 리스트 만들기
k_list = [1, 5, 10, 20, 30]

# 4. 반복문으로 하나씩 꺼내서 테스트 (핵심)
for k in k_list:
    # 선생님(모델) 모셔오기 (k명만 봐라)
    knn = KNeighborsClassifier(n_neighbors=k)

    # 공부하기 (학습 데이터로만)
    knn.fit(X_train, y_train)

    # 시험 보기 (시험 데이터로)
    y_pred = knn.predict(X_test)

    # 채점하기
    score = accuracy_score(y_test, y_pred)

    # 결과 출력 (문제에 나온 형식 그대로)
    print(f'n_neighbors가 {k}일 때 정확도: {score:.3f}')
k 값 성격 예상 정확도 분석
1 매우 예민 높음/보통 운 좋으면 잘 맞히지만, 이상한 데이터에 잘 속음
5, 10 적당함 가장 높음 데이터의 흐름을 가장 잘 파악한 상태 (Golden K)
20, 30 둔감함 낮아짐 이웃을 너무 많이 봐서 판단력이 흐려짐

 

===================== 게임=====================

군집화(Clustering)

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

# 1. 게임 이용자 데이터(이용 시간, 레벨)를 불러오기.
# 이 데이터에는 '정답(이 사람이 어떤 그룹인지)'이 없다.
# 오직 수치만 보고 비슷한 사람을 찾아야 한다.
game_usage = pd.read_csv('./game_usage.csv')
game_usage.head(3)

X = game_usage['time spent']
Y = game_usage['game level']

# 2. 산점도(Scatter plot) 그리기
plt.title("Game player data")
plt.xlabel('Time spent')
plt.ylabel('Game level')

# 3. plt.axis('equal')의 의미:
# 가로축의 1단위 길이와 세로축의 1단위 길이를 화면상에서 '똑같이' 보이게 맞춘다.
# 만약 이용 시간은 0~1000분이고 레벨은 0~10이라면,
# 이 옵션을 켰을 때 그래프가 옆으로 엄청나게 길어지거나 데이터가 한곳에 뭉쳐 보이게 된다.
# plt.axis('equal')

plt.scatter(X, Y)
plt.show()

 

K-Means(K-평균 군집화)

K-Means가 그룹을 결정하는 방식.

  1. 중심점 찍기: 처음에 k개의 점을 아무데나 찍는다.
  2. 영역 확장(동그라미): 각 중심점에서 동그라미를 점점 키운다고 상상해보자.
  3. 내 편 만들기: 어떤 데이터 점이 그 동그라미 안에 먼저 닿으면 "너는 내 팀이야!"라고 가져가는 식이다. (실제로는 모든 점과의 거리를 계산해서 가장 가까운 중심점 팀으로 들어간다.)
  4. 중심 이동: 팀원이 정해지면, 그 팀원들의 정중앙으로 중심점을 옮긴다.
  5. 무한 반복: 이 과정을 더 이상 중심점이 움직이지 않을 때까지 반복한다.
from sklearn import cluster

# 전처리란? AI에게 데이터를 주기 전에 AI가 오해하지 않도록 깔끔하게 다듬는 작업
# K-Means는 점들 사이의 **'거리'**를 재서 그룹을 묶음.
# 그런데 '시간'은 0~500이고 '레벨'은 0~10이면, AI는 숫자가 큰 '시간'의 차이만 엄청나게 중요하게 생각하고 '레벨'은 무시하게됨
# 시간과 레벨 모두 0~1 사이의 숫자로 똑같이 맞춰주는 작업(스케일링)이 바로 여기서 필요한 '전처리'

# K-Means로 그룹을 나누고 그래프를 그리는 함수 정의
def kmeans_predict_plot(data, k):
    # 1. 모델 설정
   # n_clusters=k: 데이터를 k개의 그룹으로 나눠라!
   # n_init=10: "제일 좋은 시작점"을 찾기 위해 초기 위치를 바꿔가며 10번 반복해라!
    # K-Means는 처음에 무작위로 기준점(중심점)을 찍고 시작함. 그런데 운 나쁘게 시작점이 이상한 데 찍히면 그룹이 엉망으로 나눠질 수 있음. 그래서 컴퓨터에게 **"시작점을 여기저기 바꿔가며 10번이나 해보고, 그중에서 가장 예쁘게 그룹이 잘 나뉜 결과 하나를 골라줘!"**라고 시키는 것
   model = cluster.KMeans(n_clusters=k, n_init=10)

    # 2. 학습(데이터를 보고 그룹의 중심점을 찾음)
   model.fit(data)

    # 3. 예측(각 데이터가 몇 번 그룹인지 0, 1, 2, 3 번호를 매김)
   labels = model.predict(data)

    # 4. 시각화 준비
   colors = np.array(['red', 'green', 'blue', 'magenta']) # 4가지 색상 준비
   plt.suptitle('k-Means clustering, k={}'.format(k))
   plt.xlabel('Time spent'), plt.ylabel('Game level')

    # 5. 그래프 그리기
    # data[:, 0]은 시간, data[:, 1]은 레벨
    # color=colors[labels]를 통해 AI가 분류한 그룹별로 색을 칠하기
   plt.scatter(data[:, 0], data[:, 1], color=colors[labels])

# 데이터를 하나로 묶고 함수 실행
gamer_data = np.column_stack((X, Y))
kmeans_predict_plot(gamer_data, k = 4)

가로(시간) 차이는 무시되고 세로(레벨)로만 뚝뚝 끊겨서 그룹이 나뉘어 보임

 

앞서 정의한 함수를 사용해서 데이터를 **딱 두 개의 그룹(k=2)**으로 나누는 실험

-> 세로축 숫자가 훨씬 커서 가로선으로 나뉨

# 1. k = 2일 때의 결과를 보기 위한 코드
from sklearn import cluster

# 2. X(시간)와 Y(레벨)를 합쳐서 [시간, 레벨] 형태의 2차원 리스트로 만듦
gamer_data = np.column_stack((X, Y))

def kmeans_predict_plot(X, k):
    # 3. 모델 설정: 그룹 2개(k=2), 초기 중심점 시도 10번
    model = cluster.KMeans(n_clusters=k, n_init=10)

    # 4. 학습: 데이터 사이의 거리를 계산해서 비슷한 것끼리 묶기
    model.fit(X)

    # 5. 예측: 각 점이 0번 그룹인지 1번 그룹인지 라벨을 붙임
    labels = model.predict(X)

    # 6. 색상 및 제목 설정
    colors = np.array(['red', 'green', 'blue', 'magenta'])
    plt.suptitle('k-Means clustering, k={}'.format(k))

    # 7. 그리기: 가로축은 시간(X[:, 0]), 세로축은 레벨(X[:, 1])
    plt.scatter(X[:, 0], X[:, 1], color=colors[labels])

kmeans_predict_plot(gamer_data, k = 2)

 

# 1. k=3으로 군집화하고 그래프를 그리기 (내부에서 plt.scatter가 실행됨)
kmeans_predict_plot(gamer_data, k = 3) # 3개의 군집 생성

# 모든 데이터를 다시 'red' 색상의 'x' 모양으로 그리기
plt.scatter(gamer_data[:, 0], gamer_data[:, 1], marker='x', s=100, c='red')

plt.show()

 

정규화(Normalization)란?

데이터의 범위를 똑같이 맞춰주는 작업입니다. 주로 Min-Max Scaling을 많이 쓴다.

  • 방법: 모든 데이터를 0과 1 사이로 압축한다.
    • 최솟값은 0이 되고, 최댓값은 1이 된다.
  • 효과: 이제 연봉도 0~1 사이, 몸무게도 0~1 사이가 된다. AI가 두 데이터를 공평하게 대하게 되어 학습 속도가 빨라지고 정확해진다.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler # 1. 스케일러 가져오기

# 데이터 로드
game_usage = pd.read_csv('./game_usage.csv')
data = game_usage[['time spent', 'game level']].values

# 2. 정규화 도구 만들기
scaler = MinMaxScaler()

# 3. fit과 transform 한꺼번에 하기 (훈련 데이터용)
# 여기서 '자(기준)'를 만들고 바로 데이터도 0~1로 바꾸기.
scaled_data = scaler.fit_transform(data)

# 4. 바뀐 데이터 확인
# 이제 scaled_data는 모두 0과 1 사이의 값이다.
print(scaled_data[:3])

# 5. 그래프로 확인 (모양은 같지만 축의 숫자가 0~1로 바뀜)
plt.scatter(scaled_data[:, 0], scaled_data[:, 1])
plt.title("Scaled Game Player Data")
plt.xlabel('Time spent (scaled)')
plt.ylabel('Game level (scaled)')
plt.show()

단계 함수 하는 일 비유
1. 기준 세우기 fit() 데이터의 최솟값, 최댓값 파악 **표준 자(Ruler)**를 제작함
2. 변환하기 transform() 실제 데이터를 0~1 사이로 바꿈 제작한 자로 길이를 재서 기록함
3. 한꺼번에 fit_transform() 기준 세우기와 변환을 동시에 함 훈련 데이터에서만 사용 권장

 

min max scaler

마지막 줄에서 계산한 평균값은 AI 학습의 **'중심점'**이 어디에 있는지를 알려준다.

  • 결과값이 0.5에 가깝다면: 데이터들이 전체 범위에서 중간쯤에 골고루 모여 있다는 뜻이다.
  • 결과값이 0.1처럼 작다면: 대부분의 유저가 낮은 수치(시간 적음, 레벨 낮음)에 쏠려 있다는 뜻이다.
  • 결과값이 0.9처럼 크다면: 헤비 유저(시간 많음, 레벨 높음)들이 많다는 뜻이다.

이렇게 0~1 사이의 평균을 보면 **"우리 데이터가 어느 쪽으로 치우쳐 있구나!"**를 직관적으로 알 수 있다.

from sklearn.preprocessing import MinMaxScaler

# 1. 스케일러 도구 준비
scaler = MinMaxScaler()

# 2. 기준(자) 만들기 (fit)
# gamer_data에서 '시간'의 최댓값/최솟값, '레벨'의 최댓값/최솟값을 찾아낸다.
scaler.fit(gamer_data)

# 3. 데이터 변환하기 (transform)
# 위에서 찾은 기준을 바탕으로 모든 데이터를 0~1 사이로 바꾼다.
# (예: 1000분 -> 1.0 / 0분 -> 0.0)
n_data = scaler.transform(gamer_data)

# 4. 평균 확인하기
# n_data[:, 0].mean() : 정규화된 '이용 시간'의 평균값
# n_data[:, 1].mean() : 정규화된 '게임 레벨'의 평균값
n_data[:, 0].mean(), n_data[:, 1].mean()

 

MinMaxScaler 결과 확인

n_data[:, 0].min(), n_data[:, 0].max()
n_data[:, 1].min(), n_data[:, 1].max()

 

4개의 군집 생성

kmeans_predict_plot(n_data, k = 4) # 4개의 군집 생성

 

 

 

 

StandardScaler (표준화)

예를 들어, 한국인의 평균 키가 155cm라고 가정한다.

  1. 평균 이동: 모든 사람의 키에서 155를 빼버린다.
    • 키가 155인 사람은 0이 된다.
    • 키가 160인 사람은 +5가 된다.
    • 키가 150인 사람은 -5가 된다.
    • 이렇게 하면 데이터의 중심이 딱 0에 오게 된다.
  2. 크기 조절: 그 상태에서 **표준편차**로 나눠준다.
    • 이렇게 하면 데이터의 퍼진 정도(표준편차)가 딱 1이 된다.
from sklearn.preprocessing import StandardScaler

standardScaler = StandardScaler()
standardScaler.fit(gamer_data)
gamer_data_scaled = standardScaler.transform(gamer_data)

gamer_data_scaled[:, 0].std(), gamer_data_scaled[:, 1].std()

 

StandardScaler 코드 결과

 

  • 왜 평균이 딱 0이 아닐까?: 컴퓨터가 소수점을 계산할 때 아주 미세한 오차가 발생해서 1.42e-16($0.000000000000000142$) 같은 숫자가 나온다. 사실상 0이라고 보면 된다.
  • 왜 1.0이 나오나요?: 모든 데이터의 흩어진 정도를 1이라는 기준으로 통일했기 때문이다.

 

# 평균 확인
gamer_data_scaled[:, 0].mean() # 결과: 0에 아주 가까운 숫자 (예: 1.42e-16)
# 표준편차 확인
gamer_data_scaled[:, 0].std()  # 결과: 1.0

 

합계, 평균, 분산, 표준편차

import numpy as np

a = np.array([10, 8, 10, 8, 8, 4])
print('합계 :', a.sum())
print('평균 :', a.mean())
print('분산 :', a.var())
print('표준편차 :', a.std())

 

구분 MinMaxScaler (정규화) StandardScaler (표준화)
범위 무조건 0 ~ 1 제한 없음 (보통 -3 ~ 3 사이)
장점 이미지 처리처럼 범위가 명확할 때 좋음 **이상치(갑자기 큰 값)**에 강함
단점 한 명이 키가 3m라면 나머지는 다 0 근처로 몰림 데이터가 0과 1 사이로 예쁘게 안 담김

꿀팁: 데이터에 갑자기 튀는 값(이상치)이 많다면 **StandardScaler**가 더 안전하고, 데이터의 범위를 딱 맞춰야 한다면 **MinMaxScaler**가 유리하다.

 

===================== 유방암 데이터=====================

유방암 데이터의 특징

특징(Feature) 수치 범위 (예시) 비유
세포의 넓이(Area) 약 143 ~ 2501 거대한 고래
세포의 매끄러움(Smoothness) 약 0.05 ~ 0.16 아주 작은 새우

 

k-NN은 점들 사이의 **'거리'**를 재서 이웃을 정한다.

  • 세포 넓이는 숫자가 100~1000 단위로 왔다 갔다 한다.
  • 매끄러움은 0.01 단위로 아주 미세하게 바뀐다.

컴퓨터 입장에서는 넓이가 10만큼 변하는 것매끄러움이 0.1 변하는 것보다 훨씬 큰 변화라고 생각한다. 실제로는 '매끄러움'이 암 진단에 더 중요할 수도 있다. 결국 체급(숫자)이 큰 데이터가 투표권을 독점하게 된다.


스케일링을 하면 무엇이 달라질까?

방금 보신 결과(0.92 내외)도 나쁘진 않지만, **StandardScaler**나 **MinMaxScaler**를 적용하면 모든 특징이 공평하게 투표권을 갖게 된다.

  • 전처리 전: "넓이" 데이터만 보고 정답을 찍는 편향된 AI
  • 전처리 후: 30개 특징(넓이, 질감, 반지름 등)을 골고루 참고하는 똑똑한 AI
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_breast_cancer

# 1. 유방암 데이터세트 읽어오기
# 유방암 세포의 특징(반지름, 질감, 넓이 등) 30개와
# 양성(0)/악성(1) 정답이 들어있다.
cancer = load_breast_cancer()

# 2. 데이터 쪼개기
# 훈련용(Train)과 테스트용(Test)으로 나눈다.
# random_state=0은 실행할 때마다 결과가 똑같이 나오게 고정하는 번호이다.
X_train, X_test, y_train, y_test = train_test_split(
    cancer.data, cancer.target, random_state=0
)

# 3. 데이터의 크기(Shape) 확인
# 훈련 데이터는 426마디(행), 특징은 30개라는 뜻이다.
print(X_train.shape, X_test.shape, y_train.shape, y_test.shape)

# 4. k-NN 모델 생성 (k=3)
knn = KNeighborsClassifier(n_neighbors=3)

# 5. 모델 학습 (공부시키기)
knn.fit(X_train, y_train)

# 6. 정확도 측정
score = knn.score(X_test, y_test)
print(f"스케일러 사용하지 않은 k-NN 정확도: {score:.2f}")

 

유방암 데이터에 **정규화(Normalization)**를 적용

① scaler.fit(X_train) : "눈금 확인하기 (자 만들기)"

  • 하는 일: X_train 데이터를 훑어보면서 각 특징(30개)의 **최솟값(x_min)**과 **최댓값(x_max)**이 얼마인지 메모한다.
  • 비유: "아, 이 데이터는 0부터 2500까지 있구나. 그럼 0을 0으로, 2500을 1로 잡는 **자(Ruler)**를 만들어야지!"라고 계획을 세우는 단계이다. (아직 데이터는 안 바뀌었습니다.)

② scaler.transform(X_train) : "실제로 변환하기 (길이 재기)"

  • 하는 일: 위에서 만든 '자'를 갖다 대고, 실제 데이터 숫자들을 0~1 사이의 숫자로 확 바꾼다.
  • 비유: "자, 아까 만든 자로 재보니까 1250은 딱 중간인 0.5네! 너는 이제 0.5라고 써라." 하고 값을 바꾸는 과정이다.

③ scaler.transform(X_test) : "같은 자로 시험 보기"

  • 중요: 테스트 데이터에는 fit을 하지 않는다! 훈련 데이터로 만든 자를 그대로 가져가서 쓰기만 한다.
  • 그래야 AI가 공부할 때의 1.0(최대치)과 시험 볼 때의 1.0(최대치)이 같은 의미를 갖게 된다.
from sklearn.preprocessing import MinMaxScaler

# 1. 정규화 도구(스케일러) 가져오기
scaler = MinMaxScaler()

# 2. 훈련 데이터 훑어보기 (fit)
# X_train의 최소/최대값을 찾아 "변환 공식"을 만든다.
scaler.fit(X_train)

# 3. 데이터 변환하기 (transform)
# 훈련 데이터와 테스트 데이터를 모두 0~1 사이로 바꾼다.
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)

# 4. 조정된 데이터로 k-NN 학습
# 이제 "세포 넓이"나 "매끄러움"이 모두 0~1 사이이므로 공평하게 학습한다.
knn.fit(X_train_scaled, y_train)

# 5. 정확도 확인
score = knn.score(X_test_scaled, y_test)
print(f"최대,최소값 스케일러 사용 후 k-NN 정확도: {score:.2f}")

이전에는 숫자가 큰 '세포 넓이' 특징이 거리를 계산할 때 압도적인 영향력을 행사해서 다른 중요한 미세한 특징들을 무시했. 하지만 스케일링 후에는 30개의 모든 특징이 동등한 힘을 갖게 되어, AI가 훨씬 더 정교하게 암 여부를 판단할 수 있게 된 것이다.

 

**표준 스케일러(StandardScaler)**

 

  • 평균 이동: 모든 데이터에서 평균값을 빼서 중심을 0에 먖춘다.
  • 크기 조절: 데이터를 표준편차로 나눠서 데이터가 퍼진 정도를 1로 맞춘다.
  • 결과: 대부분의 데이터가 -3에서 3 사이의 값으로 모이게 된다.

머신러닝에서는 보통 StandardScaler를 먼저 시도하는 것이 일반적이다. 데이터에 특이한 값(이상치)이 섞여 있을 때 MinMaxScaler보다 훨씬 더 안정적인 성능을 보여주기 때문이다.

 

# 1. 표준 스케일러(Standardization) 도구 가져오기
from sklearn.preprocessing import StandardScaler

# 2. 스케일러 생성 및 기준 학습(fit)
scaler = StandardScaler()
# 훈련 데이터에서 각 특징의 평균(mean)과 표준편차(std)를 계산.
scaler.fit(X_train)

# 3. 데이터 변환(transform)
# (데이터 - 평균) / 표준편차 공식을 사용하여 모든 특징을 표준화한다.
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)

# 4. 표준화된 데이터로 k-NN 모델 학습
# 이제 모든 특징이 평균 0을 중심으로 비슷한 폭(표준편차 1)으로 퍼져 있다.
knn.fit(X_train_scaled, y_train)

# 5. 모델 평가
score = knn.score(X_test_scaled, y_test)
print(f"표준 스케일러 사용 후 k-NN 정확도: {score:.2f}")

 

===================== 붓꽃 (k-means) =====================

붓꽃(Iris) 데이터를 이용해 **군집화(Clustering)**를 준비

KMeans로 정답 없이 스스로 그룹을 나누는 과정을 시뮬레이션

from sklearn.datasets import load_iris
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

# 1. 붓꽃 데이터 적재
iris = load_iris()

# 2. 특징 데이터(X) 출력
# 꽃받침(sepal)과 꽃잎(petal)의 길이/너비 정보가 담긴 150개의 행렬이다.
print(iris.data)

# 3. 데이터프레임 생성
# 넘파이 배열 형태의 데이터를 보기 편하게 표(DataFrame)로 변환한다.
all_features = ['sepal length', 'sepal width', 'petal length', 'petal width']
iris_df = pd.DataFrame(iris.data, columns=all_features)

# 4. 상단 데이터 확인
# 컬럼명이 붙은 예쁜 표 형태로 상위 5개 행을 보여준다.
print(iris_df.head())

# 5. 정답표(Label) 확인
# 사실 군집화(KMeans)에서는 이 정답을 사용하지 않고 학습한다.
# 나중에 AI가 나눈 그룹이 실제 정답과 얼마나 비슷한지 '비교'할 때만 쓴다.
print(iris.target)

 

분석에 사용할 **특징(Feature)**들을 최종적으로 선택

① 2개만 사용할 때 (petal length, petal width)

  • 장점: plt.scatter를 이용해 2차원 평면에 즉시 시각화가 가능하다. 눈으로 그룹이 잘 나뉘었는지 확인하기 가장 좋다.
  • 이유: 붓꽃 데이터에서는 꽃받침보다 꽃잎의 크기가 품종을 구분하는 데 훨씬 결정적인 힌트가 되기 때문에 2개만 써도 결과가 꽤 잘 나온다.

② 4개 모두 사용할 때 (all_features)

  • 장점: AI가 꽃받침의 미세한 차이까지 학습하므로 분류 정확도가 더 높아질 수 있다.
  • 단점: 4차원 데이터를 우리 눈으로 직접 볼 수 없어서, 그래프를 그리려면 차원 축소(PCA) 같은 추가 기술이 필요하다.
# 1. 사용할 특징 선택
# 주석처럼 2개만 선택하면 2차원 그래프를 그리기 매우 편해진다.
# feature_names = ['petal length', 'petal width']
# feature = iris_df[feature_names]

# 2. 현재 선택: 4개 전체 특징 사용
# 꽃받침(Sepal)과 꽃잎(Petal)의 정보를 모두 사용하므로 AI가 더 풍부한 정보를 얻는다.
feature = iris_df[all_features]

# 3. 앞부분 3개 데이터 확인
feature.head(3)

 

AI에게 **"이 150개의 꽃 데이터를 보고 비슷한 것끼리 3그룹으로 묶어봐"**라고 명령

출력된 150개의 숫자(0, 1, 2)는 AI가 스스로 분류한 그룹 번호이다.

  • 숫자의 의미: 여기서 0번이 꼭 '세토사'를 의미하는 것은 아니다. AI는 그저 "이 데이터들은 서로 비슷하니까 0번 그룹으로 묶을게"라고 판단한 것뿐이다.
  • 실제 정답과의 비교: iris.target(실제 정답)과 km_model.labels_(AI 예측)를 나란히 놓고 보면, AI가 얼마나 정확하게 그룹을 찾아냈는지 알 수 있다.
# 1. KMeans 모델 생성
# n_clusters=3: 실제 붓꽃 품종이 3종류이므로 3개의 그룹으로 나누라고 설정한다.
# n_init=10: 최적의 중심점을 찾기 위해 무작위 시작점을 바꿔가며 10번 반복 실험한다.
km_model = KMeans(n_clusters=3, n_init=10)

# 2. 모델 학습 (군집화 실행)
# '정답' 없이 오직 특징(feature) 데이터들의 거리만 계산하여 그룹을 나눈다.
km_model.fit(feature)

# 3. 결과 확인 (라벨 출력)
# 150개의 데이터 각각이 어떤 그룹(0, 1, 2)에 속하게 되었는지 번호를 보여준다.
print(km_model.labels_)

 

**'중심점(Centroids)'**과 '성능 평가' 단계

1. 세 군집의 중심 좌표 (cluster_centers_)

km_model.cluster_centers_는 AI가 데이터를 묶은 뒤, 그 그룹의 한가운데(평균치)를 계산한 결과

print('세 군집의 중심 좌표')
print('첫 번째 군집:', km_model.cluster_centers_[0]) # feature가 2개면 2개, 4개면 4개나옴
print('두 번째 군집:', km_model.cluster_centers_[1])
print('세 번째 군집:', km_model.cluster_centers_[2])
군집 특징 1 (Sepal L) 특징 2 (Sepal W) 특징 3 (Petal L) 특징 4 (Petal W)
중심 0 5.006 3.428 1.462 0.246
중심 1 5.901 2.748 4.393 1.433
중심 2 6.850 3.073 5.742 2.071

 

accuracy_score(km_model.labels_, iris.target)를 통해 정확도를 확인했는데, 여기에는 **'라벨 뒤바뀜(Label Mismatch)'**이라는 함정이 있을 수 있다.

  1. AI는 이름표를 모름: AI는 "비슷한 것끼리 묶어보니 0번 그룹이야"라고 했을 뿐, 그게 실제 '세토사(0)'인지 '버지니카(2)'인지 모른다.
  2. 번호가 다를 수 있음: 실제 정답에서 '세토사'가 0번인데, AI는 세토사 그룹을 찾은 뒤 우연히 **'1번'**이라는 이름을 붙일 수 있다.
  3. 결과: 그룹은 완벽하게 나눴음에도 불구하고, 번호가 서로 매칭되지 않아 정확도가 0으로 나올 수도 있다.

해결 팁: 따라서 군집화에서는 단순히 정확도 점수만 보기보다는, **혼동 행렬(Confusion Matrix)**을 그려서 "어떤 그룹이 어떤 정답과 매칭되는지" 먼저 확인하는 것이 정석이다

from sklearn.metrics import accuracy_score
accuracy_score(km_model.labels_, iris.target)

 

데이터 분석의 **'정답지(Ground Truth)'**를 확인

1. groupby('target').mean()이 하는 일

  • 그룹화: 전체 150개의 데이터를 정답(0, 1, 2) 별로 50개씩 묶는다.
  • 평균 계산: 각 그룹 내에서 4가지 특징(꽃받침/꽃잎의 길이와 너비)의 평균값을 구한다.

2. 결과 표 해석 (데이터의 특징)

종 (Target) 이름 특징 요약
0 세토사 (Setosa) 모든 수치(특히 꽃잎)가 매우 작다. 다른 종과 확연히 구분된다.
1 버시컬러 (Versicolor) 수치가 중간 정도이다.
2 버지니카 (Virginica) 꽃잎의 길이와 너비가 가장 크다.
features = ['petal length', 'petal width']
# iris_data에서 세 번째, 네 번째 열을 추출함
# iris_df = pd.DataFrame(iris.data[:, [2, 3]], columns=features)
iris_df = pd.DataFrame(iris.data[:, :], columns=all_features)
iris_df['target'] = pd.Series(iris.target)
print('종에 따라 달라지는 꽃잎 길이와 너비의 평균값')
print(iris_df.groupby('target').mean())

 

 

AI가 드디어 150송이의 붓꽃들에게 **"너희는 몇 번 그룹이야"**라고 최종 명찰을 달아줌

AI가 학습한 내용을 바탕으로 실제 예측 결과(0, 1, 2)를 뽑아내고, 이를 다시 사람이 보기 편한 데이터프레임(표) 형태로 정리

 

predict 결과에서 꼭 알아야 할 점

  • 방 번호는 무작위이다: AI가 예측한 0번 그룹이 실제 데이터의 0번(Setosa)과 다를 수 있습니다. AI는 "이 꽃들은 비슷해!"라고 묶었을 뿐, 품종 이름은 모르기 때문이다.
  • 일관성: 만약 스케일링을 하지 않았다면, sepal length 값이 큰 쪽을 기준으로 그룹이 나뉘었을 확률이 높다.
  • 정답과의 합체: 현재 iris_df에는 실제 정답(target)이 있고, predict에는 AI의 답(cluster)이 있습니다. 이 둘을 합쳐서 비교해보면 AI가 얼마나 똑똑한지 바로 알 수 있다.
# feature_names = ['petal length', 'petal width']
# feature = iris_df[feature_names]

# 1. 사용할 특징 선택 (4개 전체 사용)
feature = iris_df[all_features]

# 2. KMeans 모델 설정 및 학습
# 3개의 군집으로 나누고, 최적의 시작점을 찾기 위해 10번 시도한다.
km_model = KMeans(n_clusters=3, n_init=10)
km_model.fit(feature)

# 3. 예측(Predict) 실행
# 학습한 '중심점'을 기준으로 150개의 데이터가 각각 어느 그룹에 속하는지 결정한다.
# km_model.labels_와 결과는 같지만, '새로운 데이터'가 들어왔을 때도 이 명령어를 쓴다.
pred = km_model.predict(feature)

# 4. 예측 결과를 표(DataFrame)로 변환
# 숫자 배열이었던 결과를 'cluster'라는 이름의 열을 가진 표로 만든다.
predict = pd.DataFrame(pred, columns=['cluster'])

# 5. 상위 5개 결과 출력
# 0번 데이터부터 4번 데이터까지 AI가 분류한 방 번호(0, 1, 2)를 보여준다.
print(predict.head())

 

데이터 분석의 마지막 단계인 시각화를 통해 AI의 분류 결과가 실제 데이터 공간에서 어떻게 자리 잡았는지 확인하는 과정

1. "초기값 설정"의 중요성 (K-Means의 단점)

작성하신 주석처럼 K-Means는 처음에 중심점을 어디에 찍느냐에 따라 결과가 완전히 달라질 수 있는 '운' 요소가 작용한다.

  • 지역 최적값(Local Optima): 운 나쁘게 시작점이 이상한 곳에 찍히면, AI가 최선의 그룹을 찾지 못하고 엉뚱한 곳에서 멈춰버린다.
  • 해결책: 이를 보완하기 위해 우리는 모델 설정 시 n_init=10을 사용했다. 이는 "서로 다른 시작점으로 10번 해보고 가장 점수가 좋은 걸 골라줘"라는 뜻이다.

2. 시각화의 특징 (4차원을 2차원으로)

우리는 **4개(Sepal L/W, Petal L/W)**의 특징을 모두 사용해 AI를 학습시켰다. 하지만 그래프는 화면의 한계상 **2개(Petal L/W)**의 축으로만 그렸다.

  • 그럼에도 불구하고 그룹이 아주 잘 나뉘어 보인다면, 꽃잎(Petal) 정보가 붓꽃 품종을 나누는 데 그만큼 결정적인 역할을 했다는 증거이다.

3. 파란색 'X' 마커의 의미

그래프 중앙의 파란색 'X'는 AI가 판단한 각 그룹의 무게중심이다.

  • 모든 데이터 점들은 가장 가까운 'X'와 한 팀이 된다.
  • 만약 어떤 점이 두 'X' 사이의 경계에 있다면, AI는 그 점을 어느 그룹으로 넣을지 가장 많이 고민했을 것이다.
# 1. 원본 특징 데이터와 AI의 예측 결과(cluster)를 옆으로 합친다.
# axis=1은 컬럼(열) 방향으로 붙이라는 뜻이다.
df = pd.concat([feature, predict], axis = 1)
print(df.head())

# 2. AI가 찾은 3개 그룹의 중심점 좌표를 표(DataFrame)로 만든다.
# 4개의 특징을 모두 사용했으므로 컬럼명도 4개 전체를 넣어준다.
centers = pd.DataFrame(km_model.cluster_centers_, columns=all_features)
print(centers.head())

# 3. 시각화를 위해 필요한 도구와 데이터를 준비한다.
import matplotlib.colors as mcolors

# 그래프의 x축은 꽃잎 길이, y축은 꽃잎 너비로 정한다. (가장 구분이 잘 되는 조합)
center_x = centers['petal length']  # 클러스터 중심의 x좌표
center_y = centers['petal width']   # 클러스터 중심의 y좌표
colors = ['orange', 'green', 'red'] # 각 그룹을 칠할 색상 리스트

# 4. 산점도(Scatter Plot) 그리기
# c=df['cluster']: AI가 분류한 번호에 따라 색상을 다르게 칠한다.
# cmap: 위에서 정한 orange, green, red 색상을 순서대로 사용한다.
plt.scatter(df['petal length'], df['petal width'],
           c=df['cluster'], cmap = mcolors.ListedColormap(colors))

# 5. 중심점 표시하기
# marker='X', c='b': 각 그룹의 정중앙(중심점)을 파란색(b) 커다란 'X'로 표시한다.
plt.scatter(center_x, center_y, marker='X', c='b', s=200) # s는 마커 크기
plt.title("K-Means Clustering: Petal Length vs Width")
plt.xlabel("Petal Length")
plt.ylabel("Petal Width")
plt.show()

 

===================== 서포트 벡터 머신 (SVM) =====================

 

  • 스케일링은 필수다: (Pipeline을 쓰면 실수 없이 편하게 할 수 있다.)
  • 비선형일 때는 'kernel'을 바꾼다: (linear 대신 'rbf' 같은 걸 쓰면 복잡한 분류도 가능해진다.)
  • C값으로 조절한다: (성능이 안 나오면 C값을 키우거나 줄여보며 실험한다.)

 

데이터 전처리

💡 용어 정리

  • 특성(Feature, X): 우리가 인공지능에게 줄 **'힌트'**이다. 여기서는 개의 '길이'와 '높이'가 된다.
  • 레이블(Label, y): 우리가 맞혀야 할 **'정답'**이다. 0(닥스훈트)과 1(진돗개)로 구분한다.

💡 왜 굳이 0과 1로 바꾸나요?

컴퓨터는 '닥스훈트'라는 글자보다 '0'이라는 숫자를 훨씬 계산하기 좋아한다. 그래서 사람이 쓰는 단어를 숫자로 바꿔주는 과정이 꼭 필요하다.

💡 SVM과의 연결고리

지난번에 질문하신 **SVM(서포트 벡터 머신)**은 이제 이 Xy 데이터를 보고 이렇게 생각할 것이다.

"음, 0번(닥스훈트) 애들은 주로 아래쪽에 모여 있고, 1번(진돗개) 애들은 위쪽에 있네? 이 둘 사이를 가르는 **가장 넓은 길(마진)**을 어디에 만들면 좋을까?"

import numpy as np # 숫자 계산과 데이터 정리를 아주 잘하는 '넘파이' 도서관을 가져온다.

# 1. 원본 데이터 입력 (길이와 높이)
# 닥스훈트 8마리의 길이와 높이 테이터이다. (길쭉하고 키가 작음.)
dach_length = [55, 57, 64, 63, 58, 49, 54, 61]
dach_height = [30, 31, 36, 30, 33, 25, 37, 34]

# 진돗개 8마리의 길이와 높이 데이터이다. (길이와 높이가 비슷비슷.)
jin_length = [56, 47, 56, 46, 49, 53, 52, 48]
jin_height = [52, 52, 50, 53, 50, 53, 49, 54]

# 2. 데이터 짝짓기 (column_stack)
# [길이, 높이] 형태로 한 마리씩 짝을 지어준다.
# d는 닥스훈트 8마리의 [길이, 높이] 목록이 된다.
d = np.column_stack((dach_length, dach_height))
# j는 진돗개 8마리의 [길이, 높이] 목록이 된다.
j = np.column_stack((jin_length, jin_height))

# 3. 데이터 합치기 (concatenate)
# 인공지능에게 공부시킬 때는 "이건 닥스훈트고 저건 진돗개야"라고 한꺼번에 줘야 한다.
# X에는 닥스훈트 8마리와 진돗개 8마리, 총 16마리의 데이터가 들어간다.
X = np.concatenate((d, j))

# 4. 정답지 만들기 (Label)
# 인공지능에게 "앞의 8개는 0번(닥스훈트), 뒤의 8개는 1번(진돗개)이야"라고 정답을 알려준다.
# [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1] 이런 리스트가 만들어진다.
y = [0]*len(d) + [1]*len(j)

# 5. 결과 확인
print('dogs :', X)    # 16마리의 [길이, 높이] 데이터 출력
print('labels :', y)  # 16마리의 정답(0 또는 1) 출력

 

 

SVM 파이프라인(학습) - **"강아지의 길이와 높이 데이터를 가져와서, 단위(cm)를 예쁘게 맞춘 뒤(Scaler), 경첩 모양의 벌점 계산법(Hinge Loss)을 써서 닥스훈트와 진돗개를 가르는 최적의 직선을 찾아라!"**라는 뜻

① 왜 MinMaxScaler 대신 StandardScaler를 쓸까?

  • MinMaxScaler: 데이터를 무조건 0과 1 사이로 구겨 넣는다. 만약 데이터에 키가 3미터인 돌연변이 강아지가 한 마리 섞여 있다면, 나머지 정상 강아지들은 0.001 근처에 다닥다닥 붙게 되어 구분이 안 된다.
  • StandardScaler: 데이터의 **'분포'**를 유지하면서 중심만 0으로 맞춘다. SVM은 데이터가 흩어져 있는 '모양'과 '거리'가 중요하기 때문에, 이상치(돌연변이)에 조금 더 강한 StandardScaler를 관례적으로 훨씬 많이 사용한다.

② loss='hinge' (경첩)가 정말 문 경첩일까?

맞다. 모양이 정말 문에 달린 경첩처럼 생겨서 붙은 이름이다.

  • 원리: 정답에서 멀리 떨어져 있으면 벌금(Loss)을 크게 때리고, 정답 근처나 안전지대에 들어오면 벌금을 0으로 만든다.
  • 사용자님 생각: "꺾어서 짤라내겠다" → 정확함! 그래프가 특정 지점에서 'V'자 형태로 꺾이기 때문에, 일정 수준 이상의 정확도에 도달하면 더 이상 신경 쓰지 않고 오직 '경계선 근처의 아슬아슬한 애들(서포트 벡터)'에만 집중하겠다는 뜻이다.

③ Loss(손실)는 '임계값'일까?

약간 다르다.

  • 임계값(Threshold): "이 선을 넘으면 진돗개다!"라고 정하는 기준점.
  • 손실(Loss): "모델이 얼마나 틀렸나?"를 측정하는 벌점 점수.
  • 인공지능은 학습할 때 이 Loss(벌점)를 최소로 줄이는 방향으로 기준점(임계값)을 계속 수정해 나가는 과정을 거친다.
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import LinearSVC

# svm이라는 이름의 '자동화 공정(Pipeline)'을 만든다.
svm = Pipeline([
    # [Step 1] 데이터 표준화 (StandardScaler)
    # 데이터를 평균 0, 표준편차 1인 표준 정규분포 형태로 만든다.
    # SVM은 '거리'를 재는 모델이라 이 과정이 없으면 키가 큰 강아지가 무조건 이겨버림
    ('scaler', StandardScaler()),

    # [Step 2] 선형 SVM 모델 설정
    # C=1: 모델의 '엄격함' 정도이다. 숫자가 작을수록 오차를 좀 더 너그럽게 봐준다.
    # loss='hinge': 아래에서 설명할 '경첩 손실' 함수를 사용한다. (SVM의 표준 계산법)
    # dual=True: 수학적 최적화 방식인데, 지금처럼 데이터(개 마릿수)가 적을 때 유리하다.
    ('linearSVC', LinearSVC(C=1, loss='hinge', dual=True))
])

# 3. 학습 시작
# 준비한 16마리의 개 데이터(X)와 정답(y)을 넣어서
# "이 길이는 닥스훈트고, 저 길이는 진돗개구나!"라고 규칙을 찾게 한다.
svm.fit(X, y)

 

지금 svm.predict([data1])을 실행하면, 우리가 앞에서 만든 파이프라인이 두 가지 일을 순식간에 처리한다.

  1. 자동 스케일링: 새 데이터 [59, 35]를 아까 학습할 때 썼던 기준(StandardScaler)에 맞춰서 크기를 조정한다. (공평한 비교를 위해)
  2. 선형 분류: 조정된 데이터를 보고 "아, 얘는 닥스훈트 쪽이네!"라고 판정한다.

데이터의 직관적인 분석

  • data1 [59, 35]: 몸길이는 긴데(59) 키는 작음(35). → 닥스훈트 확률이 높다.
  • data2 [53, 54]: 몸길이(53)와 키(54)가 거의 비슷함. → 진돗개 확률이 높다.
# 0은 닥스훈트, 1은 진돗개라고 이름을 붙여준다. (컴퓨터의 숫자를 사람의 언어로 통역)
dog_classes = {0:'Dachshund', 1:'Jindo dog'}

# 새로운 강아지 두 마리의 데이터이다.
data1, data2 = [59, 35], [53, 54]

# 1. svm.predict([data1])
# 왜 [[59, 35]] 처럼 2차원으로 줄까?
# 인공지능 모델은 보통 한 번에 '여러 마리'를 예측하도록 설계되어 있다.
# 그래서 "강아지 1마리 데이터(1차원)"가 아니라, "강아지 목록(2차원)"을 통째로 넘겨줘야 한다.
# 비유하자면, 시험지 한 장(데이터)이 아니라 시험지 뭉치(데이터 목록)를 받는 게 규칙이다.
y_pred = svm.predict([data1])

# 결과 출력: y_pred[0]은 예측된 숫자(0 또는 1)이고, 이걸 dog_classes에서 찾아서 이름을 보여준다.
print('데이터 :', data1, ', 판정 결과:', dog_classes[y_pred[0]])

# 두 번째 강아지 데이터도 같은 방식으로 예측한다.
y_pred = svm.predict([data2])
print('데이터 :', data2, ', 판정 결과:', dog_classes[y_pred[0]])

# # 두 마리를 동시에 목록으로 만들어서 던져주기. (2차원 리스트)
# results = svm.predict([data1, data2])
#
# # 결과는 [0, 1] 이런 식으로 리스트 형태로 돌아옴.
# print([dog_classes[res] for res in results])
# # 출력: ['Dachshund', 'Jindo dog']

 

데이터를 시각화

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

# 1. CSV 파일 불러오기
# 'two_classes.csv'라는 엑셀 같은 파일을 읽어오기.
df = pd.read_csv('./two_classes.csv')

# 데이터의 가장 마지막 3줄을 출력해서 모양을 확인.
print(df.tail(3))

# 2. 데이터 끼리끼리 분류하기 (필터링)
# 정답(y)이 1인 데이터(양성)만 골라내기.
df_positive = df[df['y']==1]
# 정답(y)이 0인 데이터(음성)만 골라내기.
df_negative = df[df['y']==0]

# 3. 산점도(Scatter plot) 그리기
# x1축과 x2축을 기준으로 점을 찍기.
# 1인 데이터는 빨간색(r), 0인 데이터는 초록색(g)으로 표시.
plt.scatter(df_positive['x1'], df_positive['x2'], color='r')
plt.scatter(df_negative['x1'], df_negative['x2'], color='g')

# 화면에 그래프를 띄우기.
plt.show()

# "겹쳐있는 애가 상당히 많음 -> 선형으로 했으면 어려웠을 것"

 

실제 데이터프레임에서 데이터를 추출하여 전처리하고, 선형 SVM 모델을 학습시키는 완성된 형태의 코드

① dual=True에서 왜 에러가 났을까?

보통 LinearSVC에서 loss='hinge'를 사용할 때는 dual=True가 필수다. 하지만 에러가 났다면 다음 두 가지 이유일 확률이 높다:

  1. 데이터의 양: 지금 데이터는 1,000개이다. 데이터 개수(n_samples)가 특성 개수(n_features, 여기서는 2개)보다 훨씬 많을 때는 dual=False가 수치적으로 더 안정적이다. 하지만 hinge 손실은 dual=False를 지원하지 않는 경우가 많아 최신 버전의 사이킷런에서는 dual='auto'로 두어 시스템이 알아서 판단하게 하는 것이 가장 안전하다.
  2. 수렴 문제: 데이터가 너무 많이 겹쳐 있으면 직선을 찾으려다 계산이 안 끝날 수 있다. 이때 시스템이 "해결이 안 된다!"라며 에러를 낼 수 있는데, 이는 겹침(비선형성) 때문일 가능성이 크다.

② loss='hinge' (경첩)는 무엇일까?

  • 경첩(Hinge) 모양: 그래프를 그리면 정답 근처에서는 0점이다가, 경계선을 침범하는 순간 직선으로 팍! 꺾여서 벌점이 올라가는 모양이다.
  • 이 방식은 **"안전한 거리(마진)를 확보한 데이터는 신경 쓰지 말고, 경계선 근처의 아슬아슬한 데이터(서포트 벡터)에만 집중해서 벌금을 매기겠다"**는 SVM의 철학을 담고 있다.

③ 마진(Margin)과 임계값(Threshold)의 차이

  • 임계값(Threshold): "이 선을 기준으로 0과 1을 나누겠다"는 딱딱한 경계선 그 자체이다.
  • 마진(Margin): 경계선에서 가장 가까운 데이터(서포트 벡터) 사이의 **'도로 폭'**이다. SVM의 목표는 이 도로 폭을 최대한 넓게(Maximum Margin) 만드는 것이다.
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import LinearSVC

# 1. 데이터 준비 (Pandas DataFrame -> Numpy Array)
# 모델이 계산할 수 있도록 '특성(x1, x2)'과 '정답(y)'을 분리.
X = df[['x1', 'x2']].to_numpy() # 'x1', 'x2' 열만 뽑아서 숫자 덩어리(Numpy)로 변환
y = df['y']                     # 'y' 열(0 또는 1)을 정답지로 설정

# 2. 파이프라인 구축
svm = Pipeline([
    # [데이터 표준화] 평균 0, 표준편차 1로 맞춤 (SVM의 필수 코스!)
    ('scaler', StandardScaler()),

    # [선형 SVM 분류기 설정]
    # C=1: 마진 오류를 어느 정도 허용할지 결정 (1은 기본값)
    # loss='hinge': SVM의 정석적인 손실 함수인 '힌지' 함수 사용
    # dual='auto': 이 부분이 에러의 핵심일 수 있다 (아래 상세 설명 참조)
    ('linearSVC', LinearSVC(C=1, loss='hinge', dual='auto'))
])

# 3. 모델 학습
# "준비된 데이터(X)와 정답(y)을 보고, 둘 사이를 가르는 최적의 직선(초평면)을 찾아라!"
svm.fit(X, y)

 

새로운 데이터 예측, 가중치 확인, 모델의 정확도 확인

  • kernel='rbf'를 쓰면 복잡하게 섞인 데이터도 입체적으로 띄워서(지난번에 말씀하신 차원 확장) 훨씬 잘 분류하기 때문에 커널 함수를 쓰는게 좋다고 한다
# 1. 새로운 데이터 예측하기
# 4마리의 가상 강아지 데이터를 던져주고 "얘네는 누구니?"라고 물어본다.
# 결과: [1, 0, 0, 1] -> [진돗개, 닥스훈트, 닥스훈트, 진돗개]로 판단한다.
y_pred = svm.predict([[0.12, 0.56], [-4, 40], [0, 40], [5,20]])

# 2. 모델의 '가중치' 확인하기 (coef_)
# 이 숫자는 모델이 'x1'과 'x2' 중 무엇을 더 중요하게 보는지 알려준다.
# 결과: [[ 5.79, -0.14]] -> x1(길이)의 숫자가 훨씬 큼.
# 이 모델은 현재 '높이(x2)'보다 '길이(x1)'를 기준으로 분류하는 게 훨씬 중요하다고 판단한 것이다.
print(svm.named_steps['linearSVC'].coef_)

# 3. 모델의 정확도 확인 (score)
# 내가 가진 전체 데이터 중 몇 개나 맞혔는지 점수를 낸다.
# 결과: 0.959 (약 95.9%의 정확도)
print(svm.score(X,y))

 

 

커널과 가우시안: "붕 뜨게 해서 땅을 친다"

사용자님의 비유가 정말 예술입니다! 그 비유를 학문적인 용어와 연결해 드릴게요.

① "붕 뜨게 하는 거" (커널 트릭)

XOR 그래프를 보면, 어떤 직선을 그어도 파란 점과 빨간 점을 완벽히 나눌 수 없다. 이때 커널 함수를 쓰면 평면(2차원)에 있던 점들을 입체(3차원) 공간으로 붕 띄운다. * 종이 위에 흩어진 구슬을 직선으로 못 나누니까, 어떤 구슬만 손으로 위로 쑥 들어 올리는 것과 같다. 그러면 그 사이로 평평한 판자를 끼워 넣어서 나눌 수 있게 된다.

② "가우시안 커널(RBF)로 땅을 치기"

가우시안 커널은 점 하나하나를 **'산봉우리'**처럼 만든다.

  • 땅 침: 각 데이터 점이 있는 위치에 '가우시안 종 모양(Gaussian Bell)'의 산을 세운다고 상상해 보자.
  • 파란 점들이 있는 곳은 산이 솟아오르고, 빨간 점들이 있는 곳은 골짜기가 된다.
  • 이렇게 만들어진 울퉁불퉁한 지형(Landscape)에서 특정 높이(땅에서 일정 높이)를 기준으로 슥 자르면, 비로소 파란색 영역과 빨간색 영역이 구분된다.

③ 가우시안 커널의 핵심: 가운대로 모으기?

가우시안 커널은 **'거리'**를 본다. 특정 중심점에서 가까우면 값이 크고, 멀어지면 0에 수렴한다.

  • "가운데로 모이게 한다"는 것은, 각 데이터 점을 기준으로 영향력이 집중되는 범위를 만든다는 의미로 해석하면 완벽하다!

활성화 함수(Activation Function)와 시그모이드(Sigmoid)

여기서부터는 **딥러닝(신경망)**의 영역으로 살짝 넘어간다.

  • SVM: "최대한 넓은 길을 만들어 딱딱 끊어서 분류하겠어!" (결과가 0 아니면 1)
  • 활성화 함수(Sigmoid 등): "이 데이터가 1번일 확률은 85% 정도야."라고 부드럽게 연결해 준다.
    • Sigmoid: S자 모양의 곡선으로, 어떤 숫자가 들어와도 0과 1 사이의 값으로 바꿔준다.
    • 딥러닝에서는 이런 함수들을 층(Layer)마다 달아서, 복잡한 비선형 관계를 계산해 낸다.
import numpy as np
import matplotlib.pyplot as plt

# 1. 랜덤 데이터 생성
np.random.seed(0)
X_xor = np.random.randn(200, 2) # 평균 0, 표준편차 1인 랜덤한 숫자 400개를 200행 2열로 만든다.

# 2. XOR 논리 만들기 (이게 핵심!)
# x1이 양수이면서 x2도 양수이거나, 둘 다 음수이면 -> False (0)
# 하나는 양수고 하나는 음수면(서로 다르면) -> True (1)
y_xor = np.logical_xor(X_xor[:, 0] > 0, X_xor[:, 1] > 0)
y_xor = np.where(y_xor, 1, 0) # True는 1로, False는 0으로 바꿉니다.

# 3. 그래프 그리기
# 1번 클래스(파란 동그라미)와 0번 클래스(빨간 네모)를 화면에 뿌린다.
plt.scatter(X_xor[y_xor == 1, 0], X_xor[y_xor == 1, 1], c='b', marker='o', label='class 1', s=50)
plt.scatter(X_xor[y_xor == 0, 0], X_xor[y_xor == 0, 1], c='r', marker='s', label='class 0', s=50)

plt.legend()
plt.xlabel("x1")
plt.ylabel("x2")
plt.title("XOR Problem")
plt.show()

 

XOR 문제를 해결하기 위해 다양한 SVM 커널(선형, 다항식, RBF)을 비교하고 시각화

① np.meshgrid (매쉬 그리드)가 왜 필요할까?

우리는 선 하나만 긋고 싶은 게 아니라, "어디까지가 파란색 영역이고 어디까지가 빨간색 영역인지" 배경 전체를 색칠하고 싶어 한다.

  • 바닥에 촘촘하게 모눈종이(meshgrid)를 깔고, 그 모눈 하나하나마다 "넌 무슨 색이야?"라고 물어본 뒤 색칠하는 과정이라고 생각하면 된다.

② kernel='poly' vs kernel='rbf'

  • poly (다항식): "내 지도를 2차원, 3차원 공식으로 휘게 만들겠어!"라고 수학적인 곡선을 그린다.
  • rbf (가우시안): **"붕 뜨게 해서 땅 치기"**의 끝판왕이다. 각 데이터 점을 기준으로 종 모양의 산(가우시안)을 만들어서, 전체 지형을 울퉁불퉁하게 만든 뒤 물을 채워 가르는 방식이다. 가장 유연하다.

③ gamma는 정말 Curve일까?

네, 아주 정확합니다!

  • gamma가 높으면: 산봉우리가 아주 뾰족해진다. (곡률이 커짐 / 데이터 하나하나에 예민함)
  • gamma가 낮으면: 산봉우리가 완만해진다. (곡선이 부드러워짐 / 둥글둥글하게 분류함)

④ decision_function (불확실성)

인공지능이 내린 판결문 뒤에 붙은 **'확신 점수'**이다.

  • 값이 +10.5라면? "이건 보나 마나 확실히 1번이다!"
  • 값이 +0.1이라면? "음... 1번인 것 같긴 한데 좀 아슬아슬하네?" (경계선 바로 근처) 그래서 이 값을 불확실성 혹은 신뢰도라고 부른다.
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np

# [함수 설명] 매쉬 그리드(Meshgrid): 바닥에 촘촘한 그물망을 깔아 색칠하는 함수
def plot_xor(X, y, model, title, xmin=-3, xmax=3, ymin=-3, ymax=3):
    # 1. np.meshgrid: 배경을 1000x1000개의 아주 작은 점(그물망)으로 나눈다.
    # "그물 형태에 맞춰서 도형이 생김" -> 맞는말. 각 그물 코마다 색을 칠하려고 만드는 것이다.
    XX, YY = np.meshgrid(np.arange(xmin, xmax, (xmax-xmin)/1000),
                         np.arange(ymin, ymax, (ymax-ymin)/1000))

    # 2. 모든 그물 점들에 대해 "너는 0이니 1이니?"라고 모델에게 물어본다.
    ZZ = np.reshape(model.predict(
        np.array([XX.ravel(), YY.ravel()]).T), XX.shape)

    # 3. plt.contourf: 모델의 답변에 따라 배경에 색을 채운다. (등고선 색칠)
    plt.contourf(XX, YY, ZZ, cmap=mpl.cm.Paired_r, alpha=0.5)

    # 4. 실제 데이터 점들을 그 위에 뿌린다. (파란 동그라미 vs 빨간 네모)
    plt.scatter(X[y == 1, 0], X[y == 1, 1], c='b', marker='o', label='class 1', s=50)
    plt.scatter(X[y == 0, 0], X[y == 0, 1], c='r', marker='s', label='class 0', s=50)

    plt.xlim(xmin, xmax)
    plt.ylim(ymin, ymax)
    plt.title(title)
    plt.xlabel("x1")
    plt.ylabel("x2")

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

# [모델 1] Linear SVM: "직선으로만 가르겠다!" -> XOR 문제에서는 무조건 실패한다.
linsvc = SVC(kernel="linear").fit(X_xor, y_xor)

# [모델 2] Polynomial SVM: "다항식(곡선)을 쓰겠다!"
# degree=2: 2차 함수(포물선 등) 형태로 휘어서 가른다.
# XOR는 x1*x2 형태라 2차식만 되어도 해결이 가능하다.
polysvc = SVC(kernel="poly", degree=2, gamma=1, coef0=0).fit(X_xor, y_xor)

# [모델 3] RBF SVM: "가우시안 커널, 즉 무한 차원으로 붕 띄우겠다!"
# 가장 강력하고 범용적이다. (계산량은 좀 더 많다.)
rbfsvc = SVC(kernel="rbf").fit(X_xor, y_xor)

# 정확도 출력 (1.0이면 100점임)
print('선형 SVM 정확도: ', accuracy_score(y_xor, linsvc.predict(X_xor)))
print('polynomial SVM 정확도: ', accuracy_score(y_xor, polysvc.predict(X_xor)))
print('rbf SVM 정확도: ', accuracy_score(y_xor, rbfsvc.predict(X_xor)))

# 시각화 비교
plt.figure(figsize=(8, 12))
plt.subplot(311)
plot_xor(X_xor, y_xor, linsvc, "Linear SVM")   # 반으로 툭 갈라버려서 많이 틀림
plt.subplot(312)
plot_xor(X_xor, y_xor, rbfsvc, "rbf SVM")      # 부드러운 곡선으로 완벽 분류
plt.subplot(313)
plot_xor(X_xor, y_xor, polysvc, "polynomial SVM") # 2차 곡선 형태로 분류
plt.tight_layout()
plt.show()

# 샘플별 예측 결과 (0 또는 1)
print('rbf SVM 이용한 샘플별 예측 결과: ', rbfsvc.predict(X_xor))

# [중요] decision_function: "불확실성" 혹은 "경계선으로부터의 거리"
# 이 숫자가 0보다 크면 1번 클래스, 작으면 0번 클래스이다.
# 숫자가 크면 클수록 "얘는 확실히 1번이야!"라고 모델이 자신만만해하는 것이다.
print('rbf SVM 이용한 샘플별 불확실성: ', rbfsvc.decision_function(X_xor))

 

===================== 결정 트리 =====================

결정 트리(Decision Tree) 아이리스 분류 코드

 

  • [3, 2, 1, 0.2]: 꽃잎이 아주 작으므로 Setosa로 판단.
  • [4.9, 2.2, 3.8, 1.1]: 중간 정도 크기이므로 Versicolor로 판단.
  • [5.3, 2.5, 4.6, 1.9]: 꽃잎이 크고 넓으므로 Virginica로 판단.

 

from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier

# 1. 데이터 로드: 꽃잎과 꽃받침의 길이/너비 데이터(X)와 꽃의 종류 정답(y)을 가져오기.
iris = load_iris()
X, y = iris.data, iris.target

# 2. 결정 트리 모델 생성
# max_depth=3: 나무의 층수를 3층으로 제한하기.
# 트리는 학습이 너무 잘 되어 '과적합'되기 쉬우므로,
# 이렇게 깊이를 제한(가지치기)해서 일반적인 규칙을 찾게 만드는 것이 중요하다.
dec_tree = DecisionTreeClassifier(max_depth=3)

# 3. 모델 학습
# "속도가 SVM보다 빠르다": SVM은 고차원으로 데이터를 보내는 복잡한 계산을 하지만,
# 트리는 단순히 "꽃잎 길이가 2cm보다 큰가?" 같은 질문으로 구역을 나누기 때문에 매우 빠르다.
dec_tree.fit(X, y)

# 4. 성능 확인
# 전체 데이터 중 모델이 맞힌 정답의 비율(정확도)을 출력한다.
print(f"학습 정확도: {dec_tree.score(X, y):.4f}")

# 5. 새로운 샘플 예측
# test_sample: [꽃받침 길이, 꽃받침 너비, 꽃잎 길이, 꽃잎 너비] 순서의 데이터이다.
test_sample = [[3, 2, 1, 0.2], [4.9, 2.2, 3.8, 1.1], [5.3, 2.5, 4.6, 1.9]]

# 예측 수행: 리스트 안에 리스트를 넣은 2차원 형태로 전달해야 한다.
predictions = dec_tree.predict(test_sample)

# 결과 해석
# 0: Setosa(세토사), 1: Versicolor(버시컬러), 2: Virginica(버지니카)
print(f"예측된 결과(숫자): {predictions}")
# 실제 꽃 이름으로 매핑해서 보기
print(f"예측된 꽃 이름: {[iris.target_names[p] for p in predictions]}")

 

===================== 앙상블 모델의 성능 비교 =====================

결정 트리 하나만 쓰는 게 아니라, 나무를 여러 개 모으거나(랜덤 포레스트), 오답을 보완하며 학습하는(그래디언트 부스팅) '앙상블(Ensemble)' 기법까지 포함된 아주 실습 코드

 

  • 결정트리: 학습(97.1%), 테스트(91.1%) → 학습 데이터는 잘 맞히는데 테스트는 조금 떨어진다 (살짝 과적합 기미)
  • 랜덤포레스트: 학습(99.0%), 테스트(95.5%) → 성능이 가장 안정적! 여러 나무가 협동하니 실수가 줄어든다.
  • 그래디언트부스팅: 학습(99.0%), 테스트(93.3%) → 나무 5개(n_estimators=5)만 썼는데도 아주 강력한 성능을 보여준다.

 

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score

# 1. 데이터 준비
iris = load_iris()
# 시각화나 계산을 편하게 하기 위해 4개의 특징 중 2개([0, 2])만 가져온다.
X, y = iris.data[:, [0, 2]], iris.target
# 데이터를 학습용(70%)과 테스트용(30%)으로 나눈다.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)

# 2. 결정 트리 (Decision Tree)
# criterion='gini': 지니계수(불순도)를 기준으로 삼는다. (잡티가 적은 쪽으로 분기!)
# max_depth=4: 나무가 너무 깊어지면(과적합) 안 되니까 4층까지만 제한한다.
tree = DecisionTreeClassifier(criterion='gini', max_depth=4, random_state=1)
tree.fit(X_train, y_train)

# 트리 성능 확인
tree_train = accuracy_score(y_train, tree.predict(X_train))
tree_test = accuracy_score(y_test, tree.predict(X_test))
print(f'결정트리 학습 정확도 : {tree_train:.3%}')
print(f'결정트리 테스트 정확도 : {tree_test:.3%}')

# 3. 랜덤 포레스트 (Random Forest)
# 나무 한 그루는 과적합되기 쉬움. 그래서 나무 10그루(n_estimators=10)를 심어 '집단지성'을 이용한다.
# "하나가 틀려도 다른 애들이 맞히면 된다!"는 원리이다.
from sklearn.ensemble import RandomForestClassifier
forest = RandomForestClassifier(criterion='gini', n_estimators=10, max_features=2, max_depth=None, random_state=1)
forest.fit(X_train, y_train)

# 포레스트 성능 확인
forest_train = accuracy_score(y_train, forest.predict(X_train))
forest_test = accuracy_score(y_test, forest.predict(X_test))
print(f'랜덤포레스트 학습 정확도 : {forest_train:.3%}')
print(f'랜덤포레스트 테스트 정확도 : {forest_test:.3%}')

# 4. 그래디언트 부스팅 (Gradient Boosting)
# 앞서 만든 나무가 틀린 오답(에러)을 다음 나무가 집중적으로 공부해서 맞히는 방식이다.
# "실수를 반복하지 말자!"며 조금씩 정답을 찾아가는 '최소비용' 전략이다.
from sklearn.ensemble import GradientBoostingClassifier
boost = GradientBoostingClassifier(n_estimators=5, learning_rate=0.5, max_depth=3, random_state=1)
boost.fit(X_train, y_train)

# 부스팅 성능 확인
boost_train = accuracy_score(y_train, boost.predict(X_train))
boost_test = accuracy_score(y_test, boost.predict(X_test))
print(f'그래디언트부스팅 학습 정확도 : {boost_train:.3%}')
print(f'그래디언트부스팅 테스트 정확도 : {boost_test:.3%}')

 

===================== 차원의 저주 =====================

3차원 공간 안에 2차원 평면 형태로 퍼져 있는 데이터를 직접 만들어보기

① 왜 이런 짓을 할까?

우리가 사는 세상은 3차원이지만, 어떤 데이터(예: 키, 몸무게, 허리둘레)는 서로 밀접하게 연관되어 있어서 실제로는 2차원 평면 같은 좁은 영역에만 모여 있는 경우가 많다.

  • 이 코드는 **"3차원 공간에 살고 있지만, 사실은 2차원적인 특징을 가진 데이터"**를 인공적으로 만든 것이다.

② 벡터 uv의 역할 (종이의 가로세로)

  • 3차원 공간이라는 커다란 상자 안에 비듬하게 기울어진 종이 한 장을 넣는다고 상상해 보자.
  • u는 그 종이의 가로 방향, v는 세로 방향이다.
  • r_coeff는 그 종이 위에서 점이 찍힐 위치이다.

③ 소음(Noise)의 의미

  • 만약 + 0.1 * np.random.rand(3,) 이 부분이 없다면, 모든 점은 완벽하게 평평한 종이 위에만 찍힌다.
  • 하지만 실제 세상의 데이터는 조금씩 오차가 있는데, 이 소음을 더해줌으로써 종이 위아래로 미세하게 울퉁불퉁하게 튀어나온 리얼한 데이터를 만든 것이다.
import numpy as np
import matplotlib.pyplot as plt

# 1. 기준이 되는 두 방향(벡터) 정하기
# u와 v는 3차원 공간에서 '바닥' 혹은 '기울어진 평면'의 기준축이 된다.
# np.sqrt()로 나누는 이유는 벡터의 길이를 1로 맞추기 위해서이다. (정규화)
u = np.array([1,1,1]) / np.sqrt(3)
v = np.array([1,0,-1]) / np.sqrt(2)

# 2. 1000개의 데이터 셋 생성하기
n_data = 1000
X = []

for _ in range(n_data) :
    # 평면 위에서의 랜덤한 위치 좌표(2개)를 뽑는다.
    r_coeff = np.random.randn(2,)

    # [핵심 로직] 3차원 공간에 점 찍기
    # 2.0 * r_coeff[0] * u + r_coeff[1] * v :
    # -> 기준 벡터 u와 v를 조합해서 '완벽한 평면' 위의 점을 만든다.
    # + 0.1 * np.random.rand(3,) :
    # -> 거기에 아주 미세한 3차원 '소음(잡티)'을 더해 평면에서 살짝 들뜨게 만든다.
    data = 2.0* r_coeff[0] * u + r_coeff[1] * v + 0.1 * np.random.rand(3,)
    X.append(data)

# 리스트를 계산하기 편한 넘파이 배열로 변환한다.
X = np.array(X)

# 3. 가시화를 위한 맷플롯립 figure 만들기
fig = plt.figure(figsize = (10, 7))

# projection="3d": 2차원 종이에 3차원 입체감을 표현하겠다고 선언한다.
ax = plt.axes(projection ="3d")

# 4. 3차원 공간에 데이터 가시화 (초록색 점들)
ax.scatter3D(X[:,0], X[:,1], X[:,2], color = "green")

plt.title("3D scatter plot of Dataset")
plt.show()

 

3차원 공간에 떠 있던 데이터를 가장 잘 설명하는 **'핵심 평면'**을 찾아 2차원으로 납작하게 누르는 PCA(주성분 분석) 과정

왜 PCA를 할까?

  1. 시각화: 3차원 이상의 데이터는 사람이 눈으로 볼 수 없지만, 2차원으로 줄이면 위 그래프처럼 한눈에 파악할 수 있다.
  2. 효율성: 데이터의 크기를 줄이면서도 핵심 정보는 지키기 때문에, 나중에 머신러닝(SVM, 결정 트리 등) 모델을 돌릴 때 계산 속도가 훨씬 빨라진다.
  3. 노이즈 제거: 아까 추가했던 미세한 잡티(0.1 수준의 소음)는 차원을 축소하는 과정에서 자연스럽게 무시되어 버린다.
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt

# 1. PCA 모델 만들기
# n_components = 2: "데이터가 비록 3차원이지만, 나는 가장 중요한 축 2개(2차원)로 요약할래!"라는 뜻.
pca = PCA(n_components = 2)

# 2. 차원 축소 실행 (fit_transform)
# fit: 3차원 데이터 X를 분석해서 어느 방향으로 데이터를 눌러야 정보가 가장 덜 손실될지 학습한다.
# transform: 학습한 방향대로 데이터를 3차원에서 2차원으로 실제로 변환(투영)한다.
X_2d_sklearn = pca.fit_transform(X)

# 3. 2차원 시각화
# 이제 X_2d_sklearn은 [x, y, z]가 아니라 [주성분1, 주성분2] 형태의 데이터를 가진다.
plt.scatter(X_2d_sklearn[:, 0], X_2d_sklearn[:, 1], color='r')

plt.title("PCA: Projecting 3D Data to 2D Plane")
plt.xlabel("First Principal Component") # 가장 데이터가 많이 퍼져 있는 첫 번째 축
plt.ylabel("Second Principal Component") # 그 다음으로 데이터가 많이 퍼져 있는 두 번째 축
plt.show()

 

**"4차원(꽃받침 길이/너비, 꽃잎 길이/너비)이라는 복잡한 데이터를 2차원 평면 위에 가장 효과적으로 그려내는 방법"**

① 왜 굳이 PCA를 할까?

꽃의 특징이 4개(sepal length, sepal width, petal length, petal width)라는 건 4차원 공간에 데이터가 있다는 뜻이다. 우리 인간은 4차원을 눈으로 볼 수 없다.

  • PCA는 이 4차원 데이터를 **가장 잘 설명할 수 있는 명당자리(2차원 평면)**에 카메라를 설치하고 사진을 찍는 것과 같다.
  • 사진을 찍으면 3차원인 우리 모습이 2차원 종이에 남듯, 데이터도 2차원으로 압축된다.

② PC1과 PC2가 뭘까?

  • PC1 (제1 주성분): 데이터들이 가장 넓게 퍼져 있는 방향의 선이다. 즉, 꽃들을 서로 구분하기 위해 가장 결정적인 차이를 보여주는 기준선이다.
  • PC2 (제2 주성분): PC1과 직각을 이루면서, 그다음으로 데이터의 차이를 잘 보여주는 선이다.

③ 그래프 해석

  • 빨간색(0번 세토사)은 혼자 저 멀리 떨어져 있는데, 이건 세토사가 다른 꽃들과 아주 뚜렷하게 구분되는 특징을 가졌다는 뜻이다.
  • 초록색과 파란색은 조금 겹쳐 있다. 얘네 둘은 특징이 비슷해서 분류할 때 조금 더 세밀한 모델(SVM이나 결정 트리 등)이 필요할 수 있다는 걸 미리 알 수 있다.

④ StandardScaler를 안 하면 어떻게 되나요?

만약 한 컬럼의 단위가 mm이고 다른 게 km라면, PCA는 숫자가 큰 mm 데이터가 훨씬 중요하다고 착각하게 된다. 그래서 **표준화(StandardScaler)**는 PCA 전의 필수 과정이다.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris

# 1. 데이터 가져오기
iris = load_iris()

# 데이터를 보기 좋게 '표(DataFrame)' 형태로 만든다.
# 꽃의 특성 데이터들과 정답(target)을 한 표에 합친다.
df = pd.DataFrame(data= np.c_[iris['data'], iris['target']],
                  columns= iris['feature_names'] + ['target'])
print(df)

# 2. 데이터 전처리 (표준화)
from sklearn.preprocessing import StandardScaler
features = iris['feature_names']

# '힌트(꽃의 정보)'와 '정답(꽃의 종류)'을 분리한다.
x = df.loc[:, features].values # 힌트 데이터 (4개 열)
y = df.loc[:,['target']].values # 정답 데이터 (1개 열)

# [중요] StandardScaler: 모든 데이터의 단위를 통일한다.
# 어떤 것은 숫자가 크고(5.1), 어떤 것은 작으면(0.2) 큰 숫자에만 모델이 휘둘릴 수 있다.
# 평균을 0으로, 표준편차를 1로 맞춰서 모든 특성이 '공평하게' 반영되게 한다.
x = StandardScaler().fit_transform(x)
print(x)

# 3. PCA 실행 (차원 축소)
from sklearn.decomposition import PCA
# n_components=2: "4차원 데이터를 정보 손실을 최소화하면서 2차원(축 2개)으로 줄여줘!"
pca = PCA(n_components=2)
pca_result = pca.fit_transform(x)

# 4. 결과 정리
# 축소된 결과를 'PC1(제1 주성분)', 'PC2(제2 주성분)'라는 이름의 표로 만든다.
principalDf = pd.DataFrame(data = pca_result, columns = ['PC1', 'PC2'])

# 원래 있던 정답(target) 열을 다시 옆에 붙여준다. (색칠할 때 써야 함)
finalDf = pd.concat([principalDf, df[['target']]], axis = 1)
print(finalDf)

# 5. 시각화 (그림 그리기)
fig = plt.figure(figsize = (5,5))
ax = fig.add_subplot(1,1,1)
ax.set_xlabel('Primary Principal Component') # 가로축: 데이터의 특징을 가장 많이 담은 선
ax.set_ylabel('Secondary Principal Component') # 세로축: 그다음으로 특징을 많이 담은 선
ax.set_title('Dimension Reduction with PCA', fontsize = 20)

# 정답 종류(0, 1, 2)에 따라 빨강, 초록, 파랑으로 색을 칠한다.
targets = [0.0, 1.0, 2.0]
colors = ['r', 'g', 'b']
for target, color in zip(targets, colors):
    idx = finalDf['target'] == target
    # 2차원 평면에 점을 찍는다. (Scatter plot)
    ax.scatter(finalDf.loc[idx, 'PC1'], finalDf.loc[idx, 'PC2'], c = color)

ax.legend(targets) # 범례 표시
ax.grid() # 모눈종이 표시
plt.show()

'머신러닝' 카테고리의 다른 글

크롤링 해서 시각화  (0) 2026.01.12
머신러닝 - 다중회귀  (0) 2026.01.11
머신러닝 - 선형 회귀 기초  (0) 2026.01.09
PANDAS  (0) 2026.01.08
SEABORN  (0) 2026.01.07