올바른 결과 도출을 위해 데이터를 사용하기 전에 데이터 전처리 과정이 꼭 필요하다.
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]
이전장까지 데이터를 만들때 리스트 내포 방식을 통해 zip()함수를 사용하여 만들었다.
이번장부터는 더 간단한 방법을 사용해보자.
import numpy as np
fish_data = np.column_stack((fish_length, fish_weight)) # 리스트 내포 대신 사용
np.column_stack() 함수는 전달받은 리스트를 일렬로 세운 다음 차례대로 나란히 연결하는 함수이다.
예로, 다음과 같은 간단한 2개의 리스트를 붙여보자.
np.column_stack(([1,2,3], [4,5,6]))
>>>
array([[1, 4],
[2, 5],
[3, 6]])
이렇게 3개의 행과 2개의 열의 리스트를 만들 수 있다.
❗이때, 전달할 리스트를 튜플(turple) 형식으로 전달한 이유는 값이 바뀌지 않도록 하기 위해 전달하는 것이다.
이제 정답 데이터를 간단하게 만들어보자.
np.ones() 함수는 원하는 개수의 1로 채운 배열을 만들어준다.
np.zeros() 함수는 원하는 개수의 0로 채운 배열을 만들어준다.
두 개의 배열을 합칠 때 np.column_stack()함수를 사용하지 않고 첫 번째 차원에 따라 배열을 연결하는 np.concatenate()함수를 사용한다.

