3. 데이터 전처리

1. 들어가며

  • 기존에는 파이썬 리스트를 이용해 원소를 하나씩 꺼내 리스트에 추가해주는 형식을 사용했으나 넘파이를 이용하면 손쉽게 만들 수 있다.
  • column_stack()을 이용하면 리스트를 일렬로 세운 다음 차례대로 나란히 연결할 수 있다.
import numpy as np
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
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]
  • 필요한 라이브러리와 데이터를 임포트 해준다.
np.column_stack(([1,2,3], [4,5,6]))
array([[1, 4],
       [2, 5],
       [3, 6]])
fish_data = np.column_stack((fish_length, fish_weight))
fish_data[:5]
array([[ 25.4, 242. ],
       [ 26.3, 290. ],
       [ 26.5, 340. ],
       [ 29. , 363. ],
       [ 29. , 430. ]])
  • 물고기의 길이와 무게를 column_stack()을 이용해 적재하고 제대로 적재되었는지 5개를 불러 확인해본다.
  • 이후 fish_data.shape 를 통해 49개의 데이터가 두개의 컬럼으로 합쳐진것을 확인 할 수 있다.
fish_data.shape
(49, 2)
print(np.ones(5))
[1. 1. 1. 1. 1.]
fish_target = np.concatenate((np.ones(35), np.zeros(14)))
fish_target
array([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.])
  • 이후 입력값 외에도 타깃값을 생성해주기 위해 np.concatenate()와 np.ones(), np.zeros()를 이용해 각각의 타깃값을 정하고 합쳐준다.
train_input, test_input, train_target, test_target = train_test_split(fish_data, fish_target, stratify=fish_target, random_state=42) # stratify 매개변수에 타깃 데이터를 전달하면 클래스 비율에 맞게 데이터를 나눕니다.
print(train_input.shape, test_input.shape)
(36, 2) (13, 2)
print(train_target.shape, test_target.shape)
(36,) (13,)
print(test_target)
[0. 0. 1. 0. 1. 0. 1. 1. 1. 1. 1. 1. 1.]
  • 이후 사이킷런의 train_test_split을 이용해 입력값과 타깃값을 훈련셋과 테스트셋으로 분할해준다.
kn = KNeighborsClassifier()
kn.fit(train_input, train_target)
kn.score(test_input, test_target)
1.0
print(kn.predict([[25,150]]))
[0.]
plt.scatter(train_input[:,0], train_input[:,1])
plt.scatter(25, 150, marker='^')
plt.xlabel('lenght')
plt.ylabel('weight')
plt.show()

output_20_0

  • 이후 훈련 데이터 값을 이용해 산점도를 그려보고 임의 물고기인 [25,150]의 크기를 가진 물고기를 kn.predict()를 가지고 예측을 해본뒤 확인을 위해 그래프에도 그려본다.
  • 하지만 결과는 빙어로 예측했다. 그래프를 살펴보면, 빙어보다는 도미에 가깝다는 것을 알 수 있으며 이는 잘못 예측이 되었다는것을 알 수 있다.
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('lenght')
plt.ylabel('weight')
plt.show()

output_23_0

  • 잘못 예측된 물고기를 index배열을 사용해 훈련 데잍 중에서 이웃 샘플을 따로 구분해서 그래프를 그려보았다.
  • 여기서 확인할 수 있는 것은 도미가 하나 밖에 포함되지 않았다는 것이다. 하지만 직관적으로 바라보았을때 도미에 더 가깝다는 것을 알 수 있다.
  • 무게와 길이의 단위가 맞지 않아 단위가 작은 길이 쪽의 빙어가 더 가깝게 예측되었다는것을 알 수 있다. 이는 스케일이 다르기 때문에 나타나는 문제로 볼 수 있었다.
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]]

2. 스케일을 조정하고 표준점수화를 진행하다.

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))
plt.xlabel('lenght')
plt.ylabel('weight')
plt.show()

output_29_0

  • x축의 스케일을 조정하고 나니 산점도가 거의 일직선으로 나타나는 형태를 보였다.
  • 이를 통해 알 수 있는 것은 x축인 길이는 가장 가까운 이웃을 찾는 데 크게 영향을 미치지 못한다는 것이다. 오로지 y축의 무게만이 고려대상이 된다는 것을 알 수 있다.
  • 알고리즘이 거리 기반일 때 샘플간의 거리에 영향을 많이 받기 때문에 제대로 사용하기 위해서는 특성값을 일정한 기준으로 맞춰주어야 한다. 이런 작업을 데이터 전처리라고 한다.
  • 가장 널리 사용되는 방법은 표준점수이다. 표준점수는 각 특성값이 0에서 표준편차의 몇 배만큼 떨어져 있는지를 나타낸다. 이를 통해 실제 특성값의 크기와 상관없이 동일한 조건으로 비교가 가능해진다.
mean = np.mean(train_input, axis=0)
std = np.std(train_input, axis=0)
  • 표준점수를 구하기 위해 훈련데이터의 입력값의 평균과 표준편차를 구하고 해당 값에서 평균값을 뺀 뒤 표준편차로 나누어 표준점수를 구한다.
print(mean, std)
[ 27.29722222 454.09722222] [  9.98244253 323.29893931]
train_scaled = (train_input - mean) / std
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()

output_35_0

  • 스케일링된 입력값을 그래프로 다시 표현하면 다음과 같다. 이제 두 축의 스케일이 비슷해진것을 알 수 있다.
kn.fit(train_scaled, train_target)
KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',
                     metric_params=None, n_jobs=None, n_neighbors=5, p=2,
                     weights='uniform')
  • 다시 데이터를 적재하고 훈련한다.
  • 여기서 주의 해야할 것은 훈련 세트 뿐 아니라 테스트 세트의 값과 샘플값 또한 훈련세트의 입력값을 통해 구한 평균과 표준편차로 나누어 동일 비율로 변환을 해줘야 한다.
test_scaled = (test_input - mean) / std
kn.score(test_scaled, test_target)
1.0
  • 예상점수는 정확하게 예측했고, 이전과 달리 도미로 예측하는 모습을 살펴볼 수 있었다. (하단)
kn.predict([new])
array([1.])
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()

output_43_0

  • 아까와는 다르게 최근접 이웃을 찾으면 전부 도미에 가깝게 정확히 분류했다는 것을 알 수 있다.

3. 마치며

  • 이번시간에는 기존에 이용한 데이터를 통해 특정 샘플을 분류하는 문제를 해결해 보았다. 하지만 정확히 예측되지 않았고, 이를 해결하기 위해 데이터 전처리 과정을 거쳐야 했다. 데이터 전처리 과정에서 x축과 y축 둘다 분류의 고려대상이 되지 못하는 문제점을 발견했고, 스케일링을 통해 두 축의 스케일을 표준점수를 이용해 동일하게 만들어 주었더니, 분류 결과가 달라졌고 정확도도 높아지는 결과를 낳았다.
  • 새로운 내용을 보다 알기 쉽게 이해할 수 있는 시간이였다.

Categories:

Updated:

Leave a comment