혼자 공부하는 머신러닝 + 딥러닝

혼자 공부하는 머신러닝 + 딥러닝 Day 3

루바의 여정 2022. 8. 24. 21:45

지난 시간...

지난 시간에는 데이터를 훈련 세트와 테스트 세트로 나누고 지도 학습으로 모델을 구현해보는 시간을 가졌었다.

그 과정에서 shuffle() 함수를 사용하여 샘플링 편향을 해결하는 방법 또한 알아보았다.

오늘 시간에는 올바른 결과 도출을 위한 데이터 전처리 과정을 알아보는 시간을 가져보도록 하겠다.

 

넘파이로 데이터 준비하기

* 데이터가 아주 큰 경우에는 파이썬 리스트로 작업하는 것은 비효율적이므로 항상 넘파이 배열을 사용하여 진행하자 *

 

넘파이의 column_stack() 함수는 전달받은 리스트를 일렬로 세운 다음 차례대로 나란히 연결한다.

이 함수를 사용하면 행과 열을 맞추어 가지런히 정리된 모습으로 합쳐진다.

 

예시)

np.column_stack(([1,2,3], [4,5,6]))
## 출력값
array([[1, 4],
       [2, 5],
       [3, 6]])

모델 훈련에 사용할 데이터인 fish_length와 fish_weight를 column_stack() 함수를 사용하여 합쳐보자.

fish_data = np.column_stack((fish_length, fish_weight))

다음은 타깃 데이터를 만들어보자.

np.ones(): 원하는 개수의 1을 채운 배열을 만들어준다.

np.zeros(): 원하는 개수의 0을 채운 배열을 만들어준다.

위의 넘파이 함수를 이용해 1이 35개인 배열과 0이 14개인 배열을 합쳐 타깃 리스트를 만들어보자

여기에서는 첫 번째 차원을 따라 배열을 연결하는 np.concatenate() 함수를 사용해보도록 하자

fish_target = np.concatenate((np.ones(35), np.zeros(14)))
## fish_target 값 출력하기
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.]

 

훈련 세트와 테스트 세트 나누기

지난 시간에는 넘파이 배열의 인덱스를 직접 섞어서 훈련 세트와 테스트 세트를 분류했다.

이번에는 사이킷런의 model.selection 모듈 아래의 train_test_split() 함수를 이용하여 훈련 세트와 테스트 세트를 분류해보자.

from sklearn.model_selection import train_test_split

## random_state 매개변수로 랜덤 시드를 지정
## stratify를 매개변수에 타깃 데이터를 전달하면 클래스 비율에 맞게 데이터를 나눈다. 
## -> 훈련 데이터가 작거나 특정 클래스의 샘플 개수가 적을 때 특히 유용하다.
train_input, test_input, train_target, test_target = train_test_split(
fish_data, fish_target, stratify=fish_target, random_state=42)

stratify 매개변수에 타깃 데이터를 전달하면 클래스 비율에 맞게 데이터를 나눠준다. 이를 지정하지 않으면 샘플링 편향이 나타날 수 있다.

 

머신러닝 프로그램

위의 준비한 데이터로 k-최근접 이웃 모델을 구현해보자.

from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier()
kn.fit(train_input, train_target)
kn.score(test_input, test_target)

## 출력값
1.0

정확도가 1.0으로 아주 완벽하게 분류되었다. 이제 25cm, 150g의 도미 데이터로 결과를 예측해보자.

print(kn.predict([[25,150]]))

## 출력값
[0.]

예측값은 빙어(0)를 예측했다. 왜 이러한 결과가 나왔는지 산점도를 사용해알아보자.

 

k-최근접 이웃은 주변의 샘플 중에서 다수인 클래스로 예측하므로 주변 샘플은 마름모, 예측하기 위한 데이터는 삼각형으로 표시하자

KNeighborsClassifier 클래스의 이웃 개수인 n_neighbors의 기본값은 5이기 때문에 5개의 이웃이 반환된다.