fish_target = np.concatenate((np.ones(35), np.zeros(14))) # [1] : 도미 [0] : 빙어 정답 데이터 만들기
print(fish_target)
>>> [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() 함수이다.
train_test_split()은 훈련 데이터를 훈련 세트와 테스트 세트로 나누는 함수이다.
여러 개의 배열을 전달할 수 있다.
테스트 세트로 나눌 비율은 test_size 매개변수에서 지정할 수 있으며 기본값은 0.25(25%)이다.
shuffle 매개변수로 훈련 세트와 테스트 세트로 나누기 전에 무작위로 섞을지 여부를 결정할 수 있다. (기본값 - True)
stratify 매개변수에 클래스 레이블이 담긴 배열(일반적으로 타깃 데이터)을 전달하면 클래스 비율에 맞게 훈련 세트와 테스트 세트를 나눠준다.
train_test_split() 함수는 사이릿런의 model_selection 모듈 내에 있다.
from sklearn.model_selection import train_test_split
train_input, test_input, train_target, test_target = train_test_split(fish_data, fish_target, stratify=fash_target, random_state=42)
stratify 매개변수는 샘플링 편향 문제를 막기 위해 클래스의 비율에 맞게 훈련 세트와 테스트 세트를 나눠준다.
random_state 매개변수는 이전장까지 사용했던 np.random.seed()와 같은 목적이다.
from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier()
kn.fit(train_input, train_target)
kn.score(test_input, test_target)
>>> 1.0
print(kn.predict([[25, 150]])) # 당연히 도미(1)인줄 알았는데 아니였다. 빙어(0)
>>> [0.]
왜 이런 일이 생긴걸까?
데이터를 시각화해서 분석해보자.
import matplotlib.pyplot as plt
plt.scatter(train_input[:,0], train_input[:,1])
plt.scatter(25, 150, marker='^') # marker 매개변수는 모양을 지정합니다.
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

육안으로 보기엔 도미에 가까운 데이터 같아보이는데 무엇이 문제일까?
이를 확인하기 위해, 우리가 사용한 KNeighborsClassifier 클래스에 대해 먼저 복기하자.
KNeighborsClassifier 클래스는 주어진 샘플에서 가장 가까운 이웃의 과반수를 통해 정답을 내리는 알고리즘이였다.
KNeighborsClassifier 클래스에서 가장 가까운 이웃을 찾아주는 kneighbors() 메서드를 제공한다.
n_neighbors 매개변수가 기본값 5이므로 5개의 이웃이 반환된다.
반환된 5개의 이웃도 같이 시각화 해보자.
distances, indexes = kn.kneighbors([[25, 150]])
plt.scatter(train_input[:,0], train_input[:,1])
plt.scatter(25, 150, marker='^') # marker 매개변수는 모양을 지정합니다.
plt.scatter(train_input[indexes, 0], train_input[indexes, 1], marker='D')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

직접 데이터를 확인해보자.
print(train_input[indexes])
>>>
[[[ 25.4 242. ]
[ 15. 19.9]
[ 14.3 19.7]
[ 13. 12.2]
[ 12.2 12.2]]]
print(train_target[indexes])
>>>[[1. 0. 0. 0. 0.]]
print(distances)
>>>[[ 92.00086956 130.48375378 130.73859415 138.32150953 138.39320793]]
산점도로 보면 도미의 데이터가 가까워보이지만 거리를 보게 되면 빙어의 데이터가 가깝다.
이는 x축 의 범위는 좁고 y축의 범위가 넓은 것을 확인할 수 있다.
따라서 y축으로 조금만 멀어져도 거리가 아주 큰 값으로 계산된다.
xlim() 함수를 통해 x축 범위를 0~1000으로 지정해보자. (
plt.scatter(train_input[:,0], train_input[:,1])
plt.scatter(25, 150, marker='^') # marker 매개변수는 모양을 지정합니다.
plt.scatter(train_input[indexes, 0], train_input[indexes, 1], marker='D')
plt.xlim((0, 1000)) # x_scale을 맞춰보자
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

위의 그림을 보다 싶이 두 특성(길이, 무게)의 값이 놓인 범위가 매우 다르다.
이를 두 특성의 스케일(scale)이 다르다라고도 말한다.
알고리즘들은 샘플 간의 거리에 영향을 많이 받으므로 특성값이 일정한 기준으로 맞춰야 한다.
그로 인해, 데이터 전처리 과정이 필요하다.
가장 널리 사용하는 전처리 방법 중 하나는 표준 점수(standard score or z 점수) 이다.
표준 점수는 각 특성값이 평균에서 표준편차의 몇 배만큼 떨어져 있는지를 나타낸다.
이를 통해 실제 특성값의 크기와 상관없이 동일한 조건으로 비교할 수 있다.
표준 점수 = (특성 값 - 평균) / 표준편차

# 표준점수 방법
mean = np.mean(train_input, axis=0)
std = np.std(train_input, axis=0)
train_scaled = (train_input - mean) / std
넘파이는 train_input의 모든 행에서 mean에 있는 두 평균값을 빼준다음 std에 있는 두 표준편차를 다시 모든 행에 적용시킨다. (알아서 - 브로드캐스팅 기능)
브로드 캐스팅은 넘파이 배열 사이에서 일어난다. train_input, mean, std가 모두 넘파이 배열

plt.scatter(train_scaled[:,0], train_scaled[:,1])
plt.scatter(25, 150, marker='^') # marker 매개변수는 모양을 지정합니다.
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

앞서 전처리를 하였기 때문에 새로운 데이터도 전처리하여 비교해야한다.
new = ([25, 150] - mean) / std
plt.scatter(train_scaled[:,0], train_scaled[:,1])
plt.scatter(new[0], new[1], marker='^') # marker 매개변수는 모양을 지정합니다.
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

이제 다시 훈련 시켜보자.
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)로 잘 인식한다.
마지막으로 가장 가까운 5개의 이웃을 한 번 확인해보자.
distacnes, 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()

샘플의 특성의 스케일이 다른 경우는 매우 많다.
이에 따라 모델을 훈련하기 전에 항상 데이터 전처리 과정을 수행해야 한다.
우리는 이를 위해 특성을 표준점수로 변환하였다.
이를 통해 데이터의 스케일을 동일하게 맞춰 올바르게 결과를 출력하는 것을 확인하였다.
JeongeunBae의 Github에서 전체 코드를 확인하실 수 있습니다. 👻
https://github.com/JeongEunBae/TIL/blob/main/Basic_ML_DL%20(%ED%98%BC%EA%B3%B5%EB%A8%B8%EC%8B%A0)/2_2_%EB%8D%B0%EC%9D%B4%ED%84%B0_%EC%A0%84%EC%B2%98%EB%A6%AC.ipynb