[혼공머신] 1주차 02-2 데이터 전처리
본 게시글은 한빛미디어의 혼자 공부하는 머신러닝+딥러닝을 바탕으로 작성되었습니다.
Numpy를 이용한 data 생성
우선, 지난번에 썼던 도미와 빙어 길이, 무게 data를 가져와 보자. 이번에는 numpy를 적극적으로 활용해서 데이터를 정리해 볼 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
import numpy as np
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]
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]
X = np.column_stack((length, weight))
y = np.concatenate((np.ones(35), np.zeros(14)))
이렇게 하면 지난번에 했던 것과 동일하게 input data와 target data를 생성할 수 있다. 교재의 말의 의하면 더욱 세련되게? 할 수 있는 방법이다. 개인적으로 pandas의 dataframe만 주야장천 쓰다 보니 numpy의 array에 조금 소홀했던 것 같다는 느낌이 약간 들었다. 나중에 시간이 되면 numpy만 따로 공부를 해보는 것도 괜찮을 것 같다는 생각이 들었다.
train_test_split을 이용한 trian/test data 생성
이제 지난 게시글에서 언급했던 train_test_split을 사용해 볼 것이다. 우리의 위대한 scikit-learn은 training data와 test data를 나누기 위한 함수로 train_test_split를 제공한다. 이것은 데이터 분석을 하면서 계속 사용하게 될 것이기에 외워야 한다.
1
2
3
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
이렇게 함수를 설정하면 전체 데이터의 25%를 test data로 분리하고, 나머지 75%를 training_data로 분리한다. 이때, 랜덤으로 데이터를 뽑아주기는 하지만, 랜덤이란 것은 예전에 했던 것과 같이 전부 도미 데이터만 뽑힐 수도 있다는 것이다. 그렇기 때문에 이를 방지하기 위해서 특정 class의 비율이 일정하게 되도록 뽑으라고 아래와 같이 stratify를 통해 y에 대해서 일정 비율로 뽑히도록 설정을 할 수도 있다.
1
2
3
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=0)
KNN Classification
이제 이렇게 한 것을 바탕으로, 지난번과 같이 KNN 알고리즘을 통해 분류를 해보도록 하자.
1
2
3
4
from sklearn.neighbors import KNeighborsClassifier
knn = KNeighborsClassifier()
knn.fit(X_train, y_train)
knn.score(X_test, y_test)
1.0
우선, 우리가 이미 가지고 있는 데이터로는 지난번과같이 잘 학습하는 것을 알 수 있다. 그럼, 이제 새로운 물고기를 가져와서 해보자. 한빛미디어는 우리를 위해서 생선을 하나 준비해 줬다고 한다. 그 물고기의 길이와 무게는 25, 150이다. 상식적으로 생각해 봤을 때, 이 정도는 도미일 것 같다. 빙어 축제 같은 곳에서 25cm짜리 빙어가 돌아다닌다고 생각하면 조금 부담스러울 것 같기도 하다.
1
knn.predict([[25, 150]])
array([0.])
우리의 컴퓨터는 이 물고기를 빙어라고 판단했다. 그러면 우리는 아, 이건 빙어구나 라고 생각을 하면 될까? 아쉽게도 그건 아니다. 물고기를 준 한빛미디어는 이 물고기가 도미일 것이라고 알려주었기 때문이다. 문제가 발생했는데 이유를 모르겠으면 우리는 또다시 graphical 하게 보는 수밖에 없다. 우선, 그래프를 그려보자.
Visualization
1
2
3
4
5
6
7
import matplotlib.pyplot as plt
plt.scatter(X_train[:, 0], X_train[:, 1])
plt.scatter(25, 150, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show
이렇게 그래프를 그려보았는데, 그래도 이해가 가지 않을 수 있다. 분명 KNN 알고리즘은 가장 가까운 5개의 포인트를 찾아서 그중에 많은 분류에 데이터를 할당한다고 배웠었다. 그런데 그래프를 보니 도미의 데이터에 더 가깝다고 생각이 되기 때문이다. 컴퓨터가 맛이 간 것일까? 한 번 컴퓨터에게 소명의 기회를 주고자, 어떤 5개의 데이터 포인트를 찾았는지 확인하자.
1
2
3
4
5
6
7
8
distance, indexes = knn.kneighbors([[25, 150]])
plt.scatter(X_train[:, 0], X_train[:, 1])
plt.scatter(25, 150, marker='^')
plt.scatter(X_train[indexes, 0], X_train[indexes, 1], marker='D')
plt.xlabel('length')
plt.ylabel('weight')
plt.show
믿을 수 없는 결과가 나왔다. 바로 위에 예쁘게 나열되어 있는 점들을 버리고, 저 왼쪽 구석에 멀리 있는 점들을 컴퓨터는 더 가깝다고 생각한 것이다. 우리가 컴퓨터를 바꿀 기회가 온 것일까? 아니면 안경을 써야 할까? 원래 가위바위보도 삼세판은 해야 하므로 컴퓨터에게 소명 기회를 마지막으로 한 번 더 주도록 하자. 우리가 설정했던 distance 변수를 출력하라고 해보자.
1
distance
array([[ 92.00086956, 130.48375378, 130.73859415, 137.17988191, 138.32150953]])
무언가가 이상하다. 가장 가까운 것의 거리가 92이고, 나머지가 130이 넘는다니 무언가 잘못된 것일 것이다. 그렇지만 아쉽게도 그런 것은 아니다. 우리는 한 가지 생각하지 않은 것이 있다. 예전에 게시글에서 잠깐 언급하려다가 만 것이 있다. 바로 단위이다. KNN 알고리즘은 점과 점 사이의 거리를 측정해서 분류를 하는데, 우리가 흔히 알고 있는 그 점과 점 사이의 거리를 계산하는 공식을 사용한다. 혹시라도 까먹을 수 있으니, 아래에 이쁘게 적어두었다.
$d=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2}$
가로축을 살펴보면 단위가 5씩 증가한다. 그런데 세로축을 보자. 세로축은 200씩 늘어난다. 따라서 가로축의 거리가 길어 보이더라도 세로축의 차이와는 비교가 되지 않을 정도로 작게 되는 것이다. 그렇기에 왼쪽에 몰려있는 점들이 더 가깝게 측정이 된 것이다. 그렇다, 우리의 컴퓨터는 거짓말을 하지 않는다. 문제는 우리의 실력뿐이다.
그렇게 된다면 우리는 어떻게 해야 할까? 이런 것도 다 방법이 있다. 우리는 이러한 것을 데이터 스케일링(Data Scaling)이라고 한다. 데이터의 스케일, 즉 값의 범위를 맞추는 과정이다. 이제 이것을 다시 한번 살펴보자.
데이터 스케일링
세로축의 데이터 자체를 변경시키는 방법도 있긴 하지만, 이것이 아직 어려운 사람도 있을 것이다. 그렇기에 일단은 데이터의 값을 변경시키기보다는 가로축의 범위를 바꾸는 방향으로 한번 해보자.
1
2
3
4
5
6
7
plt.scatter(X_train[:, 0], X_train[:, 1])
plt.scatter(25, 150, marker='^')
plt.scatter(X_train[indexes, 0], X_train[indexes, 1], marker='D')
plt.xlim(0, 800)
plt.xlabel('length')
plt.ylabel('weight')
plt.show
가로축의 범위를 세로축의 범위와 비슷하게 800으로 했더니, 그래프의 형태가 바뀌었다. 거의 일직선이 되어버렸다. 이렇게 보면 우리의 컴퓨터가 왜 도미가 아닌 빙어라고 결정을 내렸는지 알 수 있을 것이다.
그런데 아까 data scaling이라고 했는데, 이게 data scaling일까? 그건 아니다. 우리는 값의 범위를 보여주는 정도만 바꾼 것이지, 값의 범위를 바꾸지는 않았다. 정확히 말하면 아직까지도 둘의 scale이 다른 것이다. 그런데 이제 어쩌라는 것인지 물어보는 사람도 있을 것이다. 이제 그것을 알려줄 것이다. 우리가 말하는 알고리즘은 문제를 해결하는 방식이 다 다르다. 그렇기 때문에 컴퓨터공학과에서 알고리즘이라는 한 학기짜리 강의로 개설되어 있는 것이다. 만약 하나만 있다면 그 고생을 해서 한 학기 동안 배울 필요도 없을 것이다.
우리가 이제까지 사용한 KNN 알고리즘은 거리를 기반으로 classification을 진행한다. 그러면 단위, 즉 스케일에 민감하게 반응할 수밖에 없다. 만약 똑같이 길이를 기준으로 한다고 해도, 그래프의 가로축이 cm, 세로축이 mm라면 세로축의 변화에 더 민감하게 반응할 수밖에 없을 것이다.
그럼 우리는 어떻게 이 문제를 해결할 수 있을까? 우리의 위대한 통계가 나설 차례이다. 통계를 하면서 지겹도록 보게 되는 표준화를 활용하는 것인데, 여기서는 표준점수(Standard Score)를 활용한다. 이것에 대한 자세한 설명이 궁금하다면 통계학 강의를 들어보면 된다. 대략 standard score를 설명하자면, 각 특성값이 평균에서 표준편차의 몇 배만큼 떨어져 있는지 나타내는 것으로, 이것을 사용하면 실제 값의 scale의 영향을 없애고, 동일한 scale로 비교할 수가 있게 된다. 계산식은 아래와 같다.
$Z=\frac{x-\bar{x}}{s}$
하지만 우리는 이걸 손으로 계산하는 것이 아니라, 컴퓨터의 도움을 받아 해결할 것이기에 참고용으로 보면 된다. 이제 코드로 작성해 보자.
1
2
3
4
mean = np.mean(X_train, axis=0)
std = np.std(X_train, axis=0)
X_scaled = (X_train-mean)/std
이렇게 우리는 standard score로 standardize 한 값을 구할 수 있게 된다. 이제 이것을 활용해서 다시 우리의 도미와 빙어를 살펴보도록 하자. 이때, 조심해야 할 것은 이 과정을 새로운 데이터에도 적용해야 제대로 보인다는 것이다. scaling을 해서 값이 -1.5에서 1.5 사이로 대충 정리가 되었는데 갑자기 150g짜리 물고기를 넣어버리면 scaling이 의미가 없어진다.
1
2
3
4
5
6
7
8
9
10
11
mean = np.mean(X_train, axis=0)
std = np.std(X_train, axis=0)
X_train_scaled = (X_train-mean)/std
new = ([25, 150]-mean)/std
plt.scatter(X_train_scaled[:, 0], X_train_scaled[:, 1])
plt.scatter(new[0], new[1], marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show
이제 가로축과 세로축의 scale이 잘 정리된 것을 확인할 수 있다. 이제 이것을 바탕으로 다시 학습을 진행해 볼 것이다. 그런데 이때, test data에도 마찬가지로 똑같이 scaling을 적용해야 한다.
1
2
3
4
5
6
7
8
9
10
11
X_test_scaled = (X_test-mean)/std
knn.fit(X_train_scaled, y_train)
distance, indexes = knn.kneighbors([new])
plt.scatter(X_train_scaled[:, 0], X_train_scaled[:, 1])
plt.scatter(new[0], new[1], marker='^')
plt.scatter(X_train_scaled[indexes, 0], X_train_scaled[indexes, 1], marker='D')
plt.xlabel('length')
plt.ylabel('weight')
plt.show
드디어 우리가 예상했던 결과가 나왔다. 제일 가까운 5개의 점이 다 도미에 있게 되었다. 결과는 이제 안 봐도 도미가 될 것이다. 이렇게 우리는 데이터를 단순히 학습만 시키는 것이 아니라, 데이터에 대한 사전 지식 또한 필요하다는 것을 데이터의 scale을 통해 알 수 있다.