[2일차] 데이터 다루기

Hyun·2023년 2월 20일
0

머신러닝/딥러닝

목록 보기
2/5

머신러닝 알고리즘

  • 지도 학습
    입력(데이터)과 타깃(정답)으로 이뤄진 훈련 데이터가 필요. 알고리즘이 정답을 맞히는 것을 학습 ex) 도미인지 빙어인지 구분

  • 비지도 학습
    타깃 없이 입력 데이터만 사용, 정답을 사용하지 않으므로 무언가를 맞힐 수 없다. 대신 데이터를 잘 파악하거나 변형하는데 도움을 줌

  • 강화 학습
    타깃이 아니라 알고리즘이 행동한 결과로 얻은 보상을 사용해 학습

훈련 세트와 테스트 세트

1일차에서 질문한 것과 같이, 도미와 빙어의 데이터와 타깃을 주고 훈련한 다음, 같은 데이터로 테스트 한다면 모두 맞히는 것이 당연하다. 따라서 훈련에 사용한 데이터로 모델을 평가하는 것은 적절하지 않다.

연습문제와 시험문제가 다르듯이 머신러닝 알고리즘의 성능을 제대로 평가하려면 훈련 데이터와 평가에 사용할 데이터가 각각 달라야 한다.

훈련 세트: 훈련에 사용되는 데이터
테스트 세트: 평가에 사용하는 데이터

먼저 도미 35마리, 빙어 14마리의 데이터를 합친 후, 앞에서부터 35개의 데이터를 훈련 세트로 사용해보자.

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마리의 데이터

fish_data= [[l,w] for l,w in zip(fish_length, fish_weight)]
fish_target = [1]*35 + [0]*14

from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier()
#모델 객체 생성성

---------------------------------------
train_input = fish_data[:35] #훈련 세트로 0부터 34번째 인덱스까지 사용
train_target = fish_target[:35] #훈련 세트로 타깃값 중 0부터 34번째 인덱스까지 사용
test_input = fish_data[35:] # 테스트 세트로 입력값 중 35번째부터 마지막 인덱스까지 사용
test_target = fish_target[35:] # 테스트 세트로 타깃값 중 35번째부터 마지막 인덱스까지 사용

kn.fit(train_input, train_target)
kn.score(test_input, test_target) 

출력: 
0.0

위 경우 빙어없이 도미만을 가지고 모델을 훈련했기 때문에 빙어를 올바르게 분류할 수 없다. 따라서 훈련 세트에 도미와 빙어를 적절히 섞어줘야 한다.
훈련 세트와 테스트 세트에 샘플이 골고루 섞여 있지 않으면 샘플링이 한쪽으로 치우쳤다는 의미로 샘플링 편향(sampling bias)라고 부른다.

샘플링 편향문제를 해결하기 위해 0부터 48까지의 index 넘파이 배열을 생성한 후, 무작위로 섞는다. 이후 앞에서부터 35개의 데이터를 이용해 훈련 세트로, 나머지 14개를 이용해 테스트 세트로 선택한다

import numpy as np

input_arr = np.array(fish_data)
target_arr = np.array(fish_target)

#print(input_arr) #넘파이 배열은 행과 열을 가지런히 출력한다.
#print(input_arr.shape) # 배열의 크기를 알려주는 속성

np.random.seed(42)
index = np.arange(49) #0부터 48까지의 배열을 생성
np.random.shuffle(index) #주어진 배열을 무작위로 섞는다

train_input = input_arr[index[:35]]
train_target = target_arr[index[:35]]

test_input = input_arr[index[35:]]
test_target = target_arr[index[35:]]
#무작위로 섞인 index 배열을 이용해 훈련, 테스트 세트를 선택

선택된 훈련, 테스트 세트를 산점도 그래프로 나타내면 아래와 같다.

import matplotlib.pyplot as plt

plt.scatter(train_input[:,0], train_input[:,1])
plt.scatter(test_input[:,0],test_input[:,1])
plt.xlabel('length')
plt.ylabel('weight')
plt.show()


파란색이 훈련 세트, 주황색이 테스트 세트로, 도미와 빙어가 각각 적절히 섞여있다.

kn.fit(train_input, train_target)
kn.score(test_input, test_target)

출력:
1.0
kn.predict(test_input)
출력:
array([0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0])

test_target
출력:
array([0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0])

