fish_length = [ 25.4 , 26.3 , 26.5 , 29.0 , 29.0 , 29.7 , 29.7 , 30.0 , 30.0 , 30.7 , 31.0 , 31.0 ,
31.5 , 32.0 , 32.0 , 32.0 , 33.0 , 33.0 , 33.5 , 33.5 , 34.0 , 34.0 , 34.5 , 35.0 ,
35.0 , 35.0 , 35.0 , 36.0 , 36.0 , 37.0 , 38.5 , 38.5 , 39.5 , 41.0 , 41.0 , 9.8 ,
10.5 , 10.6 , 11.0 , 11.2 , 11.3 , 11.8 , 11.8 , 12.0 , 12.2 , 12.4 , 13.0 , 14.3 , 15.0 ]
fish_weight = [ 242.0 , 290.0 , 340.0 , 363.0 , 430.0 , 450.0 , 500.0 , 390.0 , 450.0 , 500.0 , 475.0 , 500.0 ,
500.0 , 340.0 , 600.0 , 600.0 , 700.0 , 700.0 , 610.0 , 650.0 , 575.0 , 685.0 , 620.0 , 680.0 ,
700.0 , 725.0 , 720.0 , 714.0 , 850.0 , 1000.0 , 920.0 , 955.0 , 925.0 , 975.0 , 950.0 , 6.7 ,
7.5 , 7.0 , 9.7 , 9.8 , 8.7 , 10.0 , 9.9 , 9.8 , 12.2 , 13.4 , 12.2 , 19.7 , 19.9 ]
데이터를 준비한다.
import numpy as np
# 넘파이의 column_stack()을 사용하여 2차원 배열을 생성
fish_data = np . column_stack (( fish_length , fish_weight ))
print ( fish_data [: 5 ]) # 넘파이 배열을 출력하면 행과 열 형태로 출력된다.
zip()와 for문으로 파이썬 리스트를 순회하는 방식이었으나,
이번에는 넘파이의 column_stack() 함수를 사용하여 간단하게 2차원 데이터를 생성한다.
[[ 25.4 242. ]
[ 26.3 290. ]
[ 26.5 340. ]
[ 29. 363. ]
[ 29. 430. ]]
넘파이 배열을 출력하면 리스트 데이터와 다르게
행과 열을 맞춰 출력해준다.
넘파이의 함수를 사용하여 타겟데이터도 생성해준다.
# 타겟 데이터 생성
# 데이터가 클 수록 넘파이 array는 효율적이다.(파이썬 리스트에 비해)
fish_target = np . concatenate (( np . ones ( 35 ), np . zeros ( 14 )))
print ( fish_target )
기존의 [1], [0] 리스트를 곱하는 방법을 사용하지 않고,
np.ones() 와 np.zeros(), np.concatenate() 함수를 사용한다.
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0.]
# 사이킷런의 train_test_split()으로 훈련 세트와 테스트 세트 나누기
from sklearn . model_selection import train_test_split
train_input , test_input , train_target , test_target = train_test_split ( fish_data , fish_target , random_state = 42 )
# 넘파이 배열의 shape 속성으로 입력 데이터가 알맞은 비율로 나누어졌는지 확인한다.
# shape 속성으로 얻은 넘파이 배열의 크기는 튜플로 표현된다.
# 타겟데이터는 1차원 배열이므로, 원소가 하나인 튜플로 표현된다.
print ( train_input .shape, test_input .shape) # (36, 2) (13, 2)
print ( train_target .shape, test_target .shape) # (36,) (13,)
기존에는 np.random.shuffle()로 만든 랜덤 인덱스를 사용했으나,
이번에는 sklearn의 train_test_split() 을 사용하여 훈련 세트와 테스트 세트를 분리한다.
train_test_split() 함수의 기본설정값은 25%를 테스트 세트로 사용한다.
넘파이의 shape 속성으로 기본설정값대로 데이터가 잘 분리되었음을 확인할 수 있다.
# 테스트 데이터를 출력하여 데이터가 알맞게 섞였는지 확인한다.
print ( test_target ) # [1. 0. 0. 0. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
# 기존 데이터의 비율이 유지되고 있지 않고 '샘플링 편향'이 나타났다.
# train_test_split() 함수의 stratify 매개변수의 타겟 데이터를 설정하여 클래스 비율에 맞게 데이터를 나눌 수 있다.
train_input , test_input , train_target , test_target = train_test_split (
fish_data , fish_target , stratify = fish_target , random_state = 42
)
print ( test_target ) # [0. 0. 1. 0. 1. 0. 1. 1. 1. 1. 1. 1. 1.]
테스트 타겟 데이터를 출력하니, 원래 데이터와 비율이 맞지 않는다.
이처럼 샘플이 골고루 섞이지 않는 상황을 방지하기 위하여,
train_test_split() 함수의 stratify 매개변수에서 타겟 데이터를 설정할 수 있다.
훈련 데이터가 작거나 특정 클래스의 샘플 개수가 적을 때 유용하다.
# KNN모델을 만들고 모델 점수를 확인한다.
from sklearn . neighbors import KNeighborsClassifier
kn = KNeighborsClassifier ()
kn . fit ( train_input , train_target )
kn . score ( test_input , test_target ) # 1.0
# 새로운 데이터를 넣고 분류 결과를 확인
# 도미(1)를 빙어(0)로 오분류한 것을 확인할 수 있다.
print ( kn . predict ([[ 25 , 150 ]])) # [0.]
준비한 train 데이터로 모델을 훈련 후 test 데이터로 점수를 확인한다.
모든 test 데이터를 완벽하게 분류하는 것을 확인 후 새 샘플 데이터의 분류를 시도하였으나
길이 25, 무게 150의 새로운 데이터를 도미(1)가 아닌, 빙어(0)로 오분류하는 것을 확인할 수 있다.
# scatter plot으로 새 샘플 데이터의 위치를 확인한다.
import matplotlib . pyplot as plt
plt . scatter ( train_input [:, 0 ], train_input [:, 1 ]) # 훈련데이터
plt . scatter ( 25 , 150 , marker = '^' ) # 새 샘플 데이터
plt . xlabel ( 'length' )
plt . ylabel ( 'weight' )
plt . show ()
scatter plot으로 확인하니,
새로운 데이터는 분명 도미 데이터에 가깝게 위치해있다.
KNeighborsClassifier 클래스의 kneighbors() 메서드와
넘파이 배열 인덱싱을 사용하여
새 샘플 데이터에 이웃하는 5개 데이터를 scatter plot으로 확인한다.
(KNeighborsClassifier 클래스가 참고하는 가까운 이웃 갯수의 기본값인 5개이다.)
# KNeighborsClassifier클래스의 참고하는 가까운 이웃 갯수의 기본값은 5이다.
# kneighbors() 메서드를 사용하여 새 샘플 데이터와 가까운 5개의 데이터를 확인한다.
# kneighbors() 메서드는 아래의 두 가지 값을 반환한다.
# distances: 각 데이터 포인트와 가장 가까운 이웃들 사이의 거리.
# indexes: 가장 가까운 이웃들의 인덱스.
distances , indexes = kn . kneighbors ([[ 25 , 150 ]])
plt . scatter ( train_input [:, 0 ], train_input [:, 1 ]) # 훈련데이터
plt . scatter ( 25 , 150 , marker = '^' ) # 새 샘플 데이터
plt . scatter ( train_input [ indexes , 0 ], train_input [ indexes , 1 ], marker = 'D' )
plt . xlabel ( 'length' )
plt . ylabel ( 'weight' )
plt . show ()
새 샘플 데이터의 이웃하는 5개 데이터 중 4개가 빙어에 속하는 것을 알 수 있다.
데이터를 직접 확인해본다.
# 새 샘플 데이터의 이웃한 5개의 데이터 중 4개가 빙어인 것을 확인
# 데이터를 직접 확인해본다.
print ( train_input [ indexes ])
print ( train_target [ indexes ])
[[[ 25.4 242. ]
[ 15. 19.9]
[ 14.3 19.7]
[ 13. 12.2]
[ 12.2 12.2]]]
[[1. 0. 0. 0. 0.]]
# 그래프의 x축과 y축의 범위가 다르기 때문에 데이터간의 거리가 잘못 측정된 것 (두 특성의 스케일이 다르다)
plt . scatter ( train_input [:, 0 ], train_input [:, 1 ])
plt . scatter ( 25 , 150 , marker = '^' )
plt . scatter ( train_input [ indexes , 0 ], train_input [ indexes , 1 ], marker = 'D' )
plt . xlim (( 0 , 1000 )) # x축 범위를 y축과 동일하게 맞춘다.
plt . xlabel ( 'length' )
plt . ylabel ( 'weight' )
plt . show ()
그래프의 y축의 범위가 x축에 비해 너무 컸기 때문에
y축의 거리가 과도하게 민감하게 측정되었던 것이다.
y축으로 조금만 멀어져도 거리가 큰 값으로 측정되어 각 샘플의 거리가 제대로 측정되지 못하였다.
xlim() 함수로 x축의 범위를 y값에 맞춘 후 그래프를 다시 확인하면 무엇이 문제였는지 명확히 확인할 수 있다.
# 데이터 전처리
# axis=0 행을 따라 각 열의 통계값을 계산
mean = np . mean ( train_input , axis = 0 )
std = np . std ( train_input , axis = 0 )
train_scaled = ( train_input - mean ) / std # 브로드캐스팅으로 모든 행에 적용됨
print ( train_scaled [: 5 ])
# 새 샘플 데이터도 스케일을 맞춰준다.
# scatter plot의 x축과 y축의 범위 확인(-1.5 ~ 1.5)
new = ([ 25 , 150 ] - mean ) / std
plt . scatter ( train_scaled [:, 0 ], train_scaled [:, 1 ])
plt . scatter ( new [ 0 ], new [ 1 ], marker = '^' )
plt . xlabel ( 'length' )
plt . ylabel ( 'weight' )
plt . show ()
데이터의 스케일을 맞춰주는 것을 데이터 스케일링라고 한다.
여기서는 표준점수 를 사용하여 데이터 스케일링을 진행하고, 이것을 데이터 '표준화'라고 한다.
(데이터 '정규화'와는 다른 개념이니 헷갈리지 않도록 유의한다.)
표준점수를 사용하는 정규화는 표준화 (Standardization)라고 한다. 표준화는 데이터의 평균을 0, 표준편차를 1로 변환하여 데이터가 정규분포를 따르도록 하는 방법이다. 이를 통해 데이터의 스케일을 맞추고, 이상치(outlier)에 덜 민감하게 만든다. 표준화의 수식은 다음과 같다: X′= (X − μ) / σ μ: 데이터의 평균 σ: 데이터의 표준편차
np.mean() 함수와 np.std() 함수를 사용하며,
axis = 0 으로 설정하면, 행을 따라 각 열의 통계값을 계산할 수 있다.
[[ 0.24070039 0.14198246]
[-1.51237757 -1.36683783]
[ 0.5712808 0.76060496]
[-1.60253587 -1.37766373]
[ 1.22242404 1.45655528]]
데이터 스케일링을 마친 scatter plot에서는 x축과 y축의 범위가 -1.5 ~ 1.5로 동일함을 알 수 있다.
해당 데이터셋으로 모델을 다시 훈련시키고, 평가한다.
평가에 사용할 테스트 데이터도 스케일링 해야 함을 잊으면 안된다.
# 스케일링이 끝난 데이터로 모델을 학습
kn . fit ( train_scaled , train_target )
# 테스트 세트도 변환 후 모델 평가
test_scaled = ( test_input - mean ) / std
kn . score ( test_scaled , test_target ) # 1.0
# 스케일링된 새 샘플 데이터를 새로운 모델에 넣어본다.
print ( kn . predict ([ new ])) # [1.]
모델 점수도 1.0이고, 새 데이터 샘플도 잘 분류하는 것을 확인할 수 있다.
마지막으로 스케일링된 데이터로 이웃하는 5개 데이터의 변화를 확인한다.
# 스케일링된 데이터로 scatter plot을 출력한다.
distances , indexes = kn . kneighbors ([ new ])
plt . scatter ( train_scaled [:, 0 ], train_scaled [:, 1 ])
plt . scatter ( new [ 0 ], new [ 1 ], marker = '^' )
plt . scatter ( train_scaled [ indexes , 0 ], train_scaled [ indexes , 1 ], marker = 'D' )
plt . xlabel ( 'length' )
plt . ylabel ( 'weight' )
plt . show ()
특정 축(특성)에 민감하게 반응하지 않고 안정적으로 예측할 수 있게 되었다.