[혼공머신러닝] Ch 2. 데이터 다루기(2)
앞선 머신러닝 알고리즘에서 길이 25센치에 무게 150g의 도미를 빙어로 예측한다는 이상한 결과가 나왔습니다. 이를 보완하기 위해 우선 앞선 데이터를 다시 불러오겠습니다.
#fish의 길이와 무게에 대한 데이터
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]
numpy의 column_stack() 함수는 전달받은 리스트를 일렬로 세운 차례대로 연결해주는 함수입니다.
이 함수를 사용하여 fish_length와 fish_weight를 합쳐보겠습니다.
import numpy as np
fish_data = np.column_stack((fish_length, fish_weight))
print(fish_data[:5])
앞에서부터 5개의 데이터를 확인함으로써 두 리스트가 잘 연결된 것을 확인했습니다. 동일한 방법으로 타깃 데이터도 만들어보겠습니다. 넘파이에서는 np.ones() 와 np.zeros() 함수를 이용하여 배열을 만들 수 있습니다. 또한 np.concatenate()함수를 이용하여 두 배열을 연결할 수 있습니다.
fish_target = np.concatenate((np.ones(35), np.zeros(14)))
print(fish_target)
fish_target을 만들었으니 훈련세트와 테스트세트로 나눠보겠습니다. 앞선 방식과 다르게 사이킷런의 model_selection 모듈 아래 있는 train_test_split() 함수를 이용합니다.
from sklearn.model_selection import train_test_split
train_test_split() 함수에는 자체적으로 랜덤 시드를 지정할 수 있는 random_state 매개변수가 존재하니, 사용해서 나눠보겠습니다.
train_input, test_input, train_target, test_target = train_test_split(
fish_data, fish_target, random_state = 42
)
train_test_split() 함수는 기본적으로 25%를 테스트 세트로 분류합니다. 잘 나누어졌는지 shape 속성으로 확인해보겠습니다.
print(train_input.shape, test_input.shape)
print(train_target.shape, test_target.shape)
이제 도미와 방어가 적절히 섞였는지 test_target을 출력하여 확인합니다.
print(test_target)
도미와 방어가 섞이기는 했지만 두 생선의 비율에 비해 샘플링 편향이 일어났습니다. 따라서 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)
조금 더 전체 비율과 비슷한 비율로 나누었습니다. 이 데이터를 가지고 앞서 진행했던 모델 훈련과 평가를 해보도록 하겠습니다.
from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier()
kn.fit(train_input, train_target)
kn.score(test_input, test_target)
이제 이 모델에 문제의 도미인 25cm에 150g의 물고기를 입력하여 예측값을 확인합니다. 1은 도미, 0은 빙어입니다.
print(kn.predict([[25, 150]]))
도미가 빙어로 출력된 것을 볼 수 있습니다. 이를 산점도로 그려 확인해봅니다.
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 클래스는 주어진 샘플에서 가장 가까운 이웃 5개 중 다수인 클래스를 예측으로 사용합니다.
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') # marker = 'D' : 산점도 마름모로 그리기
plt.xlabel('length')
plt.ylabel('weight')
plt.show()
산점도로 확인해보니 5개중 하나만 도미고, 4개의 데이터가 빙어입니다.
print(train_input[indexes])
print(train_target[indexes])
직접 데이터를 확인해 본 결과, 더 명확히 1개만 도미고 나머지 이웃은 빙어임을 알 수 있습니다.
이번엔 kneighbors() 매서드에서 이웃 샘플까지의 거리를 알 수 있는 distances 배열을 출력해보면,
산점도에서는 도미보다 빙어가 훨씬 많이 떨어져있는데, distances 배열에서는 그렇지 않은 것을 알 수 있습니다.
거리와 산점도가 다르게 나온 이유는 x축과 y축의 scale이 다르기 때문입니다.
length는 10부터 40이 주 범위인 반면에 weight는 0부터 1000까지의 넓은 범위를 갖고 있습니다.
x축의 scale을 조정하여 범위를 동일하게 맞추고 산점도를 다시 그려봅니다.
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('length')
plt.ylabel('weight')
plt.show()
x축의 scale을 조정하여 범위를 동일하게 맞추면, 다음과 같이 산점도가 나오게 됩니다.
이러한 데이터라면 length는 weight에 비해 크게 영향을 미치지 못하게 됩니다.
따라서 두 특성의 범위가 다른 것을 고려하여, 특성값을 일정한 기준으로 맞춰주어야 하고 이런 작업을 데이터를 전처리한다고 말합니다.
이럴 때 가장 흔하게 사용되는 방법은 표준점수로, 각 특정 값이 0에서 표준편차의 몇 배만큼 떨어져있는지를 나타냅니다.
이를 이용하면 실제 특성 값의 크기와 상관 없이 동일한 조건으로 비교할 수 있습니다.
각 특성별로 계산해야하기 때문에 mean과 std를 axis = 0으로 지정하고 계산해보겠습니다.
mean = np.mean(train_input, axis = 0)
std = np.std(train_input, axis = 0)
print(mean, std)
이제 train_input 데이터에서 평균을 빼고 표준편차로 나누어 표준 점수를 구합니다.
train_scaled = (train_input - mean)/std
plt.scatter(train_scaled[:,0], train_scaled[:,1])
plt.scatter(25, 150, marker = '^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()
다시 산점도를 그려봤을 때 이런 결과가 나온 것은 데이터는 전부 표준점수화 시켜두고 25, 150의 샘플은 그대로 입력했기 때문입니다.
샘플 데이터인 25, 150도 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()
산점도도 잘 그려지고 범위도 표준점수화 된 것을 확인했으니 모델을 다시 훈련합니다.
kn.fit(train_scaled, train_target)
test_scaled = (test_input - mean)/std #test dataset도 표준점수화하기
kn.score(test_scaled, test_target)
이제 앞서 잘못 분류했던 데이터를 predict 해보겠습니다.
print(kn.predict([new])) # 1 = 도미, 0 = 빙어
결과값은 1로, 도미로 예측하였습니다.
마지막으로 kneighbors() 함수를 사용하여 근접한 이웃을 구하는 산점도를 그려봅니다.
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()
이제 샘플에서 가까운 점들은 모두 도미로 옳게 출력이 된 것을 확인할 수 있습니다.
Comments