적절히 섞인 훈련 세트로 모델을 훈련시킨 후, 테스트 세트로 평가했을때 예측 결과와 실제 타깃이 동일한 것을 확인할 수 있다.

그런데 문제가 발생한다. 길이가 25cm이고 무게가 150g인 도미를 모델이 빙어라고 예측하기 때문이다.

먼저 넘파이를 활용해 데이터를 준비해보자. 앞 시간에서는 2차 리스트를 만들기 위해 파이썬 리스트를 순회하면서 원소를 하나씩 꺼내 생선 하나의 길이와 무게를 리스트 안의 리스트로 직접 구성했었다.

fish_data = [[l,w] for l,w in zip(length, weight)]
fish_target = [1] * 35 + [0] * 14

하지만 넘파이를 이용하면 훨씬 간편하게 만들 수 있다. 넘파이의 column_stack() 함수는 전달받은 리스트를 일렬로 세운 다음, 차례대로 나란히 연결한다. concatenate() 함수는 첫 번째 차원을 따라 배열을 연결하는 기능을 한다. p90 참고
*안전한 튜플로 매개변수 값을 사용한다.

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

#예) np.column_stack(([1,2,3], [4,5,6])) #안전한 튜플로 매개변수 값을 사용

fish_data = np.column_stack((fish_length, fish_weight))
fish_target= np.concatenate((np.ones(35), np.zeros(14))) #동일하게 튜플로 매개변수 전달, 동일 차원으로 연결(column_stack과 비교됨)

앞에서는 넘파이 배열의 인덱스를 직접 섞어서 훈련 세트와 테스트 세트로 나누었다.

np.random.seed(42)
index = np.arange(49) #0부터 48까지의 배열을 생성
np.random.shuffle(index) #주어진 배열을 무작위로 섞는다

train_input = input_arr[index[:35]]
train_target = target_arr[index[:35]]

test_input = input_arr[index[35:]]
test_target = target_arr[index[35:]]

하지만 이 방법은 번거롭다는 단점이 있다. 더 세련된 방법으로 사이킷런이 제공하는 유틸리티 도구인 train_test_split() 함수를 사용한다. 이 함수는 전달되는 리스트나 배열을 알아서 섞은 후, 비율에 맞게 훈련 세트와 테스트 세트로 나누어 준다. 기본 비율은 25%이다.

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) #무작위로 섞어준 후, 기본적으로 25%를 테스트 케이스로 떼어낸다.

print(train_input.shape, test_input.shape) #2차 리스트
print(train_target.shape, test_target.shape) #1차 리스트
print(test_target)

출력:
(36, 2) (13, 2)
(36,) (13,)
[1. 0. 0. 0. 1. 1. 1. 1. 1. 1. 1. 1. 1.]

13개의 테스트 세트중에 10개가 도미(1)이고, 3개가 빙어(0)이다. 잘 섞인 것 같지만 3.3:1 의 비율로, 빙어의 비율이 조금 모자라다. 원래 도미와 빙어의 개수의 비율이 2.5:1 이므로 샘플링 편향이 여기서도 조금 나타난 것을 볼 수 있다.

이처럼 무작위로 데이터를 나누었을 때 샘플이 골고루 섞이지 않을 수 있다. 훈련 세트와 테스트 세트에 샘플의 클래스 비율이 일정하지 않다면 모델이 일부 샘플을 올바르게 학습할 수 없을 것이다.

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)

출력:
[0. 0. 1. 0. 1. 0. 1. 1. 1. 1. 1. 1. 1.]

빙어가 하나 늘어 테스트 세트의 비율이 2.25:1 이 되었다. 위 예제는 데이터가 작아 전체 훈련 데이터의 비율과 동일하게 맞출 수 없지만 꽤 비슷한 비율이 되었다.

데이터가 모두 준비되었으므로, 도미를 빙어로 예측하는 문제를 확인해보자.

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

print(kn.predict([[25,150]])) #특정 도미 데이터에 대해 예측이 실패한 경우

출력:
[0.]

샘플링 편향문제를 해결했음에도 불구하고, 해당 도미 데이터를 빙어로 예측하고 있다. 산점도를 살펴보았을때도 도미에 가깝다. 하지만 막상 주어진 도미 데이터와 가장 근접한 5개의 데이터를 살펴보면 4개가 빙어 데이터인 것을 확인할 수 있다.

