본문 바로가기
Python/ML

[머신러닝] 데이터 전처리(생선 분류 문제3)

by JooRi 2023. 1. 20.
728x90
SMALL
* 교재: 혼자 공부하는 머신러닝+딥러닝 (hanbit.co.kr)

 

* 문제: 데이터 전처리로 특성의 스케일을 조정한다.

새로운 샘플로 스케일이 다른 두 특성의 문제점을 경험해 보고 이를 해결하기 위해 데이터 전처리 작업을 거쳐 도미와 빙어를 구분하는 머신러닝 프로그램을 만든다.

 

* 문제 해결 과정

1. 생선 데이터 준비하기

2. 넘파이로 2차원 리스트 만들기

3. 훈련 세트와 테스트 세트 만들기

  1) train_test_split() 함수 임포트하기

  2) fish_data와 fish_target 나누기

4. 모델 훈련 및 평가하기

5. 데이터 전처리

6. 전처리 데이터로 모델 훈련 및 평가하기

 

 

1. 생선 데이터 준비하기

도미 35마리와 빙어 14마리를 합친 총 49마리의 생선의 길이와 무게 리스트를 준비해 보자.

 

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]

 

하나의 생선 데이터를 샘플이라고 부른다.

도미 35마리, 빙어 14마리가 있으므로 전체 생선 데이터는 총 49개의 샘플이 있다.

 

 

2. 넘파이로 2차원 리스트 만들기

넘파이를 임포트 한 후 length와 weight 리스트를 2차원 리스트로 만들어보자.

 

import numpy as np
fish_data = np.column_stack((fish_length, fish_weight))

 

np.column_stack() 함수는 리스트를 일렬로 세운 다음 차례대로 나란히 연결한다.

 

 

길이 리스트와 무게 리스트가 잘 연결되었는지 처음 5개의 데이터를 출력해 보자.

 

print(fish_data[:5])

 

[[ 25.4 242. ]

 [ 26.3 290. ]

 [ 26.5 340. ]

 [ 29. 363. ]

 [ 29. 430. ]]

 

 

컴퓨터가 문자를 이해할 수 있게 도미와 빙어를 숫자 1과 0으로 표현해 보자.

 

fish_target = np.concatenate((np.ones(35), np.zeros(14)))
print(fish_taret)
# 출력: [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.ones()와 np.zeros() 함수는 각각 원하는 개수의 1과 0을 채운 배열을 만든다.

np.ones()와 np.zeros() 함수를 사용하여 1이 35개인 배열(도미 35마리)과 0이 14개(빙어 14마리)인 배열을 만들었다. 

np.concatenate() 함수는 첫 번째 차원을 따라 배열을 연결한다. np.concatenate() 함수를 사용하여 두 배열을 연결하였다.

 

 

3. 훈련 세트와 테스트 세트 만들기

 

 

1) train_test_split() 함수 임포트하기

 

from sklearn.model_selection import train_test_split

 

사이킷런은 전달되는 리스트나 배열을 비율에 맞게 훈련 세트와 테스트 세트로 나누어 주는 train_test_split() 함수를 제공한다.

train_test_split() 함수는 사이킷런의 model_selection 모듈 아래에 있다.

 

 

2) fish_data와 fish_target 나누기

 

train_input, test_input, train_target, test_target = train_test_split(fish_data, fish_target, random_state=42)

 

train_test_split() 함수는 자체적으로 랜덤 시드를 지정할 수 있는 random_state 매개변수가 있다. 이를 사용하여 훈련 세트와 테스트 세트를 나누었다.

랜덤 시드(radom_state)는 42로 지정하였다.

 

fish_data와 fish_target 이 2개의 배열을 전달했으므로 각각 2개씩 나뉘어 총 4개의 배열이 반환된다.

처음 2개는 입력 데이터(train_input, test_input), 나머지 2개는 타깃 데이터(train_target, test_target)이다.

 

 

잘 나누었는지 입력 데이터의 크기를 출력해 보자.

 

print(train_input.shape, test_input.shape)  # 출력: (36, 2) (13, 2)
print(train_target.shape, test_target.shape)  # 출력: (36,) (13,)

 

넘파이 배열의 shape 속성으로 입력 데이터의 크기를 출력해 보았다.

훈련 데이터와 테스트 데이터는 각각 36개와 13개로 나누었다.

