이 포스팅은 혼자 공부하는 머신러닝 + 딥러닝 책을 공부하고 정리한 것 입니다.
앞에서 만든 머신러닝 모델을 25cm,250g으로 테스트를 하면 도미가 아니라 빙어로 나온다. 그 이유는 무엇일까?
넘파이로 데이터 준비하기
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
fish_data=np.column_stack((fish_length,fish_weight))
넘파이의 columm_stack() 함수는 전달받은 리스트를 일렬로 세운 다음 차례대로 나란히 연결한다. 연결할 리스트는 파이썬 튜플로 전달한다.
다음과 같은 방법으로 쉽게 타겟 데이터를 생성할 수 있다.
fish_target=np.concatenate((np.ones(35),np.zeros(14)))
print(fish_target)
사이킷런으로 훈련 세트와 테스트 세트 나누기
앞에서는 넘파이 배열의 인덱스를 직접 섞어서 훈련 세트와 테스트 세트로 나누었다. 사실 이 방법은 조금 번거롭다. 이번에는 좀 더 세련된 방법을 사용해 보겠다.
사이킷런은 머신러닝 모델을 위한 알고리즘뿐만 아니라 다양한 유틸리티 도구도 제공한다.
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)
print(train_input.shape,test_input.shape)
fish_data 와 fish_target 2개의 배열을 전달했으므로 2개씩 나뉘어 총 4개의 배열이 반환된다.
print(train_target.shape,test_target.shape)
도미와 빙어가 잘 섞였는지 테스트 데이터를 출력해보면
print(test_target)
다음과 같은데 이는 모집단의 비율인 2.5:1 보다 더 편향된 3.3:1 이다. 이처럼 무작위로 데이터를 나누었을 때 샘플이 골고루 섞이지 않을 수 있다.
이러한 현상은 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)
빙어가 하나 늘어 테스트 세트 비율이 2.25:1 이 되었다. 이 예제는 데이터가 작아 전체 훈련 데이터의 비율과 동일하게 맞출 수 없지만 꽤 비슷한 비율이다.
수상한 도미 한마리
앞에서 준비한 데이터로 k-최근접 이웃을 훈련해 본다.
from sklearn.neighbors import KNeighborsClassifier
kn=KNeighborsClassifier()
kn.fit(train_input, train_target)
kn.score(test_input,test_target)
완벽한 결과이다. 테스트 세트의 도미와 빙어를 모두 올바르게 분류했다. 이 모델에 다른 도미 데이터를 넣고 결과를 확인해 보면 당연히 도미(1)이 나와야 할 것이다.
print(kn.predict([[25,150]]))
그러나 0이 나왔다. 왜 그럴까? 산점도를 그려보면
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()
이 샘플은(삼각형) 분명히 오른쪽 위로 뻗어 있는 다른 도미 데이터에 더 가깝다. 왜 이 모델은 왼쪽 아래에 낮게 깔린 빙어 데이터에 가깝다고 했을까?
k-최근접 이웃은 주변의 샘플 중에서 다수인 클래스를 예측으로 사용한다. KNeighborsClassifier 클래스는 주어진 샘플에서 가장 가까운 이웃을 찾아주는 kneighbors() 메서드를 제공한다. 이 메서드는 이웃까지의 거리와 이웃 샘플의 인덱스를 반환 한다. KNeighborsClassifier 클래스의 이웃 개수인 n_neighbors의 기본값은 5이므로 5개의 이웃이 반환된다.
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') #'D' = 마름모
plt.xlabel('length')
plt.ylabel('weight')
plt.show()
가장 가까운 이웃에 도미가 하나밖에 포함되지 않았다. 나머지 4개의 샘플은 모두 빙어이다.
print(train_input[indexes])
print(train_target[indexes])
길이가 25cm, 무게가 150g인 생선에 가장 가까운 이웃에는 빙어가 압도적으로 많았다. 따라서 이 샘플의 클래스를 빙어로 예측하는 것은 무리가 아니다. 왜 가장 가까운 이웃을 빙어라고 생각한 것일까? distances 배열을 출력해 보자 이 배열에는 이웃 샘플까지의 거리가 담겨 있다.
print(distances)
기준을 맞춰라
x축은 범위가 좁고 y축은 범위가 넓다. 따라서 y축으로 조금만 멀어져도 거리가 아주 큰 값으로 계산된다. 이 때문에 오른쪽 위의 도미 샘플이 이웃으로 선택되지 못한것이다.
이를 눈으로 명확히 확인하기 위해 x축의 범위를 동일하게 0~1000으로 맞추어 본다.
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') #'D' = 마름모
plt.xlim((0,1000))
plt.xlabel('length')
plt.ylabel('weight')
plt.show()
두 특성(길이와 무게)의 값이 놓인 범위가 매우 다르다. 이를 두 특성의 스케일(scale)이 다르다고도 말한다. 데이터를 표현하는 기준이 다르면 알고리즘이 올바르게 예측할 수 없다. 알고리즘이 거리 기반일 때 특히 그렇다. 여기에는 k-최근접 이웃도 포함된다. 이런 알고리즘들은 샘플 간의 거리에 영향을 많이 받으므로 제대로 사용하려면 특성값을 일정한 기준으로 맞춰주어야 한다. 이런 작업을 데이터전처리 라고 부른다.
가장 널리 사용하는 저처리 방법 중 하나는 표준점수(standard score) 이다. 표준점수는 각 특성값이 0에서 표준편차의 몇 배 만큼 떨어져 있는지를 나타낸다. 이를 통해 실제 특성값의 크기와 상관없이 동일한 조건으로 비교할 수 있다
.
mean=np.mean(train_input,axis=0) #평균 계산
std=np.std(train_input,axis=0) #표준편차 계산 (분산의 제곱근)
print(mean,std)
특성마다 값의 스케일이 다르므로 평균과 표준편차는 각 특성별로 계산해야 한다. 이를 위해 axis=0으로 지정한다.
train_scaled=(train_input - mean) / std #표준점수=원본 - 평균 / 표준편차
plt.scatter(train_scaled[:,0],train_scaled[:,1])
plt.scatter(25,150,marker='^')
plt.xlabel('length')
plt.ylabel('weigth')
plt.show()
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('weigth')
plt.show()
테스트 세트도 훈련 세트의 평균과 표준편차로 변환해야 한다.
kn.fit(train_scaled,train_target)
test_scaled=(test_input-mean)/std
kn.score(test_scaled,test_target)
print(kn.predict([new]))
드디어 도미(1) 로 예측했다. 마지막으로 kneighbors() 함수로 이 샘플의 k-최근접 이웃을 구한 다음 산점도로 그려보겠다. 특성을 표준점수로 바꾸었기 때문에 k-최근접 이웃 알고리즘이 올바르게 거리를 측정했을 것이다. 이로 인해 가장 가까운 이웃게 변화가 생겼을 것으로 기대할 수 있다.
distance,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('weigth')
plt.show()
결론
새로운 테스트셋을 이전 머신러닝 모델에 예측을 했더니 엉뚱한 값이 나왔다. 이는 샘플의 두 특성인 길이와 무게의 스케일이 다르기 때문이다. 길이보다 무게의 크게이 따라 예측값이 좌지우지 되었다. 대부분의 머신러닝 알고리즘은 특성의 스케일이 다르면 잘 작동하지 않는다.
이를 위해 특성을 표준점수로 변환했다. 데이터를 전처리할 때 주의할 점은 훈련 세트를 변환한 방식 그대로 테스트 세트를 변환해야 한다는 것이다.
'Machine Learning > Basic' 카테고리의 다른 글
[3-3] 특성 공학과 규제 (0) | 2021.04.06 |
---|---|
[3-2] 선형 회귀 (0) | 2021.04.05 |
[3-1] k-최근접 이웃 회귀 (0) | 2021.04.05 |
[2-1] 훈련 세트와 테스트 세트 (0) | 2021.04.03 |
[1] 머신러닝 맛보기 - 마켓과 머신러닝 (0) | 2021.04.03 |