*kneighbors() 메서드는 주어진 샘플에서 가장 가까운 이웃을 찾아 주는 기능을 한다. 이 메서드는 이웃까지의 거리와 이웃 샘플의 인덱스를 반환한다. 이웃 갯수인 n_neighbors의 기본값은 5이다.

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

distance, indexes = kn.kneighbors([[25, 150]])
plt.scatter(train_input[indexes,0], train_input[indexes,1], marker='D')

plt.xlabel('length')
plt.ylabel('weight')
plt.show()

이는 x축의 범위가 좁고(10~40), y축의 범위가 넓어(0~1000) y축으로 조금만 멀어져도 거리가 아주 큰 값으로 계산되기 때문이다.따라서 오른쪽 위의 도미 샘플이 이웃으로 선택되지 못한 것이다. 눈으로 명확하게 확인하기 위해 x축의 범위를 동일하게 0~1000으로 맞춰보면 아래와 같다.

plt.scatter(train_input[:, 0],train_input[:,1])
plt.scatter(25,150, marker='^') #marker 매개변수는 모양을 지정

distance, indexes = kn.kneighbors([[25, 150]])
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축)은 가장 가까운 이웃을 찾는데 크게 영향을 미치지 못하고 오로지 생선의 무게(y축)만 고려 대상이된다. 이는 두 특성의 스케일(scale)이 다르기 때문인데, 이는 매우 흔하다. 따라서 두 특성값을 일정한 기준으로 맞춰 주어야 한다. 이 작업을 데이터 전처리(data preprocessing)이라고 부른다.

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

표준 점수는 기존 값에 평균을 빼고 표준편차를 나누어 주면 된다. 이를 위해 넘파이의 mean, std 함수를 사용한다. 특성마다 값의 스케일이 다르므로 평균과 표준편차는 각 특성별로 계산해야 한다. axis를 0으로 설정하면 행을 따라 각 열의 통계 값을 계산한다. p100 참고.

mean = np.mean(train_input, axis = 0) #평균
std = np.std(train_input, axis = 0) #표준편차

print(mean, std)

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

train_scaled = (train_input - mean) / std #표준점수 구하기, 브로드캐스팅 기능 사용됨

위 식은 train_input의 모든 행을 자신의 열에 해당하는 mean의 평균값으로 빼준다. 그다음 std에 있는 두 표준편차를 각각 해당하는 열의 모든 행에 적용해 나눠준다. 이런 넘파이 기능을 브로드캐스팅(broadcasting)이라고 부른다.

테스트할 새로운 샘플 또한 동일한 기준으로 변환시켜야 한다.
1) 동일한 기준으로 변환시키지 않았을때, 2) 동일한 기준으로 변환시켰을 때

1)
plt.scatter(25,150,marker='^')
2)
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()

1) 동일한 기준으로 변환시키지 않았을때

2) 동일한 기준으로 변환시켰을 때

표준점수로 나타내진 새로운 데이터셋으로 k-최근접 이웃 모델을 다시 훈련시켜보자. 테스트 세트는 훈련 세트의 기준과 동일하게 변환되어야 한다.

kn.fit(train_scaled, train_target) #새로운 데이터셋으로 다시 훈련시킴
test_scaled = (test_input - mean) / std #테스트 세트를 평가하기 위해 훈련 세트의 기준으로 테스트 세트를 변환시킨다.
kn.score(test_scaled, test_target)

출력:
[1.]

모든 테스트 세트의 샘플을 완벽하게 분류한 것을 확인할 수 있다. 이제 새로운 샘플 new 에 대해 모델의 예측을 출력해보자.

print(kn.predict([new]))

출력:
[1.]

올바르게 도미(1)로 예측한 것을 확인할 수 있다.
다시 가장 가까운 데이터 5개를 표현하면 아래와 같다.

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

정리) 스케일이 다른 특성 처리

대부분의 특성들의 스케일은 서로 다르다. 특성의 스케일이 다르면 머신러닝 알고리즘이 잘 작동하지 않는다. 이를 위해 특성을 표준점수로 변환할 수 있다. 대부분의 경우 표준점수로 충분하고, 가장 널리 사용되는 방법이기도 하다.

데이터를 전처리할때는 훈련 세트를 변환한 방식 그대로 테스트 세트를 변환해야 한다. 그렇지 않으면 특성값이 엉뚱하게 변환될 것이고 훈련세트로 훈련한 모델이 제대로 동작하지 않을 것이다.

profile
better than yesterday

0개의 댓글