입력 데이터는 2개의 열이 있는 2차원 배열이고 타깃 데이터는 1차원 배열이다.

 

 

도미와 빙어가 잘 섞였는지 테스트 데이터를 출력해 보자.

 

print(test_target)  # 출력: [1. 0. 0. 0. 1. 1. 1. 1. 1. 1. 1. 1. 1.]

 

13개의 테스트 세트 중에 10개가 도미이고(1) 3개가 빙어(0)이다. 빙어의 비율이 조금 모자라다.

원래 도미와 빙어의 개수가 35개와 14개이므로 두 생선의 비율은 2.5:1이다. 하지만 이 테스트 세트에서는 3.3:1의 비율이 나왔다. 샘플링 편향이 조금 나타난 것이다.

 

 

샘플링 편향을 해결하기 위해 데이터를 다시 나누어보자.

 

train_input, test_input, train_target, test_target = train_test_split(fish_data, fish_target, stratify=fish_target, random_state=42)
print(test_target)  # 출력: [0. 0. 1. 0. 1. 0. 1. 1. 1. 1. 1. 1. 1.]

 

stratify 매개변수에 타깃 데이터를 전달하여 클래스 비율에 맞게 데이터를 다시 나누었다.

빙어가 하나 늘어서 0이 4개가 출력되었다.

 

 

4. 모델 훈련 및 평가하기

이제 데이터 준비가 끝났다. 모델을 훈련하고 평가해 보자.

 

from sklearn.neighbors import KNeighborsClassifier  
kn = KNeighborsClassifier()
kn.fit(train_input, train_target)  # 훈련
kn.score(test_input, test_target)  # 평가
# 출력: 1.0

 

사이킷런 k-최근접 이웃 분류 모델을 만드는 클래스인 KNeighborsClassifier를 임포트 하고 KNeighborsClassifier 클래스의 kn 객체를 만든 후 fit()을 호출해 모델을 훈련하였다.

score()를 호출해 모델의 성능을 평가했더니 1.0이 출력되었다. 도미와 빙어를 올바르게 분류하였다.

 

 

도미 데이터를 넣고 결과를 확인해 보자.

 

print(kn.predict([[25, 150]]))  # 출력: [0.]

 

도미는 1인데 빙어로 예측하였다...

 

 

왜 도미를 빙어로 예측하였는지 산점도를 그려 확인해 보자.

 

import matplotlib.pyplot as plt
plt.scatter(train_input[:,0], train_input[:,1])
plt.scatter(25, 150, marker='^')  # marker 매개변수는 모양을 지정한다.
plt.xlable('length')
plt.ylable('weight')
plt.show()

 

 

새로운 샘플(도미 데이터)을 구분하기 쉽게 marker 매개변수를 '^'으로 지정하여 삼각형으로 표현하였다.

삼각형은 오른쪽 위로 뻗어있는 도미 데이터에 가까운데 빙어 데이터라고 예측하였다...

 

 

주어진 샘플 [25, 150]에서 가장 가까운 이웃을 찾아보자.

 

distances, indexes = kn.kneighbors([[25, 150]])

 

kneighbors() 메서드는 k-최근접 이웃 객체의 메서드로, 입력한 데이터에 가장 가까운 이웃을 찾아 거리와 이웃 샘플의 인덱스를 반환한다.

 

 

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.xlable('length')
plt.ylable('weight')
plt.show()

 

 

indexes 배열을 사용하여 이웃 샘플을 마름모(marker='D')로 표현하였다.

삼각형 샘플에 가장 가까운 5개의 샘플이 초록색 다이아몬드로 나타났다. 예측 결과와 마찬가지로 가장 가까운 이웃에 도미가 없다.

그냥 눈으로 그래프를 볼 때 삼각형 샘플과 가까운 것은 도미(파란색 점)인데 빙어로 예측한 이유는 바로 x축(10~40)이 y축(0~1000)보다 범위가 좁기 때문이다. 즉 y축으로 조금만 멀어져도 거리가 아주 큰 값으로 계산된다. 따라서 오른쪽 위의 도미 샘플이 이웃으로 선택되지 못한 것이다.       

 

 

타깃 테이터로 확인해 보자.

 

print(train_target[indexes])  # 출력: [[1. 0. 0. 0. 0.]]

 

길이 25cm, 무게 150g인 생선에 가장 가까운 이웃에는 빙어가 압도적으로 많다.

 

 

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))
plt.xlable('length')
plt.ylable('weight')
plt.show()

 

 