## 주어진 샘플에서 가장 가까운 이웃을 찾아 주는 kneighbors() 사용
## distance: 이웃 샘플까지의 거리
## 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') ## 이웃 데이터
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

마름모: 이웃 데이터, 삼각형: 예측 데이터

주변 데이터가 도미가 1개, 빙어가 4개로 표시되었다. 이를 distance값으로 확인해보자.

print(distances)

## 출력값
[[ 92.00086956 130.48375378 130.73859415 138.32150953 138.39320793]]

가장 가까운 샘플까지의 거리는 92, 그 다음 샘플까지의 거리는 약 130이다. 하지만 위 산점도를 보면 92까지의 거리보다 130까지의 거리가 몇 배는 되어보인다. 이는 x축, y축의 범위가 달라서 나타나는 현상이다. x축 또한 범위를 0~1000으로 하여 다시 산점도를 그려보자.

## x축, y축의 범위를 동일하게 맞춰준 산점도
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)) ## x축의 범위를 0~1000으로
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

 

마름모: 이웃 데이터, 삼각형: 예측 데이터

 

산점도가 거의 일직선으로 나타난다. 이러한 산점도는 가까운 이웃을 찾는데 크게 영향을 미치지 못할 것이다.

위 처럼 두 특성(길이와 무게)의 값이 놓인 범위가 다른것을 두 특성의 스케일이 다르다고도 말한다. 특성 간 스케일이 다른 일은 흔하기 때문에 특성값을 일정한 기준으로 맞춰주는 데이터 전처리 과정을 거쳐야한다.

 

가장 널리 사용하는 전처리 방법 중 하나는 표준점수(z 점수)이다. 표준점수는 각 특성값이 평균에서 표준편차의 몇 배만큼 떨어져 있는지를 나타낸다. 이를 통해 실제 특성값의 크기와 상관없이 동일한 조건으로 비교할 수 있다.

np.mean(): 평균을 계산한다.

np.std(): 표준편차를 계산한다.

## 표준 점수를 사용한 전처리
mean = np.mean(train_input, axis = 0)
std = np.std(train_input, axis = 0)

원본 데이터에서 평균을 빼고 표준편차로 나누어 표준점수로 변환하자.

## 표준점수로 변환
train_scaled = (train_input - mean) / std

표준점수로 변환한 train_scaled를 가지고 다시 산점도를 그려보자.

## 표준점수로 변환한 산점도
plt.scatter(train_scaled[:,0], train_scaled[:,1])
plt.scatter(25, 150, marker = '^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

예측 데이터 혼자 왼쪽 상단에 존재한다. 이는 훈련 세트만 전처리를 진행했기 때문이다. 예측 데이터도 전처리 과정을 진행해야한다. 

여기서 중요한 점은 훈련 세트의 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()

삼각형: 예측 데이터

데이터 전처리를 진행한 데이터셋으로 k-최근접 이웃 모델을 다시 훈련하고 예측해보자.

예측데이터와 마찬가지로 테스트 세트도 훈련 세트의 mean과 std로 전처리를 진행해야한다.

## 데이터 훈련
kn.fit(train_scaled, train_target)

## 테스트 세트도 훈련 세트의 mean, std로 변환해야 한다.
test_scaled = (test_input - mean) / std

## 예측
print(kn.predict([new]))

## 출력값
[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()

마름모: 이웃 데이터, 삼각형: 예측 데이터

가장 가까운 샘플은 모두 도미가 된 것을 알 수 있다. 이제 특성값의 스케일에 민감하지 않고 안정적인 예측을 할 수 있다.

 

정리

1. 데이터가 클수록 파이썬 리스트는 비효율적이므로 넘파이 배열을 사용하자.

2. train_test_split() 함수를 사용해 넘파일 배열을 섞지 않고도 데이터를 분류할 수 있다.

3. 특성의 스케일이 다르면 머신러닝 알고리즘이 잘 작동하지 않으므로 전처리 과정으로 특성값을 일정한 기준으로 맞춰준다.

4. 훈련 세트의 mean, std 값으로 예측 데이터, 테스트 세트도 전처리해야한다.