xlim() 함수는 맷플롯립에서 x축의 범위를 지정하기 위해 사용한다.

x축과 y축의 범위를 0~1000으로 동일하게 하였더니 모든 데이터가 수직으로 늘어선 형태가 되었다.

따라서  x축(생선의 길이)은 가장 가까운 이웃을 찾는 데 영향을 미치지 못한다. 오로지 y축(생선의 무게)만 고려 대상이 된다. 즉 두 특성(길이와 무게)의 값이 놓인 범위가 매우 다르다. 이를 '두 특성의 스케일이 다르다'라고 한다.

이를 해결하기 위해 데이터 전처리 작업을 해보자. 

 

 

5. 데이터 전처리

데이터 전처리는 머신러닝 모델에 훈련 데이터를 주입하기 전에 가공하는 단계로, 특성값을 일정한 기준으로 맞춰준다.

 

 

가장 널리 사용하는 전처리 방법 중 하나인 표준점수를 사용해 보자.

표준점수는 훈련 세트의 스케일을 바꾸는 대표적인 방법으로, 각 특성값이 평균에서 표준편차의 몇 배만큼 떨어져 있는지를 나타낸다.

표준 점수를 얻으려면 원본 데이터에서 특성의 평균을 빼고 표준편차로 나누면 된다.

 

mean = np.mean(train_input, axis=0)  # mean은 평균
std = np.std(train_input, axis=0)  # std는 표준편차
print(mean, std)  # 출력: [ 27.29722222 454.09722222] [  9.98244253 323.29893931]

 

np.mean() 함수는 평균을 계산하고, np.std() 함수는 표준편차를 계산한다. 이 두 함수는 모두 넘파이에서 제공한다.

train_input은 (36, 2) 크기의 배열이다.

특성마다 값의 스케일이 다르므로 평균과 표준편차는 각 특성별로 계산해야 한다. 이를 위해 axis=0으로 지정하면 행을 따라 각 열의 통계값을 계산한다.

 

 

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

 

train_scaled = (train_input - mean) / std

 

넘파이는 train_input의 모든 행에서 mean에 있는 두 평균값을 빼준 후 std에 있는 두 표준편차를 다시 모든 행에 적용한다.

이런 넘파이 기능을 브로드캐스팅이라고 한다.

 

 

6. 전처리 데이터로 모델 훈련 및 평가하기

 

 

위에서 표준점수로 변환한 train_scaled를 만들었다. 이 데이터와 샘플을 다시 산점도로 그려보자.

 

plt.scatter(train_scaled[:,0], train_scaled[:,1])
plt.scatter(25, 150, marker='^')  
plt.xlable('length')
plt.ylable('weight')
plt.show()

 

 

샘플 [25, 150]을 동일한 비율로 변환하지 않았더니 예상과 다른 그래프가 나왔다.

 

 

샘플 [25, 150]을 동일한 기준으로 변환하고 다시 산점도를 그려보자.

 

new = ([25, 150] - mean) / std
plt.scatter(train_scaled[:,0], train_scaled[:,1])
plt.scatter(new[0], new[1], marker='^')  
plt.xlable('length')
plt.ylable('weight')
plt.show()

 

 

x축과 y축의 범위가 -1.5~1.5로 바뀌었다. 즉 훈련 데이터의 두 특성이 비슷한 범위를 차지하였다.

 

 

이제 다시 모델을 훈련하고 평가해 보자.

 

kn.fit(train_scaled, train_target)
test_scaled = (test_input - mean) / std
kn.score(test_scaled, test_target)  # 출력: 1.0

 

훈련을 마친 후 테스트 세트도 훈련 세트의 평균과 표준편차로 변환하여 모델을 평가하였더니 1.0이 출력되었다.

 

 

모델의 예측을 다시 출력해 보자.

 

print(kn.predict([new]))  # 출력: [1.]

 

드디어 도미로 예측하였다.

 

 

마지막으로 이 샘플의 k-최근접 이웃을 구한 다음 산점도로 그려보자.

 

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.xlable('length')
plt.ylable('weight')
plt.show()

 

 

삼각형 샘플에서 가장 가까운 샘플은 모두 도미이다.

특성값의 스케일에 민감하지 않고 안정적인 예측을 할 수 있는 모델을 만들었다!

728x90
LIST

댓글